Fork me on GitHub

React-Web与React-Native项目横向对比

 进行RN开发也差不多有1年的时间了,这篇文章是篇阶段性的总结文章,主要进行React在web和native应用上的对比。

渲染工作(Renderers)

  React诞生之初仅是为了进行DOM视图渲染。它以“数据即视图”的哲学去进行大型项目页面的复杂状态维护,提高项目的鲁棒性。随后React也开始兼容支持native端的编写,react-dom继续支持web的renderer工作,以react-native-renderer去进行React-Native的renderer工作。随后社区生态越来越繁盛,也有类似京东的taro同样以React的语法通过babel进行ast转换成支持各种小程序(微信、支付宝、字节)的语法格式。

  以上都是官方文档那句Learn Once, Write Anywhere的具现。

JS运行环境(Engine)

  在Web端主要看是什么浏览器,像我们最常接触的Chrome,运行环境就是大名鼎鼎的V8。而在IOS、Android的模拟器或是真机上,React-Native使用的是JavaScriptCore,它同时也是Safari的WebKit引擎内置的JS运行环境。由于在IOS上众所周知的访问权限因素,JSCore在IOS中是没有使用JIT的。

  值得注意的是,从0.60.4rn版本开始,Android已经有新的JS引擎可以替换使用了。它就是Hermes,这篇文章不进行更多的引擎解释,大概意思就是提升了不少js运行的性能,贴一篇携程的文章。至于IOS,还是由于平台的审核🚫等问题,在引擎上未作出调整。

  PS,React-Native之于原生App,其实就相当于我们编写的一个组件,<RNContainer></RNContainer>承载了我们RN的运行环境。这同时也意味着原生Appd端可以直接在我们的最外层props上挂载属性

打包(Build)

  在Web端的项目中,目前主流的打包方案都是使用Webpack,而Webpack版本也更新的非常快,写这篇文章时已经到Webpack5了。而在1年前我才把前东家的打包方式从3升到了4,提升了一定打包效率🤷‍♂️。不过升级5还是要谨慎,之前看一个技术群的小伙伴们讨论,阿里的antd里面使用了很多babel-runtime的内容在升级5后都扑街了。

  Webpack涉及很大一部分的知识体系,其实在官方文档中都有,想要提升,阅读官方文档➕实践是最理想的方式。不过根据现在的一些脚手架发展,我们可以发现很多配置社区已经帮你集成好了,即形成了沙盒,让业务开发者专注于业务上的内容。这样其实有利有弊,利肯定就是项目从零开始那段空白“开荒”时间,大大减少。另外集成的约束规范(lint、commit-hook、ts等)会帮助我们统一编码风格,减少一些麻瓜错误。弊端也很显著,不利于开发者自身对完整项目构建的掌控,想要自定义改造升级Webpack困难。

  在Native端的项目中,其实React维护团队也对📦方式进行了一定的封装,Metro是React-Native集成的打包方案,里面初始情况下就自带了很多es next的语言特性兼容。不需要我们再在config文件中引入额外的babel-runtimepolyfill。默认情况下和Webpack的打包方式类似,同样是一个入口entry文件(默认是index.js),最终输出一个bundle文件(main.jsbundle),里面是所有js代码及相关依赖。

业务(Coding)

  如果你是有经验的React开发者,我想完成基本的业务实现是没有问题的…

语法(Language)

  其实都是在写React。

事件(Event)

  习惯了Web端的开发者,一开始切换到Native上玩,对于那些点击事件都会下意识的整个onClick上去,不过区别于Web端的合成事件系统,Native已经是另外一个体系了。通常的点击是onPress, 当然也有长按支持onLongPress等等,建议先完整过一遍React-Native的文档再开始动手。

  在Web端我们可以在全局或者具体的DOM节点上挂载某个事件的监听(冒泡or捕获,还要视浏览器的兼容),在Native端则没有了这些能力。那在不同组件我怎么知道是否触发了这些事件呢?通常我们会实现一个比较简单的EventEmitter类,配置一个全局的观察者模式进行事件监听、触发、移除等操作。当然也有现有的库支持,如Node自带的events

布局(Layout)

  在Web页面中我们常用div进行布局,在RN中最主要进行布局的盒子则是View组件。

  在RN中,官方文档推荐的布局样式是使用flex,并且RN默认的方向是纵向的,习惯了横向的开发者别忘记先设置flex-direction

  position: absolute定位在安卓上表现有时会出现异常,如点击不到,对比IOS,此时是正常的。这个时候往往需要设置z-index

  在看常见的Web布局:

  移动端的页面布局:

  其实我们可以看见比较明显的差异在于,native端在进行布局的时候还需要对顶部的状态栏(通过StatusBar组件)进行定制,常见是顶部状态栏是否显示(hidden属性)、背景颜色(backgroundColor属性,该属性只有Android能配置)、字体颜色(barStyle,支持三种模式选择enum('default', 'light-content', 'dark-content')),半透明处理(translucent,该内容仅Android支持,作用在于将状态栏与我们的app页面主题背景颜色统一)等。

  处理了StatusBar后,就是我们的导航栏样式设置。对于笔者目前的React-Natvie项目而言,使用的是react-navigation3.x版本,导航栏的样式相关配置首先得先通过createStackNavigator路由堆栈处理相关页面组件,再在对应组件的静态属性navigationOptions上进行导航栏配置。具体如何配置可以查阅官方文档

  这里主要想提及的是如何兼容IOS及Android的顶部导航栏高度,因为Android的StatusBar有一个currentHeight高度,所以在按GUI还原页面时,Android端高度须要减去这个状态栏高。而IOS状态栏高度可以理解为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
static navigationOptions = ({ navigation }) => {
return {
// 返回导航栏的相关配置参数
headerStyle: {
height: Platform.OS === 'ios' ? tabBarHeight : tabBarHeight + StatusBar.currentHeight,
paddingTop: Platform.OS === 'ios' ? 0 : StatusBar.currentHeight,
elevation: 0,
},
headerTransparent: true,
headerTitle: '',
headerTitleStyle: {},
}
}

错误捕获(Error Capture)

  相同点是,同步方法中我们可以使用try...catch...的方式进行抓取,render异常可以通过React的错误边界进行抓取进行降级处理。不同点是,引发渲染异常时,web表现是白屏,native则会造成rn容器崩溃(闪退)。调试模式下,native端会报红屏,console.error同样可以触发红屏,console.warn则是黄屏。

  在web应用中,对于那些没有进行捕获的异常最终会冒泡到全局上。可以在全局统一对这类问题进行处理,Web端处理这类型问题一般需要做两件事情: 挂载onerror(message, source, lineno, colno, error)以及onunhandledrejection事件监听,前者是全局的错误捕获,但是无法处理异步,如promise内部的reject,该异常需要通过onunhandledrejection进行捕获,这个监听回调会收到一个event对象,内部有reasonpromise属性,分别可以拿到抛出的异常及对应异常的promise,还能通过preventDefault方法进行事件冒泡拦截,取消输出到控制台。

  在RN中也有相近的做法,它同样也得做两件事。不过在RN中分了两个工具库去做对应的事情,它们分别是global.ErrorUtilspromise/setimmediate/rejection-tracking。具体降级处理方案见《React-Native疑难踩坑记录》一文中的错误捕获章节。

路由(Router)

  在SPA应用中我们进行页面跳转,本质上是没有再像过往的多页应用那样重新向服务端请求Content-Type: text/html的新页面。而是通过路由hashhistory事件trigger进行局部内容替换(卸载->挂载)来达到“跳转”的目的。

  之前进行web端开发时使用的是react-routev3.x版本,现在去npm官方查了下已经到5.x版本了。中间4.x版本记忆比较深的改动是,将原本耦合的路由嵌套结构独立了出来。5.x后续有时间可以去看看官方的changelog。

  RN端的路由跳转我个人觉得和Web端差别还是比较大的,从实现角度来说,RN使用的路由库是react-navigation,并且原生的路由跳转本质上是一个堆栈结构,对标RN路由配置也能看出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// In App.js in a new project

import React from "react";
import { View, Text } from "react-native";
import { createStackNavigator, createAppContainer } from "react-navigation";

class HomeScreen extends React.Component {
render() {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<Text>Home Screen</Text>
</View>
);
}
}

const AppNavigator = createStackNavigator({
Home: {
screen: HomeScreen
}
});

export default createAppContainer(AppNavigator);

// 被createStackNavigator配置的组件props会被注入navigation对象,支持回退、跳转到具体路由等功能

  那么React Web端人员转到React-Native端进行路由page开发的时候可能遇到什么问题呢?

  这个问题也是直觉上的问题: 堆栈结构导致进行路由跳转后,之前的页面不一定会被卸载。这也就意味着我们的componentWillUnmount周期内的一些逻辑有可能不会被执行。所以在编写业务逻辑时要注意该问题。

状态管理 (State)

  这个倒是一致的,本质上是React生态的补充。reduxmobx等选择取决于你的开发团队的历史背景。

自适应(Responsive)

  在移动端H5应用中我们可以配置rem,根据设计稿及实际手机长宽比设置根font-size大小,统一以rem进行大小比例响应。当然有时候少部分机型可能有兼容性问题,我们可以通过原始的@media screen进行补充设置。

  RN端同样是通过进行GUI设计稿和手机屏幕宽高进行比例计算得到缩放系数再应用到实际布局中。具体是结合Dimensions.get('window')拿到可视窗口宽高计算。目前已知问题有:

  • Dimensions.get('screen')拿到的屏幕宽度不精确。
  • 部分安卓机有底部虚拟按键占用屏幕高度,并且该值无法精确获取。

Mock

  除了一些像roadhogumi框架自己封装好的本地代理Mock方案,自己处理的话,一种是直接引后端配置的远程mock数据,如Yapi;另一种就是自己手撸一个本地的mock服务(不推荐)。