为自己搭建一个鹊桥 -- 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 }
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 }