zoukankan      html  css  js  c++  java
  • 为自己搭建一个鹊桥 -- 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
  • 相关阅读:
    调查问卷
    SQL 基础学习(1):下载DB Browser for SQLite. 下载graphviz(为了使用Rails ERD的前提)出现❌,已debug.
    路由完整实例代码
    如何自定义JSTL标签与SpringMVC 标签的属性中套JSTL标签报错的解决方法
    CSS样式表、JS脚本加载顺序与SpringMVC在URL路径中传参数与SpringMVC 拦截器
    SpringMVC的解释与搭建Maven私有代理服务器
    单调队列 bzoj3126 [Usaco2013 Open]Photo
    二分图 crf的军训
    单调队列 JC loves Mkk
    测试开发CICD——Git——window上安装git——配置基本信息
  • 原文地址:https://www.cnblogs.com/ms-uap/p/5306309.html
Copyright © 2011-2022 走看看