Permalink
Browse files

Allow developers to load JavaScript bundle from any source.

Summary: This patch adds two pieces of functionality:

- Exposes `JSBundleLoader` to allow a developer to load JavaScript bundles as they choose.
- Adds `ReactBridge.loadScripFromFile` method which loads a JavaScript bundle from an arbitrary file path.

Example usage:

```
JSBundleLoader jsBundleLoader = new JSBundleLoader() {
    Override
    public void loadScript(ReactBridge reactBridge) {
        reactBridge.loadScriptFromFile("/sdcard/Download/index.android.bundle");
    }
};

mReactInstanceManager = ReactInstanceManager.builder()
        .setApplication(getApplication())
        .setJSBundleLoader(jsBundleLoader)
        .setJSMainModuleName("") /* necessary due to TODO(6803830) */
        .addPackage(new MainReactPackage())
        .setInitialLifecycleState(LifecycleState.RESUMED)
        .build();
```

cc ide
Closes #3189

Reviewed By: svcscm

Differential Revision: D2535819

Pulled By: mkonicek

fb-gh-sync-id: f319299dbe29bab3b7e91f94249c14b270d9fec3
  • Loading branch information...
arbesfeld authored and facebook-github-bot-7 committed Oct 28, 2015
1 parent b59f250 commit 3a743ef228a14e07c77c5488b080413643ec9c4b
@@ -77,7 +77,7 @@
private @Nullable ReactContextInitParams mPendingReactContextInitParams;
/* accessed from any thread */
- private final @Nullable String mBundleAssetName; /* name of JS bundle file in assets folder */
+ private @Nullable String mJSBundleFile; /* path to JS bundle on file system */
private final @Nullable String mJSMainModuleName; /* path to JS bundle root on packager server */
private final List<ReactPackage> mPackages;
private final DevSupportManager mDevSupportManager;
@@ -176,7 +176,7 @@ protected void onPostExecute(ReactApplicationContext reactContext) {
private ReactInstanceManager(
Context applicationContext,
- @Nullable String bundleAssetName,
+ @Nullable String jsBundleFile,
@Nullable String jsMainModuleName,
List<ReactPackage> packages,
boolean useDeveloperSupport,
@@ -185,7 +185,7 @@ private ReactInstanceManager(
initializeSoLoaderIfNecessary(applicationContext);
mApplicationContext = applicationContext;
- mBundleAssetName = bundleAssetName;
+ mJSBundleFile = jsBundleFile;
mJSMainModuleName = jsMainModuleName;
mPackages = packages;
mUseDeveloperSupport = useDeveloperSupport;
@@ -224,30 +224,29 @@ private static void initializeSoLoaderIfNecessary(Context applicationContext) {
SoLoader.init(applicationContext, /* native exopackage */ false);
}
+ public void setJSBundleFile(String jsBundleFile) {
+ mJSBundleFile = jsBundleFile;
+ }
+
/**
* Trigger react context initialization asynchronously in a background async task. This enables
* applications to pre-load the application JS, and execute global code before
* {@link ReactRootView} is available and measured.
*/
public void createReactContextInBackground() {
- if (mUseDeveloperSupport) {
+ if (mUseDeveloperSupport && mJSMainModuleName != null) {
if (mDevSupportManager.hasUpToDateJSBundleInCache()) {
// If there is a up-to-date bundle downloaded from server, always use that
onJSBundleLoadedFromServer();
- return;
- } else if (mBundleAssetName == null ||
- !mDevSupportManager.hasBundleInAssets(mBundleAssetName)) {
- // Bundle not available in assets, fetch from the server
+ } else {
mDevSupportManager.handleReloadJS();
- return;
}
+ return;
}
- // Use JS file from assets
+
recreateReactContextInBackground(
new JSCJavaScriptExecutor(),
- JSBundleLoader.createAssetLoader(
- mApplicationContext.getAssets(),
- mBundleAssetName));
+ JSBundleLoader.createFileLoader(mApplicationContext, mJSBundleFile));
}
/**
@@ -564,7 +563,7 @@ private void moveReactContextToCurrentLifecycleState(ReactApplicationContext rea
private final List<ReactPackage> mPackages = new ArrayList<>();
- private @Nullable String mBundleAssetName;
+ private @Nullable String mJSBundleFile;
private @Nullable String mJSMainModuleName;
private @Nullable NotThreadSafeBridgeIdleDebugListener mBridgeIdleDebugListener;
private @Nullable Application mApplication;
@@ -575,11 +574,21 @@ private Builder() {
}
/**
- * Name of the JS budle file to be loaded from application's raw assets.
+ * Name of the JS bundle file to be loaded from application's raw assets.
+ *
* Example: {@code "index.android.js"}
*/
public Builder setBundleAssetName(String bundleAssetName) {
- mBundleAssetName = bundleAssetName;
+ return this.setJSBundleFile("assets://" + bundleAssetName);
+ }
+
+ /**
+ * Path to the JS bundle file to be loaded from the file system.
+ *
+ * Example: {@code "assets://index.android.js" or "/sdcard/main.jsbundle}
+ */
+ public Builder setJSBundleFile(String jsBundleFile) {
+ mJSBundleFile = jsBundleFile;
return this;
}
@@ -639,21 +648,23 @@ public Builder setInitialLifecycleState(LifecycleState initialLifecycleState) {
* Before calling {@code build}, the following must be called:
* <ul>
* <li> {@link #setApplication}
- * <li> {@link #setBundleAssetName} or {@link #setJSMainModuleName}
+ * <li> {@link #setJSBundleFile} or {@link #setJSMainModuleName}
* </ul>
*/
public ReactInstanceManager build() {
Assertions.assertCondition(
- mUseDeveloperSupport || mBundleAssetName != null,
- "JS Bundle has to be provided in app assets when dev support is disabled");
+ mUseDeveloperSupport || mJSBundleFile != null,
+ "JS Bundle File has to be provided when dev support is disabled");
+
Assertions.assertCondition(
- mBundleAssetName != null || mJSMainModuleName != null,
- "Either BundleAssetName or MainModuleName needs to be provided");
+ mJSMainModuleName != null || mJSBundleFile != null,
+ "Either MainModuleName or JS Bundle File needs to be provided");
+
return new ReactInstanceManager(
Assertions.assertNotNull(
mApplication,
"Application property has not been set with this builder"),
- mBundleAssetName,
+ mJSBundleFile,
mJSMainModuleName,
mPackages,
mUseDeveloperSupport,
@@ -9,7 +9,7 @@
package com.facebook.react.bridge;
-import android.content.res.AssetManager;
+import android.content.Context;
/**
* A class that stores JS bundle information and allows {@link CatalystInstance} to load a correct
@@ -22,18 +22,22 @@
* should be used. JS bundle will be read from assets directory in native code to save on passing
* large strings from java to native memory.
*/
- public static JSBundleLoader createAssetLoader(
- final AssetManager assetManager,
- final String assetFileName) {
+ public static JSBundleLoader createFileLoader(
+ final Context context,
+ final String fileName) {
return new JSBundleLoader() {
@Override
public void loadScript(ReactBridge bridge) {
- bridge.loadScriptFromAssets(assetManager, assetFileName);
+ if (fileName.startsWith("assets://")) {
+ bridge.loadScriptFromAssets(context.getAssets(), fileName.replaceFirst("assets://", ""));
+ } else {
+ bridge.loadScriptFromFile(fileName, fileName);
+ }
}
@Override
public String getSourceUrl() {
- return "file:///android_asset/" + assetFileName;
+ return fileName;
}
};
}
@@ -51,7 +55,7 @@ public static JSBundleLoader createCachedBundleFromNetworkLoader(
return new JSBundleLoader() {
@Override
public void loadScript(ReactBridge bridge) {
- bridge.loadScriptFromNetworkCached(sourceURL, cachedFileLocation);
+ bridge.loadScriptFromFile(cachedFileLocation, sourceURL);
}
@Override
@@ -70,7 +74,7 @@ public static JSBundleLoader createRemoteDebuggerBundleLoader(
return new JSBundleLoader() {
@Override
public void loadScript(ReactBridge bridge) {
- bridge.loadScriptFromNetworkCached(sourceURL, null);
+ bridge.loadScriptFromFile(null, sourceURL);
}
@Override
@@ -65,7 +65,7 @@ private native void initialize(
* All native functions are not thread safe and appropriate queues should be used
*/
public native void loadScriptFromAssets(AssetManager assetManager, String assetName);
- public native void loadScriptFromNetworkCached(String sourceURL, @Nullable String tempFileName);
+ public native void loadScriptFromFile(@Nullable String fileName, @Nullable String sourceURL);
public native void callFunction(int moduleId, int methodId, NativeArray arguments);
public native void invokeCallback(int callbackID, NativeArray arguments);
public native void setGlobalVariable(String propertyName, String jsonEncodedArgument);
@@ -367,10 +367,18 @@ public void onReactInstanceDestroyed(ReactContext reactContext) {
}
public String getSourceMapUrl() {
+ if (mJSAppBundleName == null) {
+ return "";
+ }
+
return mDevServerHelper.getSourceMapUrl(Assertions.assertNotNull(mJSAppBundleName));
}
public String getSourceUrl() {
+ if (mJSAppBundleName == null) {
+ return "";
+ }
+
return mDevServerHelper.getSourceUrl(Assertions.assertNotNull(mJSAppBundleName));
}
@@ -638,38 +638,35 @@ static void loadScriptFromAssets(JNIEnv* env, jobject obj, jobject assetManager,
auto bridge = extractRefPtr<Bridge>(env, obj);
auto assetNameStr = fromJString(env, assetName);
- env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_start"));
+ env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromAssets_start"));
auto script = react::loadScriptFromAssets(env, assetManager, assetNameStr);
#ifdef WITH_FBSYSTRACE
FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_"
"executeApplicationScript",
"assetName", assetNameStr);
#endif
- env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_read"));
+ env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromAssets_read"));
executeApplicationScript(bridge, script, assetNameStr);
- env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_done"));
+ env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromAssets_done"));
}
-static void loadScriptFromNetworkCached(JNIEnv* env, jobject obj, jstring sourceURL,
- jstring tempFileName) {
+static void loadScriptFromFile(JNIEnv* env, jobject obj, jstring fileName, jstring sourceURL) {
jclass markerClass = env->FindClass("com/facebook/react/bridge/ReactMarker");
auto bridge = jni::extractRefPtr<Bridge>(env, obj);
- std::string script = "";
- env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_start"));
- if (tempFileName != NULL) {
- script = react::loadScriptFromFile(jni::fromJString(env, tempFileName));
- }
+ auto fileNameStr = fileName == NULL ? "" : fromJString(env, fileName);
+ env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromFile_start"));
+ auto script = fileName == NULL ? "" : react::loadScriptFromFile(fileNameStr);
#ifdef WITH_FBSYSTRACE
- auto sourceURLStr = fromJString(env, sourceURL);
+ auto sourceURLStr = sourceURL == NULL ? fileNameStr : fromJString(env, sourceURL);
FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "reactbridge_jni_"
"executeApplicationScript",
"sourceURL", sourceURLStr);
#endif
- env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_read"));
+ env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromFile_read"));
executeApplicationScript(bridge, script, jni::fromJString(env, sourceURL));
- env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromNetworkCached_exec"));
+ env->CallStaticVoidMethod(markerClass, gLogMarkerMethod, env->NewStringUTF("loadScriptFromFile_exec"));
}
static void callFunction(JNIEnv* env, jobject obj, jint moduleId, jint methodId,
@@ -821,7 +818,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
makeNativeMethod(
"loadScriptFromAssets", "(Landroid/content/res/AssetManager;Ljava/lang/String;)V",
bridge::loadScriptFromAssets),
- makeNativeMethod("loadScriptFromNetworkCached", bridge::loadScriptFromNetworkCached),
+ makeNativeMethod("loadScriptFromFile", bridge::loadScriptFromFile),
makeNativeMethod("callFunction", bridge::callFunction),
makeNativeMethod("invokeCallback", bridge::invokeCallback),
makeNativeMethod("setGlobalVariable", bridge::setGlobalVariable),

16 comments on commit 3a743ef

@icefoggy

This comment has been minimized.

Show comment
Hide comment
@icefoggy

icefoggy Oct 29, 2015

Good news for Android developers!

Good news for Android developers!

@mkonicek

This comment has been minimized.

Show comment
Hide comment
@mkonicek

mkonicek Oct 30, 2015

Contributor

🎉 🎈

Contributor

mkonicek replied Oct 30, 2015

🎉 🎈

@mariostallone

This comment has been minimized.

Show comment
Hide comment
@mariostallone

mariostallone Nov 3, 2015

Contributor

Awesome!!!

Contributor

mariostallone replied Nov 3, 2015

Awesome!!!

@sailei1

This comment has been minimized.

Show comment
Hide comment
@sailei1

sailei1 Nov 19, 2015

supports ios ?

supports ios ?

@hufeng

This comment has been minimized.

Show comment
Hide comment

👍

@brentvatne

This comment has been minimized.

Show comment
Hide comment
@brentvatne

brentvatne Nov 21, 2015

Collaborator

@sailei1 - this was already possible on ios

Collaborator

brentvatne replied Nov 21, 2015

@sailei1 - this was already possible on ios

@sailei1

This comment has been minimized.

Show comment
Hide comment
@Richard-Cao

This comment has been minimized.

Show comment
Hide comment
@Richard-Cao

Richard-Cao Nov 30, 2015

setJSBundleLoader no method
android gradle compile version 0.16.0

setJSBundleLoader no method
android gradle compile version 0.16.0

@satya164

This comment has been minimized.

Show comment
Hide comment
@satya164

satya164 Nov 30, 2015

Collaborator

@Richard-Cao It's called setJSBundleFile and it takes a string (the file path) as argument.

Collaborator

satya164 replied Nov 30, 2015

@Richard-Cao It's called setJSBundleFile and it takes a string (the file path) as argument.

@Richard-Cao

This comment has been minimized.

Show comment
Hide comment
@Richard-Cao

Richard-Cao Nov 30, 2015

I know, but how to use custom JSBundleLoader? @satya164

I know, but how to use custom JSBundleLoader? @satya164

@satya164

This comment has been minimized.

Show comment
Hide comment
@Richard-Cao

This comment has been minimized.

Show comment
Hide comment
@Richard-Cao

Richard-Cao Dec 1, 2015

thanks, I will try @satya164

thanks, I will try @satya164

@Richard-Cao

This comment has been minimized.

Show comment
Hide comment
@Richard-Cao

Richard-Cao Dec 1, 2015

https://facebook.github.io/react-native/docs/embedded-app-android.html#sharing-a-reactinstance-across-multiple-activities-fragments-in-your-app
this suggest have a singleton "holder" that holds a ReactInstanceManager

but your code if want to download bundle, when Activity init, ReactInstanceManager not a singleton

how @satya164

https://facebook.github.io/react-native/docs/embedded-app-android.html#sharing-a-reactinstance-across-multiple-activities-fragments-in-your-app
this suggest have a singleton "holder" that holds a ReactInstanceManager

but your code if want to download bundle, when Activity init, ReactInstanceManager not a singleton

how @satya164

@satya164

This comment has been minimized.

Show comment
Hide comment
@satya164

satya164 Dec 1, 2015

Collaborator

@Richard-Cao You can write another singleton class which just uses a single instance of the bundlemanager.

Collaborator

satya164 replied Dec 1, 2015

@Richard-Cao You can write another singleton class which just uses a single instance of the bundlemanager.

@Richard-Cao

This comment has been minimized.

Show comment
Hide comment
@mtostenson

This comment has been minimized.

Show comment
Hide comment
@mtostenson

mtostenson Dec 8, 2015

Contributor

Just what I was waiting for!

Contributor

mtostenson replied Dec 8, 2015

Just what I was waiting for!

Please sign in to comment.