前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Taro跨平台开发实践 多端编译原理与踩坑总结

首页2019-06-08 15:30:21Front-End
Taro小程序跨平台

老板给的需求是「小程序、H5、App 都要有,人只有这几个」。这种时候多端框架就成了必选项。我们那阵子用 Taro 做了一个三端并行的项目,写一套 React-like 代码同时出微信小程序、H5 和 React Native 包。真跑起来才发现,「一套代码多端运行」这句话里,省掉的是重复的业务逻辑,省不掉的是对每个端的了解。样式在哪一端会失效、哪个 API 只有小程序有、RN 为什么点不动 View,这些都得自己踩。这篇把 Taro 的编译原理先讲清楚,再把当时记下来的坑和技巧归拢成几类,方便你上手前先有个心理预期。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • Vue-like、React-like 的代码凭什么能在小程序里跑起来
  • 编译时处理和运行时适配各自负责什么,AST 在中间起什么作用
  • Taro 的多端能力是怎么拼出来的,runtime、组件、redux、router 分别对应什么
  • React-like 不等于 React,具体差在哪些地方
  • 样式、端能力、UI 组件库这三类差异怎么填坑
  • 条件编译、process.env.TARO_ENV、多端同名文件这三个技巧怎么用
  • React Native 端的启动流程和样式限制
  • 2019 年的 Taro 1.x 和现在的版本差在哪,哪些结论已经过期

# 一、小程序开发框架解决的是什么问题

小程序有自己的一套 DSL,wxml 加 wxss 加 js,跟你熟悉的 React 或者 Vue 都不一样。业务方要的是同一套页面在多个端上跑,开发者想用的是自己顺手的语法,中间这道鸿沟就是多端框架存在的理由。

小程序多端开发框架的整体格局

那么问题来了,Vue-like、React-like 的代码,凭什么能在小程序里运行?

Vue-like 与 React-like 代码在小程序中运行的原理示意

# 1.1 基本原理

答案拆开只有两句话:

  • 编译时处理,把你写的语法转译成小程序语法
  • 运行时适配,管理生命周期、数据,处理事件等

这两件事缺一不可。光有编译,转出来的模板没有数据驱动和生命周期,就是一堆死页面;光有运行时,你写的 JSX 小程序根本不认。Taro 走的是编译加运行时的组合路线,重活压在编译期,运行期只留必要的适配层。

编译这一步的流程,就是编译原理课上那条链路:

源代码->词法/语法/语义分析->抽象语法树->转换->目标代码

编译流程从源代码到目标代码的完整链路

中间那个「抽象语法树」是整套方案的支点。源码被解析成一棵结构化的树之后,改代码这件事就从「字符串替换」变成了「操作树节点」。把 JSX 里的 <View> 节点换成 <view>,把 onClick 属性换成 bindtap,都是在树上做增删改,改完再打印回目标语法。

抽象语法树 AST 的结构示意

这也解释了后面会讲到的一堆限制。既然是静态分析代码再改写,那你写得越动态,编译器就越猜不到你想干什么。小程序端不支持在 render() 之外定义 JSX、JSX 里必须用单引号这些规矩,根子都在这儿。

# 1.2 各家框架的多端支持情况

那几年多端框架扎堆出现,各自支持的端不一样,选型的时候这张对比得先看:

各小程序框架的多端支持情况对比

选型这事我的经验是,别只看支持的端多不多,要看你真正要发的那两三个端做得深不深。支持十个端但每个端都半残,不如支持三个端每个都能上线。

# 二、Taro 的原理

Taro 是一套多端统一开发框架,支持用 React 的开发方式编写一次代码,生成能运行在微信小程序、H5、React Native 等平台的应用。

它的整体架构分成编译和运行两条线,下面这几张图把这两条线拆开了:

Taro 整体架构,编译时与运行时两条主线 Taro 编译流程的详细拆解 Taro 把 JSX 转换成小程序模板的过程 Taro 编译产物与小程序原生结构的对应关系

编译这条线做的事,是把 React-like 的写法拆成小程序认识的两部分,模板结构进 wxml,状态和逻辑进 js。JSX 里的表达式、条件渲染、列表渲染都要在这一步翻译成 wx:if、wx:for 这类指令。

光有编译还不够,跑起来还需要每个端各自的一套底座。

Taro 的多端能力,各端提供各自的 runtime 实现 各端组件库的对应实现 Taro 对 redux 等状态管理方案的多端适配 Taro 路由在各端的实现差异

所谓多端能力,落到实处就是两句:

  • 提供相应端的 Runtime 实现,具备适配 API 的能力
  • 提供统一的基础组件,编译时替换为相应端的组件实现

前一句管 API。你调 Taro.request,在小程序端它转成 wx.request,在 H5 端它转成 fetch 或者 XMLHttpRequest,在 RN 端又是另一套。后一句管组件。你写 <View>,编译到小程序变 <view>,编译到 H5 变 <div>,编译到 RN 变 RN 的 View。

理解了这两句,后面所有的坑就都好解释了:只要某个端底下没有对应的实现,这层抽象就漏了。

# 三、Taro 项目中遇到的坑

# 3.1 那些真会绊到人的地方

1、React-like,不完全等同 React

React-like 与标准 React 的差异清单

这是最容易掉进去的一个。因为语法长得一模一样,你会下意识按 React 的习惯写,然后在编译期收到一条看不懂的报错。原因前面说过,Taro 1.x 靠静态分析改写代码,凡是它分析不出来的动态写法都会被拒绝。

2、端能力差异不可避免,某些功能在相应端上没有支持

不同端之间能力差异的对照

框架能抹平的是写法,抹不平的是能力。小程序有的原生能力,浏览器里根本不存在;RN 有的原生模块,小程序里也调不到。碰到这类差异,只能自己在业务层做分支。

3、样式支持、写法上存在差异

三端样式支持的差异对照之一 三端样式支持的差异对照之二 三端样式支持的差异对照之三

样式是多端项目里返工最多的部分。H5 最灵活,小程序次之,RN 最弱。你按 H5 的习惯写一版,到 RN 上会发现选择器全被忽略了。

4、还没有真正支持多端的 UI 组件库能在 Taro 上使用

当时可选的 UI 组件库及其多端支持情况

这条是 2019 年的现状,当时组件库生态确实很薄,多数只覆盖小程序端。后面会讲现在的情况。

5、React Native 的 view 不支持 click 事件,需要用 Touchable 组件

React Native 中点击事件需要用 Touchable 系列组件包裹

第一次遇到这个会很懵,代码没报错,样式也对,就是点不动。RN 里的 View 本来就不是可交互元素,要响应点击得用 Touchable 系列组件包一层。

6、React Native 不支持 text-overflow,而是提供原生支持

RN 的做法是给 Text 组件传入 numberOfLines={num} 属性来控制行数。问题在于 Taro 当时没有暴露原生 RN Text 组件的 numberOfLines 属性,所以通过 Text 基础组件没法实现多端统一的文本截断。

(原文这里的组件名被反引号截断成了两截,这里按 Text 改正了。)

H5 端的情况相对乐观,主要差异集中在两点:小程序的页面、组件样式都是独立的,H5 会受同名样式影响;小程序的组件会多出一层标签。

小程序组件与 H5 组件在 DOM 结构上的层级差异

第一条尤其要当心。你在小程序上验证过没问题的样式,搬到 H5 上可能被别的页面的同名 class 覆盖掉,因为小程序天然是样式隔离的,H5 不是。这个差异不会报错,只会让你在某个页面上看到一个诡异的边距。

7、多端要求较高

用多端框架并不等于可以不懂各个端,实际要求反而更高:

  • 对不同端的具体差异有所了解
  • 样式实现较为苛刻,需兼顾多端、有所取舍
  • 端能力差异可能需要自己填坑

8、多端填坑方式

多端差异的封装与填坑思路

我们当时摸出来的实践就四条:

  • 对多端差异做好封装
  • 采用 BEM 命名方式管理样式
  • 采用全局样式维护基础组件 @tarojs/components 的样式
  • 基于 @tarojs/components 自行封装组件库

(原文这两条里包名写的是 @taro/components,实际的包名是 @tarojs/components,这里改正了。)

前两条是重点。差异封装的意思是,别在业务组件里到处写 if (端 === 'weapp'),而是把这类判断收进一层工具函数或者一层适配组件,业务代码只调统一接口。BEM 则是为了兼容 RN 只支持类选择器这条硬限制,你不可能在 RN 上写嵌套选择器,那就干脆从命名上把层级表达出来。

# 3.2 记在本子上的注意事项

下面这些是当时一条条撞出来的,按类别归了一下:

命名和写法约束

  • 函数需要 on+函数名 来规范命名
  • 在 Taro 中,JS 代码里必须书写单引号,特别是 JSX 中,如果出现双引号,可能会导致编译错误
  • 小程序端不支持在 render() 之外定义 JSX,比如在外面写 renderForm()
  • Taro 当时还没有支持 React.Fragment 语法

这几条都是编译期静态分析带来的约束,前面讲原理时提过。单引号那条最玄学,报错信息经常指不到真正的位置,排查了一下午才发现是某个属性里混了双引号。

组件与数据

  • 子组件中接收的 props 需要定义 defaultProps,否则小程序端报错
  • 状态更新一定是异步的,React 的状态更新不一定是异步
  • 根据环境变量引入不同平台组件、编译到对应平台
  • 页面布局拆分组件的方法,由上到下、由左到右拆分

defaultProps 那条是因为小程序的自定义组件要求属性有明确的类型和默认值,编译期需要据此生成 properties 声明,缺了它生成不出来。

样式与 RN 限制

  • css 样式单位写 PX 大写会转成 rem 单位
  • 文字要包在 Text 组件里面,否则不显示
  • position:fixed 在 React Native 不支持
  • Animation 和 transform 在 React Native 动画不支持

环境相关

  • 运行时报缺少包,需要在 .rn_temp 目录里面安装

# 四、几个真正省事的技巧

坑说完了,说点好用的。Taro 处理多端差异的思路很统一,都是「同名不同后缀」,编译时按目标端挑文件。理解了这一条,下面三个技巧就是同一个套路的三种用法。

# 4.1 样式文件条件编译

假设目录中同时存在以下两个文件:

- index.scss
- index.rn.scss
@前端进阶之旅: 代码已经复制到剪贴板

当在 JS 文件中引用样式文件写 import './index.scss' 时,RN 平台会找到并引入 index.rn.scss,其他平台会引入 index.scss。

这招解决的是前面那条最难受的限制:RN 不支持组合选择器,也不支持一堆 CSS 属性。你不可能为了迁就 RN 把所有端的样式都写成扁平的类选择器,那 H5 端的表达力就废了一半。有了这个机制,公共样式留在 index.scss 里正常写,RN 那份单独降级,两边互不干扰。

# 4.2 内置环境变量 process.env.TARO_ENV

process.env.TARO_ENV 用于判断当前编译类型,当时有 weapp / swan / alipay / h5 / rn / tt 六个取值。可以通过这个变量来书写不同环境下的代码,编译时会将不属于当前编译类型的代码去掉,只保留当前编译类型下的代码。

关键在「编译时去掉」这几个字。这不是运行时的 if 判断,而是构建阶段就把另一个分支整段删掉。所以你在小程序包里不会带上 H5 的那份代码,包体积不会因为多端而膨胀。

比如想在微信小程序和 H5 端分别引用不同资源:

if (process.env.TARO_ENV === 'weapp') {
  require('path/to/weapp/name')
} else if (process.env.TARO_ENV === 'h5') {
  require('path/to/h5/name')
}
@前端进阶之旅: 代码已经复制到剪贴板

同时也可以在 JSX 中使用,用来决定不同端要加载的组件:

render () {
  return (
    <View>
      {process.env.TARO_ENV === 'weapp' && <ScrollViewWeapp />}
      {process.env.TARO_ENV === 'h5' && <ScrollViewH5 />}
    </View>
  )
}
@前端进阶之旅: 代码已经复制到剪贴板

这里有个坑要注意,判断必须写成能被静态分析出来的形式。把 process.env.TARO_ENV 先赋值给一个变量再判断,编译器就认不出来了,两个分支都会被打进包里。

新版本的 Taro 支持的端比这六个多,取值列表也扩充了,具体有哪些以你装的版本文档为准,别照抄这个列表。

# 4.3 统一接口的多端文件

这是同一套思路的完整版,适合封装差异比较大的组件和工具。

fe
  • 一、小程序开发框架解决的是什么问题
    • 1.1 基本原理
    • 1.2 各家框架的多端支持情况
  • 二、Taro 的原理
  • 三、Taro 项目中遇到的坑
    • 3.1 那些真会绊到人的地方
    • 3.2 记在本子上的注意事项
  • 四、几个真正省事的技巧
    • 4.1 样式文件条件编译
    • 4.2 内置环境变量 process.env.TARO_ENV
    • 4.3 统一接口的多端文件
  • 五、React Native 端的实践
    • 5.1 把 Taro 项目跑到 RN 上
    • 5.2 样式,以 RN 的约束为准
    • 5.3 两个容易写出来的性能问题
  • 六、这篇写于 2019 年,现在的 Taro 已经不一样了
  • 总结
  • 参考

← Ionic3 升级 Ionic4 变更对比与迁移避坑记录React Native 适配 Android 与 iOS 完整总结篇 →