笔者所在的趣店 FED 早在去年 10 月份就已全面使用 Taro 框架开发小程序(当时版本为 1.1.0-beta.4),至今也上线了 2 个微信小程序、2 个支付宝小程序。

之所以选用 Taro,解决微信小程序原生开发的痛点是一方面,另一方面团队也有多端统一开发的诉求,Taro 无疑是当时支持最好的。另外 React 也符合个人及团队的整体技术栈,可显著降低团队学习成本。

可以说,Taro 在小程序端、H5 端支持程度已经不错,也有不少上线实例可以查看,但在 React Native 的支持上,Github 中公开的项目在 RN 这块均未适配:

这种现况可以理解,毕竟要做到多端统一是有一定难度的,需准确把握各端差异,并做出合理取舍,而 Taro 虽以多端为设计目标,可重心在小程序端,没有对多端做出一定的开发约束,无从下手也便正常。笔者曾在 2018 iWeb 峰会 - 厦门站做过《多端统一开发实践》的分享,提到用 Taro 开发 RN 端的坑与大体思路,并加以实践。

结合趣店 FED 在过去小半年的实践经验,我们开发了首个 Taro 三端统一应用:taro-yanxuan(高仿网易严选微信小程序),用以探讨本文的重点:Taro 开发多端应用的正确姿势。

相关代码已开源:github.com/js-newbee/t…

在线预览

小程序端已支持微信小程序、支付宝小程序,但无法提供在线版,请 clone 代码本地运行。

H5 端、RN 端可在线预览(直接调用了网易严选接口,若要体验登录、购物车功能,请使用网易邮箱账号登录):

小程序 H5 - 访问链接 React Native
请 clone 代码本地运行 Expo Snacks

如下是 React Native 的运行截图:

首页、分类 二级分类、详情 购物车、个人

样式管理

样式管理是多端开发的首要挑战,因为 React Native 与一般 Web 样式支持度差异较大,上述几个未适配 RN 的多端项目多数已栽在样式上了,用到了大量 RN 不支持的样式,这种情况再要去兼容 RN 无异于重写页面,想必也是有心无力了。这也是本文所强调的,需把握正确的多端开发姿势。

样式上 H5 最为灵活,小程序次之,RN 最弱,统一多端样式即是对齐短板,也就是要以 RN 的约束来管理样式,同时兼顾小程序的限制,核心可以用三点来概括:

  • 使用 Flex 布局
  • 基于 BEM 写样式
  • 采用 style 属性覆盖组件样式

使用 Flex 布局

在进一步阐述之前,需先了解 RN 端几个影响样式方案的主要差异:

  • display 只有 flex / noneposition 只有 relative / absolute
  • 不支持标签选择器、子代选择器、伪元素,不支持 background: url() 等;
  • 文本要用 Text 标签包裹,文本的样式不能加在 View 标签上,只能加在 Text 标签上。

使用 Flex 布局,不单单是因为 RN 的 View 标签有默认样式 display: flex; flex-direction: column,更重要的是 Flex 可以解决幽灵空白问题:

// View 标签高度不会是 100px,图片下方会有几像素空白,称为幽灵空白
<View>
<Image src={...} style={{ height: '100px' }}
</View>
复制代码

常规解决方案是在 View 标签上设置 font-size / line-height: 0, 或 Image 标签 display: inline-block 等,但这些在 RN 中都不支持,给 View 标签设置 display: flex 算是唯一可靠方案了。

何况 Flex 布局能力强大,为啥不用呢?只需要注意一点,RN 中 View 标签默认主轴方向是 column,如果不将其他端改成与 RN 一致,就需要在所有用到 display: flex 的地方都显式声明主轴方向。

基于 BEM 写样式

RN 实际上只支持一种样式声明方式,即声明 style 属性:

<View style={{ height: '100%' }}
复制代码

这也导致 Taro 在 RN 端基本只支持 class 选择器这一种写法(最终编译成对象字面量),BEM(Block Element Modifier)在此处就恰如其分的发挥了作用:

  1. 避免样式冲突(RN、小程序样式独立,但 H5 不是)
  2. 自解释、语义化

例如每行 2 个元素的列表,每行最后 1 个元素有特定样式,用伪元素选择器 :nth-child(even) 很容易实现,在 RN 中就需要自行计算了:

{list.map((item, index) => (
<View className={classNames('block__element',
index % 2 === 1 && 'block__element--even'
)} />
)}
复制代码

基于 BEM 写 class 样式,不依赖其他选择器,虽然会让代码稍显繁琐,但也能保证多端都是行得通的,不存在支持问题。

采用 style 属性覆盖组件样式

小程序、RN 在页面、组件间传递样式时均有问题:

// 目前 Taro RN 端还未实现往组件传递 className 对应样式
<CompA compClass='my-style' /> // CompA,样式不生效
<View className={this.props.compClass} />
复制代码

上述场景小程序虽可通过组件外部样式 externalClasses 实现,但官网文档有强调 "在同一个节点上使用普通样式类和外部样式类时,两个类的优先级是未定义的,因此最好避免这种情况";用全局样式倒是可以,但这样样式就不好维护了。

那么,通过 style 传递、覆盖组件样式也就成了唯一可选方案了。需要注意一点,样式文件是会经过编译处理兼容多端的,但 style 方式需要运行时兼容:

<Comp style={postcss({ background: '#fff' })} />

// 简单演示,如 RN 不支持 background,需改成 background-color
function postcss(style) {
const { background, ...restStyle } = style
const newStyle = {}
if (background) {
newStyle.backgroundColor = background
}
return { ...newStyle, ...restStyle }
}
复制代码

从这个角度看,styled-components 或许是多端开发的最佳样式方案,然而 Taro 还不支持。另外微信小程序官方文档中有提到 "尽量避免将静态的样式写进 style 中,以免影响渲染速度",全部样式都写到 style 属性中恐怕不靠谱,但只用来覆盖少量样式不见得会有太大影响。

样式兼容

即便是把握了如上样式管理思路,多端样式差异的问题依然存在,例如 white-space: nowrap 这个样式在 RN 端会报错,Taro 有提供解决方案:

.text {
/*postcss-pxtransform rn eject enable*/
white-space: nowrap;
/*postcss-pxtransform rn eject disable*/
}
复制代码

但项目中不止一处会有这个问题,都这样写实在不太美观,可以用 Sass mixins 稍微封装下:

@mixin eject($attr, $value) {
/*postcss-pxtransform rn eject enable*/
#{$attr}: $value;
/*postcss-pxtransform rn eject disable*/
} .text {
@includes eject(white-soace, nowrap);
}
复制代码

Sass mixins 并不能解决差异,但对于部分各端不兼容的样式,通过 Sass mixins 统一处理是比较合理的方式,代码相对美观也方便维护。

端能力差异

相较于样式,端能力的差异倒是还好,各端差异是客观存在的,更不用说 RN 在 iOS 与 Android 上就已存在大量差异。

应对端能力差异,要么改变实现思路,例如 RN 端还不支持 Taro.(get/set)StorageSync,那就改用 async / await + Taro.(get/set)Storage 实现,要么就得使用环境判断方式了。

Taro 提供 process.env.TARO_ENV 用于环境判断,多数小的差异都可以用这种方式来解决:

function foo() {
if (process.env.TARO_ENV === 'weapp') {
// 微信小程序逻辑
}
if (process.env.TARO_ENV === 'h5') {
// H5 逻辑
}
if (process.env.TARO_ENV === 'rn') {
// RN 逻辑
}
}
复制代码

这个时候也比较考验开发者的封装能力了,一般是建议将这些差异逻辑的判断统一起来,例如在 src/utils 中进行封装,对外提供一致的接口,尽量不要在业务页面中杂糅太多的判断。

而对于简单的环境判断处理不了的问题,就只能动用原生开发了,例如 Taro 还不支持 RN 端的 WebView 组件,就需要自己用原生 RN 实现:

// Taro 页面,根据环境引入 RN 原生页面
import { WebView } from '@tarojs/components'
const WebViewRN = process.env.TARO_ENV === 'rn' ? require('./rn').default : null export default class extends Component {
render() {
return process.env.TARO_ENV === 'rn' ?
<WebViewRN src={this.url} /> :
<WebView src={this.url} />
}
} // 原生 RN 页面,从 react-native 引入 WebView
import Taro, { Component } from '@tarojs/taro'
import { WebView } from 'react-native' export default class WebViewRN extends Component {
render() {
return <WebView source={{ uri: this.props.src }} />
}
}
复制代码

process.env.TARO_ENV 的处理是编译时而不是运行时,也就是说若不是编译 RN,上述用原生写的 RN 页面不会被打包,保证了编译成其他端时不会引入不支持的内容。

原生页面能够引入,多端问题也就有了基本的实现保障。

Taro RN 端的坑

Taro RN 端目前小问题还是不少的,本项目开发过程中也顺带解了几个 bug:

除此之外还有好几个问题,时间关系还未提 pr 解决,暂且先绕过,但其中有两个坑还是值得一说的。

onClick

RN 的 View 标签不支持 onClick ,但这又是很通常的需求,原生解决方式是套一层 Touchable 组件,如:

<TouchableOpacity onPress={this.handlePress}>
<View>{...}</View>
</TouchableOpacity>
复制代码

而 Taro 是引入 PanResponder 响应用户操作:

<View
{...PanResponder.carete({ ...})}
style={wrapperStyle}
>
<WrappedComponent style={innerStyle} />
</View>
复制代码

问题在于这样多嵌套了一层 View,并把样式拆分成 wrapperStyle、innerStyle 分别应用,但样式拆分有问题,导致绑定 onClick 之后元素的样式错乱了,这点在开发过程中还是相当坑的。

宽高自适应

onClick 的问题也还好,改改样式能绕过去,宽高自适应的坑就比较尴尬了。

小程序、H5 可用 rpx / em 实现自适应,而 RN 的自适应方案麻烦些,一般需通过 Dimensions 获取宽高再进行换算。Taro.pxTransform() 可解决该问题,但编译 RN 端样式文件时并没有考虑这点,即 width: 100px 会被编译成 width: 50,而不是 width: Taro.pxTransform(100),无法适配不同的屏幕尺寸。

因此,目前 Taro RN 端还不好做到自适应,要么非百分比的宽高都用 style + Taro.pxTransform(),要么就得自己写个脚本去处理编译后的样式文件。

这两个问题都提了 issue 2204 2205,有需要的可以关注下解决进度

Taro H5 端的坑

Taro 对 H5 端的支持度尚可,若仅仅想要实现兼容小程序与 H5,也仍建议采用 BEM 写样式 + style 属性覆盖组件样式的方案,可以有效规避小程序自定义组件的诸多局限,只是在 CSS 特性上就不用像 RN 那样拘束,transform、伪元素等使用起来无压力。

另外就是小程序、RN 都没有跨域问题,但 H5 会有,这个可通过 devServer.proxy 解决,以及编译打包的静态资源是固定文件名,建议改成带 hash 值方便缓存管理,这些配置在项目里的 src/config 中都能找到,就不再复述了。

H5 端的坑更多的是集中在内置组件不够完善、端能力缺失较多,毕竟 Taro 的设计是以微信小程序为基准,去补充其他端的差异,编译成小程序就是直接用的小程序内置组件,但在 H5 端就需要一整套功能对等的内置组件,Taro 要做到一致所需的繁杂细节也可想而知。

举一个比较明显的坑来说,就是还不支持 Taro.switchTab(),暂时只能用如下方式先绕过:

if (process.env.TARO_ENV === 'h5') {
Taro.navigateBack({ delta: Taro.getCurrentPages().length - 1 })
setTimeout(() => { Taro.redirectTo({ url }) }, 100)
}
复制代码

好在官方已计划在接下来的 1.3 版本重构 H5 TabBar,到时这个问题也就解决了。

其他

要做到多端统一,能说的细节点实在太多,上述实现思路虽然简单,但背后也都是隐含着对各端差异的斗争与取舍,本文也仅是列出最基本的几点,用于阐述 Taro 多端开发的核心思路。

本项目代码没有做过多封装,方便阅读,也实现了足够多的样式细节进行踩坑,具体涉及的踩坑点、注意事项都在代码中以注释 // TODO(Taro 还未支持的)、// NOTE(开发技巧、注意事项)注明了,更多内容就有待各位去实践、体会了。

总结

如前言所说,Taro 虽然是以多端为设计目标,但重心是小程序端,RN 端目前的支持情况不算特别理想。但充分理解多端差异、掌握正确的多端开发姿势(特别是样式管理方面,避免项目成型后再去兼容需要大动刀斧)之后,在简单的项目上是完全可以一展拳脚的。

若说 2 个礼拜开发一个小程序,是稀疏平常的事,但 2 个礼拜即搞定了小程序端(微信、支付宝、百度等等),还搞定了 H5、React Native 端,后续更新也只要改一处地方,这产出、维护效率就实在太惊人了,这大抵也就是 "Write once, run anywhere" 的魅力所在(虽然在前端领域极容易发展成 "Write once, debug everywhere"

Taro 多端开发的正确姿势:打造三端统一的网易严选(小程序、H5、React Native)的更多相关文章

  1. 打造一款 刷Java 知识的小程序(二)

    学习Java的神器已上线,面向广大Java爱好者! 之前写的一篇:打造一款 刷Java 知识的小程序(一) 一.第二版做了什么? 第一版小程序只具有初级展示功能,知识点都是hardcode在代码里面的 ...

  2. 跨界!Omi 发布多端统一框架 Omip 打通小程序与 Web 腾讯开源 2月28日

    https://mp.weixin.qq.com/s/z5qm-2bHk_BCJAwaodrMIg 跨界!Omi 发布多端统一框架 Omip 打通小程序与 Web 腾讯开源 2月28日

  3. 跨界 - Omi 发布多端统一框架 Omip 打通小程序与 Web

    Omip 今天,Omi 不仅仅可以开发桌面 Web.移动 H5,还可以直接开发小程序!直接开发小程序!直接开发小程序! Github Omi 简介 Omi 框架是微信支付线研发部研发的下一代前端框架, ...

  4. 打造一款 刷Java 知识的小程序(一)

    一.为什么要打造 Java要学的东西太多了,所以准备把这些知识汇总到一起,而小程序是一个比较好的入口,借助微信客户端,打开方便. 二.打造成什么样 首页展示:包含了Java各大知识点模块 知识点展示: ...

  5. 第三章 “我要点爆”微信小程序云开发之点爆方式页面和爆炸之音页面制作

    点爆方式选择页面制作 在app.json中配置页面路径增加selectbao点爆方式选择,编译创建页面文件,点爆方式选择页面通过单选按钮组选择跳转到相应的点爆页面. <view class=&q ...

  6. 21天打造分布式爬虫-Crawl类爬取小程序社区(八)

    8.1.Crawl的用法实战 新建项目 scrapy startproject wxapp scrapy genspider -t crawl wxapp_spider "wxapp-uni ...

  7. 小程序多端框架全面测评:chameleon、Taro、uni-app、mpvue、WePY

    摘要: 微信小程序开发技巧. 作者:coldsnap 原文:小程序多端框架全面测评 Fundebug经授权转载,版权归原作者所有. 最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一 ...

  8. BeautyWe.js 一套专注于微信小程序的开发范式

    摘要: 小程序框架... 作者:JerryC 原文:BeautyWe.js 一套专注于微信小程序的开发范式 Fundebug经授权转载,版权归原作者所有. 官网:beautywejs.com Repo ...

  9. 微信小程序开发带来的思考

    若无小程序开发经验,可先阅读 玩转微信小程序 一文. 微信小程序正式上线已有几周时间,相信它的开发模式你已烂熟于胸,可能你也有所疑问,我竟能用 web 语言开发出如此流畅的几乎原生体验的应用.可能你又 ...

随机推荐

  1. Finance公式说明

    公式说明 代码 说明 Y 期末余额 JY 期末借方余额 DY 期末贷方余额 C 期初余额 JC 期初借方余额 DC 期初贷方余额 SY 本期实际发生额 SL 利润表本年实际发生额 SY 期末余额 SJ ...

  2. DWR使用总结

      这两天学了下DWR,现在总结一下. DWR是方便使用AJAX连接JS和JAVA的的一个框架,把服务器端 Java 对象的方法公开给 JavaScript 代码. 如果是用dwr2.0的jar包,还 ...

  3. Markdown 尝试

    目录 简介 参数模型 vs. 非参数模型 创新点 at the modeling level at the training procedure 模型结构 attention kernel Full ...

  4. github菜鸟入门

    github菜鸟入门教程 闲来无事,研究了下github的玩法,完毕总结:简单好玩,上档次! 一.首先注册github的账号 二.下载安装git 三.新建仓库 1.点击右上方的+号选择首项新建仓库 2 ...

  5. Chapter5_初始化与清理_成员初始化

    在java中,成员初始化在使用之前应该都要保证已经完成初始化.对于在方法体中的局部变量,如果没有使用指定初始化的方法对成员变量进行初始化,编译器会提示一个错误.而对于类的数据成员,编译器会对这些成员赋 ...

  6. Appium+Python自动化 3 -获取 app 包名和 activity

    方法一: ①手机通过USB连接电脑 ②打开手机上被测app ③在电脑上 dos命令窗口,输入命令 adb shell dumpsys window w | findstr \/ | findstr n ...

  7. thinkphp 5 使用oss

    简单的tp5中上传到 图片到oss我本地开发环境为:WAMP;php版本:5.6.19TP版本:5.1.13 1.使用composer 安装 composer require aliyuncs/oss ...

  8. (Swiftmailer)高效的PHP邮件发送库

    Swiftmailer是一个类似PHPMailer邮件发送组件,它也支持HTML格式.附件发送,但它发送效率相当高,成功率也非常高,很多PHP框架都集成了Swiftmailer. Swiftmaile ...

  9. 15. Life Cycle of the Products 产品的生命周期

    15. Life Cycle of the Products 产品的生命周期 (1) We can see how the product life cycle works by looking at ...

  10. vue学习-自动行合并的table

    测试的效果 测试的html源码截图 v-table在tableGroup.js中定义,以下就render方法,行的所有单元格都在tableGrouper.js中处理 render:function(h ...