10-Vue 运行时与构建版本
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-loader 或 vueify 预编译为渲染函数。这是推荐的生产环境方案。
// 运行时+编译器:可以编译模板字符串
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 的模块标准,使用 require 和 module.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 的核心可以划分为两个主要部分:
- 编译器(Compiler):将模板字符串编译为渲染函数(render function)
- 运行时(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 编译时与运行时分层架构
四、代码示例
示例 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 版本为什么默认不包含编译器?
这是出于体积和架构清晰性的考虑:
- 体积优化:大多数项目不需要运行时编译,默认排除编译器可以减少 bundle 体积
- 架构分层:编译器和运行时是独立的模块,可以按需引入
- 工程化推荐:现代项目应该使用构建工具预编译模板
如果确实需要运行时编译(如动态模板字符串),可以通过配置 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 的构建版本体系:
- Vue2 提供了 UMD/CJS/ESM 三种模块格式的运行时+编译器和仅运行时版本,通过文件名后缀区分功能
- Vue3 的构建体系更加现代化,分为 Global、ESM-Browser、ESM-Bundler 和 CJS 四大类,默认推荐
esm-bundler版本配合构建工具使用 - 运行时+编译器 包含模板编译功能,适用于 CDN 引入和动态模板场景;仅运行时 体积更小,是工程化项目的推荐方案
- ESM 格式 相比 UMD/CJS 具有静态结构优势,天然支持 Tree-shaking
- Vue3 将核心功能拆分为独立包(
@vue/reactivity、@vue/runtime-core等),实现了更细粒度的 Tree-shaking
在实际项目中,应该根据使用场景(CDN/构建工具/SSR)和是否需要运行时编译来选择合适的构建版本。对于现代前端工程化项目,推荐使用 Vue3 的 esm-bundler 版本,配合 Vite/Webpack 等构建工具,享受最佳的开发体验和运行时性能。
七、思考题
-
如果你正在维护一个通过 CDN 引入 Vue2 的老项目,迁移到 Vue3 时应该如何选择构建版本?
-
设计一个需要动态渲染用户输入模板的场景,分析运行时编译器的必要性和安全风险(XSS)。
-
对比 Vue2 和 Vue3 的 Tree-shaking 机制,解释为什么 Vue3 的响应式 API 可以只打包 3KB?
-
在 SSR 项目中,服务端和客户端是否应该使用相同的 Vue 构建版本?为什么?
更多推荐



所有评论(0)