切换日光/暗黑模式
088. 模块注册器与组件打包配置
学习目标
这一节实现通用表格的模块注册器,并开始把组件打包成可发布的包。
学完后,你应该能理解:
- 模块注册器解决什么问题;
- 全局模块注册器和局部模块注册器有什么区别;
- 如何添加、删除、覆盖模块;
- 为什么模块安装要支持前置和后置覆盖;
package.json里哪些字段影响发包;main、module、types分别代表什么;tsup和tsc在打包里分别负责什么。
模块注册器的目标
前面模块还是手动安装。
类似:
ts
useConfigModule(autoTable)
useStateModule(autoTable)
useHandlerModule(autoTable)
useRenderModule(autoTable)这种写法能跑。
但外部项目无法方便地添加自己的模块。
所以需要一个模块注册器。
它负责维护模块列表,并按顺序安装到表格对象上。
模块结构
每个模块都包含几个基本信息:
seq:执行顺序;key:模块标识;use:模块函数。
模块函数接收 autoTable 对象。
执行后,把自己的能力挂到 autoTable 上。
例如:
ts
{
seq: 10,
key: "queryForm",
use: useQueryFormModule,
}为什么模块可以为空
模块映射里允许某个模块值为空。
这是为了删除模块。
如果想删除某个默认模块,可以把对应 key 的值置空。
安装时过滤掉空模块。
这样就能实现:
- 默认组件带查询表单;
- 某个项目不需要查询表单;
- 项目局部把查询表单模块删除。
添加模块
模块注册器提供添加模块的方法。
添加时,把模块放进内部映射。
key 是模块标识。
value 是模块定义。
可以理解成:
ts
moduleMapper[key] = module后续安装时,注册器会读取这个映射。
安装模块
安装模块时,注册器会做几件事:
- 合并默认模块、本地模块、覆盖模块;
- 过滤空模块;
- 按
seq排序; - 依次执行模块函数;
- 把能力安装到
autoTable对象上。
核心思路还是:
ts
modules.forEach(module => {
module.use(autoTable)
})只是模块来源更灵活。
全局模块注册器
全局模块注册器放在组件库层面。
它影响所有通用表格实例。
例如组件库默认带:
- 配置模块;
- 状态模块;
- 事件模块;
- 渲染模块;
- 查询表单模块;
- 搜索栏模块。
只要使用这个通用表格,都会先加载这些全局模块。
局部模块注册器
局部模块注册器挂在具体的表格 Hook 或表格实例上。
它只影响当前这类表格。
例如项目里同时对接两个后端系统:
useCimTable;useErpTable。
它们可以基于同一个通用表格,但做不同扩展。
一个可以删除查询表单。
另一个可以额外添加按钮。
这就是局部模块注册器的价值。
删除模块示例
假设某个表格不需要查询表单。
可以在局部注册器里删除 queryForm 模块。
删除后,查询表单本身不会渲染。
它往搜索栏添加的按钮也不会出现。
因为整个查询表单模块没有执行。
这比只隐藏 UI 更干净。
添加模块示例
局部注册器也可以添加模块。
例如给 ERP 表格添加一个自定义按钮。
这个模块可以通过搜索栏渲染钩子往搜索区域注册内容。
按钮点击后,可以读取当前表格状态。
例如读取当前数据条数:
ts
autoTable.state.data.length这说明模块不仅能渲染 UI,也能使用表格对象上的状态和方法。
覆盖模块示例
覆盖模块是用同一个 key 注册新的模块。
例如覆盖默认查询表单。
新的模块可以不再渲染原来的查询区域,而是在表格底部渲染一段提示或公告。
这样就实现了“同一个表格能力,不同项目有不同呈现方式”。
为什么要同时支持三种动作
复杂组件在不同项目里需求差异很大。
只支持添加还不够。
还需要:
- 删除默认功能;
- 覆盖默认功能;
- 调整顺序;
- 在默认模块前后插入模块。
这样组件库才能被多个项目复用。
否则组件越强,越容易因为默认功能太多而不适合某个项目。
进入组件打包
模块注册器完成后,下一步是把通用表格打成 npm 包。
这样其他项目安装包后,也能通过模块注册器添加或删除模块。
打包前要先处理几个文件:
package.json;- 打包入口文件;
tsup.config.ts;- 类型声明生成配置;
- 需要导出的工具和服务。
package.json 的包名
name 是包名。
别人安装或引入包时会用它。
例如:
ts
import { useAutoTable } from "@scope/agent"这里的 @scope/agent 就来自包名。
项目初始化时,name 可能只是文件夹名。
发包前需要改成真正要发布的包名。
private
private 控制包是否允许发布。
如果要发布到 npm,不能是 true。
公开 npm 包通常需要:
json
{
"private": false
}如果公司要发私有包,一般不是直接发公开 npm。
而是搭建私有 npm 仓库。
课程里提到可以用 Nexus 这类工具做私有仓库。
版本号
公开 npm 的版本不能重复。
同一个包已经发布过 1.0.0,下一次就必须换版本号。
私有仓库是否允许覆盖同版本,取决于仓库配置。
所以正式发包要养成改版本号的习惯。
npm 包可信度
npm 上有些包会显示是否来自可信发布流程。
如果包不是通过标准 CI/CD 流程发布,而是本机手动发布,可信度会差一些。
原因是无法确认发布者环境是否安全。
例如开发者 token 泄露后,攻击者也可能发布恶意版本。
这也是为什么大型开源包会重视发布流程。
type: "module"
type: "module" 表示这个包按 ES Module 处理。
没有它时,有些打包器可能按 CommonJS 理解。
现代前端项目通常更偏向 ES Module。
所以组件库发包时,需要明确模块格式。
main、module、types
这三个字段定义入口。
main 通常给 CommonJS 使用。
module 通常给 ES Module 使用。
types 指向 TypeScript 类型声明文件。
例如:
json
{
"main": "dist/main.cjs",
"module": "dist/main.js",
"types": "dist/main.d.ts"
}使用者只写包名引入时,打包器会根据这些字段找到真正入口。
files
files 控制发布到 npm 的文件范围。
组件库打包后,只需要发布 dist。
所以可以配置:
json
{
"files": ["dist"]
}这样源码、临时文件、测试文件不会被一起发布。
打包入口文件
组件库需要一个入口文件。
例如:
ts
// src/main.ts
export { useAutoTable } from "./auto-table"课程里先只导出通用表格相关内容。
完整组件库还要导出更多组件、类型和工具。
但这里目标是验证通用表格打包和扩展能力。
所以先做最小可用导出。
tsup.config.ts
tsup 用来把源码打包成发布文件。
配置里要指定:
- 入口文件;
- 输出格式;
- 是否清理
dist; - 是否处理样式;
- 哪些依赖不打进包里。
输出格式常见有三种:
esm:给 ES Module 使用;cjs:给 CommonJS 使用;umd:给浏览器 script 标签使用。
这一节先只打 esm。
正式组件库通常会同时输出多种格式。
为什么不用 tsup 生成类型
tsup 可以生成类型声明。
但这里用了声明合并和类型扩展。
生成出来的类型结构不够理想。
所以课程里选择:
tsup负责打包 JS 和样式;tsc负责生成类型声明。
这比全部交给一个工具更可控。
tsconfig 配置
打包用的 tsconfig 和开发运行用的 tsconfig 可以不同。
开发项目需要服务 Vite、React、Node 配置。
组件库打包则需要生成声明文件。
关键配置是:
json
{
"declaration": true
}它告诉 TypeScript 生成 .d.ts 类型声明。
外部依赖
打包组件库时,不应该把所有依赖都塞进包里。
例如 React、Ant Design 这类通常作为外部依赖。
否则使用者项目里可能出现重复 React 或样式冲突。
所以打包配置需要 external。
它表示哪些依赖不打包进产物。
样式打包
通用表格组件可能引用样式文件。
例如 Sass。
所以打包配置要能处理样式。
生成后,dist 里会出现 JS 文件、样式文件和类型声明文件。
使用者安装包后才能正确拿到组件样式。
导出 Token 服务
测试新项目时,表格请求后端需要 token。
所以课程里把 token 服务也导出。
新项目可以直接设置一个测试 token。
这样在验证组件包时,不需要重新实现一套认证逻辑。
打包产物
运行打包命令后,会生成 dist。
里面包含:
- 入口 JS;
- 样式文件;
- 类型声明文件;
- 列组件相关声明;
- 通用表格相关声明。
入口 JS 使用 ES Module 输出。
从产物可以看到 export 语法。
这一节的核心
模块注册器让通用表格从“内部固定模块”变成“外部可增删改模块”。
组件打包让这种扩展能力可以跨项目复用。
这一节还没有系统展开完整组件库工程化。
但已经跑通关键链路:
- 模块注册;
- 全局和局部扩展;
- 包入口导出;
- JS 打包;
- 类型声明生成;
- 准备发布到 npm。