Vue 3 CDN 引入完全指南:从全局构建到 ES Module 的实战选择
Vue 3 CDN 引入完全指南:从全局构建到 ES Module 的实战选择
「vue3 cdn」这个关键词背后其实藏着两种完全不同的需求。第一种是「我想最快地在一个 HTML 页面里跑起来 Vue 3,不想装 Node、不想配构建工具」;第二种是「我的项目已经在用工程化了,但希望把 Vue 的运行时交给公共 CDN 托管,减轻自己服务器的带宽和缓存压力」。这两件事看起来都是往页面里塞一个 script 标签,但技术路径、踩坑点、性能表现差别很大。
下面把 vue3 cdn 这件事从文件类型、代码写法、组件组织、路由状态管理、生产环境配置到常见报错,完整地梳理一遍。看完之后,你应该能判断自己的场景到底该用哪种方式。
先理清:CDN 上的 Vue 3 到底有几个文件
打开 unpkg 或 jsDelivr 上的 vue 包,你会看到一堆名字很像的 JS 文件。它们不是随便打包出来的,而是面向不同运行环境的构建产物。选错文件是 vue3 cdn 用法中最常见的翻车原因。
全局构建:vue.global.js / vue.global.prod.js
这是 IIFE 格式的构建,加载完毕后会把一个名为 Vue 的对象挂到 window 上,同时内置了模板编译器。也就是说,你可以直接在 HTML 里写 {{ }} 插值和 v-if、v-for,Vue 会在运行时把 DOM 里的模板编译成渲染函数。对只想快速验证想法、或者给传统后端渲染的页面加一点交互的场景来说,这是最省事的入口。带 .prod 后缀的是生产构建,去掉了开发期警告和调试信息,体积更小。
ESM 构建:vue.esm-browser.js / vue.esm-browser.prod.js
这是面向浏览器原生 <script type="module"> 的 ES Module 构建,同样包含模板编译器,但它不挂全局变量,而是通过 import 引入具体的 API。它适合希望保持模块化写法、又不想引入构建工具的现代项目。
运行时构建:vue.runtime.esm-browser.js
名字里带 runtime 的版本不包含模板编译器。这意味着你不能再写字符串模板或 DOM 模板,只能用渲染函数或已经编译好的组件。它体积更小,但直接在浏览器里使用时灵活性差很多。
打包器构建:vue.esm-bundler.js
这个文件是给 Vite、webpack、Rollup 这类工具使用的,里面依赖 process.env.NODE_ENV 之类的打包期特性。如果你把它直接写进 <script type="module"> 的 import 路径,浏览器会直接报错。这是「我明明引入的是 ESM 为什么跑不起来」的典型原因。
记住一条判断标准:文件名里带 global 或 esm-browser 的才能在浏览器里直接加载;带 esm-bundler 的必须经过构建工具处理。
最省事的写法:用全局构建跑一个页面
新建一个 HTML 文件,两行 script 就能让页面动起来。这是 vue3 cdn 最经典的使用姿势,也是很多教学示例采用的方式。
<div id="app">
<p>{{ message }}</p>
<button @click="count++">点击了 {{ count }} 次</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const message = ref('Hello Vue 3')
const count = ref(0)
return { message, count }
}
}).mount('#app')
</script>
这里有几个细节值得留意。第一,createApp 的作用域是挂载点内部,页面上如果还有别的框架或者老版本的 Vue 实例,两者不会互相干扰,这一点比 Vue 2 的全局挂载要干净得多。第二,ref 返回的是响应式引用,在模板里会自动解包,所以在标签里写 count 而不是 count.value。第三,路径里的 vue@3 是一个版本范围,每次 CDN 发新版你都可能拿到不同的代码,生产环境建议锁定到具体的次版本号。
如果只是给服务端渲染出来的页面加一点交互,可以不用 createApp 的完整组件写法,直接给根组件传一个简单的对象即可,模板从挂载点的 innerHTML 读取。这种方式对后端模板引擎比较友好,也不会和 Jinja、Thymeleaf、Blade 之类的语法冲突——前提是你把 Vue 的插值定界符改掉,否则两边的 {{ }} 会互相覆盖。
更现代的写法:ES Module 与 import map
如果你希望代码看起来更接近平时写的 Vue 项目,可以在 type="module" 的脚本里直接 import。问题在于,Vue 3 的 ESM 构建内部会引用裸模块路径,浏览器不认识这种写法,所以需要 importmap 来做一次路径映射。
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js"
}
}
</script>
<script type="module">
import { createApp, ref, computed } from 'vue'
createApp({
setup() {
const price = ref(99)
const qty = ref(2)
const total = computed(() => price.value * qty.value)
return { price, qty, total }
},
template: `<div>合计:{{ total }} 元</div>`
}).mount('#app')
</script>
importmap 的写法让后续所有模块都能统一用 'vue' 这个标识符,切换 CDN 或升级版本时只改一处。需要注意的是,importmap 必须出现在任何 module 脚本之前,而且一个文档里通常只应该有一个。较老的浏览器对这个特性支持有限,如果目标用户环境比较复杂,全局构建反而更稳。
另一种思路是不写 importmap,直接使用像 esm.sh 这样的转换型 CDN,它会自动处理好裸模块路径和依赖递归,你只需要 import { createApp } from 'https://esm.sh/vue@3' 就行。代价是多了一层重定向,首次加载的往返次数会多一些。
组件化:在 CDN 场景下怎么组织代码
没有构建工具,就没有单文件组件。但这不代表不能拆组件。最常见的做法是把每个组件写成一个普通的 JS 对象,用 app.component() 全局注册,或者在某一个组件的 components 选项里局部注册。
模板有两种来源:一种是写成字符串,缺点是编辑器里没有语法高亮,反引号里的 HTML 也容易漏掉转义;另一种是在页面里用 <template id="xxx"> 标签写模板,运行时通过 template: '#xxx' 引用。后者在写法上更接近原生 HTML,浏览器也不会把未知标签渲染出来,是纯 CDN 场景里比较舒服的一种折中。
如果你确实很想在浏览器里直接写 .vue 文件,社区里有 vue3-sfc-loader 这类方案,它在运行时把 SFC 编译成组件对象。但这条路会带来明显的首屏延迟,只适合演示、文档或者内部工具,不建议用在面向用户的正式页面上。
Vue Router 与 Pinia 如何一起走 CDN
多页面跳转要用路由,共享状态要用状态管理。这两个库在 CDN 上同样有对应的构建产物,关键是版本要和 Vue 3 对齐:Vue Router 用 4.x,Pinia 用 2.x。混用 Vue 2 时代的 3.x 版本会出现 createRouter is not a function 之类的错误。
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js",
"vue-router": "https://unpkg.com/vue-router@4/dist/vue-router.esm-browser.js",
"pinia": "https://unpkg.com/pinia@2/dist/pinia.esm-browser.js"
}
}
</script>
<script type="module">
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import { createPinia } from 'pinia'
const router = createRouter({
history: createWebHashHistory(),
routes: [{ path: '/', component: { template: '<div>首页</div>' } }]
})
createApp({ template: '<router-view />' })
.use(router)
.use(createPinia())
.mount('#app')
</script>
这里用哈希模式是有意为之:页面通常是从静态服务器或某个后端路由直接返回的 HTML,没有配置 history fallback,用 createWebHistory 一旦刷新子路由就会 404。要改成 history 模式,得先确认服务器把所有路径都回退到同一个入口文件。
生产环境需要处理的几件事
锁定版本,别用浮动标签
vue@3 指向的是最新的大版本,看起来方便,但意味着一次 CDN 发版就可能改变你线上的运行结果。稳妥的做法是写成 vue@3.x.x 这样的精确版本,升级时主动修改并回归测试。
生产用 prod 构建
开发构建里包含了大量警告和校验代码,体积可能翻倍,还会在控制台输出提示。正式环境一定切换到 .prod 文件,或者使用带 NODE_ENV 判断的默认入口。
加上完整性校验与备用源
给 script 标签加 integrity 和 crossorigin 属性,可以防止 CDN 被篡改后下发恶意代码。同时准备一个备用地址,在主源超时或失败时切换,公共 CDN 偶发的区域性故障并不罕见。
缓存与协议
带上版本的静态文件名天然适合长缓存,配合 Cache-Control: immutable 能让老用户几乎不再重新下载。全站启用 HTTPS,避免混合内容被浏览器拦截;HTTP/2 或 HTTP/3 下多文件并行加载的劣势会小很多,但依然建议减少请求数量。
国内访问公共 CDN 时,节点分布和解析质量差异比较明显。可以对比 cdnjs、unpkg、jsDelivr 以及国内的一些静态资源服务的实际响应时间,再决定主用哪个。如果访问群体集中在某个区域,自建一个简单的静态资源服务往往是更可控的选择。
什么时候用 CDN,什么时候该上构建工具
适合 vue3 cdn 的场景其实很明确:单页小工具、活动落地页、给已有后端渲染页面增加交互、需要嵌入第三方系统而不想引入整套前端工程、教学演示和在线代码片段。这些场景的共同点是规模小、依赖少、部署方式简单,一个 HTML 文件就是全部产物。
反过来,当项目需要多人协作、需要 TypeScript 类型检查、需要按路由拆包、需要组件测试和热更新时,构建工具带来的收益远远超过它增加的成本。此时 CDN 的正确用法是只作为生产环境的资源分发层——也就是构建产物仍然由构建工具生成,只是把第三方依赖或者全部静态资源部署到 CDN 上。这两件事并不矛盾,很多团队就是这么做的。
还有一个中间态值得提一下:用 ESM 构建加 importmap 做开发,用打包工具做发布。这样本地不用起 Node 服务就能调页面,上线时又有完整的优化流程。代价是两种环境的行为需要保持一致,否则容易出现「本地好好的,上线就报错」。
常见报错与排查思路
提示 runtime compilation is not supported。说明你引入了 runtime 版本,却在组件里写了 template 字符串或者用了 DOM 模板。换成带编译器的 vue.global.js 或 vue.esm-browser.js 即可,或者改用渲染函数。
在 module 里写 Vue.xxx 报未定义。ESM 构建不会挂全局变量,必须用 import 引入所需 API。混用两种写法是新手最容易犯的错。
页面先闪一下花括号再渲染。模板在脚本执行前就已经在 DOM 里可见了,加上 v-cloak 属性并配一段 [v-cloak]{display:none} 的样式就能解决。
路由跳转正常,刷新就 404。服务器没有配置 fallback,改用哈希模式,或者让后端把所有未匹配路径都回退到入口 HTML。
控制台出现多个 Vue 实例的警告。通常是页面里同时加载了两份不同来源的 Vue,检查是否有某个库自带了一份 Vue,或者 importmap 与实际 import 的地址不一致。
小结
用 vue3 cdn 这件事本身没有难度,难的是在正确的场景下选对文件名、选对加载方式。全局构建适合快速起步和渐进增强,ESM 加 importmap 适合想要模块化又不想上构建工具的团队,而生产环境无论走哪条路,都别忘了锁版本、用生产构建、加校验和准备备用源。把这些细节处理好之后,一个 HTML 文件也能撑起一个体验相当不错的 Vue 3 应用。





