In iOS UIWebView and WK WebView, JavaScript and OC interaction, Cookie management look at me enough (I)

Keywords: Javascript iOS github Fragment

Preface

In iOS development, it is used to display an html page, H5 page, and WebView is often used as a control. Speaking of WebView, how much do you know? Is it a simple presentation, or do you want to interact with OC to achieve more complex functions? This article will introduce you to WebView in iOS, and take you step by step to understand and master the use of WebView, JavaScript and Objective interaction, Cookie management, js debugging and so on.

The article is divided into the following parts because it involves more contents:

For some of the things mentioned in this article, here I have prepared a Demo Small partners in need can download.

Contents of this article

  • Preface
  • UIWebView
    • Basic Usage of UIWebView
    • Interaction between JavaScript and Objective in UIWebView
      • UIWebView OC calls JS
        1. stringByEvaluatingJavaScriptFromString:
        2. JavaScriptCore(iOS 7.0 +)
      • UIWebView JS Calls OC
        1. Custom URL Scheme (intercepting URL)
        2. JavaScriptCore(iOS 7.0 +)
    • Cookie Management of UIWebView
      • Cookie introduction
      • Cookie management
  • To be continued

UIWebView

Basic Usage of UIWebView

The first thing I want to introduce is our old friend UIWebView. It is believed that UIWebView, like UILabel, is the first control to be touched. In fact, the use of UIWebView is relatively simple (functionality basically meets the needs), simple creation, and invocation.

- (void)loadRequest:(NSURLRequest *)request;
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;

These methods can be loaded.
Of course, if you need to monitor the results of page loading or decide whether to allow opening a URL, you need to set the delegate of UIWebView. The proxy only needs to follow the <UIWebView Delegate> protocol and implement the following options in the proxy:

__TVOS_PROHIBITED @protocol UIWebViewDelegate <NSObject>

@optional
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webViewDidFinishLoad:(UIWebView *)webView;
- (void)webView:(UIWebView *)webView didFailLoadWithError:(nullable NSError *)error;

@end

Interaction between JavaScript and Objective in UIWebView

Some good third-party implementations are not discussed in detail here, such as WebViewJavascriptBridge After reading the following sections, I believe you will also implement a simple bridge.

UIWebView OC calls JS

1. stringByEvaluatingJavaScriptFromString:

The most commonly used method is to simply call -(nullable NSString*) stringByEvaluating JavaScriptFromString:(NSString*) script; for example:

    self.navigationItem.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];

Although it is more convenient, there are also disadvantages:

  1. This method cannot determine whether an error occurred after calling a js method. When an error occurs, the return value is nil, and when a method is called without the return value itself, the return value is nil, so it is impossible to determine whether the call is successful.
  2. The type of return value is nullable NSString*, which means that when the JS method invoked has a return value, it is returned as a string, which is not flexible enough. When the return value is an Array of js, it is more troublesome to parse the string.

For these shortcomings, you can solve them by using JavaScript Core (iOS 7.0+).

2. JavaScriptCore(iOS 7.0 +)

Surely you will not be unfamiliar with it. In the boiling JSPatch banned incident of the past few days, the core is it. Because JavaScript Core's JS-OC mapping can replace various JS methods to OC methods, its dynamic (with runtime insecurity) has become the main reason why JSPatch is banned by Apple. Here's how UIWebView implements OC - > JS through JavaScript Core.

In fact, WebKit has a built-in js environment. Usually after the page is loaded, we get the js context, and then get the return value through the evaluateScript: method of JSContext. Because this method obtains a JSValue object, it supports JavaScript data types such as Array, Number, String, object and so on.

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    //Update the title. This is the method described above.
    //self.navigationItem.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];

    //Get the javascript context for the UIWebView
    JSContext *jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    //This is also a way to get the title.
    JSValue *value = [self.jsContext evaluateScript:@"document.title"];
    //Update title
    self.navigationItem.title = value.toString;
}

This method solves the problem of stringByEvaluating JavaScriptFromString: the return value is only NSString.

So if I execute a non-existent method, such as

[self.jsContext evaluateScript:@"document.titlexxxx"];

Then there must be an error, which can be retrieved by setting the block @property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);.

//Set an exception callback before invoking
[self.jsContext setExceptionHandler:^(JSContext *context, JSValue *exception){
        NSLog(@"%@", exception);
}];
//Execution method
JSValue *value = [self.jsContext evaluateScript:@"document.titlexxxx"];

This method also solves the problem of stringByEvaluating JavaScriptFromString: after calling the js method, there are errors that can not be caught.

UIWebView JS Calls OC

1. Custom URL Scheme (intercepting URL)

For example, darkangel:///. In html or js, when clicking a button to trigger an event, the method jumps to a link composed of a custom URL Scheme, which is captured in Objective-C to parse the necessary parameters and realize an interaction between JS and OC. For example, there is a label a in the page, which links as follows:

<a href="darkangel://smsLogin?username=12323123&code=892845">Short Message Authentication Logon</a>

In Objective-C, as long as the UIWebViewDelegate protocol is followed, the method is triggered before each link is opened.

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

In this method, the link is captured and NO is returned to execute the corresponding OC method.

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    //Standard URL s include scheme, host, port, path, query, fragment, etc.
    NSURL *URL = request.URL;    
    if ([URL.scheme isEqualToString:@"darkangel"]) {
        if ([URL.host isEqualToString:@"smsLogin"]) {
            NSLog(@"Short Message Authentication Code Logon with parameters of %@", URL.query);
            return NO;
        }
    }
    return YES;
}

When the user clicks on the SMS to verify the login, the console will output the SMS authentication code to login with the parameter of username = 12323123 & code = 892845. Parameters can be a json format string with URLEncode, so that complex parameters can be passed (for example WebViewJavascriptBridge).

Advantages: versatility, can cooperate with h5 page dynamic. For example, an activity in the page links to the activity details page. When native has not been developed, the link can be an h5 link. When native has been developed, it can be jumped to the native page through this method to realize page dynamics. And this scheme is suitable for Android and iOS, and has strong generality.

Disadvantage: Can not directly get the return value of this interaction, more suitable for one-way reference, and do not care about callback scenarios, such as h5 page Jump to native page.

In fact, WebViewJavascriptBridge The solution is to intercept the URL. In order to solve the disadvantage of not getting the return value directly, it uses a function called callback as a parameter, passes it to OC (js - > oc, transfer parameters and callbackId) through some encapsulation, and then executes on the OC end, callback (oc - > js, transfer return value parameters) through block to achieve asynchronous acquisition of return value. For example, call on the JS side

//JS calls OC sharing method (of course OC registration in advance) share as method name, shareData as parameter, followed by callback function
WebViewJavascriptBridge.callHandler('share', shareData, function(response) {
   //OC shares the results of success or failure through block callbacks
   alert(response);   
});

See its source code in detail, it is worth learning.

2. JavaScriptCore(iOS 7.0 +)

In addition to intercepting URL s, you can also use the JavaScript Core mentioned above. It is very powerful. Where is it strong? Now let's explore.

Of course, you still need to get the js context when the page is loaded. Once we get it, we can do powerful method mapping.

For example, in js, I defined a method of sharing

function share(title, imgUrl, link) {
     //OC implementation is needed here
}

Implemented in OC as follows

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    //Method of mapping js function to OC
    [self convertJSFunctionsToOCMethods];
}

- (void)convertJSFunctionsToOCMethods
{
    //Get the javascript context of the UIWebview
    //self holds jsContext
    //@property (nonatomic, strong) JSContext *jsContext;
    self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    //js calls oc
    //share is the method name of js, assigned to a block with oc code inside.
    //This method will eventually print out all received parameters. The js parameters are not fixed.
    self.jsContext[@"share"] = ^() {
        NSArray *args = [JSContext currentArguments];//Get all the parameters in share
        //The elements in args are JSValue, objects that need to be converted to OC
        NSMutableArray *messages = [NSMutableArray array];
        for (JSValue *obj in args) {
            [messages addObject:[obj toObject]];
        }
        NSLog(@"Click to share js Returns parameters:\n%@", messages);
    };
}

Somewhere in html or js, click the a tag to invoke the share method and pass parameters, such as

<a href="javascript:void(0);" class="sharebtn" onclick="share('Share title', 'http://Cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg', location.href) "> Sharing activities, get 30 yuan red envelope </a>"

At this point, if the user clicks on the sharing activity and gets the 30 yuan red envelope label, all the parameters will be printed in the console.

The above code implements the OC method to replace the JS implementation. It is very flexible and relies mainly on these Api s.

@interface JSContext (SubscriptSupport)
/*!
@method
@abstract Get a particular property on the global object.
@result The JSValue for the global object's property.
*/
- (JSValue *)objectForKeyedSubscript:(id)key;
/*!
@method
@abstract Set a particular property on the global object.
*/
- (void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying> *)key;

self.jsContext[@"yourMethodName"] = your block; this writing not only replaces the JS method as OC implementation when yourMethodName method exists, but also adds methods when the method does not exist. In short, there are replacements and additions.

So if I want to write a js method with two parameters and one return value, how should oc be replaced?

js medium

//This method passes in two integers, sums them, and returns the result.
function testAddMethod(a, b) {
     //OC is required to implement a+b and return
      return a + b;
}
//js call
console.log(testAddMethod(1, 5));    //output  6

oc direct substitution method

self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b) {
      return a + b;
};

So when called in js

//js call
console.log(testAddMethod(1, 5));    //Output 6, method a + b

If oc replaces this method by multiplying two numbers

self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b) {
      return a * b;
};

Call js again

console.log(testAddMethod(1, 5));    //Output 5, this method becomes a * b.

To counter this, call the original implementation of the method and multiply the original result by 10.

//Call the original implementation of the method, multiply the original result by 10
JSValue *value = self.jsContext[@"testAddMethod"];
self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b) {
    JSValue *resultValue = [value callWithArguments:[JSContext currentArguments]];
    return resultValue.toInt32 * 10;
};

Call js again

console.log(testAddMethod(1, 5));    //Output 60, this method becomes (a + b) * 10

The above methods are all synchronous functions. If I want to implement the method that JS calls OC and asynchronously receives callbacks, what should I do? For example, there is a share button in h5. After the user clicks on it, he calls on the native share (Weichat share, Weibo share, etc.). When the native share succeeds or fails, he calls back the H5 page and tells it the share result. The H5 page refreshes the corresponding UI to show the success or failure of the share.

This problem requires a certain understanding of js. The following is the JS code.

//statement
function share(shareData) {
    var title = shareData.title;
    var imgUrl = shareData.imgUrl;
    var link = shareData.link;
    var result = shareData.result;
      //do something
    //Here we simulate asynchronous operations
    setTimeout(function(){
          //After 2s, call back true to share success
       result(true);
    }, 2000);
}

//This is what you need to write when calling
share({
      title: "title", 
     imgUrl: "http://img.dd.com/xxx.png", 
     link: location.href, 
     result: function(res) {    //Functions as parameters
         console.log(res ? "success" : "failure");
    }
});

From the encapsulation point of view, the parameter of the share method of js is an object, which contains several necessary fields and a callback function. This callback function is a bit like OC block. The caller passes a function into a function as a parameter. In appropriate time, the implementer of the method calls the function to achieve asynchronous callback to the caller. So what if OC implements share method at this time? In fact, it's probably like this:

//Call-backs
self.jsContext[@"share"] = ^(JSValue *shareData) {    //First of all, it's important to note that the parameters of the callback cannot be written directly to the NSDictionary type. Why?
    //Look carefully, the print is indeed an NSDictionary, but the result field corresponds not to a block but to an NSDictionary.  
      NSLog(@"%@", [shareData toObject]);     
    //Get the result attribute of the shareData object, which corresponds to a function of javascript.
    JSValue *resultFunction = [shareData valueForProperty:@"result"];
    //Callback block to convert js function to OC block
    void (^result)(BOOL) = ^(BOOL isSuccess) {
        [resultFunction callWithArguments:@[@(isSuccess)]];
    };
    //Analog asynchronous callback
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"Callback Sharing Success");
        result(YES);
    });
};

Some of these pits, which have been clearly written in the comments of the code, need to pay attention to the conversion of function of JavaScript and block of Objective-C.

From the above discussions and attempts, it is enough to prove the strength of JavaScript Core, which is no longer open here, and small partners can explore by themselves.

Cookie Management of UIWebView

Cookie introduction

Speaking of Cookie, maybe some of the small partners will be relatively unfamiliar, some of the small partners will be more familiar. If all the pages in the project are implemented purely native, we may never touch the Cookie in general. But here's Cookie, because it's really important, and there are a lot of pits created by it.

Cookie is the most widely used place on the Web to record various states. For example, if you open Baidu in Safari, then log in to your account, and then open all Baidu-related pages, it will be login status, and when you turn off the computer, the next time you turn on Safari to open Baidu, you will find that it is still login status, in fact, this uses Cookie. Cookie records some information about your Baidu account, validity period, etc. It also maintains the statistics of login status when cross-domain requests are made.


You can see that Cookies have different domains and different validity periods. Generally, Cookies in domains like. baidu.com are designed to maintain some state across domains.

So in App, the most common use of Cookie s is to maintain login status. Generally, the Native side has its own complete login and registration logic, and most pages are implemented natively. Of course, there are also some pages that are implemented by h5. Although the loading of H5 pages through WebView in App is more or less a bit of a performance problem, it does not feel smooth or experience well, but its flexibility is unmatched by Native App. Thus, there is a need to maintain the user's login status when the Native user is in the login state and opens an H5 page.

This requirement seems simple, how to achieve it? The general solution is that Native saves the login status Cookie and adds it to the h5 page to maintain the login status. In fact, there are many pits, such as user login or exit, h5 page login status has changed, need to refresh, when to refresh? Question of Cookie Loss in WKWebView? Here's a brief introduction to the Cookie management of UIWebView, followed by a section on WKWebView.

Cookie management

UIWebView's Cookie management is very simple, generally we do not need to operate Cookie manually, because all Cookies will be managed by the singleton [NSHTTP Cookie Storage Shared HTTP Cookie Storage], and UIWebView will automatically synchronize Cookie in Cookie Storage, so as long as we login and exit normally on the Native side, h5 refreshes at the appropriate time, we can maintain the login status correctly. No extra operation is required.

There may be situations where we need to add a fixed Cookie to make a distinction when accessing a link, and then we can do it through a header.

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
[request addValue:@"customCookieName=1314521;" forHTTPHeaderField:@"Set-Cookie"];
[self.webView loadRequest:request];

You can also actively manipulate NSHTTP Cookie Storage to add a custom Cookie

NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{
    NSHTTPCookieName: @"customCookieName", 
    NSHTTPCookieValue: @"1314521", 
    NSHTTPCookieDomain: @".baidu.com",
    NSHTTPCookiePath: @"/"
}];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];    //Cookie exists and overrides, but no additions exist.

There are also some common methods, such as reading all cookies

NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;

Cookie is converted to HTTP Header Fields and added to the request header

//Converting Cookies arrays to requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//Setting the request header
request.allHTTPHeaderFields = requestHeaderFields;

Overall, UIWebView's Cookie management is relatively simple, small partners can write their own demo test, play your imagination.

To be continued

The introduction of UIWebView, the interaction between JS and OC using UIWebView, and the management of Cookie are briefly introduced here. If there is a small partner for WebViewJavascriptBridge More interested, you can leave a message, according to the message I think about writing an article, analysis of its detailed implementation.

In addition, we will introduce the use of WKWebView, some OC and JS interaction, Cookie management, how to debug in Safari and some unknown pits, etc.

Reprinted: http://www.jianshu.com/p/ac45d99cf912

Posted by Devil_Banner on Sat, 15 Dec 2018 20:21:04 -0800