React Native 项目整合 CodePush 之完全指南

本文使用的环境:

  • React@16.3.1
  • React Native@0.55.4
  • react-native-code-push@5.3.4
  • Android SDK@23
  • Android Build Tool@23.0.3
  • Gradle@2.14.1
  • Android Gradle Plugin@2.2.3

Why CodePush?

CodePush 是微软提供的一个热更新前后台方案,它对 React Native 项目有很好的支持。

目前针对 React Native 的 hot update 方案有许多,但是 CodePush 是最成熟稳定的方案,它最大的特点是提供了完整的后台工具。它主要的优点是:

  • 微软出品,大厂保证
  • 良好的多环境支持(Testing,Staging, Production)
  • 灰度发布、自动回滚等等特性
  • 良好的数据统计支持:下载、安装、出错一目了然
  • 强大的 CLI 工具,一个终端搞定全部流程

由于 React Native 执行的是脚本 js 文件,对热更新有天然的亲和,有余力的团队可以尝试实现自己的框架,一个简单的实现思路是:

  • 修改加载 jsBundle 的代码,转而从指定的本地存储位置去加载。如果没有,下载 bundle, 并且本次打开使用 app 包中的 bundle。
  • 如果找到 jsBundle 文件,调用 api 比较版本号,如果不一致,则从指定服务器下载最新的 bundle 进行替换。
  • 通过反射调用私有方法,在下载完成的回调中更新运行时资源,从而能立即看到更新的效果。
  • 使用类似 google-diff-match-patch 的 diff 工具,生成差异化补丁,不必下载完整 bundle,从而大大减小补丁包体积。

网上有很多资料和源码,这里就不细述了。

后台配置

为了使用 Code Push 发布热更新,我们需要向微软服务注册我们的应用。这部分工作微软提供了强大的命令行工具:CodePush CLI

CodePush CLI

安装 cli 工具

npm 全局安装:

npm install -g code-push-cli

关联账号

使用命令

code-push register

注册一个账号,可以直接使用 GitHub 账号授权,完成后将 token 复制回命令行中。

授权返回的token

使用 whoami 查看登录状态:

code-push whoami

注册应用

登录成功后,我们注册一个app:

code-push app add 你的App名称 android react-native

注意一定要为 Android 和 iOS 分别注册,两者的更新包内容会有差异。

注册成功

查询状态

每个 App 有不同的运行时环境,比如 Production,Staging等,我们也可以配置自己的环境。查看 App 的不同环境和部署状况:

code-push deployment ls 注册的app名称

查询状态

目前我们还没有发布任何更新,所以表中的状态是空的。

到这里就完成了后端的基本配置。

App端配置

版本兼容

安装 Code Push 环境前首先要 check 版本的兼容性问题,不同的RN版本需要使用不同的 Code Push,原则上我们建议将 RN 和 CodePush 都升级到最新版本。

下表是官方文档中的兼容性说明:

React Native version(s) Supporting CodePush version(s)
<0.14 Unsupported
v0.14 v1.3 (introduced Android support)
v0.15-v0.18 v1.4-v1.6 (introduced iOS asset support)
v0.19-v0.28 v1.7-v1.17 (introduced Android asset support)
v0.29-v0.30 v1.13-v1.17 (RN refactored native hosting code)
v0.31-v0.33 v1.14.6-v1.17 (RN refactored native hosting code)
v0.34-v0.35 v1.15-v1.17 (RN refactored native hosting code)
v0.36-v0.39 v1.16-v1.17 (RN refactored resume handler)
v0.40-v0.42 v1.17 (RN refactored iOS header files)
v0.43-v0.44 v2.0+ (RN refactored uimanager dependencies)
v0.45 v3.0+ (RN refactored instance manager code)
v0.46 v4.0+ (RN refactored js bundle loader code)
v0.46-v0.53 v5.1+ (RN removed unused registration of JS modules)
v0.54-v0.55 v5.3+ (Android Gradle Plugin 3.x integration)

安装包

使用命令:

npm info react-native-code-push

来查看包相关信息。

我们建议始终将RN、React以及一些相关库升级到最新版本。在根目录下使用命令:

npm install --save react-native-code-push

来安装最新版本的 CodePush。

也可以参照上面的兼容性表格,安装指定版本:

npm install --save react-native-code-push@5.1.4

工程配置(Android)

如果工程创建的时候比较早,可能是使用命令create-react-native-app来创建的,则需要在根目录执行:

npm run eject

来改变工程结构,防止后面的兼容性问题。

配置安卓工程,官方提供了两种途径:

  • 使用命令行工具rnpm(现在已经被整合到React Native CLI工具中了)。执行
react-native link react-native-code-push
  • 手动配置

如果你是新手,或者对 gradle、安卓工程结构不了解,我们强烈建议执行一次手动配置,帮助理解到底发生了什么。

手动配置

step 1

android/settings.gradle文件中添加:

include ':app', ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')

这个文件定义了哪些 module 应该被加入到编译过程,对于单个 module 的项目可以不用需要这个文件,但是对于 multiModule 的项目我们就需要这个文件,否则 gradle 不知道要加载哪些项目。这个文件的代码在初始化阶段就会被执行。

我们添加的内容告诉 gradle:去 node_modules 目录下的 react-native-code-push 加载 CodePush 子项目。

step 2

android/app/build.gradle 中的 dependencies 方法中添加依赖:

...
dependencies {
    ...
    compile project(':react-native-code-push')
}

这样就能在主工程中引用到 CodePush 模块了。

step 3

继续在 android/app/build.gradle 中,添加在编译打包阶段 CodePush 需要执行的 task 引用:

...
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"
...

这段代码其实就是调用了 project 对象的 apply 方法,传入了一个以 from 为 key 的 map。完整写出来就是这样的:

project.apply([from: '../../node_modules/react-native-code-push/android/codepush.gradle'])

apply fromapply plugin的区别在于,前者是从指定 url 去加载脚本文件,后者则用是从仓库拉取 plugin id 对应的二进制执行包。

step 4

CodePush 发布有各种环境(deployment),默认有 Staging 和 Production,我们需要在 buildType 中配置对应的环境,并且设置 PushKey,从而让 App 端的 CodePush RunTime 根据不同的健值来下载正确的更新包。

查询各个环境 Key 的方法是使用上文安装的 CLI 工具:

code-push deployment ls App名称 -k

查询CodePushKey

上表中的 Deployment Key 就是对应环境的 Key 值了。

android/app/build.gradle 中,配置 buildTypes:

buildTypes {

    // 对应Production环境
    release {
        ...
        buildConfigField "String", "CODEPUSH_KEY", '"从上述结果中复制的production值"'
        ...
    }

    // 对应Staging环境
    releaseStaging {
        // 从 release 拷贝配置,只修改了 pushKey
        initWith release
        buildConfigField "String", "CODEPUSH_KEY", '"从上述结果中复制的stagingkey值"'
    }

    debug {
        buildConfigField "String", "CODEPUSH_KEY", '""'
    }
}

注意这里不同 buildType 的命名,Staging 环境对应的 buildType 就叫 releaseStaging,要符合这样的命名规范。

Debug 环境虽然用不到 CodePush, 但是也要配置空的 Key 值,否则会报错。

step 5

处理完引用关系后,我们修改 MainApplication.java,在 App 执行时启动 CodePush 服务:

// 声明包
import com.microsoft.codepush.react.CodePush;

public class MainApplication extends Application implements ReactApplication {

    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        ...
        // 重写 getJSBundleFile() 方法,让 CodePush 去获取正确的 jsBundle
        @Override
        protected String getJSBundleFile() {
            return CodePush.getJSBundleFile();
        }

        @Override
        protected List<ReactPackage> getPackages() {
            return Arrays.<ReactPackage>asList(
                new MainReactPackage(),
                // 创建一个CodePush运行时实例
                new CodePush(BuildConfig.CODEPUSH_KEY, MainApplication.this, BuildConfig.DEBUG)
                ...
            );
        }
    };
}

js端引入 Code Push

配置完项目工程后,我们将 CodePush 引入到 js 端。

首先将 App 的根组件包裹在 CodePush 中:

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

AppRegistry.registerComponent('BDCRM', () => codePush(App));

CodePush 会在 App 启动后自动去 check 和更新最新的版本,我们可以添加一些配置,让它在进入后台的时候也执行检查:

let codePushOptions = { checkFrequency: codePush.CheckFrequency.MANUAL };
AppRegistry.registerComponent('BDCRM', () => codePush(codePushOptions)(App));

CodePush js端的 api 不多,我们可以用这些 api 控制更新的一系列流程,常用的有:

// 检测是否有更新包可用
codePush.checkForUpdate(deploymentKey: String = null, handleBinaryVersionMismatchCallback: (update: RemotePackage) => void): Promise<RemotePackage>;

// 获取本地最新更新包的属性
codePush.getCurrentPackage(): Promise<LocalPackage>;

// 重启app(即使不用在 Hot Updating,也挺有用的)
codePush.restartApp(onlyIfUpdateIsPending: Boolean = false): void;

// 手动进一次更新
codePush.sync(options: Object, syncStatusChangeCallback: function(syncStatus: Number), downloadProgressCallback: function(progress: DownloadProgress), handleBinaryVersionMismatchCallback: function(update: RemotePackage)): Promise<Number>;

更多详细信息见文档

使用 CodePush CLI 发布更新

完成前后端的配置,打包发布应用后,后续的改动我们就能通过 CLI 工具来发布啦!

升级前首先要 check:

  • 应用的版本号要有更新(app/build.gradle: defaultConfig/versionName)
  • js bundle 要有改动,Code Push 会 diff 前后版本,如果代码一致会认为是无效的更新包

打开终端,进入到工程目录,完整发布命令是:

code-push release-react <appName> <platform>
[--bundleName <bundleName>]
[--deploymentName <deploymentName>]
[--description <description>]
[--development <development>]
[--disabled <disabled>]
[--entryFile <entryFile>]
[--gradleFile <gradleFile>]
[--mandatory]
[--noDuplicateReleaseError]
[--outputDir <outputDir>]
[--plistFile <plistFile>]
[--plistFilePrefix <plistFilePrefix>]
[--sourcemapOutput <sourcemapOutput>]
[--targetBinaryVersion <targetBinaryVersion>]
[--rollout <rolloutPercentage>]
[--privateKeyPath <pathToPrivateKey>]
[--config <config>]

命令参数很多,但用途都一目了然,嫌每次打麻烦的话,做成脚本也可以。

一般来说,我们发布应用首先会在测试环境进行稳定性测试,通过后再发布到生产环境中:

  • 打包发布 Staging 环境
code-push release-react 应用名 --platform android --deploymentName Staging --description "修复一些bug"

这样,我们 Staging 环境就可以收到更新推送啦,具体加载新 bundle 的实际,和我们在应用中配置的策略有关,上文已经介绍过了。

  • 测试 ok 后,提升(Promoting)到 Production 环境,并且进行灰度20%发布
code-push promote 应用名 Staging Production --rollout 20%
  • 在生产环境验证 ok,使用 patch 将灰度修改为100%,进行全网发布:
code-push patch 应用名 Production -rollout 100%

以上就是按照 测试 - 灰度 - 全部发布 步骤的一个典型 CodePush 发布工作流。

总体来说,CodePush 能满足我们灰度发布 React Native 应用的大部分需求了,由微软提供的服务器端支持可以节省很多工作,是一个成熟可靠的方案。如果要说缺点,可能有几个需要考虑一下:

  • 服务器速度,国内网络状况可能会影响下发的成功率和效率。
  • 污染代码,在 js 端必须将根节点包裹到 CodePush 模块中去,污染了代码。
  • 冗余,如果只是想要简单的下发小体积的 js bundle,CodePush 显得太“重”,过于冗余了,这时候用轻量化的方案更好。

总之,我们根据自己项目的需要去进行选型就好了!

更多细节,可以参考文档

0

从 React 到 Preact 迁移指南

为什么选 Preact

Fast 3kB alternative to React with the same ES6 API.
React 的 3kb 轻量化方案,拥有同样的 ES6 API

  • 体积小。React v15.6.1 有 49.8kb,最新的 React v16.0 小了很多,有 34.8kb。而 Preact 官网声称只有 3kb,实测 v8.2.5 版本有
    4.1kb,但这已经比 React 小了至少 30kb,在移动端网页开发中占了不少优势。
  • 性能高。是最快的虚拟 DOM 框架之一,具体可以查看 Results for js web frameworks benchmark – round 6
  • 生态好。官方提供 preact-compat,可以无缝使用 React 生态系统中的各类组件。
  • 更方便。相比 React,Preact 添加了几个更为便捷的特性,包括可以直接使用标准的 HTML 属性(如 classfor),propsstatecontext 作为参数传进了 render() 等。

除了 Preact,React-like 中比较出名的还有 Inferno,它大小在9kb左右。它和前面两者相比,还加了一个新的特性就是无状态组件(stateless components)也支持生命周期事件,从而可以减少 class 的使用,做到更加轻量。性能比 React 和 Preact 都要好,其它方面和 Preact 差不多。但是有个问题是 Inferno 用了一些相对较新的特性,比如 PromiseMapSetWeakMap 等,所以浏览器兼容性比 Preact 要差点。

迁移指南

官方文档

官方文档提供了2种途径,都是在原项目上把 React 替换成 Preact,我们为了保持项目干净采用创建新项目的方式来做。

准备

1. 创建项目

Preact 官方提供了2种方式来创建项目:

  1. preact-cli
  2. Preact Boilerplate / Starter Kit

官方推荐用方式一,但我们为了方便定制采用了方式二来创建,去掉了 preact-compat 等暂时没用到库。

2. 复制代码

创建完项目,把原项目中 src 目录下的代码(index.js、组件等)拷贝到新项目 src 目录下就可以了。

这样准备工作就做好了,当然现在项目还是跑不起来,会各种报错,接下来我们需要修改代码。

修改代码

react

react 替换成 preact

import React from 'react';
// =>
import { h } from 'preact';

Preact 的 h() 相当于 React 中的 createElement(),可以阅读 WTF is JSX 了解更多。

import { render } from 'react-dom';
// =>
import { render } from 'preact';
import { Component } from 'react';
// =>
import { Component } from 'preact';

redux

直接替换成 preact-redux 就可以,其它都一样:

import { Provider } from 'react-redux';
// =>
import { Provider } from 'preact-redux';

router

我们原项目采用的是 React Router v3,Preact 官方提供了 preact-router,它并不是 React Router 的 preact 兼容版本,而是另一种轻量级的路由方案。如果你还是想用 React Router,可以它的 v4 版本,因为 v4 版本可以直接和 Preact 一起使用,而不需要 preact-compat 来做兼容。

所以如果你有比较复杂的需求的话,比如路由嵌套、视图合成等等,可以考虑用 React Router v4;如果只是想要比较基本的路由需求的话,那就用 preact-router 好了。我们项目路由需求不复杂,为了追求轻量,就采用了 preact-router,毕竟 React Router 比 preact-router 大了很多。

因为不兼容,所以改动也是比较大的:

import { Router, Route, IndexRoute } from 'react-router';
// =>
import Router from 'preact-router';
import { hashHistory } from 'react-router';
// =>
import createHashHistory from 'history/createHashHistory';
const hashHistory = createHashHistory();
const routes = (
  <Router history={hashHistory}>
    <IndexRoute component={Home} />
    <Route path="about" component={About} />
    <Route path="inbox" component={Inbox} />
  </Router>
);
// =>
const routes = (
  <Router history={hashHistory}>
    <Home path="/" />
    <About path="/about" />
    <Inbox path="/inbox" />
  </Router>
);

<Route/>onEnter/onLeave 钩子也没了,不过没关系,因为用组件的生命周期才是更合理的。

这样做完路由配置之后,组件中用到路由的部分也不一样了。原先取当前页面的 URL 或者 query 是这样的:

class Home extends Component {
  componentWillMount() {
    const { router } = this.context;
    const { params } = this.props;

    const url = router.location.pathname;
    const { id } = params;
  }
}

现在就只需要:

class Home extends Component {
  componentWillMount() {
    const { url, id } = this.props;
  }
}

是不是更简单了。preact-router 引入了更少的 API,语法也更加简洁。

有了路由,当然也少不了链接。原先我们会写成这样:

import { Link } from 'react-router';

<Link to="/about">关于</Link>

现在只需要写成普通的 <a/> 标签就可以了,因为 preact-router 会自动匹配 <a/> 标签到路由:

<a href="/about">关于</a>

当然如果你要修改激活状态样式的话还是可以用 <Link/>,但注意这里用的是 href="/" 而不是 to="/"

import { Link } from 'preact-router/match';

<Link activeClassName="active" href="/">Home</Link>

总结

上面只是梳理了我们在做迁移时需要改动的地方,因为之前并没有用第三方的 React 组件,所以也没有用 preact-compat,除了 router 改得比之前更简单了,其它基本不需要怎么改动,整体来说迁移成本不高。

迁移后效果最明显的就是编译出来的 js 小了很多,基本是之前的1/3,所以移动端网页开发推荐用 Preact 替换 React。

2+