组件库会包含几十甚至上百个组件,但是应用的时候往往只使用其中的一部分。这个时候如果全部引入到项目中,就会使输出产物体积变大。按需加载的支持是组件库中必须考虑的问题。
目前组件的按需引入会分成两个方法:
- 经典方法:组件单独分包 + 按需导入 + babel-plugin-component ( 自动化按需引入);
- 次时代方法:ESModule + Treeshaking + 自动按需 import(unplugin-vue-components 自动化配置)。
分包与树摇(Treeshaking)
传统的解决方案就是将组件库分包导出,比如将组件库分为 List、Button、Card,用到哪个加载哪个,简单粗暴。这样写有两个弊端:
- 需要了解软件包内部构造 例: import "ui/xxx" or import "ui/package/xxx";
- 需要不断手工调整组件加载预注册。
// 全部导入
const SmartyUI = require("smarty-ui-vite")
// 单独导入
const Button = require("smarty-ui-vite/button")
好在后面有 babel-plugin-component,解决了需要了解软件包构造的问题。当然你需要按照约定规则导出软件包。
// 转换前
const { Button } = require("smarty-ui-vite")
// 转换后
const Button = require("smarty-ui-vite/button")
随着时代的发展,esmodule 已经成为了前端开发的主流。esmodule 带来好处是静态编译,也就是说,在编译阶段就可以判断需要导入哪些包。
// 动态引入的不可确定性
const m = (Math.random() > 0.5) ? require("a") : require("b")
这样就给 Treeshaking 提供了可能。Treeshaking 是一种通过语法分析去除无用代码的方法。目前,Treeshaking 逐渐成为了构建工具的标配,Rollup、Vite、新版本的 Webpack 都支持了这个功能。
比如:组件库只使用了 Button。
import { Button } from 'smarty-ui-vite'
使用 ES 模块并且只引用了 Button,编译器会自动将其他组件的代码去掉。
自动导入黑科技
unplugin-vue-components 的 auto importing 支持。
用户故事(UserStory)
为组件库添加按组件分包导出功能,适配按需加载需要。
任务分解(Task)
- 实现分包导出脚本;
- 测试按需加载。
实现分包导出
分包导出相当于将组件库形成无数各子软件包,软件包必须满足一下要求:
- 文件名即组件名;
- 独立的 package.json 定义,包含 esm 和 umd 的入口定义;
- 每个组件必须以 Vue 插件形式进行加载;
- 每个软件包还需要有单独的 css 导出。
- 重构代码结构
首先需要在原有代码上进行重构:
- 首先将组件目录由 【button】 改为 【Button】
特别提醒:git 提交的时候注意,默认 git 修改的时候是忽略大小写的。需要修改一下 git 配置才可以提交。
# 禁止忽略大小写
git config core.ignorecase false
- Button 组件入口 index.ts 默认作为插件导出。
重构前:
import Button from "./Button";
import { App } from "vue";
// 导出 Button 组件
export default Button
重构后:
import Button from "./Button";
import { App } from "vue";
// 导出Button组件
export { Button };
// 导出Vue插件
export default {
install(app: App) {
app.component(Button.name, Button);
},
};
另外要注意的是,原 uno.css 是在 entry.ts 中引入的,在子组件包中将不会以 entry.ts 为入口。如果希望 uno.css 能够正常 build,需要在子组件包入口 index.ts 引入 uno.css。
- 编写分包导出脚本
默认导出方式是通过配置 vite.config.ts 的 build 属性完成。但是在分包导出的时候需要每个组件都分别配置自己的配置文件,而且需要由程序自动读取组件文件夹,根据文件夹的名字遍历打包,还需要为每个子组件包配上一个 package.json 文件。
新建一个 scripts/build.ts 文件。
首先需要学会的是如何使用代码让 vite 打包。
导入 vite.config.ts中的配置,然后调用 vite 的 build api 打包。
// 读取 vite 配置
import { config } from "../vite.config";
import { build, InlineConfig, defineConfig, UserConfig } from "vite";
// 全量打包
build(defineConfig(config as UserConfig) as InlineConfig);
读取组件文件夹遍历组件库文件夹。
const srcDir = path.resolve(__dirname, "../src/");
fs.readdirSync(srcDir)
.filter((name) => {
// 过滤文件只保留包含index.ts
const componentDir = path.resolve(srcDir, name);
const isDir = fs.lstatSync(componentDir).isDirectory();
return isDir && fs.readdirSync(componentDir).includes("index.ts");
})
.forEach(async (name) => {
// 文件夹遍历
});
为每个模块定制不同的编译规则。编译规则如下:
- 导出文件夹为 dist/ <组件名>/ 例: dist/Button;
- 导出模块名为: index.es.js、index.umd.js;
- 导出模块名为: <组件名> iffe 中绑定到全局的名字。
const outDir = path.resolve(config.build.outDir, name);
const custom = {
lib: {
entry: path.resolve(srcDir, name),
name, // 导出模块名
fileName: `index`,
formats: [`esm`, `umd`],
},
outDir,
};
Object.assign(config.build, custom);
await build(defineConfig(config as UserConfig) as InlineConfig);
最后还需要为每个子组件包定制一个自己的 package.json。因为根据 npm 软件包规则,当你 import 子组件包的时候,会根据子包中的 package.json 文件找到对应的模块。
// 读取
import Button from "smarty-ui-vite/Button"
子包的 package.json。
{
"name": "smarty-ui-vite/Button",
"main": "index.umd.js",
"module": "index.umd.js"
}
生成 package.json 使用模版字符串实现。
fs.outputFile(
path.resolve(outDir, `package.json`),
`{
"name": "smarty-ui-vite/${name}",
"main": "index.umd.js",
"module": "index.umd.js",
}`,
`utf-8`
);
最后把上面的代码组合一下就可以了。
import fs from "fs-extra";
import path from "path";
import { config } from "../vite.config";
import { build, InlineConfig, defineConfig, UserConfig } from "vite";
const buildAll = async () => {
// const inline: InlineConfig =
// viteConfig;
// 全量打包
await build(defineConfig(config as UserConfig) as InlineConfig);
// await build(defineConfig({}))
const srcDir = path.resolve(__dirname, "../src/");
fs.readdirSync(srcDir)
.filter((name) => {
// 只要目录不要文件,且里面包含index.ts
const componentDir = path.resolve(srcDir, name);
const isDir = fs.lstatSync(componentDir).isDirectory();
return isDir && fs.readdirSync(componentDir).includes("index.ts");
})
.forEach(async (name) => {
const outDir = path.resolve(config.build.outDir, name);
const custom = {
lib: {
entry: path.resolve(srcDir, name),
name, // 导出模块名
fileName: `index`,
formats: [`es`, `umd`],
},
outDir,
};
Object.assign(config.build, custom);
await build(defineConfig(config as UserConfig) as InlineConfig);
fs.outputFile(
path.resolve(outDir, `package.json`),
`{
"name": "smarty-ui-vite/${name}",
"main": "index.umd.js",
"module": "index.umd.js",
}`,
`utf-8`
);
});
};
buildAll();
由于脚本是使用typescript编写的所以需要使用 esno 运行。
pnpm i esno -D
在package.json中添加脚本
"scripts": {
"build": "pnpm build:components",
"build:all": "vite build",
"build:components": "esno ./scripts/build.ts",
}
运行代码
pnpm build
测试按需加载
假设页面中只使用 Button 按钮,那么只调用Button子包中的 js、css 就可以了。
<h1>Demo IFFE OnDemand</h1>
<div id="app"></div>
<link rel="stylesheet" href="../../dist/style.css">
<script src="../../node_modules/vue/dist/vue.global.js"></script>
<script src="../../dist/button/index.iife.js"></script>
<script>
const { createApp } = Vue
console.log('vue', Vue)
console.log('SmartyUI', Button)
createApp({
template: `
<div style="margin-bottom:20px;">
<SButton color="blue">主要按钮</SButton>
<SButton color="green">绿色按钮</SButton>
<SButton color="gray">灰色按钮</SButton>
<SButton color="yellow">黄色按钮</SButton>
<SButton color="red">红色按钮</SButton>
</div>
<div style="margin-bottom:20px;"
>
<SButton color="blue" plain>朴素按钮</SButton>
<SButton color="green" plain>绿色按钮</SButton>
<SButton color="gray" plain>灰色按钮</SButton>
<SButton color="yellow" plain>黄色按钮</SButton>
<SButton color="red" plain>红色按钮</SButton>
</div>
<div style="margin-bottom:20px;">
<SButton size="small" plain>小按钮</SButton>
<SButton size="medium" plain>中按钮</SButton>
<SButton size="large" plain>大按钮</SButton>
</div>
<div style="margin-bottom:20px;">
<SButton color="blue" round plain icon="search">搜索按钮</SButton>
<SButton color="green" round plain icon="edit">编辑按钮</SButton>
<SButton color="gray" round plain icon="check">成功按钮</SButton>
<SButton color="yellow" round plain icon="message">提示按钮</SButton>
<SButton color="red" round plain icon="delete">删除按钮</SButton>
</div>
<div style="margin-bottom:20px;">
<SButton color="blue" round plain icon="search"></SButton>
<SButton color="green" round plain icon="edit"></SButton>
<SButton color="gray" round plain icon="check"></SButton>
<SButton color="yellow" round plain icon="message"></SButton>
<SButton color="red" round plain icon="delete"></SButton>
</div>
`
})
.use(Button.default)
.mount('#app')
</script>
复盘
这节课的主要内容是为组件库添加分包导出功能,使组件库提供按需加载。组件库具备良好的按需加载能力,可以使提高页面性能。虽然目前 ESM Treeshaking 已经非常流行,但是还是有很多场合需要分包按需引入的支持。
另外,分包引入需要每个子组件包都分别使用不同的配置调用 vite 导出。这需要编写相对较为复杂的脚本完成。工程化中很重要的一部分就是要根据实际需求编写自动化的脚本。
这节课已进行了一个简单的实践,在编写的时候特别要注意配置的复用性。比如:子包与主包是在一个一个配置文件下做的不同的配置,这个然叔都是动了一番脑筋才实现的。
最后留一些思考题帮助大家复习,也欢迎在留言区讨论。
- 组件如何才能实现按需引入?
- 如何实现组件分包导出?
- 如何批量生成 package.json 文件?
下节课,我们将给大家讲解如何将开发文档发布上线,下节课见。