react-native/React/Executors/RCTWebViewExecutor.m

233 lines
7.1 KiB
Objective-C

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTDefines.h"
#if RCT_DEV // Debug executors are only supported in dev mode
#import "RCTWebViewExecutor.h"
#import <objc/runtime.h>
#import "RCTLog.h"
#import "RCTUtils.h"
static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...)
{
va_list args;
va_start(args, fmt);
NSString *description = [[NSString alloc] initWithFormat:fmt arguments:args];
RCTLogError(@"%@", description);
NSError *error = [NSError errorWithDomain:NSStringFromClass([RCTWebViewExecutor class])
code:3
userInfo:@{NSLocalizedDescriptionKey:description}];
callback(nil, error);
va_end(args);
}
@interface RCTWebViewExecutor () <UIWebViewDelegate>
@end
@implementation RCTWebViewExecutor
{
UIWebView *_webView;
NSMutableDictionary *_objectsToInject;
NSRegularExpression *_commentsRegex;
NSRegularExpression *_scriptTagsRegex;
}
RCT_EXPORT_MODULE()
@synthesize valid = _valid;
- (instancetype)initWithWebView:(UIWebView *)webView
{
if ((self = [super init])) {
_webView = webView;
}
return self;
}
- (id)init
{
return [self initWithWebView:nil];
}
- (void)setUp
{
if (!_webView) {
_webView = [[UIWebView alloc] init];
}
_objectsToInject = [[NSMutableDictionary alloc] init];
_commentsRegex = [NSRegularExpression regularExpressionWithPattern:@"(^ *?\\/\\/.*?$|\\/\\*\\*[\\s\\S]*?\\*\\/)" options:NSRegularExpressionAnchorsMatchLines error:NULL],
_scriptTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\/?script[^>]*?)>" options:0 error:NULL],
_webView.delegate = self;
}
- (void)invalidate
{
_valid = NO;
_webView.delegate = nil;
_webView = nil;
}
- (UIWebView *)invalidateAndReclaimWebView
{
UIWebView *webView = _webView;
[self invalidate];
return webView;
}
- (void)executeJSCall:(NSString *)name
method:(NSString *)method
arguments:(NSArray *)arguments
context:(NSNumber *)executorID
callback:(RCTJavaScriptCallback)onComplete
{
RCTAssert(onComplete != nil, @"");
[self executeBlockOnJavaScriptQueue:^{
if (!self.isValid || ![RCTGetExecutorID(self) isEqualToNumber:executorID]) {
return;
}
NSError *error;
NSString *argsString = RCTJSONStringify(arguments, &error);
if (!argsString) {
RCTReportError(onComplete, @"Cannot convert argument to string: %@", error);
return;
}
NSString *execString = [NSString stringWithFormat:@"JSON.stringify(require('%@').%@.apply(null, %@));", name, method, argsString];
NSString *ret = [_webView stringByEvaluatingJavaScriptFromString:execString];
if (ret.length == 0) {
RCTReportError(onComplete, @"Empty return string: JavaScript error running script: %@", execString);
return;
}
id objcValue = RCTJSONParse(ret, &error);
if (!objcValue) {
RCTReportError(onComplete, @"Cannot parse json response: %@", error);
return;
}
onComplete(objcValue, nil);
}];
}
/**
* We cannot use the standard eval JS method. Source will not show up in the
* debugger. So we have to use this (essentially) async API - and register
* ourselves as the webview delegate to be notified when load is complete.
*/
- (void)executeApplicationScript:(NSString *)script
sourceURL:(NSURL *)url
onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
if (![NSThread isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self executeApplicationScript:script sourceURL:url onComplete:onComplete];
});
return;
}
RCTAssert(onComplete != nil, @"");
__weak RCTWebViewExecutor *weakSelf = self;
_onApplicationScriptLoaded = ^(NSError *error){
RCTWebViewExecutor *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
strongSelf->_valid = error == nil;
onComplete(error);
};
if (_objectsToInject.count > 0) {
NSMutableString *scriptWithInjections = [[NSMutableString alloc] initWithString:@"/* BEGIN NATIVELY INJECTED OBJECTS */\n"];
[_objectsToInject enumerateKeysAndObjectsUsingBlock:^(NSString *objectName, NSString *blockScript, BOOL *stop) {
[scriptWithInjections appendString:objectName];
[scriptWithInjections appendString:@" = ("];
[scriptWithInjections appendString:blockScript];
[scriptWithInjections appendString:@");\n"];
}];
[_objectsToInject removeAllObjects];
[scriptWithInjections appendString:@"/* END NATIVELY INJECTED OBJECTS */\n"];
[scriptWithInjections appendString:script];
script = scriptWithInjections;
}
script = [_commentsRegex stringByReplacingMatchesInString:script
options:0
range:NSMakeRange(0, script.length)
withTemplate:@""];
script = [_scriptTagsRegex stringByReplacingMatchesInString:script
options:0
range:NSMakeRange(0, script.length)
withTemplate:@"\\\\<$1\\\\>"];
NSString *runScript =
[NSString
stringWithFormat:@"<html><head></head><body><script type='text/javascript'>%@</script></body></html>",
script
];
[_webView loadHTMLString:runScript baseURL:url];
}
/**
* In order to avoid `UIWebView` thread locks, all JS executions should be
* performed outside of the event loop that notifies the `UIWebViewDelegate`
* that the page has loaded. This is only an issue with the remote debug mode of
* `UIWebView`. For a production `UIWebView` deployment, this delay is
* unnecessary and possibly harmful (or helpful?)
*
* The delay might not be needed as soon as the following change lands into
* iOS7. (Review the patch linked here and search for "crash"
* https://bugs.webkit.org/show_bug.cgi?id=125746).
*/
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC);
dispatch_after(when, dispatch_get_main_queue(), ^{
RCTAssertMainThread();
block();
});
}
/**
* `UIWebViewDelegate` methods. Handle application script load.
*/
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
RCTAssertMainThread();
if (_onApplicationScriptLoaded) {
_onApplicationScriptLoaded(nil); // TODO(frantic): how to fetch error from UIWebView?
}
_onApplicationScriptLoaded = nil;
}
- (void)injectJSONText:(NSString *)script
asGlobalObjectNamed:(NSString *)objectName
callback:(RCTJavaScriptCompleteBlock)onComplete
{
if (RCT_DEBUG) {
RCTAssert(!_objectsToInject[objectName],
@"already injected object named %@", _objectsToInject[objectName]);
}
_objectsToInject[objectName] = script;
onComplete(nil);
}
@end
#endif