目前主流的模塊規范
- 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
上面這段代碼就是 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
方法
CommonJs 和 ES6 Module 的區別
其實上面我們已經說到了一些區別
-
CommonJs
導出的是變量的一份拷貝,ES6 Module
導出的是變量的綁定(引用) -
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
中內聯調用
在html
中內聯調用
瀏覽器導入路徑規則
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
的區別
希望您讀完文章后,能對前端的模塊化有更深的了解