寫在前面:

Webview是我們前端開發從PC端演進到移動端的一個重要載體,現在大家每天使用的App,webview都發揮著它的重要性。接下來讓我們從webview看世界。

一、適用場景

提到應用場景,大家最直觀的能想到一些App內嵌的頁面,為我們提供各種各樣的交互,就像下面圖片裏的這樣:

其實webview的應用場景遠遠不止這些,其實在一些PC的軟體裏,和我們交互的也是我們的html頁面,只是穿著webview的衣服,衣服太美而我們沒有發現他們的真諦。

另外,還有一些網路機頂盒裡的交互,也是webview在和我們打交道,比如一些早期的IPTV裏的EPG都是運行在webview裏的,它們基於webkit內核,儘管我們使用的交互方式是遙控器。

當然,今天我們會從native的角度切入,帶大家認識真正的webview。

二、與App native的交互

說了這麼多,其實目前使用頻率最多的,還是客戶端內嵌的webview,小到我們地鐵裏用手機看的一篇公眾號文章,大到我們使用App中的一些重要交互流程,其實都是webview打開m頁去承接的。那麼,到底m頁怎麼和native去交互的呢?

目前javascript和客戶端(後面統稱native)交互的常見方式有兩種,一種是通過JSBridge的方式,另一種是通過schema的方式。

1. JSBridge

首先,我們來說說JSBridge。體現的形式其實就是,當我們在native內打開m頁,native會在全局的window下,為我們注入一個Bridge。這個Bridge裡面,會包含我們與native交互的各種方法、比如判斷第三方App是否安裝、獲取網路信息等等功能。

舉個例子:

/**
* 作用域下的JSBridge,
* 和實例化後的getNetInfomation,
* 均根據實際約定情況而定,
* 這裡只是用來舉例說明
*/
const bridge = window.JSBridge;
console.log(bridge.getNetInfomation());

IOS端

在IOS中,主要使用WebViewJavascriptBridge來註冊,可以參考Github WebViewJavascriptBridge

jsBridge = [WebViewJavascriptBridge bridgeForWebView:webView];

...

[jsBridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
// to do
}];

Android

在Android中,需要通過addJavascriptInterface來註冊

class JSBridge{
@JavascriptInterface //注意這裡的註解。出於安全的考慮,4.2 之後強制要求,不然無法從 Javascript 中發起調用
public void getNetInfomation(){
// to do
};
}

webView.addJavascriptInterface(new JSBridge();, "JSBridge");

2. Schema url

如果說Bridge的方式是隻能在native內部交互,那麼schame url的不緊可以在native內交互,也是可以跨app來交互的。schema也是目前我們轉轉使用的主要方式,它類似一個偽協議的鏈接(也可以叫做統跳協議),比如:

schema://path?param=abc

在webview裏,當m頁發起schema請求時,native端會去進行捕獲。這裡可以順帶給大家普及一下IOS和Android的知識,具體如下:

IOS端

以UIWebView為例,在IOS中,UIWebView內發起網路請求時,可以通過delegate在native層來攔截,然後將捕獲的schema進行觸發對應的功能或業務邏輯(利用shouldStartLoadWithRequest)。代碼如下:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
//獲取scheme url後自行進行處理
NSURL *url = [request URL];
NSString *requestString = [[request URL] absoluteString];
return YES;
}

Android端

在Android中,可以使用shouldoverrideurlloading來捕獲schema url。代碼如下:

public boolean shouldOverrideUrlLoading(WebView view, String url){
//讀取到url後自行進行分析處理

//這裡注意:如果返回false,則WebView處理鏈接url,如果返回true,代表WebView根據程序來執行url
return true;
}

上面分別是IOS和Android簡單的schema捕獲代碼,可以在函數中根據自己的需求,執行對應的業務邏輯,來達到想要的功能。


當然,剛才我們提到通過schema的方式可以進行跨端交互,那具體如何操作呢?

其實對於JavaScript,在webview裏基本是一樣的,也是發起一個schema的請求,只不過在native側會有些許變化。

首先,給大家普及一個小知識,就是在natvie中(包括IOS和Android),會通過schema找到相匹配的App。其中IOS不可以重複,就像appId一樣;安卓可以重複,遇到重複情況時,會彈窗讓用戶選擇其中之一。

那麼,有了這個知識點做鋪墊,就可以理解,當我們在其他app中,像這個schema發起請求時,系統底層(IOS & Android)會通過schema去找到所匹配的app,然後將此App拉起。拉起app後,對應處理如下:

IOS端

在IOS端內,會將schema作為參數傳入一個提前定義好的回調函數內,然後執行該回調函數。此回調函數,可以通過得到的schema去進行解析,然後定向到app內的固定的某個頁面。

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation{
// 參數 url 即為獲取的 schema
// to do
}

Android端

在Android端內,會稍微麻煩一些,在外部的m頁,會發起一個schema的偽協議鏈接,系統會去根據這個schema去檢索,需要被拉起的App需要有一個配置文件,大致如下:

<activity
android:name=".activity.StartActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="zhuanzhuan"/>
</intent-filter>
</activity>

以上面的代碼為例,在上面配置中scheme為zhuanzhuan,只要是 "zhuanzhuan://" 開頭的schema的鏈接都會調起配置該schema的Activity(類似上面代碼的 StartActivity),此Activity會對這個 schema url 做處理,例如:

public class StartActivity extends TempBaseActivity {
Intent intent;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

intent = getIntent();
Uri uri = intent.getData();
}
}

例如上面的代碼,可以在此Activity中,通過 intent 中的 getData 方法,獲取到傳入的schema的相關信息,如下圖:

這也是我們在第三方app內,可以調起自己app的原理。當然現在市場上一些app,為了怕有流量流失,會對schema進行限制,只有plist白名單裏的schema才能對應拉起,否則會被直接過濾掉。比如我們的wx爸爸,開通白名單後,纔可以使用更多的jsApiList,通過schema的拉起就是其中之一,在此不做贅述…… :)

三、webview的進化

對於webview,要說進化、或者蛻變,讓我第一想到的就是IOS的WKWebView了,每一個事物存在都有它的必然,讓我們一起看看這個super版的webview。

1. WKWebView的出現

目前混合開發已然成為了主流,為了提高體驗,WKWebView在IOS8發布時,也隨之一起誕生。在這之前IOS端一直使用的是UIWebView。

從性能方面來說,WKWebView會比UIWebView高很多,可以算是一次飛躍。它採用了跨進程的方案,用 Nitro JS 解析器,高達 60fps 的刷新率。同時,提供了很好的H5頁面支持,類比UIWebView還多提供了一個載入進度的屬性。目前一些一線互聯網app在IOS已經切換到了WKWebView,所以感覺我們無法拒絕。

整個WKWebView的初始化也很簡單:

WKWebView *webView = [[WKWebView alloc] init];
NSURL *url = [NSURL URLWithString:@"https://m.zhuanzhuan.com"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];

基本和UIWebView的很像。

2. WKWebView 與 UIWebView的對比

上面有提到性能的提升,為什麼 app 接入 WKWebView 之後,相對比 UIWebView 內存佔用小那麼多,主要是因為網頁的載入和渲染這些耗內存和性能的過程都是由 WKWebView 進程去實現的(WKWebView是獨立於app的進程)。如下圖:

這樣,互相進程獨立相當於把整個App的進程對內存的佔用量減少,App進程會更為穩定。況且,即使頁面進程崩潰,體現出來的就是頁面白屏或載入失敗,不會影響到整個App進程的崩潰。

除了上面說的性能以外,WKWebView會比UIWebView多了一個詢問過程。在伺服器完成響應之後,會詢問獲取內容是否載入到容器內,在控制上會比UIWebView更細粒度一點,也可以在一些通信上更好的和m頁進行交互。大概流程如下圖:

WKWebView 的代理協議為 WKNavigationDelegate,對比 UIWebDelegate 首先跳轉詢問,就是載入 URL之前的一次調用,詢問開發者是否下載並載入當前 URL,UIWebView 只有一次詢問,就是請求之前的詢問,而 WKWebView 在 URL 下載完畢之後還會發一次詢問,讓開發者根據伺服器返回的 Web 內容再次做一次確定。

四、任重而道遠

前面說到WKWebView這麼贊,其實開發中也有一些痛點。不同於UIWebView,WKWebView很多交互都是非同步的,所以在很大程度上,在和m頁通信的時候,提高了開發成本。

1. cookie

首先就是cookie問題,這個目前我認為也是WKWebView在業界的一個坑。之前出現過一個問題,就是在IOS登陸完成後,馬上進入m頁,會有登錄態的cookie獲取不到的問題。這個問題在UIWebView中是不存在的。

經過調研發現,主要問題是UIWebView對cookie是通過NSHTTPCookieStorage來統一處理的,服務端響應時寫入,然後在下次請求時,在請求頭裡會帶上相應的cookie,來做到m頁和native共享cookie的值。

但是在WKWebView中,則不然。它雖然也會對NSHTTPCookieStorage來寫入cookie,但卻不是實時存儲的。而且從實際的測試中發現,不同的IOS版本,延遲的時間還不一樣,無意對m頁的開發者是一種挑戰。同樣,發起請求時,也不是實時讀取,無法做到和native同步,導致頁面邏輯出錯。

針對這個問題,目前我們轉轉的解決方法是需要客戶端手動幹預一下cookie的存儲。將服務響應的cookie,持久化到本地,在下次webview啟動時,讀取本地的cookie值,手動再去通過native往webview寫入。大致流程如下圖:

當然這也不是很完美的解決方案,因為偶爾還有spa的頁面路由切換的時候丟失cookie的問題。cookie的問題還需要我們和客戶端的同學繼續去探索解決。在這裡,如果大家有什麼好的建議和處理方法歡迎留言,大家一起學習進步。

2. 緩存

除了cookie以外,WKWebView的緩存問題,最近我們也在關注。由於WKWebView內部默認使用一套緩存機制,開發者可以操作的許可權會有限制,特別是IOS8版本,也許是當時剛誕生WKWebView的緣故,還很不完善,根本沒法操作(當然相信IOS8很快會退出歷史舞臺)。對於一些m頁的靜態資源,偶爾會出現緩存不更新的情況,著實讓人頭疼。

但在IOS 9 之後,系統提供了緩存管理的介面 WKWebsiteDataStore

// RemoveCache
NSSet *websiteTypes = [NSSet setWithArray:@[
WKWebsiteDataTypeDiskCache,
WKWebsiteDataTypeMemoryCache]];
NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteTypes
modifiedSince:date
completionHandler:^{
}];

至於IOS8,就只能通過刪除文件來解決了,一般WKWebView的緩存數據會存儲在這個目錄裏:

~/Library/Caches/BundleID/WebKit/

可通過刪除該目錄來實現清理緩存。


另外,以上我們說的痛點以外,還有webview的通病,就是我們每次首次打開m頁時,都要有webview初始化的過程,那麼如何減少初始化webview的時間,也是我們可以提高頁面打開速度的一個重要環節。

當然,為了提高頁面的打開速度,咱們m頁也可以跟native去結合,做一些離線方案,目前轉轉內部也有一些離線頁面的項目有上線,今天就不在此展開。

講到這裡,我們也進入尾聲了,也許不久的將來各種新興的技術會掩蓋一些webview的光環,像react-native、小程序、安卓的輕應用開發等等,但是不可否認的是,webview不會輕易退出歷史舞臺,我們會把交互做的更好,我們也有情懷。哪有什麼歲月靜好,只不過有人負重前行……

推薦閱讀:

相關文章