欢迎使用CSDN-markdown编辑器

React Native Android入门实战及深入源码分析系列(3)——热部署加载离线JSBundle文件

本Markdown编辑器使用[StackEdit][6]修改而来,用它写博客,将会带来全新的体验哦:
本文为老曾原创,转载需注明出处:http://blog.csdn.net/minimicall?viewmode=contents
上两节我们已经学些了如何编译源码,这一节我们来学习如何实施热部署。也就是更新我们的应用的时候,不需要发布版本,而是把JsBundle放到服务器上,然后下载下来,进行加载。
要实现这一步骤的第一个前提就是,加载离线JsBundle。

- 默认的MainActivity

我们首先来看工程默认的MainActivity 如下:

public class MainActivity extends ReactActivity {

    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "videolegend";
    }

    /**
     * Returns whether dev mode should be enabled.
     * This enables e.g. the dev menu.
     */
    @Override
    protected boolean getUseDeveloperSupport() {
       // return BuildConfig.DEBUG;
        return false;
    }

    /**
     * A list of packages used by the app. If the app uses additional views
     * or modules besides the default ones, add more packages here.
     */
    @Override
    protected List<ReactPackage> getPackages() {
        return Arrays.<ReactPackage>asList(
            new MainReactPackage()
        );
    }
}

然而,这段代码并不能看出什么鬼。只是注册了了一个MainReactPackage而已。
但是,它继承了ReactActivity。所以,我们可以把注意力放到ReactActivity上。

- ReactActivity

在ReactActivity里面有两个方法:

 /**
     * Returns the name of the bundle in assets. If this is null, and no file path is specified for
     * the bundle, the app will only work with {@code getUseDeveloperSupport} enabled and will
     * always try to load the JS bundle from the packager server.
     * 这句话很重要,如果 getUseDeveloperSupport开关被打开,那么它总是会从packager server拉取JS bundle文件。
     * e.g. "index.android.bundle"
     */
    protected @Nullable String getBundleAssetName() {
        return "index.android.bundle";
    };

    /**
     * Returns a custom path of the bundle file. This is used in cases the bundle should be loaded
     * from a custom path. By default it is loaded from Android assets, from a path specified这句话就是说,如果你要自定义bundle加载,那么就修改这个地方的返回。
     * by {@link }.getBundleAssetName
     * e.g. "file://sdcard/myapp_cache/index.android.bundle"
     */
    protected @Nullable String getJSBundleFile() {
        Toast.makeText(this,JS_BUNDLE_LOCAL_PATH,Toast.LENGTH_SHORT);
        return null;
    }

上面,我已经备注了。你要从手机的/sdcard/里面加载jsbundle,必须满足两个条件:
1、getJSBundleFile这里设置好返jsbundle的路径;
2、getUseDeveloperSupport开关必须要关闭。因为如果asset里面没有jsbundle,而且开关打开了,那么程序会从packager server获取jsbundle。

根据上面两个点,我们进行改动。这里贴出我们改动后的代码。

- 改动后的MainActivity

改动后的MainActivity主要是要关闭开发支持开关。
具体代码如下:

package com.videolegend;


import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;

import java.util.Arrays;
import java.util.List;

public class MainActivity extends KKReactBaseActivity {

    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "videolegend";
    }

    /**
     * Returns whether dev mode should be enabled.
     * This enables e.g. the dev menu.
     */
    @Override
    protected boolean getUseDeveloperSupport() {
       // return BuildConfig.DEBUG;关闭开发支持开关
        return false;
    }

    /**
     * A list of packages used by the app. If the app uses additional views
     * or modules besides the default ones, add more packages here.
     */
    @Override
    protected List<ReactPackage> getPackages() {
        return Arrays.<ReactPackage>asList(
            new MainReactPackage()
        );
    }
}

注意到这里我们的MainActivity不在是继承ReactActivity
而是继承KKReactBaseActivity,我们基本是拷贝了ReactActivity的代码。然后修改的。

package com.videolegend;

/**
 * Created by zengjinlong on 16/3/30.
 */

import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.provider.Settings;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.EditText;
import android.widget.Toast;

import com.facebook.common.logging.FLog;
import com.facebook.react.LifecycleState;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactRootView;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;

import java.io.File;
import java.util.List;

import javax.annotation.Nullable;

/**
 * @Author: zengjinlong
 * @Date: 2016-03-30
 * @purpose: 基于ReactActivity来改的,为了支持热部署,就最好支持离线bundle
 */
public abstract class KKReactBaseActivity extends Activity implements DefaultHardwareBackBtnHandler {
    private static final String TAG = "KKReactBaseActivity";
    private static final String REDBOX_PERMISSION_MESSAGE =
            "Overlay permissions needs to be granted in order for react native apps to run in dev mode";
    public static final String JS_BUNDLE_LOCAL_FILE = "videolegend.bundle";
    //JS bundle的本地加载路径
    public static final String JS_BUNDLE_LOCAL_PATH = Environment.getExternalStorageDirectory().toString() + File.separator + JS_BUNDLE_LOCAL_FILE;


    private @Nullable
    ReactInstanceManager mReactInstanceManager;
    private LifecycleState mLifecycleState = LifecycleState.BEFORE_RESUME;
    private boolean mDoRefresh = false;

    /**
     * Returns the name of the bundle in assets. If this is null, and no file path is specified for
     * the bundle, the app will only work with {@code getUseDeveloperSupport} enabled and will
     * always try to load the JS bundle from the packager server.
     * e.g. "index.android.bundle"
     */
    protected @Nullable String getBundleAssetName() {
        return "index.android.bundle";
    };

    /**
     * Returns a custom path of the bundle file. This is used in cases the bundle should be loaded
     * from a custom path. By default it is loaded from Android assets, from a path specified
     * by {@link }.getBundleAssetName
     * e.g. "file://sdcard/myapp_cache/index.android.bundle"
     */
    protected @Nullable String getJSBundleFile() {
        Toast.makeText(this,JS_BUNDLE_LOCAL_PATH,Toast.LENGTH_SHORT);
        return JS_BUNDLE_LOCAL_PATH;//这里返回jsbundle文件路径
       // return null;
    }

    /**
     * Returns the name of the main module. Determines the URL used to fetch the JS bundle
     * from the packager server. It is only used when dev support is enabled.
     * This is the first file to be executed once the {@link ReactInstanceManager} is created.
     * e.g. "index.android"
     */
    protected String getJSMainModuleName() {
        return "index.android";
    }

    /**
     * Returns the launchOptions which will be passed to the {@link ReactInstanceManager}
     * when the application is started. By default, this will return null and an empty
     * object will be passed to your top level component as its initial props.
     * If your React Native application requires props set outside of JS, override
     * this method to return the Android.os.Bundle of your desired initial props.
     */
    protected @Nullable
    Bundle getLaunchOptions() {
        return null;
    }

    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     * e.g. "MoviesApp"
     */
    protected abstract String getMainComponentName();

    /**
     * Returns whether dev mode should be enabled. This enables e.g. the dev menu.
     */
    protected abstract boolean getUseDeveloperSupport();

    /**
     * Returns a list of {@link ReactPackage} used by the app.
     * You'll most likely want to return at least the {@code MainReactPackage}.
     * If your app uses additional views or modules besides the default ones,
     * you'll want to include more packages here.
     */
    protected abstract List<ReactPackage> getPackages();

    /**
     * A subclass may override this method if it needs to use a custom instance.
     */
    protected ReactInstanceManager createReactInstanceManager() {
        ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setJSMainModuleName(getJSMainModuleName())
                .setUseDeveloperSupport(getUseDeveloperSupport())
                .setInitialLifecycleState(mLifecycleState);

        for (ReactPackage reactPackage : getPackages()) {
            builder.addPackage(reactPackage);
        }

        String jsBundleFile = getJSBundleFile();
        Log.d(TAG,"jsBundleFile:"+jsBundleFile);
        if (jsBundleFile != null) {
            Log.d(TAG,"setJSBundleFile now");
            builder.setJSBundleFile(jsBundleFile);
        } else {
            builder.setBundleAssetName(getBundleAssetName());
        }

        return builder.build();
    }

    /**
     * A subclass may override this method if it needs to use a custom {@link ReactRootView}.
     */
    protected ReactRootView createRootView() {
        return new ReactRootView(this);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
            // Get permission to show redbox in dev builds.
            if (!Settings.canDrawOverlays(this)) {
                Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                startActivity(serviceIntent);
                FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
                Toast.makeText(this, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
            }
        }

        mReactInstanceManager = createReactInstanceManager();
        ReactRootView mReactRootView = createRootView();
        mReactRootView.startReactApplication(mReactInstanceManager, getMainComponentName(), getLaunchOptions());
        setContentView(mReactRootView);
    }

    @Override
    protected void onPause() {
        super.onPause();

        mLifecycleState = LifecycleState.BEFORE_RESUME;

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostPause();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        mLifecycleState = LifecycleState.RESUMED;

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostResume(this, this);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.destroy();
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (mReactInstanceManager != null) {
            mReactInstanceManager.onActivityResult(requestCode, resultCode, data);
        }
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (mReactInstanceManager != null &&
                mReactInstanceManager.getDevSupportManager().getDevSupportEnabled()) {
            if (keyCode == KeyEvent.KEYCODE_MENU) {
                mReactInstanceManager.showDevOptionsDialog();
                return true;
            }
            if (keyCode == KeyEvent.KEYCODE_R && !(getCurrentFocus() instanceof EditText)) {
                // Enable double-tap-R-to-reload
                if (mDoRefresh) {
                    mReactInstanceManager.getDevSupportManager().handleReloadJS();
                    mDoRefresh = false;
                } else {
                    mDoRefresh = true;
                    new Handler().postDelayed(
                            new Runnable() {
                                @Override
                                public void run() {
                                    mDoRefresh = false;
                                }
                            },
                            200);
                }
            }
        }
        return super.onKeyUp(keyCode, event);
    }

    @Override
    public void onBackPressed() {
        if (mReactInstanceManager != null) {
            mReactInstanceManager.onBackPressed();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }
}

这样,我们就支持了离线的jsbundle加载,这为热部署提供了支持。下面一节,我们实现我们的热部署细节。
包括版本管理。

上一篇:PG中国开发者社区—王健PG峰会专访


下一篇:NTP时间服务构建