这一整个月我几乎投入全部工做以外的时间在组件库的框架搭建上,特别是组件按需引入的探索让我原本发量就很少的头顶更加荒芜。关于组件按需引入的探索已经告一段落,虽然结果很不如意,可是过程当中积累了不少东西,值得分享。css
一个组件库会提供不少的组件,有时候用户只想使用其中的部分组件,那么在打包时,未使用的组件就应该被过滤,减少打包以后的体积。实现按需引入组件的思路有两种:vue
如今流行的几款vue ui组件库(element-ui、ant-design-vue、iview)都是使用的方案一。以element-ui为例,虽然使用时咱们的写法是import { Button } from 'element-ui'
,可是这种写法的前提是安装 babel-plugin-component
插件。这种看似es模块的引入方式,其实是在编译阶段,针对引用路径作替换。node
import { Button } from 'components'
复制代码
被替换成react
var button = require('components/lib/button')
require('components/lib/button/style.css')
复制代码
第一种方案比较成熟,实现起来也不复杂(多入口打包,生成多个模块),具体的代码我会再写一篇文章介绍。咱们先来看一下第二种方案的实现原理。webpack
在谈tree shaking以前咱们先来看另外一个概念DCE(dead code elimination 意为消除无用代码)。无用代码指那些不会被执行或者执行结果不会被使用以及只读不写的代码。先来看一个例子。git
新建一个文件夹,初始化项目es6
npm init -y
复制代码
安装打包工具webpack4github
npm i webpack webpack-cli -D
复制代码
建立入口文件web
写一下最基本的webpack配置npm
打包以后结果以下
webpack
复制代码
能够看到无用代码依然存在。那是由于咱们的mode选择的none,webpack自己是不会帮咱们消除无用代码的,js消除无用代码借助的是uglify这个代码压缩混淆工具。咱们把webpack的mode设置为production,默认开启uglify及其余生产环境工具,再打包一次。格式化后代码以下
再来尝试一下加上模块以后的状况。
module.js中有两个函数,m1和m2,用CommonJs规范导出。index.js中引入时只使用m1。production模式下打包,结果以下
很明显,整个module.js的代码都被打包了。这也很好理解,require引入的是module.exports整个对象,使用了对象中的其中一个属性,这个对象就再也不是无用代码了。
tree shaking也能够消除无用代码,它和传统DCE的不一样之处在于它的消除原理是依赖于ES6的模块特性。也就是说,要想使用tree shaking,必须使用import/export语法导入导出模块。
下面咱们尝试一下,添加两个esModule
无用代码、导入未使用的模块以及未导入的代码都被干掉了。tree shaking大法好啊!
经过上面的例子咱们能够发现tree shaking的强大之处还有esModule的好处。
esModule的模块依赖关系是肯定的,和运行时的状态无关。基于此特性能够进行静态分析,在运行以前就知道哪些模块被引入了。 这就是tree-shaking优化的基础。
经过上面的例子咱们能够暂时得出如下结论:
既然tree shaking这么好用,那若是我基于esModule标准编写vue组件,export每一个组件,而后使用时只import须要的组件,这样打包时能够利用tree shaking自动帮我消除没用到的组件,这样不就是按需引入了吗?想到这里内心真的美滋滋。
犹豫再三,我选择了tree shaking方案来实现按需引入,由于我写组件库的出发点是学习沉淀,若是用了别人用过不少次的成熟方案,那还怎么折腾。选好方向以后,那就开始coding吧。
既然要使用tree shaking来作按需引入,那么组件库必须使用esModule规范,也就是说打包工具只能选择rollup。
webpack的output.libraryTarget只有 var、this、window、global、commonjs、commonjs二、amd、amd-require、umd、jsonp
关于rollup和webpack的区别你们能够查看rollup的官方文档,后面我也会考虑再写一篇文章比较rollup和webpack,也建议你们尝试一下rollup。它们的众多差别性中最重要的就是rollup支持打包生成esModule,而webpack不支持。下面咱们就来实践一下,用rollup打包咱们的组件库。
新建一文件夹,就叫VUI吧
npm init -y
复制代码
npm i rollup @babel/core @babel/plugin-transform-runtime
@babel/preset-env rollup-plugin-babel rollup-plugin-terser node-sass
rollup-plugin-postcss rollup-plugin-vue2 -D
复制代码
npm i @babel/runtime-corejs2 -S
复制代码
rollup中的扩展主要经过plugin实现,下面分别解释一下上面部分依赖的做用,部分常见的依赖就不解释了,不了解的同窗能够查一下它们的文档
下面是compA/CompA.vue
<template>
<div class="comp-a">
{{msg}}
<span class="text">{{text}}</span>
</div>
</template>
<script>
export default {
name: 'CompA',
props: {
text: {
type: String,
default: ''
}
},
data() {
return {
msg: 'hello compA'
}
}
};
</script>
<style lang="scss" scoped>
.comp-a {
color: red;
&:hover {
font-size: 20px;
}
.text {
color: blue;
}
}
</style>
复制代码
下面是compA/index.js
import CompA from './CompA.vue'
CompA.install = (Vue) => {
Vue.component(CompA.name, CompA)
}
export default CompA
复制代码
下面是compB/CompB.vue
<template>
<div class="comp-b">
{{msg}}
<span class="text">this is compB</span>
</div>
</template>
<script>
export default {
name: 'CompB',
data() {
return {
msg: 'hello compB'
}
}
};
</script>
<style lang="scss" scoped>
.comp-b {
color: yellow;
&:hover {
font-size: 20px;
}
.text {
color: green;
}
}
</style>
复制代码
下面是compB/index.js
import CompB from './CompB.vue'
CompB.install = (Vue) => {
Vue.component(CompB.name, CompB)
}
export default CompB
复制代码
本文主要的讨论点在于组件库的按需引入,因此组件的写法等细节点就不细说了。你们有疑问能够留言讨论。
先贴出.babelrc文件
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2,
"useESModules": true
}
]
]
}
复制代码
下面对部分配置进行说明
modules: false
useESModules: true
贴出rollup.config.js
import { terser } from "rollup-plugin-terser";
import babel from 'rollup-plugin-babel';
import vue from 'rollup-plugin-vue2';
import postcss from 'rollup-plugin-postcss';
export default {
input: 'src/index.js',
output: [
{
file: 'lib/v-ui.esm.js',
format: 'esm'
},
{
file: 'lib/v-ui.umd.js',
name: 'v-ui',
format: 'umd',
exports: 'named'
}
],
plugins: [
vue(),
postcss(),
terser(),
babel({
exclude: 'node_modules/**',
runtimeHelpers: true
})
]
};
复制代码
一样,本文讨论的是组件库的按需引入,因此对于rollup的用法细节及插件机制不会详述。没接触过rollup的同窗建议看看官方文档。下面对配置文件作简要说明
format: 'esm'
表示输出esModule规范的模块,使用tree shaking的前提。 format: 'umd'
表示输出通用模块定义,以amd,cjs,iife为一体。这样作的目的是支持组件库的其余引入方式,好比require、cdn等。{
"name": "VUI",
"version": "1.0.0",
"description": "",
"main": "lib/v-ui.umd.js",
"sideEffects": false,
"module": "lib/v-ui.esm.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/runtime-corejs2": "^7.7.2"
},
"devDependencies": {
"@babel/core": "^7.7.2",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.7.1",
"node-sass": "^4.13.0",
"rollup": "^1.27.3",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-postcss": "^2.0.3",
"rollup-plugin-terser": "^5.1.2",
"rollup-plugin-vue2": "^0.8.1"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
}
}
复制代码
下面对部分关键配置作出说明
rollup -c
复制代码
成功的打包出两个文件。下面是v-ui.esm.js格式化以后的代码
function t(t, e) {
void 0 === e && (e = {});
var n = e.insertAt;
if (t && "undefined" != typeof document) {
var s = document.head || document.getElementsByTagName("head")[0],
o = document.createElement("style");
o.type = "text/css", "top" === n && s.firstChild ? s.insertBefore(o, s.firstChild) : s.appendChild(o), o.styleSheet ? o.styleSheet.cssText = t : o.appendChild(document.createTextNode(t))
}
}
t(".comp-a {\n color: red; }\n .comp-a:hover {\n font-size: 20px; }\n .comp-a .text {\n color: blue; }\n");
var e = {
render: function () {
var t = this.$createElement,
e = this._self._c || t;
return e("div", {
staticClass: "comp-a"
}, [this._v("\n " + this._s(this.msg) + "\n "), e("span", {
staticClass: "text"
}, [this._v(this._s(this.text))])])
},
staticRenderFns: [],
name: "CompA",
props: {
text: {
type: String,
default: ""
}
},
data: () => ({
msg: "hello compA"
}),
install: function (t) {
t.component(e.name, e)
}
};
t(".comp-b {\n color: yellow; }\n .comp-b:hover {\n font-size: 20px; }\n .comp-b .text {\n color: green; }\n");
var n = {
render: function () {
var t = this.$createElement,
e = this._self._c || t;
return e("div", {
staticClass: "comp-b"
}, [this._v("\n " + this._s(this.msg) + "\n "), e("span", {
staticClass: "text"
}, [this._v("this is compB")])])
},
staticRenderFns: [],
name: "CompB",
data: () => ({
msg: "hello compB"
}),
install: function (t) {
t.component(n.name, n)
}
},
s = [e, n],
o = {
install: function (t) {
s.map((function (e) {
return t.component(e.name, e)
}))
}
};
export default o;
export {
e as CompA, n as CompB
};
复制代码
代码打包好了,暂时不作分析,先来试用一下吧。
本地开发,咱们直接把组件库链接到全局
npm link
复制代码
用@vue/cli新建一个vue项目,就叫vuitest吧
vue create vuitest
复制代码
项目建立好以后引入VUI
npm link VUI
复制代码
main.js中使用VUI,先试一试总体引入
改一下项目中的HelloWorld.vue,使用VUI
跑起来看看
npm run serve
复制代码
效果很不错,至少组件库能用了,先给本身点个赞。下面咱们打包vuitest
npm run build
复制代码
打包以后的文件太大了,我就不截图了。这里咱们须要验证的是两个组件和样式是否是都被打包了。
很明显,两个组件和样式都被打包了。
下面尝试一下按需引入
跑起来
npm run serve
复制代码
打包看看
npm run build
复制代码
一样,咱们来验证一下两个组件和样式的打包状况
惊喜的是未被引入的CompB打包的时候被干掉了,惊吓的是CompB的样式没有被干掉。
辛苦了半天,咱们总算实现了一个阉割版的按需引入。剩下的问题就是样式怎么办。
咱们先来分析一下VUI打包以后的v-ui.esm.js
那如今咱们须要作的事情就是让样式和组件关联起来,加载某一个组件的同时加载对应的样式。
解决组件和样式关联问题,我想到的第一个解决方案是css in js。如今最流行的css in js库是style-components,但是这个库是为react量身打造的,引入以后甚至还得安装react依赖。这里我忍不住想吐槽,我找了几个流行的css in js库,发现它们基本都是绑定react的,有几个声称是框架无关,可是官方文档里写的推荐框架依然是react...... 兜兜转转好几圈,终于找到一个vue-styled-components,先试一试
rollup -c
复制代码
贴出打包后格式化的代码
import t from "@babel/runtime-corejs2/helpers/esm/taggedTemplateLiteral";
import n from "vue-styled-components";
function e() {
var n = t(["\n .comp-a {\n color: red;\n &:hover {\n font-size: 20px;\n }\n .text {\n color: blue;\n }\n }\n"]);
return e = function () {
return n
}, n
}
var s = {
render: function () {
var t = this.$createElement,
n = this._self._c || t;
return n("compa-style", [n("div", {
staticClass: "comp-a"
}, [this._v("\n " + this._s(this.msg) + "\n "), n("span", {
staticClass: "text"
}, [this._v(this._s(this.text))])])])
},
staticRenderFns: [],
name: "CompA",
components: {
"compa-style": n.span(e())
},
props: {
text: {
type: String,
default: ""
}
},
data: () => ({
msg: "hello compA"
})
};
function o() {
var n = t(["\n .comp-b {\n color: yellow;\n &:hover {\n font-size: 20px;\n }\n .text {\n color: green;\n }\n }\n"]);
return o = function () {
return n
}, n
}
s.install = function (t) {
t.component(s.name, s)
};
var r = {
render: function () {
var t = this.$createElement,
n = this._self._c || t;
return n("compb-style", [n("div", {
staticClass: "comp-b"
}, [this._v("\n " + this._s(this.msg) + "\n "), n("span", {
staticClass: "text"
}, [this._v("this is compB")])])])
},
staticRenderFns: [],
name: "CompB",
components: {
"compb-style": n.span(o())
},
data: () => ({
msg: "hello compB"
}),
install: function (t) {
t.component(r.name, r)
}
},
a = [s, r],
i = {
install: function (t) {
a.map((function (n) {
return t.component(n.name, n)
}))
}
};
export default i;
export {
s as CompA, r as CompB
};
复制代码
能够看到样式和组件确实关联起来了。 在vuitest中也打包一次
npm run build
复制代码
看看两个组件和样式的打包状况
嗯... 状况更糟了,全部的组件和样式都被打包了。简单分析一下v-ui.esm.js,应该是打包以后的代码有反作用,没法被tree shaking,就连加了sideEffects: false都不行。
style-components方案宣布失败,不甘心的我又尝试了jss、vue-emotion等其余支持vue的css in js方案,无一例外所有以失败了结。看来这些库在编写时并未考虑tree shaking的状况,或者说如今esModule仍为普遍使用。
花了一周多的时间解决样式问题,尝试N多方案以后都无果,我最终能想到的解决方案只剩下样式单独打包,以组件为单位进行拆分,而后使用时借鉴babel-plugin-component的思路,单独引入样式。但是这样又违背了我使用tree shaking的初心。
很遗憾花费了近一个月的时间探索无果,最终我决定放弃探索样式问题,转而借鉴babel-plugin-component的思路。若是哪位大佬有合适的解决方案,跪求指点。
留下了没技术的泪水...
在尝试tree shaking的过程当中遇到了一个很奇怪的问题,至今还未不理解,贴出来你们看看。
写vue组件的过程当中,props的值会有Boolen类型,但是当出现type: Boolean
这样的代码时,tree shaking直接失效了。具体代码以下
先看正常的状况
vuitest打包后,组件和样式打包状况以下
VUI打包,vuitest打包。组件和样式的打包状况以下
能够看到未引入的compA的组件和样式都被打包了,tree shaking失效了。
刚开始遇到这个问题的时候为了定位问题,我尝试修改了文章开头的那个例子
彷佛uglify对于属性值为Boolean的状况有什么特殊处理,暂时我也没找到思路,只能深刻源码找找缘由了。哪位大佬知道缘由欢迎留言点拨。
尽管webpack4加入了sideEffects字段,改善了对于tree shaking的支持状况。可是tree shaking的发展状况依然不容乐观啊。现阶段在没有其余方案的帮助下单纯利用tree shaking特性来实现组件库的按需引入看来还有难度。今天看到Node新版本13.2.0正式支持ES Modules特性,可能在不久的将来tree shaking的支持度也会愈来愈好,这也算是一个使人欣慰的好消息了。