Stories

Detail Return Return

前端工程化4:如何去做js模塊化開發?ES Modules/CommonJS有什麼區別? - Stories Detail

1. 如何去做js模塊化開發 => 模塊化標準 + 加載器

1.1、我們説討論的僅限於javascript代碼的模塊化,如果要涉及到所有文件的模塊化請使用webpack。
1.2、那麼js的模塊化可以用一句話概括:模塊化標準 + 加載器;本文主要介紹模塊化標準。

2. 幾種模塊化標準對比:

CommonJS

1、以同步的模式加載模塊:通常在Nodejs環境使用,不適合瀏覽器

1. 因為服務器讀本地磁盤文件會比較快,所以nodejs的執行機制是在啓動的時候加載所有模塊,不需要在執行過程中才加載模塊;
2. 如果在瀏覽器端代碼執行時去同步require很多模塊,也會影響頁面執行效率。

2、一個文件就是一個模塊,且每個模塊都有單獨的作用域;因為模塊輸出的是一個值的淺拷貝。
3、require(模塊)加載的是一個對象,該對象是運行時生成;
4、導入導出

// 導出
module.exports = {
    name: 'wangyi',
    age: 18
}

// 導入
const { name, age } = require('./module.js')

ES Modules

1、以異步的模式加載模塊:支持Nodejs環境使用,也適合瀏覽器(ES6以上才支持的規範,存在兼容性問題,最好配合 webpack 進行加載);
2、ES Modules的導入導出是固定用法,輸出的是值的只讀引用(原始值變了,取值跟着變);
3、ES Modules的導入導出不是對象而是對外接口,該接口在代碼編譯時就完成,執行效率更高
4、導入導出

// 注意:此處是固定用法,export 後面不是對象;export default 後面才是對象

// 導出1
export { name, age }
// 導入1
import { name, age } from './module.js'

// 導出2
export default { name, age }
// 導入2
import module from './module.js'
const { name, age } = module

AMD + require.js

1、以異步的方式加載模塊,可以指定回調函數;
2、AMD規範配合require.js庫作為加載器使用;
3、目前絕大多數第三方庫都支持AMD
4、使用起來比較複雜
5、模塊js文件請求頻繁,因為每個模塊都會創建一個script標籤去請求文件
6、導入導出

define('module1', ['jquery', './module2'], function($, module2){
  return {
    // 可以在裏面使用依賴的 $、module2 模塊
  }
})

CMD + sea.js

CMD規範配合sea.js庫作為加載器使用,實現了模塊化開發(淘寶);後來sea.js被require.js兼容了,便不再使用。

3. 模塊化標準使用最佳實踐

3.1、Nodejs環境 => CommonJS

3.1.1、因為服務器讀本地磁盤文件會比較快,所以 nodejs 的執行機制是在啓動的時候加載所有模塊,不需要在執行過程中才加載模塊;
3.1.2、如果在瀏覽器端代碼執行時去同步 require 很多模塊,也會影響頁面執行效率。
3.1.3、module 對象是在 Nodejs環境定義的,配合 require 函數使用;如果説誰是 CommonJS 的加載器,那就是 Nodejs環境。

3.2、瀏覽器端 => ES Modules + Webpack

3.2.1、因為瀏覽器端會有很多的異步加載且當前的ES6開發比較簡單,所以瀏覽器端適合使用ES Modules。
3.2.2、ES Modules 通常經過 webpack + babel 進行轉換;將其轉換成立即執行函數的方式,以此來模仿塊級作用域;(webpack 也支持在源碼中使用 CommonJS 和 ESM 互相導入導出,但一般不用)
3.2.3、因為 webpack 是在 nodejs 環境運行,所以其配置文件通常使用 CommonJS 規範。
3.2.4、因為 ES Modules 通常需要配合打包工具進行使用,所以 webpack 可以算得上它的加載器。

4. ES Module 基本使用知識點

4.1、ES Modules雖然是ES6才出現的規範,但是未來瀏覽器原生支持

4.2、ES Modules支持在script標籤上直接定義使用:
4.2.1. ESM 自動採用嚴格模式,忽略 'use strict'
4.2.2. 每個 ES Module 都是運行在單獨的私有作用域中
4.2.3. ESM 是通過 CORS 的方式請求外部 JS 模塊的
4.2.4. ESM 的 script 標籤會延遲執行腳本,和defer一樣的效果

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>ES Module - 模塊的特性</title>
</head>
<body>
  <!-- 通過給 script 添加 type = module 的屬性,就可以以 ES Module 的標準執行其中的 JS 代碼了 -->
  <script type="module">
    console.log('this is es module')
  </script>

  <!-- 1. ESM 自動採用嚴格模式,忽略 'use strict' -->
  <script type="module">
    console.log(this)
  </script>

  <!-- 2. 每個 ES Module 都是運行在單獨的私有作用域中 -->
  <script type="module">
    var foo = 100
    console.log(foo)
  </script>
  <script type="module">
    console.log(foo)
  </script>

  <!-- 3. ESM 是通過 CORS 的方式請求外部 JS 模塊的 -->
  <!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->

  <!-- 4. ESM 的 script 標籤會延遲執行腳本,和defer一樣的效果 -->
  <script type="module" src="demo.js"></script>
  <p>需要顯示的內容</p>
</body>
</html>

4.3、ES Modules 的 export和 import:

  • 導出:

    • export { } 後面不是一個對象,而是固定用法 { XXX },import { sss } from module也是固定用法,不是解構語法;export default { } 後面才可以跟一個對象、字符串等都行。
    • export 導出的是一個引用關係,而且是隻讀的!不是深拷貝的對象。
var name = 'foo module'

function hello () {
  console.log('hello')
}

class Person {}

export { 
  name as foo, 
  hello, 
  Person 
}
  • 導入(導入路徑必須完整):

    • 文件名稱必須完整,不能省略.js、/index.js;
    • 相對路徑也必須完整,不能省略./;
    • 可以絕對路徑或者完整的url
import不支持動態導入,需要使用import().then()
import { foo, hello, Person } from './module.js'
console.log(name, hello, Person)

// 只加載模塊不提取模塊變量,可以簡寫:import './module.js'
import {} from './module.js'
import './module.js'

// 導入模塊內的全部變量
import * as mod from './module.js'
console.log(mod)

// 動態導入
import('./module.js').then(function (module) {
  console.log(module)
})

4.4、ES Modules 的 export default

  • export default 後面可以跟對象、字符串等類型
  • export 過後可以繼續添加 export default
  • import 的第一個位置默認對應export default 導出的值
// module.js
var name = 'jack'
var age = 18

export { name, age }
console.log('module action')
export default 'default export'
// import.js
// import { name, age, default as title } from './module.js'
// abc 為 export default 導出值的重命名,abc 後面的 { } 不是對象解構,而是固定用法
// import abc from './module.js'
import abc, { name, age } from './module.js'
console.log(name, age, abc)

4.5、ES Modules 的瀏覽器兼容
IE基本不兼容ES Modules

插件 browser-es-module-loader 用於兼容ES Modules(開發階段可用,不建議生產環境使用)
1、在html中直接使用,參考:npm官方地址

// 該方法需要動態的去解析腳本執行ESM,性能差!只能在開發階段使用。
// script加上nomodule 屬性,避免在支持ESM的瀏覽器上執行兩次
// babel-browser-build.js為babel的運行環境(瀏覽器端)
<script nomodule src="dist/babel-browser-build.js"></script>
// ES Modules把代碼讀出來交給babel轉換
<script nomodule src="dist/browser-es-module-loader.js"></script>
 
<!-- script type=module loading -->
<script nomodule  type="module" src="path/to/module.js"></script>

...

2、npm中使用,估計還是作為依賴資源動態解析ESM(不建議使用)

npm install browser-es-module-loader --save-dev

4.6、ES Modules 的NodeJS支持情況(8.5+版本)
NodeJS 8.5以上的版本支持ES Modules,但是還是實驗版本;

// 第一,將文件的擴展名由 .js 改為 .mjs;(nodejs 12.10版本以上不需要修改文件名了)
// 第二,啓動時需要額外添加 `--experimental-modules` 參數;

import { foo, bar } from './module.mjs'

console.log(foo, bar)

// 此時我們也可以通過 esm 加載內置模塊了
import fs from 'fs'
fs.writeFileSync('./foo.txt', 'es module working')

// 也可以直接提取模塊內的成員,內置模塊兼容了 ESM 的提取成員方式
import { writeFileSync } from 'fs'
writeFileSync('./bar.txt', 'es module working')

// 對於第三方的 NPM 模塊也可以通過 esm 加載
import _ from 'lodash'
_.camelCase('ES Module')

// 不支持,因為第三方模塊都是導出默認成員
// import { camelCase } from 'lodash'
// console.log(camelCase('ES Module'))

4.7、ES Modules 和CommonJS相互使用(在NodeJS環境中)

  • ESM 中可用導入CommonJS
  • CommonJS中不能導入ESM
  • CommonJS始終只會導出一個默認成員
  • import不是解構導出對象,只能:import mod from './commonjs.js'

4.8、ES Modules 和CommonJS的差異(在NodeJS環境中)

// nodejs、CommonJS,文件名為:mjs
// 加載模塊函數
console.log(require)

// 模塊對象
console.log(module)

// 導出對象別名
console.log(exports)

// 當前文件的絕對路徑
console.log(__filename)

// 當前文件所在目錄
console.log(__dirname)
// nodejs、ES Modules,文件名為:mjs
// require, module, exports 自然是通過 import 和 export 代替

// __filename 和 __dirname 通過 import 對象的 meta 屬性獲取
// const currentUrl = import.meta.url
// console.log(currentUrl)

// 通過 url 模塊的 fileURLToPath 方法轉換為路徑
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
console.log(__filename)
console.log(__dirname)

5. Webpack 基於所有資源去做模塊化

完整的講解參考後續文章:待續;

5.1、支持新特性語言版本的編譯
5.2、針對javascript模塊化打包
5.3、針對所有資源,例如樣式、圖片、字體等進行模塊化

對於1、2兩點,grunt、gulp等腳手架可以很好的解決,但是無法解決第3點。

6、Rollup:專門針對ES Modules進行打包的輕量化工具

  • webpack 大而全 => 適合做大型應用程序
  • rollup 小而美 => 適合做類庫

7、Parcel:零配置專用打包器,簡單易用

8、參考資料:

ES6-模塊與-CommonJS-模塊的差異

特別鳴謝:拉勾教育前端高薪訓練營

Add a new Comments

Some HTML is okay.