基于ReactNative实现动态加载

avatar
Android @奇舞团Android团队

背景

最近看到某厂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;
    }
}

运行效果如下:

有任何问题欢迎留言,源码地址github.com/77Y/react-n…

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

关注微信公众号,最新技术干货实时推送

image