為自己搭建一個鵲橋 -- Native Page與Web View之間的JSBridge實現方式


說起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     }
View Code

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>
View Code

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         }
View Code

仔細想想。。好像也沒什么不對,夠動態,夠簡單。。。但現實總是殘酷的,實際使用過程中突然發現,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     }
View Code

一切的魔法都在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     }
View Code

現在是見證奇跡的時候了,來看看在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>
View Code

代碼都很直接,唯一需要說明的就是一定要注意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     }
View Code

接下來我們在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>
View Code

運行起來,等待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     }
View Code

然后在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>
View 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.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     }
View Code

結果如下。

最后如果你覺得寫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     }
View Code

修改我們的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     }
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM