實際開發(fā)中發(fā)現(xiàn)一個項目中會使用到多個組件庫,對于這些組件庫有一些使用的是按需加載,但是也有一些使用的是全局注冊。
對于一些大的第三方組件庫需要按需導(dǎo)入。
如:import ElButton from 'element-ui/lib/button;
但是這種按需導(dǎo)入在頁面使用組件非常多的場景時,開發(fā)繁瑣,體驗不友好。
當(dāng)然這些組件庫也會推薦一些babel插件來提升開發(fā)體驗和性能優(yōu)化。
如babel-plugin-component、babel-plugin-import、babel-plugin-transform-imports等。
babel-plugin-transform-imports可以通過自己的配置來實現(xiàn)以下效果。
import { Row, Grid as MyGrid } from react-bootstrap';
import { merge } from 'lodash';
↓ ↓ ↓ ↓ ↓ ↓
import Row from 'reactbootstrap/lib/Row';
import MyGrid from 'react-bootstrap/lib/Grid';
import merge from 'lodash/merge';`
但babel-plugin-transform-imports同樣有一些缺陷:它無法由一個導(dǎo)入生成多個導(dǎo)入。
這也就意味著使用babel-plugin-transform-imports無法對antd,element-ui這樣的UI組件庫進(jìn)行模塊按需導(dǎo)入:這些UI組件庫,除了js模塊的導(dǎo)入外,往往還有一個樣式模塊。
詳情請參考https://bitbucket.org/amctheatres/babel-transform-imports/src/master/
因此參考babel-plugin-transform-imports實現(xiàn)一個vue項目的組件按需導(dǎo)入。
1. 獲取頁面所有標(biāo)簽。
Vue中的每個頁面其實都是一個.vue的文件,這種文件,Vue稱之為組件頁面,其使用vue-loader來進(jìn)行解析。
當(dāng)解析某個vue頁面時通過node-html-parser來獲取到當(dāng)前頁面的所有標(biāo)簽。使用sync-disk-cache來將獲取到的標(biāo)簽保存在本地。


頁面源碼會被vue-loader解析成上圖所示,其中包含四種類型的代碼塊。
一、因此根據(jù)loader的resourceQuery屬性是否含有type=template來獲取template塊。
二、使用node-html-parser對template代碼塊進(jìn)行解析(深層遍歷,去重)
三、創(chuàng)建sync-disk-cache對象,暴露get、set、clear方法
2. 使用babel在代碼編譯時將目標(biāo)標(biāo)簽按照特定規(guī)則進(jìn)行引入、并將組件進(jìn)行注冊。
一、babel插件暴露lib(配置組件導(dǎo)入地址)、style(配置樣式導(dǎo)入地址)方法。
二、在visitor指定 Program節(jié)點,并根據(jù)路徑獲取sync-disk-cache保存的本地標(biāo)簽。
三、在export default語句中完成剩下的邏輯, 也就是表達(dá)式ExportDefaultDeclaration的訪問器中:
因為組件最終導(dǎo)入的形式為:
import Comp1 from 'path/to/comp1';
import Comp2 from 'path/to/comp2';
在vue文件中,script里面一定會導(dǎo)出一個對象
export default {
data() {
return {};
},
};
此時要將Comp1、Comp2注冊則需要寫成以下這種形式。
export default {
components: {
Comp1: Comp1,
Comp2: Comp2,
},
data() {
return {};
},
};
理論來說需要這樣,但是這樣去改太復(fù)雜了,雖然有babel的幫助,至少要考慮:
- 當(dāng)前組件的配置對象里面是否有
components這個選項,沒有還需要特殊的復(fù)雜處理 - 當(dāng)前組件的配置對象里面有
components這個選項,低于注冊的那個組件,他是不是已經(jīng)注冊過了,如果是,還有可能語法報錯。 - 他是不是復(fù)雜的組件配置對象的寫法,比如
const config = {
components: {},
data: ...
// ....
};
export default config;
還有可能是這樣的:
const compontns = { ... };
export default {
components: compontns,
data: ...
// ....
};
要處理的情況太多了,從另一個角度想,它最終導(dǎo)出的一定是一個組件的配置對象,因此可以把它解出來。
比如 export default ....
不管他這個....是一個字面量還是一個變量都轉(zhuǎn)換成
const _thisCpnponentConfig = ...
export _thisCpnponentConfig;
在這種情況下如果要給他注冊組件只需擴展這個_thisCpnponentConfig里面的components選項就好了。
因此上面的代碼可以寫成
const _thisCpnponentConfig = ...;
// 先初始化一下,避免他開始沒有
thisCpnponentConfig.components = thisCpnponentConfig.components || {};
thisCpnponentConfig.components['Comp1'] = Comp1;
export _thisCpnponentConfig;
3. 如何使用
npm i vue-template-label-loader;
npm i babel-plugin-vue-auto-import;
3.1.1. 在webpack配置中配置該loader:
const { clear } = require('vue-template-label-loader/lib/store');
// 在每次構(gòu)建時, 都清空上一次存儲信息。
clear();
module.exports = {
module: {
rules: [{
test: /\.vue$/,
use: [{
loader: 'vue-template-label-loader',
options: {}
exclude: {}
},{
loader: 'vue-loader',
options: {
//...
},
}]
},
]}
};
3.1.2. 在vue.config.js中
const { clear } = require('vue-template-label-loader/lib/store');
// 在每次構(gòu)建時, 都清空上一次存儲信息。
clear();
module.exports = {
chainWebpack: (config) => {
config.module
.rule('vue')
.set('exclude', [/node_modules/])
.use('vue-template-label-loader')
.loader('vue-template-label-loader')
.end()
},
};
3.2. 配置babelrc.js
該工具需要配合 vue-template-label-loader 使用
function isCapitalStart(string) {
if (!string) {
return false
}
const first = string[0];
const reg = new RegExp(/[A-Z]/);
return reg.test(first);
}
function toLine(string) {
return string.replace(/([A-Z][a-z]*)([A-Z][a-z]*)/g,"$1-$2").toLowerCase();
}
function kebabCase(str) {
var hyphenateRE = /([^-])([A-Z])/g;
return str
.replace(hyphenateRE, '$1-$2')
.replace(hyphenateRE, '$1-$2')
.toLowerCase();
}
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: [
[
'babel-plugin-vue-auto-import',
{
// excludeTags: ['List', 'HelloWorld'],
lib(tag) {
// 如果某個標(biāo)簽需要自動導(dǎo)入,請返回導(dǎo)入路徑, 不需要則返回null
if (tag.startsWith('el-')) {
return `element-ui/lib/${tag.replace('el-', '')}`;
}
if (tag.startsWith('a-')) {
return `ant-design-vue/lib/${tag.replace('a-', '')}`;
}
if(isCapitalStart(tag) || tag.startsWith('i-')) {
const tagName = tag.startsWith('i-') ? tag.replace('i-', '') : toLine(tag)
return `view-design/src/components/${toLine(tagName)}/index.js`
}
return null
},
style(tag) {
// 如果某個標(biāo)簽需要自動樣式文件,請返回導(dǎo)入路徑,無則返回null
if (tag.startsWith('el-')) {
const label = tag.replace('el-', '');
return `element-ui/lib/theme-chalk/${label}.css`;
}
if (tag.startsWith('a-')) {
const tagName = tag.replace('a-', '');
return `ant-design-vue/lib/${tagName}/style`;
}
return null;
},
},
],
],
};
4. 注意事項
因為element-ui部分組件有使用到icon但是組件不會去自動導(dǎo)入icon,因此icon需要手動進(jìn)行全局導(dǎo)入。
import Vue from 'vue'
import { Icon } from 'element-ui'
import 'element-ui/lib/theme-chalk/icon.css';
Vue.component(Icon.name, Icon)
ant-desgin-vue的model組件使用時會報錯,具體原因是,按需引入的常用寫法中沒有調(diào)用到Vue.use所執(zhí)行的自定義指令。解決方案如下
// main.js
import { Modal }from 'ant-design-vue';
Modal.install(Vue)`
view-desgin 根據(jù)官方文檔,需要在main.js將樣式全部導(dǎo)入
import 'view-design/dist/styles/iview.css';