按需加载是所有组件库都会提供的一个根底能力,本文会剖析ElementUIVantvarlet几个组件库的实现并进行相应实际,帮忙你彻底搞懂其实现原理。

先搭个简略的组件库

笔者从ElementUIcopy了两个组件:AlertTag,并将咱们的组件库命名为XUI,当前目录构造如下:

组件都放在packages目录下,每个组件都是一个独自的文件夹,最根本的构造是一个js文件和一个vue文件,组件反对应用Vue.component形式注册,也反对插件形式Vue.use注册,js文件就是用来反对插件形式应用的,比方Alertjs文件内容如下:

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.importedImportSpecifier.local,这两个有什么区别呢,在于是否应用了别名导入,比方:

import { Alert } from 'xui'

这种状况importedlocal是一样的,然而如果应用了别名:

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源码。

Vantantd也都是采纳这种形式,只是应用的插件不一样,这两个应用的都是babel-plugin-import,babel-plugin-component其实也是forkbabel-plugin-import

Tree Shaking形式

Vant组件库除了反对应用后面的Babel插件按需加载外还反对Tree Shaking形式,实现也很简略,Vant最终公布的代码里提供了三种标准的源代码,别离是commonjsumdesmodule,如下图:

commonjs标准是最常见的应用形式,umd个别用于cdn形式间接在页面引入,而esmodule就是用来实现Tree Shaking的,为什么esmodule能实现Tree Shakingcommonjs标准不行呢,起因是esmodule是动态编译的,也就是在编译阶段就能确定某个模块导出了什么,引入了什么,代码执行阶段不会扭转,所以打包工具在打包的时候就能剖析出哪个办法被应用了,哪些没有,没有用到的就能够释怀的删掉了。

接下来批改一下咱们的组件库,让它也反对Tree Shaking,因为咱们的组件自身就是esmodule模块,所以不须要批改,然而要批改一下导出的文件index.js,因为目前还不反对上面这种形式导出:

import { Alert } from 'xui'

减少如下代码:

// index.js// ...export {    Alert,    Tag}// ...

接下来须要批改package.json,咱们都晓得package.json里的main字段是用来批示包的入口文件,那么是不是只有把这个字段指向esmodule的入口文件就行了呢,其实是不行的,因为通常状况下它都是指向commonjs模块入口,而且一个包有可能反对nodejsweb两种环境应用,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.jsonmodulesideEffects的配置,而后从main.js里删除组件引入和注册的代码,而后批改vue.config.js文件。因为这个插件的官网文档比拟简洁,看不出个所以然,所以笔者是参考内置的 vant解析器来批改的:

返回的三个字段含意应该是比拟清晰的,importName示意引入的组件名,比方Alertpath示意从哪里引入,对于咱们的组件库就是xuisideEffects就是存在副作用的文件,根本就是配置对应的款式文件门路,所以咱们批改如下:

// 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.modulepkg.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。