组件库会包含几十甚至上百个组件,但是应用的时候往往只使用其中的一部分。这个时候如果全部引入到项目中,就会使输出产物体积变大。按需加载的支持是组件库中必须考虑的问题。
目前组件的按需引入会分成两个方法:
babel-plugin-component
( 自动化按需引入);ESModule
+ Treeshaking
+ 自动按需 import
(unplugin-vue-components
自动化配置)。传统的解决方案就是将组件库分包导出,比如将组件库分为 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
导出。首先需要在原有代码上进行重构:
特别提醒:git 提交的时候注意,默认 git 修改的时候是忽略大小写的。需要修改一下 git 配置才可以提交。
# 禁止忽略大小写
git config core.ignorecase false
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
///
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 buttontitle>
<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: `
主要按钮
绿色按钮
灰色按钮
黄色按钮
红色按钮
搜索按钮
编辑按钮
成功按钮
提示按钮
删除按钮
`}).use(Button.default).mount('#app')
script>
body>
html>
pnpm dev
浏览器查看结果