{T}

组件库入口自动生成器:组件导出、全局注册、Hooks 与类型汇总

概述

当组件数量变多、目录层级变深时,统一入口文件再靠手写维护,错误率和维护成本都会迅速上升——路径写错、漏导出、重名冲突、文件删了入口还留旧导出。本文记录组件库入口生成器从 v1 到 v4 的演进:从递归扫描 .vue 生成 named export,到生成 install(app) 全局注册插件,再到导出 use*.ts 的 Hooks,最后汇成统一类型入口。核心工程意识是「同源生成、幂等性、边界清晰、职责分离、工具链兼容」。

学习目标

  • fs + path 递归扫描组件目录,自动生成统一导出入口,而非手写维护
  • 用标记位(单标记 / 双标记)隔离「脚本接管区」与「手写保留区」,保证可重复执行
  • 区分 app.use(plugin)app.component(name, component),正确生成全局注册插件
  • 把组件、Hooks(use*.ts)、类型(types.ts)三类导出统一到同一份扫描结果里
  • 理解 export type * 的兼容性问题与「类型入口用 re-export 而非文本拼接」的原因

一、为什么需要自动生成入口

典型风险包括:路径写错、漏导出、重名冲突、文件删了但入口还留着旧导出。解决方案是用自动化脚本递归扫描组件目录,生成统一导出入口;更重要的是把它升级成完整的公共 API 生成器,覆盖组件导出、全局注册插件、Hooks 导出、类型汇总四个层次。

这类脚本的最小可用技术栈其实就是 fs + path + 字符串处理,不需要一上来引入复杂 AST 工具——目标非常明确,只是生成固定格式的导出语句。

二、标记位设计:不破坏用户手写内容

自动生成入口时,最关键的不是「能不能写进去」,而是「如何不破坏用户手写内容」。最基础的方案是用单标记 // appended by scripts;更优的是双标记 AUTO-GENERATED-EXPORTS-START / END,只在标记区间内覆盖,区间外手写内容完全保留。

递归扫描的核心流程是 fs.readdirSync + 判断是否目录后递归,对非 .vue 文件直接跳过:

js
function walkDir(dir) {
  const files = fs.readdirSync(dir)
  files.forEach((file) => {
    const filePath = path.join(dir, file)
    const stat = fs.statSync(filePath)
    if (stat.isDirectory()) return walkDir(filePath)
    if (path.extname(filePath) !== '.vue') return
    // 进入导出逻辑
  })
}

生成导出语句时注意:Windows 路径分隔符是反斜线,所以 replace(/\\/g, '/') 这一步很关键。写回文件时先 indexOf 标记位,存在就截断到标记位再追加,不存在就补标记后追加——这保证了脚本幂等,不会无限重复追加同样内容。

三、从导出脚本升级到全局注册插件

既然脚本已经能扫描组件,就应继续扩展它生成 install(app) 和默认导出。关键转变是:从 export { default as Xxx } from '...' 改成先 import Xxx from '...',才能在 install(app) 里复用组件变量。

一个高频易错点:把注册逻辑写成 app.use(Component)app.use(plugin) 用于安装插件,app.component(name, component) 才用于注册全局组件。正确结构:

ts
const plugin = {
  install(app) {
    app.component('VHeader', VHeader)
    app.component('VMenu', VMenu)
  },
}

export default plugin

组件名像 headermenu 这类名字容易和原生标签或 ESLint 规则撞上,应建立统一前缀(如 VHeader)。当混合使用默认导出和命名导出时,需要显式指定 output.exports = 'named'。组件库对外提供两种用法:按需 import { IconList } from 'xxx',或全局 import Components from 'xxx'; app.use(Components)

四、Hooks 与类型导出补全公共 API

组件本身能全局注册后,像 VForm 配套的 useForm 如果没导出,消费方就无法完整使用。扩展规则:发现 .ts 文件名以 use 开头,就作为 hook/composable 导出;发现 types.ts,就集中导出到统一类型入口。

js
if (extension === '.ts' && baseName.startsWith('use')) {
  hooksExports += `export { ${baseName} } from '${importPath}'\n`
}
if (extension === '.ts' && baseName.startsWith('types')) {
  typesExports += `export * from '${importPath.replace('.ts', '')}'\n`
}

不要直接把多个类型文件内容拼接成一个大文件——标识符重复、相对路径 import 失效、第三方类型冲突都会冒出来。正确方案是统一 re-export 入口:export * from './components/form/types',保留每个类型文件原本的模块边界。TypeScript 5.0 已支持 export type *,但当前 dts 生成工具链对该语法处理不够稳定,务实折中是先退回 export * from。另外把 types.d.ts 改成 types.ts,让类型模块进入正常 TS 解析与 dts 生成链。脚本写文件成功后顺手执行 pnpm run format,把生成与规范收尾串成同一条流水线。

最终验证标准不是只看 dist 里有没有 index.d.ts,而是回到 Playground 真正 import type { FormSchema } from 'el-admin-components',看编辑器和项目是否都能识别。

五、最终生成的入口结构与职责分离

把上述四层规则跑完后,最终生成的入口文件大致长这样:组件 import 集中一处,全局注册插件单独成块,按需导出和 Hooks 导出各成一节,类型入口用 re-export 汇总。

ts
// 组件导入
import AvList from './components/av-list/index.vue'
import VForm from './components/form/index.vue'

// 全局注册插件
export const globalPlugin = {
  install(app) {
    app.component('AvList', AvList)
    app.component('VForm', VForm)
  }
}

export default globalPlugin

// 组件按需导出
export { AvList, VForm }

// Hooks 导出
export { useForm } from './components/form/useForm'

// 类型入口(可能独立文件)
export * from './components/form/types'
export * from './components/table/types'

脚本内部也要从「一个大字符串」拆成多段可组合内容:componentImports(import 语句)、pluginContentLines(install 里的注册行)、componentsExports(组件 named export)、hooksExports(hooks 导出)。这四段来自同一次递归扫描,从根本上避免「导出清单、注册清单、类型清单漂移」。这也对应入口生成器的四个演进阶段:v1 组件导出、v2 全局注册、v3 Hooks 导出、v4 类型汇总。组件库对外 API 的完整层次是:组件(按需导出 + 全局注册)、hooks / composables(按需导出)、插件(默认导出)、类型定义(类型入口汇总)。


常见问题

问题原因解决方案
每次执行脚本都重复追加导出只做追加没清旧生成区用标记位先截断旧内容再追加
Windows 下导入路径不对path.relative() 返回反斜线写入前统一 replace(/\\/g, '/')
某些 .d.ts、常量文件被误导出没严格按扩展名过滤只保留 .vueuse*.tstypes.ts
手写 main.ts 被脚本覆盖没设计标记位边界引入单 / 双标记,只改自动生成区
headermenu 被 ESLint 报错组件名与保留标签或规则冲突给冲突项加前缀或建立统一前缀策略
app.use() 后组件没真正注册install 里误用 app.use(Component)改成 app.component(name, Component)
VForm 渲染了但 useForm 不能导入脚本只处理了 .vue给脚本增加 .ts + startsWith('use') 规则
多个类型文件拼起来报重复声明类型文件上下文被破坏改成统一 export * from 入口
export type * from 导致打包不稳定dts 工具链兼容性先退回 export * from,优先保证链路稳定

延伸阅读