关于前端:浅析组件库实现按需引入的几种方式

55次阅读

共计 6622 个字符,预计需要花费 17 分钟才能阅读完成。

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

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

正文完
 0