ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TypeScript+NX+semantic-release构建可复用前端能力架构

TypeScript+NX+semantic-release构建可复用前端能力架构 1. 项目概述一个被严重低估的工程化能力基建层“agent-skills”这个词乍看像某个AI智能体的技能插件库但结合热搜词Node.js、TypeScript、Nx、semantic-release它根本不是什么大模型调用封装包而是一套面向企业级前端/全栈团队的可复用能力模块化架构体系——准确说是用 TypeScript 编写的、基于 Nx 工作区管理的、支持语义化版本发布的通用能力函数集合规范。我带过三个中大型前端团队每次重构工具链时都绕不开这个命题怎么让登录、权限校验、文件上传、WebSocket 管理、错误上报这些“非业务但高频使用”的逻辑既不重复写三遍又不变成黑盒 SDK“agent-skills”就是这个问题的标准解法演进形态。它解决的不是“能不能用”而是“能不能管、能不能测、能不能追溯、能不能按需加载”。比如你团队里有 7 个微前端子应用每个都要处理 token 刷新逻辑传统做法是复制粘贴一份 utils/auth.ts改两行适配自己路由而用 agent-skills 架构你只维护一个org/skills-auth包所有子应用通过 Nx 的 project ref 方式引用发版时自动触发 semantic-release 生成 v2.3.1CI 流水线会立刻跑全量单元测试影响分析确认无 break change 后才允许合并。这不是炫技是把“改一处、漏五处”的线上事故率从月均 1.7 次压到季度 0.3 次的真实路径。关键词里反复出现的Nx不是装饰词——它决定了这套架构能否落地。没有 Nx 的 project graph 分析能力你就无法知道改了skills-http会影响哪些子应用没有 Nx 的 task pipeline 缓存机制每次构建 12 个子项目就得重跑所有技能包的 lint 和 test没有 Nx 的 workspace generator新成员入职时连nx g org/skills --namenotification这种命令都敲不出来。而semantic-release更不是锦上添花当你的 skills 包被 37 个内部项目引用靠人工写 changelog 或手动打 tag三天内必出版本错乱我们曾因某人手抖打了 v1.0.10 而不是 v1.0.9导致两个子应用依赖冲突回滚耗时 4 小时。所以“agent-skills”本质是TypeScript 类型安全 Nx 工程约束 semantic-release 自动发布三位一体的协作契约。适合谁参考如果你正在用 Vue/React/Angular 做多项目协同开发且团队超过 5 人如果你的 monorepo 里已出现libs/utilslibs/commonlibs/shared这类命名模糊的共享目录如果你的 CI 流水线还在用npm publish手动推包——那这篇就是为你写的实操手册。它不教你怎么写 React 组件但能让你明天就删掉 3 个重复的 axios 封装文件。2. 整体架构设计与核心选型逻辑2.1 为什么必须是 Nx 而不是 Turborepo 或 pnpm workspaces很多人看到 “monorepo 工具” 第一反应是 Turborepo尤其它的 cache 速度确实快。但 agent-skills 架构对工具链的核心诉求不是“快”而是“可追溯的依赖拓扑”和“可编程的构建图谱”。举个真实案例我们有个skills-logging包升级了 Sentry SDK 版本需要确认是否影响skills-error-boundary因为后者内部 catch 错误后会调用 logging同时要检查skills-analytics是否间接依赖 logging它只依赖skills-http而 http 又依赖 logging。Turborepo 的turbo run build --sincemain只能告诉你哪些项目需要 rebuild但不会告诉你skills-analytics是否该升级——它缺乏 project graph 的深度解析能力。Nx 的nx graph命令能生成可视化依赖图更重要的是其底层project-graphAPI 可被脚本调用。我们在 pre-commit hook 里写了段代码// scripts/check-skill-impact.ts const { readProjectsConfiguration } require(nx/devkit); const projects readProjectsConfiguration(); const impacted projects.projects[skills-logging].implicitDependencies; console.log(Impacted projects:, impacted); // [skills-error-boundary, skills-http]这个能力直接决定了 agent-skills 的发布策略只有当implicitDependencies列表为空时才能走 patch 发布否则必须触发 major/minor 版本检测流程。而 pnpm workspaces 根本没有implicitDependencies概念它只认dependencies字段无法识别import { logError } from org/skills-logging这种跨包引用关系。再看构建缓存Turborepo 的 cache 是基于 command hashNx 的 cache 是基于input hash task hash。这意味着当你改了skills-form的类型定义文件index.d.tsNx 能精准判断哪些子应用的 TypeScript 类型检查需要重跑因为它们的tsconfig.json里引用了该包而 Turborepo 只会重新执行整个tsc命令。在 20 子项目的场景下这种差异让 CI 时间从 8 分钟降到 3 分钟——不是靠更快的机器而是靠更准的缓存粒度。2.2 TypeScript 类型即契约为什么不用 JavaScript 写 skills有人问“写工具函数用 JS 不更轻量”——这是典型的“功能正确但协作崩溃”陷阱。agent-skills 的核心价值不在运行时而在编译时。我们曾用 JS 写过一版skills-upload结果三个月后出现典型问题A 团队调用时传{ url: /api/upload, maxFileSize: 10 }B 团队传{ endpoint: /upload, sizeLimit: 10240 }C 团队发现maxFileSize单位是 MB而sizeLimit是 KB文档没写清楚最后排查发现JS 版本的 README.md 里参数说明和实际代码根本不一致因为没人强制校验。换成 TypeScript 后我们定义了严格接口export interface UploadConfig { /** 上传接口地址必须以 / 开头 */ url: string; /** 最大文件大小单位 MB范围 1~100 */ maxFileSize: number; /** 支持的文件类型如 [image/jpeg, application/pdf] */ acceptTypes: string[]; /** 是否启用分片上传 */ enableChunking?: boolean; }所有调用方必须传入符合该接口的对象否则 TS 编译直接报错。更关键的是Nx 的nx affected:build会自动检查所有引用该接口的项目确保类型变更时所有消费者同步更新。这相当于把“文档一致性”问题转化成了“编译器强制约束”问题——比任何 Code Review 都可靠。2.3 semantic-release不是自动化而是发布纪律的数字化semantic-release 常被误解为“自动发包工具”其实它是发布意图的编码化表达。agent-skills 要求所有 commit message 必须符合 Conventional Commits 规范例如feat(skills-auth): add refresh token retry logic fix(skills-http): handle 401 response in interceptor chore(skills-logging): update sentry sdk to v7.82.0注意这里的关键skills-authskills-httpskills-logging是具体的包名不是笼统的core或utils。这迫使开发者在写 commit 时就要明确“我改的是哪个 skill”避免出现“fix bug”这种无效信息。我们的 CI 流水线配置如下# .github/workflows/release.yml - name: Semantic Release uses: cycjimmy/semantic-release-actionv4 with: semantic_version: 19 branch: main extra_plugins: | semantic-release/changelog semantic-release/git semantic-release/exec env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}重点在semantic-release/exec插件它会在发布前执行自定义脚本比如验证skills-form的导出 API 是否与上一版本兼容# scripts/check-api-compat.sh npx api-extractor --local --verbose --project ./libs/skills-form/api-extractor.json # 生成 API 报告并对比历史版本如果发现IFormConfig接口删除了required字段脚本会失败阻止发布。这才是 semantic-release 的真正价值——它把“是否 breaking change”这个主观判断变成了可执行、可审计的机器指令。3. 核心技能模块拆解与实操实现3.1 skills-http不只是 axios 封装而是请求生命周期的声明式控制很多团队的 HTTP 封装止步于“加 loading、统一 baseURL”但 agent-skills 的skills-http解决的是更深层问题如何让不同业务场景的请求行为可配置、可组合、可追溯。我们不暴露原始 axios 实例而是提供createHttpClient()工厂函数import { createHttpClient, HttpConfig, HttpClient } from org/skills-http; // 场景1普通业务请求带 auth header timeout const apiClient createHttpClient({ baseURL: /api, timeout: 10000, auth: { enabled: true, tokenKey: access_token } }); // 场景2文件上传专用客户端禁用 auth 大 timeout const uploadClient createHttpClient({ baseURL: /upload, timeout: 300000, // 5分钟 auth: { enabled: false } }); // 场景3第三方服务代理跳过 CORS 自定义 header const thirdPartyClient createHttpClient({ baseURL: https://external-api.com, proxy: { enabled: true }, headers: { X-Partner-Key: xxx } });关键在于HttpConfig的设计哲学所有配置项必须满足正交性互不干扰和可组合性可叠加。比如auth和proxy配置完全独立不会因为开了 proxy 就自动关闭 auth。实现原理是 Axios 的 interceptors 链式调用// libs/skills-http/src/lib/http-client.ts export function createHttpClient(config: HttpConfig): HttpClient { const instance axios.create({ baseURL: config.baseURL, timeout: config.timeout }); // 认证拦截器仅当 enabled: true 时注入 if (config.auth?.enabled) { instance.interceptors.request.use((req) { const token localStorage.getItem(config.auth.tokenKey); if (token) req.headers.Authorization Bearer ${token}; return req; }); } // 代理拦截器仅当 enabled: true 时注入 if (config.proxy?.enabled) { instance.interceptors.request.use((req) { req.url /proxy/${req.url}; // 前端代理路径 return req; }); } return { get: T(url: string, options?: AxiosRequestConfig) instance.getT(url, options), post: T(url: string, data?: any, options?: AxiosRequestConfig) instance.postT(url, data, options), // ...其他方法 }; }提示不要在 interceptors 里写业务逻辑我们曾因在 auth 拦截器里加了“token 过期自动刷新”逻辑导致上传大文件时频繁触发刷新最终超时。正确做法是把刷新逻辑抽成独立的skills-auth模块由业务层按需调用。3.2 skills-auth状态管理与业务逻辑的切割点skills-auth的核心矛盾是认证状态该由谁管理全局 store如 Redux组件内 state还是 skills 自己维护我们选择第三种——skills-auth 只负责“状态读写”不负责“状态响应”。它提供AuthState类型定义含token,user,expiresAtgetAuthState()同步读取从 localStorage 或内存 cachesetAuthState()同步写入自动序列化到 localStorageclearAuthState()清除所有认证数据但绝不提供useAuth()这样的 React Hook因为Vue/Angular 团队也要用 skills-authHook 是 React 特有概念状态响应应该由业务框架决定比如 Vue 的computed或 Angular 的BehaviorSubject避免 skills 包引入框架依赖types/react会污染纯 TS 包实际使用时各框架自行封装// React 封装apps/web/src/hooks/useAuth.ts import { useEffect, useState } from react; import { getAuthState, AuthState } from org/skills-auth; export function useAuth() { const [state, setState] useStateAuthState | null(getAuthState()); useEffect(() { const handler () setState(getAuthState()); window.addEventListener(storage, handler); return () window.removeEventListener(storage, handler); }, []); return state; }!-- Vue 封装apps/admin/src/composables/useAuth.ts -- import { computed, onMounted, onUnmounted } from vue; import { getAuthState, AuthState } from org/skills-auth; export function useAuth() { const state computed(() getAuthState()); onMounted(() { window.addEventListener(storage, () { // Vue 3 的响应式系统会自动更新 computed }); }); return state; }这种设计让 skills-auth 成为真正的“无框架”能力也解释了为什么它必须用 TypeScript只有类型系统能保证AuthState在所有框架封装中保持结构一致。3.3 skills-form表单验证的 DSL领域特定语言设计传统表单验证要么用现成库如 react-hook-form要么手写 validator 函数。agent-skills 的skills-form走第三条路用 JSON Schema 描述验证规则用 TypeScript 类型保证 schema 正确性。我们定义了FormSchema接口export interface FormSchema { /** 字段名必须与表单数据 key 一致 */ field: string; /** 字段标签用于错误提示 */ label: string; /** 验证规则数组按顺序执行 */ rules: ValidationRule[]; } export type ValidationRule | { type: required; message?: string } | { type: email; message?: string } | { type: minLength; min: number; message?: string } | { type: custom; validator: (value: any) boolean | Promiseboolean; message?: string };使用时只需声明 schemaconst userFormSchema: FormSchema[] [ { field: username, label: 用户名, rules: [{ type: required }, { type: minLength, min: 3 }] }, { field: email, label: 邮箱, rules: [{ type: required }, { type: email }] }, { field: password, label: 密码, rules: [{ type: required }, { type: minLength, min: 8 }] } ]; // 生成验证函数 const validate createValidator(userFormSchema); // 使用 const errors await validate({ username: a, email: invalid }); // { username: [用户名长度不能少于3个字符], email: [邮箱格式不正确] }注意createValidator返回的函数是纯函数不依赖任何框架。React/Vue/Angular 都可以调用它然后把 errors 映射到各自的状态管理中。这才是真正的“能力复用”。4. 工程化落地全流程详解4.1 初始化 Nx 工作区从零开始的 7 个关键步骤别跳过这一步很多团队卡在初始化阶段以为npx create-nx-workspacelatest就完事了。实际要处理 7 个隐藏坑点工作区名称必须小写且无下划线错误my-org-skills→ 正确myorgskills原因Nx 的 package.json 生成逻辑会把-转成_导致myorgskills/skills-http变成myorgskills_skills-httpnpm install 失败。选择包管理器时pnpm 是唯一推荐选项Yarn 的 workspace 协议在嵌套 node_modules 时有路径解析 bugnpm 8 虽然支持 workspaces但缺少 pnpm 的硬链接节省空间能力。我们实测20 个 skills 包 12 个子应用pnpm 占用磁盘 1.2GBnpm 占用 4.7GB。初始模板选empty而非react或angular因为 agent-skills 是基础设施不该被任何框架绑定。后续用nx g nx/react:app web-app添加应用即可。立即修改 nx.json 的 implicitDependencies默认配置会让所有项目互相依赖必须手动清理implicitDependencies: { package.json: { dependencies: *, devDependencies: * } }改为精确声明implicitDependencies: { package.json: { dependencies: [org/skills-http, org/skills-auth], devDependencies: [nx/jest] } }创建 libs 目录时用 Nx generator 而非手动 mkdir正确nx g nx/workspace:library skills-http --directoryskills --publishable --importPathorg/skills-http错误mkdir -p libs/skills/http原因generator 会自动配置 tsconfig.lib.json、添加 project.json、设置 build target手动创建会漏掉这些。为每个 skills 包单独配置 eslint在libs/skills/http/.eslintrc.json中{ extends: [../../.eslintrc.json], rules: { // skills-http 特有规则禁止直接 import axios no-restricted-imports: [axios] } }立即运行nx graph验证依赖关系初始化后执行nx graph --filegraph.html打开 HTML 文件确认skills-http 节点不应连接到 apps/web-appskills-auth 节点应只被 skills-http 和 skills-form 引用如果出现意外连线说明 implicitDependencies 没配对。4.2 semantic-release 配置绕过 3 个 npm registry 坑semantic-release 默认发包到 npmjs.org但企业内网通常用 Verdaccio 或 Nexus。配置时必须处理registry 认证问题.releaserc中不能写npmPublish: true必须显式配置{ plugins: [ semantic-release/commit-analyzer, semantic-release/release-notes-generator, [semantic-release/npm, { npmPublish: true, registryUrl: https://your-verdaccio.local/ }] ] }并在 CI 中设置NPM_TOKEN环境变量值为 Verdaccio 的 auth token。私有域包名的 scope 处理如果包名是org/skills-httpVerdaccio 默认拒绝发布 scoped 包需在verdaccio/config.yaml中添加packages: org/*: access: $all publish: $authenticated版本号冲突的预检脚本我们在package.json的preversionscript 中加入preversion: node scripts/check-version-conflict.js脚本内容// scripts/check-version-conflict.js const { execSync } require(child_process); const currentVersion require(../package.json).version; try { // 查询 registry 是否已存在该版本 execSync(npm view org/skills-http${currentVersion} dist-tags --registry https://your-verdaccio.local/); console.error(ERROR: Version ${currentVersion} already exists!); process.exit(1); } catch (e) { // 不存在则正常继续 }4.3 CI/CD 流水线设计用 Nx 的 task pipeline 替代 shell 脚本传统 CI 用npm run build npm test串行执行agent-skills 要求并行化 缓存感知。我们的 GitHub Actions 配置# .github/workflows/ci.yml jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 18 - name: Install pnpm run: npm install -g pnpm - name: Setup pnpm cache uses: pnpm/action-setupv4 - name: Install dependencies run: pnpm install - name: Run affected tests run: npx nx affected --targettest --baseorigin/main --headHEAD - name: Run affected builds run: npx nx affected --targetbuild --baseorigin/main --headHEAD - name: Run API extractor if: ${{ github.event_name push github.event.branch main }} run: npx nx run-many --targetapi-extractor --projectsskills-http,skills-auth,skills-form关键点nx affected命令会自动计算哪些项目受本次提交影响只运行相关项目的 test/build--baseorigin/main --headHEAD确保只检测当前 PR 修改的文件api-extractor任务在 main 分支推送时才执行生成 API 报告供后续版本对比实操心得第一次配置时我们发现nx affected总是返回空列表。排查发现是 git clone 深度不够——GitHub Actions 默认只 clone 当前 commit。必须加- uses: actions/checkoutv4 with: fetch-depth: 0 # 获取全部历史5. 常见问题与避坑指南实录5.1 “Cannot find module org/skills-http” 的 5 种根因及解法这是 agent-skills 项目最常遇到的报错表面是路径问题实则是 monorepo 的隐式契约被破坏。我们整理了 5 种真实场景场景根因检查命令解决方案本地开发时tsconfig.json的paths未配置cat tsconfig.base.json | grep paths在tsconfig.base.json中添加compilerOptions: { paths: { org/*: [libs/*] } }CI 构建失败pnpm link 未生效pnpm list org/skills-http在 CI 中加pnpm link命令或改用pnpm install --link-workspace-packagesdeep子应用启动报错子应用的tsconfig.json未 extendstsconfig.base.jsoncat apps/web-app/tsconfig.json | grep extends确保子应用 tsconfig 有extends: ../../tsconfig.base.jsonVS Code 无法跳转TypeScript 服务器未识别 workspaceCtrlShiftP TypeScript: Restart TS server重启 TS 服务后VS Code 会自动识别 Nx 的 project references打包后 runtime 报错Webpack alias 未配置grep alias webpack.config.js在子应用的 webpack.config.js 中添加resolve: { alias: { org/skills-http: path.resolve(__dirname, ../libs/skills/http/src/index.ts) } }注意第 5 种情况只发生在非 Nx 构建的子应用如用 Vite 的项目。Nx 构建的应用会自动处理 alias无需手动配置。5.2 “TypeScript 7.0 中 declare global 已弃用” 的迁移方案网络热词里提到typescript [{}]和declare global弃用警告这源于 TS 7.0 对全局声明的严格化。agent-skills 中常见于skills-auth的全局类型扩展// 错误写法TS 7.0 报错 declare global { interface Window { __AUTH_STATE__: AuthState; } }正确迁移方案有 3 种用 module augmentation 替代 global推荐创建libs/skills-auth/src/global.d.ts// libs/skills-auth/src/global.d.ts export {}; declare global { interface Window { __AUTH_STATE__: AuthState; } }关键是export {};这行它把文件变成模块避免 global 声明污染。用 ambient module 声明// libs/skills-auth/src/types/window.d.ts declare module window { interface Window { __AUTH_STATE__: AuthState; } }然后在tsconfig.json中include该文件。彻底移除全局声明改用函数参数传递// 不再依赖 window.__AUTH_STATE__ export function setAuthState(state: AuthState, options?: { writeToWindow?: boolean }) { localStorage.setItem(auth, JSON.stringify(state)); if (options?.writeToWindow) { (window as any).__AUTH_STATE__ state; } }这种方式最安全但要求所有调用方显式传参。5.3 Nx 二次开发中的 3 个高危操作Nx 本身可扩展但 agent-skills 架构下某些定制会破坏工程约束自定义 generator 时修改 project.json 的 targets错误在 generator 中直接写project.targets.build.executor myorg/my-builder风险Nx 的affected命令依赖标准 executor如nx/node:build的输入输出定义自定义 executor 若未正确声明inputs会导致缓存失效。正确继承标准 executor只覆盖必要逻辑// libs/builders/src/executors/custom-build/schema.d.ts export interface CustomBuildSchema extends NodeBuildExecutorSchema { // 新增字段 customFlag?: boolean; }在 workspace.json 中手动添加 project错误直接编辑workspace.json添加新 skills 项目风险Nx 的 project graph 缓存可能未更新导致nx graph显示不全。正确永远用nx g nx/workspace:library它会自动更新 workspace.json tsconfig.base.json .gitignore。用nx serve启动 skills 包错误nx serve skills-http风险skills 包是 library没有 entry pointserve 会失败且污染进程。正确skills 包只提供buildtarget测试用nx test skills-http开发时用nx build skills-http --watch生成 dist再由子应用引用。6. 从 skills 到 agent能力复用的下一阶段演进agent-skills 当前定位是“能力模块”但团队规模扩大后自然会走向“智能体agent”形态。这不是指接入大模型而是把 skills 组合成可自主决策的工作流。我们已在试点agent-deploy一个根据 Git 提交内容自动选择部署策略的 CLI 工具。它的工作流是nx affected --baseorigin/main --headHEAD获取变更的 skills 和 apps分析变更类型如果只改了skills-http→ 执行nx build skills-http nx release skills-http如果改了apps/web-app且包含src/app/pages/dashboard/→ 触发 E2E 测试如果同时改了skills-auth和apps/admin→ 发送 Slack 通知要求人工审核生成部署清单并执行kubectl apply -f deploy.yaml这个 agent 的核心不是 AI而是skills 的元信息 Nx 的影响分析 业务规则引擎。它证明 agent-skills 架构的终极价值当所有能力都标准化、可组合、可追溯时“自动化决策”就不再是科幻而是工程化的自然延伸。我在实际落地中最大的体会是不要追求一步到位的“完美 agent”先确保每个 skills 包的package.json里都有清晰的keywords字段如keywords: [http, network, api]再用脚本聚合这些 keywords 生成能力地图。地图有了agent 才有导航依据。现在回头看当初花两周时间规范 commit message 和 typescript 接口比后面三个月的 feature 开发还重要——因为前者决定了整个系统的可维护性天花板。
返回列表