目前主流的模塊規範javascript
(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
規範html
Nodejs
環境所使用的模塊系統就是基於CommonJs
規範實現的,咱們如今所說的CommonJs
規範也大可能是指Node
的模塊系統前端
關鍵字:module.exports
exports
vue
// 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
的引用,成爲了一個模塊內的局部變量java
關鍵字:require
node
const foo = require('./foo.js') console.log(foo.age) //1
假設如下目錄爲 src/app/index.js
的文件 調用 require()
react
./moduleA
相對路徑開頭在沒有指定後綴名的狀況下
先去尋找同級目錄同級目錄:src/app/
webpack
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
git
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
結束es6
/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
以前 javascript
一直沒有屬於本身的模塊規範,因此社區制定了 CommonJs
規範, Node
從 Commonjs
規範中借鑑了思想因而有了 Node
的 module
,而 AMD 異步模塊
也一樣脫胎於 Commonjs
規範,以後有了運行在瀏覽器上的 require.js
es6 module
基本語法:
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(){}
等等不少// 命名導出 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
導出變量的區別
// 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
的 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
狀況,後面單獨說如何在不一樣環境中使用
由於 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()
能夠在 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
在瀏覽器中兼容並非很好兼容性表,這裏就不介紹瀏覽器支持狀況了,咱們通常不會直接在瀏覽器中使用
在 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 } });
從 webpack2
就默認支持 es module
了,並默認支持 CommonJs
,支持導入 npm
包, 這裏 import
語法上面寫太多 就再也不寫了
rollup
專一於 es module
,能夠將 es module
打包爲主流的模塊規範,注意這裏與 webpack
的區別,咱們能夠在 webpack
的 js
中使用 Commonjs
語法, 可是 rollup
不支持,rollup
須要 plugin
支持,包括加載 node_modules
下的包 form 'react'
也須要 plugin
支持
能夠看到 es module
在瀏覽器
與node
中兼容性差與實驗功能的
咱們大多時候在 打包工具 中使用
在最後咱們說一下常常跟 es module
一塊兒出現的一個名詞 Tree-shaking
Tree-shaking
咱們先直譯一下 樹木搖晃 就是 搖晃樹木把上面枯死的樹葉晃下來,在代碼中就是把沒有用到的代碼刪除Tree-shaking
最先由 rollup
提出,以後 webpack 2
也開始支持
這都是基於 es module
模塊特性的靜態分析
下面代碼使用 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
的支持狀況
// 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
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
的區別
但願您讀完文章後,能對前端的模塊化有更深的瞭解