我他喵的到底要怎樣才能在生產環境中用上 ES6 模組化?
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 模組是啥、有多好、為什麼要用之類的,沒幾個是講到底該怎麼在生產環境中使用的(也有可能是我搜尋姿勢不對),說不出話。遂撰此文,希望能幫到後來人。
且本人水平有限,如果文中有什麼錯誤,歡迎在下方評論區批評指出。
參考連結
相關文章
- Java列舉類在生產環境中的使用方式Java
- 如何建立 Angular library 並在生產環境中消費Angular
- 在生產環境使用Docker部署應用Docker
- [譯] this(他喵的)到底是什麼 — 理解 JavaScript 中的 this、call、apply 和 bindJavaScriptAPP
- JeecgBoot 如何在生產環境關閉 Swagger 文件bootSwagger
- 單例模式在生產環境jedis叢集中的應用單例模式
- ForkJoinPool在生產環境中使用遇到的一個問題
- 這一次,我要弄懂javascript的模組化JavaScript
- ES6 - 模組化
- ES6模組化
- selenium模組,web自動化,環境配置Web
- 寫害羞的程式碼才能模組化
- 在生產環境中使用預寫日誌WAL的SQLite - victoriaSQLite
- ES6 模組化與 CommonJS 模組化區別JS
- ES6的模組化語法
- 關於前端模組化 CommonJS、AMD、CMD、ES6中模組載入前端JS
- 【機器學習】在生產環境使用Kafka構建和部署大規模機器學習機器學習Kafka
- Webpack中的sourcemap以及如何在生產和開發環境中合理的設定sourcemap的型別Web開發環境型別
- 前端ES6模組化,Webpack,前端Web
- 在生產環境中除錯 Angular 應用程式而不顯示源對映除錯Angular
- 【Swagger】2.不在生產環境暴露,可以修改預設地址Swagger
- 出海產品需要多元化?獵豹、赤子城、位元組跳動他們是怎樣做的?
- 我是怎樣讓網站用上HTML5 Manifest網站HTML
- 客戶故事丨熱血青年要怎樣才能創業成功?這事他們琢磨了10年創業
- 如何在生產環境中通過Restful API的方式請求重啟Spring Boot應用?RESTAPISpring Boot
- es6模組化的匯入匯出
- Telescope 在生產環境配置的許可權,還是彈出 403 頁面
- 我們怎樣才能學好資料分析(一)
- ES6與CommonJS中的模組處理JS
- 怎樣安裝python的GPIO模組Python
- 如何一步步在生產環境上部署django和vueDjangoVue
- ES6模組化之export和import的用法ExportImport
- Visual Studio中Es6的開發環境搭建開發環境
- 前端模組化:CommonJS,AMD,CMD,ES6前端JS
- 【深入淺出ES6】模組化Modules
- 你生產環境的 Composer 是這樣嗎?
- JavaScript 中的模組化JavaScript
- 要怎樣才能夠完美的編寫高效能的RPC框架RPC框架
- ES6模組與commonJS模組的差異JS