這一整個月我幾乎投入全部工做以外的時間在組件庫的框架搭建上,特別是組件按需引入的探索讓我原本髮量就很少的頭頂更加荒蕪。關於組件按需引入的探索已經告一段落,雖然結果很不如意,可是過程當中積累了不少東西,值得分享。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的支持度也會愈來愈好,這也算是一個使人欣慰的好消息了。