Skip to content

搭建项目架构

本节对标 Vue 3 源码仓库 vuejs/core 的整体架构设计

项目定位

我们要实现一个 mini 版的 Vue 3,目标是覆盖 Vue 3 的核心原理,而不是实现一个功能完整的框架。

项目特点:

  • 库,而不是业务项目
  • Monorepo 管理多个子包
  • TypeScript 编写
  • 希望打包产物可读性高

项目结构设计

Vue 3 源码采用 Monorepo 架构,将不同职责拆分为独立的 package:

mini-vue/
├── packages/
│   ├── reactivity/          # 响应式系统
│   │   └── src/
│   │       ├── reactive.ts
│   │       ├── effect.ts
│   │       ├── ref.ts
│   │       ├── computed.ts
│   │       └── index.ts
│   ├── runtime-core/        # 运行时核心
│   │   └── src/
│   │       ├── vnode.ts
│   │       ├── renderer.ts
│   │       ├── component.ts
│   │       ├── scheduler.ts
│   │       └── index.ts
│   ├── runtime-dom/         # DOM 平台渲染
│   │   └── src/
│   │       ├── nodeOps.ts
│   │       ├── patchProp.ts
│   │       └── index.ts
│   ├── compiler-core/       # 编译器核心
│   │   └── src/
│   │       ├── parse.ts
│   │       ├── transform.ts
│   │       ├── codegen.ts
│   │       └── index.ts
│   └── shared/              # 公共工具
│       └── src/
│           └── index.ts
├── pnpm-workspace.yaml
├── tsconfig.json
├── rollup.config.js
└── package.json

为什么要这样拆分?

Vue 3 源码最大的设计亮点之一是 模块解耦

  • reactivity 可以独立于 Vue 使用(比如在 Node.js 中单独用响应式能力)
  • runtime-core 不依赖具体平台(DOM / Canvas / Native 都可以)
  • runtime-dom 是 DOM 平台的具体实现
  • compiler-core 负责模板到 render 函数的编译,与运行时完全分离

这种设计和 React 的 react + react-dom + react-reconciler 分离思想一致。

Monorepo 技术选型

pnpm workspace

与 mini-react 项目一致,使用 pnpm workspace 管理 monorepo。

初始化:

bash
npm install -g pnpm
pnpm init

创建 pnpm-workspace.yaml

yaml
packages:
  - 'packages/*' # 声明所有子包都在 packages 目录下,pnpm 会自动识别并管理它们之间的依赖关系

各子包的 package.json

reactivity 为例:

json
{
  "name": "@mini-vue/reactivity", // 包名使用 @mini-vue 作用域,与 monorepo 中其他包保持一致的命名空间
  "version": "0.0.1",
  "main": "index.js", // CommonJS 入口,供 require() 方式引用
  "module": "dist/reactivity.esm-bundler.js", // ESM 入口,供 import 方式引用,打包工具会优先使用此字段
  "types": "dist/reactivity.d.ts", // TypeScript 类型声明文件入口
  "scripts": {
    "build": "rollup -c" // 使用 rollup 进行打包,-c 表示读取 rollup.config.js 配置文件
  }
}

shared 包作为公共依赖:

json
{
  "name": "@mini-vue/shared",
  "version": "0.0.1",
  "main": "src/index.ts" // 直接指向源码入口,因为 shared 包在开发阶段不需要单独打包,其他子包会直接引用源码
}

reactivity 中引用 shared

bash
pnpm add @mini-vue/shared --filter @mini-vue/reactivity --workspace

开发规范

TypeScript 配置

json
{
  "compilerOptions": {
    "target": "ESNext", // 编译目标为最新的 ES 标准,保留所有现代语法特性
    "module": "ESNext", // 模块系统使用 ESM,支持 import/export 语法
    "lib": ["ESNext", "DOM"], // 包含 ESNext 和 DOM 的类型定义,因为我们需要操作浏览器 DOM API
    "moduleResolution": "Node", // 使用 Node.js 风格的模块解析策略(查找 node_modules、index 文件等)
    "strict": true, // 启用所有严格类型检查选项,提高代码质量
    "sourceMap": true, // 生成 source map 文件,方便调试时定位到 TypeScript 源码
    "resolveJsonModule": true, // 允许导入 JSON 文件并自动推断类型
    "esModuleInterop": true, // 允许 ESM 和 CJS 模块之间的互操作,支持 import x from 'cjs-module' 语法
    "noUnusedLocals": true, // 报告未使用的局部变量,减少冗余代码
    "noUnusedParameters": true, // 报告未使用的函数参数
    "noImplicitReturns": false, // 允许函数中不是所有路径都有返回值(Vue 源码中部分函数如此)
    "skipLibCheck": true, // 跳过第三方库的类型检查,加快编译速度
    "baseUrl": ".", // 设置项目根目录为基准路径,配合 paths 使用
    "paths": {
      "@mini-vue/*": ["packages/*/src"] // 路径别名映射,使 @mini-vue/reactivity 等导入路径解析到对应子包的 src 目录
    }
  },
  "include": ["packages/*/src"] // 只编译 packages 下各子包的 src 目录中的 TypeScript 文件
}

paths 配置是关键,它让我们可以用 @mini-vue/reactivity 这样的路径引用其他子包。

测试框架

使用 vitest,与 mini-react 项目保持一致:

bash
pnpm add vitest -D -w

配置 vitest.config.ts

ts
// 从 vitest 中导入 defineConfig,用于获得配置的类型提示和智能补全
import { defineConfig } from 'vitest/config'
// 导入 path 模块,用于处理文件路径
import path from 'path'

export default defineConfig({
  test: {
    globals: true, // 全局注入 describe/it/expect 等测试 API,无需每个测试文件手动导入
  },
  resolve: {
    // 配置模块别名,使测试中 import '@mini-vue/xxx' 能正确解析到对应子包的源码目录
    alias: {
      '@mini-vue/reactivity': path.resolve(__dirname, 'packages/reactivity/src'),
      '@mini-vue/runtime-core': path.resolve(__dirname, 'packages/runtime-core/src'),
      '@mini-vue/runtime-dom': path.resolve(__dirname, 'packages/runtime-dom/src'),
      '@mini-vue/compiler-core': path.resolve(__dirname, 'packages/compiler-core/src'),
      '@mini-vue/shared': path.resolve(__dirname, 'packages/shared/src'),
    },
  },
})

打包工具

同样选择 rollup,原因与 mini-react 一致:

  • 库项目,不是业务项目
  • 打包产物可读性高
  • 原生支持 ESM
  • Vue 3 源码也使用 rollup

安装:

bash
pnpm add rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-commonjs -D -w

基础 rollup 配置:

js
// 导入 rollup 插件:TypeScript 编译支持
import typescript from 'rollup-plugin-typescript2'
// 导入 rollup 插件:解析 node_modules 中的第三方模块
import resolve from '@rollup/plugin-node-resolve'
// 导入 rollup 插件:将 CommonJS 模块转换为 ES 模块,使其能被 rollup 处理
import commonjs from '@rollup/plugin-commonjs'

export default {
  // 打包入口文件,指定 reactivity 包的源码入口
  input: 'packages/reactivity/src/index.ts',
  output: [
    {
      file: 'packages/reactivity/dist/reactivity.cjs.js',
      format: 'cjs', // 输出 CommonJS 格式,供 Node.js 环境下 require() 使用
    },
    {
      file: 'packages/reactivity/dist/reactivity.esm-bundler.js',
      format: 'es', // 输出 ES Module 格式,供现代打包工具(webpack/vite)使用
    },
  ],
  plugins: [
    resolve(), // 解析第三方依赖
    commonjs(), // 转换 CJS 模块为 ESM
    typescript(), // 编译 TypeScript 为 JavaScript
  ],
}

本节小结

本节完成了:

  1. 项目结构设计 — 采用 Monorepo,拆分为 5 个核心子包
  2. 开发规范 — TypeScript + vitest + rollup
  3. 模块依赖关系 — shared → reactivity → runtime-core → runtime-dom / compiler-core

下一节开始进入 Vue 3 最核心的模块 — 响应式系统

用心学习,用代码说话 💻