react-native App更新方案

前言

用react-native(一下简称RN)开发的app的更新方案有很多,其中比较火的是热更新方案,有官方推荐的pushy和微软的code-push
文档很详细,接入也比较简单
这里主要介绍一种最传统的更新方案,也是很多原生开发在使用的方案——全量更新

全量更新

顾名思义,即每次更新通过http去下载新版本包去然后去做一个覆盖安装,这种做法在更新迭代中会避免很多不必要的麻烦,而且在这个5G都要到来的时代,网络资源大小的限制也显得不那么重要。

步骤

获取当前APP版本号

react-native是不提供获取版本号模块的,所以我们需要自己去写一个原生模块去获取当前app的版本号。
传送门:原生模块

新建一个模块文件夹

新建一个文件夹在android/app/src/main/java/com/your-app-name/下,这里我取名叫appinfo

新建一个类

在appinfo新建一个类为AppInfoModule.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.your-app-name.appinfo;

import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;

import com.facebook.react.uimanager.IllegalViewOperationException;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;

import java.util.Map;
import java.util.HashMap;

public class AppInfoModule extends ReactContextBaseJavaModule {

public AppInfoModule(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
public String getName() {
return "AppInfo"; //后面需要调用的模块名
}

@ReactMethod
public void getVersion(Callback successCallback) { //后面需要调用的方法名,不需要传参
try {
PackageInfo info = getPackageInfo();
if(info != null){
successCallback.invoke(info.versionName); //这里我是一回调函数的形式返回,也可以用promise的方式
} else {
successCallback.invoke("");
}
} catch (IllegalViewOperationException e){
successCallback.invoke("");
}
}

private PackageInfo getPackageInfo(){
PackageManager manager = getReactApplicationContext().getPackageManager();
PackageInfo info = null;
try{
info = manager.getPackageInfo(getReactApplicationContext().getPackageName(),0);
return info;
} catch (Exception e){
e.printStackTrace();
} finally {
return info;
}
}
}

注册模块

用于在JavaScript 中访问,新建一个AppInfoPackage.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.your-app-name.appinfo;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class AppInfoPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new AppInfoModule(reactContext)); //你的模块
return modules;
}

}

暴露方法

暴露该模块,修改android/app/src/main/java/com/your-app-name/MainApplication.java文件的getPackages方法

1
2
3
4
5
6
7
8
9
10
import com.your-app-name.appinfo.AppInfoPackage;
......
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new AppInfoPackage()); //导入注册好的模块
return packages;
}
......

JavaScript调用

1
2
3
4
NativeModules.AppInfo.getVersion((version) => {
console.log(version);
//output 1.1.0
});

版本校验

写一个版本比较接口,把获取到的版本号提交上去,以此来判断是否需要更新

提供一个php的版本比较方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private function compareVersion($v1,$v2) 
{
if(empty($v2)){
return false;
}
$l1 = explode('.',$v1);
$l2 = explode('.',$v2);
$len = count($l1) > count($l2) ? count($l1): count($l2);
for ($i = 0; $i < $len; $i++) {
$n1 = $l1[$i] ? $l1[$i] : 0;
$n2 = $l2[$i] ? $l2[$i] : 0;
if ($n1 > $n2) {
return true;
} else if ($n1 < $n2) {
return false;
}
}
return false;
}

下载新版本

版本校验需要更新,得到更新的地址后去下载保存到本机

获取读取存储的权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
......
(async () => {
try {
const permissions = [
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
];

const granteds = await PermissionsAndroid.requestMultiple(permissions);
if (granteds["android.permission.ACCESS_FINE_LOCATION"] === "granted" && granteds["android.permission.READ_EXTERNAL_STORAGE"] === "granted" && granteds["android.permission.WRITE_EXTERNAL_STORAGE"] === "granted") {
} else {
ToastAndroid.show('授权被拒绝', ToastAndroid.LONG)
}
} catch (err) {
ToastAndroid.show(err.toString(), ToastAndroid.LONG);
}
})();

react-native-fs下载

接入文档react-native-fs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import RNFS from 'react-native-fs';

const downloadDest = `${RNFS.DownloadDirectoryPath}/app.apk`;

const options = {
fromUrl: UPDATEURL, //更新包的地址
toFile: downloadDest, //下载后保存的地址
background: true,
begin: (res) => {
}
};
const ret = RNFS.downloadFile(options);
ret.promise.then(res => {
// 下载成功
}).catch(err => {
ToastAndroid.show(err, ToastAndroid.LONG);
});

安装

下载成功后,考虑到用户体验,还得有一个自动安装吧~
当然去打开指定路径的文件也是需要我们自己写原生模块的。

按照上面的流程,我新建了update模块和UpdateModule.java类
这里流程很上面的获取版本号一致,我直接贴UpdateModule.java的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.your-app-name.update;

import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;

import android.content.Intent;
import android.net.Uri;
import android.os.Build;
//使用FileProvider需要一定的配置
import androidx.core.content.FileProvider;

import java.io.File;
import java.util.Map;
import java.util.HashMap;

public class UpdateModule extends ReactContextBaseJavaModule {
private ReactApplicationContext context;
public UpdateModule(ReactApplicationContext reactContext) {
super(reactContext);
context = reactContext;
}

@Override
public String getName() {
return "Update";
}

@ReactMethod
public void installApp(final String path, Callback successCallback) {
try {
if (Build.VERSION.SDK_INT >= 24) { //android 7.0以后,处于安全考虑必须使用FileProvider打开文件
Uri contentUri = FileProvider.getUriForFile(context, "com.your-app-name.fileprovider", new File(path));

Intent intent = new Intent(Intent.ACTION_VIEW)
.setDataAndType(contentUri, "application/vnd.android.package-archive");

intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

context.startActivity(intent);
} else {
Intent intent = new Intent(Intent.ACTION_VIEW).setDataAndType(Uri.parse("file://" + path), "application/vnd.android.package-archive").setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
successCallback.invoke("安装成功");
} catch (Exception e) {
successCallback.invoke("安装失败");
}
}
}

更新下逻辑代码,下载成功后自动安装

1
2
3
4
5
6
7
8
9
10
......
ret.promise.then(res => {
// 下载成功
NativeModules.Update.installApp(downloadDest, (res) => {
// 安装成功
});
}).catch(err => {
ToastAndroid.show(err, ToastAndroid.LONG);
});
......
Do the best!