2018 年距離第一代 iOS 系統釋出(2007 年)已經過去 11 年,這 11 年中移動端日益成熟,Web 端的時代逐步轉移到了移動端,自然而然 Web 端的開發技術棧開始逐步移動到移動端。這就引發一個尷尬的局面,Web 端的同學不瞭解移動端的開發知識,移動端不瞭解 Web 端的開發知識。為了解決這個問題,知識小集打算從基礎出發,介紹 JavaScript 與 iOS 互動時用到的技術點,比如 JavaScriptCore、JavaScript 基礎、JavaScriptCore 的實際使用場景(深度剖析 JSPatch 的實現)等。而今天這篇就是其中的一篇,主要介紹一個 Hybrid WebView 的實現。接下來我們會把文章逐步發出來供我們的讀者朋友參考,我們初步定的目錄如下(如果你不想錯過我們這個專題,關注我們的公眾號【知識小集】吧,關於這個專題有什麼建議都可以通過公眾號告訴我們):
- 前言
- JavaScript 基礎知識
- JavaScript 進階
- JavaScript-native 除錯
- 開啟本地 Webserver
- WKWebView 概述
- JavaScriptCore 總覽
- JavaScript 與 ObjectiveC 間的型別轉換
- JavaScript 與 ObjectiveC 通訊
- ObjectiveC 與 JavaScript 通訊
- 自己動手實現一個 Hybrid WebView
- JSPatch 中的 JavaScriptCore
- JSPatch 中的 Runtime
- JSPatch 原理深度剖析
- JSPatch 雜談
- 讀 Aspects 理解 runtime
自己動手實現一個 Hybrid WebView
如今,端與 Web 頁的互動越來越頻繁,很多頁面都交給 Web 頁面來實現,而有些情況下 Web 需要與端進行互動。面對這種需求,各種第三方庫源源不斷出現,而 WebViewJavascriptBridge 無疑是 star 最多的一個。其實目前在 iOS 開發當中,大多數都切換到了 WKWebView,且對 Web 的互動越來越重,所以不妨自己實現一個 Hybrid WebView 來滿足自己的業務需求。一個 Hybrid WebView 最基本的應該滿足雙方可以自由通訊。
- WebView 上的事件可以傳遞到端上;
- WebView 可以從端上獲取資料;
- 端可以監聽到 WebView 上發生的事件。
本文旨在說明一個 Hybrid WebView 需要的技術手段,所以打算從一個具體的需求出發,一步一步搭建一個 Hybrid WebView。大多數的文章只會講解端上如何實現,而本文會結合前端一塊講講兩端是如何實現的。
需求說明
Web 頁面上有一張圖和一個儲存按鈕,當點選儲存按鈕時會提示使用者是否需要儲存圖片到相簿。如果儲存成功,按鈕的標題將變為已儲存,否則標題為儲存到相簿。如果已儲存,下次進入 Web 頁時顯示已儲存。
分析上面的需求,可以拆分為:
- 頁面載入後,需要獲取圖片是否已經儲存過,如果已儲存,按鈕的標題為“已儲存”,否則為“儲存到相簿”;
- 點選按鈕需要提示使用者“是否需要儲存圖片到相簿”,點選“儲存”執行儲存操作。點選取消將什麼也不做;
- 儲存成功,按鈕上的標題需要變為“已儲存”。
分析完上面具體需求後,轉換為技術需要考慮的問題:
- 頁面載入後,Web 頁可以從端上獲取到圖片是否已經儲存的狀態;
- 點選儲存按鈕,需要在端上提示使用者,使用者點選儲存需要把圖片儲存到相簿,這時需要獲取到當前顯示的圖片,也就是說需要把 Web 頁面中的資料傳遞到端;
- 儲存成功後需要修改 Web 頁面按鈕的標題。
先做一個 Web 頁面
整體頁面是如上圖所示。我們逐步剖析是如何實現的。
在前面的章節中(這些章節後續會發出來),已經介紹了在 Web 頁面中執行 JavaScript 。可以把一段 JavaScript 程式碼嵌入到 HTML 中,這時在 HTML 中可以直接呼叫 JavaScript 程式碼,而 JavaScript 可以通過 DOM 動態來操作 HTML 中的標籤,這樣既可以達到動態修改 Web 頁。
Web與端通訊的JS程式碼,這段程式碼是嵌入在 HTML 中的。
<script>
// 標記儲存的狀態
var saved = false;
// 儲存事件
function saveaction(){
if (saved) {
return;
}
alert("確定要儲存該圖片嗎?");
// 傳送訊息給客戶端 JS 中傳送訊息給 OC
var param = {url : "https://raw.githubusercontent.com/iOS-Tips/iOS-tech-set/master/images/qrcode.jpg"};
window.webkit.messageHandlers.JSBridge.postMessage(JSON.stringify(param));
};
// 儲存成功後端會呼叫這個方法通知Web頁儲存成功
function save_success(){
change_state(true);
};
// 修改是否已儲存的狀態,修改按鈕標題
function change_state(issaved){
saved = issaved;
var button = document.getElementById('saveid');
if (issaved){
// 如果已經儲存,修改按鈕的標題為已儲存,否則顯示 儲存到相簿
button.innerText = "已儲存";
} else {
button.innerText = "儲存到相簿";
}
}
</script>
複製程式碼
儲存到相簿 按鈕,監聽點選事件,當點選按鈕後會呼叫 saveaction
函式。
<div id="saveid" class="save_button" onclick="saveaction()">儲存到相簿</div>
複製程式碼
而 saveaction
函式首先會發一個 alert("確定要儲存該圖片嗎?")
到端,端會執行 WKUIDelegate
代理方法,我們在這個方法需要彈窗端內的提示框:
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"溫馨提示" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"儲存" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
self.isOKAction = YES;
completionHandler();
}]];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
self.isOKAction = NO;
completionHandler();
}]];
[self presentViewController:alert animated:YES completion:nil];
}
複製程式碼
當使用者點選儲存按鈕後,會儲存圖片到相簿。所以客戶端需要拿到圖片的地址,這是需要給端傳送圖片的地址。如果想給端傳送一條訊息,直接在 Web 頁通過 JavaScript 執行,其中 xxxx 是端與Web之間約定的名字。
window.webkit.messageHandlers.xxxx.postMessage(JSON.stringify(param))
複製程式碼
而我們此時定義的名字是 JSBridge
,當使用者點選儲存後,需要根據Web傳遞過來的 URL 儲存圖片。
var param = {url : "https://raw.githubusercontent.com/iOS-Tips/iOS-tech-set/master/images/qrcode.jpg"};
window.webkit.messageHandlers.JSBridge.postMessage(JSON.stringify(param));
複製程式碼
當端接收到 Web 發過來的訊息後,會呼叫 WKScriptMessageHandler
的代理方法,在這個方法中我們來下載圖片並儲存到相簿:
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.body isKindOfClass:[NSString class]]) {
if ([message.name isEqualToString:kScriptMsgName] && self.isOKAction) {
// 儲存圖片
NSDictionary *msgInfo = [NSJSONSerialization JSONObjectWithData:[message.body dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:nil];
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:msgInfo[@"url"]]]];
if (image) {
UIImageWriteToSavedPhotosAlbum(image, self, @selector(imageSavedToPhotosAlbum:didFinishSavingWithError:contextInfo:), nil);
}
}
}
}
複製程式碼
當把圖片儲存到相簿後,需要重新整理 Web 頁面上的按鈕的標題,這時需要執行 Web 頁中已經定義好的 change_state
方法:
- (void)updateSaveState:(BOOL)isSave
{
NSString *script = isSave ? @"change_state(true);" : @"change_state(false);";
[self.webView evaluateJavaScript:script completionHandler:^(id _Nullable msg, NSError * _Nullable error) {}];
}
複製程式碼
至此,我們還剩下最後一件事沒有完成,當載入出 WebView 後,需要根據本地是否已經儲存了圖片更新按鈕的標題,直接呼叫 updateSaveState
函式即可。
總結
本文主要介紹一個 Hybrid WebView 如何實現,它僅僅是從一個具體的需求出發,而如果做一個通用 Hybrid WebView 框架需要兩端設計一種通訊規則。具體細節可以參考味精的兩篇關於 Hybrid 的實踐 (從零收拾一個hybrid框架)。本文的 demo 會在這個專題完成後一塊放出。