原文發表在個人博客上。最近搗鼓了一下 ES6 的模塊化,分享一些經驗 :)javascript
Python3 已經發布了九年了,Python 社區卻還在用 Python 2.7;而 JavaScript 社區正好相反,你們都已經開始把尚未實現的語言特性用到生產環境中了 (´_ゝ `)前端
雖然這種奇妙狀況的造成與 JavaScript 自身早期的設計缺陷以及瀏覽器平臺的特殊性質都有關係,但也確實可以體現出 JavaScript 社區的技術棧迭代是有多麼屌快。若是你昏迷個一年半載再去看前端圈,可能社區的主流技術棧已經變得它媽都不認識了(若是你沒什麼實感,能夠看看《在 2016 年學習 JavaScript 是一種怎樣的體驗》這篇文章,你會感覺到的,你會的)。java
隨着 JavaScript 愈來愈普遍的應用,朝着單頁應用(SPA)方向發展的網頁與代碼量的愈發龐大,社區須要一種更好的代碼組織形式,這就是模塊化:將你的一大坨代碼分裝爲多個不一樣的模塊。node
可是在 ES6 標準出臺以前,因爲標準的缺失(連 CSS 都有 @import
,JavaScript 卻連個毛線都沒),這幾年裏 JavaScript 社區裏冒出了各類各樣的模塊化解決方案(羣魔亂舞),懵到一種極致。主要的幾種模塊化方案舉例以下:webpack
主要用於服務端,模塊同步加載(也所以不適合在瀏覽器中運行,不過也有 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
即異步模塊定義(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 模塊標準出來以前,主要的模塊化方案就是上述 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 這個工具也是有些年頭了,它經過打包全部的依賴來讓你可以在瀏覽器中使用 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
我記得這玩意最開始出來的時候號稱爲「下一代的模塊打包工具」,而且自帶了可大大減少打包體積的 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,就是那個喪心病狂想要把啥玩意都給模塊化的模塊打包工具。既然人家已經到了 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 模塊是啥、有多好、爲何要用之類的,沒幾個是講到底該怎麼在生產環境中使用的(也有多是我搜索姿式不對),說不出話。遂撰此文,但願能幫到後來人。
且本人水平有限,若是文中有什麼錯誤,歡迎在下方評論區批評指出。