如果你已经用 Vue 写过一段时间的业务代码,第一次打开微信小程序开发者工具时,大概率会有一种熟悉又陌生的割裂感:这边刚习惯单文件组件、热更新和 Composition API,那边就要面对 WXML、手动 setData、照着 JSON 注册组件,开发体验仿佛退回很多年前。Weapp-vite 和 Wevu 这两套东西,就是想改变这个局面。Weapp-vite 负责把 Vite 的构建能力带进小程序工程,Wevu 则负责在缺少 DOM 的运行时环境里还原 Vue 的开发模型。这篇文章我会把编译链路的每个关键环节、Wevu 运行时的核心机制、以及实际落地时踩过的坑逐一拆开讲。适合已经会用 Vue、正准备深入小程序底层原理,或者对"前端框架如何跨端运行"感兴趣的开发者。
1. 为什么值得重走一遍:Weapp-vite 与 Wevu 的定位
1.1 小程序开发的"基础设施缺失"问题
先回到最根本的问题:微信小程序不是普通 Web 页面。它没有浏览器 DOM,逻辑层跑在 JavaScript 引擎里,渲染层跑在 WebView 中,两层之间只能用 setData 传数据。这意味着你在 Vue Web 项目里依赖的很多东西——直接操作 DOM、事件冒泡、运行时模板渲染——在小程序里天然不存在。
小程序第一波框架潮里,wepy 和 mpvue 都做过"用类 Vue 语法写小程序"的尝试。mpvue 当年靠的是把 Vue 2 整个 runtime 搬进小程序逻辑层,让 Vue 实例在内存里正常跑,再通过 setData 把视图同步到 WXML。思路没错,但 Vue 2 的响应式、虚拟 DOM 和组件更新机制都是为浏览器设计的,硬搬过来就会遇到性能瓶颈:一次数据更新经常导致整页数据重新 setData,在小程序里这是很致命的。
后来的 uni-app、Taro 转向了"编译时为主"的路线,把 Vue 模板在编译期尽量翻译成小程序原生 DSL,减少运行时负担。Weapp-vite 和 Wevu 本质上也是这个思路的延续,只不过它做得更彻底:构建层完全基于 Vite,运行时层做成一套轻量适配,让 Vue 3 的 Composition API 和模板写法在小程序里尽量接近原生体验。
1.2 Weapp-vite 负责编译,Wevu 负责运行时
把这两样东西拆开看,职责其实非常清晰。
Weapp-vite 解决的是"构建"问题:怎么把一个 .vue 单文件组件、一套 Vite 工程,编译成微信小程序认识的四件套(JS、WXML、WXSS、JSON)。它要做的事情包括解析 app.json 页面清单、拆解 SFC、把 Vue 模板指令翻译成 WXML 指令、把样式转成 WXSS、按需打包依赖。你可以把它理解成一个专门面向小程序产物格式的 Vite 插件集合。
Wevu 解决的是"运行时"问题:当代码跑在小程序逻辑层时,怎么让 Vue 的响应式系统、组件实例、生命周期、事件系统真正工作起来。它要做的事情包括提供 createApp/createComponent 之类的入口、管理组件实例树、执行 render 函数产出虚拟 DOM、计算数据变更路径、最后通过 setData 同步给渲染层。
按我的理解,Weapp-vite 是"编译时",Wevu 是"运行时",两者合在一起才构成一套完整的跨端方案。只做编译,没有运行时承接,模板翻译得再漂亮也驱动不起来;只做运行时,没有编译期的指令映射,运行时也得在 JavaScript 里做大量模板解释,性能上扛不住。
1.3 这篇文章适合谁读
如果你是 Vue 开发者,想了解"用 Vue 语法写小程序"背后到底发生了什么,这篇文章能帮你建立整体认知。如果你已经在用 Weapp-vite / Wevu 或者类似的方案做项目,文中关于 setData 调度、模板指令映射、生命周期映射的细节,能帮你在排查问题时更快定位。
我写的时候会尽量讲清楚"为什么":为什么模板要这样编译、为什么 setData 不能频繁调用、为什么虚拟 DOM 在小程序里不是用来操作 DOM 的。这些原理理解了,换任何一套跨端方案你都能快速上手。
2. 编译链路拆解:.vue 文件如何变成小程序"四件套"
2.1 Vite 插件如何接管 .vue 文件
Vite 本身是一个通用构建器,它并不认识小程序。Weapp-vite 要做的事,就是通过 Vite 的插件钩子,把整个构建流程重新定向到小程序产物。
第一个关键问题是入口。Web 项目的入口是 index.html,小程序没有这个概念,它的入口是 app.json,里面声明了页面路径、window 配置、tabBar 等。插件需要拦截构建入口,把 app.json 当作起点,读取 pages 字段拿到所有页面路径,把这些页面文件挂到一个虚拟模块图上,让 Vite 知道要构建哪些模块。
第二个关键问题是 .vue 文件的处理。这步通常借助 @vue/compiler-sfc 完成:插件里注册一个 transform 钩子,遇到 .vue 文件时先解析成 template、script、style 三个块,然后分别交给对应的编译流程。编译后的结果不是一个文件,而是一组文件,插件要在输出阶段把它们写成小程序的四件套目录结构。
整个链路大致是这样的:
- 读取 app.json,确定页面与组件清单
- 扫描并解析所有 .vue / .ts / .js 文件
- 对 .vue 文件做 SFC 拆解、模板指令翻译、脚本编译
- 把所有模块打包成一个个 JS chunk,每个页面/组件输出对应的 WXML、WXSS、JSON
- 依赖预构建与按需编译在 Vite 内部完成,node_modules 里的代码会被打包进产物
这跟普通 Vite 构建的最大区别是产物的组织方式:不是输出 index.html + 静态资源,而是输出一个小程序开发者工具可以直接导入的 dist 目录。
2.2 模板编译:Vue 指令到 WXML 的映射
模板编译是整条链路里最直观、也最繁琐的部分。Vue 模板和 WXML 都是一种声明式 DSL,但语法细节完全不同,编译就是把两边对齐。
最核心的指令映射大概是这样:
| Vue 模板语法 | WXML 对应写法 | 说明 |
|---|---|---|
| v-if / v-else-if / v-else | wx:if / wx:elif / wx:else | 条件渲染一一对应,比较简单 |
| v-for="item in list" | wx:for="{{list}}" wx:for-item="item" wx:key="id" | 循环渲染,注意 wx:key 的生成 |
| :title="msg" | title="{{msg}}" | 属性绑定变成 WXML 数据绑定 |
| @click="onClick" | bindtap="onClick" | 事件名映射,click 对应 tap |
| v-model="value" | value="{{value}}" + bindinput | 表单双向绑定需要拆成两部分 |
| 插槽保留,具名插槽对应 name 属性 |
事件映射是我实际开发中容易踩坑的地方。Vue 里 @click、@change、@input 这些事件名到了小程序里要对应成 bindtap、bindchange、bindinput,名字不一样。编译期必须维护一张事件名映射表,同时还需要处理事件修饰符,比如 .stop、.prevent,这些小程序的 WXML 没有原生支持,只能在生成的事件处理函数里手动调用对应 API 来模拟。
模板编译还有一个难题:动态组件和复杂指令。比如 Vue 里的 、Teleport、KeepAlive,小程序没有现成对应物,编译期只能枚举所有可能性,生成条件渲染分支,或者直接报错提示不支持。这也是为什么跨端方案对"小程序不友好的 Vue 特性"会有能力边界。
插槽的处理在小程序里也比较特殊。Vue 的作用域插槽可以拿到子组件内部数据,而小程序的插槽传递数据能力有限,通常需要通过额外属性把数据绑上去,编译期要完成这场"数据搬运"。
2.3 script 与 style 的处理:Composition API 与 scoped 样式
脚本部分,Weapp-vite 支持 Vue 3 的