【从 0 到 1 搭建 Vue 组件库框架】4. 定制组件库的打包体系

导航

导航:0. 导论

上一章节: 3. 集成 lint 代码规范工具

下一章节:(编写中)5. 设计组件库的样式方案

本章节示例代码仓:Github

这节分享的内容是偏个性化的,涉及的前置内容也比较多,需要读者尽可能多地掌握包管理、依赖、构建工具(Vite)的相关知识,才方便跟进思路,理解内容。我推荐大家可以先对之前的相关内容做一些回顾:

1. 基于 pnpm 搭建 monorepo 工程目录结构

2. 在 monorepo 模式下集成 Vite 和 TypeScript - 上

2. 在 monorepo 模式下集成 Vite 和 TypeScript - 下

下面所介绍的只是一种实现统一打包体系的思路,其中有借鉴其他开源软件的成分,也有根据我自己的想法以及结合现有架构自行调整的成分,并不是什么金科玉律。不熟悉的朋友可以跟着流程实践尝试,努力形成自己的理解;熟悉的朋友可以看看能不能从中获取一些新的思路,也欢迎提出自己的想法和建议。

为什么要定制打包体系

公共配置提取

我们先前 2. 在 monorepo 模式下集成 Vite 和 TypeScript - 上 中,对于每一个子包都要准备一个 vite.config.ts 文件,例如:

ts 复制代码
// packages/button/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  // 增加插件的使用
  plugins: [vue()],
  build: {
    lib: {
      entry: './src/index.ts',
      name: 'OpenxuiButton',
      fileName: 'openxui-button',
    },
    minify: false,
    rollupOptions: {
      external: [
        // 除了 @openxui/shared,未来可能还会依赖其他内部模块,不如用正则表达式将 @openxui 开头的依赖项一起处理掉
        /@openxui.*/, 
        'vue'
      ],
    },
  }
})

其实,各个包的打包配置,其中有 99% 都是相同的。 即使存在着外部依赖(rollupOptions.external),包名(lib.fileName) 这样的不同配置,仔细思考也会发现它们都能够从各自的 package.json 中自动获取:

  • 包名、全局变量名可以根据 name 字段生成,为啥要手动填写呢?
  • 需要被外部化处理的依赖项也在 peerDependenciesdependencies 中,为啥要手动维护呢?
json 复制代码
// packages/button/package.json
{
  "name": "@openxui/button",

  // 其他配置。。。

  "peerDependencies": {
    "vue": ">=3.0.0"
  },
  "dependencies": {
    "@openxui/shared": "workspace:^"
  }
}

构建全量产物

除了上面的原因,还有一个因素促使我们去定制打包体系:我们要不要提供更丰富的产物供用户选择?

目前我们构建出的 umdes 以及 d.ts 类型声明产物,足以让通过包管理器(npm / pnpm)集成组件的用户正常使用,我们称这种产物为常规产物常规产物适用于构建场景,必须配合包管理器使用。

但是这些产物如果直接通过 <script src="xxxx"></script> 的方式引入,是无法正常工作的。这是因为我们的 umd 产物经过了依赖外部化处理(回顾:2. 在 monorepo 模式下集成 Vite 和 TypeScript - 上),直接引用会缺少大量依赖。这种场景需要取消依赖的外部化处理,构建出全量产物。 例如 element-plus 产物中的 dist/index.full.jsvue 产物中的 vue(.runtime).global.js全量产物适用于非构建场景,不必配合包管理器使用。

全量产物的使用场景其实是不容忽视的,一方面,许多新手用户需要这样一个快速上手的途径(可以暂时不折腾构建工具);另一方面,在线演示的沙盒环境也正需要这样的产物。

在全量产物的基础上,还可以进一步扩展:

这时我们回过头来想想,Vite 对于这些各式各样的打包需求都能支持,但是它们各自对应了不同的打包配置,Vite 无法在一次构建中生成全部类型的产物 。 这就更需要我们在 Vite 的基础上增强构建能力,演化出自己的打包体系。

总结

根据上面提到的线索,我们总结出以下定制打包体系的理由:

  • 促进 Clean Code,消除大量的重复代码。
  • 集中维护构建配置,避免分散管理造成的多地多次修改的困扰。
  • 提高构建的自动化程度,进一步降低维护构建配置的心智负担。
  • 增强构建能力,支持同时生成不同类型的产物。

当然,我们的例子中为了演示方便,只有寥寥几个子包,体现不出优化工作的必要性。但是我们去看那些成熟的 monorepo 开源软件仓库,动辄几十个子包,对于它们而言,则是实实在在的需求推动了它们的优化。

打包体系初步分析

如果你观察过很多开源软件,会发现它们的打包路数都五花八门、各不相同。由此可以看出,打包体系是"因包而异"的。在动手实现前,我们有必要先分析并设计自己的体系。

我们先以一款优秀的 2D 渲染库 pixi.js(采用 monorepo 方式组织) 为案例,学习其统一构建的流程:

  • 每个子包中移除了专属的 rollup.config 或者 vite.config 文件。
  • 在根目录下有一个集中的脚本,一口气完成整个构建任务。
  • 在构建脚本中,首先获取所有的子包工作目录。
  • 遍历上一步获取到的子包列表,获取每个子包的文件目录、package.json 等信息,在循环中结合子包信息动态拼接出每个包构建配置。
  • 最后调用构建工具 API / CLI 读取配置,执行构建。

其过程大致可以用以下流程表现出来:

flowchart TB s[开始] f[结束] g[getPkgs] r[getPkgInfo] ge[generateConfig] b[build] ef{endForEach?} n[nextPkg] s --> g -- forEach --> n --> r --> ge --> b --> ef ef -- Y --> f ef -- N --> n
  • getPkgs - 获取待构建的子包。
  • forEachnextPkgendForEach? - 控制子包列表的遍历。
  • getPkgInfo - 获取子包 package.json 等信息。
  • generateConfig - 生成子包的构建配置。
  • build - 执行构建。

我们的组件库并不计划编写一个集中脚本去编排所有流程 ,我们打算充分利用包管理器和构建工具的能力。之前在 2. 在 monorepo 模式下集成 Vite 和 TypeScript - 上 中讲过,pnpm 本身的命令,就足以实现"获取所有待构建的子包"、"对子包进行拓扑排序"、"遍历子包执行脚本"。

bash 复制代码
pnpm --filter ./packages/** run build

因此,上述例子中的获取子包 getPkgs,循环控制 forEach 系列,我们都不打算自己实现,而是借助 pnpm 本身的能力。

另外,我们也不计划废除每个子包中的 vite.config,去调用 Vite 的 API 单独实现 build 构建方法。而是计划在 vite.config 中调用公共的 generateConfig 方法直接生成完善的打包配置,通过 vite build 的 CLI 命令去读取配置,启动构建进程。

届时,vite.config 配置会大幅简化,变成类似下面的形式。

ts 复制代码
import { generateConfig } from '@openxui/build'

export default generateConfig(/** ... */);

这样一来,我们就只需要考虑如何实现生成打包配置的 generateConfig 方法 。当然,generateConfig 的实现也依赖于 getPkgInfo 对各自子包 package.json 信息的获取

实现前准备

创建 build 子模块

在正式实现打包体系之前,我们先做好以下准备:创建 packages/build 子包作为组件库项目的"构建能力"模块,所有与构建配置相关的封装都集中于此。

bash 复制代码
📦openx-ui
 ┣ 📂...
 ┣ 📂packages
 ┃ ┣ 📂build
 ┃ ┃ ┣ 📂src
 ┃ ┃ ┃ ┣ 📜...            # 其他源码文件
 ┃ ┃ ┃ ┗ 📜index.ts       # 源码出口
 ┃ ┃ ┣ 📜package.json     # 包的信息
 ┃ ┃ ┗ 📜vite.config.ts   # 构建配置
 ┃ ┗ 📂...
 ┗ 📜...

build 包的 package.json 可参考其他子包(例如 openxui/shared,回顾:2. 在 monorepo 模式下集成 Vite 和 TypeScript - 上)的形式填写,唯一需要注意 peerDependencies 的不同:

json 复制代码
// packages/build/package.json
{
  // 其他相似配置省略...

  "peerDependencies": {
    // 构建模块的上游正是我们集成的构建工具 `Vite`
    "vite": ">=3.0.0",
    "vue": ">=3.0.0"
  },
  "peerDependenciesMeta": {
    "vue": {
      "optional": true
    }
  },
}

build 包中的 vite.config 暂时不管,等实现打包体系后,用新的方式获取构建配置。

build 子模块依赖注意事项

在实现 build 包的过程中,需要注意以下两点:

  1. build 包不引用其他子包内部依赖。
  2. build 包谨慎安装只提供 esm 产物的外部依赖。

按照先前的规划,我们要在 vite.config 中直接引入源码获取构建配置:

ts 复制代码
// packages/build/vite.config.ts
import { generateConfig } from './src';

export default generateConfig(/** ... */);

但是,Vite 解析配置文件 vite.config 的方式却相对简单直接,本质上是先用 esbuildts 配置解析成 js,再通过 Node.js 原生的机制加载,并不像构建代码那样时做了很多兼容性处理

关于第一条注意事项 。这是由于加载 vite.config 时,各种路径别名能力------无论是 tsconfigpaths 还是 Vite 自身的 alias 都无法作用,内部模块之间的引用无法定位到正确的源码。

ts 复制代码
// build 模块中引入内部依赖,会导致 `vite.config` 配置文件解析出错。
import {/** ... */} from '@openxui/shared'

关于第二条注意事项.ts 后缀的配置文件在 package.json 中不声明 "type": "module" 的情况下无法使用 esm 模块。这个例子甚至更好复现,我们试着在一个 vite.config.ts 中引入纯 esm 依赖 lodash-es

ts 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { camelCase } from 'lodash-es'

console.log(camelCase('hello'));

export default defineConfig({
  plugins: [vue()],
})

这种情况下,我们只能把配置文件改成 vite.config.mts 来适应这些纯 esm 产物的外部依赖。

前端工程化探索的道路上永远充满了你意想不到的坑。但是只要我们发现问题时能够冷静分析原因,及时补充相关知识,它们终将变得不再神秘而费解。这就是为何我在前面的分享中,总是希望大家在实战的同时,也能同步了解包管理、模块化、配置解析、构建过程等细节。

想要详细了解这个踩坑过程的话,可以阅读下面的资料:

Issue:Make vite.config.ts support pure esm modules without setting type: "module"

Issue: ESM-only packages are not supported in vite.config.ts

Vite是如何对我们写的(vite.config.x)进行解析?

实现打包体系

接下来,我们开始编写代码完成 @openxui/build 子模块,并在过程中穿插实现思路的讲解。

代码组织

下面是 @openxui/build 的源码文件结构。我们依然将 src/index.ts 作为模块的出口;generateConfig 中负责实现生成构建配置的主体方法;由于不方便引入其他公共方法模块,我们需要建立 utils 目录统一存放本模块用到的公共方法。

bash 复制代码
# packages/build/src
📦src
 ┣ 📂generateConfig   # 实现生成构建配置的主体方法
 ┃ ┣ 📜external.ts    # 依赖外部化相关,获取构建配置的 build.rollupOptions.external 字段
 ┃ ┣ 📜index.ts       # 模块出口,主题方法实现,整合构建配置
 ┃ ┣ 📜lib.ts         # 产物相关,获取构建配置的 build.lib 字段
 ┃ ┣ 📜options.ts     # 配置项对象声明
 ┃ ┣ 📜pluginMoveDts.ts            # 移动 d.ts 产物的自定义插件
 ┃ ┣ 📜pluginSetPackageJson.ts     # 自动将产物路径写入 package.json 的自定义插件
 ┃ ┗ 📜plugins.ts     # 插件相关,获取构建配置的 plugins 字段
 ┃
 ┣ 📂utils            # 存放本模块用到的公共方法
 ┃ ┣ 📜formatVar.ts   # 变量名格式转换方法,如驼峰式,连字符式等
 ┃ ┣ 📜index.ts       # 公共方法统一出口
 ┃ ┣ 📜json.ts        # JSON 文件的读写
 ┃ ┣ 📜resolvePath.ts # 路径的处理方法
 ┃ ┗ 📜typeCheck.ts   # 判断对象类型的方法
 ┃
 ┗ 📜index.ts         # 模块出口

generateConfig - 获取构建配置的主方法

我们在 src/generateConfig/index.ts 中实现 generateConfig 的主体方法:

  • 首先要处理自定义的构建选项 options,并且读取子包的 package.json。它们将决定生成构建配置的具体行为。
  • 生成构建配置的整体过程是比较复杂的,于是我们将其拆分成三部分:
    • 产物相关的配置build.lib
    • 依赖相关的配置build.rollupOptions.external
    • 插件相关的配置plugins
  • 最后为了使我们的打包体系具有扩展性,我们还要将初步生成的构建配置与用户自定义的 Vite 配置做一个深合并,得到最终构建配置。
ts 复制代码
// packages/build/src/generateConfig/index.ts
import { mergeConfig, UserConfig } from 'vite';
import { PackageJson } from 'type-fest';
import { readJsonFile, absCwd } from '../utils';
import { getOptions, GenerateConfigOptions } from './options';
import { getPlugins } from './plugins';
import { getExternal } from './external';
import { getLib } from './lib';

/**
 * 生成 Vite 构建配置
 * @param customOptions 自定义构建选项
 * @param viteConfig 自定义 vite 配置
 */
export async function generateConfig(
  customOptions?: GenerateConfigOptions,
  viteConfig?: UserConfig,
) {
  /** 获取配置选项 */
  const options = getOptions(customOptions);

  // 获取每个子包的 package.json 对象
  const packageJson = await readJsonFile<PackageJson>(absCwd('package.json'));

  // 生成产物相关配置 build.lib
  const libOptions = getLib(packageJson, options);

  // 生成依赖外部化相关配置 build.rollupOptions.external
  const external = getExternal(packageJson, options);

  // 插件相关,获取构建配置的 plugins 字段
  const plugins = getPlugins(packageJson, options);

  // 拼接各项配置
  const result: UserConfig = {
    plugins,
    build: {
      ...libOptions,
      rollupOptions: {
        external,
      },
    },
  };

  // 与自定义 Vite 配置深度合并,生成最终配置
  return mergeConfig(result, viteConfig || {}) as UserConfig;
}

// 导出其他模块
export * from './plugins';
export * from './options';
export * from './lib';
export * from './external';
export * from './pluginMoveDts';
export * from './pluginSetPackageJson';

构建选项定义

generateConfig 方法中使用到的构建选项 GenerateConfigOptions 是在 src/generateConfig/options.ts 中定义的:

  • 每一个构建选项的含义可以参考对应的注释;
  • defaultOptions 用于设置构建选项的缺省值;
  • getOptions 用于合并自定义选项与缺省选项,生成完整的构建选项;
ts 复制代码
// packages/build/src/generateConfig/options.ts
import { PackageJson } from 'type-fest';
import type { GenerateConfigPluginsOptions } from './plugins';

/** 自定义构建选项 */
export interface GenerateConfigOptions extends GenerateConfigPluginsOptions {
  /**
   * 代码入口
   * @default 'src/index.ts'
   */
  entry?: string;

  /**
   * 产物输出路径,同:https://cn.vitejs.dev/config/build-options.html#build-outdir
   * @default 'dist'
   */
  outDir?: string;

  /**
   * 生成的文件名称,
   *
   * 默认情况下取 package 包名,转换为 kebab-case,如:@openx/request -> openx-request
   *
   * 当产物为 umd 格式时,驼峰化后的 fileName 会作为全局变量名,如:openx-request -> openxRequest
   */
  fileName?: string;

  /**
   * 打包模式
   * - package - 常规构建。会将所有依赖外部化处理,打包出适用于构建场景的 `es`、`umd` 格式产物。并在构建结束后将产物路径回写入 package.json 的入口字段中。
   * - full - 全量构建。大部分依赖都不做外部化处理,打包出适用于非构建场景的 `umd` 格式产物。不参与 d.ts 的移动;不参与构建完成后的产物路径回写。
   * - full-min - 在全量构建的基础上,将产物代码混淆压缩,并生成 sourcemap 文件。
   * @default 'package'
   */
  mode?: 'package' | 'full' | 'full-min';

  /**
   * 是否将 d.ts 类型声明文件的产物从集中目录移动到产物目录,并将类型入口回写到 package.json 的 types 字段。
   *
   * 必须在 mode 为 packages 时生效。
   *
   * 输入 tsc 编译生成 d.ts 文件时所读取的 tsconfig 文件的路径。
   * @default ''
   */
  dts?: string;

  /**
   * 完成构建后,准备回写 package.json 文件前对其对象进行更改的钩子。
   *
   * 必须在 mode 为 packages 时生效。
   */
  onSetPkg?: (pkg: PackageJson) => void | Promise<void>;
}

/** 构建选项的默认值 */
export function defaultOptions(): Required<GenerateConfigOptions> {
  return {
    entry: 'src/index.ts',
    outDir: 'dist',
    fileName: '',
    mode: 'package',
    dts: '',
    onSetPkg: () => {},
    pluginVue: false,
    pluginInspect: false,
    pluginVisualizer: false,
    pluginReplace: false,
  };
}

/** 解析构建选项 */
export function getOptions(options?: GenerateConfigOptions): Required<GenerateConfigOptions> {
  return {
    ...defaultOptions(),
    ...options,
  };
}

ts 复制代码
// packages/build/src/generateConfig/plugins.ts
/** 预设插件相关配置选项 */
export interface GenerateConfigPluginsOptions {
  /**
   * 是否启用 @vitejs/plugin-vue 进行 vue 模板解析。配置规则如下,对于其他插件也适用。
   * - false / undefined 不启用该插件
   * - true 启用该插件,采用默认配置
   * - Options 启用该插件,应用具体配置
   * @default false
   */
  pluginVue?: boolean | VueOptions;

  /**
   * 是否启用 vite-plugin-inspect 进行产物分析。
   * @default false
   */
  pluginInspect?: boolean | InspectOptions;

  /**
   * 是否启用 rollup-plugin-visualizer 进行产物分析。
   * @default false
   */
  pluginVisualizer?: boolean | PluginVisualizerOptions;

  /**
   * 是否启用 @rollup/plugin-replace 进行产物内容替换。
   * @default false
   */
  pluginReplace?: boolean | RollupReplaceOptions;
}

getPkgInfo - 实现 package.json 的读写

package.json 的读写不一定需要自己实现,可以集成 npm 包 read-pkg-up 以及 write-pkg 分别处理。

但是我们的实例中打算自己实现,一方面因为 package.json 的读写并不复杂,另外也是因为这两个包只支持 esm 模块 (package.json 中声明了 "type": "module"),会导致 Vite 加载配置文件时出现错误(除非用 .mts 文件)。

我们在 src/utils/json.ts 中实现读写方法:

ts 复制代码
// packages/build/src/utils/json.ts
import {
  readFile,
  writeFile,
} from 'node:fs/promises';

/**
 * 从文件中读取出 JSON 对象
 * @param filePath 文件路径
 * @returns JSON 对象
 */
export async function readJsonFile<
  T extends Record<string, any> = Record<string, any>,
>(filePath: string): Promise<T> {
  const buffer = await readFile(filePath, 'utf-8');
  return JSON.parse(buffer);
}

/**
 * 将 JSON 对象写入文件
 * @param filePath 文件路径
 * @param rests {@link JSON.stringify} 的参数
 */
export async function writeJsonFile(filePath: string, ...rests: Parameters<typeof JSON.stringify>) {
  await writeFile(filePath, JSON.stringify(...rests), 'utf-8');
}

我们注意到读取 JSON 文件的方法 readJsonFile 是具有泛型参数的,这个泛型负责指定返回对象的类型。为了类型安全,我们需要 package.json 配置对象的类型定义。这里推荐安装 npm 包 type-fest,它是一个纯类型库,除了包含 package.jsontsconfig.json 这种复杂配置对象的类型声明,还包括了大量的 TypeScript 类型运算方法。

bash 复制代码
pnpm --filter @openxui/build i -S type-fest

进而在 src/generateConfig/index.ts 中,我们得以从 type-fest 中导出 PackageJson 接口作为泛型参数传入 readJsonFile,使读取到的 package.json 对象具有完整的类型:

ts 复制代码
// packages/build/src/generateConfig/index.ts
// ...
import { PackageJson } from 'type-fest';

export async function generateConfig(/** ... */) {
  // ...
  const packageJson = await readJsonFile<PackageJson>(absCwd('package.json'));
  // ...
}

其他工具方法的实现

除了 package.json 的读写外,我们还需要很多其他的工具方法。首先就是实现对象类型检查的相关方法:src/utils/typeCheck.ts

ts 复制代码
// packages/build/src/utils/typeCheck.ts
export function isObjectLike(val: unknown): val is Record<any, any> {
  return val !== null && typeof val === 'object';
}

export function isFunction(val: unknown): val is Function {
  return typeof val === 'function';
}

相对路径与绝对路径的计算也是高频率使用的方法。例如我们要获取每个子包的 package.json 文件路径就需要使用 absCwd 方法,因为通过 pnpm --filter xxx 为每个子包执行任务时,脚本执行目录 process.cwd() 会变成子包的根目录,而不是整个项目的根目录。

另外,我们的路径解析工具还会处理 WindowsLinux 系统下文件分隔符的差异。

我们在 src/utils/resolvePath.ts 中实现路径解析相关方法。

ts 复制代码
// packages/build/src/utils/resolvePath.ts
import { relative, resolve, sep } from 'node:path';

/** 给予一个基础路径,获取到一个以此为基准计算绝对路径的方法 */
export function usePathAbs(basePath: string) {
  return (...paths: string[]) => normalizePath(resolve(basePath, ...paths));
}

/** 获取相对于当前脚本执行位置的绝对路径 */
export const absCwd = usePathAbs(process.cwd());

/** 给予一个基础路径,获取到一个以此为基准计算相对路径的方法 */
export function usePathRel(basePath: string) {
  return (path: string, ignoreLocalSignal: boolean = true) => {
    const result = normalizePath(relative(basePath, path));
    if (result.slice(0, 2) === '..') {
      return result;
    }
    return ignoreLocalSignal ? result : `./${result}`;
  };
}

/** 获取相对于当前脚本执行位置的相对路径 */
export const relCwd = usePathRel(process.cwd());

/** 抹平 Win 与 Linux 系统路径分隔符之间的差异 */
function normalizePath(path: string) {
  if (sep === '/') {
    return path;
  }
  return path.replace(new RegExp(`\\${sep}`, 'g'), '/');
}

我们的产物文件名默认情况下是通过 package.json 中的 name 字段生成的,例如:@openxui/build => openxui-build。因此我们还需要实现变量名转换的工具方法。

lodash 提供了这些方法,但是由于这个库并不提供兼容性产物------lodash 包只支持 cjslodash-es 包只支持 esm,容易导致 vite.config 出现依赖解析错误,因此我们选择在 src/utils/formatVar.ts 自己实现。

ts 复制代码
// packages/build/src/utils/formatVar.ts
function splitVar(varName: string) {
  const reg = /[A-Z]{2,}(?=[A-Z][a-z]+|[0-9]|[^a-zA-Z0-9])|[A-Z]?[a-z]+|[A-Z]|[0-9]/g;
  return varName.match(reg) || <string[]>[];
}

/** 将变量名转换为肉串形式:@openxui/build -> openxui-build */
export function kebabCase(varName: string) {
  const nameArr = splitVar(varName);
  return nameArr.map((item) => item.toLowerCase()).join('-');
}

/** 将变量名转换为驼峰形式:@openxui/build -> openxuiBuild */
export function camelCase(varName: string, isFirstWordUpperCase = false) {
  const nameArr = splitVar(varName);
  return nameArr.map((item, index) => {
    if (index === 0 && !isFirstWordUpperCase) {
      return item.toLowerCase();
    }
    return item.charAt(0).toUpperCase() + item.slice(1).toLowerCase();
  }).join('');
}

最后,我们建立 src/utils/index.ts 为工具方法提供统一出口。

ts 复制代码
// packages/build/src/utils/index.ts
export * from './formatVar';
export * from './typeCheck';
export * from './json';
export * from './resolvePath';

常规构建与全量构建

在上文中,我们提到了常规构建全量构建 的两个概念,我们在 generateConfig 接口中定义的 mode 字段用于区分两种构建行为:

  • 常规构建
    • 对应 mode 的值为 package
    • 产物适用于构建场景,必须配合包管理器使用。
    • 构建 umdcjs 格式的产物。
    • 构建完成后要将产物路径回写入 package.json 的入口字段。
    • 所有的依赖都要外部化处理。
    • 不混淆压缩产物代码,不生成 sourcemap。
    • 典型 Vite 配置案例:
ts 复制代码
export default defineConfig({
  build: {
    lib: {
      entry: './src/index.ts',
      formats: ['es', 'umd'],
      fileName: (format) => `pkg-name.${format}.js`
    }
    rollupOptions: {
      external: [
        ...Object.keys(packageJson['dependencies']),
        ...Object.keys(packageJson['peerDependencies']),
      ],
    },
  }
})
  • 全量构建
    • 对应 mode 的值为 fullfull-min
    • 产物适用于非构建场景,无需包管理器,支持浏览器环境直接引入。
    • 只产出 umd 格式产物。
    • 构建完成后不回写 package.json
    • 只外部化 vue 这样的 peerDependencies(上游框架),其他依赖项都要打包进来。
    • modefull-min 时,在全量构建的基础上,混淆压缩产物代码、生成 sourcemap。
    • 典型 Vite 配置案例:
ts 复制代码
export default defineConfig({
  build: {
    lib: {
      entry: './src/index.ts',
      formats: ['umd'],
      // 当 mode 为 full-min 时,后缀为 .full.min.js
      fileName: () => `pkg-name.full.js`
    },
    // 当 mode 为 full-min 时为 true,压缩产物代码
    minify: false,
    // 当 mode 为 full-min 时为 true,生成 sourcemap 文件
    sourcemap: false,
    rollupOptions: {
      external: [
        ...Object.keys(packageJson['peerDependencies']),
      ],
    },
  }
})

因此,在后续拼接构建配置的过程中,我们要注意根据不同的构建模式,采取不同的构建行为。

产物格式 build.lib

generateConfig 将整个拼接构建配置的过程按照配置类别划分成了三部分,其中 getLib 方法负责生成产物格式相关的配置项 build.lib,在 src/generateConfig/lib.ts 中实现:

ts 复制代码
// packages/build/src/generateConfig/lib.ts
import { PackageJson } from 'type-fest';
import { LibraryOptions, LibraryFormats, BuildOptions } from 'vite';
import { statSync } from 'node:fs';
import { join } from 'node:path';
import {
  kebabCase,
  camelCase,
  absCwd,
  relCwd,
} from '../utils';
import { getOptions, GenerateConfigOptions } from './options';

/**
 * 获取 build.lib 产物相关配置
 * @param packageJson package.json 文件内容
 * @param options 构建选项
 */
export function getLib(
  packageJson: PackageJson = {},
  options: GenerateConfigOptions = {},
): Pick<BuildOptions, 'lib' | 'minify' | 'sourcemap' | 'outDir' | 'emptyOutDir'> {
  const {
    entry,
    outDir,
    mode,
    fileName,
  } = getOptions(options);

  // 文件名称,默认取 package.json 的 name 字段转换成 kebab-case:@openxui/build => openxui-build
  const finalName = fileName || kebabCase(packageJson.name || '');

  const libOptions: LibraryOptions = {
    entry,
    // 全量构建只生产 umd 产物
    formats: mode === 'package' ? ['es', 'umd'] : ['umd'],
    name: camelCase(finalName),
    fileName: (format) => {
      const formatName = format as LibraryFormats;
      return getOutFileName(finalName, formatName, mode);
    },
  };

  return {
    lib: libOptions,
    // full-min 模式下全量构建,需要混淆代码,生成 sourcemap 文件,且不清空产物目录
    minify: mode === 'full-min' ? 'esbuild' : false,
    sourcemap: mode === 'full-min',
    emptyOutDir: mode === 'package',
    outDir,
  };
}

/**
 * 获取产物文件名称
 * @param fileName 文件名称
 * @param format 产物格式
 * @param buildMode 构建模式
 */
export function getOutFileName(fileName: string, format: LibraryFormats, buildMode: GenerateConfigOptions['mode']) {
  const formatName = format as ('es' | 'umd');
  const ext = formatName === 'es' ? '.mjs' : '.umd.js';
  let tail: string;
  // 全量构建时,文件名后缀的区别
  if (buildMode === 'full') {
    tail = '.full.js';
  } else if (buildMode === 'full-min') {
    tail = '.full.min.js';
  } else {
    tail = ext;
  }
  return `${fileName}${tail}`;
}

interface EntryInfo {
  /** 子包源码入口文件的绝对路径 */
  abs: string;

  /** 子包源码入口文件相对于脚本执行位置的路径 */
  rel: string;

  /** 子包源码入口是不是文件 */
  isFile: boolean;
}

/**
 * 解析子包源码入口
 * @param entry 源码入口路径
 * @returns 子包源码入口信息,解析结果
 */
export function resolveEntry(entry: string): EntryInfo {
  /** 入口绝对路径 */
  const absEntry = absCwd(entry);

  /** 入口是否为文件 */
  const isEntryFile = statSync(absEntry).isFile();

  /** 入口文件夹绝对路径 */
  const absEntryFolder = isEntryFile ? join(absEntry, '..') : absEntry;

  return {
    abs: absEntry,
    rel: relCwd(absEntryFolder),
    isFile: isEntryFile,
  };
}

依赖外部化 build.rollupOptions.external

getExternal 方法负责生成依赖外部化相关的配置项 build.rollupOptions.external,在 src/generateConfig/external.ts 中实现。

在常规构建中,getExternal 会将 package.json 中所有的依赖项都推入 build.rollupOptions.external;全量构建中,则只推入 peerDependencies 部分。

ts 复制代码
// packages/build/src/generateConfig/external.ts
import { PackageJson } from 'type-fest';
import { getOptions, GenerateConfigOptions } from './options';

/**
 * 获取 build.rollupOptions.external 依赖外部化相关的配置
 * @param packageJson package.json 文件内容
 * @param options 构建选项
 */
export function getExternal(
  packageJson: PackageJson = {},
  options: GenerateConfigOptions = {},
) {
  const {
    dependencies = {},
    peerDependencies = {},
  } = packageJson;

  const { mode } = getOptions(options);

  const defaultExternal: (string | RegExp)[] = [
    // 将所有 node 原生模块都进行外部化处理
    /^node:.*/,
  ];

  const toReg = (item: string) => new RegExp(`^${item}`);

  return defaultExternal.concat(
    Object.keys(peerDependencies).map(toReg),

    // 全量构建时,依赖不进行外部化,一并打包进来
    mode === 'package' ? Object.keys(dependencies).map(toReg) : [],
  );
}

插件管理

generateConfig 的最后一部分是插件管理相关的配置,按照我们的规划,插件管理又分为两个部分:

管理预设插件

第一部分,我们集成了一些常用的插件作为预设,通过 GenerateConfigOptions 继承的 GenerateConfigPluginsOptions 选项快速设置这些插件是否应用到构建配置中。

我们选择了以下四款插件作为预设:

当然,要给 build 包安装好这些插件。

bash 复制代码
pnpm --filter @openxui/build i -S @vitejs/plugin-vue vite-plugin-inspect rollup-plugin-visualizer @rollup/plugin-replace

每一个插件预设的选项,都按照以下规则处理:

  • 选项为 false 或 undefined,不集成插件。
  • 选项为 true,以默认配置集成插件。
  • 选项为 object 对象,则将此对象作为插件配置集成插件。

具体实现如下:

ts 复制代码
// packages/build/src/generateConfig/plugins.ts
import inspect, { Options as InspectOptions } from 'vite-plugin-inspect';
import { visualizer, PluginVisualizerOptions } from 'rollup-plugin-visualizer';
import vue, { Options as VueOptions } from '@vitejs/plugin-vue';
import replace, { RollupReplaceOptions } from '@rollup/plugin-replace';
import { PluginOption } from 'vite';
import { PackageJson } from 'type-fest';
import { isObjectLike } from '../utils';
import type { GenerateConfigOptions } from './options';
import { pluginSetPackageJson } from './pluginSetPackageJson';
import { pluginMoveDts } from './pluginMoveDts';

/** 预设插件相关配置选项 */
export interface GenerateConfigPluginsOptions {
  /**
   * 是否启用 @vitejs/plugin-vue 进行 vue 模板解析。配置规则如下,对于其他插件也适用。
   * - false / undefined 不启用该插件
   * - true 启用该插件,采用默认配置
   * - Options 启用该插件,应用具体配置
   * @default false
   */
  pluginVue?: boolean | VueOptions;

  /**
   * 是否启用 vite-plugin-inspect 进行产物分析。
   * @default false
   */
  pluginInspect?: boolean | InspectOptions;

  /**
   * 是否启用 rollup-plugin-visualizer 进行产物分析。
   * @default false
   */
  pluginVisualizer?: boolean | PluginVisualizerOptions;

  /**
   * 是否启用 @rollup/plugin-replace 进行产物内容替换。
   * @default false
   */
  pluginReplace?: boolean | RollupReplaceOptions;
}

/**
 * 获取预设插件配置
 * @param options 预设插件相关配置选项
 */
export function getPresetPlugins(options: GenerateConfigPluginsOptions = {}) {
  const result: PluginOption[] = [];

  result.push(
    getPresetPlugin(options, 'pluginVue', vue),
    getPresetPlugin(options, 'pluginInspect', inspect),
    getPresetPlugin(options, 'pluginVisualizer', visualizer),
    getPresetPlugin(options, 'pluginReplace', replace),
  );

  return result;
}

/**
 * 获取完整的插件配置
 * @param packageJson package.json 文件内容
 * @param options 构建选项
 */
export function getPlugins(
  packageJson: PackageJson = {},
  options: GenerateConfigOptions = {},
) {
  const { mode, dts } = options;

  const result = getPresetPlugins(options);

  if (mode === 'package') {
    // 常规构建的情况下,集成自定义插件,回写 package.json 的入口字段
    result.push(pluginSetPackageJson(packageJson, options));

    if (dts) {
      // 常规构建的情况下,集成自定义插件,移动 d.ts 产物
      result.push(pluginMoveDts(options));
    }
  }

  return result;
}

/**
 * 处理单个预设插件
 * @param options 预设插件相关配置选项
 * @param key 目标选项名称
 * @param plugin 对应的插件函数
 * @param defaultOptions 插件默认选项
 */
export function getPresetPlugin<K extends keyof GenerateConfigPluginsOptions>(
  options: GenerateConfigPluginsOptions,
  key: K,
  plugin: (...args: any[]) => PluginOption,
  defaultOptions?: GenerateConfigPluginsOptions[K],
) {
  const value = options[key];
  if (!value) {
    return null;
  }

  return plugin(
    isObjectLike(value) ? value : defaultOptions,
  );
}

实现构建完成后修改 package.json 的插件

第二部分,我们要自己实现 Vite 插件来扩展构建过程中的行为。

Vite 插件的原理非常简单:Vite 在构建流程中提供了多个钩子,所谓钩子就是在构建过程中的重要节点执行的函数,插件开发者可以在钩子函数中获取到配置对象、上下文、产物等数据,对它们进行读写,从而达到影响构建过程的目的。

一个典型的 Vite 插件是一个包含插件名称 name 以及各种 Vite 钩子函数的对象:

ts 复制代码
export default function myPlugin(options) {
  return {
    name: 'my-plugin',
    configResolved(config) {
      // ...
    },

    transform(code, id) {
      // ...
    },

    resolveId(id) {
      // ...
    },

    load(id) {
      // ...
    },

    closeBundle() {
      // ...
    }
  }
}

Vite 的各种钩子的执行顺序可以参考下图(图片来源:掘金小册:深入浅出 Vite):

想要更详细地了解如何编写 Vite 插件,可以进行下面的拓展阅读:

Vite 官方文档:插件 API

掘金小册:深入浅出 Vite

从0开始编写一个vite打包产物分析插件

手把手教你开发一个快速、高性能、高质量压缩图片的 Vite 插件

回到我们的代码中,我们要在 src/generateConfig/pluginSetPackageJson.ts 中实现一个自定义插件,用于在构建完成后,将产物路径正确写入 package.json 的入口字段 (关于入口字段,回顾:1. 基于 pnpm 搭建 monorepo 工程目录结构)。

插件的实现比较容易,只需要在 Vite 构建结束阶段的钩子 closeBundle 中,对之前读取到的 package.json 对象进行修改,然后再写回文件中即可:

ts 复制代码
// packages/build/src/generateConfig/pluginSetPackageJson.ts
import { PluginOption } from 'vite';
import { PackageJson } from 'type-fest';
import { basename } from 'node:path';
import {
  isFunction,
  isObjectLike,
  absCwd,
  relCwd,
  kebabCase,
  writeJsonFile,
} from '../utils';
import { getOutFileName, resolveEntry } from './lib';
import { getOptions, GenerateConfigOptions } from './options';

/**
 * 自定义插件,实现对 package.json 内容的修改与回写。
 * @param packageJson package.json 文件内容
 * @param options 构建选项
 */
export function pluginSetPackageJson(
  packageJson: PackageJson = {},
  options: GenerateConfigOptions = {},
): PluginOption {
  const {
    onSetPkg,
    mode,
    fileName,
    outDir,
    dts,
  } = getOptions(options);

  if (mode !== 'package') {
    return null;
  }

  const finalName = fileName || kebabCase(packageJson.name || '');

  return {
    name: 'set-package-json',
    // 只在构建模式下执行
    apply: 'build',
    async closeBundle() {
      const packageJsonObj = packageJson || {};

      // 将 types main module exports 产物路径写入 package.json
      const exportsData: Record<string, any> = {};

      // 获取并设置 umd 产物的路径
      const umd = relCwd(
        absCwd(outDir, getOutFileName(finalName, 'umd', mode)),
        false,
      );
      packageJsonObj.main = umd;
      exportsData.require = umd;

      // 获取并设置 es 产物的路径
      const es = relCwd(
        absCwd(outDir, getOutFileName(finalName, 'es', mode)),
        false,
      );
      packageJsonObj.module = es;
      exportsData.import = es;

      // 获取并设置 d.ts 产物的路径
      if (dts) {
        const dtsEntry = getDtsPath(options);
        packageJsonObj.types = dtsEntry;
        exportsData.types = dtsEntry;
      }

      if (!isObjectLike(packageJsonObj.exports)) {
        packageJsonObj.exports = {};
      }
      Object.assign(packageJsonObj.exports, { '.': exportsData });

      // 支持在构建选项中的 onSetPkg 钩子中对 package.json 对象进行进一步修改
      if (isFunction(onSetPkg)) {
        await onSetPkg(packageJsonObj);
      }

      // 回写入 package.json 文件
      await writeJsonFile(absCwd('package.json'), packageJsonObj, null, 2);
    },
  };
}

/** 根据源码入口和产物目录,计算出 d.ts 类型声明的入口的相对地址 */
function getDtsPath(options: GenerateConfigOptions = {}) {
  const {
    entry,
    outDir,
  } = getOptions(options);

  const { rel, isFile } = resolveEntry(entry);

  /** 入口文件 d.ts 产物名称 */
  const entryFileName = isFile ? basename(entry).replace(/\..*$/, '.d.ts') : 'index.d.ts';

  return relCwd(
    absCwd(outDir, rel, entryFileName),
    false,
  );
}

实现 d.ts 移动的插件

先前,我们在 2. 在 monorepo 模式下集成 Vite 和 TypeScript - 下 中采用了 vue-tsc 整体生成 d.ts 类型声明产物,再通过 mv-dts.ts 脚本将产物分别移动到各个子包中。

这里,我们在 src/generateConfig/pluginMoveDts.ts 中通过自定义插件,将移动 d.ts 产物的过程集成到每个子包的构建过程中。

之前,我们通过 npx vue-tsc -p tsconfig.src.json --composite false --declaration --emitDeclarationOnly 指令,读取了 tsconfig.src.json 文件,生成了所有子包的 d.ts 产物,集中到了根目录的 dist 下面。

由于 dist 中产物的目录结构与我们的子包目录结构相似,我们利用这一点,通过计算相对路径的方式,可以准确定位产物移动的源路径和目标路径。 为了顺利计算出相对路径,我们的插件也需要依赖 typescript 中的方法,读取 tsconfig.src.json 文件中的根路径 rootDiroutDir(tsc 产物路径) 字段作为计算依据。

当然,为了使用 typescript 中的方法,还是要给 build 包增添相应的依赖。

bash 复制代码
pnpm --filter @openxui/build i -S typescript

明确了思路以后,插件的实现并不复杂,同样只需要使用到构建结束阶段的钩子 closeBundle

ts 复制代码
// packages/build/src/generateConfig/pluginMoveDts.ts
import { PluginOption } from 'vite';
import { getParsedCommandLineOfConfigFile, sys } from 'typescript';
import { cp } from 'node:fs/promises';
import { getOptions, GenerateConfigOptions } from './options';
import {
  absCwd,
  usePathAbs,
  usePathRel,
} from '../utils';
import { resolveEntry } from './lib';

/** 自定义插件,将 d.ts 产物从集中目录移动到子包的产物目录 */
export function pluginMoveDts(options: GenerateConfigOptions = {}): PluginOption {
  const {
    entry,
    outDir,
    mode,
    dts,
  } = getOptions(options);

  if (mode !== 'package' || !dts) {
    return null;
  }

  // 解析用于生成 d.ts 总体产物的 tsconfig 文件,并获取解析后的配置
  const tsconfigs = getParsedCommandLineOfConfigFile(dts, {}, sys as any);

  if (!tsconfigs) {
    throw new Error(`Could not find tsconfig file: ${dts}`);
  }

  // 解析出来的路径都是绝对路径
  const { rootDir, outDir: tsOutDir } = tsconfigs.options;

  if (!rootDir || !tsOutDir) {
    throw new Error(`Could not find rootDir or outDir in tsconfig file: ${dts}`);
  }

  const relRoot = usePathRel(rootDir);
  const absRoot = usePathAbs(rootDir);

  /** 当前包相对于根目录的路径 */
  const relPackagePath = relRoot(process.cwd());

  // 源码入口的相对路径
  const { rel: relEntryPath } = resolveEntry(entry);

  return {
    name: 'move-dts',
    apply: 'build',
    async closeBundle() {
      const source = absRoot(tsOutDir, relPackagePath, relEntryPath);
      const target = absCwd(outDir, relEntryPath);
      try {
        // 移动产物
        await cp(source, target, {
          force: true,
          recursive: true,
        });
      } catch (err) {
        // eslint-disable-next-line no-console
        console.log(`[${relPackagePath}]: failed to move dts!`);
        // eslint-disable-next-line no-console
        console.error(err);
      }
    },
  };
}

集成打包体系

到此,我们的打包体系就已经全部实现了,最后在 src/index.ts 的出口中导出一下:

ts 复制代码
// packages/build/src/index.ts
export * from './generateConfig';

下面,我们开始进入集成阶段,来让我们的整套组件库都应用上最新的成果。

构建纯 JS / TS 模块

推荐在 packages/build 目录下创建 build.config.ts 文件,导出一些构建预设:

ts 复制代码
// packages/build/build.config.ts
import { UserConfig } from 'vite';
import {
  generateConfig as baseGenerateConfig,
  GenerateConfigOptions,
} from './src';
import { absCwd } from './src/utils';

/** 构建普通的纯 TS / JS 模块的预设 */
export function generateConfig(
  customOptions?: GenerateConfigOptions,
  viteConfig?: UserConfig,
) {
  return baseGenerateConfig({
    // 指定 d.ts 文件相关 tsconfig 的位置
    dts: absCwd('../../tsconfig.src.json'),
    ...customOptions,
  }, viteConfig);
}

之后我们以 build 包的自我构建为例,在 vite.config.ts 中引入 build.config.ts 中的预设即可:

ts 复制代码
// packages/build/vite.config.ts
import { generateConfig } from './build.config';

export default generateConfig();

之后测试一下打包的效果,不要忘记先整体生成 d.ts 产物,否则我们的 d.ts 移动插件 pluginMoveDts 将会报错。

bash 复制代码
pnpm run type:src
pnpm --filter @openxui/build run build

成功完成构建;构建产物、d.ts 产物均在正确的路径;package.json 的入口字段也指向正确。

接着,我们可以按照同样的方式构建 @openxui/shared 包。

ts 复制代码
// packages/shared/vite.config.ts
import { generateConfig } from '../build/build.config';

export default generateConfig();

可以针对 vite.config 文件关闭 eslint 的禁止用相对路径引用其他包的规则(回顾:3. 集成 lint 代码规范工具):

diff 复制代码
// .eslintrc.js
module.exports = defineConfig({
  // ...
  overrides: [
    // 对于 vite 和 vitest 的配置文件,不对 console.log 进行错误提示
    {
      files: [
        '**/vite.config.*',
        '**/vitest.config.*',
        'scripts/**/*',
      ],
      rules: {
+       'import/no-relative-packages': 'off',
        'no-console': 'off',
      },
    },
  ],
})

构建 Vue 组件模块

由于构建 Vue 组件需要 @vitejs/plugin-vue 的支持,所以我们有必要在 packages/build/build.config.ts 中再建立一个构建 Vue 组件专用的预设:

ts 复制代码
// packages/build/build.config.ts
import { UserConfig } from 'vite';
import {
  generateConfig as baseGenerateConfig,
  GenerateConfigOptions,
} from './src';
import { absCwd } from './src/utils';

/** 构建普通的纯 TS / JS 模块的预设 */
export function generateConfig(/** ... */) {
  // ...
}

/** 构建 Vue 组件模块的预设 */
export function generateVueConfig(
  customOptions?: GenerateConfigOptions,
  viteConfig?: UserConfig,
) {
  return generateConfig({
    pluginVue: true,
    ...customOptions,
  }, viteConfig);
}

我们以 @openxui/button 为例,应用新的打包体系,其他组件可以按照相同的方式集成。

ts 复制代码
// packages/build/vite.config.ts
import { generateVueConfig } from '../build/build.config';

export default generateVueConfig();

构建组件库主包

组件库主包 @openxui/ui 是所有组件库模块的总出口(回顾:2. 在 monorepo 模式下集成 Vite 和 TypeScript - 上),我们不仅要进行常规构建,还要提供全量构建产物,这意味这我们要执行两次不同的构建行为。

我们利用 Vite构建模式 特性,配合我们 GenerateConfigOptions 中的构建选项 mode 来实现。

首先先在 @openxui/uipackage.json 中定义三种不同的构建行为:

diff 复制代码
// packages/ui/package.json
{
  // ...
  "scripts": {
-   "build": "vite build",
+   "build:package": "vite build --mode package",
+   "build:full": "vite build --mode full",
+   "build:full-min": "vite build --mode full-min",
+   "build": "pnpm run build:package && pnpm run build:full && pnpm run build:full-min",
    "test": "echo test"
  },
  // ...
}

vite.config.ts 中获取构建模式 mode,传入到我们 generateVueConfig 的构建选项中。

ts 复制代码
// packages/ui/vite.config.ts
import { defineConfig } from 'vite';
import { generateVueConfig } from '../build/build.config';

export default defineConfig(({ mode }) => generateVueConfig({ mode }));

我们试着运行组件库主包的构建指令:

bash 复制代码
pnpm --filter @openxui/ui run build

可以看到三次不同的构建行为都成功串联配合了起来;构建产物也完全符合预期------ .full.js 文件中只外部化了 vue,其他依赖内容都被悉数打包进来。

整体构建调整

为了适应我们新的打包体系,我们对之前的整体构建流程也做一些微调:

  • 因为移动 d.ts 产物的过程已经集成到了各子包构建的过程中,所以我们可以删去之前的 scripts/dts-mv.ts 脚本。
  • package.json 中也一并删去 scripts/dts-mv.ts 相关的步骤。
diff 复制代码
// package.json
{
  // ...
  "scripts": {
    // ...
-   "mv-type": "tsx ./scripts/dts-mv.ts",
-   "build:ui": "pnpm run type:src && pnpm --filter ./packages/** run build && pnpm run mv-type"
+   "build:ui": "pnpm run type:src && pnpm --filter ./packages/** run build",
  },
}

最后来一次完美的整体构建,结束本章的全部内容:

bash 复制代码
pnpm run build:ui

结尾与资料汇总

按照惯例,我们在结尾梳理一下本章的思路:

  1. 我们首先明晰了定制自己的打包体系的目的:将重复部分抽取为预设、集中管理构建配置、提高自动化程度、支持复杂的构建行为。
  2. 在分析目的的过程中,我们提出了全量构建的概念。与产出构建场景专用产物的常规构建 不同,全量构建主要产出适用于非构建场景的产物。
  3. 之后,我们选取开源 monorepo 仓库作为案例,研究其构建过程:获取并遍历子包 -> 读取 package.json -> 拼接构建配置 -> 调用构建 API。
  4. 我们计划用 pnpm --filter 的能力实现子包的获取与遍历,用 vite build 自然执行构建,因而明确了自己的打包体系只需要实现上述两个特性:读取 package.json、拼接构建配置。
  5. 之后,我们创建 @openxui/build 包,结合先前分析的思路以及我们的特殊需求,一步步实现了打包体系。我们的打包体系具有以下能力:
  • 读取各包的 package.json,自动用其中的重要字段合成构建配置。
  • 配合 vue-tsc,支持为各子包生成 d.ts 类型声明产物。
  • 结合产物信息,自动更新 package.json 文件。
  • 支持不同的构建模式:常规构建与全量构建。
  • 支持与原始 Vite 配置合并,具有高度拓展性。
  1. 最后使用全新的打包体系替代了先前零散的 vite.config,使整体构建流程得到进一步优化。

官网与文档:

Vite

Vite 官方文档:插件 API

pixi.js

lodash

read-pkg-up: 读取 package.json 的工具方法

write-pkg: 写入 package.json 的工具方法

type-fest: 多种实用的类型工具

@vitejs/plugin-vue

vite-plugin-inspect

rollup-plugin-visualizer

@rollup/plugin-replace

分享博文:

Vite是如何对我们写的(vite.config.x)进行解析?

sourcemap 这么讲,我彻底理解了

关于sourcemap,这篇文章就够了

掘金小册:深入浅出 Vite

从0开始编写一个vite打包产物分析插件

手把手教你开发一个快速、高性能、高质量压缩图片的 Vite 插件

相关推荐
四喜花露水7 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy17 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
洛卡卡了41 分钟前
从单层到 MVC,再到 DDD:架构演进的思考与实践
架构·mvc
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称1 小时前
购物车-多元素组合动画css
前端·css
编程一生1 小时前
回调数据丢了?
运维·服务器·前端
乌恩大侠2 小时前
O-RAN Fronthual CU/Sync/Mgmt 平面和协议栈
5g·平面·fpga开发·架构