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加载,这为热部署提供了支持。下面一节,我们实现我们的热部署细节。
包括版本管理。