基於rollup的元件庫打包體積優化
背景
前段時間對公司內部的元件庫(類似element-ui)做了打包體積優化,現在抽點時間記錄下。以前也做過構建速度的優化,具體可以看元件庫webpack構建速度優化
一些存在的問題
最開始打包是基於webpack的,在按需載入上存在的體積冗餘會比較大,如:
webpack
打包特有的模組載入器函式,這部分其實有些多餘,最好去掉- 使用
babel
轉碼時,babel
帶來的helper
函式全部是內聯狀態,需要轉成import
或require
來引入 - 使用
transform-rumtime
對一些新特性新增polyfill
,也是內聯狀態,需要轉成import
或require
來引入 vue-loader
帶來的額外程式碼,如normalizeComponent
,不做處理也是內聯transform-vue-jsx
帶來的額外函式引入,如mergeJSXProps
,不做處理也是內聯
以上幾個問題,如果只是一份程式碼,那不會有太大問題,但是如果是按需載入,使用者一旦引入多個元件,以上的程式碼就會出現多份,帶來嚴重的影響
import { Button, Icon } from 'gs-ui'
複製程式碼
以上程式碼會轉成
import Button from 'gs-ui/lib/button.js'
import Icon from 'gs-ui/lib/icon.js'
複製程式碼
這樣,就會出現多份相同的helper
函式程式碼,多份webpack
的模組載入器函式,而且還不好去重
尋找解決方案
討論過後主要有以下幾種選擇
採用後編譯
我們也認同這種方案,採用後編譯可以解決上面的各種問題,也有元件庫是這樣做的,比如cube-ui,但是這樣有些不方便,因為使用者需要設定各種alias
,還要保證好各種編譯環境,如jsx
,而且未來可能會引入flow
,會更加不方便,所以暫時不考慮
使用rollup打包,設定external(當然webpack也可以)外聯helper函式
使用rollup
打包,可以直接解決問題1和問題4,設定external
可以解決transform-runtime
等帶來的helper
,這取決於相關外掛實現時是不是通過import
或require
來新增helper
的,如果是直接copy
的話,那就還得另找辦法。最後決定就這種方案進行嘗試
使用rollup對打包進行重構
使用rollup
打包可能某些習慣和webpack
有些出入,在這裡很多事需要引入外掛來完成,比如引入node_modules
中的模組的話,需要加入rollup-plugin-node-resolve
,載入commonjs
模組需要引入rollup-plugin-commonjs
等等。另外還有些比較麻煩的,比如經常會這樣寫
import xx from './xx-folder'
複製程式碼
然後希望模組打包器可以識別成
import xx from './xx-folder/index.js'
複製程式碼
在rollup
裡還是需要用外掛來完成這件事,找到的外掛都沒能滿足各種需求,比如還需要對alias
也能識別然後加上index.js
,最後還是需要自己實現這個外掛
基本的rollup配置應該差不多是這樣的
{
output: {
format: 'es',
// file: xx,
// paths:
},
input: 'xxx',
plugins: [
vue({
compileTemplate: true,
htmlMinifier: {
customAttrSurround: [[/@/, new RegExp('')], [/:/, new RegExp('')]],
collapseWhitespace: true,
removeComments: true
}
}),
babel({
...babelrc({
addModuleOptions: false,
addExternalHelpersPlugin: false
}),
exclude: 'node_modules/**',
runtimeHelpers: true
}),
localResolve({
components: path.resolve(__dirname, '../components')
}),
alias({
components: path.resolve(__dirname, '../components'),
resolve: ['.js', '.vue']
}),
replace({
'process.env.NODE_ENV': JSON.stringify('development')
})
],
// external
}
複製程式碼
這裡採用的rollup-plugin-vue
的版本是v3.0.0
,不採用v4
,因為打包出來的體積更小,功能完全滿足元件庫需要。因為會存在各種約定,比如元件肯定是存在render
函式(不一定指的就是手寫render
或jsx
,只是不會有在js
中使用template
這種情況,這樣的好處是可以使用runtime-only
的vue
),元件肯定不存在style
部分等等。
babel
的配置上基本不會有改變,只是rollup-plugin-babel
加上了runtimeHelpers
,用來開啟transform-runtme
的。可能你會覺得為了更精簡體積,應該去掉transform-runtime
,這點我持保留意見,這裡使用transform-runtime
的主要作用是為了接管babel-helpers
,因為這個babel-helpers
無法被external
。另外整個元件庫用到的babel-runtime
其實也不多,主要是類似Object.assign
這樣的函式,像這些函式,使用的話還是需要加上transform-runtime
的,或者需要自己實現,感覺沒什麼必要。類似Array.prototype.includes
這種無法被transform-runtime
處理的還是會避免使用的
localResolve
是自己實現的外掛,用來新增index.js
,並且能支援alias
,
alias
外掛用來新增alias
,並且需要設定字尾
replace
外掛用來替換一些環境變數,比如開發環境會有錯誤提示,生成環境不會有,這裡展示的是開發環境的配置。
配置external
所有優化的關鍵在於external
上,除了最基本的vue
需要external
外,還有比如Button
元件內部依賴了Icon
元件,那是需要把Icon
元件external
的
// Button 元件
import Icom from 'components/icon'
複製程式碼
其實就是所有的元件和共用的util
函式都需要external
,當然這裡本來就存在了,不是本次優化要做的
主要要處理的是babel-helper
等helper
函式,但是這裡不能做到,我也沒有去了解babel
是如何對這塊進行處理的,最後還是需要transform-runtime
來接管它。
rollup
的external
配置是支援函式型別的,大概看tranform-runtime
這個外掛原始碼可以找到addImport
這些方法,可以知道polyfill
是通過import
來引入的,可以被external
,所以只需要在rollup
配置的external
新增上類似函式就可以達到我們想要的效果
{
external (id) {
// 對babel-runtime進行external
return /^babel-runtime/.test(id) // 當然別忘了還有很多 比如vue等等,這裡就不寫了
}
}
複製程式碼
這裡就可以解決問題2和問題3
另外問題5,這個是如何來的呢,比如在寫jsx
時,可能會這樣寫
// xx元件
export default {
render () {
return (
<div>
<ToolTip {...{props: tooltipProps}} />
{/* other */}
</div>
)
}
}
複製程式碼
在某個元件中依賴了另一個元件,考慮到擴充套件性,是支援對另一個元件進行props
設定的,所以經常會這樣寫,在template
中的話就類似於v-bind="tolltipProps"
這個時候transform-vue-jsx
外掛是會引入一個helper
函式的,也就是babel-helper-vue-jsx-merge-props
,大概看看transform-vue-jsx
原始碼也可以得知,這個helper
也是import
進來的,所以可以把external
改成
{
external (id) {
return /^babel/.test(id)
}
}
複製程式碼
這樣就可以做到對所有helper
都使用import
的形式來引入,而且使用rollup
打包後的程式碼更可讀,大概長這樣
// Alert元件
import _defineProperty from 'babel-runtime/helpers/defineProperty';
import Icon from 'gs-ui/lib/icon.js';
var Alert = { render: function render() {
var _class;
var _vm = this;var _h = _vm.$createElement;var _c = _vm._self._c || _h;return _c('transition', { attrs: { "name": "gs-zoom-in-top" } }, [_vm.show ? _c('div', { class: (_class = { 'gs-alert': true }, _defineProperty(_class, 'gs-alert-' + _vm.type, !!_vm.type), _defineProperty(_class, 'has-desc', _vm.desc || _vm.$slots.desc), _class) }, [_vm.showIcon ? _c('div', { staticClass: "gs-alert-icon", class: { "gs-alert-icon-top": !!_vm.desc } }, [_vm._t("icon", [_c('gs-icon', { attrs: { "name": _vm.icon } })])], 2) : _vm._e(), _vm._v(" "), _c('div', { staticClass: "gs-alert-content" }, [_vm.title || _vm.$slots.default ? _c('div', { staticClass: "gs-alert-title" }, [_vm._t("default", [_vm._v(_vm._s(_vm.title))])], 2) : _vm._e(), _vm._v(" "), _vm.desc || _vm.$slots.desc ? _c('div', { staticClass: "gs-alert-desc" }, [_vm._t("desc", [_vm._v(_vm._s(_vm.desc))])], 2) : _vm._e(), _vm._v(" "), _vm.closable ? _c('div', { staticClass: "gs-alert-close", on: { "click": _vm.close } }, [_vm._t("close", [_vm._v(" " + _vm._s(_vm.closeText) + " "), !_vm.closeText ? _c('gs-icon', { attrs: { "name": "close" } }) : _vm._e()])], 2) : _vm._e()])]) : _vm._e()]);
}, staticRenderFns: [],
name: 'GsAlert',
components: _defineProperty({}, Icon.name, Icon),
// props
// data
// methods
};
/* istanbul ignore next */
Alert.install = function (Vue) {
Vue.component(Alert.name, Alert);
};
export default Alert;
複製程式碼
vue外掛把vue元件中的template
轉成render
函式,babel外掛做語法轉換,因為external
的存在,保留了模組關係,整個程式碼看起來很清晰,很舒服,不像webpack
,都會新增一個模組載入函式...
優化後和優化前的體積對比
下面的截圖是生產環境的版本,也就是沒有了程式碼提示,也已經壓縮混淆後的程式碼體積對比 左邊是優化前,右邊是優化後