实际开发中发现一个项目中会使用到多个组件库,对于这些组件库有一些使用的是按需加载,但是也有一些使用的是全局注册。
对于一些大的第三方组件库需要按需导入。
如:import ElButton from 'element-ui/lib/button
;
但是这种按需导入在页面使用组件非常多的场景时,开发繁琐,体验不友好。
当然这些组件库也会推荐一些babel
插件来提升开发体验和性能优化。
如babel-plugin-component
、babel-plugin-import
、babel-plugin-transform-imports
等。
babel-plugin-transform-imports
可以通过自己的配置来实现以下效果。
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
同样有一些缺陷:它无法由一个导入生成多个导入。
这也就意味着使用babel-plugin-transform-imports
无法对antd
,element-ui
这样的UI组件库进行模块按需导入:这些UI组件库,除了js模块的导入外,往往还有一个样式模块。
详情请参考https://bitbucket.org/amctheatres/babel-transform-imports/src/master/
因此参考babel-plugin-transform-imports
实现一个vue项目的组件按需导入。
1. 获取页面所有标签。
Vue
中的每个页面其实都是一个.vue
的文件,这种文件,Vue
称之为组件页面,其使用vue-loader
来进行解析。
当解析某个vue
页面时通过node-html-parser
来获取到当前页面的所有标签。使用sync-disk-cache
来将获取到的标签保存在本地。
页面源码会被vue-loader
解析成上图所示,其中包含四种类型的代码块。
一、因此根据loader
的resourceQuery
属性是否含有type=template
来获取template
块。
二、使用node-html-parser
对template
代码块进行解析(深层遍历,去重)
三、创建sync-disk-cache
对象,暴露get
、set
、clear
方法
2. 使用babel在代码编译时将目标标签按照特定规则进行引入、并将组件进行注册。
一、babel
插件暴露lib
(配置组件导入地址)、style
(配置样式导入地址)方法。
二、在visitor
指定 Program
节点,并根据路径获取sync-disk-cache
保存的本地标签。
三、在export default
语句中完成剩下的逻辑, 也就是表达式ExportDefaultDeclaration
的访问器中:
因为组件最终导入的形式为:
import Comp1 from 'path/to/comp1';
import Comp2 from 'path/to/comp2';
在vue
文件中,script
里面一定会导出一个对象
export default {
data() {
return {};
},
};
此时要将Comp1
、Comp2
注册则需要写成以下这种形式。
export default {
components: {
Comp1: Comp1,
Comp2: Comp2,
},
data() {
return {};
},
};
理论来说需要这样,但是这样去改太复杂了,虽然有babel
的帮助,至少要考虑:
- 当前组件的配置对象里面是否有
components
这个选项,没有还需要特殊的复杂处理 - 当前组件的配置对象里面有
components
这个选项,低于注册的那个组件,他是不是已经注册过了,如果是,还有可能语法报错。 - 他是不是复杂的组件配置对象的写法,比如
const config = {
components: {},
data: ...
// ....
};
export default config;
还有可能是这样的:
const compontns = { ... };
export default {
components: compontns,
data: ...
// ....
};
要处理的情况太多了,从另一个角度想,它最终导出的一定是一个组件的配置对象,因此可以把它解出来。
比如 export default ....
不管他这个....是一个字面量还是一个变量都转换成
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');
// 在每次构建时, 都清空上一次存储信息。
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');
// 在每次构建时, 都清空上一次存储信息。
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) {
// 如果某个标签需要自动导入,请返回导入路径, 不需要则返回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) {
// 如果某个标签需要自动样式文件,请返回导入路径,无则返回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
但是组件不会去自动导入icon
,因此icon
需要手动进行全局导入。
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
组件使用时会报错,具体原因是,按需引入的常用写法中没有调用到Vue.use
所执行的自定义指令。解决方案如下
// main.js
import { Modal }from 'ant-design-vue';
Modal.install(Vue)`
view-desgin
根据官方文档,需要在main.js
将样式全部导入
import 'view-design/dist/styles/iview.css';