基于reactnative实现动态加载

背景

最近看到某厂Android端物联网Demo演示应用中可动态加载模块,具体操作是在后台拖拽生成一个模块和链接地址。然后在Android端刷新首页即可看到新添加的模块。下载Demo代码之后发现用到了facebook开源的react-native框架。然后打算研究一下是否能模拟动态下发模块的效果。

于是决定从以下几个方面来实现这个过程。

1、服务端——实现首页接口及下载接口

服务端用Spring Boot来实现,Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。对于开发微服务非常便捷。

2、Android端——显示可加载的模块以及下载模块

即一个Android工程

3、两个动态下发的模块(jsbundle)

React Native实现

环境搭建

1、jdk

针对不同操作系统下载安装即可 下载地址

2、maven

Apache Maven,是一个软件(特别是Java软件)项目管理及自动构建工具,类似于Android中的Gradle。下载地址

3、nodejs

Node.js是一个基于Chrome V8引擎的JavaScript运行时。下载地址

4、android环境

大家都懂

5、react native

在终端执行npm install -g react-native-cli

部分可能需要手动配置环境变量,全部安装完成后,来看一下我本地各个软件的版本

jdk版本

1
2
$ java -version
java version "1.8.0_121"

maven版本

1
2
$ mvn -v
Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T00:41:47+08:00)

node版本

1
2
$ node -v
v10.9.0

react native 版本

1
2
3
$ react-native -v
react-native-cli: 2.0.1
react-native: 0.57.8

Server开发

这部分比较简单,就三个接口。在spring.io初始化一个maven项目然后下载下来,用Eclipse或IntelliJ IDEA打开即可。

然后在pom.xml增加web配置

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

增加以下三个接口

1
2
3
4
5
6
7
8
Android端获取首页配置
@RequestMapping(value = "/home", method = RequestMethod.GET)

react端获取bundle信息
@RequestMapping(value = "/bundle/{id}", method = RequestMethod.GET)

Android下载bundle
@RequestMapping(value = "/download/bundle/{name}", method = RequestMethod.GET, produces = "application/zip")

接口的具体实现可以看源码,都比较简单。

React Native开发

初始化项目

1
react-native init AModel

初始化完成之后可以用 react-native run-android看一下运行效果。直接运行会在本地起一个node server,这个时候访问的js bundle就是访问的这个server上的。我们创建两个项目分别是AModel和BModel。具体可以看源码,这里不是我们的重点。

离线打包

这一步是把之前从node server访问的js文件打成离线包,方便动态加载,打包命令如下:

1
2
3
4
5
6
7
8
9
react-native AModel --entry-file index.js --bundle-output ./AModel/AModel.bundle --platform android --assets-dest ./bundle --dev false

react-native BModel --entry-file index.js --bundle-output ./BModel/BModel.bundle --platform android --assets-dest ./bundle --dev false

//entry-file JS文件的路径
//bundle-output JSbundle文件的生成目录
//platform 平台 Android或iOS
//assets-dest 资源文件的生成目录
//dev 开发模式

然后把两个bundle分别打成zip包。

Android开发

仍然用react native生成一个工程,我们只用它的Android工程,之所以不直接使用Android Studio生成是为了使用react native添加好的"com.facebook.react:react-native:+"的依赖。

1
react-native init host

创建好之后,删除MainApplication中多余的代码只保留以下代码。

1
2
3
4
5
6
7
8
public class MainApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}

创建MainActivity,在进入主页之后调用接口,获取有哪些模块可以加载

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
55
56
private void getHomeInfo() {

OkHttpClient okHttpClient = new OkHttpClient();
//192.168.100.14是本地server的ip地址,保证手机和电脑在统一局域网
Request request = new Request.Builder().url("http://192.168.100.14:8080/home").method("GET", null).build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {

@Override
public void onFailure(Call call, IOException e) {
}

@Override
public void onResponse(Call call, Response response) throws IOException {

HomeResponse homeResponse = new Gson().fromJson(response.body().string(), HomeResponse.class);

for (final HomeResponse.Bundle bundle : homeResponse.data) {

runOnUiThread(new Runnable() {
@Override
public void run() {

Button button = new Button(MainActivity.this);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(10, 10, 10, 10);
button.setLayoutParams(params);
button.setText(bundle.desc);
button.setTextColor(Color.WHITE);
button.setBackground(getResources().getDrawable(R.drawable.bg));
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

// 检查是否下载过,如果已经下载过则直接打开
String f = MainActivity.this.getFilesDir().getAbsolutePath() + "/" + bundle.name + "/" + bundle.name + ".bundle";
File file = new File((f));
if (file.exists()) {
DispatchUtils.dispatchModel = bundle.name;
DispatchActivity.start(MainActivity.this);
} else {
download(bundle.name);
}

}
});

linearLayout.addView(button);
}
});

}

}
});
}

点击可加载的模块,如果已经下载过,则直接打开,否则下载后打开

1
2
3
4
5
6
7
8
9
10
try {
//下载之后解压,然后打开
ZipUtils.unzip(MainActivity.this.getFilesDir().getAbsolutePath() + "/" + bundleName + ".zip", MainActivity.this.getFilesDir().getAbsolutePath());

DispatchUtils.dispatchModel = bundleName;
DispatchActivity.start(MainActivity.this);

} catch (Exception e) {
e.printStackTrace();
}

重点

这里的重点是,统一个模块分发的DispatchActivity作为入口,所有的模块打开都走这里。然后重写createReactActivityDelegate,这里面指定了要加载的模块的路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DispatchActivity extends ReactActivity {

public static void start(Context context) {
Intent starter = new Intent(context, DispatchActivity.class);
context.startActivity(starter);
}

@Override
protected ReactActivityDelegate createReactActivityDelegate() {

DispatchDelegate delegate = new DispatchDelegate(this, DispatchUtils.dispatchModel);

return delegate;
}
}
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
public class DispatchDelegate extends ReactActivityDelegate {

private Activity activity;
private String bundleName;


public DispatchDelegate(Activity activity, @Nullable String mainComponentName) {
super(activity, mainComponentName);
this.activity = activity;
this.bundleName = mainComponentName;
}

@Override
protected ReactNativeHost getReactNativeHost() {

ReactNativeHost mReactNativeHost = new ReactNativeHost(activity.getApplication()) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}

@Nullable
@Override
protected String getJSBundleFile() {
// 这里指定JSBundleFile的入口,从而实现加载不同的模块
String file = activity.getFilesDir().getAbsolutePath() + "/" + bundleName + "/" + bundleName + ".bundle";
return file;
}

@Nullable
@Override
protected String getBundleAssetName() {
return bundleName + ".bundle";
}

@Override
protected String getJSMainModuleName() {
return "index";
}
};
return mReactNativeHost;
}
}

运行效果如下:

有任何问题欢迎留言,源码地址https://github.com/77Y/react-native-spring

1
2
3
4
├── AModel  模块A
├── BModel 模块B
├── host Android
└── rn-server 服务端