說起JSBridge,大家最熟悉的應該就是微信的WeixinJSBridge,通過它各個公眾頁面可以調用后台方法和微信進行交互,為用戶提供相關功能。我們就來說說UWP下怎么樣實現我們自己的JSBridge。
在win10之前,如果需要實現JSBridge,我們大概有兩種方法:
1. window.external.notify
做過webview的小伙伴肯定都熟悉,html頁面可以通過window.external.notify將消息發送出去,然后客戶端使用WebView.ScriptNotify事件接收,但是兩邊都只能用字符串來交流,所以通常我們都會定義好消息格式(比如json)。現在在UWP中使用這種方法有個限制,就是你需要在.appxmanifest里把站點加到Content URIs中,告訴系統那些域名的js腳本是可以調用windows.external.notify方法的,當然如果是本地js就沒有這個限制的,添加方法如下圖。
但是我們總會有些特殊需求,比如微信/淘寶應用怎么辦?域名隨時可能增加,總不能每次都更新manifest,然后更新商店吧!在8.1的時候我們還可以使用WebView.AllowedScriptNotifyUris在應用中動態添加信任站點,但是win10中這個接口已經廢棄了,如果你的應用並不需要頻繁/動態更改信任站點,這個方法還是可用的。
后台處理完結果之后,可以通過WebView.InvokeScript/InvokeScriptAsync方法調用當前頁面中的js方法:
第一個參數是js方法名,第二個參數是調用這個方法需要的參數。
需要注意的是這個方法很容易出錯,一定要注意異常捕獲:(, 而且生成的異常基本都是一些0xXXXXX的code。

1 public sealed partial class MainPage : Page 2 { 3 BridgeObject.Bridge _bridge = new BridgeObject.Bridge(); 4 5 public MainPage() 6 { 7 this.InitializeComponent(); 8 9 this.wv.ScriptNotify += Wv_ScriptNotify; 10 11 this.Loaded += MainPage_Loaded; 12 } 13 14 private async void Wv_ScriptNotify(object sender, NotifyEventArgs e) 15 { 16 await (new MessageDialog(e.Value)).ShowAsync(); 17 18 //返回結果給html頁面 19 await this.wv.InvokeScriptAsync("recieve", new[] { "hehe, 我是個結果"}); 20 } 21 22 private void MainPage_Loaded(object sender, RoutedEventArgs e) 23 { 24 //我們事先寫好了一個本地html頁面用來做測試 25 this.wv.Navigate(new Uri("ms-appx-web:///assets/html/index.html", UriKind.RelativeOrAbsolute)); 26 } 27 }
html代碼:

1 <!DOCTYPE html> 2 3 <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> 4 <head> 5 <meta charset="utf-8" /> 6 <title></title> 7 8 <script> 9 10 //通知后台 11 function func1() 12 { 13 14 window.external.notify("this is a message"); 15 16 } 17 18 //這個方法用來接收后台的結果 19 function recieve(value) 20 { 21 output.textContent = value; 22 } 23 24 </script> 25 </head> 26 <body> 27 <div style="margin-top:100px"> 28 <button id="fun1Btn" onclick="func1();">Call method 1</button> 29 <div id="output"></div> 30 </div> 31 </body> 32 </html>
2. Url
是的,你沒有看錯,我們也可以通過url實現JSBridge,這也是我們在放棄上一種方法之后的一個備選方案,因為手淘就有之前說到的問題,站點可能不是固定的,而更新應用明顯不是個明智的選擇。具體就是每次html頁面需要調用后台code的時候,都發起一次頁面跳轉,當然跳轉的url符合一定的規則,並可以加上參數,然后我們用WebView.NavigationStarting事件截獲這次跳轉,並Cancel調這次跳轉,這樣一個看似可行的方案出爐啦,還是熱乎的呢!!
代碼其實很簡單,就是解析url參數,然后再通過WebView.InvokeScript/InvokeScriptAsync方法返回結果給頁面(這個方法不針對站點)。

1 private void Wv_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args) 2 { 3 if(args.Uri.OriginalString.StartsWith("http://our/jsbridge/url/pattern")) 4 { 5 //是一次jsbridge調用,取消本次跳轉 6 args.Cancel = true; 7 8 //這里具體解析url的參數 9 } 10 }
仔細想想。。好像也沒什么不對,夠動態,夠簡單。。。但現實總是殘酷的,實際使用過程中突然發現,WebView的Url有最大長度限制,而且這個值比Android和IOS都要小很多,導致很多參數被截斷了,最后只好放棄了。
就在上面兩種方案都不能完美適應所有需求的時候,另外一種bulingbuling的方法出現在我們眼前:WebView.AddWebAllowedObject,這個方法是win10中新添加的方法,允許我們把Windows Runtime對象直接傳遞給JS調用!
下面是這個方法的定義:
public void AddWebAllowedObject(string name, object pObject)
name是對象在js中對應的全局變量名,通過這個方法傳入到html頁面中的對象都是掛在js的window對象上的,pObject就是要傳入的對象。
首先新建一個Windows Runtime Component工程,添加一個新的類Bridge,我們之后就把這個傳給也main,看看這個類有什么特殊的。

1 //這個attribute是必須的,有了他我們的對象才能傳遞給WebView 2 [AllowForWeb] 3 public sealed class Bridge 4 { 5 /// <summary> 6 /// 提示一條消息 7 /// </summary> 8 /// <param name="msg"></param> 9 public void showMessage(string msg) 10 { 11 new MessageDialog(msg).ShowAsync(); 12 } 13 14 15 }
一切的魔法都在AllowForWebAttribute這個特性上,有了它,我們的對象就可以傳遞給webview,但是這里有一點一定要萬分小心,必須在NavigationStarting調用AddWebAllowedObject方法才可以!(我不會告訴你,我在DomLoaded事件里折騰了好久。。。)

1 public sealed partial class MainPage : Page 2 { 3 BridgeObject.Bridge _bridge = new BridgeObject.Bridge(); 4 5 public MainPage() 6 { 7 this.InitializeComponent(); 8 9 this.wv.NavigationStarting += Wv_NavigationStarting; 10 11 this.Loaded += MainPage_Loaded; 12 } 13 14 private void MainPage_Loaded(object sender, RoutedEventArgs e) 15 { 16 //我們事先寫好了一個本地html頁面用來做測試 17 this.wv.Navigate(new Uri("ms-appx-web:///assets/html/index.html", UriKind.RelativeOrAbsolute)); 18 } 19 20 private void Wv_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args) 21 { 22 //OURBRIDGEOBJ這個是我們的對象插入到頁面之后對象的變量名,這是一個全局變量,也就是window.OURBRIDGEOBJ 23 this.wv.AddWebAllowedObject("OURBRIDGEOBJ", _bridge); 24 } 25 }
現在是見證奇跡的時候了,來看看在js中怎么調用這個對象?(請忽略我這水平不怎么樣的html code。。。)

1 <!DOCTYPE html> 2 3 <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> 4 <head> 5 <meta charset="utf-8" /> 6 <title></title> 7 8 <script> 9 10 function func1() { 11 // 首先判斷我們對象是否正確插入 12 if (window.OURBRIDGEOBJ) { 13 //調用的我們消息函數 14 window.OURBRIDGEOBJ.showMessage("呵呵呵,我是個message"); 15 } 16 } 17 </script> 18 </head> 19 <body> 20 <div style="margin-top:100px"> 21 <button id="fun1Btn" onclick="func1();">Call method 1</button> 22 </div> 23 </body> 24 </html>
代碼都很直接,唯一需要說明的就是一定要注意js中調用方法時首字母都是小寫(即使你在后台定義的首字母大寫!當然這應該也是為了符合js的使用習慣),來看看結果。
當然如果它只有這點本事的話,並不會讓人很激動,畢竟我們以前也可以做到。
繼續之前,想想win10之前如果要通過jsbridge調用后台代碼實現一個異步操作會怎么實現呢?
1). 首先我們的js調用和WebView.InvokeScript是分開,所以通常我們要為每一次js調用生成一個id
2). 后台完成操作之后,通過InvokeScript方法返回結果時,需要把本次調用id傳回去,告訴頁面這個哪次調用的結果
3). 然后js再根據這個id回調繼續之前的操作。
但是現在我們可以拋棄那些繁瑣的步驟了,我們的Windows Runtime Component支持異步(IAsyncAction/IAsyncOperation<T>),而js又支持Promise,結合在一起,你懂的!
先給我們的類添加一個簡單的異步方法。

1 //這個attribute是必須的,有了他我們的對象才能傳遞給WebView 2 [AllowForWeb] 3 public sealed class Bridge 4 { 5 /// <summary> 6 /// 提示一條消息 7 /// </summary> 8 /// <param name="msg"></param> 9 public void showMessage(string msg) 10 { 11 new MessageDialog(msg).ShowAsync(); 12 } 13 14 public Windows.Foundation.IAsyncOperation<int> giveMeAnObject(int num) 15 { 16 return Task.Run(async () => 17 { 18 //延遲3秒鍾,模擬異步任務:) 19 await Task.Delay(3000); 20 21 return ++num; 22 }).AsAsyncOperation(); 23 } 24 }
接下來我們在js端,用promise.then來等待結果。

1 <!DOCTYPE html> 2 3 <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> 4 <head> 5 <meta charset="utf-8" /> 6 <title></title> 7 8 <script> 9 10 function func1() { 11 // 首先判斷我們對象是否正確插入 12 if (window.OURBRIDGEOBJ) { 13 //調用的我們消息函數 14 window.OURBRIDGEOBJ.showMessage("呵呵呵,我是個message"); 15 } 16 } 17 18 function func2() { 19 if (window.OURBRIDGEOBJ) { 20 21 //對於js來說winrt的異步操作都會對應到promise上 22 var result = window.OURBRIDGEOBJ.giveMeAnObject(12); 23 24 // 等待結果 25 result.then(function (nextNum) { 26 // nextNum就是IAsyncOperation<int>的真正返回值 27 output.textContent = nextNum; 28 }); 29 30 } 31 } 32 </script> 33 </head> 34 <body> 35 <div style="margin-top:100px"> 36 <button id="fun1Btn" onclick="func1();">Call method 1</button> 37 <button id="fun2Btn" onclick="func2();">Call method 2</button> 38 <div id="output" /> 39 </div> 40 </body> 41 </html>
運行起來,等待3秒之后,結果出來了!
另外這里再補充下評論中小伙伴關於事件的調用方法,其實事件的使用很簡單,唯一需要注意的是c#的事件名稱,到js里全都變成了小寫的,下面是代碼。
首先為我們的Bridge類添加一個事件和觸發事件的公開方法(方便調試)。

1 //這個attribute是必須的,有了他我們的對象才能傳遞給WebView 2 [AllowForWeb] 3 public sealed class Bridge 4 { 5 private IBridgeMethods _methods = null; 6 7 public event EventHandler<int> SomethingChanged; 8 9 public void FireEvent() 10 { 11 SomethingChanged?.Invoke(this, 1234); 12 } 13 14 /// <summary> 15 /// 提示一條消息 16 /// </summary> 17 /// <param name="msg"></param> 18 public void ShowMessage(string msg) 19 { 20 _methods?.ShowMessage(msg); 21 } 22 23 public IAsyncOperation<int> giveMeAnObject(int num) 24 { 25 return _methods?.GiveMmeAnObject(num); 26 } 27 28 /// <summary> 29 /// 初始化個方法的實現 30 /// </summary> 31 /// <param name="obj"></param> 32 public void Init(IBridgeMethods obj) 33 { 34 _methods = obj; 35 } 36 }
然后在js中添加listener,這里是要用js的標准方法!

1 <!DOCTYPE html> 2 3 <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> 4 <head> 5 <meta charset="utf-8" /> 6 <title></title> 7 8 <script> 9 10 function func1() { 11 // 首先判斷我們對象是否正確插入 12 if (window.OURBRIDGEOBJ) { 13 //調用的我們消息函數 14 window.OURBRIDGEOBJ.showMessage("呵呵呵,我是個message"); 15 } 16 } 17 18 function func2() { 19 if (window.OURBRIDGEOBJ) { 20 21 //對於js來說winrt的異步操作都會對應到promise上 22 var result = window.OURBRIDGEOBJ.giveMeAnObject(12); 23 24 // 等待結果 25 result.then(function (nextNum) { 26 // nextNum就是IAsyncOperation<int>的真正返回值 27 output.textContent = nextNum; 28 }); 29 30 } 31 } 32 33 function bindEvent() { 34 if (window.OURBRIDGEOBJ) { 35 //注意事件名稱!!! 36 window.OURBRIDGEOBJ.addEventListener("somethingchanged", function (value) { 37 output.textContent = "我是個事件回調: value="+value; 38 }); 39 } 40 } 41 </script> 42 </head> 43 <body> 44 <div style="margin-top:100px"> 45 <button id="fun1Btn" onclick="func1();">Call method 1</button> 46 <button id="fun2Btn" onclick="func2();">Call method 2</button> 47 <button id="bindBtn" onclick="bindEvent();">Bind event</button> 48 <div id="output" /> 49 </div> 50 51 52 </body> 53 </html>
最后在窗口上添加一個觸發按鈕。

1 public sealed partial class MainPage : Page 2 { 3 BridgeObject.Bridge _bridge = new BridgeObject.Bridge(); 4 5 public MainPage() 6 { 7 this.InitializeComponent(); 8 9 this.wv.NavigationStarting += Wv_NavigationStarting; 10 11 this.Loaded += MainPage_Loaded; 12 } 13 14 private void MainPage_Loaded(object sender, RoutedEventArgs e) 15 { 16 //我們事先寫好了一個本地html頁面用來做測試 17 this.wv.Navigate(new Uri("ms-appx-web:///assets/html/index.html", UriKind.RelativeOrAbsolute)); 18 } 19 20 private void Wv_NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args) 21 { 22 //OURBRIDGEOBJ這個是我們的對象插入到頁面之后對象的變量名,這是一個全局變量,也就是window.OURBRIDGEOBJ 23 this.wv.AddWebAllowedObject("OURBRIDGEOBJ", _bridge); 24 } 25 26 private void Button_Click(object sender, RoutedEventArgs e) 27 { 28 // 觸發自定義事件 29 _bridge.FireEvent(); 30 } 31 }
結果如下。
最后如果你覺得寫component限制太多的話(繼承都不讓用。。),可以使用接口定義方法,然后在類庫中實現這些方法也是一個不錯的方案,下面是一個比較簡單的實現供參考。
我們的jsbridge接口,包含我們准備提供的方法。

1 /// <summary> 2 /// 用來定義JSBridge中實現的方法 3 /// </summary> 4 public interface IBridgeMethods 5 { 6 IAsyncOperation<int> GiveMmeAnObject(int num); 7 void ShowMessage(string message); 8 }
修改我們的Bridge類,所有的方法都通過上面的接口來提供。

1 //這個attribute是必須的,有了他我們的對象才能傳遞給WebView 2 [AllowForWeb] 3 public sealed class Bridge 4 { 5 private IBridgeMethods _methods = null; 6 7 8 /// <summary> 9 /// 提示一條消息 10 /// </summary> 11 /// <param name="msg"></param> 12 public void ShowMessage(string msg) 13 { 14 _methods?.ShowMessage(msg); 15 } 16 17 public IAsyncOperation<int> giveMeAnObject(int num) 18 { 19 return _methods?.GiveMmeAnObject(num); 20 } 21 22 /// <summary> 23 /// 初始化個方法的實現 24 /// </summary> 25 /// <param name="obj"></param> 26 public void Init(IBridgeMethods obj) 27 { 28 _methods = obj; 29 } 30 }