Skip to content

1.2 构建与调试:Vue 3 的开发工作流

引言

Vue 3 采用了现代化的构建系统,结合 esbuild 的快速开发构建和 Rollup 的优化生产构建,为开发者提供了高效的开发体验。本节将深入分析 Vue 3 的构建流程、开发模式配置以及调试技巧,帮助开发者更好地理解和使用 Vue 3 的构建系统。

构建系统架构

双构建策略

Vue 3 采用了双构建策略:

  • 开发构建:使用 esbuild,追求构建速度
  • 生产构建:使用 Rollup,追求包体积优化和 Tree-shaking

这种策略在开发效率和生产优化之间找到了最佳平衡点。

核心构建文件

1. 主构建脚本 (build.js)

是生产构建的核心脚本,负责:
javascript
// 构建参数解析
const {
  formats,
  all: buildAllMatching,
  devOnly,
  prodOnly,
  withTypes: buildTypes,
  sourceMap,
  release: isRelease,
  size: writeSize,
} = values

// 并行构建优化
async function runParallel(maxConcurrency, source, iteratorFn) {
  const ret = []
  const executing = []
  for (const item of source) {
    const p = Promise.resolve().then(() => iteratorFn(item, source))
    ret.push(p)
    if (source.length >= maxConcurrency) {
      executing.push(p.then(() => executing.splice(executing.indexOf(p), 1)))
    }
    if (executing.length >= maxConcurrency) {
      await Promise.race(executing)
    }
  }
  await Promise.all(ret)
  return ret
}

关键特性

  • 支持模糊匹配构建目标
  • 并行构建优化,充分利用多核 CPU
  • 自动生成包大小报告
  • 支持增量构建

2. 开发模式脚本 (dev.js)

使用 esbuild 提供快速的开发构建:
javascript
// esbuild 配置
esbuild
  .context({
    entryPoints: [resolve(__dirname, `${pkgBasePath}/src/index.ts`)],
    outfile,
    bundle: true,
    external,
    sourcemap: true,
    format: outputFormat,
    globalName: pkg.buildOptions?.name,
    platform: format === 'cjs' ? 'node' : 'browser',
    plugins,
    define: {
      __COMMIT__: `"dev"`,
      __VERSION__: `"${pkg.version}"`,
      __DEV__: prod ? `false` : `true`,
      __TEST__: `false`,
      __BROWSER__: String(
        format !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches,
      ),
      __GLOBAL__: String(format === 'global'),
      __ESM_BUNDLER__: String(format.includes('esm-bundler')),
      __ESM_BROWSER__: String(format.includes('esm-browser')),
      __CJS__: String(format === 'cjs'),
      __SSR__: String(format !== 'global'),
      __COMPAT__: String(target === 'vue-compat'),
      __FEATURE_SUSPENSE__: `true`,
      __FEATURE_OPTIONS_API__: `true`,
      __FEATURE_PROD_DEVTOOLS__: `false`,
      __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__: `true`,
    },
  })
  .then(ctx => ctx.watch())

关键特性

  • 使用 esbuild 的 watch 模式实现热重载
  • 丰富的编译时常量定义
  • 支持多种输出格式
  • 自动外部依赖处理

构建流程详解

从源码到 dist 的完整过程

1. 源码组织

packages/
├── vue/
│   ├── src/
│   │   ├── index.ts          # 完整构建入口
│   │   ├── runtime.ts        # 运行时入口
│   │   └── dev.ts           # 开发版本入口
│   └── dist/                # 构建输出
├── reactivity/
│   ├── src/
│   │   ├── index.ts
│   │   ├── reactive.ts
│   │   └── ...
│   └── dist/
└── ...

2. 构建流程

3. 多格式输出配置

中定义了多种输出格式:

javascript
/** @type {Record<PackageFormat, OutputOptions>} */
const outputConfigs = {
  'esm-bundler': {
    file: resolve(`dist/${name}.esm-bundler.js`),
    format: 'es',
  },
  'esm-browser': {
    file: resolve(`dist/${name}.esm-browser.js`),
    format: 'es',
  },
  cjs: {
    file: resolve(`dist/${name}.cjs.js`),
    format: 'cjs',
  },
  global: {
    file: resolve(`dist/${name}.global.js`),
    format: 'iife',
  },
  // runtime-only builds
  'esm-bundler-runtime': {
    file: resolve(`dist/${name}.runtime.esm-bundler.js`),
    format: 'es',
  },
  'esm-browser-runtime': {
    file: resolve(`dist/${name}.runtime.esm-browser.js`),
    format: 'es',
  },
  'global-runtime': {
    file: resolve(`dist/${name}.runtime.global.js`),
    format: 'iife',
  },
}

格式说明

格式用途特点
esm-bundler打包工具使用保留 ES 模块语法,外部依赖
esm-browser现代浏览器直接使用内联依赖,ES 模块语法
cjsNode.js 环境CommonJS 格式
global传统浏览器IIFE 格式,全局变量
runtime-only运行时版本不包含编译器

TypeScript 配置深度解析

开发配置 (tsconfig.json)

定义了开发时的 TypeScript 配置:
json
{
  "compilerOptions": {
    "baseUrl": ".",
    "target": "es2016",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "isolatedModules": true,
    "isolatedDeclarations": true,
    "paths": {
      "@vue/compat": ["packages/vue-compat/src"],
      "@vue/*": ["packages/*/src"],
      "vue": ["packages/vue/src"]
    }
  }
}

关键配置解析

  • isolatedModules: 确保每个文件可以独立编译
  • isolatedDeclarations: 支持独立的类型声明生成
  • paths: 模块别名配置,支持 Monorepo 内部引用
  • strict: 启用严格的类型检查

构建配置 (tsconfig.build.json)

专门用于类型声明文件生成:
json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "declaration": true,
    "emitDeclarationOnly": true,
    "stripInternal": true,
    "composite": false
  }
}

关键特性

  • emitDeclarationOnly: 只生成类型声明文件
  • stripInternal: 移除内部 API 的类型声明
  • 精确控制包含的源文件

开发模式与热重载

esbuild 开发构建

开发模式使用 esbuild 的优势:

  1. 极快的构建速度

    • Go 语言编写,原生性能
    • 并行处理和增量编译
    • 内置 TypeScript 支持
  2. Watch 模式

javascript
const plugins = [
  {
    name: 'log-rebuild',
    setup(build) {
      build.onEnd(() => {
        console.log(`built: ${relativeOutfile}`)
      })
    },
  },
]

esbuild.context(config).then(ctx => ctx.watch())
  1. 编译时常量
javascript
define: {
  __DEV__: prod ? `false` : `true`,
  __VERSION__: `"${pkg.version}"`,
  __BROWSER__: String(format !== 'cjs'),
  // ... 更多常量
}

模块别名系统

定义了统一的模块别名:
javascript
const entries = {
  vue: resolveEntryForPkg('vue'),
  'vue/compiler-sfc': resolveEntryForPkg('compiler-sfc'),
  'vue/server-renderer': resolveEntryForPkg('server-renderer'),
  '@vue/compat': resolveEntryForPkg('vue-compat'),
}

// 自动生成其他包的别名
for (const dir of dirs) {
  const key = `@vue/${dir}`
  if (dir !== 'vue' && !nonSrcPackages.includes(dir)) {
    entries[key] = resolveEntryForPkg(dir)
  }
}

这个别名系统在 Rollup、esbuild 和 Vitest 中共享使用。

调试技巧与最佳实践

SourceMap 配置

1. 开发模式 SourceMap

javascript
// dev.js 中的配置
esbuild.context({
  sourcemap: true,  // 生成内联 sourcemap
  // ...
})

2. 生产模式 SourceMap

javascript
// rollup.config.js 中的配置
function createConfig(format, output) {
  return {
    output: {
      sourcemap: !!process.env.SOURCE_MAP,
      // ...
    }
  }
}

断点调试设置

1. VS Code 调试配置

.vscode/launch.json 中配置:

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Vue 3 Source",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/scripts/dev.js",
      "args": ["vue", "-f", "global"],
      "console": "integratedTerminal",
      "sourceMaps": true,
      "outFiles": ["${workspaceFolder}/packages/*/dist/**/*.js"]
    }
  ]
}

2. 浏览器调试

html
<!DOCTYPE html>
<html>
<head>
  <title>Vue 3 Debug</title>
</head>
<body>
  <div id="app"></div>
  <!-- 使用开发版本,包含 sourcemap -->
  <script src="./packages/vue/dist/vue.global.js"></script>
  <script>
    const { createApp } = Vue
    
    createApp({
      template: `<h1>{{ message }}</h1>`,
      data() {
        return {
          message: 'Hello Vue 3!'
        }
      }
    }).mount('#app')
  </script>
</body>
</html>

测试环境配置

配置了完整的测试环境:
typescript
export default defineConfig({
  define: {
    __DEV__: true,
    __TEST__: true,
    __VERSION__: '"test"',
    // ... 测试环境常量
  },
  resolve: {
    alias: entries,  // 使用相同的别名配置
  },
  test: {
    globals: true,
    setupFiles: 'scripts/setup-vitest.ts',
    projects: [
      {
        name: 'unit',
        exclude: ['**/e2e/**', '**/{vue,vue-compat,runtime-dom}/**'],
      },
      {
        name: 'unit-jsdom',
        include: ['packages/{vue,vue-compat,runtime-dom}/**/*.{test,spec}.*'],
        environment: 'jsdom',
      },
      {
        name: 'e2e',
        environment: 'jsdom',
        include: ['packages/vue/__tests__/e2e/*.spec.ts'],
      },
    ],
  },
})

实践操作指南

本地构建 Vue 3 源码

1. 环境准备

bash
# 确保 Node.js 版本 >= 18.12.0
node --version

# 安装 pnpm
npm install -g pnpm

# 克隆源码
git clone https://github.com/vuejs/core.git
cd core

# 安装依赖
pnpm install

2. 开发构建

bash
# 构建所有包(开发模式)
pnpm dev

# 构建特定包
pnpm dev vue
pnpm dev reactivity

# 指定格式构建
pnpm dev vue -f esm-bundler
pnpm dev vue -f global

# 生产模式构建
pnpm dev vue -p

3. 生产构建

bash
# 构建所有包
pnpm build

# 构建特定包
pnpm build vue
pnpm build reactivity runtime-core

# 指定格式
pnpm build vue --formats esm-bundler,cjs

# 只构建生产版本
pnpm build vue -p

# 包含类型声明
pnpm build vue -t

# 生成大小报告
pnpm build vue --size

设置调试环境

1. 创建调试项目

bash
# 创建测试项目
mkdir vue3-debug
cd vue3-debug
npm init -y

2. 链接本地 Vue 3

bash
# 在 Vue 3 源码目录
cd /path/to/vue-core
pnpm build vue -f global

# 在测试项目中
cp /path/to/vue-core/packages/vue/dist/vue.global.js ./

3. 创建调试页面

html
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Vue 3 Debug</title>
  <style>
    body { font-family: Arial, sans-serif; padding: 20px; }
    .debug-info { background: #f5f5f5; padding: 10px; margin: 10px 0; }
  </style>
</head>
<body>
  <div id="app"></div>
  
  <script src="./vue.global.js"></script>
  <script>
    const { createApp, ref, reactive, computed } = Vue
    
    createApp({
      setup() {
        const count = ref(0)
        const state = reactive({ name: 'Vue 3' })
        
        const doubleCount = computed(() => count.value * 2)
        
        const increment = () => {
          count.value++
          console.log('Count updated:', count.value)
        }
        
        // 在这里设置断点进行调试
        debugger
        
        return {
          count,
          state,
          doubleCount,
          increment
        }
      },
      template: `
        <div>
          <h1>{{ state.name }} Debug</h1>
          <div class="debug-info">
            <p>Count: {{ count }}</p>
            <p>Double Count: {{ doubleCount }}</p>
            <button @click="increment">Increment</button>
          </div>
        </div>
      `
    }).mount('#app')
  </script>
</body>
</html>

创建测试用例

1. 单元测试示例

typescript
// test/reactivity.spec.ts
import { describe, it, expect } from 'vitest'
import { ref, reactive, computed } from '@vue/reactivity'

describe('Reactivity System', () => {
  it('should create reactive ref', () => {
    const count = ref(0)
    expect(count.value).toBe(0)
    
    count.value++
    expect(count.value).toBe(1)
  })
  
  it('should create reactive object', () => {
    const state = reactive({ count: 0 })
    expect(state.count).toBe(0)
    
    state.count++
    expect(state.count).toBe(1)
  })
  
  it('should create computed value', () => {
    const count = ref(0)
    const doubleCount = computed(() => count.value * 2)
    
    expect(doubleCount.value).toBe(0)
    
    count.value = 5
    expect(doubleCount.value).toBe(10)
  })
})

2. 组件测试示例

typescript
// test/component.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { defineComponent, ref } from 'vue'

const Counter = defineComponent({
  setup() {
    const count = ref(0)
    const increment = () => count.value++
    
    return { count, increment }
  },
  template: `
    <div>
      <span data-testid="count">{{ count }}</span>
      <button @click="increment" data-testid="increment">+</button>
    </div>
  `
})

describe('Counter Component', () => {
  it('should render initial count', () => {
    const wrapper = mount(Counter)
    expect(wrapper.find('[data-testid="count"]').text()).toBe('0')
  })
  
  it('should increment count when button clicked', async () => {
    const wrapper = mount(Counter)
    
    await wrapper.find('[data-testid="increment"]').trigger('click')
    expect(wrapper.find('[data-testid="count"]').text()).toBe('1')
  })
})

3. 运行测试

bash
# 运行所有测试
pnpm test

# 运行单元测试
pnpm test-unit

# 运行特定测试文件
pnpm test reactivity

# 运行测试并生成覆盖率报告
pnpm test-coverage

# 监听模式
pnpm test --watch

性能优化与最佳实践

构建性能优化

1. 并行构建

javascript
// 利用多核 CPU 进行并行构建
const maxConcurrency = cpus().length

async function buildAll(targets) {
  await runParallel(maxConcurrency, targets, build)
}

2. 增量构建

bash
# 只构建变更的包
pnpm build --changed

# 基于 git diff 的增量构建
git diff --name-only HEAD~1 | grep "packages/" | cut -d'/' -f2 | sort -u

3. 缓存优化

javascript
// esbuild 缓存配置
esbuild.context({
  // 启用增量构建
  incremental: true,
  // 缓存目录
  metafile: true,
})

调试性能优化

1. 选择性构建

bash
# 只构建需要调试的包
pnpm dev reactivity  # 只构建响应式系统
pnpm dev runtime-core  # 只构建运行时核心

2. 格式选择

bash
# 开发时使用 global 格式,便于浏览器调试
pnpm dev vue -f global

# 测试时使用 esm-bundler 格式
pnpm dev vue -f esm-bundler

总结

Vue 3 的构建与调试系统体现了现代前端工程化的最佳实践:

  1. 双构建策略:开发时追求速度,生产时追求优化
  2. 模块化设计:清晰的包结构和依赖关系
  3. 类型安全:完整的 TypeScript 支持和类型声明
  4. 调试友好:丰富的 SourceMap 和调试工具支持
  5. 测试完善:多层次的测试策略和工具

通过深入理解这些构建和调试机制,开发者不仅能更好地使用 Vue 3,还能将这些最佳实践应用到自己的项目中。在后续章节中,我们将基于这些基础设施,深入探讨 Vue 3 各个核心模块的实现原理。


微信公众号二维码