Taro 和 uni-app 如何把代码转换成小程序?编译时和运行时有什么区别?
对比编译型转换与 Taro 3 的运行时架构(模拟 DOM、自定义渲染器、模板递归),以及 uni-app 的条件编译和逻辑层/视图层拆分。
参考答案
简答
- 编译时方案在构建阶段把源码(如 JSX)静态翻译成小程序的 WXML、JS 和 WXSS,运行开销小,但只能支持能静态分析的写法。
- Taro 3 改为运行时方案:在逻辑层用
@tarojs/runtime模拟精简的 DOM 和 BOM,让 React、Vue 等框架直接运行。 - React 通过基于
react-reconciler的自定义渲染器操作这棵模拟 DOM 树,Taro 再把树序列化后setData,由预先生成的、可以相互引用的<template>渲染出界面。 - uni-app 在编译时把 Vue 单文件组件拆成逻辑层和视图层,并用
#ifdef等条件编译为不同平台保留不同代码。 - 运行时方案换来更完整的框架能力,代价是额外的运行时体积和数据传输,长列表等场景仍需要关注 setData 开销。
编译时方案
Taro 文档把这类框架称为"编译型的小程序框架",早期 Taro 通常被归为这一类:用 Babel 解析 JSX,把模板部分转换成 WXML,把组件逻辑转换成小程序的 Page、Component 调用。
这种方式生成的代码接近手写小程序,但 JSX 是任意 JavaScript 表达式,编译器只能识别有限的写法,动态渲染、高阶组件等用法会受到限制,开发体验和完整的 React 有差距。
Taro 3 的运行时方案
Taro 文档对实现原理的说明可以概括为四步:
- 模拟浏览器环境:
@tarojs/runtime在逻辑层实现了精简的 DOM、BOM API、事件系统以及 Web 框架和小程序框架之间的桥接层。 - 自定义渲染器:ReactDOM 体积大、包含大量浏览器兼容代码,Taro 借助
react-reconciler实现了自己的渲染器(@tarojs/react),把 React 的更新落到 Taro DOM 树上。 - 模板递归:小程序模板必须在构建时确定,不能运行时生成。Taro 利用
<template>可以引用其他<template>的特性,把 Taro DOM 树中每个节点对应渲染成一个 template,构建产物中的base.xml就是这些模板的集合。 - setData 驱动:把 Taro DOM 树的序列化数据
setData给视图层,模板相互引用后渲染出最终界面。
因为 React、Vue 都只和模拟 DOM 打交道,Taro 3 可以同时支持多个框架。代价是所有界面更新都要经过模拟 DOM 和 setData 两层,节点越多,序列化和渲染的成本越高。
uni-app 的做法
uni-app 文档说明,除 H5 外,uni-app 在各平台都分为逻辑层和视图层,虽然开发者把 JS 和 CSS 写在同一个 .vue 文件里,但编译时已经把它们拆开。逻辑层运行在独立的 JS 引擎中,不能使用 window、document 等浏览器 API。
平台差异主要靠条件编译处理:
vue
<template>
<!-- #ifdef MP-WEIXIN -->
<button open-type="share">分享</button>
<!-- #endif -->
</template>
<script>
// #ifdef H5
console.log('只在 Web 端打包');
// #endif
</script>
#ifdef只在指定平台保留,#ifndef在指定平台以外保留,以#endif结束;平台名写错时,#ifdef中的代码不会生效,而#ifndef中的代码会生效,反向条件更容易踩坑。static目录下按平台命名的子目录可以实现文件级条件编译。
uni-app 的性能文档也提醒:数据变化会同步到视图层,页面不拆组件时,一个小字段的变化可能导致整页数据同步;data 中不应放视图用不到的变量。
常见追问
- 跨端框架里能用
document.querySelector吗?Taro 运行时提供的是逻辑层里的精简 DOM,拿不到渲染层的真实布局信息;测量尺寸要用小程序的createSelectorQuery或框架封装的 API。 - 运行时方案会让小程序变慢吗?会增加运行时体积和初始化开销,一般业务影响有限,节点多的长列表需要用虚拟列表等手段,setData 的通用优化见《setData 为什么会慢?如何优化小程序的数据更新?》。