Duan Hong +

RN 之旅

入职微影的近两年里,一直在 react 的阵营中耕耘:团队内部开发了 pepper (类似 create-react-app 的自动化工具)、公共组件 vision-ui ,并先后协同其他团队成员,完成 M 站微信端演出票微信端电影票 等诸多项目的重构工作,也取得了不错的成绩。

如今 react 日益壮大, react-native 逐渐成熟、react-vr 也踏上历程, 因而在 react 方向的横向扩展,也就成了团队接下来的研究方向。

终于,16 年的下半年,迎来了拥抱 react-native 的机会。

注:下文中简称 react-native 为 RN

背景介绍

娱票儿票房分析是我司出品的一款电影数据类应用,为电影爱好者和从业者提供专业、精准、实时的行业数据查询工具,内容包括实时票房、预售票房、上座率、全国排片、城市排片、影院排片、观音人次、影院排期、电影票房排行榜等。 — Apple Store 介绍

票房分析

原票房分析 app 属于混合模式移动应用(Hybrid App), 即在 Web App 上包一层 Native 的壳子,从而兼备 Native 和 Web 的一些优势:

但另一方面,也意味着在体验和性能上的妥协。考虑到本次重构的目标:

怎么实现上述目标成了首先要解决的问题?

改版方案

针对上面提到的重构目标,内部研究了两种方案:

方案确定后,重构的任务自然而然的归结为以下几个方面

迁移原有 Native 的功能

原票房分析 的 Native 功能主要为:

通过与客户端协商好的 URL Schema 协议进行交互。RN 方面,提供了 Native Modules 作为 Native 功能的延伸,JS 可以直接调用 Native Modules 中对外的接口,以应对 RN 中缺少的、高性能、多线程的一些场景。这点,在整合原有 App 功能上极为方便,也赋予了混合 RN App 的能力,即在原有 Native 中整合 RN 的过度方案。

图表渲染

由于原 web 图表基于 echarts 的缘故,RN 版的 react-native-echarts 成了目前最适合的方案。虽然该方案是基于 webview 实现的图表,但能将性能和体验的降级的控制在图表展示上,且完全兼容旧版 API ,还算完美。

RN 版的其他实现,大部分比较基础和简单。也有一些基于 Native 封装的方案 看起来也挺丰富,但在需求匹配上不如 react-native-echarts 适合。

热更新

它解决了 Native 开发最受诟病的问题,具备了 Web 快速迭代的能力。

CodePush is a service from Microsoft that makes it easy to deploy live updates to your React Native app. If you don’t like going through the app store process to deploy little tweaks, and you also don’t like setting up your own backend, give CodePush a try. — react-native offical site

说到它,不得不提两句前阵子的 Apple警告邮件 事件,一时让「 热更方案岌岌可危 」。对 RN 来讲,虽然是躺着中枪,但开发者对事件的反应来看,没有热更的日子简直不敢想 😂。

项目结构设计

在项目结构上,延续了 react 项目的经验:

project design

第三方依赖方面,与 react 的差异主要在 UI 上,*-react-native-* 都是针对 RN 的,而 redux 全家桶两个项目间共享。如若再移植一套 web 方案,只需提供一套 web 端的 UI 方案( react-native-web 更进一步,兼容 web 的做法为移植提供了更多的便利,观望中…)。

名称 说明
react  
react-native  
热更新  
react-native-code-push 热更新方案
UI  
react-native-extended-stylesheet 样式,可拓展,支持全局变量
react-native-scrollable-tab-view 可滚动 Tabs
widow-ui 公共组件库
native-echarts ECharts 图表库
react-native-swiper 开屏引导插件
react-native-vector-icons Icon
国际化  
react-native-i18n 国际化支持
统计  
react-native-google-analytics-bridge ga 统计
react-native-talkingdata talkingdata 统计
redux 全家桶  
redux  
react-redux react 集成
redux-actions 便捷的 action 创建处理
redux-logger action 日志
redux-saga 异步 action,基于 Generator
reselect mapStateToProps 的优化再封装

插件安装:在安装 RN 插件时,有些要安装完毕后,还要执行下 react-native link xxx-xxx。它们的区别主要在于是否有依赖封装的 Native Modules 模块,react-native link 所做的就是把依赖的 Native Modules 模块配置到 ios 和 android 项目中。这个操作会对 Native 的代码做些修改,具体的改动部分可以参考相关插件的手动配置说明(但建议大家也熟悉一下,卸载的时候会方便些 [捂脸] )。

万事俱备,让我们进入实战环节,Let’s do it

实战演练

实战部分,会提到开发中常见的一些问题,还有些代码上的优化,主要涵盖以下方面:

调试

以 IOS 调试为例,在当前 app 中,使用快捷键 + D 即可调出调试选项(摇一摇就不太好使了吧~)

debugger react-native app

为便于开发调试,推荐开启以下选项

名称 说明
Remote JS Debugging 在 Chrome 中查看 log 信息,👍
Hot Reload 保留应用状态的即时刷新,告别 + R

如果你也和我一样,对黄色的警告信息简直不能忍受:

console.disableYellowBox = true; 

红屏(redbox)的错误就不能忽视了。需要注意的是,不管红屏还是黄屏都不会在正式发布的 app 里出现,当然,红屏的话应用就直接闪退了。

RedBoxes and YellowBoxes are automatically disabled in release (production) builds. — react-native official site

补充一下:开发的大部分场景下,都不需要 Xcode 或者 Android Studio,尽管使用自己喜爱的 IDE 就好了。

$ react-native run-ios # 会自己调出 IOS 模拟器
$ react-native run-android # Android 模拟器需要手动启动

组件

首先说下第三方组件的引用,以 react-native-vector-icons 为例:

$ yarn add react-native-vector-icons # 下载
$ react-native link react-native-vector-icons # 配置 native 包引用

第一步安装组件时都会用到,第二步只针对依赖 native 实现的组件。该 Icon 组件实现依赖 Native Modules ,所以需要将对应 native 的实现部分配置到对应的 native 项目中(ios 和 android 目录下都会有相应修改),之后就可以像 RN 内置的组件那样使用了( RN 组件在项目初始化的时候就已经配置好了)。

下面大致介绍下,如何用 JS 实现一个服务费组件:

import React, { Component } from 'react';
import { View, Text, Switch, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  fee: {
    flexDirection: 'row',
    justifyConent: 'flex-end',
    alignItems: 'center'
  },
  text: {
    color: '#333',
    fontSize: 12
  },
})

export default class Fee extends Compoennt {
  static defaultProps = {
    disabled: false,
    value: true,
    onChange: () => {},
  }
  render() {
      const { disabled, onChange, value } = this.props;
    return (
        <View style={styles.fee}>
            <Text style={styles.text}>
            服务费
            </Text>
            <Switch
                disabled={disabled}
                onValueChange={onChange}
                value={value}/>
        </View>
    )
  }
}

将文件保存成 Fee/index.js 即可

├── Fee
│   └── index.js

需要注意的是,由于 Switch 的实现基于原生组件,对应平台的 UI 样式会有些差异:

switch difference

针对平台性的差异,RN 官方推荐了两种做法

依赖上述方案,可以做到与系统 UI 风格的协调统一,当前,完全保持一致也是可以的。对于 UI 细节的调整,就要使用到 StyleSheet 了,也就是接下来要讲到的样式。

样式

StyleSheet 基本类似于 web 平台的样式方案 CSS in JS ,类似的原因在于,这个是面向 native 组件的,差异还是有的,这里是完整的支持列表。没有了选择器和 media query 的干扰,CSS in JS 的方案用在此处还是比较可行的,至于公共样式的提取,react-native-extended-stylesheet 处理的还算完美。

import ESS from 'react-native-extended-stylesheet'

// define global style, color or fontSize
ESS.build({
  'color-yellow': '#ff5200',
  'color-blue': 'blue',
  'size-small': 12,
})

// usage in StyleSheet
const styles = ESS.create({
  button: {
    color: '$color-yellow',
    fontSize: '$size-small'
  },
  active: {
    color: '$color-blue'
  }
})

<Button style={styles.button} title='OK'/>
// combine multiple styles
<Button style={[styles.button, isActive ? styles.active || {}]} title='OK'/>
  
// usage in js
<Icon size={28} color={ESS.value('color-yellow')} name='share'/>

其他的暂且放下,今天只想说 Flexbox 。web 平台布局方案 Flexbox 的引入,使得 web 到 RN 的过度简直完美,背后的 yoga 功不可没。

更新推送

如果代码的更新不涉及 native 部分(iOS 或者 android 目录)的修改, code-push 是不二之选。

参考 react-native-code-push 官方文档,集成使用甚为便捷:

import codePush from "react-native-code-push";

@codePush({ checkFrequency: codePush.CheckFrequency.ON_APP_RESUME }) // check updates at every resume
class MyApp extends Component {
  ...
}

发布更新的环节:

$ code-push login # 设置好账户信息
$ code-push app add xxx # 创建 app

# 发布命令
$ code-push release-react [app-name] [platform] [options]

# release demo
$ code-push release-react deadpool-ios ios -m -t 3.4.4 # 针对 3.4.4 的 iOS 推送强制更新
# 默认推送为 Staging 状态,相当于预上线,测试完毕后可切换到正式版本
$ code-push promote deadpool-ios Staging Production

当然,更新的回滚也是支持的

# Release a ios production rollback
code-push rollback deadpool-ios Production

更多选项请参考官方文档

小结

关于页面跳转的卡顿问题,利用 InteractionMamanger 的收效显著

import { InteractionMamanger } from 'react-native';

// heavy jobs 
InteractionManager.runAfterInteractions(() => {
  // api request or page navigation
})

对于 ListView 的性能问题,在 43 以后的版本中用 FlatList 解决

Redux 方面,web 到 RN 都是通用的。RN 版中,尝试了 redux-saga,基于 Generator 实现的异步 action 方案,比 redux-thunk 更具灵活性。在 saga 里一个 action 的处理流程被拆分4 部分:

以上便是 saga 处理 action 的过程,完整的 reqestMovies 如下:

function* requestMovies({ params }) {
  // fetch previous movies state from store
  const originalMovies = select(store => store.movies)
  if(originalMovies.length) {
      // using cached movies
    put(receiveMoviesAction(originalsMovies))
  }
  try {
    // send http request to fetch movies
    const moviesData = yield call(GET, API_MOVIES_URL, params);
    if(moviesData.ret === 0) {
      put(receiveMoviesAction(movieData.movies))
    } else {
      put(receiveMoveisFailedAction())
    }
    // hide loading state
    put(hideLoadingStateAction())
  } catch(err) {
    // process error state
  }
} 

对比 redux-thunk , saga 使得 action 更加存粹,业务处理逻辑全部拆分到 saga 的各个流程当中,易于多流程的组合和测试(官方示例的登录操作

总结

ready to release

Learn Once, Write Everywhere

web 到 RN 的平滑过度,是该理念的有效证明。Redux 模块和 Flexbox 的复用,将差异集中缩减至 UI 环节,性能敏感部分又可切换至 Native 实现,更加坚定了对 RN 未来的信心。

react-vr 的开源,似乎宣告了 VR 时代的来临,你准备好了吗?

Wanna say something ?

Blog

Friends