本节导读:承接 2.1 的语法线鉴定,本节深入模板内部:插值与指令的通用边界、样式绑定的平台差异、事件绑定的编译去向与事件对象的差异。核心结论提前给:模板写的是 Vue,跑的是各端,查模板问题先看编译产物。
模板是跨端差异的第一现场,因为它是唯一"必然被翻译"的层——你在 template 里写的每个标签和事件,编译器都要转写成目标平台的语法。H5 端翻成 DOM 操作,微信端翻成 wxml 的指令与事件绑定。理解这条翻译链,模板层的绝大多数怪问题都有了排查入口。
插值 {{ }}、条件渲染 v-if 与 v-show、列表渲染 v-for、属性绑定 v-bind,这些核心指令全端通用,放心书写。边界在细节处:v-for 必须配 key,且 key 要用业务唯一标识而非数组下标——小程序端对列表差异比对的方式与浏览器不同,用下标做 key 在头部插入时会出现整列表错位的渲染异常;v-if 与 v-show 的选择在小程序端比 H5 更偏向前者,因为小程序的节点创建成本低于 H5 的组件实例,但频繁切换的浮层仍建议 v-show 减少通信。
<template> <view> <!-- 正确:业务标识做 key --> <view v-for="item in goods" :key="item.id" class="row"> <text>{{ item.name }}</text> <text>{{ item.price | currency }}</text> </view> </view> </template>
上面这行过滤器写法在 Vue3 工程里已经失效(2.1 节的差异表),实际项目里应改为方法或计算属性,这里保留作对照提醒。
<template> <view class="card" :class="{ active: picked }">对象语法,全端可用</view> <view class="card" :class="[picked ? 'on' : '', size]">数组语法,nvue 不支持</view> <view class="card" :style="{ color: theme }">内联样式对象,全端可用</view> </template>
对象语法与内联样式全端安全;数组语法依赖 css 类名拼接,在 nvue 页面(原生排版引擎)里不受支持。另一个隐蔽差异是小程序端的样式隔离:自定义组件默认样式隔离,页面样式选择器进不了组件内部,除非显式开启,这一点与 H5 端"页面样式全局生效"的直觉相反,第 3 章讲组件时展开。

@tap 是跨端事件的默认选择:微信端编译为 bindtap,H5 端合成点击监听。事件对象的差异是模板层最深的一个坑——同一处点击,两端的 event 字段不同:
<template> <view class="list"> <view v-for="g in goods" :key="g.id" :data-id="g.id" :data-name="g.name" @tap="onPick">{{ g.name }}</view> </view> </template> <script> export default { methods: { onPick(e) { // 传参的标准姿势:把业务数据挂在 data- 属性上,从 dataset 读回 const { id, name } = e.currentTarget.dataset; console.log('选中商品:', id, name); // 坐标类字段两端都有,但命名不同 const x = e.detail ? e.detail.x : e.pageX; } } }; </script>
三条经验:传参一律走 data- 属性加 currentTarget.dataset,不要依赖闭包参数(在小程序的事件系统里拿不到你想拿的上下文);区分 target 与 currentTarget——事件委托场景下 target 是实际触发的子节点,带业务数据的通常是 currentTarget;需要阻止冒泡用事件修饰符 .stop,编译器会翻译成对应端的机制,不要手写各端的原生 API。
插值里能写多复杂的表达式,各端的回答不一样,根源仍在编译产物。H5 章虚机跑在浏览器里,方法调用、三元、算式都随意;小程序端的插值被翻成数据字段,复杂表达式会被编译器提取改写,某些写法(比如插值里调用依赖浏览器对象的方法)直接在运行期断掉。工程公约建议把插值瘦身为"读一个字段",复杂计算交给计算属性:
<template> <!-- 不稳妥:插值里做格式化与方法调用,各端产物差异大 --> <text>{{ goods.map(g => g.name).join('、') }}</text> <!-- 稳妥:插值只读计算属性,逻辑进 script --> <text>{{ joinedNames }}</text> </template> <script> export default { computed: { joinedNames() { return (this.goods || []).map(g => g.name).join('、'); } } }; </script>
另一条边界是"操作真实节点"的需求(表单焦点、动画中间态、富文本量尺)。H5 端可以拿到 DOM,小程序端没有这个概念,uni-app 给出的统一出口是节点查询 API 与各端的脚本扩展机制(wxs 与 renderjs,分别在微信端与 App/H5 端承接视图层轻逻辑)。写跨端模板时先问一句:这段操作能不能退回数据驱动?能退回的退回,退不了的圈进条件编译并给各端各写一份视图层实现。
v-model 在 input 与 textarea 上全端可用,编译器负责同步时机;但选取类组件的值同步各有差异:picker 组件靠 change 事件回传、switch 布尔绑定生效、slider 拖动结束才触发 change,@input 的触发频率也不同(H5 每键触发,小程序端高频输入有节流行为)。涉及输入联想、实时校验的功能,按"change 为准、input 做辅助提示"设计,能避开大部分端差。
模板之下是响应式系统。下一节进入数据驱动的引擎室,顺路把 TypeScript 的实践一并交代。