58车商通RN落地与实践

导语

本文从RN的简介到在车商通落地,从宏观设计到深层次分析其通信原理,再到热更新的进阶设计三个方面,阐述了React Native在车商通中的设计以及流程

背景

58车商通

58车商 通是为车商打造发布及收车卖车、CRM管理、商机线索、库存管理、全网同步、营销推广功能等全方位SAAS服务的移动端APP。

主要功能包括:

1. 发布车源(核心功能)帮助车商快速发布车源,和主App 58同城起到相辅相成的作用。 需求变化比较频繁,开始采用原生开发,但是无法满足需求的频繁变化,于是2017年改版为Hybrid。 后续考虑改版为RN     

2. 库存管理(核心功能)帮助车商高效的管理自己的车辆,包括已经发布的,已经售出的,已经下架的,等等,以及定价,预警等相关功能。 需求变化相对稳定。

3. 客户管理(核心功能)帮助车商管理自己的客户,区分出客户的购买意向程度,以及后续沟通等等。 变化频率相对稳定

4. 营销推广(核心功能)帮助车商推广车辆,包括置顶,刷新服务等,变化频率相对较高

总结: 58车商通是58集团二手车部门的一个重要的App产物,帮助车商方便管理自己的车源,随时发布,同步其他市场。 而 RN 模块第一次尝试放在了车商通的每日任务模块。

为什么会选中每日任务模块

每日任务模块: 用户每日登陆之后可以通过每日的任务来赚取积分。 用于推广等功能等。

那我们为什么要用每日任务模块来做呢。

首先, 每日任务模块体量较轻,入手起来相对比较简单

其次, 线上有成熟的h5,如果出现严重问题的话,我们可以及时切换到线上h5进行补救

然后, 每日任务模块虽然量轻,但是可以覆盖协议的大部分功能。

最后就是这个模块需求变化相对来说比较快,也可以检验热更新模块。

接下来我们来聊聊RN。

引言

开发已经经历了几个阶段,从Native App 到 WebApp大火,再到苹果公司禁Web,又发展到了Hybrid的Web与原生共生。 再到React Native,这种利用Js 转成原生的取中方案,本文将就58车商通介绍下RN在其中的应用场景,以及发展阶段。

RN特点

Learn Once,Write AnyWhere.

如下图: 我们可以清楚的看到RN是构建在React和JSX的基础上的。

使用RN的优点及不足

一个新的挑战: 如何将开发成本和用户体检做好更好的平衡呢?

上面已经说了简单说了一些RN开发的背景,接下来简单介绍下RN的特性及优缺点

RN特性:

a) 提供了原生控件支持

使用RN可以使用底层原生控件,iOS可以使用UITabBar、   

UINavigationController等标准的iOS平台组件; 在Android平台我们可以使用   

Drawer控件; 这样,就让我们的App从使用上和视觉上拥有像原生App一样的验

b) 异步执行

所有的JavaScript逻辑与原生平台之间的所有操作都采用异步执行模式,原生模

块使用额外线程

c) 触屏处理

RN引入了一个类似于iOS上Responder Chain响应链事件处理机制的响应体系,  

并基于此为开发者提供了诸如TouchableHighlight等更高级的组件,实现了高性 

能的图层点击与接触处理

但是,说到底RN毕竟也是前端基于JSCore通过Runtime机制或者反射机制来和Native端进行交互,所以RN还是有缺点存在的

RN缺点以及不足:

1. 首先来说现在业内对于React Native的项目更新还是个很大的问题,58车商通现在使用的版本是0.53,如果我们想升级RN那必须发版,并且基础库以及各个业务模块可能都会受到影响。

2. React Native 的列表性能较差(不是滑动效果,是内存占用方面),如果cell 很多,那容易崩溃,原因是RN的list控件不像Native 端有重用机制,不过现在业内借鉴native的重用机制的思路也有解决方案,也是目前我们的一个优化方向

3. React Native 的调用基于JSCore 和 Runtime的消息转发机制,所以调起Native的组件,仍然是有一定的延迟,这个也是RN的一个瓶颈或者说上限,这也是为什么大家会说RN的效率是非常接近原生,但是远远高于WebApp的原因。

4. React Native 和Native 端是异步调用,所以有些操作必须要屏蔽用户操作行为,不过这个问题目前带来的问题影响很小 

其实,综上来说,React Native 的开发总体上还是利远大于弊,而且,热更新对于App的Native开发,尤其是 iOS开发来说,价值很大,所以我们选择落地RN项目,并实际运用到车商通中来。

58车商通RN模块整体技术架构

车商通RN整体分为三个部分:

- 客户端: 提供部分基础组件、提供交互协议、支持UI渲染展示页面

- 服务端: 提供业务接口、提供项目bundle下载地址

- 热更新平台: 提供项目bundle文件、支持更新、回滚

58车商通客户端设计和开发

客户端整体框架如下:

1. 入口组件

每个应用程序都有一个对应的入口文件,前端页面的开发项目目录如下:

index.js是Android和iOS渲染前端UI的统一入口

import { AppRegistry } from "react-native";

import App from "./src/index"; //业务口

AppRegistry.registerComponent("入口名字", () => App);

把当前APP的前端UI对象注册到AppRegistry组件中;

- AppRegistry 是运行所有 React Native 应用程序的 JS 入口点

- 应用程序入口组件需要通过 AppRegistry.registerComponent 来注册它们自身

- 当注册完应用程序组件后,Native就会加载jsbundle文件并触发 AppRegistry.runApplication运行应用

2. RN通信机制

这部分内容,其实网上的资料比较多,而我们结合源码,和已经一些已经公开的知识点,简单跟大家分享一下。 后续如果大家感兴趣,可以在单拿出一篇文章来分析讨论。 下面我们来简单看下RN从JS 端开始调起原生方法的原理(以OC为例):

OC生成一张模块配置表,包含所有模块和模块里的方法,根据特定的标识宏(RCT_EXPORT_MODULE()),将可以暴露的方法暴露给JS。

OC-JS交互流程: (注: 此图是网上的示意图,但是表达的意思很明确,所以借用一下)

1. js调用OC模块暴露出来的方法

2. 把调用方法分解为ModuleName、MethodName、arguments,再丢给 MessageQueue处理

3. 把js的callback函数缓存在MessageQueue的一个成员变量里面,同时生成一 个CallbackID来代表callback; 在通过保存在MessageQueue的模块配置表 把ModuleName、MethodName转成ModuleID、MethodID

4. 把ModuleID、MethodID、CallbackID和其他参数传给OC JavaScriptCore)

5. OC接到消息,通过模块配置表拿到对于的模块和方法

6. RCTModuleMethod对js传过来的参数进行处理

7. OC模块方法执行完,执行block回调

8. 调用第6步中RCTModuleMethod生成的block

9. block带着CallbackID和block传过来的参数去掉用js里的MessageQueue方法 invokeCallbackAndReturnFlushedQueue

10. MessageQueue通过CallbackID找到相应的js的callback方法

11. 调用callback方法,并把OC带过来的参数一起传过去完成回调

以上就是通信交互整个流程,但是在实际业务开发中我们发现,RN提供的基础组件已经不能满足我们的业务开发,部分需要依赖native原生功能来实现比如: 模块间的跳转、分享等,这就需要我们与native底层约定一些交互方法来满足各种各样的业务场景,基于此前端封装了一个中间交互的协议层。

3. 58车商通RN交互协议设计

协议层是native和前端对NativeModules进行了一些约定的封装和处理,方便业务使用。

目前前端协议层通过一下几种类型类集中封装的:

RN跳转类:

- 调起Native组件类 如: loading、toast等

- 调起native功能类 如: 分享、埋点、定位等

协议约定:

1. native封装模块宏CSTRNComponent到NativeModules下

2. 在模块宏CSTRNComponent下声明函数宏CSTRNHandler

3. 函数固定传参两个,第一个是协议交互的所有参数param(jsonString类型),第

二个固定传入callback函数

4. 参数param格式

param = { action, params };

也是固定两个参数

action:唯一确定调起的native组件,都是以CST开头的 如:action = "CSTLoadPageRNWeb"表示RN跳转H5

params:协议交互参数,每个协议都有不同的参数,具体协议的参数和native约定 如:

跳转H5协议传参

let params = {

url, //h5链接

jumpParameter, //参数

isDestoryBeforePage, //当前页面是否销毁关闭

title, //页面titl

...other

};

5. 回调函数

CSTRNHandler的第二个参数就是协议的回调函数,传入function类型

固定接收两个参数

(error, event) => {

第一个参数error是一个错误对象(没有发生错误的时候为 null

第二个参数event是native返回给前端的具体回调数据

(jsonString类型)

}

6. 前端封装

根据以上约定规则,下面来看一下前端具体的协议封装,以跳转H5页面为例

const nativeBridge = NativeModules.CSTRNComponent.CSTRNHandler;

function loadPage(

{ url, isDestoryBeforePage = 0, jumpParameter, title, ...other },

disappearCallBack = (error, nativeData) => {}, //回调函数

action = "CSTLoadPageRNWeb"

) {

let params = {

url, //h5链接

jumpParameter, //参数

isDestoryBeforePage, //当前页面是否销毁关闭

title, //页面title

...other

};

const error = checker(

arguments,

[

{

url: "s|r",

jumpParameter: "o",

isDestoryBeforePage: "n",

title: "s"

},"f","s"

],"loadPage"

);

// 线上环境报错不调起 Native 的 API

if (error === "error") {

return error;

}

let paramToNative = { action, params };

nativeBridge(JSON.stringify(paramToNative), disappearCallBack);

4.58车商通-RN UI页面开发

在上面我们大概了解了RN APP中的启动流程和交互方式,那么如何应用要我们的日常开发中能,下面来介绍一下前端的UI的开发。

1、本地开发流程

为保证RN版本的匹配,和一些代码规范的统一,在开发自己项目时需要克隆RN种子工程。 在开发过程中需要使用到本地调试,下面我们看看iOS端调试。

2、本地调试

a) iOS模拟器启动页面:

b) 启动Chrome浏览器调试:

command+D弹出模拟器工具类 选择选项Debug Js Remotely

c) 浏览器自动打开链接:http://localhost:8081/debugger-ui/

这时能在浏览器上查看所有前端js代码了

d) 断点调试

打开目的js文件,找到要调试的函数,直接打断点,当执行该函数时就直接断点拦截了。

58车商通-RN落地服务端开发设计

服务端在整个RNAPP中承担的角色,就是为APP提供基本的数据接口服务和提供RN项目资源信息和下载地址;

此处就讲一下RN项目下载更新流程。

服务端接口返回数据模型:

{

"respData": {

"h5Url": "",

"business": {

"version": "90",

"remoteUrl": "https://j1.58cdn.com.cn/escstatic/rn/apptest/10021/android/10021_90_android_business.tgz"

},

"resource": {

"version": "90",

"remoteUrl": "https://j1.58cdn.com.cn/escstatic/rn/apptest/10021/android/10021_90_android_assets.tgz"

},

"bundleId": "10021",

"unpacking": true,

"downNow": false

},

"respCode": 0

}

服务端流程如下:

58车商通-热更新平台设计

1. 热更新流程

在传统的web开发中,我们修改完js之后在浏览器上就能直接看到效果,JavaScript本身就是一门动态语言,并不需要编译,浏览器每次刷新都拉取新的js文件; 针对web应用最简单也最有效的优化就是缓存,当js没有更新,浏览器就不需要下载新的js文件;

在RN实现动态更新也是同样的思路,RN中前端JS代码最终都会打包成jsbundle文件,我们在需求更新时,在应用中从远程下载这个文件,并重新加载,就可以完成动态更新同时无需通过App Store重新发布;

APP RN资源更新流程:

1、 APP启动时:

2、 启动项目时:

所有前面的更新流程都会依赖于前端的RN资源,那么下面我们来看一下如何把一个项目在平台上录入、编译打包和上线的。

2. 平台资源录入

在项目开发完成,提交到公司Git代码仓库,就可以使用RN资源管理平台录入资源了;

平台功能如下:

项目录入:

在填写完信息后,平台会根据填写的git地址,把当前项目代码下载到服务器,并npm install安装当前项目需要的所有依赖; 这个过程因为需要安装项目依赖,所以时间比较长,项目初始化完成就可以对项目编译打包了;

信息录入时,会为每个项目分配一个唯一的ID,也是客户端区分项目的唯一标识。

3. 58车商通-RN打包编译流程

RN项目编译打包流程:

打包产物jsbundle

打包之后JSBundle文件的结构,基本分为3部分:

-  头部: 全局定义,主要是define,require等全局模块的定义;

-  中间: 模块定义,RN框架和业务的各个模块定义;

-  尾部: 引擎初始化和入口函数执行;

在RN打包过程中,解析依赖关系,为每个模块添加一个id:

1、需求

在实际的RN业务开发中,我们会涉及到很多个业务,这些业务基本上也不会耦合,这时我们就需要创建多个RN项目,每个项目编译打包成独立的bundle; 对于RN APP来说,即使只有一个helloworld页面,在使用官方命令react-native bundle打出来的jsbundle文件大约为530KB以上,RN依赖模块本身就占了99%以上; 如果更新的话,需要从网络上拉取整个包下载时间长,还会海鸥飞用户流量,每次进入RN页面还都要执行RN基础模块的定义; 在RN项目开发中基础库react和react-native是不变的,我们可以抽离这两个依赖打成common.bundle内置于APP中;

2、目标

- 抽离react和react-native打包成common.bundle

- 减小线上下发业务bundle体积,减少下载时间、节省用户流量

- 可预加载common.bundle,提升打开页面速度

3、分析

- 通过分析bundle结构和依赖查找,最终可以通过标记法进行分包

- 打包bundle时,根据entryFile进行深度遍历依赖分析,模块id不断递增,即越早引用的模块,id越小

- 在分析依赖时标记哪些模块属于common.bundle,哪些模块属于业务bundle

- 打包时先引入base.js保证common.bundle的模块id都在前面,先收集common.bundle的模块

- 在遍历进行依赖收集,输出业务bundle

//base.js

import React, { Component } from "react";

import {} from "react-native";

4. 拆分打包流程

编译打包之后本地项目打包结果如下:

iOS端:

Android端:   

5. 小结

优点:

- 一次性打包输出common.bundle和业务bundle,效率高

- 用户只需下载业务bundle,减少流量消耗和下载时间

缺点:

- 直接引用react-native作为基础,common中可能会引入一些用不到的模块

总体上利大于弊,开发中也不可预知需要用到react-native哪些模块,直接打包一个全集也未尝不可。

6. 项目提测、上线

项目提测流程:

项目上线流程:

7. 项目回滚

在日常开发中,纵然有多轮测试,也避免不了发布到线上不会存在问题,我们传统的解决方案就是定位问题,找出原因,解决完之后重新发布上线; 如果是一个流量很大的需求,同时又出现了线上不容易解决的问题,此时线上就会出现长时间的功能无法使用的情况; 这是我们就可以考虑到回滚,先把项目回滚到一个可用的版本,然后再来解决自己的问题。

RN项目回滚流程如下:

8. 展望

理论上我们还可以指定规则,解耦业务,根据不同的业务模块,划分出更多的bundle,每个bundle的模块id按照某个值开始,避免重复,类似android插件化处理资源id策略,按需加载业务bundle。

采坑

俗话说工欲善其事,必现踩其坑,下面我们分享一下我们在实践过程中遇到的坑:

1. 统一封装RN Fetch请求时android底层报错 

作为前端开发人员,网络请求工具对大家来说肯定不陌生。 iOS的AFNetworking,Android的okHttp等。 但是对于RN来说,我们最常用到的就是js原生的Fetch请求了。

React Native提供了和web标准一致的Fetch API,用于满足开发者访问网络的需求。

错误原因:  

在封装RN fetch请求时header进行了统一设置设置如下:

在实际请求时android底层抛异常 如下:

查看RN Android底层源码看到 RN的fetch请求在Android底层Content-Type只支持multipart/form-data类型。

但是iOS底层支持application/json、text/plain、application/x-www-form-urlencoded类型; 不支持multipart/form-data。

解决方案:

在封装是需要分端去指定当前请求头,android端headers中Content-Type只能是multipart/form-data

2. Android端Fetch POST请求无参数时,报异常

错误原因:

前端统一封装POST请求如下:

如果POST请求无需传参 此时FormData如下:

FormData {_parts: Array(0)}

Android底层会报错,错误如下:  

解决方案:  

前端兼容POST参数,如果POST请求无需传参 也就是params为空时,前端拼接一个空的part,part的key、value值都置为空。

3. RN页面数据注入问题

3.1、页面props注入

iOS端:

通过RCTRootView的初始化函数你可以将任意属性传递给React Native应用。 参数initialProperties必须是NSDictionary的一个实例。 这一字典参数会在内部被转化为一个可供JS组件调用的JSON对象。

android端:

android底层目前无法实现参数注入的形式。

在实际业务开发中也无法使用此方面来实现RN页面的数据注入。

3.2、使用消息监听的方式

iOS端:

前端监听:

OC端需要自定义实现消息监听模块,在前端首次调用消息监听时OC端去注册监听事件,此时不会执行前端监听回调,需要前端再次调用监听事件。

android端:

前端监听:

android端消息监听使用的是RN封装的DeviceEventEmitter消息监听事件,可以在android端直接通过sendEvent方法发送广播消息,前端监听接收,实现消息的收发。

消息监听的方式整体的是可行的方案,但是android端和iOS端消息监听的实现差异太大,无法做到两端统一,此方案也不是可选的最优方案。

3.3、事件交互获取数据

最终我们的实现方案是前端和native端定义双方交互事件,通过native事件通信的方式去获取RN页面想要的数据。

4. iOS 端内存不释

在测试过程中我们发现,iOS端在数次退出进入RN模块后会崩溃,纠其原因是

bridge没有释放,需要我们释放一下

总结

本文总结了RN在58车商通的应用场景以及整体设计流程,还包括热更新的几个阶段设计,从业务和技术两个方面进行了阐述。 后续会持续的更新RN系列。 如果后续同学们想研究下RN的底层实现,我们可以在发一篇文章共大家参考。

经过大家的共同努力,我们终于在车商通3.7.3期顺利上线了两个RN模块,但是我们也还有很多优化的空间。 后续我们也会持续跟进。 目前思路是从以下几个方面来继续优化

1. 前端工作,优化列表控件。 目前的列表性能还不是特别好,还需要下一步继续优化

2. 热更新继续深入优化,最终做到增量更新(任重而道远)。

3. 优化首屏加载白屏的问题,需要native 端与前端一起来做。

4. native 内部代码结构还需要进一步调整,重构代码。

由于我们的RN 项目是先在公司的主站RN项目开源前落地并开发完成的,目前阶段,我们还没有做更深一步的差量化更新(以补丁包的形式下发更新),所以,深度上讲我们目前没有涉及更深层次。 如今,公司主站App,更加成熟的RN体系已经开源,我们可以借鉴或者应用更加成熟的RN体系到应用的到项目中,以提高App的开发效率,缩短更新流程,降低更新风险

参考文章

1. ReactNative中文网

2. ReactNative GitHub源码

3. Fetch API

作者简介

桑锐/58同城/ iOS高级开发工程师

龚成辉 /58同城/   前端高级开发工程师

阅读推荐

1.  58二手车在移动平台上的AI技术探索

2. 深度|数据智能在二手车业务场景中的探索与沉淀——关于用户流量的预测与识别

3.  机器学习在58二手车估价系统实践

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章