按需加载是所有组件库都会提供的一个根底能力,本文会剖析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.jsmodule.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.jsconst 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.jsimport 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。