阅读视图

发现新文章,点击刷新页面。

我的 Monorepo 实践经验:从基础概念到最佳实践

本文将系统地整理我在 Monorepo 和前端工程化方面的一些实践经验,先简单介绍几个概念。

Monorepo(单一代码仓库):Monorepo 是将多个应用和共享库放在同一个仓库中进行管理。与多仓库模式相比,Monorepo 更加高效,尤其在代码共享、依赖升级和跨项目协作方面。

工程化:通过规范、工具链和流程,把“能跑”提升到“可维护、可协作、可持续交付”。它覆盖的不只是开发,还包括构建、测试、发布、质量保障等完整生命周期。

Monorepo 里最关键的抽象:应用包与库包

应用包(App)

应用包是最终会被部署的项目,通常放在 apps/*,例如 Web、Admin、Docs、BFF 等。

库包(Package)

库包用于复用能力,通常放在 packages/*,本身一般不直接部署,而是被应用包消费。

在 Monorepo 里,库包常见有三种策略:

  1. 可发布包(Publishable Package)
  2. 预构建包(Compiled Package)
  3. 源码引用包(Source Package)

选择哪种策略取决于具体场景,没有绝对的优劣之分。

三种库包策略介绍

1) 可发布包:面向仓库外复用

如果这个包是要提供给外部团队或开源用户使用,那么应该采用可发布包的方式。

优点:

  • 对外分发标准清晰,边界明确
  • 可独立版本化,兼容性管理更规范

代价:

  • package.json 字段配置更复杂(nameexportstypesfilespublishConfig 等)
  • 可能需要考虑 CJS/ESM 的导出兼容与消费方式(如 import/require、不同 bundler 解析差异)
  • 需要维护发版流程、版本语义和变更记录
  • 在仓库内频繁迭代时,版本与锁文件更新会增加心智负担

2) 预构建包:面向仓库内复用、兼顾稳定与性能

预构建包首先会构建出 dist 文件,应用包再消费这些构建结果。

优点:

  • 应用包构建更加稳定,模块之间的边界更加清晰
  • 减少重复转译,尤其在大仓库中

代价:

  • 需要维护 build 步骤与产物一致性
  • 调试链路比直接使用源码更长

一个典型配置:

{
  "name": "@workspace/utils",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "default": "./dist/index.js"
    }
  },
  "scripts": {
    "build": "tsc"
  }
}

在工作区里引用通常用:

{
  "dependencies": {
    "@workspace/utils": "workspace:*"
  }
}

3) 源码引用包:开发体验优先

源码引用包直接导出 src/*.ts(x),由应用包的构建工具(如 Vite、Webpack等)完成转译。

优点:

  • 配置简单,改完即生效,开发体验好
  • 少一层“先打包再消费”的步骤

代价:

  • 应用包需要承担类型检查和转译的成本
  • 对 TypeScript 配置一致性要求更高

示例:

{
  "name": "@workspace/utils",
  "exports": {
    "./tool": "./src/tool.ts"
  }
}

这种模式通常不需要 build 脚本,但建议保留独立的类型检查脚本(例如 typecheck)。

很多团队最终会混用这三种策略:

  • UI 组件用预构建或源码引用
  • 配置类包(eslint、tsconfig)走源码引用
  • SDK 或公共能力包走可发布流程

如何初始化一个靠谱的 Monorepo

最好的学习方法就是模仿。可以参考 Turborepo Getting Started 页面。提供的示例

例如,Kitchen Sink中的 @repo/ui 采用了预构建包的方式。如果你想使用预构建包,可以重点查看 apps/admin 是如何消费它的。示例中还展示了 eslint-config 和 tsconfig 这类公共配置包的使用。不同包通过继承 base 配置,既能保证代码风格一致,也能针对不同项目做细化适配(比如规则微调或插件加载)。

如果你更倾向于使用源码引用包,可以参考Vite + React 示例。这个例子里的 @repo/ui 采用的就是源码引用包。

另外,Turborepo 还有一个面向 AI 的 best-practices/RULE.md,也可以读一读:链接

因为 Turborepo 本身就是 Monorepo 的任务编排工具,所以它的文档和示例质量都很高,值得反复参考。

共享 tsconfig.json:把配置做成一个包

在 monorepo 中创建一个共享的 tsconfig 配置包(比如放在 packages/typescript-config 里)是一个常见的做法:把一些通用的 TypeScript 配置写在基础配置(base.json)里,然后让各个项目(比如 Next.js 应用、库项目等)通过 extends 引用这个基础配置。这样整个仓库能有一致的 TS 设置。

packages/typescript-config 可以声明一个 package.json

{
  "name": "@repo/typescript-config"
}

关于 tsconfig references

在 Monorepo 中,很多人都会接触到 TypeScript 的 references 配置。Turborepo 官方建议,大多数情况下不需要使用 TypeScript 项目引用。

参考: You likely don’t need TypeScript Project References

它会引入额外的配置和缓存层,这可能会在使用 Turborepo 时带来问题,且很少能带来实际的好处。

具体来说:

  • 额外的配置:使用 TypeScript 项目引用时,你需要在不同的项目之间配置相应的 tsconfig.json 文件,这增加了配置的复杂度。

  • 额外的缓存层:TypeScript 项目引用为每个项目生成独立的构建输出,需要将缓存目录配置到.gitignore中,turbo.json中。

但在某些特定场景下,如 Hono RPC 的前后端类型联动时,项目引用非常必要。 参考:

如果你使用了源码引用包,建议统一关键编译选项(如 modulemoduleResolution),避免跨包解析不一致。

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Bundler"
  }
}

references 示例:

{
  "references": [{ "path": "../packages/utils/tsconfig.json" }]
}

关于 package imports(子路径导入)

在编写子包代码时,可以在 tsconfig 中使用 compilerOptions.paths 来创建别名。然而,这个别名只在当前 TypeScript 配置上下文中有效,不会被其他包自动读取。

如果你使用 TypeScript 5.4+,推荐使用 Node.js 的子路径导入(imports)来替代 TypeScript 的路径别名,在 package.json 内编写:

{
  "imports": {
    "#*": "./src/*"
  }
}

那源码中可以这样引用自己的文件:

import { MY_STRING } from "#utils.ts"; // Uses .ts extension
export const Button = () => {
  return <button>{MY_STRING}</button>;
};

通过这种方式,可以在模块内部使用子路径导入,不会受 TypeScript 配置的限制。这里的imports 主要解决包内部如何引用,exports 主要解决包对外暴露什么。也就是说,跨包消费还是走包名和 exports,而不是把 imports 当成跨包 alias。

这种模式下要注意导入路径和产物格式保持一致(例如编译包需要使用 .js 后缀)。

关于跨包“跳转到定义”的实现

在 Monorepo 项目中,多个包通常是彼此依赖的。如果你希望在 IDE(如 VSCode)中通过“跳转到定义”功能,在不同包之间轻松导航(例如,从 ui 包跳转到 utils 包中的代码),需要进行一些配置,以确保不同包之间的 TypeScript 类型信息能够正确链接和识别。

对于预构建包,当包已经编译后,跳转到定义的功能通常不会直接跳转到源代码。例如,点击一个 A.js 的导出,编辑器将跳转到 dist 文件夹中的生成代码,而不是源代码。为了确保跳转功能正常工作,需要在 TypeScript 配置文件中启用 declarationdeclarationMap 选项。这样生成的 .d.ts(类型声明文件)和 .d.ts.map(源映射文件)就能帮助编辑器找到原始的 TypeScript 源代码。

配置示例:

{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true
  }
}

我个人对这个问题还有另一种替代方法,可以通过配置 allowImportingTsExtensionsrewriteRelativeImportExtensions 来解决,同时改成源码引用包。启用这两个选项后,编辑器会识别并允许在代码中显式地使用 .ts 扩展名进行模块导入,这样跳转功能会直接指向原始的 TypeScript 源代码。代码里原有的import ’./A.js' 也可以改成import ’./A.ts'了。

相关配置如下:

{
  "compilerOptions": {
    "allowImportingTsExtensions": true,
    "rewriteRelativeImportExtensions": true
  }
}

配置项解释:

  • allowImportingTsExtensions:此配置项允许你在导入模块时显式使用 .ts 扩展名。默认情况下,TypeScript 会自动忽略文件扩展名,但启用此选项后,你可以在 import 语句中明确指定 .ts 扩展名。

  • rewriteRelativeImportExtensions:此配置项使得 TypeScript 在生成 JavaScript 代码时,会自动将相对路径导入的 .ts.tsx 扩展名重写为 .js 扩展名。这样,在 TypeScript 代码中使用 .ts 扩展名导入文件时,最终生成的 JavaScript 代码会使用 .js 扩展名,从而确保路径的兼容性。

通过这些配置,开发者可以更方便地在不同包之间进行跳转,提升开发效率。

用Turborepo 管理Package Graph 和 Task Graph

在一个大型项目中,或者跨语言项目中,可能会有很多命令。不同的包可能有自己的build命令,dev命令,lint命令,test命令,类型检查命令等等。这些包之间,可能还有依赖关系。一个命令以另一个命令完成为前提,而Turborepo可以很好的完成,这一节我们展示如何用 Turborepo 进行管理。

Package Graph(包图)

Turborepo 自动从你的 Monorepo 结构和各个子包的 package.json 里找出来的依赖关系图。比如你有一个 apps/web 应用,它依赖两个库 packages/uipackages/utils,Turborepo 就会把这些关联“连成线”,构成一个图,形成所有包之间的依赖网络。这个图是 Task Graph 的基础。

Task Graph(任务图)

任务图是 Turborepo 通过你的 turbo.json 配置和上面那个 Package Graph 从你的任务定义里构建出来的一个 有向无环图。节点(node)是任务(比如 build、lint、test),边(edge)表示任务之间的依赖关系——也就是 “这个任务要等另一个任务跑完才能运行”。

如果一个任务(比如 build)在 turbo.json 里写了 dependsOn: ["^build"],这就表示“在当前包的 build 任务之前,先跑掉所有它依赖的包的 build 任务”。这种依赖关系会被表示成一条从依赖任务指向当前任务的边。

例如,执行apps/web 应用的build命令之前,会先运行 packages/uipackages/utilsbuild命令。Turborepo 还有自己的缓存策略,通过指定任务的inputsoutputs,它可以观察文件是否改动,如果没有改动,就可以直接跳过这个步骤。同时,这两个任务还可以最大程度地并行化执行,提升构建效率。

此外,任务还有分类,如持续任务,可以将任务声明为"persistent": true。一些持续任务可能还需要另一个任务始终同时运行,如后端服务器,亦或者是路由库的router-cli,通过with字段可以设置自动启动。一些任务即使非TS语言仓库,也支持加入到Turborepo的任务流中。

Turborepo还支持使用tui在一个终端内同时查看所有日志并与任务进行交互。

Turborepo 能够提高构建与任务运行的效率,通过并行执行、缓存命中等优化手段加速你的 Monorepo 工作流。更多相关内容,请参考Crafting your repository - Configuring tasks

Catalog 目录协议

在 Monorepo 中,使用相同的依赖项版本非常常见。通过 pnpm-workspace.yaml 中的 Catalog 协议,我们可以减少依赖的重复并保持一致性:

  • 维护唯一版本 - 我们通常希望在工作空间中共同的依赖项版本一致。 Catalog 让工作区内共同依赖项的版本更容易维护。 重复的依赖关系可能会在运行时冲突并导致错误。 当使用打包器时,不同版本的重复依赖项也会增大项目体积。
  • 易于更新 — 升级或者更新依赖项版本时,只需编辑 pnpm-workspace.yaml 中的目录,而不需要更改所有用到该依赖项的 package.json 文件。
  • 减少合并冲突 — 由于在升级依赖项时不需要编辑 package.json 文件,所以这些依赖项版本更新时就不会发生 git 冲突。

如果你使用的pnpm,可以参考这个文档,如果是bun,则可以参考这个文档

以 pnpm 管理的 workspace 为例,它是这么使用的:

pnpm-workspace.yaml 中定义:

packages:
  - packages/*

# 定义目录和依赖版本号
catalog:
  react: ^18.3.1
  redux: ^5.0.1
{
  "name": "@example/app",
  "dependencies": {
    "react": "catalog:",
    "redux": "catalog:"
  }
}

Enginue 和包管理器配置

在 Monorepo 中使用合适的包管理器配置是至关重要的。尤其是在使用 pnpm 时,可以指定运行的 Node 版本以及 pnpm 的版本,确保包管理器和 Node.js 的版本一致,避免版本不兼容的问题。

{
  "engines": {
    "node": ">=10",
    "pnpm": ">=9"
  },
  "packageManager": "pnpm@9.3.0"
}

在本地开发时, 如果其版本与 engines 字段中指定的版本不匹配,pnpm 将始终失败并报错。

与之相对应的,还有一个pnpm-workspace.yaml的配置字段nodeVersion,当同时设置了 engine-strict=true 时,npm 会在安装包时检查你的 Node.js 版本是否大于或等于设置的版本范围(应当填精确的语义化版本号),如果不符合,安装会被拒绝。例如,当开发公共包时,设置这个选项可以保证不安装不支持特定node版本的依赖。参见链接

nodeVersion: 22.22.0
engineStrict: true

还有一个就是如果你使用了nvm配置,有时候项目根部会有一个.nvmrc文件来指定版本,这样在该目录下唤起Node时,会自动启动相应版本的Node。 例如可以设置useNodeVersion: 16.16.0。pnpm 将自动安装指定的 Node.js 版本,并使用它来运行pnpm run命令pnpm node。参见链接

Monorepo 的 hoist

Hoisting(提升)是指在安装依赖时,某些依赖会被提升到 node_modules 的顶层(根目录)。这种行为确保了在整个项目中可以共享某些常用的依赖包,而不是每个子包都单独安装一份。这有助于避免重复安装相同版本的依赖,减少磁盘空间的占用。

在 npm 和 yarn 中,依赖项的 hoisting 行为通常是自动的。当你安装依赖时,它们会根据包的依赖关系被扁平化,并被提升到 node_modules 根目录中。在 pnpm 中,依赖不会像在 npm 或 yarn 中那样自动扁平化,而是根据每个包的依赖结构创建嵌套的 node_modules 目录。

转存失败,建议直接上传图片文件

默认情况下,pnpm 创建一个半严格的 node_modules,所有依赖项都会被提升到 node_modules/.pnpm/node_modules。这使得 node_modules 中的所有包都可以访问未列出的依赖项,而 node_modules 之外的模块不行。通过这种布局,大多数的包都可以正常工作。

但是也会有一些不能正常工作例外,但你可以通过配置来控制。这种情况下,可能需要设置publicHoistPattern属性。命中的模块,会安装到根模块目录node_modules中。例如,之前版本的pnpm的默认配置['types', 'eslint', '@prettier/plugin-*', 'prettier-plugin-'],项目如果依赖了 eslintbabel,可以看到根模块目录中如下所示。一般来说,我们不需要关心这个,如果需要配置,依赖的文档会讲这些。

> tree node_modules -L 1
node_modules
  ├── @babel
  ├── @eslint
  ├── @types
  ├── @typescript-eslint
  ├── eslint
  ├── eslint-config-ali
  ├── eslint-import-resolver-node
  ├── eslint-module-utils
  ├── eslint-plugin-import
  ├── eslint-plugin-jsx-plus

关于 hoist 的更多知识,可以参考这个文章:A diagram to show how pnpm works

一些值得设置的 npmrc 配置或者 pnpm-workspace 设置

npmrcpnpm-workspace 中设置适当的配置项,能有效提高项目管理效率。

npmrc

  • registry:指定 npm 使用的默认注册表 URL。

  • save-exact:确保依赖项以精确版本安装,而不是使用版本范围,例如^1.2.3。

pnpm

  • prefer-frozen-lockfile:强制使用锁定文件中的依赖版本。如果设置为 true,即使 package.json 中的依赖有更新,也会优先使用锁定文件(pnpm-lock.yaml)中的版本,避免自动升级。

  • overrides:强制指定某些依赖包的版本,无论这些包在其他依赖包中是否有版本冲突。例如,假设你有两个依赖包 AB,它们依赖于同一个包 C,但它们的版本不同。通过 overrides,你可以强制这两个包都使用 C 的同一版本。


参考资料

  1. TypeScript 5.4: Auto-import support for subpath imports
  2. Turborepo TypeScript 指南
  3. 设置(pnpm-workspace.yaml)
  4. A diagram to show how pnpm works 5.Crafting your repository - Configuring tasks

AI 写代码总是半途而废?试试这个免费的工作流工具

作为一个用过多种 IDE 的开发者,我想分享一个让我效率 up up 的小工具。

你有没有遇到过这种情况?

  • 跟 AI 聊了半天需求,代码写了一半,上下文满了,AI "失忆"了
  • 项目做到一半搁置,一周后回来完全忘了做到哪了
  • 想加一个功能,结果 AI 把之前的代码改坏了

这些问题都有一个共同原因:上下文衰减(Context Rot)

简单来说,AI 的"记忆"是有限的。当对话太长时,它会慢慢忘掉之前说过的话,导致代码质量下降。

GSD 是什么?

GSD = Get Shit Done(把事做完)

它是一个开源的 AI 编程工作流框架,核心思路很简单:

把项目信息存到文件里,而不是全部塞给 AI。

就像你写代码会用 Git 做版本控制一样,GSD 帮你做"AI 对话的版本控制"。

GSD for Trae

原版 GSD 是为 Claude Code 设计的。因为我日常用 Trae,所以做了这个适配版本。

安装只需一行命令:

npx gsd-trae

或者:

bash <(curl -s https://raw.githubusercontent.com/Lionad-Morotar/get-shit-done-trae/main/install.sh)

它能帮你做什么?

1. 新项目规划

输入 /gsd:new-project,它会:

  • 问你一系列问题,搞清楚你要做什么
  • 自动研究技术方案(可选)
  • 生成项目路线图

2. 阶段式开发

大项目拆成小阶段:

  • /gsd:plan-phase 1 - 规划第一阶段
  • /gsd:execute-phase 1 - 执行第一阶段
  • /gsd:verify-work - 验证做得对不对

每完成一个阶段,进度都会被记录,随时可以接着做。

3. “断点续传”

关掉电脑、明天再来,输入 /gsd:progress,AI 马上知道:

  • 项目做到哪了
  • 接下来该做什么
  • 之前的决策是什么

实际使用感受

我用了一个月,相比 Trae 的 Plan Build 模式最明显的变化:

以前:一个功能聊到一半,AI 开始"胡言乱语",只能新开对话重来

现在:每个阶段都有清晰的目标和验收标准,AI 一直保持在正确的方向上

以前:同时开多个功能,代码互相冲突

现在:按阶段来,做完一个再做下一个,井井有条(进阶用户也可以选择 Worktree 模式)

以前:Plan 文档随意仍在 .trae 的文档目录,没有管理,很难查找

现在:结构化的目录,GSD 和开发者都能轻松阅读

适合谁用?

  • 用 Trae/Gemini/Claude 写代码的开发者
  • 做独立项目、 side project 的人
  • 觉得 AI 编程"聊不动"的新手

相比其他工具的优势

市面上有不少 AI 编程工作流工具,比如 GitHub 的 Spec Kit、OpenSpec、BMAD 等。GSD 的定位不太一样:

工具 特点 GSD 的区别
Spec Kit 企业级、严格阶段门控、30分钟启动 GSD 更轻量,5分钟上手,没有繁琐的仪式
OpenSpec 灵活快速、Node.js 运行 GSD 额外解决了 Context Rot 问题,支持断点续传
BMAD 21个 AI Agent、完整敏捷流程 GSD 不模拟团队,而是聚焦"让开发者高效完成项目"

简单说:如果你期待快速而结构化的流程,又不想被复杂的企业开发规范束缚的同时,确保 AI 编程能稳定输出,GSD 可能是目前最合适的选择。

它是免费的

开源项目,GitHub 地址: github.com/Lionad-Moro…

MIT 协议,可以随便用、随便改。

最后说一句

AI 编程工具越来越强大,但工具只是工具。

好的工作流能让你事半功倍,而 GSD 就是这样一套经过验证的工作流。

不需要改变你现有的开发习惯,安装后输入 /gsd:new-project 试试看。


如果你试过觉得好用,欢迎点个 Star ⭐

如果发现问题,也欢迎提 Issue

“啪啪啪”三下键盘,极速拉起你的 uni-app 项目!

说实话,我也不想造轮子。但试了一圈之后,我发现了一个让我忍不了的问题:选了不要某个功能,生成的代码里居然还有它的 import 和空壳文件。 与其花半小时手动删代码,不如用 hy-uni —— 三下键盘,1 秒钟搞定!


🚫 那些年,我们新建项目后手动删过的代码

如果你经常用社区的高分脚手架创建项目,一定会遇到这个进退两难的死胡同:

  • 官方模板太"毛坯":API 拦截器、状态管理全要自己从 0 开始配。新手直接劝退。

  • 社区模板太"精装":不仅送你一堆组件,还送你几个业务全景页。新建项目第一件事,就是花半小时去删那些不需要的页面和 npm 包。最痛苦的是,删的时候还得提心吊胆,生怕漏删了某个 import 导致整个项目一跑就白屏报错。

第 21 次从头搭项目时,我终于受不了了。于是,我过年时花了点时间写了 hy-uni


🎯 先说结论:三下键盘,极速拉起项目

一条命令,三下键盘,1 秒钟,带给你一个干干净净的、随时可进入业务开发的工业级 uni-app 项目:

# ⚡ 极速拉起纯净骨架(1 秒钟)
npx hy-uni my-app --pure
# 或者 📋 交互式精装配置(30 秒内完成)
npx hy-uni my-app

核心理念:你不要的功能,连一行代码、一段注释、一个 npm 依赖,都不该出现在最终的产物中。


⚡ 速度对比(为什么说"极速"?)

方案 时间 特点
hy-uni --pure ⚡ 1 秒 三下键盘极速拉起纯净骨架
hy-uni (交互) 📋 30 秒 选择功能后自动生成完整项目
官方脚手架 5 分钟+ 毛坯房,需要自己配置工程化
社区全量模板 10 分钟+ 功能全但冗余,需要手动删代码

关键对比:hy-uni 不仅快,而且不用删代码 —— 你不选的功能从代码到依赖全部消失。


💻 极客最爱的"双轨"构建体验

很多老手开发者拥有"代码洁癖",喜欢毫无业务代码的"极净空壳";也有很多开发者希望项目能"满级出生",自带网络请求和主题切换方案。

在这款 CLI 中,我们将选择权完全交还给你。

路线 A:极速构建"极致纯净"空壳(老手狂喜)

对于只想要**"帮我把工程化基建搭好,其他的我自己来"**的极客,你只需在命令后敲入一个 --pure 参数:


npx hy-uni my-app --pure

啪啪啪三下键盘,敲下回车,1秒钟静默生成。 没有任何繁琐的交互问答选项,你将直接获得一个强迫症狂喜的极净项目:

  • 只有基础工程化体系:Vue 3 + TypeScript + Vite + UnoCSS + Pinia 开箱即用。

  • 没有任何网络请求、主题切换、业务示例等多余代码。

  • 目录结构极其纯粹,没有多余的文件夹。

路线 B:交互式精装配置(开箱即用)

如果不加 --pure,CLI 则会提供完全可定制的丝滑交互面板:


┌ 🚀 火叶 - 快速创建高性能 uni-app 项目
│
● 模板来源: 缓存 (~/.huoye/templates/) [2天前更新]
│
◇ 请输入项目名称:
│ my-app
│
◇ 请选择创建路径:
│ ./demo
│
◇ 是否需要网络请求层?
│ ○ Yes / ● No
│
◆ 是否需要业务示例页面?
│ ○ Yes / ● No
│
◆ 是否需要主题管理?
│ ○ Yes / ● No
│
◆ 确认创建项目?
│ ● Yes / ○ No
│
◇ 🎉 恭喜!您的项目已准备就绪。
│
◇ Getting Started  ─────────╮
│                           │
│ $ cd demo/my-app          │
│ $ pnpm install            │
│ $ pnpm dev:h5             │
│                           │
├───────────────────────────╯

此时,选择全选 Yes 的你,将获得一个"满级配置"项目:

  • 封装极佳的 Http 客户端、请求拦截器体系及全局错误分类处理机制。

  • 完善的亮暗色主题无缝切换落地方案及 CSS 变量体系。

最硬核的是:无论你是走纯净路线还是全选路线,生成的项目 App.vuemain.ts 以及 package.json 中的所有代码,都会像你自己手写的一般融洽,没有任何一点"被暴力注销掉"的痕迹。

💡 温馨提示:三个功能之间有依赖关系。"业务示例页面"依赖"网络请求层"——因为示例必须有 API 封装才能跑起来。所以如果你不选"网络请求层",CLI 就不会问你要不要"业务示例"。这样设计是为了保证生成的项目永远可以直接运行,没有任何破碎的依赖关系。


💡 三种使用场景速查

我想要 命令 适合谁
极速纯净空壳 npx hy-uni my-app --pure 有代码洁癖的老手,想自己搭业务
交互式精装配置 npx hy-uni my-app 想要完整方案,但不想要冗余代码
本地开发版本 npx hy-uni my-app --local 项目贡献者,想用最新开发模板

📂 看看生成出来的项目差异

路线 A 生成结果(--pure)

my-app/
├── src/
│ ├── pages/
│ │ ├── index/index.vue
│ │ └── about/about.vue
│ ├── layouts/default.vue
│ ├── store/index.ts
│ ├── utils/
│ │ ├── platform.ts
│ │ ├── system.ts
│ │ ├── data.ts
│ │ └── time.ts
│ ├── style/
│ └── static/
├── vite.config.ts
├── tsconfig.json
└── package.json ← 只有基础依赖

路线 B 生成结果(全选)


my-app/
├── src/
│ ├── pages/
│ │ ├── index/index.vue
│ │ ├── about/about.vue
│ │ ├── theme/ ← 新增
│ │ └── examples/ ← 新增
│ │ ├── api-demo.vue
│ │ ├── form-demo.vue
│ │ └── list-demo.vue
│ ├── api/ ← 新增
│ │ ├── client.ts
│ │ ├── interceptors.ts
│ │ ├── errors.ts
│ │ └── modules/
│ ├── composables/
│ │ └── useTheme.ts ← 新增
│ ├── config/
│ │ └── theme.ts ← 新增
│ ├── components/
│ │ └── ThemeToggle.vue ← 新增
│ ├── store/
│ │ ├── theme.ts ← 新增
│ │ ├── index.ts
│ │ └── modules/
│ │ ├── app.ts
│ │ └── counter.ts ← 新增
│ ├── layouts/default.vue
│ ├── utils/
│ ├── style/
│ └── static/
├── vite.config.ts
├── tsconfig.json
└── package.json ← 完整的依赖列表

对比一目了然 —— 不选就是真的没有,不是"注释掉"。


🛠️ 不只是干净:开箱即用的重型工程底座

不管你怎么选裁剪,hy-uni 都为你提供了工业级的开发体验,包含了 7 个 Vite 核心插件的自动装配:

插件 作用
vite-plugin-uni-pages 页面自动路由生成
vite-plugin-uni-layouts 布局系统搭建
vite-plugin-uni-manifest manifest 编程化配置
vite-plugin-uni-components 组件按需自动导入
unplugin-auto-import Vue / uni-app API 自动导入
UnoCSS 原子化极速 CSS 构建
mp-selector-transform 小程序选择器兼容隔离转换

这意味着,创建完项目后:

  • 你不需要手动导入 refonMounted

  • 你不需要手动去繁琐的 pages.json 注册页面和组件。

  • 路径别名 @/src/ 已全部打通。

  • 开发体验直接拉满。


✨ 你到底能得到什么?

基础工程化(所有项目都有)

  • Vue 3 + TypeScript —— 类型安全,开发爽

  • Vite 5 —— 毫秒级热更新,极速开发

  • 7 个 Vite 插件 —— 页面自动路由、组件自动导入、manifest 编程化配置等,全配好

  • UnoCSS —— 按需生成原子化 CSS,再也不用手写 class

  • Pinia 状态管理 —— 开箱即用的持久化存储(适配小程序)

  • ESLint + TypeScript 类型检查 —— 代码规范自动化

可选功能 1:网络请求层

选了它,项目会多出完整的 src/api/ 目录:

import { get, post } from "@/api"
// GET 请求,自动拼接 params
const users = await get("/users", { page: 1, limit: 10 })
// POST 请求
const result = await post("/users", { name: "张三", age: 25 })

你获得了什么:

  • HTTP 客户端(基于 uni.request,支持 GET/POST/PUT/DELETE/PATCH)

  • 请求/响应/错误拦截器(自动注入 Token、处理超时等)

  • 7 种自定义错误分类(网络、超时、鉴权、权限等)

  • 跨平台兼容(H5 / 小程序 / App 无缝切换)

  • 完整的 API 模块化示例

不选它? src/api/ 目录根本不存在,package.json 里也没任何相关依赖。干干净净。

可选功能 2:主题管理

选了它,你就能这样用:

<script setup>
import { useTheme } from "@/composables/useTheme"
const { isDark, themeStore } = useTheme()
</script>
<template>
<button @click="themeStore.toggleTheme()">
{{ isDark ? "切换到亮色" : "切换到暗色" }}
</button>
</template>

你获得了什么:

  • 亮色/暗色/跟随系统 三种主题模式

  • 8 种预设主色调,可自定义

  • 20+ CSS 变量自动注入

  • 多端适配(H5 用 CSS 变量、小程序用全局事件、App 用状态栏同步)

  • 主题切换组件 + 完整的设置页面

不选它? 上面所有文件全部消失。布局组件里的主题代码也会被移除,替换成一个固定的 background-color: #f8f8f8 —— 不是留空,而是提供正确的 fallback。

可选功能 3:业务示例页面

选了它(需要先选网络请求层),你会得到 3 个完整的业务演示:

  • API 调用演示 —— 列表获取、详情查看、数据创建的完整流程

  • 表单演示 —— 输入、选择、复选、日期选择器,带表单验证

  • 列表演示 —— 上拉加载、下拉刷新、搜索过滤的完整实现

这不是 "Hello World",每个页面都是可以直接拿来改改就用的业务代码

不选它? 这些示例页面全部消失,首页上的导航入口也会一起消失(不会留下死链接)。


⚙️ 底层揭秘:如何做到代码级无痕裁剪?

一般的脚手架提供的是"多套模板分支组合"。而 hy-uni 创新性地引入了 "特征标记系统 (Feature Markers)",实现了一份源码,2^N 种自由组合引擎

我们在架构底层源码中,巧妙地隐藏了特定的注释标记:

1. 单行精确抹除

如果在 CLI 里没选 examples 示例功能,下面带有 // 【examples】 标记的代码行,会从物理层面直接消失:

export * from "./modules/app"
export { useCounterStore } from "./modules/counter" // 【examples】

2. 块级区域剥离(支持多语言环境)

如果没选 theme 主题功能,被包裹的代码块整块剥离(支持 TS、SCSS、Vue 甚至 HTML 注释):

<!-- 【theme:start】 -->
<view class="nav-link" @click="goToPage('/pages/theme')">
    <text>主题设置</text>
</view>
<!-- 【theme:end】 -->

3. 独门绝技:反向兜底(Fallback)裁剪

这是市面上其他脚手架极难做到的技术细节。针对"如果不选某个高阶模块,我仍然需要保留一套写死的基础兜底代码"的场景,我们设计了 ! 反向保留标记:


.layout {
    // 【!theme:start】 (如果没选动态主题,就保留这段写死的极简灰色背景)
    background-color: #f8f8f8;
    // 【!theme:end】

    // 【theme:start】 (如果选了主题,才保留动态的 CSS 变量注入机制)
    background-color: var(--bg-color-primary);
    transition: background-color 0.3s;
    // 【theme:end】
}

正是这套底层切割引擎,加上我们对 npm 依赖 dependencies 的按树剥离,以及支持功能间的链式感知(不支持底层功能时不展示进阶询问逻辑),才铸就了极致纯净的代码产物质量。


🔧 进阶:把它变成你们团队的专属黑科技

"这套裁剪逻辑不错,但我司有祖传架构,我单纯想白嫖这套神级裁剪引擎怎么办?"

完全没问题。整个脚手架能力是靠底层模板根目录的 .templaterc.json 驱动的:

{
"features": {
    "auth": {
           "name": "权限管理",
           "files": ["src/store/user.ts"],
           "dependencies": ["jwt-decode"]
        }
    }
}

结合在你的祖传代码里打上好 // 【auth】 标记,你就可以把 hy-uni 当作你们内部团队私有化的高阶脚手架来直接复用!

(剧透:在这个大版本之后,我们将正式支持 hy-uni template add 命令,允许你直接接管并挂载任意外部 Git 仓库,搭建你的私有定制生态!)


🚀 立即体验(极速拉起只需 3 个命令)

别再对着一堆乱糟糟的精装房一筹莫展了:

# 极速纯净版
npx hy-uni my-app --pure

创建后的常用命令

cd my-app
pnpm install

# 开发命令
pnpm dev:h5 # H5 本地开发(localhost:3000)
pnpm dev:mp # 微信小程序开发
pnpm dev:app # App 开发

# 构建命令
pnpm build:h5 # H5 生产构建
pnpm build:mp # 小程序构建

# 检查命令
pnpm lint # ESLint 检查 + 自动修复
pnpm type-check # TypeScript 类型检查


📊 跟现有方案对比

官方模板 社区全量模板 hy-uni
创建后能直接开发 ❌ 需要自己搭 ✅ 能,但要先删一堆 ✅ 开箱即用
功能选择 ❌ 无 ❌ 无 / 模板分支 ✅ 交互式按需选择
不要的功能 N/A ⚠️ 自己删(怕误删) ✅ 从代码到依赖全清理
生成代码质量 空壳 ⚠️ 可能有残留 ✅ 零残留,像手写的
模板维护成本 ⚠️ 高(N 个分支) ✅ 低(1 份模板)
极速纯净模式 --pure 1秒钟

🔗 获取地址(直达阵地)

核心源码不到 500 行,没有任何冗余包装。如果你也是代码洁癖患者,恰好懂我对极致整洁的坚持,欢迎来给我点一个宝贵的 Star!使用中发现任何 Bug,随时 Issue 见!


📌 总结

hy-uni

  • 我只想要骨架--pure 1秒钟搞定,零冗余

  • 我想要完整方案 → 交互式选择,按需组合

  • 我想要纯净但有示例 → 选 API + 示例,不选主题

  • 我想用自己的模板 → 即将支持,用我们的引擎

核心理念:你不要的功能,连一行代码都不该出现。


🚀 现在就试试


npx hy-uni my-app

让我们一起告别"删文件夹"的时代。

Vitest Environment UniApp:让 uni-app E2E 测试变得前所未有的简单

FliPPeDround

前端工程师 · 开源爱好者 · 正在找工作

对我的项目感兴趣?查看我的简历 · resume

如果你曾尝试为 uni-app 项目编写 E2E 测试,你大概率会遇到这样的困境:官方提供的 Jest 环境配置复杂、文档更新滞后、与现代测试工具链集成困难。更糟糕的是,当你想要使用 Vitest 这种更现代、更快速的测试框架时,却发现没有合适的 uni-app 环境支持。

为了解决这些痛点,vitest-environment-uniapp 应运而生。作为一款轻量级的 Vitest 自定义环境,它让 uni-app 项目的 E2E 测试变得前所未有的简单和高效。

📖 介绍

vitest-environment-uniapp 是一个专门为 Vitest 设计的 uni-app E2E 测试环境。它深度集成了 DCloud 官方的 @dcloudio/uni-automator,让你能够在 Vitest 框架下无缝运行 uni-app 的自动化测试。

这个工具的出现,填补了 uni-app 现代化测试工具链的空白。它不仅保持了与官方 automator 的完全兼容性,还充分发挥了 Vitest 的性能优势,为开发者提供了一个更快速、更现代化的测试解决方案。

🚀 核心功能与技术优势

1. 无缝集成 Vitest 生态

vitest-environment-uniapp 完全兼容 Vitest 的配置系统,你可以像使用其他 Vitest 环境一样简单配置:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'uniapp',
    environmentOptions: {
      uniapp: {
        compile: true,
        platform: 'mp-weixin',
        projectPath: './src',
        port: 5121,
      },
    },
  },
})

2. 支持多平台测试

基于 @dcloudio/uni-automator 的强大能力,该工具支持 uni-app 的多个平台:

  • 微信小程序(mp-weixin)
  • app(需要额外安装 HBuilderX)
  • H5 平台(需要额外安装 playwright)

3. 智能环境管理

工具内部实现了智能的环境初始化和清理机制:

  • 自动管理 uni-app 程序的生命周期
  • 支持远程调试模式
  • 内置超时保护机制
  • 完善的错误处理和日志输出

4. TypeScript 完整支持

提供了完整的 TypeScript 类型定义,让开发者在编写测试代码时享受完整的类型提示和智能补全:

{
  "compilerOptions": {
    "types": [
      "vitest-environment-uniapp/types"
    ]
  }
}

🧪 为什么 E2E 测试如此重要

在软件开发中,单元测试固然重要,但 E2E(End-to-End)测试在构建高质量代码过程中扮演着不可替代的角色。

提升代码可靠性

E2E 测试模拟真实用户的使用场景,从用户界面到后端服务的完整流程进行验证。与单元测试不同,E2E 测试能够发现:

  • 组件间的集成问题
  • 路由跳转逻辑错误
  • 状态管理异常
  • 平台兼容性问题

对于 uni-app 这种跨平台开发框架,E2E 测试尤为重要。它能够确保你的应用在不同平台上都能正常运行,避免出现"在开发环境正常,上线后出问题"的尴尬情况。

降低维护成本

虽然编写 E2E 测试需要投入一定的时间成本,但从长远来看,它能显著降低维护成本:

  • 减少回归测试时间:自动化测试可以在几分钟内完成原本需要数小时的手动测试
  • 快速定位问题:当出现 bug 时,E2E 测试能够快速定位问题所在
  • 增强重构信心:有了完善的测试覆盖,你可以放心地进行代码重构,而不必担心破坏现有功能
  • 文档化业务逻辑:测试代码本身就是最好的业务逻辑文档

提升团队协作效率

E2E 测试作为项目质量的"守门员",能够:

  • 统一团队对功能实现的理解
  • 减少 code review 时的争议
  • 让新成员快速理解项目功能
  • 建立持续集成的质量保障体系

📦 快速上手

安装依赖

首先,安装必要的依赖:

pnpm i -D vitest-environment-uniapp @dcloudio/uni-automator

⚠️ 注意:@dcloudio/uni-automator 是必需的依赖,必须安装

配置 Vitest

在项目根目录创建或修改 vitest.config.ts

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    environment: 'uniapp',
    environmentOptions: {
      uniapp: {
        compile: true,
        platform: 'mp-weixin',
        projectPath: './src',
        port: 5121,
      },
    },
  },
})

配置 TypeScript

tsconfig.json 中添加类型定义:

{
  "compilerOptions": {
    "types": [
      "vitest-environment-uniapp/types"
    ]
  }
}

添加测试脚本

package.json 中添加测试命令:

{
  "scripts": {
    "test": "vitest"
  }
}

编写测试用例

创建测试文件,例如 pages/index.test.ts

import { beforeAll, describe, expect, it } from 'vitest'

describe('首页测试', () => {
  let page: Page
  beforeAll(async () => {
    page = await program.currentPage()
    await page.waitFor(3000)
  })

  it('检查页面标题', async () => {
    const el = await page.$('.uni-helper-logo__label')
    const titleText = await el.text()
    expect(titleText).toEqual('uni-helper')
  })
})

运行测试

执行测试命令:

pnpm test

🎯 环境配置选项详解

environmentOptions.uniapp 支持以下配置选项:

  • compile: 是否在测试前编译项目(默认:false)
  • platform: 目标平台,如 mp-weixinmp-alipay
  • projectPath: uni-app 项目路径
  • port: 开发服务器端口
  • devtools.remote: 是否启用远程调试模式

💡 提示:完整的配置参数参考 uni-app 官方文档。需要注意的是,官方文档可能存在更新滞后,建议以实际可用参数为准。

🔧 技术实现细节

vitest-environment-uniapp 的实现基于 Vitest 的自定义环境 API,核心类 UniAppEnvironment 实现了以下功能:

  1. 环境初始化:在 setup 方法中初始化 uni-app automator
  2. 全局对象注入:将 uniprogram 注入到全局作用域
  3. 资源清理:在 teardown 方法中正确清理资源
  4. 错误处理:完善的错误捕获和日志输出机制

工具内部还实现了智能的状态管理,避免重复初始化,确保测试环境的稳定性。

🌟 总结

vitest-environment-uniapp 为 uni-app 开发者提供了一个现代化、高效的 E2E 测试解决方案。它不仅解决了传统测试工具配置复杂的问题,还充分发挥了 Vitest 的性能优势。

通过完善的 E2E 测试,你可以:

  • 提升代码质量和可靠性
  • 降低长期维护成本
  • 增强团队协作效率
  • 建立持续集成的质量保障体系

如果你正在开发 uni-app 项目,并且想要建立完善的测试体系,vitest-environment-uniapp 绝对值得一试。它会让你的测试工作变得前所未有的简单和高效。

📚 相关资源


最后

vitest-environment-uniapp 是一个免费的开源软件,遵循MIT协议,社区的赞助使其能够有更好的发展。

你的赞助会帮助我更好的维护@uni-helper,如果对你有帮助,请考虑赞助一下😊

你的star🌟也是对我的很大鼓励,Github

欢迎反馈问题和提pr共建

更多关于uni-helper更多文章

❌