CommonJS 和 ES6 的模塊標準有什麼區別

目前主流的模塊規範

  • 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是否是一個核心模塊 如pathhttp,優先導入核心模塊
不是核心模塊 會從當前文件的同級目錄的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,分析每一步過程:

  1. 執行 node main.js -> 第一行 require(a.js),(node 執行也可以理解爲調用了require方法,我們省略require(main.js)內容)
  2. 進入 require(a)方法: 判斷緩存(無) -> 初始化一個 module -> 將 module 加入緩存 -> 執行模塊 a.js 內容,(需要注意 是加入緩存, 後執行模塊內容)
  3. a.js: 第一行導出 a = 1 -> 第二行 require(b.js)(a 只執行了第一行)
  4. 進入 require(b) 內 同 1 -> 執行模塊 b.js 內容
  5. b.js: 第一行 b = 11 -> 第二行 require(a.js)
  6. require(a) 此時 a.js 是第二次調用 require -> 判斷緩存(有)-> cachedModule.exports -> 回到 b.js(因爲js對象引用問題 此時的 cachedModule.exports = { a: 1 }
  7. b.js:第三行 輸出 { a: 1 } -> 第四行 修改 b = 22 -> 執行完畢回到 a.js
  8. a.js:第二行 require 完畢 獲取到 b -> 第三行 輸出 { b: 22 } -> 第四行 導出 a = 2 -> 執行完畢回到 main.js
  9. main.js:獲取 a -> 第二行 輸出 { a: 2 } -> 執行完畢

以上就是nodemodule模塊解析和運行的大致規則

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就是: varletconstfunctionfunction*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爲導出的命名,默認導出的keydefault
  • import 'module',副作用,只是運行module,不爲了導出內容例如 polyfill,多次調用次語句只能執行一次
  • import('module'),動態導入返回一個 PromiseTC39stage-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)
  1. 執行 main.js -> 導入 bar.js
  2. bar.js -> 導入 foo.js
  3. 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中使用

nodejs es-modules.md

在 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 moduleimport 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 可以進行代碼分割,靜態資源處理,HRMrollup 專注於 es moduletree-shaking更加強大的,精簡

如果是開發應用可以使用 webpack,因爲可以進行代碼分割,靜態資源,HRM,插件
如果是開發類似 vuereact 等類庫,rollup 更好一些,因爲可以使你的代碼精簡,無冗餘代碼,執行更快,導出多種模塊語法

結語

本文章介紹了 Commonjs 和 ES6 Module,導入導出的語法規則,路徑解析規則,兩者的區別,容易混淆的地方,在不同環境的區別,在不同環境的使用,Tree-shaking,與 webpackrollup 的區別
希望您讀完文章後,能對前端的模塊化有更深的瞭解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章