🧠 架构总览
本章讲系统是如何组织的、为什么这样组织、底层每一层在解决什么问题。
读完之后你应该能:
- 📐 给同事画出整条构建链路
- 🔍 定位一个 bug 应该去哪个包查
- 🎯 判断一个新 feature 应该加在哪层
一、🧭 设计哲学
1.1 单一真理源(Single Source of Truth)
核心约束
整个 kit 的所有工具链能力,只能读 project.config.ts,不允许自行维护另一份配置。
传统 MPA 项目的痛苦:
❌ 传统工具链:三份配置不同步
webpack.config.js ← 列出入口文件
package.json ← scripts: "build:home", "build:settings"
src/routes.json ← 同一份 page 列表的第三个副本
改一个 page 名字,三处都要改,漏掉一处就跑不起来。
lhx-kit 的做法:
✅ lhx-kit:所有工具读同一个配置
1project.config.ts ← SSOT:唯一可写位置
2 ↓
3 ├─ @lhx-kit/cli(build / dev / add)
4 ├─ @lhx-kit/vite-plugin(input / alias / define)
5 ├─ @lhx-kit/offline(whitelist / pages)
6 └─ @lhx-kit/config(validateAgainstFilesystem)
1.2 分层架构
┌─────────────────────────────────────────────────────────┐
│ Layer 4: UI 层 │
│ (examples / 用户自建项目) │
│ React / Vue 组件 + 业务逻辑 │
└─────────────────────┬───────────────────────────────────┘
│ peer dep
┌─────────────────────▼───────────────────────────────────┐
│ Layer 3: 能力层 │
│ @lhx-kit/runtime @lhx-kit/renderer │
│ 浏览器侧运行时 配置驱动渲染 │
│ (request/mobile/...) (schema/walker/...) │
└─────────────────────┬───────────────────────────────────┘
│ import
┌─────────────────────▼───────────────────────────────────┐
│ Layer 2: 编排层 │
│ @lhx-kit/vite-plugin @lhx-kit/offline │
│ 构建管线编排 离线包生产 │
└─────────────────────┬───────────────────────────────────┘
│ uses
┌─────────────────────▼───────────────────────────────────┐
│ Layer 1: 基础层 │
│ @lhx-kit/config (ssot) @lhx-kit/cli (input) │
└─────────────────────────────────────────────────────────┘
四层严格单向依赖:
- Layer 4 依赖 Layer 3 / 2 / 1
- Layer 3 依赖 Layer 1(不依赖 Layer 2)
- Layer 2 依赖 Layer 1
- Layer 1 不依赖任何内部包
为什么 Layer 3 不能依赖 Layer 2
如果 @lhx-kit/runtime 依赖 @lhx-kit/vite-plugin,就意味着"运行时代码需要构建时代码才能跑"—— SSR / Node.js 环境会直接炸。这是**"运行时代码零构建依赖"**的硬约束。
1.3 可渐进采用(Progressive Adoption)
最小集:只用 CLI 创建项目 → 然后就用普通 Vite
↓
扩展:启用 runtime/mobile 做适配
↓
扩展:启用 renderer 做 A/B 配置化
↓
扩展:启用 offline 打离线包
↓
扩展:启用 CDN 外挂优化首屏
每一步都可选;不用某个能力时,它不出现在 bundle 里。
二、📦 Monorepo 布局
unkown/
unkown/
├── apps/
│ └── docs/ # 📘 文档站(Rspress)
├── examples/
│ ├── vmpa/ # 🟢 Vue3 MPA 示例
│ └── rmpa/ # 🔵 React MPA 示例
├── packages/
│ ├── config/ # @lhx-kit/config — Layer 1 SSOT
│ ├── cli/ # @lhx-kit/cli — Layer 1 入口
│ ├── vite-plugin/ # @lhx-kit/vite-plugin — Layer 2 编排
│ ├── offline/ # @lhx-kit/offline — Layer 2 离线
│ ├── runtime/ # @lhx-kit/runtime — Layer 3 能力
│ └── renderer/ # @lhx-kit/renderer — Layer 3 渲染
├── scripts/
├── pnpm-workspace.yaml
└── tsconfig.base.json
为什么 docs 放
apps/ 不放 packages/
packages/ 是发布到 npm 的库
apps/ 是部署到服务器的应用
docs 是一个部署到 GitHub Pages 的 Rspress 站点,它不会被 import,也不发 npm——属于应用而非库。分开放避免 pnpm -r publish 误发。
三、🔗 包依赖关系图
project.config.ts
(SSOT)
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌─────────────┐ ┌───────────┐
│ config │◄───│ vite-plugin │ │ offline │
└──────────┘ └─────┬───────┘ └─────┬─────┘
▲ │ │
│ peer │ uses │ uses
│ ▼ │
│ ┌─────────────┐ │
└─────────┤ cli ├─────────┘
└─────┬───────┘
│
┌─────┴────────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ runtime │ │ renderer │
└────┬─────┘ └────┬─────┘
│ │
└──────┬───────────┘
▼
用户业务代码 (examples)
四、🎬 完整执行时序:以 lhx-cli dev 为例
┌─────────────────────────────────────────────────────────┐
│ 1. 用户执行 lhx-cli dev │
│ ↓ │
│ 2. packages/cli/src/bin.ts 入口 │
│ cac 路由到 commands/dev-build.ts │
│ ↓ │
│ 3. @lhx-kit/config::loadProjectConfig │
│ - jiti 动态加载 TS 配置(不预编译) │
│ - zod.parse 严格校验 │
│ - validateAgainstFilesystem 做文件存在性检查 │
│ ↓ │
│ 4. @lhx-kit/cli::context.ts │
│ 组装 Context {project, env, mode, pages} │
│ ↓ │
│ 5. 启动 Vite(createServer) │
│ vite.config.ts 里 plugins: [lhxKit()] │
│ ↓ │
│ 6. @lhx-kit/vite-plugin::config() hook │
│ - 生成 .lhx-kit/pages/*.html(中间 HTML) │
│ - 翻译成 Vite UserConfig: │
│ · input: {home: '.../home.html', ...} │
│ · resolve.alias: [...] │
│ · define: {LHX_API_BASE, LHX_MODE, ...} │
│ · rollupOptions.output.manualChunks │
│ · experimentalMinChunkSize: 10KB │
│ ↓ │
│ 7. Vite dev server started on :5173 │
│ ↓ │
│ 8. 浏览器请求 /home │
│ → cleanUrls middleware 映射到 home.html │
│ → Vite 按 entry 流程加载 │
│ ↓ │
│ 9. HMR ready │
└─────────────────────────────────────────────────────────┘
build 流程基本相同,只是第 7 步变成 vite build,第 8 步变成 transform() 改写 CDN + generateBundle() 重组 chunk。完整流程见 Vite 插件实现详解。
五、📚 各包职责详解
5.1 @lhx-kit/config — 单一真理源
职责:读取、校验、规范化配置。
packages/config/src/loader.ts showLineNumbers
import {createJiti} from 'jiti';
export async function loadProjectConfig(rootDir: string) {
const configPath = findConfig(rootDir, 'project.config');
const jiti = createJiti(rootDir, {
moduleCache: false, // ← 关键:关掉 require 缓存让 HMR 生效
interopDefault: true // 保证 export default 读成默认导出
});
const raw = await jiti.import(configPath);
return resolveProjectConfig(raw); // zod.parse + 默认值填充
}
为什么用 jiti 不用 esbuild / tsx
| 工具 |
启动成本 |
ESM / TS 支持 |
备注 |
esbuild |
🔴 50MB 二进制 |
✅ |
依赖 native binary,Docker 交叉编译踩坑 |
tsx |
🟡 中 |
✅ |
进程级,启动慢 |
ts-node |
🔴 慢 |
⚠️ |
ESM 兼容有缺陷 |
jiti |
🟢 纯 JS |
✅ |
适合程序内加载单个配置文件 |
我们只需要加载一个几十行的 TS 文件,不需要全量 TS 编译。jiti 是正好匹配这个场景的轻量方案。
算法:resolveEnv 环境降级
packages/config/src/helpers.ts
const ENV_FALLBACK_ORDER = ['prod', 'staging', 'test', 'dev'] as const;
export function resolveEnv(project: ResolvedProjectConfig, mode: string): EnvEntry {
// 1. 精确匹配
const direct = project.envs[mode];
if (direct) return direct;
// 2. 降级链:找第一个存在的
for (const name of ENV_FALLBACK_ORDER) {
const env = project.envs[name];
if (env) return env;
}
// 3. schema 已经保证至少有一个,走不到这里
throw new Error('unreachable: env schema requires at least one entry');
}
5.2 @lhx-kit/cli — 入口
packages/cli/src/
cli/
├── bin.ts # #!/usr/bin/env node 入口
├── index.ts # cac 路由注册
├── context.ts # Context 构造(读配置 + 环境)
├── commands/
│ ├── create.ts # 脚手架创建
│ ├── add.ts # add page / component / ...
│ ├── dev-build.ts # dev / build / preview
│ ├── info.ts # 配置摘要打印
│ ├── doctor.ts # 环境与配置诊断
│ ├── offline.ts # offline 子命令
│ └── upgrade.ts # 升级(占位)
├── ast/ # ts-morph 封装
├── scaffold/ # 模板拷贝与占位替换
└── ui.ts # 交互式 UI(prompts + kolorist)
5.3 @lhx-kit/vite-plugin — MPA 编排器
核心:把 project.config.ts 翻译成 Vite 能理解的 UserConfig,并接管 MPA 相关的所有产物重写。
5 个钩子时序见 Vite 插件实现详解。
5.4 @lhx-kit/runtime — 浏览器侧能力
subpath exports 设计
import {setupMobile} from '@lhx-kit/runtime/mobile'; // ✅ 只引 mobile
import {createRequest} from '@lhx-kit/runtime/request'; // ✅ 只引 request
// 其他子模块不会被 tree-shake 进来
完整子模块见 Runtime 概览。
5.5 @lhx-kit/renderer — 配置驱动 UI
renderer 的 5 个核心函数
schema.ts → PageSchema / ComponentSchema 类型
walker.ts → walkComponents() 组件树遍历
expression.ts → resolveValue() CSP 安全表达式求值
merge.ts → mergeSchema() patch 合并
variant.ts → resolveVariant() A/B 选组
完整算法见 Renderer 概览。
5.6 @lhx-kit/offline — 离线打包
打包流程
dist/ → 白名单过滤 → sha256 指纹 → manifest.json → AdmZip → .zip
↓
HTML CDN urls 置空
完整算法见 离线打包。
六、🧪 依赖表
所有 生产依赖 汇总如下(devDependencies 省略):
| 包 |
依赖 |
作用 |
@lhx-kit/config |
jiti zod |
加载 TS 配置 + 运行时校验 |
@lhx-kit/cli |
cac prompts kolorist execa fs-extra giget jiti ts-morph |
命令解析 / 交互 / 彩色输出 / 子进程 / 文件系统 / 模板拉取 / TS 加载 / AST 操作 |
@lhx-kit/vite-plugin |
@lhx-kit/config @lhx-kit/runtime |
peer: vite ^5/6/7 |
@lhx-kit/runtime |
axios ua-parser-js |
HTTP 客户端 + UA 解析 |
@lhx-kit/renderer |
zod |
peer: react >=18 / vue >=3(均可选) |
@lhx-kit/offline |
adm-zip fs-extra zod |
zip 打包 / 文件系统 / 校验 |
为什么选 zod 而不是 joi / yup / ajv
| 特性 |
zod |
joi |
yup |
ajv |
| TS 优先(infer) |
✅ |
❌ |
🟡 |
❌ |
| Bundle 体积 |
🟡 54KB |
🔴 150KB |
🟢 40KB |
🟡 60KB |
| Discriminated union |
✅ |
❌ |
❌ |
✅ |
| 错误信息可读性 |
🟢 |
🟡 |
🟡 |
🔴 |
我们需要的场景:TS 类型推导 + 精细的错误信息 + discriminated union。zod 是唯一全部满足的方案。体积问题通过 dynamic import 在客户端规避(见 Renderer 概览 §4)。
七、📖 约定大于配置
| 约定 |
含义 |
src/pages/<name>/entry.{ts|tsx} |
每个 page 的入口文件位置 |
src/pages/<name>/render.json |
renderer 就近 schema(可选) |
src/pages/<name>/router.{ts|tsx} |
page 内路由(可选) |
.lhx-kit/pages/ |
插件生成的中间 HTML 目录 |
dist/<name>/index.html |
每个 page 的最终产物 |
dist/shared/assets/ |
跨 page 共享的 chunks |
dist-offline/manifest.json |
离线包清单 |
dist-offline/files/ |
离线包待打包文件 |
不要打破约定
违反上面任何一条,都需要修改 Vite 插件源码才能让工具链识别。约定是 lhx-kit 的 API,不是内部实现细节。
八、🎯 性能设计原则
详细论证见 ⚡ 性能优化
- HTTP/2 下 chunk 门槛 = 10KB:拆小了亏 RTT,拆大了亏缓存粒度
- 家族分组 + 最小 chunk 合并双策略:
FAMILY_GROUPS 限定 React / Vue 全家桶不分裂
- CDN 外挂可选:打进 bundle 是基线,CDN 是优化
- 预压缩双格式:
.gz + .br 都产,nginx 按 Accept-Encoding 协商
九、🔭 扩展性设计
9.1 怎么加一个新模板
packages/cli/templates/vue3-admin/
vue3-admin/
├── template.json # 元数据(feature 清单 / postCreate 提示)
├── files/ # 基础文件
└── features/ # 按勾选追加
不需要改 CLI 源码。模板是数据驱动的,create 命令扫描 templates/ 目录发现可选模板。
9.2 怎么加一个新运行时子模块
packages/runtime/src/tracker.ts
// 1. 新增文件
export function createTracker(opts) { ... }
packages/runtime/package.json
{
"exports": {
"./tracker": { // 2. 追加 exports 字段
"types": "./dist/tracker.d.ts",
"default": "./dist/tracker.js"
}
}
}
packages/runtime/tsup.config.ts
{
entry: ['src/*.ts'] // 3. 已经是通配符,自动包含
}
用户写 import {createTracker} from '@lhx-kit/runtime/tracker' 即可。
9.3 怎么加一个新 vite 插件钩子行为
packages/vite-plugin/src/plugin.ts
// 直接在 mainPlugin 对象里加一个 hook
const mainPlugin: Plugin = {
name: 'lhx-kit',
enforce: 'pre',
async config(userConfig, env) { ... },
transform(code, id) { ... },
generateBundle(options, bundle) { ... },
// ↓ 新增
handleHotUpdate(ctx) { ... }
};
十、🔗 下一步