整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          WKWebView適配(實戰篇)

          、Cookie適配

          1.現狀

          WKWebView適配中最麻煩的就是cookie同步問題

          WKWebView采用了獨立存儲控件,因此和以往的UIWebView并不互通

          雖然iOS11以后,iOS開放了WKHTTPCookieStore讓開發者去同步,但是還是需要考慮低版本的 同步問題,本章節從各個角度切入考慮cookie同步問題

          2.同步cookie(NSHTTPCookieStorage->WKHTTPCookieStore)

          iOS11+

          可以直接使用WKHTTPCookieStore遍歷方式設值,可以在創建wkwebview時候就同步也可以是請求時候

          // iOS11同步 HTTPCookieStorag到WKHTTPCookieStore
          WKHTTPCookieStore *cookieStore = self.wkWebView.configuration.websiteDataStore.httpCookieStore;
          
          - (void)syncCookiesToWKCookieStore:(WKHTTPCookieStore *)cookieStore  API_AVAILABLE(ios(11.0)){
              NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
              if (cookies.count == 0) return;
              for (NSHTTPCookie *cookie in cookies) {
                  [cookieStore setCookie:cookie completionHandler:^{
                      if ([cookies.lastObject isEqual:cookie]) {
                          [self wkwebviewSetCookieSuccess];
                      }
                  }];
              }
          }

          同步cookie可以在初始化wkwebview的時候,也可以在請求的時候。初始化時候同步可以確保發起html頁面請求的時候帶上cookie

          例如:請求在線頁面時候要通過cookie來認證身份,如果不是初始化時同步,可能請求頁面時就是401了

          iOS11-

          通過前端執行js注入cookie,在請求時候執行

          //wkwebview執行JS
          - (void)injectCookiesLT11 {
              WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
              [self.wkWebView.configuration.userContentController addUserScript:cookieScript];
          }
          //遍歷NSHTTPCookieStorage,拼裝JS并執行
          - (NSString *)cookieString {
              NSMutableString *script = [NSMutableString string];
              [script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"];
              for (NSHTTPCookie *cookie in NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies) {
                  // Skip cookies that will break our script
                  if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
                      continue;
                  }
                  [script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n", cookie.name, [self formatCookie:cookie]];
              }
              return script;
          }
          //Format cookie的js方法
          - (NSString *)formatCookie:(NSHTTPCookie *)cookie {
              NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@",
                                  cookie.name,
                                  cookie.value,
                                  cookie.domain,
                                  cookie.path ?: @"/"];
              if (cookie.secure) {
                  string = [string stringByAppendingString:@";secure=true"];
              }
              return string;
          }

          但是上面方法執行js,也無法保證第一個頁面請求帶有cookie

          所以請求時候創建request需要設置cookie,并且loadRequest

          -(void)injectRequestCookieLT11:(NSMutableURLRequest*)mutableRequest {
              // iOS11以下,手動同步所有cookie
              NSArray *cookies = NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies;
              NSMutableArray *mutableCookies = @[].mutableCopy;
              for (NSHTTPCookie *cookie in cookies) {
                  [mutableCookies addObject:cookie];
              }
              // Cookies數組轉換為requestHeaderFields
              NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:(NSArray *)mutableCookies];
              // 設置請求頭
              mutableRequest.allHTTPHeaderFields = requestHeaderFields;
          }

          3.反向同步cookie(WKHTTPCookieStore->NSHTTPCookieStorage)

          wkwebview產生的cookie也可能在某些場景需要同步給NSHTTPCookieStorage

          iOS11+可以直接用WKHTTPCookieStore去同步,

          iOS11-可以采用js端獲取,觸發bridge同步給NSHTTPCookieStorage

          但是js同步方式無法同步httpOnly,所以真的遇到了,還是要結合服務器等方式去做這個同步。

          二、JS和Native通信

          1.Native調用JS

          將代碼準備完畢后調用API即可,回調函數可以接收js執行結果或者錯誤信息,So Easy。

          [self.wkWebView evaluateJavaScript:jsCode completionHandler:^(id object, NSError *error){}];

          2.注入JS

          其實就是提前注入一些JS方法,可以提供給JS端調用。

          比如有的框架會將bridge直接通過這種方式注入到WK的執行環境中,而不是從前端引入JS,這種好處就是假設前端的JS是在線加載,JS服務器掛了或者網絡問題,這樣前端頁面就失去了Naitve的Bridge通信能力了。

          -(instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly;
          
          //WKUserScriptInjectionTime說明
          typedef NS_ENUM(NSInteger, WKUserScriptInjectionTime) {
              WKUserScriptInjectionTimeAtDocumentStart, /**文檔開始時候就注入**/
              WKUserScriptInjectionTimeAtDocumentEnd /**文檔加載完成時注入**/
          } API_AVAILABLE(macos(10.10), ios(8.0));

          3.JS調用Native

          3-1.準備代理類

          代理類要實現WKScriptMessageHandler

          @interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler>
            @property (nonatomic, weak) id<WKScriptMessageHandler> scriptDelegate;
            - (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate;
          @end

          WKScriptMessageHandler就一個方法

          @implementation WeakScriptMessageDelegate
          - (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate {
              self = [super init];
              if (self) {
                  _scriptDelegate = scriptDelegate;
              }
              return self;
          }
          
          - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
              [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
          }

          3-2.設置代理類

          合適時機(一般初始化)設置代理類,并且指定name

          NSString* MessageHandlerName = @"bridge";
          [config.userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:MessageHandlerName];

          3-3.bridge的使用(JS端)

          執行完上面語句后就會在JS端注入了一個對象"window.webkit.messageHandlers.bridge"

          //JS端發送消息,參數最好選用String,比較通用
          window.webkit.messageHandlers.bridge.postMessage("type");

          3-4.Native端消息的接收

          然后native端可以通過WKScriptMessage的body屬性中獲得傳入的值

          - (void)userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
              if ([message.name isEqualToString:HistoryBridageName]) {
                
              } else if ([message.name isEqualToString:MessageHandlerName]) {
                  [self jsToNativeImpl:message.body];
              }
          }

          3-5.思考題

          這里我們為什么要使用WeakScriptMessageDelegate,并且再設置個delegate指向self(controller),為什么不直接指向?

          提示:可以參考NSTimer的循環引用問題

          3-6.完整的示例

          -(void)_defaultConfig{
             WKWebViewConfiguration* config = [WKWebViewConfiguration new];
             …… ……
             …… ……
             WKUserContentController* userController = [[WKUserContentController alloc] init];
             config.userContentController = userController;
             [self injectHistoryBridge:config];
             …… ……
             …… ……     
          }
          
          -(void)injectHistoryBridge:(WKWebViewConfiguration*)config{
              [config.userContentController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:HistoryBridageName];
              NSString *_jsSource = [NSString stringWithFormat:
                                     @"(function(history) {\n"
                                     "  function notify(type) {\n"
                                     "    setTimeout(function() {\n"
                                     "      window.webkit.messageHandlers.%@.postMessage(type)\n"
                                     "    }, 0)\n"
                                     "  }\n"
                                     "  function shim(f) {\n"
                                     "    return function pushState() {\n"
                                     "      notify('other')\n"
                                     "      return f.apply(history, arguments)\n"
                                     "    }\n"
                                     "  }\n"
                                     "  history.pushState = shim(history.pushState)\n"
                                     "  history.replaceState = shim(history.replaceState)\n"
                                     "  window.addEventListener('popstate', function() {\n"
                                     "    notify('backforward')\n"
                                     "  })\n"
                                     "})(window.history)\n", HistoryBridageName
                                     ];
              WKUserScript *script = [[WKUserScript alloc] initWithSource:_jsSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
              [config.userContentController addUserScript:script];
          }

          3-7.其它問題

          在iOS8 beta5前,JS和Native這樣通信設置是不行的,所以可以采用生命周期中做URL的攔截去解析數據來達到效果,這里不做贅述,可以自行參考網上類似UIWebview的橋接原理文章

          三、實戰技巧

          1.UserAgent的設置

          添加UA

          實際過程中最好只是原有UA上做添加操作,全部替換可能導致服務器的拒絕(安全策略)

          日志中紅線部分是整個模擬器的UA,綠色部門是UA中的ApplicationName部分

          iOS9上,WKWebview提供了API可以設置ua中的ApplicationName

          config.applicationNameForUserAgent = [NSString stringWithFormat:@"%@ %@", config.applicationNameForUserAgent, @"arleneConfig"];

          全部替換UA

          iOS9以上直接可以指定wkwebview的customUserAgent,iOS9以下的話,設置NSUserDefaults

          if (@available(iOS 9.0, *)) {
             self.wkWebView.customUserAgent = @"Hello My UserAgent";
          }else{
             [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":@"Hello My UserAgent"}];
             [[NSUserDefaults standardUserDefaults] synchronize];
          }

          2.監聽進度和頁面的title變化

          wkwebview可以監控頁面加載進度,類似瀏覽器中打開頁面中的進度條的顯示

          頁面切換的時候也會自動更新頁面中設置的title,可以在實際項目中動態切換容器的title,比如根據切換的title設置navigationItem.title

          原理直接通過KVO方式監聽值的變化,然后在回調中處理相關邏輯

          //kvo 加載進度
          [self.webView addObserver:self
                        forKeyPath:@"estimatedProgress"
                        options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                        context:nil];
          //kvo title
          [self.webView addObserver:self
                        forKeyPath:@"title"
                        options:NSKeyValueObservingOptionNew
                        context:nil];
          
          /** KVO 監聽具體回調**/
          - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
              if ([keyPath isEqual:@"estimatedProgress"] && object == self.webView) {
                  ALLOGF(@"Progress--->%@",[NSNumber numberWithDouble:self.webView.estimatedProgress]);
              }else if([keyPath isEqualToString:@"title"]
                       && object == self.webview){
                  self.navigationItem.title = self.webView.title;
              }else{
                  [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
              }
          }
          
          /**銷毀時候記得移除**/
          [self.webView removeObserver:self
                     forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
          [self.webView removeObserver:self
           					forKeyPath:NSStringFromSelector(@selector(title))];
          
          

          3.Bridge通信實戰

          下面介紹自己實現的bridge通信框架,前端無需關心所在容器,框架層做適配。

          import {WebBridge} from 'XXX'
          /**
          * 方法: WebBridge.call(taskName,options,callback)
          * 參數說明: 
          *	taskName String task的名字,用于Native處理分發任務的標識
          * options  Object 傳遞的其它參數
          * callback function 回調函數
          *.         回調參數
          *					 json object native返回的內容
          **/
          WebBridge.call("Alert",{"content":"彈框內容","btn":"btn內容"},function(json){
          	 console.log("call back is here",JSON.stringify(json));
          });

          上面調用了Native的Alert控件,然后返回調用結果。

          調用到的Native代碼如下:

          //AlertTask.m
          #import "AlertTask.h"
          #import <lib-base/ALBaseConstants.h>
          @interface AlertTask (){}
          @property (nonatomic,weak) ArleneWebViewController* mCtrl;
          @end
          
          @implementation AlertTask
          -(instancetype)initWithContext:(ArleneWebViewController*)controller{
              self = [super init];
              self.mCtrl = controller;
              return self;
          }
          -(NSString*)taskName{
              return @"Alert";
          }
          -(void)doTask:(NSDictionary*)params{
              ALShowAlert(@"Title",@"message");//彈出Alert
              NSMutableDictionary* callback = [ArleneTaskUtils basicCallback:params];//獲取callback
              [callback addEntriesFromDictionary:params];
              [self.mCtrl callJS:callback];//執行回調
          }
          @end
          
          

          關于我

          期待與要求上進的您進一步溝通

          微信號:maako127

          可以加入我的公眾號(二碼前端說),定期更新前端相關技術干貨

          年做了大量的 HTML5 項目,遇到了很多坑。在這個過程中學到了一些之前不具備的知識,所以這篇文章就簡單分享一下這方面的話題。

          傳統的MPA

          首先,說一個比較古老的東西,叫做 MPA。

          MPA 的全稱是 Multi-page Application,意思是整個應用(站點)由多個完整的 html 構成。用戶在頁面 1 點擊跳轉,需要向服務端請求頁面 2,請求成功后渲染。而用戶返回時,相當于是點擊了瀏覽器的返回,頁面退回到之前的歷史記錄,并重新加載出來。

          在這樣的模式下,頁面間切換慢、不流暢的問題比較突出,尤其是在移動端。

          同時,它還產生了幾個小問題:

          • 跳轉動畫:頁面間的跳轉無法實現轉場動畫效果。
          • 如果前一個頁比較長,用戶滑動到頁面比較靠下方的位置后點擊,返回時,頁面無法默認停留在原位置。
          • iOS 右滑返回產生問題,從頁面 1 跳轉到頁面 2,再從頁面 2 跳轉到頁面 3,右滑返回,會直接回到頁面 1 前的頁。

          SPA

          隨著對移動端體驗需求的提高以及技術的進步,另一種模式 SPA(Single-page Application)逐漸成為主流。

          SPA 簡單來說,就是原來在 MPA 中的多個 html,現在被放在了一個 html 中,并被分成若干個片段。跳轉、返回的本質變成了分段的「隱藏」與「顯示」。跳轉不需要反復對服務端進行請求,從而使得頁面與頁面之間切換更加快速流暢。

          在這樣的機制下,跳轉與返回完全由代碼控制,所以可以通過代碼定義頁面轉場的效果、返回。

          在設計轉場動畫時,我們需要留意的是導航欄是 Native 的還是 HTML5 的。如果導航欄是 Native 的,那 HTML5 頁面不包括導航欄,它相當于是網頁外的元素,不在轉場效果的設計范圍內。

          WebView

          說 HTML5 的跳轉,就不得不說 WebView。簡單來說,WebView 是在 App 中用于顯示 web 內容的容器。上文提到的 MPA 和 SPA,都裝在了這個叫做 WebView 的容器中。

          用戶點擊頁面中的元素進行跳轉,除了前面的兩種方式外,還有第三種:新打開 WebView 的方式。在這樣的方式下,跳轉的本質是 HTML5「告訴」Native,由 Native 執行打開新 WebView,并在新 WebView 中加載頁面。

          因為 Native 的機制,打開新 WebView 的同時,之前的 WebView 會被自然、完整地保留。所以這時,之前的幾個問題就變為:

          • 跳轉動畫:頁面間的跳轉動畫由 WebView 之間的跳轉動畫來決定。
          • 返回后頁面停留在原位置:完美支持。
          • iOS 右滑返回:完美支持。

          不過需要注意的地方是,打開新 WebView 是一個資源消耗比較大的操作。如果我們在設計一個流程時,需要比較多的連續使用這種方式,需要和研發同學進行充分的溝通。

          比較特殊的Replace

          前述的三種跳轉,都會產生歷史記錄。MPA、SPA 的歷史記錄是在 HTML5 中產生,新開 WebView 中的記錄是在 Native 中產生。

          在 MPA 或 SPA 中,如果跳轉時使用 Replace 方法,它會用新頁面替換之前的頁面,歷史記錄中沒有之前頁面的記錄。

          這是一種特殊的跳轉方式,在設計一些不可逆的流程時可考慮使用。

          多頁面回退

          了解了上述的幾種機制后,我們來看一個小的應用場景──多頁面回退。

          我們在實際業務中,經常會有這樣的需求。假設我們有 1、2、3 三個頁組成的一個流程,在頁面 3 上有個「完成」按鈕點擊回到頁面 1。在不同的交互模式下,實現這樣的跳轉有著不同的機制。

          1. SPA模式下的正常跳轉

          這種模式是 3 個頁面都在一個 WebView 中。點擊頁面 3 中的「完成」按鈕,回退 -2 ,即回退 2 步歷史記錄,到頁面 1。

          2. 新打開WebView

          打開新 WebView 又分三種方式。

          如果我們把 3 個頁面,拆分到 2 個 WebView 中,如下圖,點擊完成按鈕,即關閉自身所在的 WebView。

          同樣是打開新的 WebView,如果我們按如下圖的方法拆分會稍微復雜。這時點擊完成按鈕,首先關閉自身所在的 WebView,當頁面 2「意識」到自己重新被展現時,自動退回 1 步到頁面 1。

          每次打開新的 WebView,這時點擊完成,回退的本質是 HTML5「告訴」Native 關閉多個 WebView。需要特別注意的是,HTML5 中實現這種方式不是天然具備的,它需要 Native 具有一次關閉多個 WebView 的能力。所以我們在設計方案時,需要了解清楚自家的 Native 是否有這樣的能力。

          總結

          以上,簡單說了幾種 HTML5 的跳轉方式。這些跳轉方式,沒有絕對的對與錯,我們在設計方案時,需要根據實際的業務需求與技術的限制,來整體考慮解決方案。

          根據個人經驗,也有幾點小帖士分享給大家:

          • 前后邏輯交織不復雜的單個頁面,可以考慮使用新 WebView 打開跳轉。
          • 如果是一個任務型的流程,可以考慮將一個任務流包在一個 WebView 中,在任務內使用 SPA 跳轉。不同的任務使用不同的 WebView。保持任務之間的關系清晰明了。
          • 設計上需要著重表現頁面間轉場動畫的效果,優先考慮使用 SPA 跳轉。
          • 為防止流程過于復雜,盡量不要自定義關閉、返回的行為。保持關閉為默認的關閉行為,保持返回為默認的返回行為。

          N 在 0.37 版本中加入了WebView功能,所以想要在使用WebView,版本必須>=0.37,發送的 message 只能是字符串,所以需要將其他格式的數據轉換成字符串,在接收到后再轉換回去,其實直接用JSON.stringify和JSON.parse就可以加載html source屬性用于指定加載的 html,可以加載在線的頁面,也可以加載本地的頁面,代碼如下:

          注意 ??在 RN 中可以加載 WebView,但是無法調試,也不能使用 alert 來驗證代碼 js 代碼是否運行成功,只能通過往 html 寫入東西(innerHTML)來驗證 js 是否運行成功。

          WebView 與 html 的通信

          webview 發送信息到 html。WebView 給 html 發送信息需要使用postMessage,而 html 接收 RN 發過來的信息需要監聽message事件,代碼如下:

          這里需要注意一點,postMessage需要在 webview 加載完成之后再去 post,如果放在commponentWillMount里由于頁面沒有加載完成就 post 信息,會導致 html 端無法監聽到 message 事件。html 發送信息到 webview。

          RN 中 debug webview 和安卓開發中看起來是差不多的,連接好設備后,在 chrome 中輸入chrome://inspect。就可以看到安卓設備上正在運行的 webview 了,點擊inspect就會開啟一個調試頁面,就可以進行 debug 了,RN 似乎默認開啟了 debug 調試,直接就可以看到 webview 中輸出的信息。


          主站蜘蛛池模板: 97av麻豆蜜桃一区二区| 国产视频一区二区| 中文字幕不卡一区| 日韩成人无码一区二区三区| 中文字幕日本精品一区二区三区| 国产福利电影一区二区三区久久老子无码午夜伦不 | 激情久久av一区av二区av三区| 精品视频一区二区三区免费| 日本美女一区二区三区| 亚洲性无码一区二区三区| 无码人妻AⅤ一区二区三区| 一区二区三区在线播放视频| 国产情侣一区二区| 偷拍激情视频一区二区三区| 麻豆精品久久久一区二区| AV怡红院一区二区三区| 高清无码一区二区在线观看吞精| 亚洲av无码片vr一区二区三区 | 亚洲av日韩综合一区二区三区| 国产成人精品一区二区秒拍| 国产精品被窝福利一区 | 中字幕一区二区三区乱码| 色偷偷久久一区二区三区| 国偷自产av一区二区三区| 成人区人妻精品一区二区不卡网站| 在线不卡一区二区三区日韩| 久久中文字幕无码一区二区| 日韩中文字幕精品免费一区| 色老头在线一区二区三区| 久久高清一区二区三区| 亚洲av无码一区二区三区乱子伦| 国产av熟女一区二区三区| 中文字幕不卡一区| 久久99国产精品一区二区| 中文字幕一精品亚洲无线一区| 成人无码一区二区三区| 国精品无码一区二区三区在线| 日本高清成本人视频一区| 亚洲综合av永久无码精品一区二区| 国产在线视频一区| 久久国产精品一区免费下载|