做者:苗典css
目前你們使用最多也是最普遍的應用打包工具就是 webpack 了,除去 webpack 自己已經提供的優化能力(例如,Tree Shaking、Code Splitting 等)以外,咱們還能作哪些事情呢,本篇主要就爲你們介紹下滴滴 WebApp 團隊在這條路上的一些探索。vue
如今愈來愈多的項目都使用 ES2015+ 開發,而且搭配 webpack + babel 做爲工程化基礎,並經過 NPM 去加載第三方依賴庫。同時爲了達到代碼複用的目的,咱們會把一些本身開發的組件庫或者是 JSSDK 抽成獨立的倉庫維護,並經過 NPM 去加載。node
大部分人已經習慣了這樣的開發方式,而且以爲很是方便實用。但在方便的背後,卻隱藏了兩個問題:webpack
代碼冗餘git
通常來講,這些 NPM 包也是基於 ES2015+ 開發的,每一個包都須要通過 babel 編譯發佈後才能被主應用使用,而這個編譯過程每每會附加不少「編譯代碼」;每一個包都會有一些相同的編譯代碼,這就形成大量代碼的冗餘,而且這部分冗餘代碼是不能經過 Tree Shaking 等技術去除掉的。github
非必要的依賴web
考慮到組件庫的場景,一般咱們爲了方便一股腦引入了全部組件;但實際狀況下對於一個應用而言可能只是用到了部分組件,此時若是所有引入,也會形成代碼冗餘。vuex
代碼的冗餘會形成靜態資源包加載時間變長、執行時間也會變長,進而很直接的影響性能和體驗。既然咱們已經認識到有此類問題,那麼接下來看看如何解決這兩個問題。npm
咱們對於上述的 2 個問題,核心的解決優化方案是:後編譯和按需引入。json
先來看下滴滴車票項目(用票人)優化先後的數據(非 gzip,壓縮後整個項目的大小):
最終減小了約 80 KB,優化效果仍是至關可觀的。
上邊的數據主要是對組件庫和一些內部通用 JSSDK 採用後編譯和按需引入策略後的效果,須要注意的是按需引入的效果是要視項目狀況而定的,這裏的數據僅供參考。
下面就分別來看看這兩個點的具體細節。
先來解釋下:
後編譯:指的是應用依賴的 NPM 包並不須要在發佈前編譯,而是隨着應用編譯打包的時候一塊編譯。
後編譯的核心在於把編譯依賴包的時機延後,而且統一編譯;先來看看它的 webpack 配置。
對具體項目應用而言,作到後編譯,其實不須要作太多,只須要在 webpack 的配置文件中,包含須要咱們去後編譯的依賴包便可(webpack 2+):
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
loader: 'babel-loader',
// 注意這裏的 include
// 除了 src 還包含了額外的 node_modules 下的兩個包
include: [
resolve('src'),
resolve('node_modules/A'),
resolve('node_modules/B')
]
},
// ...
]
},
// ...
}複製代碼
咱們只須要把後編譯的模塊 A 和 B 經過 webpack 的 include 配置包含進來便可。
可是這裏會存在一些問題,舉個例子,以下圖:
上述所示的應用中依賴了須要後編譯的包 A 和 B,而 A 又依賴了須要後編譯的包 C 和 D,B 依賴了不須要後編譯的包 E;重點來看依賴包 A 的狀況:A 自己須要後編譯,而後 A 的依賴包 C 和 D 也須要後編譯,這種場景咱們能夠稱之爲嵌套後編譯,此時若是依舊經過上邊的 webpack 配置方式的話,還必需要顯示的去 include 包 C 和 D,但對於應用而言,它只知道自身須要後編譯的包 A 和 B,並不知道 A 也會有須要後編譯的包 C 和 D,因此應用不該該顯示的去 include 包 C 和 D,而是應該由 A 顯示的去聲明本身須要哪些後編譯模塊。
爲了解決上述嵌套後編譯問題,咱們開發了一個 webpack 插件 webpack-post-compile-plugin,用於自動收集後編譯的依賴包以及其嵌套依賴;來看下這個插件的核心代碼:
var util = require('./util')
function PostCompilePlugin (options) {
// ...
}
PostCompilePlugin.prototype.apply = function (compiler) {
var that = this
compiler.plugin(['before-run', 'watch-run'], function (compiler, callback) {
// ...
var dependencies = that._collectCompileDependencies(compiler)
if (dependencies.length) {
var rules = compiler.options.module.rules
rules && rules.forEach(function (rule) {
if (rule.include) {
if (!Array.isArray(rule.include)) {
rule.include = [rule.include]
}
rule.include = rule.include.concat(dependencies)
}
})
}
callback()
})
}複製代碼
原理就是在 webpack compiler 的 before-run
和 watch-run
事件鉤子中去收集依賴而後附加到 webpack module.rule 的 include 上;收集的規則就是查找應用或者依賴包的 package.json 中聲明的 compileDependencies 做爲後編譯依賴。
因此對於上述應用的狀況,使用 webpack-post-compile-plugin 插件的 webpack 配置:
var PostCompilePlugin = require('webpack-post-compile-plugin')
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
loader: 'babel-loader',
include: [
resolve('src')
]
},
// ...
]
},
// ...
plugins: [
new PostCompilePlugin()
]
}複製代碼
當前項目的 package.json 中添加 compileDependencies 字段來指定後編譯依賴包:
// app package.json
{
// ...
"compileDependencies": ["A", "B"]
// ...
}複製代碼
A 還有後編譯依賴,因此須要在包 A 的 package.json 中指定 compileDependencies:
// A package.json
{
// ...
"compileDependencies": ["C", "D"]
// ...
}複製代碼
PS: 關於 babel-plugin-transform-runtime 和 babel-polyfill 的選擇問題,對於應用而言,咱們建議的是採用 babel-polyfill。由於一些第三方包的依賴會判斷全局是否支持某些特性,而不去作 polyfill 處理。例如:vuex 會檢查是否支持 Promise
,若是不支持則會報錯;或者說在代碼中有相似 "foobar".includes("foo")
的代碼的話 babel-plugin-transform-runtime 也是不能正確處理的。
固然,後編譯的技術方案確定不是完美無瑕的,它也會有一些缺點。
雖然有一些缺點,可是綜合考慮到成本/收益,目前來看採用後編譯仍不失爲一種不錯的選擇。
後編譯主要解決的問題是代碼冗餘,而按需引入主要是用來解決非必要的依賴的問題。
按需引入針對的場景主要是組件庫、工具類依賴包。由於不論是組件庫仍是依賴包,每每都是「大而全」的,而在開發應用的時候,咱們可能只是使用了其一部分能力,若是所有引入的話,會有不少資源浪費。
爲了解決這個問題,咱們須要按需引入。目前主流組件庫或者工具包也都是提供按需引入能力的,可是基本都是提供對編譯後模塊引入。
而咱們推薦的是對源碼的按需引入,配合後編譯的打包方案 。
可是實際上咱們可能會遇到一些向後兼容問題,不能一竿子打死,例如以前已經建立的項目,目前沒有人力或者時間去作對應的升級改造,那麼咱們對內的一些組件庫或者工具包目前須要作一點犧牲:提供兩個入口,一個編譯後的入口,一個源碼入口。
這裏涉及到一個 NPM 包有兩個入口的問題,不過還好這個問題 webpack 2+ 或者 rollup 已經幫咱們處理了,即編譯後入口依舊使用 package.json 中的 main 字段,而後源碼的入口使用 module 字段,能夠參見 rollup pkg.module wiki。這樣咱們就能實現兩個入口共享,既能保證向後兼容,又能夠保證使用 webpack 2+ 或者 rollup 的入口直接指向的就是源碼,在這樣的基礎上能夠很直接的利用後編譯了。
後編譯和按需引入一個最最典型的場景就是咱們的組件庫,這裏分享下咱們對於組件庫(基於 Vue)的實踐經驗。
按需引入,在沒有後編譯的時候,其實咱們已經實現了在編譯發佈的時候直接作到自動根據各模塊分別編譯,這樣使用方就能夠直接引入對應目錄的入口文件。這個原理很簡單:遍歷源碼目錄下的模塊目錄,獲得各個入口,動態修改了組件庫 webpack 配置的入口。而這個過程在後編譯場景中就不存在了,能夠直接引入到源碼所對應的模塊入口,由於後編譯不須要依賴包本身編譯,只須要應用去編譯就行了。
對於組件而言,若是是前編譯的話,通常咱們會編譯出入口 JS 文件,以及樣式 CSS 文件,這樣若是來實現按需引入的話,多是這樣的:
import Dialog from 'cube-ui/lib/dialog'
import 'cube-ui/lib/dialog/style.css'複製代碼
即便是在後編譯場景下,雖然不須要處理樣式問題了,可是仍是會遇到按需引入的時候,路徑不夠優雅:
import Dialog from 'cube-ui/src/modules/dialog'複製代碼
以上不論是哪一種,老是不夠優雅,幸虧有一個 babel 插件 babel-plugin-transform-imports 來幫助咱們優雅的按需引入。可是對於咱們編譯後的場景,還須要引入樣式,爲此,咱們對其作了統一,在 babel-plugin-transform-imports 上作了加強的 babel-plugin-transform-modules 插件,增設了 style 配置項。
因此不論是不是使用了後編譯,咱們想要作到按需引入,只須要:
import { Dialog } form 'cube-ui'複製代碼
這樣寫就能夠了,若是你是使用的後編譯,直接引入的是源碼,那麼只須要在 .babelrc 文件中增長以下配置:
"plugins": [
["transform-modules", {
"cube-ui": {
"transform": "cube-ui/src/modules/${member}",
"preventFullImport": true,
"kebabCase": true
}
}]
]複製代碼
而若是是 webpack 1 或者說使用的組件庫是已經編譯後的,那隻須要增設 style
配置項便可:
"plugins": [
["transform-modules", {
"cube-ui": {
"transform": "cube-ui/lib/${member}",
"preventFullImport": true,
"kebabCase": true,
"style": true
}
}]
]複製代碼
這樣咱們就經過一個插件實現了優雅的按需引入,不論是不是使用了後編譯,對於開發者而言只須要修改下 babel 的配置便可,而不須要大肆去修改源碼中的引入路徑。
以上就是咱們基於 webpack 的編譯優化的一點探索,這裏能夠總結下使用 webpack 作應用編譯打包的「最佳實踐」:
後編譯 + 按需引入
再搭配上 babel-preset-env, babel-plugin-transform-modules 開發體驗以及收益效果更好。
歡迎你們關注滴滴FE博客: github.com/DDFE/DDFE-b…