Vue 运行时与构建版本

深入理解 Vue2/Vue3 不同构建版本的差异,掌握运行时+编译器与仅运行时的区别,学会根据项目场景选择正确的构建版本。

一、前言

在日常开发中,我们通过 npm install vue 或 CDN 引入 Vue 时,很少关注底层使用了哪个构建版本。然而,Vue 官方提供了多种构建产物(UMD、CJS、ESM、Global 等),每种产物在体积、功能、使用场景上都有显著差异。理解这些构建版本的设计原理,不仅能帮助我们优化项目打包体积,还能深入理解 Vue 的编译时与运行时分层架构。

本章将从 Vue2 和 Vue3 的构建版本入手,系统分析不同模块格式、运行时+编译器与仅运行时的区别,以及 Tree-shaking 友好构建的设计思路。

二、核心内容

2.1 Vue2 构建版本概览

Vue 2.x 在 dist/ 目录下提供了以下主要构建文件:

构建文件 模块格式 运行时+编译器 适用场景
vue.js UMD 开发环境 CDN 引入
vue.min.js UMD 生产环境 CDN 引入
vue.runtime.js UMD 开发环境 + 构建工具
vue.runtime.min.js UMD 生产环境 + 构建工具
vue.common.js CJS 旧版构建工具(Webpack 1)
vue.common.dev.js CJS 开发环境 CJS
vue.common.prod.js CJS 生产环境 CJS
vue.esm.js ESM 现代构建工具(Webpack 2+)
vue.esm.browser.js ESM 现代浏览器原生 ESM

Vue2 的构建版本命名遵循一个清晰的规则:

vue.[runtime.][compiler.][module-format.][production/development.]js

运行时+编译器(Runtime + Compiler):包含完整的模板编译器,可以在客户端将模板字符串编译为渲染函数。适用于以下场景:

  • 通过 CDN 直接引入 Vue,使用 template 选项
  • 使用 Vue.compile() 进行即时编译

仅运行时(Runtime Only):不包含模板编译器,体积比完整版小约 30%。所有模板必须在构建阶段通过 vue-loadervueify 预编译为渲染函数。这是推荐的生产环境方案。

// 运行时+编译器:可以编译模板字符串
new Vue({
  template: '<div>{{ msg }}</div>'  // 客户端编译
})

// 仅运行时:必须使用预编译的渲染函数
new Vue({
  render: h => h(App)  // 构建时已编译
})

2.2 Vue3 构建版本体系

Vue 3.x 的构建体系更加现代化,主要分为以下几类:

构建文件 模块格式 用途
vue.global.js Global(IIFE) CDN 引入,直接暴露 Vue 全局变量
vue.global.prod.js Global(IIFE) 生产环境 CDN
vue.esm-browser.js ESM 现代浏览器原生 <script type="module">
vue.esm-browser.prod.js ESM 生产环境浏览器 ESM
vue.esm-bundler.js ESM 配合 Webpack/Vite/Rollup 等构建工具
vue.cjs.js CJS Node.js 服务端渲染(SSR)
vue.cjs.prod.js CJS 生产环境 SSR

Vue3 的构建版本命名更加直观:

vue.[global/esm-bundler/esm-browser/cjs].[prod.]js

与 Vue2 不同的是,Vue3 的 esm-bundler 版本默认不包含运行时编译器。如果需要运行时编译功能,需要显式配置构建工具:

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      // 使用带编译器的 Vue3 构建版本
      'vue': 'vue/dist/vue.esm-bundler.js'
    }
  }
})

2.3 模块格式详解:UMD / CJS / ESM

UMD(Universal Module Definition)

UMD 是一种兼容多种模块系统的格式,可以同时支持 AMD、CJS 和全局变量方式:

// UMD 包装结构示意
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined'
    ? module.exports = factory()           // CJS
    : typeof define === 'function' && define.amd
      ? define(factory)                     // AMD
      : (global = global || self, global.Vue = factory())  // Global
}(this, function () {
  // Vue 源码...
}))

特点

  • 兼容性最好,可在任何环境中使用
  • 无法进行 Tree-shaking,会导入整个库
  • 体积较大,不适合现代构建工具
CJS(CommonJS)

CJS 是 Node.js 的模块标准,使用 requiremodule.exports

// CJS 使用方式
const Vue = require('vue')
const { createApp } = require('vue')

// Vue3 CJS 构建内部结构
const Vue = require('./vue.cjs.js')
console.log(Vue.createApp)  // 创建应用实例
console.log(Vue.ref)        // 响应式 API

特点

  • 主要用于 Node.js / SSR 场景
  • 同步加载,运行时确定依赖
  • 现代构建工具可以对 CJS 做一定程度的 Tree-shaking,但效果不如 ESM
ESM(ECMAScript Modules)

ESM 是 ES6 标准的模块系统,使用 import / export

// ESM 使用方式
import { createApp, ref, computed } from 'vue'

// 支持命名导出,便于 Tree-shaking
import * as Vue from 'vue'

特点

  • 静态结构,编译时即可确定依赖关系
  • 天然支持 Tree-shaking
  • 现代浏览器和构建工具原生支持
  • Vue3 的 esm-bundler 版本专为构建工具优化

2.4 运行时+编译器 vs 仅运行时

Vue 的核心可以划分为两个主要部分:

  1. 编译器(Compiler):将模板字符串编译为渲染函数(render function)
  2. 运行时(Runtime):创建虚拟 DOM、响应式系统、组件生命周期管理等
模板字符串  →  [编译器]  →  渲染函数  →  [运行时]  →  虚拟 DOM  →  真实 DOM
编译器的工作流程
// 编译器将模板转换为渲染函数
const template = `<div class="app">
  <h1>{{ title }}</h1>
  <p v-if="show">{{ content }}</p>
</div>`

// 编译后的渲染函数(简化示意)
function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", { class: "app" }, [
    _createElementVNode("h1", null, _toDisplayString(_ctx.title), 1 /* TEXT */),
    (_ctx.show)
      ? (_openBlock(), _createElementBlock("p", { key: 0 },
          _toDisplayString(_ctx.content), 1 /* TEXT */))
      : _createCommentVNode("v-if", true)
  ]))
}

Vue3 的编译器还会进行大量的编译时优化:

  • 静态提升(Static Hoisting):将静态节点提升到渲染函数外部,避免重复创建
  • PatchFlag 标记:在虚拟 DOM 节点上标记动态部分,减少 diff 时的比较范围
  • Block Tree:收集动态子节点,实现靶向更新
体积对比
版本 Vue2 体积(gzip) Vue3 体积(gzip)
运行时+编译器 ~33KB ~40KB
仅运行时 ~23KB ~22KB
编译器单独 ~10KB ~18KB

Vue3 的编译器体积更大,因为引入了更多的编译时优化逻辑。但这些优化在运行时带来了显著的性能提升。

2.5 Vue3 各构建版本适用场景

Global 构建(vue.global.js
<!-- 开发环境 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<!-- 生产环境 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<script>
  const { createApp, ref } = Vue
  createApp({
    setup() {
      const count = ref(0)
      return { count }
    }
  }).mount('#app')
</script>

适用场景

  • 快速原型开发
  • 简单的 HTML 页面增强
  • 不依赖构建工具的传统项目
  • 通过 data-* 属性进行渐进式增强
ESM Browser 构建(vue.esm-browser.js
<script type="module">
  import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'

  createApp({
    setup() {
      const count = ref(0)
      return { count }
    }
  }).mount('#app')
</script>

适用场景

  • 现代浏览器原生 ESM 支持
  • 配合 Import Maps 使用
  • 无需构建工具的开发环境
<!-- 使用 Import Maps -->
<script type="importmap">
{
  "imports": {
    "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
  }
}
</script>
ESM Bundler 构建(vue.esm-bundler.js

这是现代前端工程化项目中最常用的构建版本:

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()]
  // 默认解析 'vue' 为 vue.esm-bundler.js
})
// main.js
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

特点

  • 配合 vue-loader@vitejs/plugin-vue 使用
  • .vue 单文件组件在构建时预编译
  • 默认不包含运行时编译器
  • 支持完整的 Tree-shaking
CJS 构建(vue.cjs.js
// server.js(Node.js SSR)
const { createSSRApp } = require('vue')
const { renderToString } = require('@vue/server-renderer')

const app = createSSRApp({
  data: () => ({ msg: 'Hello SSR' }),
  template: `<div>{{ msg }}</div>`
})

renderToString(app).then(html => {
  console.log(html)
})

适用场景

  • Node.js 服务端渲染(SSR)
  • 旧版构建工具或测试环境

2.6 Tree-shaking 友好构建

Vue3 从设计之初就充分考虑了 Tree-shaking 支持。与 Vue2 的命名空间导出不同,Vue3 采用命名导出:

// Vue2:命名空间导出,无法 Tree-shaking
import Vue from 'vue'
Vue.use(Vuex)
Vue.use(VueRouter)

// Vue3:命名导出,支持 Tree-shaking
import { createApp, ref, computed, watch, onMounted } from 'vue'
// 未使用的 API 不会被打包

Vue3 源码内部也大量使用 /*#__PURE__*/ 注释标记纯函数,帮助构建工具识别可删除的代码:

// Vue3 源码中的 PURE 标记
export const createApp = /*#__PURE__*/ (() => {
  // 实现...
})()

export const ref = /*#__PURE__*/ (value) => {
  // 实现...
}

实际 Tree-shaking 效果对比:

使用场景 Vue2 打包体积 Vue3 打包体积
仅使用响应式 API ~23KB ~3KB(@vue/reactivity)
完整框架 ~23KB ~22KB
完整框架 + 所有特性 ~33KB ~40KB

Vue3 将核心功能拆分为独立的包:

  • @vue/reactivity:响应式系统(可独立使用)
  • @vue/runtime-core:运行时核心(与平台无关)
  • @vue/runtime-dom:浏览器平台运行时
  • @vue/compiler-core:编译器核心
  • @vue/compiler-dom:浏览器平台编译器
// 可以只使用响应式系统,不依赖 Vue 框架
import { ref, computed, watch, effect } from '@vue/reactivity'

const count = ref(0)
const double = computed(() => count.value * 2)

watch(count, (newVal) => {
  console.log('count changed:', newVal)
})

三、Mermaid 图表

Vue 构建版本选择决策树

选择 Vue 构建版本

使用构建工具?

需要运行时编译?

现代浏览器?

vue.esm-bundler.js
配置 alias

vue.esm-bundler.js
默认推荐

vue.esm-browser.js
原生 ESM

vue.global.js
传统 CDN

配合 vue-loader
模板字符串编译

.vue SFC 预编译
Tree-shaking 优化

Import Maps 支持
模块化加载

全局 Vue 变量
UMD 兼容

Vue 编译时与运行时分层架构

运行时 Runtime

编译时 Compile Time

模板 Template

解析器 Parser

AST 抽象语法树

转换器 Transformer

优化: 静态提升
PatchFlag 标记
Block Tree

代码生成器 Code Generator

渲染函数 Render Function

响应式系统 Reactivity

虚拟 DOM VDOM

Diff & Patch

真实 DOM

四、代码示例

示例 1:检测当前使用的 Vue 构建版本

// version-check.js
import * as Vue from 'vue'

// 检查 Vue 版本
console.log('Vue version:', Vue.version)

// 检查是否包含编译器
function hasCompiler() {
  try {
    // Vue3 的 compile 函数
    if (Vue.compile) {
      const result = Vue.compile('<div>test</div>')
      return typeof result === 'function' || !!result.render
    }
    // Vue2 的 compile 函数
    if (typeof window !== 'undefined' && window.Vue) {
      return !!window.Vue.compile
    }
    return false
  } catch (e) {
    return false
  }
}

console.log('Has compiler:', hasCompiler())

// 检查模块格式
function detectModuleFormat() {
  if (typeof window !== 'undefined' && window.Vue) {
    return 'Global / UMD'
  }
  if (typeof module !== 'undefined' && module.exports) {
    return 'CommonJS'
  }
  if (typeof importScripts === 'function') {
    return 'ES Modules'
  }
  return 'Unknown'
}

console.log('Module format:', detectModuleFormat())

示例 2:Vue3 运行时编译器配置

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      // 方式1:使用完整版(含编译器)
      'vue': 'vue/dist/vue.esm-bundler.js',

      // 方式2:使用仅运行时(推荐,体积更小)
      // 'vue': 'vue/dist/vue.runtime.esm-bundler.js'
    }
  },
  define: {
    // 开启运行时编译器(vue.esm-bundler.js 需要配合此配置)
    __VUE_OPTIONS_API__: true,
    __VUE_PROD_DEVTOOLS__: false
  }
})
// main.js - 使用运行时编译
import { createApp } from 'vue'

// 使用模板字符串(需要运行时编译器)
const app = createApp({
  template: `
    <div class="runtime-compile-demo">
      <h1>{{ title }}</h1>
      <button @click="increment">Count: {{ count }}</button>
    </div>
  `,
  data() {
    return {
      title: 'Runtime Compiler Demo',
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
})

app.mount('#app')

示例 3:手动调用编译器 API

// compiler-demo.js
import { compile, compileToFunction } from 'vue'

// 编译模板为渲染函数
const template = `
  <div class="user-card">
    <h2>{{ user.name }}</h2>
    <p v-if="user.bio">{{ user.bio }}</p>
    <span v-for="tag in user.tags" :key="tag">{{ tag }}</span>
  </div>
`

// compile 返回包含 render 和 staticRenderFns 的对象
const { render, staticRenderFns, errors, tips } = compile(template, {
  // 编译选项
  comments: false,
  whitespace: 'condense'
})

console.log('Compiled render function:')
console.log(render.toString())

if (errors.length) {
  console.error('Compile errors:', errors)
}

// compileToFunction 直接返回可执行的渲染函数
const renderFn = compileToFunction(template)
console.log('Render function:', renderFn)

示例 4:Tree-shaking 效果验证

// tree-shaking-demo.js
// 以下未使用的导入不会被打包进最终产物

import {
  createApp,      // 使用
  ref,            // 使用
  computed,       // 使用
  watch,          // 未使用 - 会被 Tree-shaking 移除
  watchEffect,    // 未使用 - 会被 Tree-shaking 移除
  onMounted,      // 使用
  onUnmounted,    // 未使用 - 会被 Tree-shaking 移除
  provide,        // 未使用 - 会被 Tree-shaking 移除
  inject,         // 未使用 - 会被 Tree-shaking 移除
  h,              // 使用
  Fragment,       // 未使用 - 会被 Tree-shaking 移除
  Teleport,       // 未使用 - 会被 Tree-shaking 移除
  Suspense,       // 未使用 - 会被 Tree-shaking 移除
  KeepAlive,      // 未使用 - 会被 Tree-shaking 移除
  Transition,     // 未使用 - 会被 Tree-shaking 移除
  TransitionGroup // 未使用 - 会被 Tree-shaking 移除
} from 'vue'

const app = createApp({
  setup() {
    const count = ref(0)
    const doubled = computed(() => count.value * 2)

    onMounted(() => {
      console.log('Component mounted')
    })

    return () => h('div', `${count.value} * 2 = ${doubled.value}`)
  }
})

app.mount('#app')

五、常见问题

Q1:为什么推荐使用仅运行时版本?

仅运行时版本不包含模板编译器,体积更小(约减少 30%)。在现代工程化项目中,.vue 单文件组件会在构建阶段通过 vue-loader@vitejs/plugin-vue 预编译为渲染函数,不需要客户端编译。将编译工作前置到构建阶段,还能享受编译时优化(静态提升、PatchFlag 等)带来的运行时性能提升。

Q2:Vue3 的 esm-bundler 版本为什么默认不包含编译器?

这是出于体积和架构清晰性的考虑:

  1. 体积优化:大多数项目不需要运行时编译,默认排除编译器可以减少 bundle 体积
  2. 架构分层:编译器和运行时是独立的模块,可以按需引入
  3. 工程化推荐:现代项目应该使用构建工具预编译模板

如果确实需要运行时编译(如动态模板字符串),可以通过配置 alias 显式引入带编译器的版本。

Q3:UMD 和 ESM 格式的主要区别是什么?

特性 UMD ESM
加载时机 运行时 编译时
Tree-shaking 不支持 支持
浏览器原生支持 是(通过 script 标签) 是(通过 script type=“module”)
静态分析 困难 容易
循环依赖处理 运行时处理 编译时检测
现代构建工具优化 有限 完整支持

Q4:如何在 CDN 项目中使用 Vue3 的 Composition API?

<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <div id="app">
    <p>Count: {{ count }}</p>
    <button @click="increment">+1</button>
  </div>

  <script>
    const { createApp, ref } = Vue

    createApp({
      setup() {
        const count = ref(0)
        const increment = () => count.value++
        return { count, increment }
      }
    }).mount('#app')
  </script>
</body>
</html>

Q5:Vue3 的 @vue/reactivity 包可以独立使用吗?

可以。@vue/reactivity 是 Vue3 响应式系统的独立包,不依赖 Vue 运行时:

import { ref, reactive, computed, watch, effect } from '@vue/reactivity'

const state = reactive({ count: 0 })
const double = computed(() => state.count * 2)

watch(() => state.count, (newVal) => {
  console.log('count changed to:', newVal)
})

effect(() => {
  console.log('double is:', double.value)
})

state.count++  // 触发 effect 和 watch

这在非 Vue 项目(如 Node.js 脚本、Vanilla JS 项目)中非常有用。

六、总结

本章系统梳理了 Vue2/Vue3 的构建版本体系:

  1. Vue2 提供了 UMD/CJS/ESM 三种模块格式的运行时+编译器和仅运行时版本,通过文件名后缀区分功能
  2. Vue3 的构建体系更加现代化,分为 Global、ESM-Browser、ESM-Bundler 和 CJS 四大类,默认推荐 esm-bundler 版本配合构建工具使用
  3. 运行时+编译器 包含模板编译功能,适用于 CDN 引入和动态模板场景;仅运行时 体积更小,是工程化项目的推荐方案
  4. ESM 格式 相比 UMD/CJS 具有静态结构优势,天然支持 Tree-shaking
  5. Vue3 将核心功能拆分为独立包(@vue/reactivity@vue/runtime-core 等),实现了更细粒度的 Tree-shaking

在实际项目中,应该根据使用场景(CDN/构建工具/SSR)和是否需要运行时编译来选择合适的构建版本。对于现代前端工程化项目,推荐使用 Vue3 的 esm-bundler 版本,配合 Vite/Webpack 等构建工具,享受最佳的开发体验和运行时性能。

七、思考题

  1. 如果你正在维护一个通过 CDN 引入 Vue2 的老项目,迁移到 Vue3 时应该如何选择构建版本?

  2. 设计一个需要动态渲染用户输入模板的场景,分析运行时编译器的必要性和安全风险(XSS)。

  3. 对比 Vue2 和 Vue3 的 Tree-shaking 机制,解释为什么 Vue3 的响应式 API 可以只打包 3KB?

  4. 在 SSR 项目中,服务端和客户端是否应该使用相同的 Vue 构建版本?为什么?

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐