前段时间对公司内部的组件库(相似element-ui)作了打包体积优化,如今抽点时间记录下。之前也作过构建速度的优化,具体能够看组件库webpack构建速度优化html
最开始打包是基于webpack的,在按需加载上存在的体积冗余会比较大,如:vue
webpack
打包特有的模块加载器函数,这部分其实有些多余,最好去掉babel
转码时,babel
带来的helper
函数所有是内联状态,须要转成import
或require
来引入transform-rumtime
对一些新特性添加polyfill
,也是内联状态,须要转成import
或require
来引入vue-loader
带来的额外代码,如normalizeComponent
,不作处理也是内联transform-vue-jsx
带来的额外函数引入,如mergeJSXProps
,不作处理也是内联以上几个问题,若是只是一份代码,那不会有太大问题,可是若是是按需加载,用户一旦引入多个组件,以上的代码就会出现多份,带来严重的影响node
import { Button, Icon } from 'gs-ui'
复制代码
以上代码会转成webpack
import Button from 'gs-ui/lib/button.js'
import Icon from 'gs-ui/lib/icon.js'
复制代码
这样,就会出现多份相同的helper
函数代码,多份webpack
的模块加载器函数,并且还很差去重git
讨论事后主要有如下几种选择github
采用后编译web
咱们也认同这种方案,采用后编译能够解决上面的各类问题,也有组件库是这样作的,好比cube-ui,可是这样有些不方便,由于用户须要设置各类alias
,还要保证好各类编译环境,如jsx
,并且将来可能会引入flow
,会更加不方便,因此暂时不考虑element-ui
使用rollup打包,设置external(固然webpack也能够)外联helper函数bash
使用rollup
打包,能够直接解决问题1和问题4,设置external
能够解决transform-runtime
等带来的helper
,这取决于相关插件实现时是否是经过import
或require
来添加helper
的,若是是直接copy
的话,那就还得另找办法。最后决定就这种方案进行尝试babel
使用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
上,除了最基本的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
,都会添加一个模块加载函数...
下面的截图是生产环境的版本,也就是没有了代码提示,也已经压缩混淆后的代码体积对比 左边是优化前,右边是优化后