React Native 在「元气阅读」的实践

avatar
前端工程师 @上海阅文信息技术有限公司

本文作者:阅文集团前端团队

原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 (id: yuewen_YFE) 获取授权,并注明作者、出处和链接。

前言

经历了三个多月的集中开发,阅文集团旗下二次元产品「元气阅读」APP 终于在各大应用商店上架了。「元气阅读」APP 大部分的功能模块基于 React Native 开发,整个开发过程前端团队趟了不少 React Native 的坑,同时也积累了不少实践心得,与大家一起分享。

一、业务背景与技术选型

在使用 React Native (以下简称RN)之前,和业界大部分团队一样, 我们 APP 的开发模式采用的是客户端(iOS/Android)内嵌 H5 的 Hybrid 开发模式。一开始,我们除了采用比较成熟的离线包方案管理静态资源,在首屏加载体验上我们也做了不少优化工作,但发现 H5 线上的体验和性能数据与原生还是有不少差距,所以我们决定引入新方案。

RN 和 Weex 已经是业界两个相对成熟的 Hybrid 解决方案,基本能满足我们的需求:

  • 用户体验:相比于 H5 页面,RN 和 Weex 在用户体验上有了很大的提升,体验几乎接近原生
  • 人力成本:相比于客户端,RN 和 Weex 的一套代码可以跑在 iOS、Android 两端,且代码重用率也较高
  • 灵活发布:RN 和 Weex 都拥有热更新能力

最终我们选择了 RN 作为解决方案,主要是考虑了几个因素:

  • 社区现状:相对 Weex,RN 的社区活跃度和 Facebook React 周边生态更好
  • 大厂背书:腾讯、京东、百度和携程都有大型产品在线上跑
  • 团队现状:早在 17 年上半年,阅文前端团队已经选型 React 作为我们前台产品线的主要研发技术栈,且大部分成员都能驾驭 React

二、应用场景

在「元气阅读」APP 中,使用 RN 开发的应用场景达到了 70% 左右。用户能看到的页面中,除了书架、注册登录和阅读引擎,其它模块几乎都是使用 RN 完成开发,「元气阅读」APP 已经属于国内大型产品中,超大规模的 RN 应用了。欢迎大家在各应用商店(iOSAndroid)搜索「元气阅读」下载体验。

▲小说书城

▲小说书城

▲漫画书城

▲漫画书城

▲元气圈

▲元气圈

▲漫画详情

▲漫画详情

▲排行榜

▲排行榜

▲分类

▲分类

三、导航管理

对于 RN 的开发,导航的前期规划十分重要,通常在搭建项目时就需要提前考虑。关于导航组件的选择,react-navigation 是个不错的选择,我们希望 react-navigation 能在业务场景更加通用。

1、统一跳转规则

Native 与 RN 互跳是最常见的需求。有了统一的 URL,只需维护一份 sitemap 和实现一个 open 接口,就可以很容易的在 Native 与 RN 中互相跳转。

react-navigation 是使用 routeName + params 的形式跳转的,所以需要在调用 router.getStateForAction 之前做一点调整:

// 修正 action: 允许 navigate/push/reset 动作传 url
if (isPushLikeAction(action) || isReplaceAction(action)) {
  if (isRouteUrl(action.routeName)) {
    // 使用 path-to-regexp 库来判断 url 对应的 routeName + params
    const route = parseRouteByUrl(action.routeName) 
    if (route) {
      action.routeName = route.name
      action.params = route.params
    }
  }
}

2、实现 404 跳转

在 Web 开发中,404 页面是一个很常见的逻辑,参照上面的方式, RN 可以这样实现:

// 修正 action: 当 navigate/push/replace 跳转到未知 routeName 时,调整为定义的 notFoundRouteName
if (isPushLikeAction(action) || isReplaceAction(action)) {
  // 修正 action: 提供 404 能力
  if (allRouteNames.indexOf(action.routeName) === -1) {
    const oldAction = { ...action }
    action.routeName = notFoundRouteName
    action.params = { action: oldAction }
  }
}

3、控制页面生命周期

在项目开发过程中,经常碰到这样的需求,回到原来页面之后要刷新原页面的数据,比如登录之后、进入详情页完成某操作之后回到列表页等。

「元气阅读」项目刚启动时 react-navigation 还是 0.x 版本,只能用 onNavigationStateChange + context 才能让页面感知 focus/blur 。1.x 版本之后,我们可以通过自带的 addListener 方法来监听 didFocus 或 didBlur 事件。

4、优化页面二次打开

「元气阅读」是一个以 RN 为入口的应用,在正常的使用过程中,需要频繁的从 RN 切换到 Native 或从 Native 切换到 RN,这样就会有多个 RN 页面(根组件),而第二个根组件在初始化的时候就需要定位到指定页面,所以和 Native 约定,通过 initialRouteUrlinitialRouteName + initialRouteParams 来告诉 RN 需要定位到什么页面:

const navigator = getActiveNavigator() // 需要全局维护一个 Navigator 的堆栈
let nextState = originGetStateForAction(action, state) // 调用原始的 getStateForAction 获取新的/初始化的状态

if (navigator) {
  const { initialRouteName, initialRouteParams, goBackOnTop } = navigator.props // 读取 navigator 的 props

  if (isInitAction(action)) {
    // 支持通过 initialRouteName & initialRouteParams 初始化到相应页面
    if (initialRouteName) {
      const initialActionPayload = { routeName: initialRouteName, params: initialRouteParams }
      const initialAction = NavigationActions.navigate(initialActionPayload)
      nextState = router.getStateForAction(initialAction, nextState) 
       if (!isTopNavigator() && nextState.index > 0) {
        // 非第一层 RN 实例且有两个页面的时候(前面 navigate 到了非一级页面),保留最后一个页面
        nextState = {
          ...nextState,
          index: 0,
          routes: nextState.routes.slice(-1),
        }
      }
    }
  } else if (isBackAction(action)) {
    // 在第一层页面,并且不是是第一个 Navigator,则调用 goBackOnTop 关闭 RN 
    if (isTopScren(state) && !isTopNavigator( ) && typeof goBackOnTop === 'function') { 
      goBackOnTop()
      if (nextState === state) {
        // 防止 Android 的物理返回键导致退出 App
        nextState = { ...nextState }
      }
    }
  }
}

return nextState

5、状态本地存储

组件 react-navigation 在 2.x 版本新增了状态本地存储功能,在 reload 之后可以直接定位到之前的页面,但是需要注意两个点:

  • 在「元气阅读」这种多个根组件的业务场景,每一个根组件的 rootNavigator 需要有个标记区分,建议以索引区分
  • 在页面出错(红屏)之后,为了避免 reload 还是停在当前错误页,可以在 componentDidCatch 里面清除本地存储

四、状态管理与数据持久化

在「元气阅读」里,我们经常需要缓存用户的信息、浏览过的书详情信息以及用户收到的消息等等,这样用户在离线访问「元气阅读」时就能避免白屏或异常的情况,而且还可以实现“秒开”。

举个例子,当用户第一次打开书籍详情页的时候,把书书籍详情的信息缓存下来;第二次再打开的时候,就可以达到秒开的效果。秒开效果可以看下图:

▲跳转详情页

▲跳转详情页

我们选择 reduxredux-persist 搭配一起使用,来实现数据共享以及数据持久化缓存。

1、redux

选择用 redux 主要是实现数据共享的功能。通过 redux 单项数据流的特点,每一步操作都有迹可循,比较容易排查问题。

在写 redux 的时候,可能大家觉得会需要写很多样板代码。在这里推荐一下 redux-actions 这个库,能够帮助我们减少一些代码量。下面简单的举一下例子:

// 常见的写法
export default (state = {}, action) => {
 switch (action.type) {
  case INCREASE:
   return {...state, total: state.total + 1}
  case DECREASE:
   return {...state, total: state.total - 1}
  default:
   return state
 }
}

// 通过 handleActions 方法
import { handleActions } from 'redux-actions'

export default handleActions({
 [INCREASE]: state => {
  ...state,
  total: state.total + 1
 },
 [DECREASE]: state => {
  ...state,
  total: state.total - 1
 }
}, initialState = {})

2、redux-persist

redux-persist 会订阅 store,一旦 store 发生变化,就会触发存储操作。这样当我们操作 store 的时候,数据也就会更新到本地了。

在开发项目的时候可能会发现,我们在 store 中共享的数据有一些可能是不需要被缓存到本地的。比如说搜索结果页,因为每次搜索的关键字不一样,结果也是不一样的,这样的数据被缓存到本地就没有意义。那我们怎么来控制一些数据不被缓存到本地呢?

redux-persist 支持配置黑白名单,意思是只持久化白名单中的数据或者不持久化黑名单中的数据。这样就可以根据需求来配置黑白名单,从而决定哪些数据需要被缓存到本地,哪些数据不需要被缓存。例如:

import { createStore, applyMiddleware, combineReducers } from 'redux'
import { persistReducer } from 'redux-persist'
import thunkMiddleware from 'redux-thunk'
import storage from 'redux-persist/lib/storage'

const rootPersistConfig = {
 storage,
 key: '***',
 blacklist: ['***'] // 黑名单
}

const enhancer = applyMiddleware(thunkMiddleware)
export const store = createStore(persistReducer(rootPersistConfig, rootReducer), enhancer)
export const persistor = persistStore(store)

五、性能优化

A compelling reason for using React Native instead of WebView-based tools is to achieve 60 frames per second and a native look and feel to your apps. Where possible, we would like for React Native to do the right thing and help you to focus on your app instead of performance optimization, but there are areas where we're not quite there yet, and others where React Native (similar to writing native code directly) cannot possibly determine the best way to optimize for you and so manual intervention will be necessary. We try our best to deliver buttery-smooth UI performance by default, but sometimes that just isn't possible.

在 RN 文档里看到一段关于性能的解读,里面提到:「目前在某些场合 RN 还不能够替你决定如何进行优化(用原生代码写也无法避免),因此人工的干预依然是必要的」,我们确实在性能优化上花费了不少精力。

1、首屏优化

运行过 RN 项目的同学不难发现,我们第一次进入 RN 页面时会有一个短暂的白屏,快至几十毫秒,慢至 1 到 2 秒,白屏时间取决于终端的性能,在低端安卓机子表现最差,而且退出后再进入,仍然会有这个白屏。我们实施了几个优化策略:

1)预加载 Bundle

在客户端启动时,就开始对 RN 的 bundle 进行预先加载,我们发现这样操作后,白屏操作的时间缩短了不少,特别是安卓设备。但这还不是最完美的,我们仍然会看到很短暂的白屏。

2)优化闪屏逻辑

由于大部分 APP 一定是先有闪屏,然后才进入首页。我们完全可以利用这个业务场景,让 RN 程序躲在闪屏下加载,直到加载完毕,通过 Bridge 通知客户端把闪屏关闭,这样就比较巧妙地解决了白屏的问题。

▲before
▲before

▲after
▲after

2、交互优先

当 JavaScript 线程中同时做很多事情时,很容易就会导致线程掉帧,表现为页面卡顿、动画切换缓慢,我们可以使用“交互优先”的原则去做优化。

1)优先执行用户可感知的操作:如页面场景切换

例如,页面转场这个场景。我们就可以把页面逻辑放在 InteractionManager.runAfterInteractions 的回调中执行,这样可以优先保证转场动画的执行,然后才是我们的页面逻辑,很好的规避了转场卡顿的问题。

2)初始化页面尽量渲染少量组件

当我们呈现一个页面给用户时,一定是要在最短时间内让用户感觉到页面已经展现完毕了,所以我们在初次展示页面时,可以优先显示固定的占位信息,配合 loading 或骨架图布局不确定的部分,与此同时我们才在背后默默的发起请求(碰到复杂页面,则可拆分多个异步请求),总之整个过程是先保证页面可见,再逐步完整。

3、长列表优化

▲组件的子树
▲组件的子树

这是一个组件的子树。对其中每个组件来说,SCU 表明了 shouldComponentUpdate 的返回内容,vDOMEq 表明了待渲染的 React 元素与原始元素是否相等,最后,圆圈的颜色表明这个组件是否需要重新渲染。

在 React 中如果只是一次这样的组件子树渲染,并不会有太大的性能问题。但如果对于分页长列表这种需要成百上千次的渲染场景,会花费很大的开销在 vDOM 的生成和 Diff 上,而这也直接导致了长列表在 RN 中严重的性能问题。那我们需要做些什么加以改进呢?先来看看这张组件更新渲染的流程图:

▲组件更新流程
▲组件更新流程

当一个组件的 state 或者 props 改变时,就进入了生命周期函数 shouldComponentUpdate,而当 shouldComponentUpdate 返回的是 true ,就会调用 render 方法生成 Virtual Dom,随后和旧的 Virtual Dom 进行比对,最终决定是否更新。所以从中我们明显地看出 SCU 和 Virtual Dom 的 Diff 是影响 Dom 更新的关键所在,为此我们分别针对这两点做了优化:

1)控制好 shouldComponentUpdate 的更新逻辑

从上图也可以看出如果 shouldComponentUpdate 返回的是 false,那程序就可以直接跳过生成 Virtual Dom 以及之后的 Diff,这对于一个大列表的场景是相当可观的优化,例如目前我们有一个 1000 条数据的列表,在下拉加载 20 条新数据时,如果没有利用 shouldComponentUpdate 进行控制,会把之前的 1000 条数据也 render 一遍,而在 shouldComponentUpdate 中控制好更新逻辑,就只需要 render 最新的那20条,是不是很大的提升!不过使用 shouldComponentUpdate 要格外小心,你一定要考虑到所有影响更新的逻辑。不然会出现真正需要更新的时候却也没能更新。

来看一个具体的例子,场景是 APP 中的分类列表页,我们在每一个列表项的 render 中打印 log,统计进入 render 的次数。首先来看看 shouldComponentUpdate 不做任何处理的情况,也就是 shouldComponentUpdate 始终返回的是 true:

shouldComponentUpdate (nextProps, nextState) {
  return true
}

▲before
▲before

再看看我们在 shouldComponentUpdate 中以图片的 uri 地址过滤掉不必要的渲染项之后的情况:

shouldComponentUpdate (nextProps, nextState) {
  if (nextProps.imgSrc.uri === this.props.imgSrc.uri) {
    return false
  } else {
    return true
  }
}

▲after
▲after

从图中左边的控制台很明显的看出,过滤后不论加载到哪一页,都只是渲染最新的20条,减少了大量不必要的渲染。再比较一下在相同条件下两者加载一千条数据的时间:

结果也是显而易见,而且在操作过程中发现未使用 shouldComponentUpdate 的情况下,越往后会越慢,到 1000 条数据时,再加载新数据所要等待的时间简直无法忍受。

2)在数组遍历时,增加唯一标识的 key 值

如果更新是不可避免的,那只能想办法去提高 Virtual Dom 的 Diff 效率。我们可以在遍历数组时给每一项加上唯一的 key 值,这样在 Diff 阶段,可以准确知道要操作的子组件,提高 Diff 的效率。

4、动画优化

合理运用动画对于 APP 的体验提升有很大帮助。但我们在应用动画时发现在有些场景会出现卡顿、掉帧的现象,本质原因是由于 JavaScript 是单线程的,如果线程中在跑一些比较重的任务,就可能会对动画的性能出现影响。下面介绍几种办法,把动画这件事尽量交于原生:

1)使用 LayoutAnimation

针对一次性动画,建议使用 LayoutAnimation,它利用了原生的 Core Animation,使动画不会被 JS 线程和主线程的掉帧所影响。

2)使用 setNativeProps

setNativeProps 方法可以使我们直接修改基于原生视图组件的属性,而不需要使用 setState 来重新渲染整个组件树。避免了渲染组件结构和同步太多视图变化所带来的大量开销。

3)使用原生驱动的方式

在 Animated 动画设定中,添加 useNativeDriver 字段,并设为 true,这样就可以把动画的执行交由原生处理。

六、发布更新

如今由于互联网高速传播的特效,事物发展的速度越来越快,产品快速迭代、试错的能力就显得尤为关键,作为开发者,对我们的挑战就是如何让开发完成的功能快速上线,下面来看看我们是怎么做的:

1、发布

我们选择 Jenkins 作为自动化部署方案。通过配置在 Jenkins 中打包脚本来实现自动打包,把 RN 的 bundle 包打到指定的位置,这样就不用每次打包之前再手动打包了,大大提高了效率。

2、热更新

由于 Native 端发布一次新版本的成本比较大,RN 的热更新能力就成为了很大的亮点。只需要把最新的 bundle 包发布到服务器,就能够让用户手中的 app 自动下载远端的 bundle 包,然后无感知的更新,可谓是特别的方便。

我们经过调研,最终选择了微软的 CodePush。它提供给 RN 和 Cordova 开发者直接部署移动应用更新给用户设备的云服务,而且还开源了 RN 版本。具体接入的教程可以查看官方网站,这里就不一一赘述了。下面主要讲几个需要注意的点:

1)注册 app

在 CodePush 上注册 app 的时候,需要区分 iOS 和 Android,例如 appName-iosappName-android,在发布的时候需要在不同的平台分开发布。

2)key 的配置:Staging 和 Production

在注册 app 的时候,会返回一套 deployment key,分别为 Production 和 Staging 环境(后续也可以自定义 deployment key 名称),在集成 CodePush SDK 的时候会用到。Production 对应生产环境的 key,Staging 对应测试环境的 key。这样就可以分别更新不同环境的包。如果想要查看 app 的 deployment key 表,可以使用下面的命令:

code-push deployment ls <appName> -k

3)RN 接入 CodePush

RN 端接入 CodePush 非常简单,只需要在根文件中加入几行代码就可以了。CodePush 传参的时候可以根据环境的不同做不同的配置。代码大致为下面这样:

import React, { Component } from 'react'
import codePush from 'react-native-code-push' // 引入 codePush
       
  const codePushOptions = __DEV__ ? {
    updateDialog: true, // 显示更新弹窗
    installMode: codePush.InstallMode.IMMEDIATE // 立即更新(会打断用户操作)
  } : {
    // 下次 app 从后台切换到前台时检查更新,并下载最新的包
    checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
    // 下次重启的时候更替换成最新的包
    installMode: codePush.InstallMode.ON_NEXT_RESTART
  }
       
  @codePush(codePushOptions)
  export default class App extends Component {
    render() {
      ...
    }
  }

checkFrequencyinstallMode 是可配置的,具体的配置可以根据需求来决定。

4)版本控制

在热更新的时候需要控制版本号,默认是当前安装包的版本(三位数版本号),如果需要指定版本号的话,可以在执行热更新命令的时候加上 -t,后面跟需要更新的版本号就行了。

七、异常监控

我们借助了腾讯 Bugly 平台进行线上异常的监控。Bugly 平台能为开发者提供异常上报与运营统计功能:

  • Bugly 会上报运行错误、崩溃和卡顿的异常,并提供相应的数据统计和告警机制,使我们可以尽可能快地感知到线上异常,掌握用户侧整体的运营稳定性和流畅度的情况;
  • Bugly 平台提供日志上报功能,可以协助定位问题;
  • Bugly 平台也可以针对版本,机型,系统,来对比异常数据的变化。

例如,下图是对 Crash 率的统计:

Crash 还可以根据系统、设备和 APP 版本等维度来细化分析。

还可以统计最影响用户的 Top 问题:

八、一些坑和小贴士

在几个月的开发过程中,我们遇到了不少坑,也发现了一些好用或者没有被注意到的小技巧,下面和大家分享其中的一部分:

1、坑

1)Image 组件在 Android 上潜在的内存泄漏 Bug

在安卓中,加载一张尺寸远大于容器的图片,内存会突然猛涨,在这张图上下滑动,程序就直接因为内存不足而崩溃了如何解决呢?其实办法也很简单,只需要设置 Image 组件的 resizeMethod 属性为 resize 即可,如下图:

▲Image的resizeMethod属性说明
▲Image 的 resizeMethod 属性说明

2)使用 InteractionManager.runAfterInteractions 时的注意事项

我们知道 InteractionManager.runAfterInteractions 的回调是需要完成动画后才执行,我们的程序中发现过这样一个的 bug,在点击某个按钮后,就怎么也进不到 runAfterInteractions 的回调中。经过排查,原来是我们执行了一个无限循环的动画(loading 效果),并且没有关闭,所以就永远进不到 runAfterInteractions 的回调了。所以大家在开发中碰到循环动画要注意处理。

3)使用 FlatList 列表出现页面跳动问题

FlatList 有一个叫 getItemLayout 的优化属性,如果你是个定高的列表项,设置这个属性可以大大提高列表渲染的效率。然后我们遇到的问题是,在高度不确实的时候,也设置了这个属性,导致最终渲染时实际高度和我们预设的值不一致,出现了跳动。所以,如果不确定高度,千万别设置 getItemLayout 属性。

▲滑动不顺畅,会发生跳动
▲滑动不顺畅,会发生跳动

▲正常滑动
▲正常滑动

4)短时间内重复点击出现多个相同页面的问题

这不单单是 RN 的问题,各端应该都无法避免。所以通常在各种技术栈的导航库中都对此进行了修复,我们刚开始的预期就是 React Navigation 在内部肯定解决了这个问题,但发现实际上并没有。于是我们就对 React Navigation 的跳转做了一次增强,思路是判断下个路由的地址和上个路由一致,那就不予处理:

▲重复跳转
▲重复跳转

▲解决后
▲解决后

function isInCurrentState (state, nextState, routeName) {
  if(nextState && nextState.routeName === routeName && !deepDiffer(state.params, nextState.params)) {
    return true
  }
  if(nextState && nextState.routes) {
    return isInCurrentState(state.routes[state.index], nextState.routes[nextState.index], routeName)
  }
  return false
}

const nextState = originGetStateForAction(action, state)

// 避免重复跳转
if (nextState && action.type === StackActions.PUSH) {
  if(isInCurrentState(state, nextState, action.routeName)) {
    return state
  }
}

2、小贴士

1) iOS 模拟器中你可能不知道的两个选项

  • 打开虚拟键盘:我们在开发输入相关场景时,iOS模拟器默认未开启键盘,需要在 HardWare->Keyboard->Toggle Software Keyboard 进行开关;
  • 慢动画开关:很多同学碰到这个问题,不知道点了哪个按键后,模拟器中的任何操作都得比无比缓慢,各种重启、清缓存都无效。这是由于我们不小心触发了快捷键,打开了慢动画模式,可以在 Debug->Slow Animations 中关闭(快捷键是 command+T)。

2)原来 RN 和原生的通信也可以是同步的

我们知道 RN 和原生的通信是异步的,但如果是一些全局的常量(环境变量、版本信息等),其实可以以同步的方式在启动 RN 时直接挂在 NativeModules 上,这样使用起来就很方便。

3)Image 组件一些值得关注的属性

  • defaultSource(iOS Only):正常我们要实现一个默认图功能,需要先给图片设置默认图链接,然后在图片下载成功的回调里再改变状态,替换默认图。这个属性就帮你做好了这些,可惜的是只支持 iOS。
  • getSize:当我们要获取图片的宽高,然后再处理图片相关逻辑,就可以用这个 API。
  • prefetch:对图片强制缓存。
  • queryCache:这个 API 可以获取到图片是否缓存,如果已缓存,则下发是在硬盘还是内存。对于要处理一些缓存逻辑还是很有用的,不过要注意的是虽然官方没有标注 Android Only,我们只在 Android 获取成功过,iOS 并没成功。

4)Text 组件里一些值得关注的属性

  • allowFontScaling(iOS Only):这个属性用来控制是否跟随系统字体大小。如果你的APP布局会因为设置字体而失控,可以考虑开启,不过此属性只支持 iOS,安卓需要其它方法解决。
  • selectable:这个属性可以用来开启文本的复制、粘贴功能。

5)FlatList如何实现一行多列

FlatList 提供了一个叫 numColumns 的属性,你只需要设置一行的列数,便可轻松实现一行多列的布局如下图:

▲一行三列的布局

▲一行三列的布局

6)调试工具

推荐使用 react-native-debugger,它集成了 Chrome 的 DevTools 以及 react-devtools ,还支持 Redux 的相关调试,可以说是很强大了。

7)性能检测

可以通过客户端自带的软件进行性能检测。iOS 推荐 Xcode 自带的 Profile;Android 推荐 Android Studio 自带的 Android Profiler

九、总结

虽然 RN 目前还存在着一些不足,但通过「元气阅读」项目实践,结果证明在人力、性能和效率上,RN 是符合我们预期的。对于 RN 在业务场景的最佳应用,我们也总结了几点:

  • 重运营场景:有运营需求的场景,适合用 RN 实现,如书城页、福利页等
  • 快速迭代场景:功能未健全,产品需迭代试错的功能场景,适合用 RN 实现,如元气圈、小说书城、漫画书城等
  • 固定信息展示场景:固定内容的信息展示页面,适合用 RN 实现,如排行榜、本大人、一级分类页等
  • 长列表场景
    • 由于 RN 列表的渲染机制限制,图+文长列表里有大量未知尺寸的图片,不太建议 RN 实现,如类似元气圈、微信朋友圈场景,暴力滑屏列表有几率出现闪白;
    • 大量已知尺寸的图片长列表、纯文字长列表,性能还可以接受

一个页面用 Native 还是 RN 来实现,除了考虑各端团队人员配比,业务场景也是一个重要的考虑因素。譬如新项目中,作品详情页用 Native 或 RN 实现都能达到验收目标,但考虑到作品详情页产品场景已经很成熟,且有不少模块与核心阅读页有较多的交互,对体验要求也特别高,我们与终端团队一致选择 Native 来实现。

写在最后

近期 Airbnb、Udacity 团队纷纷表示弃用 RN,笔者认为大家大可不必为此忧心忡忡。Airbnb 列举的条例,其中不少项是可优化,或者结论是有待考究的;另外一些也有公司内部自身存在的问题。最近 Facebook 团队宣布正在努力打造一次大的升级,其中提到的对线程模型、异步渲染和桥接的优化方向,也让我们十分期待,我们有理由相信 RN 的未来会更好,也希望能通过这篇分享有更多的同学加入 RN 的大家庭,共同打造更好的 RN 生态。

更多分享,请关注YFE: