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

原文發表在個人博客上。最近搗鼓了一下 ES6 的模塊化,分享一些經驗 :)javascript

Python3 已經發布了九年了,Python 社區卻還在用 Python 2.7;而 JavaScript 社區正好相反,你們都已經開始把尚未實現的語言特性用到生產環境中了 (´_ゝ `)前端

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

JavaScript 模塊化現狀

隨着 JavaScript 愈來愈普遍的應用,朝着單頁應用(SPA)方向發展的網頁與代碼量的愈發龐大,社區須要一種更好的代碼組織形式,這就是模塊化:將你的一大坨代碼分裝爲多個不一樣的模塊。node

可是在 ES6 標準出臺以前,因爲標準的缺失(連 CSS 都有 @import,JavaScript 卻連個毛線都沒),這幾年裏 JavaScript 社區裏冒出了各類各樣的模塊化解決方案(羣魔亂舞),懵到一種極致。主要的幾種模塊化方案舉例以下:webpack

CommonJS

主要用於服務端,模塊同步加載(也所以不適合在瀏覽器中運行,不過也有 Browserify 之類的轉換工具),Node.js 的模塊化實現就是基於 CommonJS 規範的,一般用法像這樣:git

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

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

modules.export = {
  bullshit: someBullshit
};

並且 require() 是動態加載模塊的,徹底就是模塊中 modules.export 變量的傳送門,這也就意味着更好的靈活性(按條件加載模塊,參數可爲表達式 etc.)。es6

AMD

即異步模塊定義(Asynchronous Module Definition),不是那個平常翻身的農企啦github

主要用於瀏覽器端,模塊異步加載(仍是用的回調函數),能夠給模塊注入依賴、動態加載代碼塊等。具體實現有 RequireJS,代碼大概長這樣:web

// 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 中直接使用,並且模塊定義與加載也比較冗長。shell

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

Browserify

既然 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

Browserify Result

Rollup.js

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

Rollup.js

可是如今 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 的效果仍是很顯著的,經測試,未使用的代碼確實不會被打包進去,比起上面幾個工具生成的結果要清爽多了:

Rollup.js Result

Webpack

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

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]

狀況呢就是這麼個狀況:

Webpack Result

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 模塊是啥、有多好、爲何要用之類的,沒幾個是講到底該怎麼在生產環境中使用的(也有多是我搜索姿式不對),說不出話。遂撰此文,但願能幫到後來人。

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

參考連接

相關文章
相關標籤/搜索