極簡 Node.js 入門 - 1.2 模組系統
阿新 • • 發佈:2020-08-05
極簡 Node.js 入門系列教程:[https://www.yuque.com/sunluyong/node](https://www.yuque.com/sunluyong/node)
本文更佳閱讀體驗:[https://www.yuque.com/sunluyong/node/module](https://www.yuque.com/sunluyong/node/module)
## Node.js 的模組
JavaScript 做為一門為網頁新增互動功能的簡單指令碼語言問世,在開始並不包含模組系統,隨著 JavaScript 解決問題越來越複雜,把所有程式碼寫在一個檔案內,用 function 區分功能單元已經不能支撐複雜應用開發了,ES6 帶來了大部分高階語言都有的 class 和 module,方便開發者組織程式碼
```javascript
import _ from 'lodash';
class Fun {}
export default Fun;
```
上面三行程式碼展示了一個模組系統最重要的兩個要素 import 和 export
1. `export`用於規定模組的對外介面
1. `import`用於輸入其他模組提供的功能
而在 ES6 之前,社群出現了很多模組載入方案,最主要的有 CommonJS 和 AMD 兩種,Node.js 誕生早於 ES6,模組系統使用的是類似 CommonJS 的實現,遵從幾個原則
1. 一個檔案是一個模組,檔案內的變數作用域都在模組內
1. 使用 `module.exports` 物件匯出模組對外介面
1. 使用 `require` 引入其它模組
`circle.js`
```javascript
const { PI } = Math;
module.exports = function area(r) {
PI * r ** 2;
};
```
上面程式碼就實現了 Node.js 的一個模組,模組沒有依賴其它模組,匯出了方法 `area` 計算圓的面積
`test.js`
```javascript
const area = require('./circle.js');
console.log(`半徑為 4 的圓的面積是 ${area(4)}`);
```
模組依賴了 circle.js,使用其對外暴露的 area 方法,計算圓的面積
## module.exports
模組對外暴露介面使用 module.exports,常見的有兩種用法:為其新增屬性或賦值到新物件
`test.js`
```javascript
// 新增屬性
module.exports.prop1 = xxx;
module.exports.funA = xxx;
module.exports.funB = xxx;
// 賦值到全新物件
module.exports = {
prop1,
funA,
funB,
};
```
兩種寫法是等價的,使用時候沒區別
```javascript
const mod = require('./test.js');
console.log(mod.prop1);
console.log(mod.funA());
```
還有另外一種直接使用 `exports` 物件的方法,但是隻能對其新增屬性,不能賦值到新物件,後面會介紹原因
```javascript
// 正確的寫法:新增屬性
exports.prop1 = xxx;
exports.funA = xxx;
exports.funB = xxx;
// 賦值到全新物件
module.exports = {
prop1,
funA,
funB,
};
```
## require('id')
### 模組型別
require 用法比較簡單,id 支援模組名和檔案路徑兩種型別
#### 模組名
```javascript
const fs = require('fs');
const _ = require('lodash');
```
示例中的 fs、lodash 都是模組名,fs 是 Node.js 內建的核心模組,lodash 是通過 npm 安裝到 `node_modules` 下的第三方模組,如果出現重名,優先使用系統內建模組
因為一個專案內可能會包含多個 node_modules 資料夾(Node.js 比較失敗的設計),第三方模組查詢過程會遵循就近原則逐層上溯(可以在程式中列印 `module.paths` 檢視具體查詢路徑),直到根據 `NODE_PATH` 環境變數查詢到檔案系統根目錄,具體過程可以參考[官方文件](https://nodejs.org/docs/latest-v12.x/api/modules.html#modules_loading_from_node_modules_folders)
此外,Node.js 還會搜尋以下的全域性目錄列表:
- $HOME/.node_modules
- $HOME/.node_libraries
- $PREFIX/lib/node
其中 `$HOME` 是使用者的主目錄, `$PREFIX` 是 Node.js 裡配置的 `node_prefix`。強烈建議將所有的依賴放在本地的 node_modules 目錄,這樣將會更快地載入,且更可靠
#### 檔案路徑
模組還可以可以使用檔案路徑載入,這是專案內自定義模組的通用載入方式,路徑可以省略拓展名,會按照 .js、.json、.node 順序嘗試
- 以 `'/'` 為字首的模組是檔案的絕對路徑,按照系統路徑查詢模組
- 以 `'./'` 為字首的模組是相對於當前呼叫 require 方法的檔案,不受後續模組在哪裡被使用到影響
### 單次載入 & 迴圈依賴
模組在第一次載入後會被快取到 `Module._cache` ,如果每次呼叫 `require('foo')` 都解析到同一檔案,則返回相同的物件,同時多次呼叫 `require(foo)` 不會導致模組的程式碼被執行多次。 Node.js 根據實際的檔名快取模組,因此從不同層級目錄引用相同模組不會重複載入。
理解的模組單次載入機制方便我們理解模組迴圈依賴後的現象
`a.js`
```javascript
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`
```javascript
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`:
```javascript
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);
```
當 main.js 載入 a.js 時,a.js 又載入 b.js,此時,b.js 會嘗試去載入 a.js
為了防止無限的迴圈會返回一個 a.js 的 exports 物件的 **未完成的副本** 給 b.js 模組,然後 b.js 完成載入,並將 exports 物件提供給 a.js 模組
因此示例的輸出是
```
main 開始
a 開始
b 開始
在 b 中,a.done = false
b 結束
在 a 中,b.done = true
a 結束
在 main 中,a.done=true,b.done=true
```
看不懂上面的過程也沒關係,日常工作根本用不到,即使看懂了也不要在專案中使用迴圈依賴!
## 工作原理
Node.js 每個檔案都是一個模組,模組內的變數都是區域性變數,不會汙染全域性變數,在執行模組程式碼之前,Node.js 會使用一個如下的函式封裝器將模組封裝
```javascript
(function(exports, require, module, __filename, __dirname) {
// 模組的程式碼實際上在這裡
});
```
- __filename:當前模組檔案的絕對路徑
- __dirname:當前模組檔案據所在目錄的絕對路徑
- module:當前的模組例項
- require:載入其它模組的方法,module.require 的快捷方式
- exports:匯出模組介面的物件,module.exports 的快捷方式
回頭看看最開始的問題,為什麼 exports 物件不支援賦值為其它物件?把上面函式新增一句 exports 物件來源就很簡單了
```javascript
const exports = module.exports;
(function(exports, require, module, __filename, __dirname) {
// 模組的程式碼實際上在這裡
});
```
其它模組 require 到的肯定是模組的 module.exports 物件,如果吧 exports 物件賦值給其它物件,就和 module.exports 物件斷開了連線,自然就沒用了
## 在 Node.js 中使用 ES Module
隨著 ES6 使用越來越廣泛,Node.js 也支援了 ES6 Module,有幾種方法
### babel 構建
使用 babel 構建是在 v12 之前版本最簡單、通用的方式,具體配置參考 [@babel/preset-env](https://babeljs.io/docs/en/babel-preset-env)
`.babelrc`
```json
{
"presets": [
["@babel/preset-env", {
"targets": {
"node": "8.9.0",
"esmodules": true
}
}]
]
}
```
### 原生支援
在 v12 後可以使用原生方式支援 ES Module
1. 開啟 `--experimental-modules`
1. 模組名修改為 `.mjs` (強烈不推薦使用)或者 package.json 中設定 `"type": module`
這樣 Node.js 會把 js 檔案都當做 ES Module 來處理,更多詳情參考[官方文件](https://nodejs.org/dist/latest-v13.x/docs/api/es