在編寫稍大些的項目的時候,模塊化和組件化是當前 JS 的最佳解決方案。在NodeJS中,通常將代碼合理拆分到不一樣的JS文件中,每個文件就是一個模塊,而文件路徑就是模塊名。javascript
模塊化代碼在 nodejs 中有如下特性:html
require
、exports
、module
三個預先定義好的變量可供使用。require函數用於在當前模塊中加載和使用別的模塊,傳入一個模塊名
(路徑),反回一個模塊導出對象。模塊名可以使用相對路徑(以./開頭),或者是絕對路徑(以/或C:之類的盤符開頭)。另外,模塊名中的.js擴展名能夠省略。如下是一個例子。前端
var foo1 = require('./foo');
var foo2 = require('./foo.js');
var foo3 = require('/home/user/foo');
var foo4 = require('/home/user/foo.js');
// foo1至foo4中保存的是同一個模塊的導出對象。
複製代碼
另外,可使用如下方式加載和使用一個JSON文件。java
var data = require('./data.json');
複製代碼
exports對象是當前模塊的導出對象,用於導出模塊公有方法和屬性。別的模塊經過require函數使用當前模塊時獲得的就是當前模塊的exports對象。如下例子中導出了一個公有方法。node
exports.hello = function () {
console.log('Hello World!');
};
複製代碼
經過module對象能夠訪問到當前模塊的一些相關信息,但最多的用途是替換當前模塊的導出對象。例如模塊導出對象默認是一個普通對象,若是想改爲一個函數的話,可使用如下方式。git
module.exports = function () {
console.log('Hello World!');
};
複製代碼
※ 官方實現require
的方式:github
function require(...) {
var module = { exports: {} };
((module, exports) => {
// Your module code here. In this example, define a function.
function some_func() {};
exports = some_func;
// At this point, exports is no longer a shortcut to module.exports, and
// this module will still export an empty default object.
module.exports = some_func;
// At this point, the module will now export some_func, instead of the
// default object.
})(module, module.exports);
return module.exports;
}
複製代碼
若是 a.js require 了 b.js, 那麼在 b 中定義全局變量 t = 111 可否在 a 中直接打印出來?面試
① 每一個 .js 能獨立一個環境只是由於 node 幫你在外層包了一圈自執行, 因此你使用 t = 111 定義全局變量在其餘地方固然能拿到. 狀況以下:算法
// b.js
(function (exports, require, module, __filename, __dirname) {
t = 111;
})();
// a.js
(function (exports, require, module, __filename, __dirname) {
// ...
console.log(t); // 111
})();
複製代碼
a.js 和 b.js 兩個文件互相 require 是否會死循環? 雙方是否能導出變量? 如何從設計上避免這種問題?編程
② 不會, 先執行的導出空對象, 經過導出工廠函數讓對方從函數去拿比較好避免. 模塊在導出的只是 var module = { exports: {} };
中的 exports, 以從 a.js 啓動爲例, a.js 還沒執行完 exports 就是 {}
在 b.js 的開頭拿到的就是 {}
而已.
另外還有很是基礎和常見的問題, 好比 module.exports 和 exports 的區別這裏也能一併解決了 exports 只是 module.exports 的一個引用. 沒看懂能夠在細看我之前發的帖子.
再晉級一點, 衆所周知, node 的模塊機制是基於 CommonJS 規範的. 對於從前端轉 node 的同窗, 若是面試官想問的難一點會考驗關於 CommonJS 的一些問題. 好比比較 AMD, CMD, CommonJS 三者的區別, 包括詢問關於 node 中 require 的實現原理等.
exports
與 module.exports
指向相同的對象的引用,而且此對象是一個空對象{}
。
console.log(module.exports === exports); // true
console.log(exports.a); // undefined
// 修改exports
module.exports.a = 100;
console.log(module.exports === exports); // true
console.log(exports); // { a: 100 }
// 修改exports
exports.b = {
a: 100
};
console.log(module.exports === exports); // true
console.log(exports); // { a: 100, b: { a: 100 } }
複製代碼
console.log(module.exports === exports); // true
module.exports = {c:100};
console.log(exports); // {}
console.log(module.exports); // {c:100}
console.log(module.exports === exports); // false
// 直接修改exports
console.log(module.exports === exports); // false
exports = {
c:100
};
console.log(exports); // {c:100}
console.log(module.exports); // {}
console.log(module.exports === exports); // false
複製代碼
1.4 系統自帶模塊
能夠經過
process.moduleLoadList
打印的 NativeModule 能夠查看到相關的模塊信息。主要系統包括:
在 V8.9.3中,主要的系統模塊包括: [ 'assert', 'buffer', 'console', 'dns', 'domain', 'events', 'fs', 'module', 'net', 'os', 'path', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers', 'tty', 'url', 'util', 'vm' ]
其中最能表明node.js的最初思想的是net, 'events'這兩個模塊。
1.5 系統 API |穩定性:2 -- 穩定的
|v8.1.1
|
1.5.1 exports 和 module.exports
先看一個例子:
// module circle.js
const { PI } = Math;
exports.area = (r) => PI * r ** 2;
exports.circumference = (r) => 2 * PI * r;
複製代碼
circle.js
文件導出了 area()
和circumference()
兩個函數。PI
是私有的)第二個例子:
// square.js
module.exports = class Square {
constructor(width) {
this.width = width;
}
area() {
return this.width ** 2;
}
};
// use square module
const Square = require('./square.js');
const mySquare = new Square(2);
console.log(`mySquare 的面積是 ${mySquare.area()}`);
複製代碼
module.exports
屬性能夠被賦予一個新的值(例如函數或對象)。1.5.2 主模塊
當 Node.js 直接運行一個文件時,require.main
會被設爲它的 module。 這意味着能夠經過 require.main === module 來判斷一個文件是否被直接運行:
module
提供了一個 filename 屬性(一般等同於 __filename),因此能夠經過檢查 require.main.filename 來獲取當前應用程序的入口點。
1.5.3 包管理器的技巧 http://nodejs.cn/api/modules.html#modules_addenda_package_manager_tips
1.5.4 緩存
模塊在第一次加載後會被緩存。 這也意味着(相似其餘緩存機制)若是每次調用 require('foo') 都解析到同一文件,則返回相同的對象。
屢次調用 require(foo) 不會致使模塊的代碼被執行屢次。 這是一個重要的特性。 藉助它, 能夠返回「部分完成」的對象,從而容許加載依賴的依賴, 即便它們會致使循環依賴。
若是想要屢次執行一個模塊,能夠導出一個函數,而後調用該函數。
模塊緩存的注意事項:
模塊是基於其解析的文件名進行緩存的。 因爲調用模塊的位置的不一樣,模塊可能被解析成不一樣的文件名(好比從 node_modules 目錄加載),這樣就不能保證 require('foo') 總能返回徹底相同的對象。
此外,在不區分大小寫的文件系統或操做系統中,被解析成不一樣的文件名能夠指向同一文件,但緩存仍然會將它們視爲不一樣的模塊,並屢次從新加載。 例如,require('./foo') 和 require('./FOO') 返回兩個不一樣的對象,而不會管 ./foo 和 ./FOO 是不是相同的文件。
建議: 文件名稱按照必定規範進行編排,無大小寫轉換後相同文件名。
1.5.6 核心模塊
1.5.7 循環 在模塊之間的互相加載時,當 main.js
加載 a.js
時,a.js
又加載 b.js
。 此時,b.js
會嘗試去加載 a.js
, 這樣就形成了無限循環。
會返 a.js
的 exports
對象的未完成的副本給 b.js
模塊。 而後 b.js
完成加載,並將 exports
對象提供給 a.js
模塊。
DEMO:
//a.js:
console.log('a 開始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 結束');
//b.js:
console.log('b 開始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 結束');
//main.js:
console.log('main 開始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);
複製代碼
分析過程:
require(a.js)
順序執行,b.js
完成加載,並將 exports
對象提供給 a.js
模塊。$ node main.js
main 開始
a 開始
b 開始
在 b 中,a.done = false
b 結束
在 a 中,b.done = true
a 結束
在 main 中,a.done=true,b.done=true
複製代碼
1.5.8 文件模塊(系統自帶的模塊)
若是按確切的文件名沒有找到模塊,則 Node.js 會嘗試帶上.js
、.json
或 .node
拓展名再加載。
.js
文件會被解析爲 JavaScript 文本文件.json
文件會被解析爲 JSON 文本文件。.node
文件會被解析爲經過 dlopen 加載的編譯後的插件模塊。以 '/' 爲前綴的模塊是文件的絕對路徑。 例如,require('/home/marco/foo.js')
會加載 /home/marco/foo.js 文件。
以 './' 爲前綴的模塊是相對於調用 require() 的文件的。
當沒有以 '/'
、'./'
或 '../'
開頭來表示文件時,這個模塊必須是一個核心模塊或加載自 node_modules
目錄。
若是給定的路徑不存在,則 require() 會拋出一個 code 屬性爲'MODULE_NOT_FOUND'
的 Error。
1.5.9 目錄做爲模塊 若是把程序和依賴庫放在統一個文件夾下,提供一個單一的入口指向它。把目錄傳給 require()
做爲一個參數,即爲 目錄做爲模塊 引用。
package.json
指定main
入口模塊:{
"name" : "some-library",
"main" : "./lib/some-library.js"
}
複製代碼
若是這是在 ./some-library 目錄中,則 require('./some-library')
會試圖加載 ./some-library/lib/some-library.js
。
package.json
指定,則 Nodejs 會試圖加載 index.js
或 index.node
require('./some-library')
: ./some-library/index.js
或 ./some-library/index.node
1.5.10 node_modules 目錄加載 傳入require()
的路徑不是一個核心模塊,Nodejs 從父目錄開始,嘗試從父目錄的node_modules
中加載模塊。 若是在'/home/ry/projects/foo.js'
文件裏調用了 require('bar.js')
,則 Node.js 會按如下順序查找:
經過在模塊名後包含一個路徑後綴,能夠請求特定的文件或分佈式的子模塊。 例如,require('example-module/path/to/file') 會把 path/to/file 解析成相對於 example-module 的位置。 後綴路徑一樣遵循模塊的解析語法。 1.5.11 從全局目錄加載 若是 NODE_PATH 環境變量被設爲一個以冒號分割的絕對路徑列表,則當在其餘地方找不到模塊時 Node.js 會搜索這些路徑。
**注意:**在 Windows 系統中,NODE_PATH
是以分號間隔的。
在當前的模塊解析算法運行以前,NODE_PATH
最初是建立來支持從不一樣路徑加載模塊的。
雖然 NODE_PATH 仍然被支持,但如今不太須要,由於 Node.js 生態系統已制定了一套存放依賴模塊的約定。 有時當人們沒意識到 NODE_PATH 必須被設置時,依賴 NODE_PATH 的部署會出現意料以外的行爲。 有時一個模塊的依賴會改變,致使在搜索 NODE_PATH 時加載了不一樣的版本(甚至不一樣的模塊)。
此外,Node.js 還會搜索如下位置:
其中 $HOME
是用戶的主目錄,$PREFIX
是 Node.js 裏配置的 node_prefix
。
這些主要是歷史緣由。
注意:強烈建議將全部的依賴放在本地的 node_modules 目錄。 這樣將會更快地加載,且更可靠。
1.5.12 [Module Scope]
__dirname
<string>
當前模塊的文件夾的名字path.dirname(__filename)
的值DEMO: 運行 /Users/demo/example.js
console.log(__dirname);
// Prints: /Users/demo
console.log(path.dirname(__filename));
// Prints: /Users/demo
複製代碼
1.5.12 [Module Scope]
__filename
<string>
當前模塊的文件名稱---解析後的絕對路徑。DEMO: 運行/Users/demo/example.js
console.log(__filename);
// Prints: /Users/demo/example.js
console.log(__dirname);
// Prints: /Users/demo
複製代碼
給定兩個模塊: a 和 b, 其中 b 是 a 的一個依賴。
文件目錄結構以下:
使用__filename
===>
/Users/mjr/app/node_modules/b/b.js
/Users/mjr/app/a.js
1.5.13 [Module Scope]
exports / module
1.5.14 require()
用於引入模塊
require.cache
: 被引入的模塊將被緩存在這個對象中。今後對象中刪除鍵值對將會致使下一次 require 從新加載被刪除的模塊。注意不能刪除 native addons(原生插件),由於它們的重載將會致使錯誤。
require.resolve(request[, options])
使用內部的 require() 機制查詢模塊的位置, 此操做只返回解析後的文件名,不會加載該模塊。
<string>
須要解析的模塊路徑。<Object>
<Array>
解析模塊的起點路徑。此參數存在時,將使用這些路徑而非默認解析路徑。 注意此數組中的每個路徑都被用做模塊解析算法的起點,意味着 node_modules 層級將從這裏開始查詢。<string>
require.resolve.paths(request])
返回一個數組,其中包含解析 request 過程當中被查詢的路徑。 若是 request 字符串指向核心模塊(例如 http 或 fs),則返回 null。
<string>
被查詢解析路徑的模塊的路徑。<Array> | <null>
DEMO:
// modules
> require.resolve.paths('aaa')
[ '/Users/zhengao/repl/node_modules',
'/Users/zhengao/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/zhengao/.node_modules',
'/Users/zhengao/.node_libraries',
'/Users/zhengao/.nvm/versions/node/v8.9.3/lib/node',
'/Users/zhengao/.node_modules',
'/Users/zhengao/.node_libraries',
'/Users/zhengao/.nvm/versions/node/v8.9.3/lib/node' ]
// 核心模塊
> require.resolve.paths('http')
null
複製代碼
1.5.15 module對象
<Object>
- 在每一個模塊中,module 的自由變量是一個指向表示當前模塊的對象的引用。 爲了方便,module.exports 也能夠經過全局模塊的 exports 對象訪問。
- module 不是全局的,而是每一個模塊本地的。只不過每一個模塊都有一個 module 對象而已。
> module
Module {
id: '<repl>',
exports: {},
parent: undefined,
filename: null,
loaded: false,
children: [],
paths: [
'/Users/zhengao/repl/node_modules',
'/Users/zhengao/node_modules',
'/Users/node_modules',
'/node_modules',
'/Users/zhengao/.node_modules',
'/Users/zhengao/.node_libraries',
'/Users/zhengao/.nvm/versions/node/v8.9.3/lib/node'
]
}
複製代碼
module.children
:
<Array>
module.exports
:
<Object>
DEMO:
// 許多人但願他們的模塊成爲某個類的實例, 須要將指望導出的對象賦值給 module.exports
//a.js
const EventEmitter = require('events');
module.exports = new EventEmitter();
// 處理一些工做,並在一段時間後從模塊自身觸發 'ready' 事件。
setTimeout(() => {
module.exports.emit('ready');
}, 1000);
// 而後,在另外一個文件中能夠這麼作:
// b.js
const a = require('./a.js');
a.on('ready', () => {
console.log('模塊 a 已準備好');
});
複製代碼
*DEMO: *
// 注意,對 module.exports 的賦值必須當即完成。 不能在任何回調中完成。不然無效
// x.js:
setTimeout(() => {
module.exports = { a: 'hello' };
}, 0);
// y.js:
const x = require('./x');
console.log(x.a);
複製代碼
exports
快捷方式
module.exports.f = ...
能夠被更簡潔地寫成 exports.f = ...
。module.exports.hello = true; // 從對模塊的引用中導出
exports = { hello: false }; // 不導出,只在模塊內有效
//當 module.exports 屬性被一個新的對象徹底替代時,也會從新賦值 exports,例如:
module.exports = exports = function Constructor() {
// ... 及其餘
};
//爲了解釋這個行爲,想象對 require() 的假設實現,它跟 require() 的實際實現至關相似:
function require(/* ... */) {
const module = { exports: {} };
((module, exports) => {
// 模塊代碼在這。在這個例子中,定義了一個函數。
function someFunc() {}
exports = someFunc;
// 此時,exports 再也不是一個 module.exports 的快捷方式,
// 且這個模塊依然導出一個空的默認對象。
module.exports = someFunc;
// 此時,該模塊導出 someFunc,而不是默認對象。
})(module, module.exports);
return module.exports;
}` 複製代碼
module.filename
: 模塊的徹底解析後的文件名。module.id
: <string>
模塊的標識符。 一般是徹底解析後的文件名。module.loaded
: <boolean>
模塊是否已經加載完成,或正在加載中。module.parent
: <Object 模塊對象>
最早引用該模塊的模塊。module.paths
:<String []>
模塊的搜索路徑。module.require(id)
:
id
: <string>
<Object>
已解析的模塊的 module.exports
module.builtinModules
:由Node.js提供的全部模塊的名稱列表。能夠用來驗證模塊是否被第三方模塊維護。1.6 Q&A
Q1: 比較AMD,CMD和 CommonJS 三者區別 A:
背景: 網頁愈來愈像桌面程序,須要一個團隊分工協做、進度管理、單元測試等等......開發者不得不使用軟件工程的方法,管理網頁的業務邏輯。由於有了模塊,咱們就能夠更方便地使用別人的代碼,想要什麼功能,就加載什麼模塊。
CommonJS規範是誕生比較早的。NodeJS就採用了CommonJS。CommonJS 是一種同步的模塊化規範,是這樣加載模塊:
var clock = require('clock');
clock.start();
複製代碼
這種寫法適合服務端,由於在服務器讀取模塊都是在本地磁盤,加載速度很快。可是,對於瀏覽器,這倒是一個大問題,由於模塊都放在服務器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於"假死"狀態。
所以,瀏覽器端的模塊,不能採用"同步加載"(synchronous),只能採用"異步加載"(asynchronous)。
這就是AMD規範誕生的背景。好比上面的例子中clock的調用必須等待clock.js
請求成功,加載完畢。
AMD,即 (Asynchronous Module Definition),這種規範是異步的加載模塊,requireJs應用了這一規範。先定義全部依賴,而後在加載完成後的回調函數中執行:
require([module], callback);
第一個參數[module]
,是一個數組,裏面的成員就是要加載的模塊;第二個參數callback,則是加載成功以後的回調函數。若是將前面的代碼改寫成AMD形式,就是下面這樣:
require(['math'], function (math) {
math.add(2, 3);
});
複製代碼
math.add()與math模塊加載不是同步的,瀏覽器不會發生假死。因此很顯然,AMD比較適合瀏覽器環境。
目前,主要有兩個Javascript庫實現了AMD規範:require.js
和 curl.js
。
AMD雖然實現了異步加載,可是開始就把全部依賴寫出來是不符合書寫的邏輯順序的,能不能像commonJS
那樣用的時候再require,並且還支持異步加載後再執行呢?
CMD (Common Module Definition), 是seajs
推崇的規範,CMD則是依賴就近,用的時候再require。它寫起來是這樣的:
define(function(require, exports, module) {
var clock = require('clock');
clock.start();
});
複製代碼
AMD和CMD最大的區別是對依賴模塊的執行時機處理不一樣,而不是加載的時機或者方式不一樣,兩者皆爲異步加載模塊。
AMD依賴前置,js能夠方便知道依賴模塊是誰,當即加載;
而CMD就近依賴,須要使用把模塊變爲字符串解析一遍才知道依賴了那些模塊,這也是不少人詬病CMD的一點,犧牲性能來帶來開發的便利性,實際上解析模塊用的時間短到能夠忽略。
Q2:Node.js 中 require()
的實現
A:
Q3: 何時使用 exports ,何時使用 module.exports
A:用一句話來講明就是,
require
方能看到的只有module.exports
這個對象,它是看不到exports
對象的,而咱們在編寫模塊時用到的exports
對象實際上只是對module.exports
的引用。
var module = {
exports:{
name:"我是module的exports屬性"
}
};
//exports是對module.exports的引用,也就是exports如今指向的內存地址和module.exports指向的內存地址是同樣的
var exports = module.exports;
console.log(module.exports); // { name: '我是module的exports屬性' }
console.log(exports); // { name: '我是module的exports屬性' }
exports.name = "我想改一下名字";
console.log(module.exports); // { name: '我想改一下名字' }
console.log(exports); // { name: '我想改一下名字' }
//看到沒,引用的結果就是a和b都操做同一內存地址下的數據
//這個時候我在某個文件定義了一個想導出的模塊
var Circle = {
name:"我是一個圓",
func:function(x){
return x*x*3.14;
}
};
exports = Circle; // 看清楚了,Circle這個Object在內存中指向了新的地址,因此exports也指向了這個新的地址,和原來的地址沒有半毛錢關係了
console.log(module.exports); // { name: '我想改一下名字' }
console.log(exports); // { name: '我是一個圓', func: [Function] }
複製代碼
exports
和 module.exports
指向的是同一個引用exports
內部做用域使用,賦值給 module.exports
能夠被require()
引用。1.7 模塊化的基本要求
1.8 模塊化的代碼規範
在 Node.js 中使用 CommonJS 使用模塊規範