目前主流的模組規範
- UMD
- CommonJs
- es6 module
umd 模組(通用模組)
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.libName = factory());
}(this, (function () { 'use strict';})));
複製程式碼
如果你在js
檔案頭部看到這樣的程式碼,那麼這個檔案使用的就是 UMD
規範
實際上就是 amd + commonjs + 全域性變數 這三種風格的結合
這段程式碼就是對當前執行環境的判斷,如果是 Node
環境 就是使用 CommonJs
規範, 如果不是就判斷是否為 AMD
環境, 最後匯出全域性變數
有了 UMD
後我們的程式碼和同時執行在 Node
和 瀏覽器上
所以現在前端大多數的庫最後打包都使用的是 UMD
規範
CommonJs
Nodejs
環境所使用的模組系統就是基於CommonJs
規範實現的,我們現在所說的CommonJs
規範也大多是指Node
的模組系統
模組匯出
關鍵字:module.exports
exports
// foo.js
//一個一個 匯出
module.exports.age = 1
module.exports.foo = function(){}
exports.a = 'hello'
//整體匯出
module.exports = { age: 1, a: 'hello', foo:function(){} }
//整體匯出不能用`exports` 用exports不能在匯入的時候使用
exports = { age: 1, a: 'hello', foo:function(){} }
複製程式碼
這裡需要注意 exports
不能被賦值,可以理解為在模組開始前exports = module.exports
, 因為賦值之後exports
失去了 對module.exports
的引用,成為了一個模組內的區域性變數
模組匯入
關鍵字:require
const foo = require('./foo.js')
console.log(foo.age) //1
複製程式碼
模組匯入規則:
假設以下目錄為 src/app/index.js
的檔案 呼叫 require()
./moduleA
相對路徑開頭
在沒有指定字尾名的情況下
先去尋找同級目錄同級目錄:src/app/
src/app/moduleA
無字尾名檔案 按照javascript
解析src/app/moduleA.js
js檔案 按照javascript
解析src/app/moduleA.json
json檔案 按照json
解析src/app/moduleA.node
node檔案 按照載入的編譯外掛模組dlopen
同級目錄沒有 moduleA
檔案會去找同級的 moduleA
目錄:src/app/moduleA
src/app/moduleA/package.json
判斷該目錄是否有package.json
檔案, 如果有 找到main
欄位定義的檔案返回, 如果main
欄位指向檔案不存在 或main
欄位不存在 或package.json
檔案不存在向下執行src/app/moduleA/index.js
src/app/moduleA/index.json
src/app/moduleA/index.node
結束
/module/moduleA
絕對路徑開頭
直接在/module/moduleA
目錄中尋找 規則同上
react
沒有路徑開頭
沒有路徑開頭則視為匯入一個包
會先判斷moduleA
是否是一個核心模組 如path
,http
,優先匯入核心模組
不是核心模組 會從當前檔案的同級目錄的node_modules
尋找
/src/app/node_modules/
尋找規則同上 以匯入react
為例先 node_modules 下 react 檔案 -> react.js -> react.json -> react.node ->react目錄 -> react package.json main -> index.js -> index.json -> index.node
如果沒找到 繼續向父目錄的node_modules
中找/src/node_modules/
/node_modules/
直到最後找不到 結束
require wrapper
Node
的模組 實際上可以理解為程式碼被包裹在一個函式包裝器
內
一個簡單的require demo
:
function wrapper (script) {
return '(function (exports, require, module, __filename, __dirname) {' +
script +
'\n})'
}
function require(id) {
var cachedModule = Module._cache[id];
if(cachedModule){
return cachedModule.exports;
}
const module = { exports: {} }
// 這裡先將引用加入快取 後面迴圈引用會說到
Module._cache[id] = module
//當然不是eval這麼簡單
eval(wrapper('module.exports = "123"'))(module.exports, require, module, 'filename', 'dirname')
return module.exports
}
複製程式碼
也可以檢視:node module 原始碼 從以上程式碼我們可以知道:
- 模組只執行一次 之後呼叫獲取的
module.exports
都是快取哪怕這個js
還沒執行完畢(因為先加入快取後執行模組) - 模組匯出就是
return
這個變數的其實跟a = b
賦值一樣, 基本型別匯出的是值, 引用型別匯出的是引用地址 exports
和module.exports
持有相同引用,因為最後匯出的是module.exports
, 所以對exports
進行賦值會導致exports
操作的不再是module.exports
的引用
迴圈引用
// a.js
module.exports.a = 1
var b = require('./b')
console.log(b)
module.exports.a = 2
// b.js
module.exports.b = 11
var a = require('./a')
console.log(a)
module.exports.b = 22
//main.js
var a = require('./a')
console.log(a)
複製程式碼
執行此段程式碼結合上面的require demo
,分析每一步過程:
執行 node main.js -> 第一行 require(a.js)
,(node
執行也可以理解為呼叫了require方法,我們省略require(main.js)
內容)進入 require(a)方法: 判斷快取(無) -> 初始化一個 module -> 將 module 加入快取 -> 執行模組 a.js 內容
,(需要注意 是先加入快取, 後執行模組內容)a.js: 第一行匯出 a = 1 -> 第二行 require(b.js)
(a 只執行了第一行)進入 require(b) 內 同 1 -> 執行模組 b.js 內容
b.js: 第一行 b = 11 -> 第二行 require(a.js)
require(a) 此時 a.js 是第二次呼叫 require -> 判斷快取(有)-> cachedModule.exports -> 回到 b.js
(因為js
物件引用問題 此時的cachedModule.exports = { a: 1 }
)b.js:第三行 輸出 { a: 1 } -> 第四行 修改 b = 22 -> 執行完畢回到 a.js
a.js:第二行 require 完畢 獲取到 b -> 第三行 輸出 { b: 22 } -> 第四行 匯出 a = 2 -> 執行完畢回到 main.js
main.js:獲取 a -> 第二行 輸出 { a: 2 } -> 執行完畢
以上就是node
的module
模組解析和執行的大致規則
es6 module
ES6
之前 javascript
一直沒有屬於自己的模組規範,所以社群制定了 CommonJs
規範, Node
從 Commonjs
規範中借鑑了思想於是有了 Node
的 module
,而 AMD 非同步模組
也同樣脫胎於 Commonjs
規範,之後有了執行在瀏覽器上的 require.js
es6 module
基本語法:
export
export * from 'module'; //重定向匯出 不包括 module內的default
export { name1, name2, ..., nameN } from 'module'; // 重定向命名匯出
export { import1 as name1, import2 as name2, ..., nameN } from 'module'; // 重定向重新命名匯出
export { name1, name2, …, nameN }; // 與之前宣告的變數名繫結 命名匯出
export { variable1 as name1, variable2 as name2, …, nameN }; // 重新命名匯出
export let name1 = 'name1'; // 宣告命名匯出 或者 var, const,function, function*, class
export default expression; // 預設匯出
export default function () { ... } // 或者 function*, class
export default function name1() { ... } // 或者 function*, class
export { name1 as default, ... }; // 重新命名為預設匯出
複製程式碼
export
規則
export * from ''
或者export {} from ''
,重定向匯出,重定向的命名並不能在本模組使用,只是搭建一個橋樑,例如:這個a
並不能在本模組內使用export {}
, 與變數名繫結,命名匯出export Declaration
,宣告的同時,命名匯出, Declaration就是:var
,let
,const
,function
,function*
,class
這一類的宣告語句export default AssignmentExpression
,預設匯出, AssignmentExpression的 範圍很廣,可以大致理解 為除了宣告Declaration
(其實兩者是有交叉的),a=2
,i++
,i/4
,a===b
,obj[name]
,name in obj
,func()
,new P()
,[1,2,3]
,function(){}
等等很多
import
// 命名匯出 module.js
let a = 1,b = 2
export { a, b }
export let c = 3
// 命名匯入 main.js
import { a, b, c } from 'module'; // a: 1 b: 2 c: 3
import { a as newA, b, c as newC } from 'module'; // newA: 1 b: 2 newC: 3
// 預設匯出 module.js
export default 1
// 預設匯入 main.js
import defaultExport from 'module'; // defaultExport: 1
// 混合匯出 module.js
let a = 1
export { a }
const b = 2
export { b }
export let c = 3
export default [1, 2, 3]
// 混合匯入 main.js
import defaultExport, { a, b, c as newC} from 'module'; //defaultExport: [1, 2, 3] a: 1 b: 2 newC: 3
import defaultExport, * as name from 'module'; //defaultExport: [1, 2, 3] name: { a: 1, b: 2, c: 3 }
import * as name from 'module'; // name: { a: 1, b: 2, c: 3, default: [1, 2, 3] }
// module.js
Array.prototype.remove = function(){}
//副作用 只執行一個模組
import 'module'; // 執行module 不匯出值 多次呼叫module.js只執行一次
//動態匯入(非同步匯入)
var promise = import('module');
複製程式碼
import
規則
import { } from 'module'
, 匯入module.js
的命名匯出import defaultExport from 'module'
, 匯入module.js
的預設匯出import * as name from 'module'
, 將module.js的
的所有匯出合併為name
的物件,key
為匯出的命名,預設匯出的key
為default
import 'module'
,副作用,只是執行module
,不為了匯出內容例如 polyfill,多次呼叫次語句只能執行一次import('module')
,動態匯入返回一個Promise
,TC39
的stage-3
階段被提出 tc39 import
ES6 module
特點
ES6 module
的語法是靜態的
import
會自動提升到程式碼的頂層
export
和 import
只能出現在程式碼的頂層,下面這段語法是錯誤的
//if for while 等都無法使用
{
export let a = 1
import defaultExport from 'module'
}
true || export let a = 1
複製程式碼
import
的匯入名不能為字串或在判斷語句,下面程式碼是錯誤的
import 'defaultExport' from 'module'
let name = 'Export'
import 'default' + name from 'module'
複製程式碼
靜態的語法意味著可以在編譯時確定匯入和匯出,更加快速的查詢依賴,可以使用lint
工具對模組依賴進行檢查,可以對匯入匯出加上型別資訊進行靜態的型別檢查
####ES6 module
的匯出是繫結的 ####
使用 import
被匯入的模組執行在嚴格模式下
使用 import
被匯入的變數是只讀的,可以理解預設為 const
裝飾,無法被賦值
使用 import
被匯入的變數是與原變數繫結/引用的,可以理解為 import
匯入的變數無論是否為基本型別都是引用傳遞
// js中 基礎型別是值傳遞
let a = 1
let b = a
b = 2
console.log(a,b) //1 2
// js中 引用型別是引用傳遞
let obj = {name:'obj'}
let obj2 = obj
obj2.name = 'obj2'
console.log(obj.name, obj2.name) // obj2 obj2
// es6 module 中基本型別也按引用傳遞
// foo.js
export let a = 1
export function count(){
a++
}
// main.js
import { a, count } from './foo'
console.log(a) //1
count()
console.log(a) //2
// export default 是無法 a 的動態繫結 這一點跟 CommonJs 有點相似 都是值的拷貝
let a = 1;
export default a
// 可以用另一種方式實現 default 的動態繫結
let a = 1;
export { a as default }
export function count(){
a++
}
// 就跟上面 main.js 一樣
複製程式碼
上面這段程式碼就是 CommonJs
匯出變數 和 ES6
匯出變數的區別
es module 迴圈引用
// bar.js
import { foo } from './foo'
console.log(foo);
export let bar = 'bar'
// foo.js
import { bar } from './bar'
console.log(bar);
export let foo = 'foo'
// main.js
import { bar } from './bar'
console.log(bar)
複製程式碼
執行 main.js -> 匯入 bar.js
bar.js -> 匯入 foo.js
foo.js -> 匯入 bar.js -> bar.js 已經執行過直接返回 -> 輸出 bar -> bar is not defined, bar 未定義報錯
我們可以使用function
的方式解決:
// bar.js
import { foo } from './foo'
console.log(foo());
export function bar(){
return 'bar'
}
// foo.js
import { bar } from './bar'
console.log(bar());
export function foo(){
return 'foo'
}
// main.js
import { bar } from './bar'
console.log(bar)
複製程式碼
因為函式宣告會提示到檔案頂部,所以就可以直接在 foo.js
呼叫還沒執行完畢的bar.js
的 bar
方法,不要在函式內使用外部變數,因為變數還未宣告(let,const
)和賦值,var
CommonJs 和 ES6 Module 的區別
其實上面我們已經說到了一些區別
CommonJs
匯出的是變數的一份拷貝,ES6 Module
匯出的是變數的繫結(export default
是特殊的)CommonJs
是單個值匯出,ES6 Module
可以匯出多個CommonJs
是動態語法可以寫在判斷裡,ES6 Module
靜態語法只能寫在頂層CommonJs
的this
是當前模組,ES6 Module
的this
是undefined
易混淆點
模組語法與解構
module語法
與解構語法
很容易混淆,例如:
import { a } from 'module'
const { a } = require('module')
複製程式碼
儘管看上去很像,但是不是同一個東西,這是兩種完全不一樣的語法與作用,ps:兩個人撞衫了,穿一樣的衣服你不能說這倆人就是同一個人
module
的語法: 上面有寫 import/export { a } / { a, b } / { a as c} FromClause
解構
的語法:
let { a } = { a: 1 }
let { a = 2 } = { }
let { a: b } = { a: 1 }
let { a: b = 2, ...res } = { name:'a' }
let { a: b, obj: { name } } = { a: 1, obj: { name: '1' } }
function foo({a: []}) {}
複製程式碼
他們是差別非常大的兩個東西,一個是模組匯入匯出,一個是獲取物件的語法糖
匯出語法與物件屬性簡寫
同樣下面這段程式碼也容易混淆
let a = 1
export { a } // 匯出語法
export default { a } // 屬性簡寫 匯出 { a: 1 } 物件
module.exports = { a } // 屬性簡寫 匯出 { a: 1 } 物件
複製程式碼
export default
和 module.exports
是相似的
ES6 module 支援 CommonJs 情況
先簡單說一下各個環境的 ES6 module
支援 CommonJs
情況,後面單獨說如何在不同環境中使用
因為 module.exports
很像 export default
所以 ES6模組
可以很方便相容 CommonJs
在ES6 module
中使用CommonJs
規範,根據各個環境,打包工具不同也是不一樣的
我們現在大多使用的是 webpack
進行專案構建打包,因為現在前端開發環境都是在 Node
環境原因,而 npm
的包都是 CommonJs
規範的,所以 webpack
對ES6
模組進行擴充套件 支援 CommonJs
,並支援node
的匯入npm
包的規範
如果你使用 rollup
,想在ES Module
中支援Commonjs
規範就需要下載rollup-plugin-commonjs
外掛,想要匯入node_modules
下的包也需要rollup-plugin-node-resolve
外掛
如果你使用 node
,可以在 .mjs
檔案使用 ES6
,也支援 CommonJs
檢視 nodejs es-modules.md
在瀏覽器環境 不支援CommonJs
node 與 打包工具webpack,rollup
的匯入 CommonJs
差異
// module.js
module.export.a = 1
// index.js webpack rollup
import * as a from './module'
console.log(a) // { a: 1, default: { a:1 } }
// index.mjs node
import * as a from './module'
console.log(a) // { default: { a:1 } }
複製程式碼
node
只是把 module.exports
整體當做 export default
打包工具除了把 module.export
整體當做 export default
,還把 module.export
的每一項 又當做 export
輸出,這樣做是為了更加簡潔
import defaultExport from './foo'
, defaultExport.foo()
import { foo } from './foo'
, foo()
使用 ES6 Module
可以在 es6module example 倉庫中獲取程式碼在本地進行測試驗證
瀏覽器中使用
你需要起一個Web伺服器
來訪問,雙擊本地執行 index.html
並不會執行 type=module
標籤
我們可以對 script
標籤的 type
屬性加上 module
先定義兩個模組
// index.js
import module from './module.js'
console.log(module) // 123
// module.js
export default 123
複製程式碼
在html
中內聯呼叫
<!-- index.html -->
<script type="module">
import module from './module.js'
console.log(module) // 123
</script>
複製程式碼
在html
中通過 script
的 src
引用
<!-- index.html -->
<script type="module" src="index.js"></script>
// 控制檯 123
複製程式碼
瀏覽器匯入路徑規則
https://example.com/apples.mjs
http://example.com/apples.js
//example.com/bananas
./strawberries.mjs.cgi
../lychees
/limes.jsx
data:text/javascript,export default 'grapes';
blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f
補充:
- 不加 字尾名 找不到具體的檔案
- 後端可以修改介面
/getjs?name=module
這一類的,不過後端要返回Content-Type: application/javascript
確保返回的是js
,因為瀏覽器是根據MIME type
識別的
因為 ES6 Module
在瀏覽器中相容並不是很好相容性表,這裡就不介紹瀏覽器支援情況了,我們一般不會直接在瀏覽器中使用
Nodejs中使用
在 Node v8.5.0
以上支援 ES Module
,需要 .mjs
副檔名
NOTE: DRAFT status does not mean ESM will be implemented in Node core. Instead that this is the standard, should Node core decide to implement ESM. At which time this draft would be moved to ACCEPTED. (上面連結可以知道
ES Module
的狀態是DRAFT
, 屬於起草階段)
// module.mjs
export default 123
// index.mjs
import module from './module.mjs'
console.log(module) // 123
複製程式碼
我們需要執行 node --experimental-modules index.mjs
來啟動
會提示一個 ExperimentalWarning: The ESM module loader is experimental.
該功能是實驗性的(此提示不影響執行)
ES Module
中匯入 CommonJs
// module.js
module.exports.a = 123 // module.exports 就相當於 export default
// index.mjs
import module from './module.js'
console.log(module) // { a: 123 }
import * as module from './module.js'
console.log(module) // { get default: { a: 123 } }
import { default as module } from './module.js';
console.log(module) // { a: 123 }
import module from 'module'; // 匯入npm包 匯入規則與 require 差不多
複製程式碼
匯入路徑規則與require
差不多
這裡要注意 module
副檔名為 .js
,.mjs
專屬於 es module
,import form
匯入的檔案字尾名只能是.mjs
,在 .mjs
中 module
未定義, 所以呼叫 module.exports,exports
會報錯
node
中 CommonJs
匯入 es module
只能使用 import()
動態匯入/非同步匯入
// es.mjs
let foo = {name: 'foo'};
export default foo;
export let a = 1
// cjs
import('./es').then((res)=>{
console.log(res) // { get default: {name: 'foo'}, a: 1 }
});
複製程式碼
webpack中使用
從 webpack2
就預設支援 es module
了,並預設支援 CommonJs
,支援匯入 npm
包, 這裡 import
語法上面寫太多 就不再寫了
rollup中使用
rollup
專注於 es module
,可以將 es module
打包為主流的模組規範,注意這裡與 webpack
的區別,我們可以在 webpack
的 js
中使用 Commonjs
語法, 但是 rollup
不支援,rollup
需要 plugin
支援,包括載入 node_modules
下的包 form 'react'
也需要 plugin
支援
可以看到 es module
在瀏覽器
與node
中相容性差與實驗功能的
我們大多時候在 打包工具 中使用
Tree-shaking
在最後我們說一下經常跟 es module
一起出現的一個名詞 Tree-shaking
Tree-shaking
我們先直譯一下 樹木搖晃 就是 搖晃樹木把上面枯死的樹葉晃下來,在程式碼中就是把沒有用到的程式碼刪除
Tree-shaking
最早由 rollup
提出,之後 webpack 2
也開始支援
這都是基於 es module
模組特性的靜態分析
rollup
下面程式碼使用 rollup
進行打包:
// module.js
export let foo = 'foo'
export let bar = 'bar'
// index.js
import { foo } from './module'
console.log(foo) // foo
複製程式碼
線上執行 我們可以修改例子與匯出多種規範
打包結果:
let foo = 'foo';
console.log(foo); // foo
複製程式碼
可以看到 rollup
打包結果非常的簡潔,並去掉了沒有用到的 bar
是否支援對匯入 CommonJs
的規範進行 Tree-shaking
:
// index.js
import { a } from './module'
console.log(a) // 1
// module.js
module.exports.a = 1
module.exports.b = 2
複製程式碼
打包為 es module
var a_1 = 2;
console.log(a_1);
複製程式碼
可以看到去掉了未使用的 b
webpack
我們下面看看 webpack
的支援情況
// src/module.js
export function foo(){ return 'foo' }
export function bar(){ return 'bar' }
// src/index.js
import { foo } from './module'
console.log(foo())
複製程式碼
執行 npx webpack -p
(我們使用webpack 4,0配置,-p開啟生成模式 自動壓縮)
打包後我們在打包檔案搜尋 bar
沒有搜到,bar
被刪除
我們將上面例子修改一下:
// src/module.js
module.exports.foo = function (){ return 'foo' }
module.exports.bar = function (){ return 'bar' }
// src/index.js
import { foo } from './module'
console.log(foo())
複製程式碼
打包後搜尋 bar
發現bar
存在,webpack
並不支援對CommonJs
進行 Tree-shaking
pkg.module
webpack
不支援 Commonjs
Tree-shaking
,但現在npm
的包都是CommonJs
規範的,這該怎麼辦呢 ?如果我發了一個新包是 es module
規範, 但是如果程式碼執行在 node
環境,沒有經過打包 就會報錯
有一種按需載入的方案
全路徑匯入,匯入具體的檔案:
// src/index.js
import remove from 'lodash/remove'
import add from 'lodash/add'
console.log(remove(), add())
複製程式碼
使用一個還好,如果用多個的話會有很多 import
語句
還可以使用外掛如 babel-plugin-lodash, & lodash-webpack-plugin
但我們不能發一個庫就自己寫外掛
這時就提出了在 package.json
加一個 module
的欄位來指向 es module
規範的檔案,main -> CommonJs
,那麼module - es module
pkg.module
webpack
與 rollup
都支援 pkg.module
加了 module
欄位 webpack
就可以識別我們的 es module
,但是還有一個問題就是 babel
我們一般使用 babel
都會排除 node_modules
,所以我們這個 pkg.module
只是的 es6 module
必須是編譯之後的 es5
程式碼,因為 babel
不會幫我們編譯,我們的包就必須是 擁有 es6 module 規範的 es5 程式碼
如果你使用了 presets-env
因為會把我們的程式碼轉為 CommonJs
所以就要設定 "presets": [["env", {"modules":false}]
不將es module
轉為 CommonJs
webpack
與 rollup
的區別
webpack
不支援匯出es6 module
規範,rollup
支援匯出es6 module
webpack
打包後程式碼很多冗餘無法直接看,rollup
打包後的程式碼簡潔,可讀,像原始碼webpack
可以進行程式碼分割,靜態資源處理,HRM
,rollup
專注於es module
,tree-shaking
更加強大的,精簡
如果是開發應用可以使用 webpack
,因為可以進行程式碼分割,靜態資源,HRM
,外掛
如果是開發類似 vue
,react
等類庫,rollup
更好一些,因為可以使你的程式碼精簡,無冗餘程式碼,執行更快,匯出多種模組語法
結語
本文章介紹了 Commonjs
和 ES6 Module
,匯入匯出的語法規則,路徑解析規則,兩者的區別,容易混淆的地方,在不同環境的區別,在不同環境的使用,Tree-shaking
,與 webpack
,rollup
的區別
希望您讀完文章後,能對前端的模組化有更深的瞭解