打包

UI组件库的打包是指将开发完成的业务代码处理成可在生产环境中运行,并供用户在浏览器上使用的过程。

在UI组件库打包的过程中,需要完成的工作如下:

(1)提供浏览器端的代码包,可以是UMD或IIFE

(2)提供Node.js环境的CommonJS模块和ESM模块代码包

(3)提供全局引入的CSS样式,并按需加载CSS样式

(4)提供source map文件

(5)对UI组件库打包的代码进行压缩

步骤 1:初始化 build 打包目录

1pnpm init

步骤 2:rollup的基础配置

安装

1pnpm install -D @vitejs/plugin-vue rollup @rollup/plugin-node-resolve rollup-plugin-esbuild

配置打包路径

build/src/common.ts

1import { fileURLToPath } from "node:url";
2import { resolve, dirname } from "node:path";
3
4export const outputPkgDir = "better-ui";
5export const filePath = fileURLToPath(import.meta.url);
6console.log('filePath', filePath);
7export const dirName = dirname(filePath);
8console.log('dirName', dirName);
9export const rootDir = resolve(dirName, "..", "..");
10console.log('rootDir', rootDir);
11export const pkgRoot = resolve(rootDir, "packages");
12console.log('pkgRoot', pkgRoot);
13export const outputDir = resolve(rootDir, outputPkgDir);
14console.log('outputDir', outputDir);
15export const outputEsm = resolve(rootDir, outputPkgDir, "es");
16console.log('outputEsm', outputEsm);
17export const outputCjs = resolve(rootDir, outputPkgDir, "lib");
18console.log('outputCjs', outputCjs);
19export const outputUmd = resolve(rootDir, outputPkgDir, "dist");
20console.log('outputUmd', outputUmd);

UMD

UMD(Universal Module Definition)打包是一种将 JavaScript 库或模块打包成可以在不同环境中使用的通用格式的方法。

UMD打包同时兼容 CommonJS、AMD和全局变量的使用方式,因此可以在项目的 <script> 中引入通过UMD打包后的产物,直接在浏览器中以访问全局变量的方式使用。

输出UMD组件包

1import { rollup } from "rollup";
2import { nodeResolve } from "@rollup/plugin-node-resolve";
3import vue from "@vitejs/plugin-vue";
4import esbuild from "rollup-plugin-esbuild";
5import { resolve } from "node:path";
6import { pkgRoot, outputUmd } from "./common.js";
7
8// umd打包
9const export umdBuildEntry = async () => {
10  const writeBundles = await rollup({
11    // 配置打包入口文件(better-ui/packages/index.ts)
12    input: resolve(pkgRoot, "index.ts"), 
13
14    // 配置插件
15    plugins: [
16      vue(),
17      nodeResolve({
18        extensions: [".ts"],
19      }),
20      esbuild(),
21    ],
22
23    // 排除不进行打包的npm包
24    external: ["vue"],
25  });
26  writeBundles.write({
27    // 指定生成的包的格式
28    format: "umd",
29    // 生成的文件
30    file: resolve(outputUmd, "index.full.js"),
31    // 自定义包的全局变量名称,也就是打包后的产物可访问的变量名称
32    name: "BetterUI",
33    // 定义UI组件库打包后所要依赖的变量
34    globals: {
35      vue: "Vue",
36    },
37  });
38};
39
40// 执行打包
41umdBuildEntry();
1node ./build/src/umdBuild.js

:::

测试UMD打包文件

1<!DOCTYPE html>
2<html lang="en">
3  <head>
4    <meta charset="UTF-8" />
5    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6    <title>Document</title>
7    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.4.15/vue.global.js"></script>
8    <script src="./better-ui/dist/index.full.js"></script>
9  </head>
10  <body>
11    <div id="app">
12      <b-button type="primary">{{message}}</b-button>
13    </div>
14    <script>
15      const { createApp, ref } = Vue;
16      const App = {
17        setup() {
18          const message = ref("浏览器引用组件库包");
19          return {
20            message,
21          };
22        },
23      };
24      const app = createApp(App);
25      app.use(BetterUI);
26      app.mount("#app");
27    </script>
28  </body>
29</html>

ESM、CJS模块化打包

ESM(ECMAScript Modules)和CJS(CommonJS)是 JavaScript 中使用的不同模块。

  • ESM是现代浏览器和Node.js支持的标准模块
  • CJS是传统意义上在 Node.js 中使用的模块系统。
1pnpm install -D fast-glob rollup-plugin-postcss
2
3node ./build/src/moduleBuild.js

ESM、CJS打包输出

1import glob from "fast-glob";
2import { nodeResolve } from "@rollup/plugin-node-resolve";
3import postcss from "rollup-plugin-postcss"
4import { rollup } from "rollup";
5import vue from "@vitejs/plugin-vue";
6import esbuild from "rollup-plugin-esbuild";
7
8import { outputPkgDir, pkgRoot, outputEsm, outputCjs } from "./common.js";
9
10const compileStyleEntry = () => {
11  const themeEntryPrefix = `@suit-ui/theme/src/`;
12  return {
13    name: "compile-style-entry",
14    resolveId(id) {
15      if (!id.startsWith(themeEntryPrefix)) return;
16      return {
17        id: id.replaceAll(themeEntryPrefix, `${outputPkgDir}/theme/src/`),
18        external: "absolute",
19      };
20    },
21  };
22};
23
24export const moduleBuildEntry = async () => {
25  const input = await glob("**/*.{js,ts,vue}", {
26    cwd: pkgRoot,
27    absolute: true,
28    onlyFiles: true,
29  });
30  const writeBundles = await rollup({
31    input,
32    plugins: [
33      compileStyleEntry(),
34      vue(),
35      nodeResolve({
36        extensions: [".ts"],
37      }),
38      esbuild(),
39      postcss({
40        pextract: true,
41      }),
42    ],
43    external: ["vue", "@vue/shared", "async-validator"],
44  });
45
46  writeBundles.write({
47    format: "esm",
48    dir: outputEsm,
49    preserveModules: true,
50    entryFileNames: "[name].mjs",
51    sourcemap: true
52  })
53  writeBundles.write({
54    format: "cjs",
55    dir: outputCjs,
56    preserveModules: true,
57    entryFileNames: "[name].cjs",
58    sourcemap: true
59  })
60};
61moduleBuildEntry()

测试模块化组件包

在打包产物根目录

1{
2  "name": "better-ui",
3  "version": "1.0.0",
4  "description": "",
5  "main": "./lib/index.cjs",
6  "module": "./es/index.mjs",
7  "directories": {
8    "lib": "lib"
9  },
10  "scripts": {
11    "test": "echo \"Error: no test specified\" && exit 1"
12  },
13  "keywords": [],
14  "author": "",
15  "license": "ISC"
16}

建立全局链接

1npm link

本地测试

play目录

1npm link better-ui

全局引入

1import { createApp } from "vue";
2import App from "./App.vue";
3
4// 全局引入
5import BetterUI from "better-ui";
6
7const app = createApp(App);
8app.use(BetterUI);
9app.mount("#app");
1<script setup lang="ts"></script>
2
3<template>
4  <div>
5    <b-button>默认</b-button>
6    <b-button type="primary">主要</b-button>
7    <b-button type="success">成功</b-button>
8    <b-button type="warning">警告</b-button>
9    <b-button type="error">错误</b-button>
10  </div>
11</template>
12
13<style scoped></style>

按需引入

1<script setup lang="ts">
2import { BButton } from "better-ui";
3</script>
4
5<template>
6  <div>
7    <b-button>默认</b-button>
8    <b-button type="primary">主要</b-button>
9    <b-button type="success">成功</b-button>
10    <b-button type="warning">警告</b-button>
11    <b-button type="error">错误</b-button>
12  </div>
13</template>
14
15<style scoped></style>

Gulp打包scss文件

build目录

1pnpm add -D gulp gulp-sass sass gulp-autoprefixer gulp-clean-css gulp-concat del @esbuild-kit/cjs-loader @esbuild-kit/cjs-loader

styleBuild.js

1import gulp from "gulp";
2import dartSass from "sass";
3import gulpSass from "gulp-sass";
4import autoprefixer from "gulp-autoprefixer";
5import cleanCSS from "gulp-clean-css";
6import gulpConcat from "gulp-concat";
7//删除文件或者文件夹
8import { deleteAsync } from "del";
9import { rootDir, pkgRoot, outputDir, outputUmd } from "./common.js";
10
11/**
12 * 全量打包CSS
13 */
14const buildScssFull = async () => {
15  const sass = gulpSass(dartSass);
16  await new Promise((resolve) => {
17    gulp
18      .src(`${pkgRoot}/theme/src/index.scss`) // 指定打包入口
19      .pipe(sass.sync()) // 编译
20      .pipe(autoprefixer({ cascade: false })) // 浏览器兼容
21      .pipe(cleanCSS()) // 压缩
22      .pipe(gulpConcat("index.min.css")) // 合并到指定文件
23      .pipe(gulp.dest(outputUmd)) // 输出到指定目录dist
24      .on("end", resolve); // 监听流完成
25  });
26};
27
28/**
29 * 按需加载打包CSS
30 */
31const buildScssModules = async () => {
32  const sass = gulpSass(dartSass);
33  await new Promise((resolve) => {
34    gulp
35      .src(`${rootDir}/packages/theme/src/**/*.scss`)
36      .pipe(sass.sync()) // 编译
37      .pipe(autoprefixer({ cascade: false })) // 兼容
38      .pipe(cleanCSS()) // 压缩
39      .pipe(gulp.dest(`${outputDir}/theme`))
40      .on("end", resolve); // 监听流完成
41  });
42  // 删除指定文件
43  deleteFiles();
44};
45
46/**
47 * 拷贝scss
48 */
49const cloneScss = async () => {
50  await new Promise((resolve) => {
51    gulp
52      .src(`${pkgRoot}/theme/src/**/*`)
53      .pipe(gulp.dest(`${outputDir}/theme/src`))
54      .on("end", resolve); // 监听流完成
55  });
56};
57
58/**
59 * 删除指定文件或文件夹
60 */
61const deleteFiles = async () => {
62  await deleteAsync(
63    [`${outputDir}/theme/index.css`, `${outputDir}/theme/common`],
64    { force: true }
65  );
66};
67
68export const buildStyle = async () => {
69  await Promise.all([cloneScss(), buildScssFull(), buildScssModules()]);
70};
71
72// 执行打包
73buildStyle();

Gulp多任务

build/src/index.js

1export * from "./umdBuild.js";
2export * from "./moduleBuild.js";
3export * from "./styleBuild.js";

build/gulpfile.js

1import gulp from "gulp";
2import {
3  umdBuildEntry,
4  moduleBuildEntry,
5  buildStyle,
6} from "./src/index.js";
7
8// 执行串行任务
9export default gulp.series(
10  gulp.series(
11    umdBuildEntry,
12    moduleBuildEntry,
13    buildStyle,
14  )
15);

build/package.json

gulp装4版本,装5版本执行esbuild-kit/cjs-loader会报错

1{
2  "scripts": {
3    "start": "gulp --require @esbuild-kit/cjs-loader  -f gulpfile.js"
4  },
5}

package.json

1{
2  "name": "better-ui",
3  "version": "1.0.0",
4  "description": "",
5  "main": "index.js",
6  "scripts": {
7    "build": "pnpm -C build start"
8  },
9  "keywords": [],
10  "author": "",
11  "license": "ISC",
12  "devDependencies": {
13    "@types/node": "^22.10.2",
14    "sass": "^1.83.0",
15    "sass-loader": "^16.0.4",
16    "typescript": "^5.7.2"
17  },
18  "dependencies": {
19    "@better-ui/components": "workspace:^",
20    "@better-ui/hooks": "workspace:^",
21    "@better-ui/theme": "workspace:^",
22    "@better-ui/utils": "workspace:^"
23  }
24}

删除组件包

确保打包的组件包都是最新的

build/src/files.js

Build/index.js

1export * from "./files.js";
2export * from "./umdBuild.js";
3export * from "./moduleBuild.js";
4export * from "./styleBuild.js";

Build/gulpfile.js

1import gulp from "gulp";
2import {
3  deletePkg,
4  umdBuildEntry,
5  moduleBuildEntry,
6  buildStyle,
7} from "./src/index.js";
8
9// 执行串行任务
10export default gulp.series(
11  gulp.series(deletePkg, umdBuildEntry, moduleBuildEntry, buildStyle)
12);

生成package.json

packages/packages.json

1{
2  "name": "better-ui",
3  "version": "1.0.0",
4  "description": "A Vue3 UI library",
5  "main": "./lib/index.cjs",
6  "module": "./es/index.mjs",
7  "directories": {
8    "lib": "lib"
9  },
10  "scripts": {
11    "test": "echo \"Error: no test specified\" && exit 1"
12  },
13  "keywords": [
14    "UI 组件库",
15    "Vue3"
16  ],
17  "peerDependencies": {
18    "vue": "^3.4.0"
19  },
20  "author": {
21    "name": "dancy",
22    "email": "codebetter@163.com",
23    "url": "https://www.codebetter.cn"
24  },
25  "license": "ISC"
26}