本文做者:Jacob Beltran
編譯:鬍子大哈 javascript翻譯原文:huziketang.com/blog/posts/…
英文鏈接:Requiring modules in Node.js: Everything you need to knowhtml
Node 中有兩個核心模塊來對模塊依賴進行管理:前端
require
模塊。全局範圍生效,不須要 require('require')
。module
模塊。全局範圍生效,不須要 require('module')
。你能夠把 require
當作是命令行,而把 module
當作是全部引入模塊的組織者。java
在 Node 中引入模塊並非什麼複雜的概念,見下面例子:node
const config = require('/path/to/file');複製代碼
require
引入的對象主要是函數。當 Node 調用 require()
函數,而且傳遞一個文件路徑給它的時候,Node 會經歷以下幾個步驟:react
require
和 module
模塊在本地引用的一種方法;本文中,我會用不一樣的例子來解釋上面的各個步驟,而且介紹在 Node 中它們對咱們寫的模塊有什麼樣的影響。json
爲了方便你們看文章和理解命令,我首先建立一個目錄,後面的操做都會在這個目錄中進行。api
mkdir ~/learn-node && cd ~/learn-node複製代碼
文章中接下來的部分都會在 ~/learn-node
文件夾下運行。瀏覽器
首先來爲你介紹 module
對象,能夠先在控制檯中看一下:緩存
~/learn-node $ node
> module
Module {
id: '<repl>',
exports: {},
parent: undefined,
filename: null,
loaded: false,
children: [],
paths: [ ... ] }複製代碼
每個模塊都有 id
屬性來惟一標示它。id
一般是文件的完整路徑,可是在控制檯中通常顯示成 <repl>
。
Node 模塊和文件系統中的文件一般是一一對應的,引入一個模塊須要把文件內容加載到內存中。由於 Node 有不少種方法引入一個文件(例如相對路徑,或者提早配置好的路徑),因此首先須要找到文件的絕對路徑。
若是我引入了一個 'find-me'
模塊,並無指定它的路徑的話:
require('find-me');複製代碼
Node 會按照 module.paths
所指定的文件目錄順序依次尋找 find-me.js
。
~/learn-node $ node
> module.paths
[ '/Users/samer/learn-node/repl/node_modules',
'/Users/samer/learn-node/node_modules',
'/Users/samer/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/samer/.node_modules',
'/Users/samer/.node_libraries',
'/usr/local/Cellar/node/7.7.1/lib/node' ]複製代碼
這個路徑列表基本上包含了從當前目錄到根目錄的全部路徑中的 node_modules 目錄。其中還包含了一些不建議使用的遺留目錄。若是 Node 在上面全部的目錄中都沒有找到 find-me.js
,會拋出一個「cannot find module error.」錯誤。
~/learn-node $ node
> require('find-me')
Error: Cannot find module 'find-me'
at Function.Module._resolveFilename (module.js:470:15)
at Function.Module._load (module.js:418:25)
at Module.require (module.js:498:17)
at require (internal/module.js:20:19)
at repl:1:1
at ContextifyScript.Script.runInThisContext (vm.js:23:33)
at REPLServer.defaultEval (repl.js:336:29)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer.onLine (repl.js:533:10)複製代碼
若是如今建立一個 node_modules
,並把 find-me.js
放進去,那麼 require('find-me')
就能找到了。
~/learn-node $ mkdir node_modules
~/learn-node $ echo "console.log('I am not lost');" > node_modules/find-me.js
~/learn-node $ node
> require('find-me');
I am not lost
{}
>複製代碼
假設還有另外一個目錄中存在 find-me.js
,例如在 home/node_modules 目錄中有另外一個 find-me.js
文件。
$ mkdir ~/node_modules
$ echo "console.log('I am the root of all problems');" > ~/node_modules/find-me.js複製代碼
當咱們從 learn-node
目錄中執行 require('find-me')
的時候,因爲 learn-node
有本身的 node_modules/find-me.js
,這時不會加載 home 目錄下的 find-me.js
:
~/learn-node $ node
> require('find-me')
I am not lost
{}
>複製代碼
假設咱們把 learn-node
目錄下的 node_modules
移到 ~/learn-node
,再從新執行 require('find-me')
的話,按照上面規定的順序查找文件,這時候 home 目錄下的 node_modules
就會被使用了。
~/learn-node $ rm -r node_modules/
~/learn-node $ node
> require('find-me')
I am the root of all problems
{}
>複製代碼
模塊不必定非要是文件,也能夠是個文件夾。咱們能夠在 node_modules
中建立一個 find-me
文件夾,而且放一個 index.js
文件在其中。那麼執行 require('find-me')
將會使用 index.js
文件:
~/learn-node $ mkdir -p node_modules/find-me
~/learn-node $ echo "console.log('Found again.');" > node_modules/find-me/index.js
~/learn-node $ node
> require('find-me');
Found again.
{}
>複製代碼
這裏注意,咱們本目錄下建立了 node_modules
文件夾,就不會使用 home 目錄下的 node_modules
了。
當引入一個文件夾的時候,默認會去找 index.js
文件,這也能夠手動控制指定到其餘文件,利用 package.json
的 main
屬性就能夠。例如,咱們執行 require('find-me')
,而且要從 find-me
文件夾下的 start.js
文件開始解析,那麼用 package.json
的作法以下:
~/learn-node $ echo "console.log('I rule');" > node_modules/find-me/start.js
~/learn-node $ echo '{ "name": "find-me-folder", "main": "start.js" }' > node_modules/find-me/package.json
~/learn-node $ node
> require('find-me');
I rule
{}
>複製代碼
若是你只是想解析模塊,而不執行的話,可使用 require.resolve
函數。它和主 require
函數所作的事情如出一轍,除了不加載文件。當沒找到文件的時候也會拋出錯誤,若是找到會返回文件的完整路徑。
> require.resolve('find-me');
'/Users/samer/learn-node/node_modules/find-me/start.js'
> require.resolve('not-there');
Error: Cannot find module 'not-there'
at Function.Module._resolveFilename (module.js:470:15)
at Function.resolve (internal/module.js:27:19)
at repl:1:9
at ContextifyScript.Script.runInThisContext (vm.js:23:33)
at REPLServer.defaultEval (repl.js:336:29)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer.onLine (repl.js:533:10)
at emitOne (events.js:101:20)
at REPLServer.emit (events.js:191:7)
>複製代碼
它能夠用於檢查一個包是否已經安裝,只有當包存在的時候才使用該包。
除了能夠把模塊放在 node_modules
目錄中,還有更自由的方法。咱們能夠把模塊放在任何地方,而後經過相對路徑(./
和 ../
)或者絕對路徑(/
)來指定文件路徑。
例如 find-me.js
文件是在 lib
目錄下,而不是在 node_modules
下,咱們能夠這樣引入:
require('./lib/find-me');複製代碼
建立一個文件 lib/util.js
而且寫一行 console.log
在裏面來標識它,固然,這個 console.log
就是模塊自己。
~/learn-node $ mkdir lib
~/learn-node $ echo "console.log('In util', module);" > lib/util.js複製代碼
在 index.js
中寫上將要執行的 node 命令,而且在 index.js
中引入 lib/util.js
:
~/learn-node $ echo "console.log('In index', module); require('./lib/util');" > index.js複製代碼
如今在 node 中執行 index.js
:
~/learn-node $ node index.js
In index Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/samer/learn-node/index.js',
loaded: false,
children: [],
paths: [ ... ] }
In util Module {
id: '/Users/samer/learn-node/lib/util.js',
exports: {},
parent:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/samer/learn-node/index.js',
loaded: false,
children: [ [Circular] ],
paths: [...] },
filename: '/Users/samer/learn-node/lib/util.js',
loaded: false,
children: [],
paths: [...] }複製代碼
注意到這裏,index
模塊(id:'.'
)被列到了 lib/util
的 parent 屬性中。而 lib/util
並無被列到 index
的 children 屬性,而是用一個 [Circular]
代替的。這是由於這是個循環引用,若是這裏使用 lib/util
的話,那就變成一個無限循環了。這就是爲何在 index
中使用 [Circular]
來替代 lib/util
。
那麼重點來了,若是在 lib/util
中引入了 index
模塊會怎麼樣?這就是咱們所謂的模塊循環依賴問題,在 Node 中是容許這樣作的。
可是 Node 如何處理這種狀況呢?爲了更好地理解這一問題,咱們先來了解一下模塊對象的其餘知識。
在全部的模塊中,exports 都是一個特殊的對象。若是你有注意的話,上面咱們每次打印模塊信息的時候,都有一個是空值的 exports 屬性。咱們能夠給這個 exports 對象加任何想加的屬性,例如在 index.js
和 lib/util.js
中給它添加一個 id
屬性:
// 在 lib/util.js 的最上面添加這行
exports.id = 'lib/util';
// 在 index.js 的最上面添加這行
exports.id = 'index';複製代碼
執行 index.js
,能夠看到咱們添加的屬性已經存在於模塊對象中:
~/learn-node $ node index.js
In index Module {
id: '.',
exports: { id: 'index' },
loaded: false,
... }
In util Module {
id: '/Users/samer/learn-node/lib/util.js',
exports: { id: 'lib/util' },
parent:
Module {
id: '.',
exports: { id: 'index' },
loaded: false,
... },
loaded: false,
... }複製代碼
上面爲了輸出結果簡潔,我刪掉了一些屬性。你能夠往 exports 對象中添加任意多的屬性,甚至能夠把 exports 對象變成其餘類型,好比把 exports 對象變成函數,作法以下:
// 在 index.js 的 console.log 前面添加這行
module.exports = function() {};複製代碼
當你執行 index.js
的時候,你會看到以下信息:
~/learn-node $ node index.js
In index Module {
id: '.',
exports: [Function],
loaded: false,
... }複製代碼
這裏注意咱們沒有使用 export = function() {}
來改變 exports
對象。沒有這樣作是由於在模塊中的 exports
變量其實是 module.exports
的一個引用,而 module.exports
纔是控制全部對外屬性的。exports
和 module.exports
指向同一塊內存,若是把 exports
指向一個函數,那麼至關於改變了 exports
的指向,exports
就再也不是引用了。即使你改變了 exports
,module.exports
也是不變的。
模塊的 module.exports
是一個模塊的對外接口,就是當你使用 require
函數時所返回的東西。例如把 index.js
中的代碼改一下:
const UTIL = require('./lib/util');
console.log('UTIL:', UTIL);複製代碼
上面的代碼將會捕獲 lib/util
中輸出的屬性,賦值給 UTIL
常量。當執行 index.js
的時候,最後一行將會輸出:
UTIL: { id: 'lib/util' }複製代碼
接下來聊一下 loaded
屬性。上面咱們每次輸出模塊信息,都能看到一個 loaded
屬性,值是 false
。
module
模塊使用 loaded
屬性來追蹤哪些模塊已經加載完畢,哪些模塊正在加載。例如咱們能夠調用 setImmediate
來打印 module
對象,用它能夠看到 index.js
的徹底加載信息:
// In index.js
setImmediate(() => {
console.log('The index.js module object is now loaded!', module)
});複製代碼
輸出結果:
The index.js module object is now loaded! Module {
id: '.',
exports: [Function],
parent: null,
filename: '/Users/samer/learn-node/index.js',
loaded: true,
children:
[ Module {
id: '/Users/samer/learn-node/lib/util.js',
exports: [Object],
parent: [Circular],
filename: '/Users/samer/learn-node/lib/util.js',
loaded: true,
children: [],
paths: [Object] } ],
paths:
[ '/Users/samer/learn-node/node_modules',
'/Users/samer/node_modules',
'/Users/node_modules',
'/node_modules' ] }複製代碼
能夠注意到 lib/util.js
和 index.js
都已經加載完畢了。
當一個模塊加載完成的時候,exports
對象才完整,整個加載的過程都是同步的。這也是爲何在一個事件循環後全部的模塊都處於徹底加載狀態的緣由。
這也意味着不能異步改變 exports
對象,例如,對任何模塊作下面這樣的事情:
fs.readFile('/etc/passwd', (err, data) => {
if (err) throw err;
exports.data = data; // Will not work.
});複製代碼
咱們如今來回答上面說到的循環依賴的問題:模塊 1 依賴模塊 2,模塊 2 也依賴模塊 1,會發生什麼?
如今來建立兩個文件,lib/module1.js
和 lib/module2.js
,而且讓它們相互引用:
// lib/module1.js
exports.a = 1;
require('./module2');
exports.b = 2;
exports.c = 3;
// lib/module2.js
const Module1 = require('./module1');
console.log('Module1 is partially loaded here', Module1);複製代碼
接下來執行 module1.js
,能夠看到:
~/learn-node $ node lib/module1.js
Module1 is partially loaded here { a: 1 }複製代碼
在 module1
徹底加載以前須要先加載 module2
,而 module2
的加載又須要 module1
。這種狀態下,咱們從 exports
對象中能獲得的就是在發生循環依賴以前的這部分。上面代碼中,只有 a
屬性被引入,由於 b
和 c
都須要在引入 module2
以後才能加載進來。
Node 使這個問題簡單化,在一個模塊加載期間開始建立 exports
對象。若是它須要引入其餘模塊,而且有循環依賴,那麼只能部分引入,也就是隻能引入發生循環依賴以前所定義的這部分。
咱們可使用 require 函數本地引入 JSON 文件和 C++ 擴展文件,理論上來說,不須要指定其擴展名。
若是沒有指定擴展名,Node 會先嚐試將其按 .js
文件來解析,若是不是 .js
文件,再嘗試按 .json
文件來解析。若是都不是,會嘗試按 .node
二進制文件解析。可是爲了使程序更清晰,當引入除了 .js
文件的時候,你都應該指定文件擴展名。
若是你要操做的文件是一些靜態配置值,或者是須要按期從外部文件中讀取的值,那麼引入 JSON 是很好的一個選擇。例若有以下的 config.json
文件:
{
"host": "localhost",
"port": 8080
}複製代碼
咱們能夠直接像這樣引用:
const { host, port } = require('./config');
console.log(`Server will run at http://${host}:${port}`);複製代碼
運行上面的代碼會獲得這樣的輸出:
Server will run at http://localhost:8080複製代碼
若是 Node 按 .js
和 .json
解析都失敗的話,它會按 .node
解析,把這個文件當作一個已編譯的擴展模塊來解析。
Node 文檔中有一個 C++ 寫的示例擴展文件,它只暴露出一個 hello()
函數,而且函數輸出 「world」。
你可使用 node-gyp
包編譯 .cc
文件,生成 .addon
文件。只須要配置 binding.gyp 文件來告訴 node-gyp
須要作什麼就能夠了。
當你有了 addon.node
文件(名字你能夠在 binding.gyp
中隨意配置)之後,你就能夠在本地像引入其餘模塊同樣引入它了:
const addon = require('./addon');
console.log(addon.hello());複製代碼
能夠經過 require.extensions
來查看對三種文件的支持狀況:
能夠清晰地看到 Node 對每種擴展名所使用的函數及其操做:對 .js
文件使用 module._compile
;對 .json
文件使用 JSON.parse
;對 .node
文件使用 process.dlopen
。
Node 的打包模塊不是很好理解,首先要先知道 exports
/ module.exports
的關係。
咱們能夠用 exports
對象來輸出屬性,可是不能直接對 exports
進行賦值(替換整個 exports
對象),由於它僅僅是 module.exports
的引用。
exports.id = 42; // This is ok.
exports = { id: 42 }; // This will not work.
module.exports = { id: 42 }; // This is ok.複製代碼
在介紹 Node 的打包過程以前先來了解另外一個問題,一般狀況下,在瀏覽器中咱們在腳本中定義一個變量:
var answer = 42;複製代碼
這種方式定義之後,answer
變量就是一個全局變量了。其餘腳本中依然能夠訪問。而 Node 中不是這樣,你在一個模塊中定義一個變量,程序的其餘模塊是不能訪問的。Node 是如何作到的呢?
答案很簡單,在編譯成模塊以前,Node 把模塊代碼都打包成函數,能夠用 module
的 wrapper
屬性來查看。
~ $ node
> require('module').wrapper
[ '(function (exports, require, module, __filename, __dirname) { ',
'\n});' ]
>複製代碼
Node 並不直接執行你所寫的代碼,而是把你的代碼打包成函數後,執行這個函數。這就是爲何一個模塊的頂層變量的做用域依然僅限於本模塊的緣由。
這個打包函數有 5 個參數:exports
,require
,module
,__filename
,__dirname
。函數使變量看起來全局生效,但實際上只在模塊內生效。全部的這些參數都在 Node 執行函數時賦值。exports
定義成 module.exports
的引用;require
和 module
都指定爲將要執行的這個函數;__filename
和 __dirname
指這個打包模塊的絕對路徑和目錄路徑。
在腳本的第一行輸入有問題的代碼,就能看到 Node 打包的行爲;
~/learn-node $ echo "euaohseu" > bad.js
~/learn-node $ node bad.js
~/bad.js:1
(function (exports, require, module, __filename, __dirname) { euaohseu
^
ReferenceError: euaohseu is not defined複製代碼
注意這裏報告出錯誤的就是打包函數。
另外,模塊都打包成函數了,咱們可使用 arguments
關鍵字來訪問函數的參數:
~/learn-node $ echo "console.log(arguments)" > index.js
~/learn-node $ node index.js
{ '0': {},
'1':
{ [Function: require]
resolve: [Function: resolve],
main:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/samer/index.js',
loaded: false,
children: [],
paths: [Object] },
extensions: { ... },
cache: { '/Users/samer/index.js': [Object] } },
'2':
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/samer/index.js',
loaded: false,
children: [],
paths: [ ... ] },
'3': '/Users/samer/index.js',
'4': '/Users/samer' }複製代碼
第一個參數是 exports
對象,初始爲空;require
和 module
對象都是即將執行的 index.js
的實例;最後兩個參數是文件路徑和目錄路徑。
打包函數的返回值是 module.exports
。在模塊內部,可使用 exports
對象來改變 module.exports
屬性,可是不能對 exports
從新賦值,由於它只是 module.exports
的引用。
至關於以下代碼:
function (require, module, __filename, __dirname) {
let exports = module.exports;
// Your Code...
return module.exports;
}複製代碼
若是對 exports
從新賦值(改變整個 exports
對象),那它就不是 module.exports
的引用了。這是 JavaScript 引用的工做原理,不只僅是在這裏是這樣。
require
沒有什麼特別的,一般做爲一個函數返回 module.exports
對象,函數參數是一個模塊名或者一個路徑。若是你想的話,盡能夠根據本身的邏輯重寫 require
對象。
例如,爲了達到測試的目的,咱們但願全部的 require
都默認返回一個 mock 值來替代真實的模塊返回值。能夠簡單地實現以下:
require = function() {
return { mocked: true };
}複製代碼
這樣重寫了 require
之後,每一個 require('something')
調用都會返回一個模擬對象。
require
對象也有本身的屬性。上面已經見過了 resolve
屬性,它的任務是處理引入模塊過程當中的解析步驟,上面還提到過 require.extensions
也是 require
的屬性。還有 require.main
,它用於判斷一個腳本是否應該被引入仍是直接執行。
例如,在 print-in-frame.js
中有一個 printInFrame
函數。
// In print-in-frame.js
const printInFrame = (size, header) => {
console.log('*'.repeat(size));
console.log(header);
console.log('*'.repeat(size));
};複製代碼
函數有兩個參數,一個是數字類型參數 size
,一個是字符串類型參數 header
。函數功能很簡單,這裏不贅述。
咱們想用兩種方式使用這個文件:
1.直接使用命令行:
~/learn-node $ node print-in-frame 8 Hello複製代碼
傳遞 8 和 「Hello」 兩個參數進去,打印 8 個星星包裹下的 「Hello」。
2.使用 require
。假設所引入的模塊對外接口是 printInFrame
函數,咱們能夠這樣調用:
const print = require('./print-in-frame');
print(5, 'Hey');複製代碼
傳遞的參數是 5 和 「Hey」。
這是兩種不一樣的用法,咱們須要一種方法來判斷這個文件是做爲獨立的腳原本運行,仍是須要被引入到其餘的腳本中才能執行。可使用簡單的 if 語句來實現:
if (require.main === module) {
// 這個文件直接執行(不須要 require)
}複製代碼
繼續演化,可使用不一樣的調用方式來實現最初的需求:
// In print-in-frame.js
const printInFrame = (size, header) => {
console.log('*'.repeat(size));
console.log(header);
console.log('*'.repeat(size));
};
if (require.main === module) {
printInFrame(process.argv[2], process.argv[3]);
} else {
module.exports = printInFrame;
}複製代碼
當文件不須要被 require 時,直接經過 process.argv
調用 printInFrame
函數便可。不然直接把 module.exports
變成 printInFrame
就能夠了,即模塊接口是 printInFrame
。
對緩存的理解特別重要,我用簡單的例子來解釋緩存。
假設你有一個 ascii-art.js
文件,打印很酷的 header:
咱們想要在每次 require
這個文件的時候,都打印出 header。因此把這個文件引入兩次:
require('./ascii-art') // 顯示 header
require('./ascii-art') // 不顯示 header.複製代碼
第二個 require 不會顯示 header,由於模塊被緩存了。Node 把第一個調用緩存起來,第二次調用的時候就不加載文件了。
能夠在第一次引入文件之後,使用 require.cache
來看一下都緩存了什麼。緩存中其實是一個對象,這個對象中包含了引入模塊的屬性。咱們能夠從 require.cache
中把相應的屬性刪掉,以使緩存失效,這樣 Node 就會從新加載模塊而且將其從新緩存起來。
對於這個問題,這並非最有效的解決方案。最簡單的解決方案是把 ascii-art.js
中的打印代碼打包成一個函數,而且 export 這個函數。這樣當咱們引入 ascii-art.js
文件時,咱們獲取到的是這個函數,因此能夠每次都能打印出想要的內容了:
require('./ascii-art')() // 打印出 header.
require('./ascii-art')() // 也會打印出 header.複製代碼
這就是我所要介紹的內容。回顧一下通篇,分別講述了:
即解析、加載、打包、VM功能處理和緩存五大步驟,以及五大步驟中每一個步驟都涉及到了什麼內容。
若是本文對你有幫助,歡迎關注個人專欄-前端大哈,按期發佈高質量前端文章。
我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點。