组件库会包含几十甚至上百个组件,但是应用的时候往往只使用其中的一部分。这个时候如果全部引入到项目中,就会使输出产物体积变大。按需加载的支持是组件库中必须考虑的问题。
目前组件的按需引入会分成两个方法:
- 经典方法:组件单独分包 + 按需导入 +
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
带来好处是静态编译,也就是说,在编译阶段就可以判断需要导入哪些包。
这样就给 Treeshaking
提供了可能。Treeshaking
是一种通过语法分析去除无用代码的方法。目前,Treeshaking
逐渐成为了构建工具的标配,Rollup
、Vite
、新版本的 Webpack
都支持了这个功能。
比如:组件库只使用了 Button
。
import { Button } from 'smarty-ui-vite'
使用 ES 模块并且只引用了 Button,编译器会自动将其他组件的代码去掉。
实现分包导出
分包导出相当于将组件库形成无数各子软件包,软件包必须满足一下要求:
- 文件名即组件名;
- 独立的
package.json
定义,包含esm
和umd
的入口定义; - 每个组件必须以
Vue
插件形式进行加载; - 每个软件包还需要有单独的
css
导出。
- 重构代码结构
首先需要在原有代码上进行重构:
- 首先将组件目录由 【button】 改为 【Button】
特别提醒:git 提交的时候注意,默认 git 修改的时候是忽略大小写的。需要修改一下 git 配置才可以提交。
# 禁止忽略大小写
git config core.ignorecase false
- Button 组件入口
index.ts
默认作为插件导出。
重构前:
import Button from "./Button";
// 导出 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);
},
};
- 编写分包导出脚本
默认导出方式是通过配置 vite.config.ts
的 build
属性完成。但是在分包导出的时候需要每个组件都分别配置自己的配置文件,而且需要由程序自动读取组件文件夹,根据文件夹的名字遍历打包,还需要为每个子组件包配上一个 package.json
文件。
新建一个 scripts/build.ts
文件。
首先需要学会的是如何使用代码让 vite
打包。
导入 vite.config.ts
中的配置,然后调用 vite
的 build api
打包。
- 修改
vite.config.ts
/// <reference types="vitest" />
import { defineConfig,UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// 引入重构后的unocss配置
import UnoCSS from './config/unocss'
const rollupOptions = {
external: ['vue', 'vue-router'],
output: {
globals: {
vue: 'Vue',
},
},
}
export const config = {
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
},
},
plugins: [
vue(), // VUE插件
vueJsx({}), // JSX 插件
UnoCSS(), // 这是重构后的unocss配置
// 添加UnoCSS插件
// Unocss({
// presets: [presetUno(), presetAttributify(), presetIcons()],
// }),
],
// 添加库模式配置
build: {
rollupOptions,
cssCodeSplit: true, // 追加 css代码分割
minify: "terser",
sourcemap: true, // 输出单独的source文件
reportCompressedSize: true, // 生成压缩大小报告
lib: {
entry: './src/entry.ts',
name: 'SmartyUI',
fileName: 'smarty-ui',
// 导出模块格式
formats: ['es', 'umd', 'iife'],
},
outDir: "./dist"
},
test: {
// enable jest-like global test APIs
globals: true,
// simulate DOM with happy-dom
// (requires installing happy-dom as a peer dependency)
environment: 'happy-dom',
// 支持tsx组件,很关键
transformMode: {
web: [/.[tj]sx$/]
}
}
}
export default defineConfig(config as UserConfig)
- 读取
vite
配置
文件名:scripts/build.ts
// 读取 vite 配置
import { config } from "../vite.config";
import { build, InlineConfig, defineConfig, UserConfig } from "vite";
// 全量打包
build(defineConfig(config as UserConfig) as InlineConfig);
- 读取组件文件夹遍历组件库文件夹
文件名:scripts/build.ts
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
最后还需要为每个子组件包定制一个自己的 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";
// 读取 vite 配置
import {config} from "../vite.config";
import { build, InlineConfig, defineConfig, UserConfig } from "vite";
const buildAll = async () => {
// 全量打包
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
代码中还使用的fs
的功能所以需要安装fs-extra
pnpm i fs-extra -D
- 在
package.json
中添加脚本
"scripts": {
"build": "pnpm build:components",
"build:all": "vite build",
"build:components": "esno ./scripts/build.ts",
}
运行代码
pnpm build
测试按需加载
假设页面中只使用 Button 按钮,那么只调用Button子包中的 js、css 就可以了。
文件名:demo/iffe/test_button.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>test button</title>
<link rel="stylesheet" href="../../dist/Button/assets/index.cb9ba4f4.css">
<script src="../../node_modules/vue/dist/vue.global.js"></script>
<script src="../../dist/Button/index.iife.js"></script>
</head>
<body>
<div id="app"></div>
<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" icon="search">搜索按钮</SButton>
<SButton color="green" icon="edit">编辑按钮</SButton>
<SButton color="gray" icon="check">成功按钮</SButton>
<SButton color="yellow" icon="message">提示按钮</SButton>
<SButton color="red" icon="delete">删除按钮</SButton>
</div>
<div style="margin-bottom:20px;">
<SButton color="blue" icon="search"></SButton>
<SButton color="green" icon="edit"></SButton>
<SButton color="gray" icon="check"></SButton>
<SButton color="yellow" icon="message"></SButton>
<SButton color="red" icon="delete"></SButton>
</div>
`}).use(Button.default).mount('#app')
</script>
</body>
</html>
- 启动项目
pnpm dev
浏览器查看结果