react-native-windows/ReactWindows/ReactNative.Shared/ReactInstanceManager.cs

854 строки
31 KiB
C#

using ReactNative.Bridge;
using ReactNative.Bridge.Queue;
using ReactNative.Chakra.Executor;
using ReactNative.Common;
using ReactNative.DevSupport;
using ReactNative.Modules.Core;
using ReactNative.Touch;
using ReactNative.Tracing;
using ReactNative.UIManager;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using static System.FormattableString;
namespace ReactNative
{
/// <summary>
/// This interface manages instances of <see cref="IReactInstance" />.
/// It exposes a way to configure React instances using
/// <see cref="IReactPackage"/> and keeps track of the lifecycle of that
/// instance. It also sets up a connection between the instance and the
/// developer support functionality of the framework.
///
/// An instance of this manager is required to start the JavaScript
/// application in <see cref="ReactRootView"/>
/// (<see cref="ReactRootView.StartReactApplication(IReactInstanceManager, string)"/>).
///
/// The lifecycle of the instance of <see cref="IReactInstanceManager"/>
/// should be bound to the application that owns the
/// <see cref="ReactRootView"/> that is used to render the React
/// application using this instance manager. It is required to pass
/// lifecycle events to the instance manager (i.e., <see cref="OnSuspend"/>,
/// <see cref="IAsyncDisposable.DisposeAsync"/>, and <see cref="OnResume(Action)"/>).
/// </summary>
public class ReactInstanceManager : IReactInstanceManager
{
private readonly List<ReactRootView> _attachedRootViews = new List<ReactRootView>();
private readonly string _jsBundleFile;
private readonly string _jsMainModuleName;
private readonly IReadOnlyList<IReactPackage> _packages;
private readonly IDevSupportManager _devSupportManager;
private readonly bool _useDeveloperSupport;
private readonly UIImplementationProvider _uiImplementationProvider;
private readonly Func<IJavaScriptExecutor> _javaScriptExecutorFactory;
private readonly Action<Exception> _nativeModuleCallExceptionHandler;
private LifecycleState _lifecycleState;
private bool _hasStartedCreatingInitialContext;
private Task _contextInitializationTask;
private Func<IJavaScriptExecutor> _pendingJsExecutorFactory;
private JavaScriptBundleLoader _pendingJsBundleLoader;
private string _sourceUrl;
private ReactContext _currentReactContext;
private Action _defaultBackButtonHandler;
/// <summary>
/// Event triggered when a React context has been initialized.
/// </summary>
public event EventHandler<ReactContextInitializedEventArgs> ReactContextInitialized;
private ReactInstanceManager(
string jsBundleFile,
string jsMainModuleName,
IReadOnlyList<IReactPackage> packages,
bool useDeveloperSupport,
LifecycleState initialLifecycleState,
UIImplementationProvider uiImplementationProvider,
Func<IJavaScriptExecutor> javaScriptExecutorFactory,
Action<Exception> nativeModuleCallExceptionHandler)
{
if (packages == null)
throw new ArgumentNullException(nameof(packages));
if (uiImplementationProvider == null)
throw new ArgumentNullException(nameof(uiImplementationProvider));
if (javaScriptExecutorFactory == null)
throw new ArgumentNullException(nameof(javaScriptExecutorFactory));
_jsBundleFile = jsBundleFile;
_jsMainModuleName = jsMainModuleName;
_packages = packages;
_useDeveloperSupport = useDeveloperSupport;
_devSupportManager = _useDeveloperSupport
? (IDevSupportManager)new DevSupportManager(
new ReactInstanceDevCommandsHandler(this),
_jsBundleFile,
_jsMainModuleName)
: new DisabledDevSupportManager();
_lifecycleState = initialLifecycleState;
_uiImplementationProvider = uiImplementationProvider;
_javaScriptExecutorFactory = javaScriptExecutorFactory;
_nativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler;
}
/// <summary>
/// The developer support manager for the instance.
/// </summary>
public IDevSupportManager DevSupportManager
{
get
{
return _devSupportManager;
}
}
/// <summary>
/// Signals whether <see cref="CreateReactContextInBackground"/> has
/// been called. Will return <code>false</code> after
/// <see cref="IAsyncDisposable.DisposeAsync"/> until a new initial
/// context has been created.
/// </summary>
public bool HasStartedCreatingInitialContext
{
get
{
return _hasStartedCreatingInitialContext;
}
}
/// <summary>
/// The URL where the last bundle was loaded from.
/// </summary>
public string SourceUrl
{
get
{
return _sourceUrl;
}
}
/// <summary>
/// Gets the current React context instance.
/// </summary>
public ReactContext CurrentReactContext
{
get
{
return _currentReactContext;
}
}
/// <summary>
/// Trigger the React context initialization asynchronously in a
/// background task. This enables applications to pre-load the
/// application JavaScript, and execute global core code before the
/// <see cref="ReactRootView"/> is available and measure. This should
/// only be called the first time the application is set up, which is
/// enforced to keep developers from accidentally creating their
/// applications multiple times.
/// </summary>
public async void CreateReactContextInBackground()
{
await CreateReactContextInBackgroundAsync().ConfigureAwait(false);
}
/// <summary>
/// Trigger the React context initialization asynchronously in a
/// background task. This enables applications to pre-load the
/// application JavaScript, and execute global core code before the
/// <see cref="ReactRootView"/> is available and measure. This should
/// only be called the first time the application is set up, which is
/// enforced to keep developers from accidentally creating their
/// applications multiple times.
/// </summary>
/// <returns>A task to await the result.</returns>
internal async Task CreateReactContextInBackgroundAsync()
{
if (_hasStartedCreatingInitialContext)
{
throw new InvalidOperationException(
"React context creation should only be called when creating the React " +
"application for the first time. When reloading JavaScript, e.g., from " +
"a new file, explicitly, use the re-create method.");
}
_hasStartedCreatingInitialContext = true;
await RecreateReactContextInBackgroundInnerAsync().ConfigureAwait(false);
}
/// <summary>
/// Recreate the React application and context. This should be called
/// if configuration has changed or the developer has requested the
/// application to be reloaded.
/// </summary>
public async void RecreateReactContextInBackground()
{
await RecreateReactContextInBackgroundAsync().ConfigureAwait(false);
}
/// <summary>
/// Recreate the React application and context. This should be called
/// if configuration has changed or the developer has requested the
/// application to be reloaded.
/// </summary>
/// <returns>A task to await the result.</returns>
internal async Task RecreateReactContextInBackgroundAsync()
{
if (!_hasStartedCreatingInitialContext)
{
throw new InvalidOperationException(
"React context re-creation should only be called after the initial " +
"create context background call.");
}
await RecreateReactContextInBackgroundInnerAsync().ConfigureAwait(false);
}
/// <summary>
/// Method that gives JavaScript the opportunity to consume the back
/// button event. If JavaScript does not consume the event, the
/// default back press action will be invoked at the end of the
/// roundtrip to JavaScript.
/// </summary>
public void OnBackPressed()
{
DispatcherHelpers.AssertOnDispatcher();
var reactContext = _currentReactContext;
if (reactContext == null)
{
Tracer.Write(ReactConstants.Tag, "Instance detached from instance manager.");
InvokeDefaultOnBackPressed();
}
else
{
reactContext.GetNativeModule<DeviceEventManagerModule>().EmitHardwareBackPressed();
}
}
/// <summary>
/// Called when the application is suspended.
/// </summary>
public void OnSuspend()
{
DispatcherHelpers.AssertOnDispatcher();
_lifecycleState = LifecycleState.BeforeResume;
_defaultBackButtonHandler = null;
if (_useDeveloperSupport)
{
_devSupportManager.IsEnabled = false;
}
var currentReactContext = _currentReactContext;
if (currentReactContext != null)
{
_currentReactContext.OnSuspend();
}
}
/// <summary>
/// Used when the application resumes to reset the back button handling
/// in JavaScript.
/// </summary>
/// <param name="onBackPressed">
/// The action to take when back is pressed.
/// </param>
public void OnResume(Action onBackPressed)
{
if (onBackPressed == null)
throw new ArgumentNullException(nameof(onBackPressed));
DispatcherHelpers.AssertOnDispatcher();
_lifecycleState = LifecycleState.Resumed;
_defaultBackButtonHandler = onBackPressed;
if (_useDeveloperSupport)
{
_devSupportManager.IsEnabled = true;
}
var currentReactContext = _currentReactContext;
if (currentReactContext != null)
{
currentReactContext.OnResume();
}
}
/// <summary>
/// Destroy the <see cref="IReactInstanceManager"/>.
/// </summary>
public async Task DisposeAsync()
{
DispatcherHelpers.AssertOnDispatcher();
// TODO: memory pressure hooks
if (_useDeveloperSupport)
{
_devSupportManager.IsEnabled = false;
}
var currentReactContext = _currentReactContext;
if (currentReactContext != null)
{
await currentReactContext.DisposeAsync().ConfigureAwait(false);
_currentReactContext = null;
_hasStartedCreatingInitialContext = false;
}
}
/// <summary>
/// Attach given <paramref name="rootView"/> to a React instance
/// manager and start the JavaScript application using the JavaScript
/// module provided by the <see cref="ReactRootView.JavaScriptModuleName"/>. If
/// the React context is currently being (re-)created, or if the react
/// context has not been created yet, the JavaScript application
/// associated with the provided root view will be started
/// asynchronously. This view will then be tracked by this manager and
/// in case of React instance restart, it will be re-attached.
/// </summary>
/// <param name="rootView">The root view.</param>
public void AttachMeasuredRootView(ReactRootView rootView)
{
if (rootView == null)
throw new ArgumentNullException(nameof(rootView));
DispatcherHelpers.AssertOnDispatcher();
_attachedRootViews.Add(rootView);
// If the React context is being created in the background, the
// JavaScript application will be started automatically when
// creation completes, as root view is part of the attached root
// view list.
var currentReactContext = _currentReactContext;
if (_contextInitializationTask == null && currentReactContext != null)
{
AttachMeasuredRootViewToInstance(rootView, currentReactContext.ReactInstance);
}
}
/// <summary>
/// Detach given <paramref name="rootView"/> from the current react
/// instance. This method is idempotent and can be called multiple
/// times on the same <see cref="ReactRootView"/> instance.
/// </summary>
/// <param name="rootView">The root view.</param>
public void DetachRootView(ReactRootView rootView)
{
if (rootView == null)
throw new ArgumentNullException(nameof(rootView));
DispatcherHelpers.AssertOnDispatcher();
if (_attachedRootViews.Remove(rootView))
{
var currentReactContext = _currentReactContext;
if (currentReactContext != null && currentReactContext.HasActiveReactInstance)
{
DetachViewFromInstance(rootView, currentReactContext.ReactInstance);
}
}
}
/// <summary>
/// Uses the configured <see cref="IReactPackage"/> instances to create
/// all <see cref="IViewManager"/> instances.
/// </summary>
/// <param name="reactContext">
/// The application context.
/// </param>
/// <returns>The list of view managers.</returns>
public IReadOnlyList<IViewManager> CreateAllViewManagers(ReactContext reactContext)
{
if (reactContext == null)
throw new ArgumentNullException(nameof(reactContext));
using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "createAllViewManagers").Start())
{
var allViewManagers = new List<IViewManager>();
foreach (var package in _packages)
{
allViewManagers.AddRange(
package.CreateViewManagers(reactContext));
}
return allViewManagers;
}
}
private async Task RecreateReactContextInBackgroundInnerAsync()
{
DispatcherHelpers.AssertOnDispatcher();
if (_useDeveloperSupport && _jsBundleFile == null && _jsMainModuleName != null)
{
if (await _devSupportManager.HasUpToDateBundleInCacheAsync())
{
OnJavaScriptBundleLoadedFromServer();
}
else
{
_devSupportManager.HandleReloadJavaScript();
}
}
else
{
RecreateReactContextInBackgroundFromBundleFile();
}
}
private void RecreateReactContextInBackgroundFromBundleFile()
{
RecreateReactContextInBackground(
_javaScriptExecutorFactory,
JavaScriptBundleLoader.CreateFileLoader(_jsBundleFile));
}
private void InvokeDefaultOnBackPressed()
{
DispatcherHelpers.AssertOnDispatcher();
_defaultBackButtonHandler?.Invoke();
}
private void OnReloadWithJavaScriptDebugger(Func<IJavaScriptExecutor> javaScriptExecutorFactory)
{
RecreateReactContextInBackground(
javaScriptExecutorFactory,
JavaScriptBundleLoader.CreateRemoteDebuggerLoader(
_devSupportManager.JavaScriptBundleUrlForRemoteDebugging,
_devSupportManager.SourceUrl));
}
private void OnJavaScriptBundleLoadedFromServer()
{
RecreateReactContextInBackground(
_javaScriptExecutorFactory,
JavaScriptBundleLoader.CreateCachedBundleFromNetworkLoader(
_devSupportManager.SourceUrl,
_devSupportManager.DownloadedJavaScriptBundleFile));
}
private void RecreateReactContextInBackground(
Func<IJavaScriptExecutor> jsExecutorFactory,
JavaScriptBundleLoader jsBundleLoader)
{
if (_contextInitializationTask == null)
{
_contextInitializationTask = InitializeReactContextAsync(jsExecutorFactory, jsBundleLoader);
}
else
{
_pendingJsExecutorFactory = jsExecutorFactory;
_pendingJsBundleLoader = jsBundleLoader;
}
}
private async Task InitializeReactContextAsync(
Func<IJavaScriptExecutor> jsExecutorFactory,
JavaScriptBundleLoader jsBundleLoader)
{
var currentReactContext = _currentReactContext;
if (currentReactContext != null)
{
await TearDownReactContextAsync(currentReactContext);
_currentReactContext = null;
}
try
{
var reactContext = await CreateReactContextAsync(jsExecutorFactory, jsBundleLoader);
SetupReactContext(reactContext);
}
catch (Exception ex)
{
_devSupportManager.HandleException(ex);
}
finally
{
_contextInitializationTask = null;
}
if (_pendingJsExecutorFactory != null)
{
var pendingJsExecutorFactory = _pendingJsExecutorFactory;
var pendingJsBundleLoader = _pendingJsBundleLoader;
_pendingJsExecutorFactory = null;
_pendingJsBundleLoader = null;
RecreateReactContextInBackground(
pendingJsExecutorFactory,
pendingJsBundleLoader);
}
}
private void SetupReactContext(ReactContext reactContext)
{
DispatcherHelpers.AssertOnDispatcher();
if (_currentReactContext != null)
{
throw new InvalidOperationException(
"React context has already been setup and has not been destroyed.");
}
_currentReactContext = reactContext;
var reactInstance = reactContext.ReactInstance;
_devSupportManager.OnNewReactContextCreated(reactContext);
// TODO: set up memory pressure hooks
MoveReactContextToCurrentLifecycleState(reactContext);
foreach (var rootView in _attachedRootViews)
{
AttachMeasuredRootViewToInstance(rootView, reactInstance);
}
OnReactContextInitialized(reactContext);
}
private void AttachMeasuredRootViewToInstance(
ReactRootView rootView,
IReactInstance reactInstance)
{
DispatcherHelpers.AssertOnDispatcher();
// Reset view content as it's going to be populated by the
// application content from JavaScript
rootView.TouchHandler?.Dispose();
rootView.Children.Clear();
rootView.Tag = null;
var uiManagerModule = reactInstance.GetNativeModule<UIManagerModule>();
var rootTag = uiManagerModule.AddMeasuredRootView(rootView);
rootView.TouchHandler = new TouchHandler(rootView);
var jsAppModuleName = rootView.JavaScriptModuleName;
var appParameters = new Dictionary<string, object>
{
{ "rootTag", rootTag },
{ "initialProps", rootView.InitialProps }
};
reactInstance.GetJavaScriptModule<AppRegistry>().runApplication(jsAppModuleName, appParameters);
}
private void DetachViewFromInstance(ReactRootView rootView, IReactInstance reactInstance)
{
DispatcherHelpers.AssertOnDispatcher();
reactInstance.GetJavaScriptModule<AppRegistry>().unmountApplicationComponentAtRootTag(rootView.GetTag());
}
private async Task TearDownReactContextAsync(ReactContext reactContext)
{
DispatcherHelpers.AssertOnDispatcher();
if (_lifecycleState == LifecycleState.Resumed)
{
reactContext.OnSuspend();
}
foreach (var rootView in _attachedRootViews)
{
DetachViewFromInstance(rootView, reactContext.ReactInstance);
}
await reactContext.DisposeAsync();
_devSupportManager.OnReactContextDestroyed(reactContext);
// TODO: add memory pressure hooks
}
private async Task<ReactContext> CreateReactContextAsync(
Func<IJavaScriptExecutor> jsExecutorFactory,
JavaScriptBundleLoader jsBundleLoader)
{
Tracer.Write(ReactConstants.Tag, "Creating React context.");
_sourceUrl = jsBundleLoader.SourceUrl;
var nativeRegistryBuilder = new NativeModuleRegistry.Builder();
var jsModulesBuilder = new JavaScriptModuleRegistry.Builder();
var reactContext = new ReactContext();
if (_useDeveloperSupport)
{
reactContext.NativeModuleCallExceptionHandler = _devSupportManager.HandleException;
}
using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "createAndProcessCoreModulesPackage").Start())
{
var coreModulesPackage =
new CoreModulesPackage(this, InvokeDefaultOnBackPressed, _uiImplementationProvider);
ProcessPackage(coreModulesPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder);
}
foreach (var reactPackage in _packages)
{
using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "createAndProcessCustomReactPackage").Start())
{
ProcessPackage(reactPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder);
}
}
var nativeModuleRegistry = default(NativeModuleRegistry);
using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "buildNativeModuleRegistry").Start())
{
nativeModuleRegistry = nativeRegistryBuilder.Build();
}
var exceptionHandler = _nativeModuleCallExceptionHandler ?? _devSupportManager.HandleException;
var reactInstanceBuilder = new ReactInstance.Builder
{
QueueConfigurationSpec = ReactQueueConfigurationSpec.Default,
JavaScriptExecutorFactory = jsExecutorFactory,
Registry = nativeModuleRegistry,
JavaScriptModuleRegistry = jsModulesBuilder.Build(),
BundleLoader = jsBundleLoader,
NativeModuleCallExceptionHandler = exceptionHandler,
};
var reactInstance = default(ReactInstance);
using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "createReactInstance").Start())
{
reactInstance = reactInstanceBuilder.Build();
}
// TODO: add bridge idle debug listener
reactContext.InitializeWithInstance(reactInstance);
reactInstance.Initialize();
using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "RunJavaScriptBundle").Start())
{
await reactInstance.InitializeBridgeAsync().ConfigureAwait(false);
}
return reactContext;
}
private void ProcessPackage(
IReactPackage reactPackage,
ReactContext reactContext,
NativeModuleRegistry.Builder nativeRegistryBuilder,
JavaScriptModuleRegistry.Builder jsModulesBuilder)
{
foreach (var nativeModule in reactPackage.CreateNativeModules(reactContext))
{
nativeRegistryBuilder.Add(nativeModule);
}
foreach (var type in reactPackage.CreateJavaScriptModulesConfig())
{
jsModulesBuilder.Add(type);
}
}
private void MoveReactContextToCurrentLifecycleState(ReactContext reactContext)
{
if (_lifecycleState == LifecycleState.Resumed)
{
reactContext.OnResume();
}
}
private void OnReactContextInitialized(ReactContext reactContext)
{
ReactContextInitialized?
.Invoke(this, new ReactContextInitializedEventArgs(reactContext));
}
private void ToggleElementInspector()
{
_currentReactContext?
.GetJavaScriptModule<RCTDeviceEventEmitter>()
.emit("toggleElementInspector", null);
}
/// <summary>
/// A Builder responsible for creating a React Instance Manager.
/// </summary>
public sealed class Builder
{
private List<IReactPackage> _packages = new List<IReactPackage>();
private bool _useDeveloperSupport;
private string _jsBundleFile;
private string _jsMainModuleName;
private LifecycleState? _initialLifecycleState;
private UIImplementationProvider _uiImplementationProvider;
private Func<IJavaScriptExecutor> _javaScriptExecutorFactory;
private Action<Exception> _nativeModuleCallExceptionHandler;
/// <summary>
/// A provider of <see cref="UIImplementation" />.
/// </summary>
public UIImplementationProvider UIImplementationProvider
{
set
{
_uiImplementationProvider = value;
}
}
/// <summary>
/// Path to the JavaScript bundle file to be loaded from the file
/// system.
/// </summary>
public string JavaScriptBundleFile
{
set
{
_jsBundleFile = value;
}
}
/// <summary>
/// Path to the applications main module on the packager server.
/// </summary>
public string JavaScriptMainModuleName
{
set
{
_jsMainModuleName = value;
}
}
/// <summary>
/// The mutable list of React packages.
/// </summary>
public List<IReactPackage> Packages
{
get
{
return _packages;
}
}
/// <summary>
/// Signals whether the application should enable developer support.
/// </summary>
public bool UseDeveloperSupport
{
set
{
_useDeveloperSupport = value;
}
}
/// <summary>
/// The initial lifecycle state of the host.
/// </summary>
public LifecycleState InitialLifecycleState
{
set
{
_initialLifecycleState = value;
}
}
/// <summary>
/// Instantiates the JavaScript executor.
/// </summary>
public Func<IJavaScriptExecutor> JavaScriptExecutorFactory
{
set
{
_javaScriptExecutorFactory = value;
}
}
/// <summary>
/// The exception handler for all native module calls.
/// </summary>
public Action<Exception> NativeModuleCallExceptionHandler
{
set
{
_nativeModuleCallExceptionHandler = value;
}
}
/// <summary>
/// Instantiates a new <see cref="ReactInstanceManager"/>.
/// </summary>
/// <returns>A React instance manager.</returns>
public ReactInstanceManager Build()
{
AssertNotNull(_initialLifecycleState, nameof(InitialLifecycleState));
if (!_useDeveloperSupport && _jsBundleFile == null)
{
throw new InvalidOperationException("JavaScript bundle file has to be provided when dev support is disabled.");
}
if (_jsBundleFile == null && _jsMainModuleName == null)
{
throw new InvalidOperationException("Either the main module name or the JavaScript bundle file must be provided.");
}
if (_uiImplementationProvider == null)
{
_uiImplementationProvider = new UIImplementationProvider();
}
if (_javaScriptExecutorFactory == null)
{
_javaScriptExecutorFactory = () => new ChakraJavaScriptExecutor();
}
return new ReactInstanceManager(
_jsBundleFile,
_jsMainModuleName,
_packages,
_useDeveloperSupport,
_initialLifecycleState.Value,
_uiImplementationProvider,
_javaScriptExecutorFactory,
_nativeModuleCallExceptionHandler);
}
private void AssertNotNull(object value, string name)
{
if (value == null)
throw new InvalidOperationException(Invariant($"'{name}' has not been set."));
}
}
class ReactInstanceDevCommandsHandler : IReactInstanceDevCommandsHandler
{
private readonly ReactInstanceManager _parent;
public ReactInstanceDevCommandsHandler(ReactInstanceManager parent)
{
_parent = parent;
}
public void OnBundleFileReloadRequest()
{
_parent.RecreateReactContextInBackground();
}
public void OnJavaScriptBundleLoadedFromServer()
{
_parent.OnJavaScriptBundleLoadedFromServer();
}
public void OnReloadWithJavaScriptDebugger(Func<IJavaScriptExecutor> javaScriptExecutorFactory)
{
_parent.OnReloadWithJavaScriptDebugger(javaScriptExecutorFactory);
}
public void ToggleElementInspector()
{
_parent.ToggleElementInspector();
}
}
}
}