按需加载是所有组件库都会提供的一个根底能力,本文会剖析 ElementUI
、Vant
及varlet
几个组件库的实现并进行相应实际,帮忙你彻底搞懂其实现原理。
先搭个简略的组件库
笔者从 ElementUI
里copy
了两个组件:Alert
和Tag
,并将咱们的组件库命名为XUI
,当前目录构造如下:
组件都放在 packages
目录下,每个组件都是一个独自的文件夹,最根本的构造是一个 js
文件和一个 vue
文件,组件反对应用 Vue.component
形式注册,也反对插件形式 Vue.use
注册,js
文件就是用来反对插件形式应用的,比方 Alert
的js
文件内容如下:
import Alert from './src/main';
Alert.install = function(Vue) {Vue.component(Alert.name, Alert);
};
export default Alert;
就是给组件增加了一个 install
办法,这样就能够应用 Vue.use(Alert)
来注册。
组件的主题文件对立放在 /theme-chalk
目录下,也是每个组件一个款式文件,index.css
蕴含了所有组件的款式,ElementUI
的源码内是 scss
文件,本文为了简略,间接复制了其 npm
包内曾经编译后的 css
文件。
最外层还有一个 index.js
文件,这个文件很显著是用来作为入口文件导出所有组件:
import Alert from './packages/alert/index.js';
import Tag from './packages/tag/index.js';
const components = [
Alert,
Tag
]
const install = function (Vue) {
components.forEach(component => {Vue.component(component.name, component);
});
};
if (typeof window !== 'undefined' && window.Vue) {install(window.Vue);
}
export default {
install,
Alert,
Tag
}
首先顺次引入组件库的所有组件,而后提供一个 install
办法,遍历所有组件,顺次应用 Vue.component
办法注册,接下来判断是否存在全局的 Vue
对象,是的话代表是 CDN
形式应用,那么主动进行注册,最初导出 install
办法和所有组件。
Vue
的插件就是一个带有 install
办法的对象,所以咱们能够间接引入所有组件:
import XUI from 'xui'
import 'xui/theme-chalk/index.css'
Vue.use(XUI)
也能够独自注册某个组件:
import XUI from 'xui'
import 'xui/theme-chalk/alert.css'
Vue.use(XUI.Alert)
为什么不间接通过 import {Alert} form 'xui'
来引入呢,很显著,会报错。
因为咱们的组件库并没有公布到 NPM
,所以通过npm link
将咱们的组件库链接到全局。
接下来笔者应用 Vue CLI
搭建了一个测试项目,运行 npm link xui
来链接到组件库。而后应用后面的形式注册组件库或某个组件,这里咱们只应用 Alert
组件。
通过测试能够发现,无论是注册所有组件,还是只注册 Alert
组件,最初打包后的 js
里都存在 Tag
组件的内容:
接下来开启本文的注释,看看如何把 Tag
去掉。
最简略的按需引入
因为每个组件都能够独自作为一个插件,所以咱们齐全能够只引入某个组件,比方:
import Alert from 'xui/packages/alert'
import 'xui/theme-chalk/alert.css'
Vue.use(Alert)
这样咱们只引入了 alert
相干的文件,当然最初只会蕴含 alert
组件的内容。这样的问题是比拟麻烦,应用上老本比拟高,最现实的形式还是上面这种:
import {Alert} from 'xui'
通过 babel 插件
应用 babel
插件是目前大多数组件库实现按需引入的形式,ElementUI
应用的是babel-plugin-component
:
能够看到能间接应用 import {Alert} form 'xui'
形式来引入 Alert
组件,也不须要手动引入款式,那么这是怎么实现的呢,接下来咱们来撸一个极简版的。
原理很简略,咱们想要的是上面这种形式:
import {Alert} from 'xui'
然而理论按需应用须要这样:
import Alert from 'xui/packages/alert'
很显著,咱们只有帮用户把第一种形式转换成第二种就能够了,而通过 babel
插件来转换对用户来说是无感的。
首先在 babel.config.js
同级新增一个 babel-plugin-component.js
文件,作为咱们插件文件,而后批改一下 babel.config.js
文件:
module.exports = {
// ...
plugins: ['./babel-plugin-component.js']
}
应用相对路径援用咱们的插件,接下来就能够欢快的编码了。
先来看一下 import {Alert} from 'xui'
对应的 AST
树
整体是一个 ImportDeclaration
,通过souce.value
能够判断导入的起源,specifiers
数组里能够找到导入的变量,每个变量是一个 ImportSpecifier
,能够看到外面有两个对象:ImportSpecifier.imported
和ImportSpecifier.local
,这两个有什么区别呢,在于是否应用了别名导入,比方:
import {Alert} from 'xui'
这种状况 imported
和local
是一样的,然而如果应用了别名:
import {Alert as a} from 'xui'
那么是这样的:
咱们这里简略起见就不思考别名状况,只应用imported
。
接下来的工作就是进行转换,看一下 import Alert from 'xui/packages/alert'
的AST
构造:
指标 AST
构造也分明了接下来的事件就简略了,遍历 specifiers
数组创立新的 importDeclaration
节点,而后替换掉原来的节点即可:
// babel-plugin-component.js
module.exports = ({types}) => {
return {
visitor: {ImportDeclaration(path) {
const {node} = path
const {value} = node.source
if (value === 'xui') {
// 找出引入的组件名称列表
let specifiersList = []
node.specifiers.forEach(spec => {if (types.isImportSpecifier(spec)) {specifiersList.push(spec.imported.name)
}
})
// 给每个组件创立一条导入语句
const importDeclarationList = specifiersList.map((name) => {
// 文件夹的名称首字母为小写
let lowerCaseName = name.toLowerCase()
// 结构 importDeclaration 节点
return types.importDeclaration([types.importDefaultSpecifier(types.identifier(name))
], types.stringLiteral('xui/packages/' + lowerCaseName))
})
// 用多节点替换单节点
path.replaceWithMultiple(importDeclarationList)
}
}
},
}
}
接下来打包测试后果如下:
能够看到 Tag
组件的内容曾经没有了。
当然,以上实现只是一个最简略的demo
,实际上还须要思考款式的引入、别名、去重、在组件中引入、引入了某个组件然而理论并没有应用等各种问题,有趣味的能够间接浏览 babel-plugin-component 源码。
Vant
和 antd
也都是采纳这种形式,只是应用的插件不一样,这两个应用的都是 babel-plugin-import,babel-plugin-component
其实也是 fork
自babel-plugin-import
。
Tree Shaking 形式
Vant
组件库除了反对应用后面的 Babel
插件按需加载外还反对 Tree Shaking
形式,实现也很简略,Vant
最终公布的代码里提供了三种标准的源代码,别离是commonjs
、umd
、esmodule
,如下图:
commonjs
标准是最常见的应用形式,umd
个别用于 cdn
形式间接在页面引入,而 esmodule
就是用来实现 Tree Shaking
的,为什么 esmodule
能实现 Tree Shaking
而commonjs
标准不行呢,起因是 esmodule
是动态编译的,也就是在编译阶段就能确定某个模块导出了什么,引入了什么,代码执行阶段不会扭转,所以打包工具在打包的时候就能剖析出哪个办法被应用了,哪些没有,没有用到的就能够释怀的删掉了。
接下来批改一下咱们的组件库,让它也反对 Tree Shaking
,因为咱们的组件自身就是esmodule
模块,所以不须要批改,然而要批改一下导出的文件index.js
,因为目前还不反对上面这种形式导出:
import {Alert} from 'xui'
减少如下代码:
// index.js
// ...
export {
Alert,
Tag
}
// ...
接下来须要批改 package.json
,咱们都晓得package.json
里的 main
字段是用来批示包的入口文件,那么是不是只有把这个字段指向 esmodule
的入口文件就行了呢,其实是不行的,因为通常状况下它都是指向 commonjs
模块入口,而且一个包有可能反对 nodejs
和web
两种环境应用,nodejs
环境是有可能不反对 esmodule
模块的,既然不能批改旧的字段,那么就只能引入新的字段,也就是 pkg.module
,所以批改package.json
文件如下:
// package.json
{
// ...
"mains": "index.js",
"module": "index.js",// 减少该字段
// ...
}
因为咱们的组件库只有 esmodule
模块,所以其实这两个字段指向的是一样的,在理论开发中,须要向 Vant
一样编译成不同类型的模块,而且公布到 npm
的模块个别也须要编译成 es5
语法的,因为这些不是本文的重点,所以就省略了这个步骤。
增加了 pkg.module
字段,如果打包工具能辨认这个字段,那么会优先应用 esmodule
标准的代码,然而到这里并没有完结,此时打包后发现 Tag
组件的内容仍然在,这是为什么呢,无妨看看上面几种导入场景:
import 'core-js'
import 'style.css'
这两个文件都只引入了,然而并没有显著的进行应用,能够把它们删了吗,显然是不行的,这被称为存在“副作用”,所以咱们须要通知打包工具哪些文件是没有副作用的,能够删掉,哪些是有的,给我留着,Vue CLI
应用的是 webpack
,对应的咱们须要在package.json
文件里新增一个 sideEffects
字段:
// package.json
{
// ...
"sideEffects": ["**/*.css"],
// ...
}
咱们的组件库里只有款式文件是存在副作用的。
接下来再打包测试,发现没有引入的 Tag
组件的内容曾经被去除了:
更多对于 Tree Shaking
的内容能够浏览 Tree Shaking。
应用 unplugin-vue-components 插件
varlet
组件库官网文档上按需引入一节里提到应用的是 unplugin-vue-components 插件:
这种形式的长处是齐全不须要本人来引入组件,间接在模板里应用,由插件来扫描引入并注册,这个插件内置反对了很多市面上风行的组件库,对于曾经内置反对的组件库,间接参考上图引入对应的解析函数配置一下即可,然而咱们的小破组件库它并不反对,所以须要本人来写这个解析器。
首先这个插件做的事件只是帮咱们引入组件并注册,实际上按需加载的性能还是得靠前面两种形式。
Tree Shaking
咱们先在上一节的根底上进行批改,保留 package.json
的module
和 sideEffects
的配置,而后从 main.js
里删除组件引入和注册的代码,而后批改 vue.config.js
文件。因为这个插件的官网文档比拟简洁,看不出个所以然,所以笔者是参考内置的 vant
解析器来批改的:
返回的三个字段含意应该是比拟清晰的,importName
示意引入的组件名,比方 Alert
,path
示意从哪里引入,对于咱们的组件库就是 xui
,sideEffects
就是存在副作用的文件,根本就是配置对应的款式文件门路,所以咱们批改如下:
// vue.config.js
const Components = require('unplugin-vue-components/webpack')
module.exports = {
configureWebpack: {
plugins: [
Components({
resolvers: [{
type: "component",
resolve: (name) => {if (name.startsWith("X")) {const partialName = name.slice(1);
return {
importName: partialName,
path: "xui",
sideEffects: 'xui/theme-chalk/' + partialName.toLowerCase() + '.css'};
}
}
}]
})
]
}
}
笔者怕前缀和 ElementUI
重合,所以组件名称前缀都由 El
改成了 X
,比方ElAlert
改成了XAlert
,当然模板里也须要改成x-alert
,接下来进行测试:
能够看到运行失常,打包后也胜利去除了未应用的 Tag
组件的内容。
独自引入
最初让咱们再看一下独自引入的形式,先把 pkg.module
和pkg.sideEffects
字段都移除,而后批改每个组件的 index.js
文件,让其反对如下形式引入:
import {Alert} from 'xui/packages/alert'
Alert
组件批改如下:
// index.js
import Alert from './src/main';
Alert.install = function(Vue) {Vue.component(Alert.name, Alert);
};
// 新增上面两行
export {Alert}
export default Alert;
接下来再批改咱们的解析器:
const Components = require('unplugin-vue-components/webpack')
module.exports = {
configureWebpack: {
mode: 'production',
plugins: [
Components({
resolvers: [{
type: "component",
resolve: (name) => {if (name.startsWith("X")) {const partialName = name.slice(1);
return {
importName: partialName,
// 批改 path 字段,指向每个组件的 index.js
path: "xui/packages/" + partialName.toLowerCase(),
sideEffects: 'xui/theme-chalk/' + partialName.toLowerCase() + '.css'};
}
}
}]
})
]
}
}
其实就是批改了 path
字段,让其指向每个组件的 index.js
文件,运行测试和打包测试后后果也是符合要求的。
大节
本文简略剖析了一下组件库实现按需引入的几种形式,有组件库开发需要的敌人能够自行抉择,示例代码请移步:https://github.com/wanglin2/ComponentLibraryImport。