我他喵的到底要怎樣才能在生產環境中用上 ES6 模組化?

blessing.studio發表於2017-07-08

  Python3 已經發布了九年了,Python 社群卻還在用 Python 2.7;而 JavaScript 社群正好相反,大家都已經開始把還沒有實現的語言特性用到生產環境中了 (´_ゝ `)

  雖然這種奇妙情況的形成與 JavaScript 自身早期的設計缺陷以及瀏覽器平臺的特殊性質都有關係,但也確實能夠體現出 JavaScript 社群的技術棧迭代是有多麼屌快。如果你昏迷個一年半載再去看前端圈,可能社群的主流技術棧已經變得它媽都不認識了(如果你沒什麼實感,可以看看《在 2016 年學習 JavaScript 是一種怎樣的體驗》這篇文章,你會感受到的,你會的)。

 JavaScript 模組化現狀

  隨著 JavaScript 越來越廣泛的應用,朝著單頁應用(SPA)方向發展的網頁與程式碼量的愈發龐大,社群需要一種更好的程式碼組織形式,這就是模組化:將你的一大坨程式碼分裝為多個不同的模組。

  但是在 ES6 標準出臺之前,由於標準的缺失(連 CSS 都有 @import,JavaScript 卻連個毛線都沒),這幾年裡 JavaScript 社群裡冒出了各種各樣的模組化解決方案(群魔亂舞),懵到一種極致。主要的幾種模組化方案舉例如下:

  CommonJS

  主要用於服務端,模組同步載入(也因此不適合在瀏覽器中執行,不過也有 Browserify 之類的轉換工具),Node.js 的模組化實現就是基於 CommonJS 規範的,通常用法像這樣:

// index.js
const {bullshit} = require('./bullshit');
console.log(bullshit());

// bullshit.js
function someBullshit() {
  return "hafu hafu";
}

modules.export = {
  bullshit: someBullshit
};

  而且 require() 是動態載入模組的,完全就是模組中 modules.export 變數的傳送門,這也就意味著更好的靈活性(按條件載入模組,引數可為表示式 etc.)。

  AMD

  即非同步模組定義(Asynchronous Module Definition),不是那個日常翻身的農企啦。

  主要用於瀏覽器端,模組非同步載入(還是用的回撥函式),可以給模組注入依賴、動態載入程式碼塊等。具體實現有 RequireJS,程式碼大概長這樣:

// index.js
require(['bullshit'], words => {
  console.log(words.bullshit());
});

// bullshit.js
define('bullshit', ['dep1', 'dep2'], (dep1, dep2) => {
  function someBullshit() {
    return "hafu hafu";
  }

  return { bullshit: someBullshit };
});

  可惜不能在 Node.js 中直接使用,而且模組定義與載入也比較冗長。

  ES6 Module?

  在 ES6 模組標準出來之前,主要的模組化方案就是上述 CommonJS 和 AMD 兩種了,一種用於伺服器,一種用於瀏覽器。其他的規範還有:

  • 最古老的 IIFE(立即執行函式);

  • CMD(Common Module Definition,和 AMD 挺像的,可以參考:與 RequireJS 的異同);

  • UMD(Universal Module Definition,相容 AMD 和 CommonJS 的語法糖規範);

  等等,這裡就按下不表。

  ES6 的模組化程式碼大概長這樣:

// index.js
import {bullshit} from './bullshit';
console.log(bullshit());

// bullshit.js
function someBullshit() {
  return "hafu hafu";
}

export {
  someBullshit as bullshit
};

  那我們為啥應該使用 ES6 的模組化規範呢?

  • 這是 ECMAScript 官方標準(嗯);

  • 語義化的語法,清晰明瞭,同時支援伺服器端和瀏覽器;

  • 靜態 / 編譯時載入(與上面倆規範的動態 / 執行時載入不同),可以做靜態優化(比如下面提到的 tree-shaking),載入效率高(不過相應地靈活性也降低了,期待 import() 也成為規範);

  • 輸出的是值的引用,可動態修改;

  嗯,你說的都對,那我tm到底要怎樣才能在生產環境中用上 ES6 的模組化特性呢?

  很遺憾,你永遠無法控制使用者的瀏覽器版本,可能要等上一萬年,你才能直接在生產環境中寫 ES6 而不用提心吊膽地擔心相容性問題。因此,你還是需要各種各樣雜七雜八的工具來轉換你的程式碼:Babel、Webpack、Browserify、Gulp、Rollup.js、System.js ……

  噢,我可去你媽的吧,這些東西都tm是幹嘛的?我就是想用個模組化,我到底該用啥子?

 

  本文正旨在列出幾種可用的在生產環境中放心使用 ES6 模組化的方法,希望能幫到諸位後來者(這方面的中文資源實在是忒少了)。

 問題分析

  想要開心地寫 ES6 的模組化程式碼,首先你需要一個轉譯器(Transpiler)來把你的 ES6 程式碼轉換成大部分瀏覽器都支援的 ES5 程式碼。這裡我們就選用最多人用的 Babel(我不久之前才知道原來 Babel 就是巴別塔裡的「巴別」……)。

  用了 Babel 後,我們的 ES6 模組化程式碼會被轉換為 ES5 + CommonJS 模組規範的程式碼,這倒也沒什麼,畢竟我們寫的還是 ES6 的模組,至於編譯生成的結果,管它是個什麼屌東西呢(笑)

  所以我們需要另外一個打包工具來將我們的模組依賴給打包成一個 bundle 檔案。目前來說,依賴打包應該是最好的方法了。不然,你也可以等上一萬年,等你的使用者把瀏覽器升級到全部支援 HTTP/2(支援連線複用後模組不打包反而比較好)以及 <script type="module" src="fuck.js"> 定義 ( ゚∀。)

  所以我們整個工具鏈應該是這樣的:

 

  而目前來看,主要可用的模組打包工具有這麼幾個:

  • Browserify

  • Webpack

  • Rollup.js

  本來我還想講一下 FIS3 的,結果去看了一下,人家竟然還沒原生的支援 ES6 Modules,而且 fis3-hook-commonjs 外掛也幾萬年沒更新了,所以還是算了吧。至於 SystemJS 這類動態模組載入器本文也不會涉及,就像我上面說的一樣,在目前這個時間點上還是先用模組打包工具比較好。

  下面分別介紹這幾個工具以及如何使用它們配合 Babel 實現 ES6 模組轉譯。

 Browserify

  Browserify 這個工具也是有些年頭了,它通過打包所有的依賴來讓你能夠在瀏覽器中使用 CommonJS 的語法來 require('modules'),這樣你就可以像在 Node.js 中一樣在瀏覽器中使用 npm 包了,可以爽到。而且我也很喜歡 Browserify 這個 LOGO

 

  既然 Babel 會把我們的 ES6 Modules 語法轉換成 ES5 + CommonJS 規範的模組語法,那我們就可以直接用 Browserify 來解析 Babel 的轉譯生成物,然後把所有的依賴給打包成一個檔案,豈不是美滋滋。

  不過除了 Babel 和 Browserify 這倆工具外,我們還需要一個叫做 babelify 的東西……好吧好吧,這是最後一個了,真的。

  那麼,babelify 是拿來幹嘛的呢?因為 Browserify 只看得懂 CommonJS 的模組程式碼,所以我們得把 ES6 模組程式碼轉換成 CommonJS 規範的,再拿給 Browserify 去看:這一步就是 Babel 要乾的事情了。但是 Browserify 人家是個模組打包工具啊,它是要去分析 AST(抽象語法樹),把那些 reuqire() 的依賴檔案給找出來再幫你打包的,你總不能把所有的原始檔都給 Babel 轉譯了再交給 Browserify 吧?那太蠢了,我的朋友。

  babelify (Browserify transform for Babel) 要做的事情,就是在所有 ES6 檔案拿給 Browserify 看之前,先把它用 Babel 給轉譯一下(browserify().transform),這樣 Browserify 就可以直接看得懂並打包依賴,避免了要用 Babel 先轉譯一萬個檔案的尷尬局面。

  好吧,那我們要怎樣把這些工具搗鼓成一個完整的工具鏈呢?下面就是喜聞樂見的依賴包安裝環節:

# 我用的 yarn,你用 npm 也差不多
# gulp 也可以全域性安裝,方便一點
# babel-preset 記得選適合自己的
# 最後那倆是用來配合 gulp stream 的
$ yarn add --dev babel-cli babel-preset-env babelify browserify gulp vinyl-buffer vinyl-source-stream

  這裡我們用 Gulp 作為任務管理工具來實現自動化(什麼,都 7012 年了你還不知道 Gulp?那為什麼不去問問神奇海螺呢?),gulpfile.js 內容如下:

var gulp       = require('gulp'),
    browserify = require('browserify'),
    babelify   = require('babelify'),
    source     = require('vinyl-source-stream'),
    buffer     = require('vinyl-buffer');

gulp.task('build', function () {
    return browserify(['./src/index.js'])
        .transform(babelify)
        .bundle()
        .pipe(source('bundle.js'))
        .pipe(gulp.dest('dist'))
        .pipe(buffer());
});

  相信諸位都能看得懂吧,browserify() 第一個引數是入口檔案,可以是陣列或者其他亂七八糟的,具體引數說明請自行參照 Browserify 文件。而且記得在根目錄下建立 .babelrc 檔案指定轉譯的 preset,或者在 gulpfile.js 中配置也可以,這裡就不再贅述。

  最後執行 gulp build,就可以生成能直接在瀏覽器中執行的打包檔案了。

➜  browserify $ gulp build
[12:12:01] Using gulpfile E:\wwwroot\es6-module-test\browserify\gulpfile.js
[12:12:01] Starting 'build'...
[12:12:01] Finished 'build' after 720 ms

 Rollup.js

  我記得這玩意最開始出來的時候號稱為「下一代的模組打包工具」,並且自帶了可大大減小打包體積的 tree-shaking 技術(DCE 無用程式碼移除的一種,運用了 ES6 靜態分析語法樹的特性,只打包那些用到了的程式碼),在當時很新鮮。

 

  但是現在 Webpack2+ 已經支援了 Tree Shaking 的情況下,我們又有什麼特別的理由去使用 Rollup.js 呢?不過畢竟也是一種可行的方法,這裡也提一提:

# 我也不知道為啥 Rollup.js 要依賴這個 external-helpers
$ yarn add --dev rollup rollup-plugin-babel babel-preset-env babel-plugin-external-helpers

  然後修改根目錄下的 rollup.config.js:

import babel from 'rollup-plugin-babel';

export default {
  entry: 'src/index.js',
  format: 'esm',
  plugins: [
    babel({
      exclude: 'node_modules/**'
    })
  ],
  dest: 'dist/bundle.js'
};

  還要修改 .babelrc 檔案,把 Babel 轉換 ES6 模組到 CommonJS 模組的轉換給關掉,不然會導致 Rollup.js 處理不來:

{
  "presets": [
    ["env", {
      "modules": false
    }]
  ],
  "plugins": [
    "external-helpers"
  ]
}

  然後在根目錄下執行 rollup -c 即可打包依賴,也可以配合 Gulp 來使用,官方文件裡就有,這裡就不贅述了。可以看到,Tree Shaking 的效果還是很顯著的,經測試,未使用的程式碼確實不會被打包進去,比起上面幾個工具生成的結果要清爽多了:

 

 Webpack

  對,Webpack,就是那個喪心病狂想要把啥玩意都給模組化的模組打包工具。既然人家已經到了 3.0.0 版本了,所以下面的都是基於 Webpack3 的。什麼?現在還有搞前端的不知道 Webpack?神奇海螺以下略。

 

  喜聞樂見的依賴安裝環節:

# webpack 也可以全域性安裝,方便一些
$ yarn add --dev babel-loader babel-core babel-preset-env webpack

  然後配置 webpack.config.js:

var path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['env']
          }
        }
      }
    ]
  }
};

  差不多就是這麼個配置,babel-loader 的其他 options 請參照文件,而且這個配置檔案的括號巢狀也是說不出話,ZTMJLWC。

  然後執行 webpack:

➜  webpack $ webpack
Hash: 5c326572cf1440dbdf64
Version: webpack 3.0.0
Time: 1194ms
    Asset     Size  Chunks             Chunk Names
bundle.js  2.86 kB       0  [emitted]  main
   [0] ./src/index.js 106 bytes {0} [built]
   [1] ./src/bullshit.js 178 bytes {0} [built]

  情況呢就是這麼個情況:

 

Tips: 關於 Webpack 的 Tree Shaking

Webpack 現在是自帶 Tree-Shaking 的,不過需要你把 Babel 預設的轉換 ES6 模組至 CommonJS 格式給關掉,就像上面 Rollup.js 那樣在 .babelrc 中新增個 "modules": false。原因的話上面也提到過,tree-shaking 是基於 ES6 模組的靜態語法分析的,如果交給 Webpack 的是已經被 Babel 轉換成 CommonJS 的程式碼的話那就沒戲了。

而且 Webpack 自帶的 tree-shaking 只是把沒用到的模組從 export 中去掉而已,之後還要再接一個 UglifyJS 之類的工具把冗餘程式碼幹掉才能達到 Rollup.js 那樣的效果。

  Webpack 也可以配合 Gulp 工作流讓開發更嗨皮,有興趣的可自行研究。目前來看,這三種方案中,我本人更傾向於使用 Webpack,不知道諸君會選用什麼呢?

 寫在後面

  前幾天我在搗鼓 printempw/blessing-skin-server 那坨 shi 一樣 JavaScript 程式碼的模組化的時候,打算試著使用一下 ES6 標準中的模組化方案,並找了 Google 大老師問 ES6 模組轉譯打包相關的資源,找了半天,幾乎沒有什麼像樣的中文資源。全是講 ES6 模組是啥、有多好、為什麼要用之類的,沒幾個是講到底該怎麼在生產環境中使用的(也有可能是我搜尋姿勢不對),說不出話。遂撰此文,希望能幫到後來人。

  且本人水平有限,如果文中有什麼錯誤,歡迎在下方評論區批評指出。

  參考連結

相關文章