基于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版本
$ java -version
java version "1.8.0_121"
maven版本
$ mvn -v
Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T00:41:47+08:00)
node版本
$ node -v
v10.9.0
react native 版本
$ react-native -v
react-native-cli: 2.0.1
react-native: 0.57.8
Server开发
这部分比较简单,就三个接口。在spring.io初始化一个maven项目然后下载下来,用Eclipse或IntelliJ IDEA打开即可。
然后在pom.xml
增加web配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
增加以下三个接口
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开发
初始化项目
react-native init AModel
初始化完成之后可以用 react-native run-android
看一下运行效果。直接运行会在本地起一个node server,这个时候访问的js bundle就是访问的这个server上的。我们创建两个项目分别是AModel和BModel。具体可以看源码,这里不是我们的重点。
离线打包
这一步是把之前从node server访问的js文件打成离线包,方便动态加载,打包命令如下:
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:+"
的依赖。
react-native init host
创建好之后,删除MainApplication中多余的代码只保留以下代码。
public class MainApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
创建MainActivity
,在进入主页之后调用接口,获取有哪些模块可以加载
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);
}
});
}
}
});
}
点击可加载的模块,如果已经下载过,则直接打开,否则下载后打开
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
,这里面指定了要加载的模块的路径。
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;
}
}
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
├── AModel 模块A
├── BModel 模块B
├── host Android
└── rn-server 服务端