组件库打包
Rollup 是一个 JavaScript 模块打包器,可以将小的代码片段编译成大的、复杂的代码库或应用程序。它使用 ES2015 的模块语法,让你可以自由地和无缝地使用你的 JavaScript 模块。
以下是一些 Rollup 的关键特性:
-
代码拆分与动态导入:Rollup 支持代码拆分和动态导入,这意味着你的代码可以被拆分为多个包,并且在运行时动态加载,以提高应用的性能。
-
树摇(Tree-shaking):这是 Rollup 的一个核心特性,它实现了静态的模块结构,可以识别出实际未被引用或未被使用的代码,移除它们以减少最终包的大小。
-
输出格式:Rollup 支持多种输出格式,包括 AMD、CommonJS、SystemJS、ESM,以及 IIFE 和 UMD 格式的自执行函数,可以用于创建 JavaScript 库。
-
插件系统: Rollup 提供了一个插件系统,可以通过插件来定制 Rollup 的行为。例如,你可以使用插件来转换 TypeScript 或 JSX,或者来处理 CSS、图片等资源。
-
支持 code splitting,可以将代码分成多个部分,使得只有需要的部分被加载,进一步减少文件体积。
准备工作
首先第一步就是安装 rollup,我们这里还是选择将 rollup 安装到工作空间里面:
pnpm add rollup rollup-plugin-typescript2 rollup-plugin-vue rollup-plugin-postcss rollup-plugin-delete rollup-plugin-copy @rollup/plugin-terser -D -w
- rollup-plugin-typescript2:这个插件提供了对 ts 的支持,用于将 ts 转换为 js
- rollup-plugin-vue:这个插件用于提供对 vue 的支持,让 rollup 能够顺利的处理 vue 文件
- rollup-plugin-postcss:该插件用于在构建过程对 CSS 相关的处理,例如我们的项目中用到了 sass,由该插件来进行处理
- rollup-plugin-delete:该插件用于删除文件或者目录,它通常用于在打包之前清除上一次打包的内容,从而保证打的包里面始终只包含这一次所打包的最新内容。
- rollup-plugin-copy:这个插件用于复制文件和目录,我们在做打包工作的时候,经常会涉及到一些文件资源需要原封不动的复制到打的包里面。
- @rollup/plugin-terser:该插件用于做 JS 的压缩工作。
打包组件
安装好依赖之后,接下来就需要书写 rollup 配置文件,下面是打包组件的配置文件代码:
import typescript from "rollup-plugin-typescript2";
import vue from "rollup-plugin-vue";
import postcss from "rollup-plugin-postcss";
import del from "rollup-plugin-delete";
import copy from "rollup-plugin-copy";
import terser from "@rollup/plugin-terser";
// 导出一个数组,该数组里面是一个一个的对象
// 每一个对象就是一个打包任务
export default [
{
// 打包组件的任务
input: "packages/components/index.ts", // 打包入口
// 打包的输出
output: {
dir: "dist/components",
format: "esm",
},
// 外部依赖,这一部分依赖不需要进行打包
external: ["vue"],
// 指定要使用的插件,注意插件是有顺序
plugins: [
del({ targets: "dist/components" }), // 先把上一次的打包内容删除掉
vue({
include: ["**/*.vue"],
}),
typescript({
tsconfig: "tsconfig.app.json",
}),
terser(),
copy({
targets: [
{ src: "packages/components/package.json", dest: "dist/components" },
],
}),
],
},
];
接下来我们来补全 packages/components/index.ts 的内容:
import Button from "./button/src/button.vue";
import Card from "./card/src/card.vue";
import type { App, Plugin } from "vue";
// 之前是每个组件有一个入口文件,每个入口文件实际上所做的事情就是给这个组件添加一个 install 方法
// 之后在其他项目中就可以单独的引入这个组件:
// import DuyiButton from '@duyiui-plus/components/button'
// import DuyiCard from '@duyiui-plus/components/card'
// 现在整体项目的入口文件,要做的事情很简单:统一给所有组件添加上 install
// 之后在其他项目中使用的时候,就可以一次性导入所有的组件
const components = [Button, Card];
const install = (app: App) => {
components.forEach((component) => {
app.component(component.name, component);
});
};
const duyiui: Plugin = {
install,
};
export default duyiui;
除了 index.ts 的补充,我们还需要对 packages/components 下面的 package.json 文件进行一个补充,内容如下:
{
"name": "@duyiui-plus/components",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"types": "types.d.ts",
"scripts": {
"test": "vitest"
},
"keywords": [],
"author": "",
"license": "ISC",
"peerDependencies": {
"vue": "^3.0.0"
}
}
- type:设置为 module,代表我们的代码使用的是 ESM 模块化规范
- types: 设置的值为 types.d.ts,这个是一个类型声明的入口文件
- peerDependencies:这个库的依赖,表示其他项目在使用我这个库的时候,需要有的依赖,这里设置的值为 vue,表示其他项目在使用这个库的时候,需要安装 vue,并且版本在 3.0.0 以上。
vue-shim.d.ts
该文件一看就是一个类型声明文件:
declare module "*.vue" {
import { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
该文件是一个 ts 的声明文件,用于告诉 ts 如何处理 .vue 类型的文件。
最后在项目根木下面的 package.json 中做如下的修改:
"type": "module",
"scripts": {
...
"build": "rollup -c"
},
打包样式
打包样式需要新书写一个打包任务,对应的代码如下:
import typescript from "rollup-plugin-typescript2";
import vue from "rollup-plugin-vue";
import postcss from "rollup-plugin-postcss";
import del from "rollup-plugin-delete";
import copy from "rollup-plugin-copy";
import terser from "@rollup/plugin-terser";
// 导出一个数组,该数组里面是一个一个的对象
// 每一个对象就是一个打包任务
export default [
{
// 打包组件的任务
input: "packages/components/index.ts", // 打包入口
// 打包的输出
output: {
dir: "dist/components",
format: "esm",
},
// 外部依赖,这一部分依赖不需要进行打包
external: ["vue"],
// 指定要使用的插件,注意插件是有顺序
plugins: [
del({ targets: "dist/components" }), // 先把上一次的打包内容删除掉
vue({
include: ["**/*.vue"],
}),
typescript({
tsconfig: "tsconfig.app.json",
}),
terser(),
copy({
targets: [
{ src: "packages/components/package.json", dest: "dist/components" },
],
}),
],
},
{
// 打包样式的任务
input: "packages/theme-chalk/src/index.scss",
output: {
file: "dist/theme-chalk/index.css",
format: "esm",
},
plugins: [
del({
targets: "dist/theme-chalk",
}),
postcss({
extract: true, // 单独生成一个 css 文件
minimize: true, // 压缩
}),
copy({
targets: [
{
src: "packages/theme-chalk/package.json",
dest: "dist/theme-chalk",
},
{
src: "packages/theme-chalk/src/fonts/*",
dest: "dist/theme-chalk/fonts",
},
],
}),
],
},
];
最后执行 pnpm build,就会将组件以及样式分别打包到 dist/components 以及 dist/theme-chalk 目录下面。
生成类型声明文件
首先第一步,在项目根目录下面的 tsconfig.app.json 文件中,添加 declaration 为 true
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"declaration": true, // add this line
"paths": {
"@/*": ["./src/*"]
}
}
之后重新打包的时候,我们就会发现在 components 下面就生成了类型说明文件。
接下来一步一步进行优化,首先 test 目录下面的文件是不需要生成类型声明文件的,针对这个情况,我们只需要修改 exclude 配置项即可:
"exclude": ["packages/components/**/test/**"],
目前为止,我们针对各个文件已经生成了类型说明文件,但是我们需要生成一个类型声明文件的入口文件,这个入口文件和 index.js 是同级别的,这个就需要我们手动的来书写代码生成。
在项目根目录下面创建 generate-types.cjs,对应的代码内容如下:
// 该文件的目的只有一个:生成类型声明文件的入口文件
const fs = require("fs");
const path = require("path");
// 定义组件和类型文件的目录路径
const componentsDir = path.resolve(
__dirname,
"dist",
"components",
"packages",
"components"
);
// 类型声明入口文件的位置
const typesFile = path.resolve(__dirname, "dist", "components", "types.d.ts");
// 获取组件目录下面所有的子目录
const components = fs.readdirSync(componentsDir);
// 定义最终写入到 types.d.ts 里面的内容
let typesContent = 'import { Plugin } from "vue";\n\n';
typesContent += "declare const duyiui: Plugin; \n";
typesContent += "export default duyiui; \n\n";
// 为每个组件的类型声明文件生成一个 export 语句
// 并且添加到 typesContent 里面
typesContent += components
.map((component) => `export * from './packages/components/${component}'`)
.join("\n");
// 至此,我们要写入到 types.d.ts 的内容已经准备好了
fs.writeFileSync(typesFile, typesContent);
console.log("类型声明文件的入口文件已经生成完毕...");
之后在项目根目录下的 package.json 中,修改脚本命令:
"scripts": {
...
"build": "rollup -c && node generate-types.cjs"
},
并且你最终要确认 packages/components 这个包的 package.json 中的 types 是正确设置了的。