笔者所在的趣店 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. windows socket 文件下载上传

    socket 图片 文件 下载上传  数据库 线程池  存入图片 等

  2. cmd下的一些小技巧

    切换盘符:[盘符]+: tips1: 在盘符A执行一条命令后的同时切换到盘符B:A:\>dir && B:(此处只能用&&或者&) tips2: 在盘符A ...

  3. mysql8 出现1521错误解决方法

    ALTER USER 'root'@'localhost' IDENTIFIED BY 'password' PASSWORD EXPIRE NEVER; #修改加密规则 ALTER USER 'ro ...

  4. 浅析b-树 b+树 以及Mysql的Innodb,Myisam引擎

    B-树性质 B-树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点. 1根节点至少有两个子节点 2每个节点有M-1个key,并且以升序排列 3位于M-1和M key的子节点的值位于 ...

  5. HDU4521

    一个改变的最长上升子序列(LIS),这种题型做的很少,今天做起来很费劲,查了很多资料,还把最基础的LIS补了一遍,具体的看代码吧,我把思路都放在了注释里面 #include<iostream&g ...

  6. RPA答疑

    变量:部分主要对于工程中的变量进行管理和设置,支持手动拖拉至右侧画布 1全局变量:全局变量可以在整个工程中传递,被不同的子流程或函数进行调用,全局可见可调用 2 流程参数:只对子流程设置传入参数,供子 ...

  7. Webpack-dev-server的proxy用法

    前言: devServer:{ contentBase:'./', proxy:{ // 当你请求是以/api开头的时候,则我帮你代理访问到http://localhost:3000 // 例如: / ...

  8. 2019.03.26 bzoj4448: [Scoi2015]情报传递(归并排序+树链剖分)

    传送门 题意简述: 给一棵nnn个点的树,树上每个点表示一个情报员,一共有mmm天,每天会派发以下两种任务中的一个任务: 1.搜集情报:指派T号情报员搜集情报 2.传递情报:将一条情报从X号情报员传递 ...

  9. JSON.parse()——Uncaught SyntaxError: Unexpected token \ in JSON at position 1

    背景:项目安全处理方面之一 ——对特殊字符进行编解码(后端编码,前端解码) 特殊字符: "    %22 \    %5C /    %2F &   %26 %   %25 '    ...

  10. 【翻译】从头开始写一个时间序列数据库-Writing a Time Series Database from Scratch

    本文来自: https://fabxc.org/tsdb/, 如翻译有误,请纠正. 我是从事监控工作的.特别是Prometheus, 一个包含自定义的时间序列库以及集成Kuberntes的监控系统. ...