基于React Native的应用热发布实践

1 热发布

网页发布 VS APP发布

网页发布:服务端上线新的网页代码,用户端通过链接直接访问。

APP发布:�发布新的包装包(应用市场:上传新的包装包;应用内升级:服务端发布新的包装包),用户端下载,安装。

网页发布属于热发布,完全�受控于服务端

APP发布的限制:
应用市场审核
用户端下载,安装

APP热发布的目的是实现类似于网页的发布方式,排除�更新对于应用市场和用户的依赖,完全由服务端控制。

2 APP热发布的方式

2.1 H5热发布

功能完全由H5实现,APP使用WebView来加载。

优势:完全热发布

劣势

  • 性能-H5的性能相比Native差得比较多,交互体验较差。
  • 缓存-H5的缓存受制于系统WebView,在无网或缓存过期情况下无法呈现界面。

2.2 数据热发布

数据由服务端控制,动态发布,APP根据数据来展现UI,实现UI有限的动态性(UI动态支持预先在APP中定义好)。

这里的数据也包括脚本数据的情况,APP通过脚本引擎执行脚本,根据结果来执行对应逻辑。

优势:数据层面热发布

劣势:不能实现UI和逻辑的完全热发布

2.3 插件化热发布

APP的独立功能作为插件,动态下载,动态运行,插件发布由服务端动态控制。

优势:功能�性热发布

劣势:使用非系统公开API,有失效风险

2.4 React Native

还有一种思路,APP作为基础容器,应用功能作为脚本文件,动态下发,�通过解析引擎动态执行,展现为Native UI,同时控制数据和业务逻辑。

脚本文件发布由服务端动态控制,能够实现整个APP或者部分功能完全热发布。

React Native是facebook开源的构建跨平台应用的框架,它的原理是,通过JavascriptCore引擎,解析JSX(类似XML的Javascript扩展语言)脚本代码,生成Virtual DOM,再转换为Native视图,同时通过桥接JS和Native,实现事件交互。

React Native通过Virtual DOM,以diff的方式交给Native渲染,�其diff算法相当高效,能够保证良好的Native性能。这是它优于H5的地方。

优势:

  • 完全热发布
  • 良好的Native性能
  • 跨平台复用:Andriod, iOS, H5。
  • 大厂出品:持续维护性(两周一个release版本);开放性,三方开源库支持。

劣势:

  • 成熟度不够。
  • 目前支持Android4.1和iOS7.0以上系统版本。

对动态性要求较高的APP,React Native提供了一种解决方案,实现类似网页的发布效果,同时保证良好的Native性能

使用React Native的APP:
Facebook,QQ,QQ音乐,QQ空间(发现Tab)

此外,Weex是阿里开源的移动端跨平台开发框架,原理与React Native类似,只不过它的脚本语言换成了Vue(也是JS框架)。

3 React Native原理

3.1 React Native集成

React Native(以下简称RN)的集成参考官网:
https://facebook.github.io/react-native/docs/getting-started.html

3.2 RN目录结构

RN工程目录结构
  • *.android.js:Android 的JS文件
  • *.ios.js:iOS的JS文件
  • android:Android的Native工程
  • ios:iOS的Native工程
  • node_modules:RN基于NodeJS的基础库。包括基本组件,以及打包脚本等等。

3.3 RN原理


RN架构

Java层
Java层为应用的入口,启动C++层的JS解析器,执行JS,并通过C++传递的渲染指令,从而构建Native UI等。核心逻辑在ReactCore中完成。在UI层面,ReactCore完成了对Native视图组件的封装,它与JS层定义的组件是一一映射的。

Java层集成众多优秀开源库,图片处理使用的是Fresco,网络通信使用的是okhttp。JS层的组件会通过这些基础库来实现功能,如Image组件使用Fresco来加载远程图片,fetch组件使用okhttp来进行网络请求。

C++层
C++层主要封装了JavaScriptCore(Android和iOS通用的JS引擎),用来实现JS的解析和执行。基于JavaScriptCore,意味着JS可以使用ES6的新特性,如class、箭头操作符等。

Bridge�实现Java和 JS之间的通信。

JSLoader用来加载JS文件,包括assets目录和本地文件。

JS层
主要实现UI布局和事件分发,主要有以下几个部分:

Component:JS层通JS/JSX编写的Component来构建Virtual DOM,Virtual DOM是DOM在内存中的一种轻量级表达方式,可以通过不同的渲染引擎生成不同平台下的UI。Component的存在让计算 DOM diff 更高效,从而使得映射到Native的视图�渲染有很好的性能。

Layout:React使用css-layout,css-layout使用JS实现了flexbox ,能编译成Native代码,最终达到跨平台的展示目的。

Lifecycle&Data:React 组件通过内部的 state 变量控制生命周期及事件回调。如getInitialState方法用于定义组件初始状态,后续组件可通过 state 属性读取该状态。当�调用setState 方法就修改状态值时,RN会自动调用 render 方法,重新渲染组件。

4 Android的RN集成

Android上,应用是通过Activity来承载的。

复杂应用通常由多个模块组成,每个模块对应一个Activity。简单来说,我们可以以Activity为模块单元。

4.1 Activity与bundle

RN通过bundle来承载应用。bundle包括JS脚本代码及其引用的资源(图片)。
在Android上,我们为每个Activity绑定一个bundle,来实现模块的独立管理。

Activity与bundle.png

在Native中,我们通过BaseReactActivity�来连接RN,它通过JsBundle和Component与Bundle关联起来。

Bundle两种形式,一个是Assets,打包在apk中一起发布;一个File,它是动态更新下载的。在有File时使用File,没有File时使用Assets。

Activity的注册我们只在Manifest中定义几个Activity模板。例如,

  • SingleTaskActivity:launch mode为SingleTask
  • StandardActivity:launch mode为Standard
  • WebViewActivity:process为单独的web进程

Activity跳转时,指定对应的模板Activity,传递bundle信息即可。

4.2 JS

JS文件结构(avcdemo.android.js)

import React, {Component} from 'React'; //引入React组件

import {
  AppRegistry,
  View,
  Text,
  Image
} from 'react-native';  //引入RN组件

class AVCDemo extends Component { //定义Component
  render() {  //render定义视图结构
    return(
      <View style={styles.full}>
        <Text style={styles.text}>Hello RN</Text>
        <Image source={require('./res/demo/kaola.png')} style={styles.img}></Image>
      </View>
    )
  }
}

var styles = {  //定义样式
  full: {
    position: 'absolute',
    left: 0,
    top: 0,
    right: 0,
    bottom: 0,
    alignItems: 'center',
    justifyContent: 'center'
  },
  text: {
    fontSize: 80
  },
  img: {
    width: 200,
    height: 200
  }
}

AppRegistry.registerComponent('demo', () => AVCDemo); //注册Component
demo

4.3 Native Activity

DemoActivity

public class DemoActivity extends AppReactActivity {

    @Override
    protected String getMainComponentName() {
        return "demo";
    }

    @Nullable
    @Override
    protected String getBundleAssetName() {
        return "avcdemo.android.bundle";
    }

}

可以看到Activity通过bundle最终绑定到Component。

4.4 Component

Component有几个基本概念:props,state,render,生命周期。

props:静态属性,提供定制参数给调用方。如Image的source。

state:动态属性,内部使用。当通过setState方法改变时,会调用render方法进行重新渲染。比如,我们获取网络数据回来时需要改变界面,这时就需要将网络数据作为一个动态属性,同时render方法中使用�该属性来进行渲染。

render:渲染方法,定义Component的视图结构。

与Activity类似,Component也有生命周期。

Component生命周期.png

4.5 RN组件

下面来看看RN对应用实现的支持。首先是组件。

实现应用的组件大致分为4个层面:

  • UI
  • 网络
  • 存储
  • 线程

4.5.1 UI

UI层面又可以细分为:UI组件,动画,交互事件。

UI组件
RN提供了较为丰富的基础组件,以及部分复杂组件。

RN UI组件

UI组件本质上会做一个到Native组件的映射。

RN UI组件 Android UI组件
View -> ViewGroup
Image -> ImageView
Text -> TextView
TextInput -> EditText
ViewPagerAndroid -> ViewPager
WebView -> WebView
ScrollView -> ScrollView
ListView -> ScrollView

RN的ListView并没有采用Native的ListView,而是基于RN的ScrollView,在js层来处理ListView的逻辑。

动画
RN提供了两种动画方式:Animated和LayoutAnimation。
Animated与Android的属性动画类似,可以实现透明度,缩放和位移等效果。
LayoutAnimation与Android LayoutAnimation类似,作用于�加入和移除视图树。

交互事件
交互事件主要有两种:点击和Touch,按键。
RN提供Touchablexxx(TouchableOpacityTouchableHighlight等)系列组件来实现点击、长按、滑动交互。

onPressIn:点击开始;
onPressOut:点击结束或者离开;
onPress:单击事件回调;
onLongPress:长按事件回调。

事件拦截(类似于Android的onInterceptTouchEvent)
onStartShouldSetResponder
onMoveShouldSetResponder

按键处理,RN提供了Andorid的back键处理BackAndroid

4.5.2 网络

RN提供fetch API用于网络请求,它本质是基于OkHttp实现。

它支持GET,POST等基本方法,包括header的传递,以及响应json格式化。

另外,它也提供XMLHttpRequest和WebSocket的支持。

4.5.3 存储

RN提供了简单的持久存储组件AsyncStorage

它支持key-value存储,效果类似于Android的SharedPreference,实际上它是存储在本地的sqlite数据库中。

如果需要传统的sqlite存储,需要自己实现。会有一些开源组件供实现参考:https://github.com/jbrodriguez/react-native-android-sqlite

4.5.4 线程

RN的应用逻辑是运行JS线程上的,独立于Android的UI线程。JS线程上的更新批量传递给Native作UI更新。

RN提供Timers来实现异步逻辑。 可以通过JS本身的异步接口来实现(async/await)。

4.6 RN扩展

RN官方提供的组件能够满足基本业务需求,如果要实现复杂应用,需要进行扩展。

目前很多RN开源组件:https://github.com/jondot/awesome-react-native

另外,也可以自己定制。定制分为几个方面:

  • JS调用Native
  • 定制JS层组件
  • 使用Native UI组件

4.6.1 JS调用Native

当JS层无法满足需求时,通过需要Native层支持提供接口,来供JS层调用。RN提供Native Modules的方式来支持。

简单来说,就是Native按照规则定义好接口,同时注册到RN中,JS层直接调用即可。

显然,这种方式定义的Native Module不能动态发布,它需要固化在APP中。

4.6.2 定制�JS层组件

Component组件
以ImageText为例(ImageText.js)

'use strict';

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

class ImageText extends Component {

  props: {
    imgUrl?: string,
    text?: string,
  }

  render() {
    return(
      <View style={styles.container}>
        <Image style={styles.img} source={{uri: this.props.imgUrl}}></Image>
        <Text style={styles.text}>{this.props.text}</Text>
      </View>
    )
  }
}

var styles = {
  container: {
    flexDirection: 'row',
    marginRight: 8,
  },
  img: {
    width: 14,
    height: 14,
  },
  text: {

  },
}

module.exports = ImageText;

�然后将Component输出:

module.exports = ImageText;

方法组件
以网络请求为例,网络请求通常会加上一些能用参数。可以定义一个网络请求组件(Requestor.js)。

'use strict';

import HOST from './Host';

function get(url, headers) {
  if (headers == null) {
    headers = {};
  }
  headers[HOST.API_VERSION_KEY] = HOST.API_VERSION_VALUE;
  console.log("headers: " + JSON.stringify(headers));
  return fetch(url, {
    headers: headers
  })
}

module.exports = {get};

需要将方法组件输出:

module.exports = {get};

常量组件

'use strict';

var apiVersion = 200;

module.exports = {
  HOST: "http://api.kkmoving.com",
  API_VERSION_KEY: "apiVersion",
  API_VERSION_VALUE: apiVersion
};

4.6.3 使用Native UI组件

Native UI组件定义参考Native UI Components

简单来说,就是定义好Native UI组件,然后通过ViewManager注册到RN中,同时在JS层定义Component,关联到Native UI组件。然后,这个Component就可以使用。

�需要注意的是,如果Native UI组件要求自定义大小,需要额外实现LayoutShadowNode并关联到ViewManager,LayoutShadowNode提供MeasureFunction回调来实现组件的丈量。具体实现可以参考RN的Text组件实现(Native 代码:ReactTextViewManager,ReactTextView,ReactTextShadowNode)。

4.7 RN工程打包

RN打包分为两个部分,一个是bundle打包,一个是APP打包。

bundle打包,是指bundle的JS文件和图片资源打包为独立的文件,�作为更新包供远程下载。这是动态bundle。

bundle打包命令:

react-native bundle --platform android --dev false \
--entry-file xxx.android.js \
--bundle-output xxx.android.bundle \
--assets-dest xxx

APP打包,首先执行bundle打包,将生成的静态bundle跟随app包一起发布。保证无网时能够展现应用基本功能。

在Android上,跟随打包会将JS文件打包到assets目录中,同时将图片资源打包到drawable目录中。

RN打包Android的方式是通过gradle,在正常apk打包过程之前,会编译RN的bundle。但默认只能打包一个bundle,如果需要支持打包多个bundle,需要修改react.gradle文件(node_modules/react-native/react.gradle)。

RN在开发调试比较方便,通过react-native run-android运行应用,JS文件编辑后,可以直接在应用上reload来实现更新,不需要重新安装APP,类似网页直接刷新的效果。

4.8 热发布实现

RN热发布的策略相对简单。

在APP端,初始发布APP时,自带静态bundle,并每个bundle维护一个版本号。
应用在适当的时机,检查bundle是否需要更新。如果有更新,则下载动态bundle。
�应用优先加载动态bundle,如无动态bundle刚加载静态bundle。

在服务端,�提供动态bundle的发布,版本维护,更新检测和下载。

这是最简单的策略,当然要做得精细,还需要考虑差量更新,bundle拆包(后面会讲)。

5 �RN实践之商品详情页

商品详情页

界面实现Demo

  • ScrollView
  • ViewPager
  • 带样式的Text
  • 远程加载的Image
  • 浮层
  • 动画
  • 地址选择
  • RN WebView

在线更新Demo

  • 检查更新
  • 下载/解压
  • 应用更新

6 踩过的坑

6.1 混淆设置

在RN中如果要使用Proguard,需要做两个设置:

  • 在build.gradle中设置:

def enableProguardInReleaseBuilds = true

  • 在proguard.pro中,基于RN官方Proguard模板,另外设置:

-keep class com.facebook.** { *; }

这一点比较坑,官方文档和各种模板中都没有提到。如果不加一句,release运行时会直接crash。

6.2 Android Studio设置

  • gradle.properties不能设置org.gradle.configureondemand=true

具体原因未知

7 待解决的问题

  • RN基础库较大(6M)。
  • Bundle文件过大(基础Bundle 500k左右)。Bundle拆包,将RN库的JS拆离到单独的基础Bundle。
  • 复杂组件的JS实现。
  • 性能分析和优化。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容