目錄
#簡單例子
require會先執行目標檔
requiremodule.exports不是黑魔法
模組類型和載入順序
模組類型
載入順序
載入資料夾
支援的檔案類型
手寫require
Module
require方法
MyModule._load
MyModule._resolveFilename
MyModule.prototype.load
加载js文件: MyModule._extensions['.js']
编译执行js文件:MyModule.prototype._compile
加载json文件: MyModule._extensions['.json']
exportsmodule.exports的区别
循环引用
总结
参考资料
首頁 web前端 js教程 深入淺析Node.js的模組載入機制

深入淺析Node.js的模組載入機制

Sep 02, 2020 am 10:34 AM
node.js

深入淺析Node.js的模組載入機制

模組是Node.js裡面一個很基本也很重要的概念,各種原生類別庫是透過模組提供的,第三方函式庫也是透過模組進行管理和引用的。本文會從基本的模組原理出發,到最後我們會利用這個原理,自己實作一個簡單的模組載入機制,也就是自己實作一個require。        

Node 使用 JavaScript 與 commonjs 模組,並將 npm/yarn 為其套件管理器。

【影片教學推薦:node js教學 】

#簡單例子

老規矩,講原理前我們先來一個簡單的例子,從這個例子入手一步一步深入原理。 Node.js裡面如果要導出某個內容,需要使用module.exports,使用module.exports幾乎可以導出任意類型的JS對象,包括字串,函數,對象,數組等等。我們先來建立一個a.js導出一個最簡單的hello world:

// a.js 
module.exports = "hello world";
登入後複製

然後再來一個b.js匯出一個函數:

// b.js
function add(a, b) {
  return a + b;
}

module.exports = add;
登入後複製

然後在index.js裡面使用他們,也就是require他們,require函數傳回的結果就是對應檔案 module.exports的值:

// index.js
const a = require('./a.js');
const add = require('./b.js');

console.log(a);      // "hello world"
console.log(add(1, 2));    // b导出的是一个加法函数,可以直接使用,这行结果是3
登入後複製

require會先執行目標檔

當我們require某個模組時,並不是只拿他的module.exports,而是會從頭開始運行這個文件,module.exports = XXX其實也只是其中一行程式碼,我們後面會講到,這行程式碼的效果其實就是修改模組裡面的exports屬性。例如我們再來一個c.js

// c.js
let c = 1;

c = c + 1;

module.exports = c;

c = 6;
登入後複製

c.js裡面我們導出了一個c,這個c經過了幾步計算,當運行到module.exports = c;這行時c的值為2,所以我們requirec.js的值就是2,後面將c的值改為了6不影響前面的這行程式碼:

const c = require('./c.js');

console.log(c);  // c的值是2
登入後複製

前面c.js的變數c是一個基本資料型,所以後面的c = 6;不會影響前面的 module.exports,那他如果是引用型呢?我們直接來試試看:

// d.js
let d = {
  num: 1
};

d.num++;

module.exports = d;

d.num = 6;
登入後複製

然後在index.js裡面require他:

const d = require('./d.js');

console.log(d);     // { num: 6 }
登入後複製

我們發現在module. exports後面給d.num賦值仍然生效了,因為d是一個對象,是一個引用類型,我們可以透過這個引用來修改他的值。其實對於引用型來說,不只在module.exports後面可以修改他的值,在模組外面也可以修改,例如index.js裡面就可以直接改:

const d = require('./d.js');

d.num = 7;
console.log(d);     // { num: 7 }
登入後複製

requiremodule.exports不是黑魔法

我們透過前面的例子可以看出來,requiremodule.exports幹的事情並不複雜,我們先假設有一個全域物件{},初始情況下是空的,當你require某個文件時,就將這個檔案拿出來執行,如果這個檔案裡面存在module.exports,當執行到這行程式碼時將module.exports的值加入這個對象,鍵為對應的檔名,最終這個物件就長這樣:

{
  "a.js": "hello world",
  "b.js": function add(){},
  "c.js": 2,
  "d.js": { num: 2 }
}
登入後複製

當你再一次require某個檔案時,如果這個物件裡面有對應的值,就直接回傳給你,如果沒有就重複前面的步驟,執行目標文件,然後將它的module.exports加入這個全域對象,並傳回給呼叫者。這個全域物件其實就是我們常聽到的快取。 所以requiremodule.exports並沒有什麼黑魔法,就只是運行並取得目標檔案的值,然後加入緩存,用的時候拿出來就行。 再看看這個對象,因為d.js是一個引用類型,所以你在任何地方獲取了這個引用都可以更改他的值,如果不希望自己模組的值被更改,需要自己寫模組時處理,例如使用Object.freeze()Object.defineProperty()之類的方法。

模組類型和載入順序

這一節的內容都是一些概念,比較枯燥,但是也是我們需要了解的。

模組類型

Node.js的模組有好幾種類型,前面我們使用的其實都是檔案模組,總結下來,主要有這兩種類型:

  1. 內建模組:就是Node.js原生提供的功能,例如fshttp等等,這些模組在Node .js進程起來時就載入了。
  2. 檔案模組:我們前面寫的幾個模組,還有第三方模組,也就是node_modules下面的模組都是檔案模組。

載入順序

載入順序是指當我們require(X)時,我們應該要按照什麼順序去哪裡找X,在官方文檔上有詳細偽代碼,總結下來大概是這麼個順序:

  1. #優先載入內建模組,即使有同名文件,也會優先使用內建模組。
  2. 不是內建模組,先去快取找。
  3. 快取沒有就去找對應路徑的檔案。
  4. 不存在對應的文件,就將這個路徑當作資料夾載入。
  5. 對應的檔案和資料夾都找不到就去node_modules下面找。
  6. 還找不到就報錯了。

載入資料夾

前面提到找不到檔案就找資料夾,但是不可能將整個資料夾都載入進來,載入資料夾的時候也是有一個載入順序的:

  1. 先看看這個資料夾下面有沒有package.json,如果有就找裡面的main字段,main字段有值就載入對應的檔案。所以如果大家在看一些第三方函式庫原始碼時找不到入口就看看他package.json裡面的main欄位吧,像是jquerymain字段就是這樣:"main": "dist/jquery.js"
  2. 如果沒有package.jsonpackage.json裡面沒有main就找index。檔。
  3. 如果這兩步都找不到就報錯了。

支援的檔案類型

require主要支援三種檔案類型:

  1. #.js.js文件是我們最常用的文件類型,載入的時候會先運行整個JS文件,然後將前面說的module.exports作為require的回傳值。
  2. .json.json文件是一個普通的文字文件,直接用JSON.parse將其轉化為物件回傳就行。
  3. .node.node檔案是C 編譯後的二進位文件,純前端一般很少接觸這個類型。

手寫require

前面其實我們已經將原理講的七七八八了,下面來到我們的重頭戲,自己實作一個require。實作require其實就是實作整個Node.js的模組載入機制,我們再來理一下需要解決的問題:

  1. 透過傳入的路徑名找到對應的文件。
  2. 執行找到的文件,同時要注入modulerequire這些方法和屬性,以便模組檔案使用。
  3. 返回模組的module.exports

#本文的手寫程式碼全部參考Node.js官方原始碼,函數名稱和變數名稱盡量保持一致,其實就是精簡版的源碼,大家可以對照著看,寫到具體方法時我也會貼上對應的源碼位址。整體的程式碼都在這個檔案裡面:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.Jjs

#Node.js模組載入的功能全部在

Module

類別裡面,整個程式碼使用物件導向的思想,

如果你對JS的物件導向還不是很熟悉可以先看看這篇文章Module類別的建構子也不複雜,主要是一些值的初始化,為了跟官方Module名字區分開,我們自己的類別命名為MyModule

function MyModule(id = '') {
  this.id = id;       // 这个id其实就是我们require的路径
  this.path = path.dirname(id);     // path是Node.js内置模块,用它来获取传入参数对应的文件夹路径
  this.exports = {};        // 导出的东西放这里,初始化为空对象
  this.filename = null;     // 模块对应的文件名
  this.loaded = false;      // loaded用来标识当前模块是否已经加载
}
登入後複製

require方法

我们一直用的require其实是Module类的一个实例方法,内容很简单,先做一些参数检查,然后调用Module._load方法,源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L970。精简版的代码如下:

MyModule.prototype.require = function (id) {
  return Module._load(id);
}
登入後複製

MyModule._load

MyModule._load是一个静态方法,这才是require方法的真正主体,他干的事情其实是:

  1. 先检查请求的模块在缓存中是否已经存在了,如果存在了直接返回缓存模块的exports
  2. 如果不在缓存中,就new一个Module实例,用这个实例加载对应的模块,并返回模块的exports

我们自己来实现下这两个需求,缓存直接放在Module._cache这个静态变量上,这个变量官方初始化使用的是Object.create(null),这样可以使创建出来的原型指向null,我们也这样做吧:

MyModule._cache = Object.create(null);

MyModule._load = function (request) {    // request是我们传入的路劲参数
  const filename = MyModule._resolveFilename(request);

  // 先检查缓存,如果缓存存在且已经加载,直接返回缓存
  const cachedModule = MyModule._cache[filename];
  if (cachedModule !== undefined) {
    return cachedModule.exports;
  }

  // 如果缓存不存在,我们就加载这个模块
  // 加载前先new一个MyModule实例,然后调用实例方法load来加载
  // 加载完成直接返回module.exports
  const module = new MyModule(filename);
  
  // load之前就将这个模块缓存下来,这样如果有循环引用就会拿到这个缓存,但是这个缓存里面的exports可能还没有或者不完整
  MyModule._cache[filename] = module;
  
  module.load(filename);
  
  return module.exports;
}
登入後複製

上述代码对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L735

可以看到上述源码还调用了两个方法:MyModule._resolveFilenameMyModule.prototype.load,下面我们来实现下这两个方法。

MyModule._resolveFilename

MyModule._resolveFilename从名字就可以看出来,这个方法是通过用户传入的require参数来解析到真正的文件地址的,源码中这个方法比较复杂,因为按照前面讲的,他要支持多种参数:内置模块,相对路径,绝对路径,文件夹和第三方模块等等,如果是文件夹或者第三方模块还要解析里面的package.jsonindex.js。我们这里主要讲原理,所以我们就只实现通过相对路径和绝对路径来查找文件,并支持自动添加jsjson两种后缀名:

MyModule._resolveFilename = function (request) {
  const filename = path.resolve(request);   // 获取传入参数对应的绝对路径
  const extname = path.extname(request);    // 获取文件后缀名

  // 如果没有文件后缀名,尝试添加.js和.json
  if (!extname) {
    const exts = Object.keys(MyModule._extensions);
    for (let i = 0; i < exts.length; i++) {
      const currentPath = `${filename}${exts[i]}`;

      // 如果拼接后的文件存在,返回拼接的路径
      if (fs.existsSync(currentPath)) {
        return currentPath;
      }
    }
  }

  return filename;
}
登入後複製

上述源码中我们还用到了一个静态变量MyModule._extensions,这个变量是用来存各种文件对应的处理方法的,我们后面会实现他。

MyModule._resolveFilename对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L822

MyModule.prototype.load

MyModule.prototype.load是一个实例方法,这个方法就是真正用来加载模块的方法,这其实也是不同类型文件加载的一个入口,不同类型的文件会对应MyModule._extensions里面的一个方法:

MyModule.prototype.load = function (filename) {
  // 获取文件后缀名
  const extname = path.extname(filename);

  // 调用后缀名对应的处理函数来处理
  MyModule._extensions[extname](this, filename);

  this.loaded = true;
}
登入後複製

注意这段代码里面的this指向的是module实例,因为他是一个实例方法。对应的源码看这里: https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L942

加载js文件: MyModule._extensions['.js']

前面我们说过不同文件类型的处理方法都挂载在MyModule._extensions上面的,我们先来实现.js类型文件的加载:

MyModule._extensions[&#39;.js&#39;] = function (module, filename) {
  const content = fs.readFileSync(filename, &#39;utf8&#39;);
  module._compile(content, filename);
}
登入後複製

可以看到js的加载方法很简单,只是把文件内容读出来,然后调了另外一个实例方法_compile来执行他。对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L1098

编译执行js文件:MyModule.prototype._compile

MyModule.prototype._compile是加载JS文件的核心所在,也是我们最常使用的方法,这个方法需要将目标文件拿出来执行一遍,执行之前需要将它整个代码包裹一层,以便注入exports, require, module, __dirname, __filename,这也是我们能在JS文件里面直接使用这几个变量的原因。要实现这种注入也不难,假如我们require的文件是一个简单的Hello World,长这样:

module.exports = "hello world";
登入後複製

那我们怎么来给他注入module这个变量呢?答案是执行的时候在他外面再加一层函数,使他变成这样:

function (module) { // 注入module变量,其实几个变量同理
  module.exports = "hello world";
}
登入後複製

所以我们如果将文件内容作为一个字符串的话,为了让他能够变成上面这样,我们需要再给他拼接上开头和结尾,我们直接将开头和结尾放在一个数组里面:

MyModule.wrapper = [
  &#39;(function (exports, require, module, __filename, __dirname) { &#39;,
  &#39;\n});&#39;
];
登入後複製

注意我们拼接的开头和结尾多了一个()包裹,这样我们后面可以拿到这个匿名函数,在后面再加一个()就可以传参数执行了。然后将需要执行的函数拼接到这个方法中间:

MyModule.wrap = function (script) {
  return MyModule.wrapper[0] + script + MyModule.wrapper[1];
};
登入後複製

这样通过MyModule.wrap包装的代码就可以获取到exports, require, module, __filename, __dirname这几个变量了。知道了这些就可以来写MyModule.prototype._compile了:

MyModule.prototype._compile = function (content, filename) {
  const wrapper = Module.wrap(content);    // 获取包装后函数体

  // vm是nodejs的虚拟机沙盒模块,runInThisContext方法可以接受一个字符串并将它转化为一个函数
  // 返回值就是转化后的函数,所以compiledWrapper是一个函数
  const compiledWrapper = vm.runInThisContext(wrapper, {
    filename,
    lineOffset: 0,
    displayErrors: true,
  });

  // 准备exports, require, module, __filename, __dirname这几个参数
  // exports可以直接用module.exports,即this.exports
  // require官方源码中还包装了一层,其实最后调用的还是this.require
  // module不用说,就是this了
  // __filename直接用传进来的filename参数了
  // __dirname需要通过filename获取下
  const dirname = path.dirname(filename);

  compiledWrapper.call(this.exports, this.exports, this.require, this,
    filename, dirname);
}
登入後複製

上述代码要注意我们注入进去的几个参数和通过call传进去的this:

  1. this:compiledWrapper是通过call调用的,第一个参数就是里面的this,这里我们传入的是this.exports,也就是module.exports,也就是说我们js文件里面this是对module.exports的一个引用。
  2. exports: compiledWrapper正式接收的第一个参数是exports,我们传的也是this.exports,所以js文件里面的exports也是对module.exports的一个引用。
  3. require: 这个方法我们传的是this.require,其实就是MyModule.prototype.require,也就是MyModule._load
  4. module: 我们传入的是this,也就是当前模块的实例。
  5. __filename:文件所在的绝对路径。
  6. __dirname: 文件所在文件夹的绝对路径。

到这里,我们的JS文件其实已经记载完了,对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L1043

加载json文件: MyModule._extensions['.json']

加载json文件就简单多了,只需要将文件读出来解析成json就行了:

MyModule._extensions[&#39;.json&#39;] = function (module, filename) {
  const content = fs.readFileSync(filename, &#39;utf8&#39;);
  module.exports = JSONParse(content);
}
登入後複製

exportsmodule.exports的区别

网上经常有人问,node.js里面的exportsmodule.exports到底有什么区别,其实前面我们的手写代码已经给出答案了,我们这里再就这个问题详细讲解下。exportsmodule.exports这两个变量都是通过下面这行代码注入的。

compiledWrapper.call(this.exports, this.exports, this.require, this,
    filename, dirname);
登入後複製

初始状态下,exports === module.exports === {}exportsmodule.exports的一个引用,如果你一直是这样使用的:

exports.a = 1;
module.exports.b = 2;

console.log(exports === module.exports);   // true
登入後複製

上述代码中,exportsmodule.exports都是指向同一个对象{},你往这个对象上添加属性并没有改变这个对象本身的引用地址,所以exports === module.exports一直成立。

但是如果你哪天这样使用了:

exports = {
  a: 1
}
登入後複製

或者这样使用了:

module.exports = {
    b: 2
}
登入後複製

那其实你是给exports或者module.exports重新赋值了,改变了他们的引用地址,那这两个属性的连接就断开了,他们就不再相等了。需要注意的是,你对module.exports的重新赋值会作为模块的导出内容,但是你对exports的重新赋值并不能改变模块导出内容,只是改变了exports这个变量而已,因为模块始终是module,导出内容是module.exports

循环引用

Node.js对于循环引用是进行了处理的,下面是官方例子:

a.js:

console.log(&#39;a 开始&#39;);
exports.done = false;
const b = require(&#39;./b.js&#39;);
console.log(&#39;在 a 中,b.done = %j&#39;, b.done);
exports.done = true;
console.log(&#39;a 结束&#39;);
登入後複製

b.js:

console.log(&#39;b 开始&#39;);
exports.done = false;
const a = require(&#39;./a.js&#39;);
console.log(&#39;在 b 中,a.done = %j&#39;, a.done);
exports.done = true;
console.log(&#39;b 结束&#39;);
登入後複製

main.js:

console.log(&#39;main 开始&#39;);
const a = require(&#39;./a.js&#39;);
const b = require(&#39;./b.js&#39;);
console.log(&#39;在 main 中,a.done=%j,b.done=%j&#39;, a.done, b.done);
登入後複製

main.js 加载 a.js 时, a.js 又加载 b.js。 此时, b.js 会尝试去加载 a.js。 为了防止无限的循环,会返回一个 a.jsexports 对象的 未完成的副本b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。

那么这个效果是怎么实现的呢?答案就在我们的MyModule._load源码里面,注意这两行代码的顺序:

MyModule._cache[filename] = module;

module.load(filename);
登入後複製

上述代码中我们是先将缓存设置了,然后再执行的真正的load,顺着这个思路我能来理一下这里的加载流程:

  1. main加载aa在真正加载前先去缓存中占一个位置
  2. a在正式加载时加载了b
  3. b又去加载了a,这时候缓存中已经有a了,所以直接返回a.exports,即使这时候的exports是不完整的。

总结

  1. require不是黑魔法,整个Node.js的模块加载机制都是JS实现的。
  2. 每个模块里面的exports, require, module, __filename, __dirname五个参数都不是全局变量,而是模块加载的时候注入的。
  3. 为了注入这几个变量,我们需要将用户的代码用一个函数包裹起来,拼一个字符串然后调用沙盒模块vm来实现。
  4. 初始状态下,模块里面的this, exports, module.exports都指向同一个对象,如果你对他们重新赋值,这种连接就断了。
  5. module.exports的重新赋值会作为模块的导出内容,但是你对exports的重新赋值并不能改变模块导出内容,只是改变了exports这个变量而已,因为模块始终是module,导出内容是module.exports
  6. 为了解决循环引用,模块在加载前就会被加入缓存,下次再加载会直接返回缓存,如果这时候模块还没加载完,你可能拿到未完成的exports
  7. Node.js实现的这套加载机制叫CommonJS

本文完整代码已上传GitHub:https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/Node.js/Module/MyModule/index.js

参考资料

Node.js模块加载源码:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js

Node.js模块官方文档:http://nodejs.cn/api/modules.html

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

作者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges

更多编程相关知识,可访问:编程教学!!

以上是深入淺析Node.js的模組載入機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

圖文詳解Node V8引擎的記憶體和GC 圖文詳解Node V8引擎的記憶體和GC Mar 29, 2023 pm 06:02 PM

這篇文章帶大家深入了解NodeJS V8引擎的記憶體和垃圾回收器(GC),希望對大家有幫助!

一文聊聊Node中的記憶體控制 一文聊聊Node中的記憶體控制 Apr 26, 2023 pm 05:37 PM

基於無阻塞、事件驅動建立的Node服務,具有記憶體消耗低的優點,非常適合處理海量的網路請求。在海量請求的前提下,就需要考慮「記憶體控制」的相關問題了。 1. V8的垃圾回收機制與記憶體限制 Js由垃圾回收機

聊聊如何選擇一個最好的Node.js Docker映像? 聊聊如何選擇一個最好的Node.js Docker映像? Dec 13, 2022 pm 08:00 PM

選擇一個Node的Docker映像看起來像是小事,但是映像的大小和潛在漏洞可能會對你的CI/CD流程和安全造成重大的影響。那我們要如何選擇一個最好Node.js Docker映像呢?

Node.js 19正式發布,聊聊它的 6 大功能! Node.js 19正式發布,聊聊它的 6 大功能! Nov 16, 2022 pm 08:34 PM

Node 19已正式發布,以下這篇文章就來帶大家詳解了解Node.js 19的 6 大特性,希望對大家有幫助!

深入聊聊Node中的File模組 深入聊聊Node中的File模組 Apr 24, 2023 pm 05:49 PM

文件模組是對底層文件操作的封裝,例如文件讀寫/打開關閉/刪除添加等等文件模組最大的特點就是所有的方法都提供的**同步**和**異步**兩個版本,具有sync 字尾的方法都是同步方法,沒有的都是異

一起聊聊Node中的事件循環 一起聊聊Node中的事件循環 Apr 11, 2023 pm 07:08 PM

事件循環是 Node.js 的基本組成部分,透過確保主執行緒不被阻塞來實現非同步編程,了解事件循環對建立高效應用程式至關重要。以下這篇文章就來帶大家深入了解Node中的事件循環 ,希望對大家有幫助!

聊聊Node.js中的 GC (垃圾回收)機制 聊聊Node.js中的 GC (垃圾回收)機制 Nov 29, 2022 pm 08:44 PM

Node.js 是如何做 GC (垃圾回收)的?下面這篇文章就來帶大家了解一下。

聊聊用pkg將Node.js專案打包為執行檔的方法 聊聊用pkg將Node.js專案打包為執行檔的方法 Dec 02, 2022 pm 09:06 PM

如何用pkg打包nodejs可執行檔?以下這篇文章跟大家介紹一下使用pkg將Node專案打包為執行檔的方法,希望對大家有幫助!

See all articles