65.9K
CodeProject 正在变化。 阅读更多。
Home

托管 Javascript-WinRT 双工通信

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (8投票s)

2015年7月10日

CPOL

9分钟阅读

viewsIcon

13736

downloadIcon

167

在本文中,我将解释如何在 Winrt 平台上创建本地 WebView 托管的 Html/Javascript 应用程序,以及如何在两者之间建立通信。

托管的 Javascript 到 WinRT 双向通信

来源

在 GitHub 上: https://github.com/Pavel-Durov/CodeProject-Hosted-Javascript-WinRT-Duplex
直接

 

 

知识要求: Html, Javascript, C#, Winrt

引言

在本文中,我将讨论如何在 WebView 中建立 C# 和 JavaScript 之间的基本通信。我将介绍 WinRT 应用程序包的使用以及与我们的目标相关的 Visual Studio 项目配置。

示例将使用 WinRT 作为平台进行演示,但相同的概念可以类似地用于所有其他平台。

这种交互将是双向的,在通信的一侧,我们将使用 Internet Explorer 网页引擎(WebView Xaml 控件),因为我们是为 Windows 开发的,而另一侧是 JavaScript。

为什么要做这件事?

乍一看,创建这种通信似乎很奇怪,但有时您就是需要它。

示例

我们需要创建一个跨平台应用程序,并且我们希望创建一个组件,该组件将在所有平台上为我们服务。

当然,我们可以仅通过在 WebView 中托管它并在所有平台上运行来仅使用 Html/JavaScript,但如果我们只需要使用某些特定于设备的功能 – 例如创建本地日志文件或录制语音消息等……。

这时您就需要实现这种交互。

全局概述

我们将创建一个 WinRT 项目,该项目在页面布局上有一个拉伸的 WebView,因此最终用户只会看到 WebView 作为其内容。

页面加载完成后,我们将导航到存储在本地文件夹中的本地 html 文件,实际上这就像使用 html 缓存文件并离线浏览 – 而无需依赖互联网。

所有应用程序逻辑将由 JavaScript 执行,C# 端仅用于调用特定于设备的功能,因此我们的目标是创建一个可在所有平台上重用的组件。

我们的应用程序架构可以用以下图表示

Visual Studio 解决方案和项目细节

调试器类型 – 应用程序进程

Visual Studio 项目有几种调试模式

托管、原生、混合、脚本

选择调试器类型

右键单击项目 -> 属性 -> 调试

当您在调试模式之间切换时(在我们的示例中为 Debug/Script),您将能够调试 JavaScript 和托管代码,但一次只能调试一种。您不能混合这些模式(只有托管和原生可以混合)。

选择托管模式以调试 C#,或选择脚本模式以调试 JavaScript;

应用程序的本地文件夹和包名称

为了创建基本集成,我们需要从 Web 视图导航到本地 HTML 文件。

这可以在没有本地文件的情况下完成,但需要特殊权限 – 本文将不讨论此主题。

构建项目一次,然后在 PC 上导航到其本地文件夹

本地文件夹路径应遵循此约定

C:\Users\<USER NAME>\AppData\Local\Packages\<PACKAGE GUID>\LocalState

如果您想简化对本地文件夹的访问,可以手动设置您的包名称以便于使用

从解决方案资源管理器中选择Package.appxmanifest文件,然后导航到“Packaging”选项卡

将包名称替换为可以在“Packages”目录层次结构中立即注意到的某个guid值。

我将我的包名称设置为一个任意值:01010101-0101-0101-0101-0101010101212

当您部署项目时,您将在“Packages”目录中看到此文件夹

这是您 PC 上到本地文件夹的实际路径

C:\Users\<USER NAME>\AppData\Local\Packages\01010101-0101-0101-0101-0101010101212

为了从代码中访问此文件夹,请使用以下代码

ApplicationData.Current.LocalFolder

// Summary:
//     Gets the root folder in the local app data store.

// Returns:
//     The file system folder that contains the files.

public StorageFolder LocalFolder { get; }

 

实现通信

为了创建基本通信,您需要从 Web 视图导航到本地 HTML 文件。构建项目一次,然后在 PC 上导航到其本地文件夹

本地文件夹路径应遵循此约定

C:\Users\<USER NAME>\AppData\Local\Packages\<PACKAGE GUID>\LocalState

在本地文件夹中创建一个 HTML 并为其命名(无论您如何命名,请记住相应地更改 NotifyScript 参数……)。

您可以手动完成,也可以简单地将文件从您的 assets(或任何您想要的)文件夹复制到您的本地文件夹。在我的示例中,我将在应用程序启动时(在OnLaunched 方法中)在我的 App 类中使用此函数。

private async void CreateLocalFile()            
{
    string indexName = "index.html";
    string localFolderPath = Path.Combine("Assets\HTML\" + "index.html");
    var item = await ApplicationData.Current.LocalFolder.TryGetItemAsync(localFolderPath);
    if (item == null)
    {
        StorageFolder InstallationFolder = Windows.ApplicationModel.Package.Current.InstalledLocation;
        StorageFile assetfile = await InstallationFolder.GetFileAsync(localFolderPath);
        await assetfile.CopyAsync(ApplicationData.Current.LocalFolder, indexName, NameCollisionOption.ReplaceExisting);
    }
}

 

HTML/JS 文件示例

<html>
<head>
    <title>This is a title that no one will notice</title>
    <script>
        function Test(message) {//The function that will be called from C#
            if (message) {
                window.external.notify(message + " " + new Date())            }
        }
    </script>
</head>
<body>
<h1>Hello from html</h1>
<input type="button" value="Send message to C#" onclick="Test('Hey')"/>
</body>
</html>

 

  • 您可以按按钮以从 Js 发送消息到 C#。

 

导航到本地 HTML 文件

要从 WebView 导航到本地 HTML 文件,您需要使用实现 IUriToStreamResolver 的自定义对象。

IUriToStreamResolver 有一个公共方法。该对象的主要目标是将文件路径转换为InputStream

public interface IUriToStreamResolver
{
    IAsyncOperation<global::Windows.Storage.Streams.IInputStream> UriToStreamAsync(Uri uri);
}

 

我们的实现

public class UrlResolver : IUriToStreamResolver
{
    public IAsyncOperation<IInputStream> UriToStreamAsync(Uri fileName)
    {
        IAsyncOperation<IInputStream> result = null;
        result = GetContent(fileName.AbsolutePath).AsAsyncOperation();
        return result;
    }
    private async Task<IInputStream> GetContent(string fileName)
    {
        IRandomAccessStream result = null;
        String path = fileName.Replace("/", "");
        var storageFile = await ApplicationData.Current.LocalFolder.TryGetItemAsync(path);
        if (storageFile != null && storageFile.IsOfType(StorageItemTypes.File))
        {
            StorageFile file = storageFile as StorageFile;
            result = await file.OpenAsync(FileAccessMode.Read);
        }
        return result;
    }
}

现在我们有了这个 Resolver 对象,我们可以在我们的 View 代码隐藏中使用它来导航到我们的本地 HTML 文件

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    Uri path = webView.BuildLocalStreamUri("someIdentifier", "index.html");
    var uriResolver = new UrlResolver();

    try
    {
        webView.NavigateToLocalStreamUri(path, uriResolver);
    }
    catch (Exception ex)
    {
        if (ex != null)
            Debug.WriteLine(ex.ToString());
    }
}

 

我们在这里调用一个 WebView 函数 BuildLocalStreamUri,它接受一个文件路径和一个键,这将为我们的文件生成一个本地 Uri。

在下一行,我们实例化我们的 StreamResolved,并将生成的 Uri 和 Resolver 对象作为参数传递给 WebView 函数 NavigateToLocalStreamUri

本地流 URI 约定应如下所示

ms-local-stream://< LOCAL FILE PATH >

注意前缀:ms-local-stream

如果您在 StreamResolved UriToStreamAsync 函数中设置断点,当您调用 NavigateToLocalStreamUri 函数时,您的断点就会被命中。该函数由 WebView 调用,因为它试图从给定的 URI 获取流。

从 JS 调用 C#

window.external.notify 函数

如果您打开 Internet Explorer Console 并在 Console 中键入函数,您将得到一个undefined作为结果,因为window.external是原生的。

在 MSDN 上阅读相关内容

https://msdn.microsoft.com/en-us/library/ms535246(v=vs.85).aspx

集成

所有从 JavaScript 到 C# 的调用以及反之亦然的调用都将通过一个名为window.external.notify的函数实现,该函数接受一个字符串参数并将其作为 NotifyEventArgs 值发送到 WebView 事件 ScriptNotify

window.external.notify("Hello from JS");

在 C# 中接收 Js 消息

为了在 C# 中接收 JavaScript 调用,您需要订阅 ScriptNotify 事件。

webView.ScriptNotify += webView_ScriptNotify;

void webView_ScriptNotify(object sender, NotifyEventArgs e)
{
        if (e != null && !String.IsNullOrEmpty(e.Value))
        {
            Debug.WriteLine("JS Message {0}", e.Value);
        }
    }
}

 

从 C# 调用 JS 方法

首先,我们需要在 JavaScript 中实现一个函数,如下所示

 

function Test(message){
    if(message){
        window.external.notify("I Recieved your message");
    }
}

 

现在我们将使用名为 NotifyScript 的 WebView 函数从 C# 调用此方法

public async Task NotifyScript()
{                           
    try
    {
        await webView.InvokeScriptAsync("Test", new string[] { "hello from C#!" });
    }
    catch (Exception e)
    {
        //throw;
    }
}

如果您按照步骤操作,请查看 Visual Studio 中的输出窗口和 JS 控制台,看看是否收到了打印的消息……

您可以使用快捷键:Ctrl + Alt + O

 

JavaScript 回调函数

通常您不会希望同步等待 C# 的响应,因为它可能需要一段时间,然后 UI 就会冻结或 JavaScript 代码卡住 –这是很糟糕的

这就是为什么您可能希望使用回调函数。概念很简单,我们将函数作为一个对象传递,并将其实例存储在某个集合中 – 在我们的情况下是一个字典,我们将释放 JavaScript 线程直到我们从 C# 代码中获得结果,届时我们将调用该方法,结果就是传入的实例将通过回调被通知。

安装 Json Nuget 包

我们将序列化的 JSON 对象从 JavaScript 发送到 C# 作为我们通信的基础,所以显然我们需要在托管代码端反序列化发送的对象 – 这样我们才能使用它。

当然,您可以自己完成并根据 JSON 对象格式从字符串中提取数据,但如果您可以使用别人已经测试过的有效库,为什么还要给自己增加难度呢?

在我的项目中,我使用 Newtonsoft.Json Nuget 作为 Json 转换器。

在 Visual Studio 中,您可以从 GUI 或从程序包管理器控制台安装 Nuget。我使用了控制台,如下所示

打开程序包管理器控制台

在快速启动中键入:“程序包管理器控制台”,然后按 ENTER,您将看到一个控制台

键入以下命令(确保您已连接到互联网)

Install-Package Newtonsoft.Json

就是这样,您已安装了 Nuget。您现在可以看到您的项目已引用它。

 

 

异步调用 C# 方法

JSON 对象作为协议

为了建立正常的通信(一如既往),我们需要发明一种双方都已知并使用的协议。

在我们的示例中,我们将基于 JSON 对象来构建此协议,如下所示

{ "data" :"", "guid": "" }

Data – 这是我们将作为数据发送到 C# 的数据(显然)。

Guid – 这是一个唯一的标识符,它帮助我们在从 C# 端获得结果时找到回调函数。

<script>
setInterval(function(){console.log("ji");},3000);    
var intervalFunction = function(){
    callbackService.Send( callbackService._formatObject("Hello From JS!"));
}
    var callbackService = {
        callbackService._guid = 0;
          //Native callbacks array, will be invoked as native invoke the NotifyJS event
        callbackService.callbacks = []; 

      //An event theat called from the native side, 
    //picked from the nativeCallBacks array by GUID 
    callbackService.NotifyJS = function (guid, data){
        if(data && guid){
            if(callbackService.callbacks[guid])
            {
                //invoking the found callback
                callbackService.callbacks[guid](data);
                //Deletes the callback from the array
                delete callbackService.callbacks[guid];  
            }
        }
    }
    
    //Sends to the C# side...
    //cmd = the desitred command (log, sql, etc...)
    //content = the content of the message
    //callback that will be invoked as native invokes NotifyJS function
    callbackService.Send = function(data, callback){
        var jObj = callbackService._formatObject( content);
        callbackService.callbacks[jObj.guid] = callback;
        //The actual javacript method that send the event to the webview
        window.external.notify(JSON.stringify(jObj));   
    } 
    
    //Gets the format of the API sent object
    //cmd = the desitred command (log, sql, etc...)
    //content = the content of the message
    //guid = an unique identifier, for determining the callback in callbacks array.
    callbackService._formatObject = function( content){
        var guid = _guid + 1;
        return {
            data   : content,
            'guid'      : guid
        };
    };
</script>

callbackService 对象有一个回调数组,其中存储了传递给 Send 函数的回调实例。callbackService 将在 C# 使用 guid 标识符将响应发送回 JavaScript 请求时调用回调。

使用后删除实例(delete)非常重要,否则这些对象将继续存在于我们应用程序的内存中。

请注意,我们没有生成复杂的 guid 值 – 我们只是使用一个计数器,该计数器在重新启动应用程序进程或刷新 html 时将被初始化为零。

在 C# 中捕获 JavaScript 调用并返回结果

为了在 C# 端捕获 JavaScript 调用,我们需要订阅 ScripNotify 事件。在此事件中,我们将收到 JSON 对象,从中提取相关数据,执行一些操作,并发送带有 guid 标识符的响应。

我在这里将使用 Newtonsoft.Json 库(我之前已安装)来解析 JSON 数据,但您可以自己解析或使用其他喜欢的库。

       

void webView_ScriptNotify(object sender, NotifyEventArgs e)
 {
    if (e != null && !String.IsNullOrEmpty(e.Value))
    {
     
            var jobj = JsonConvert.DeserializeObject<JObject>(e.Value);
            JToken dataValue;
            JToken guidValue;
            if (jobj.TryGetValue("data", out dataValue) && jobj.TryGetValue("guid", out guidValue)) 
            {
                //DoSomeWork(guidValue.Value<string>());
                await Task NotifyScript("TestNative", guidValue.Value<string>(), "{'data': 'success'}");

            }
    }
}

当我们反序列化 JSON 对象时,我们会从中提取数据,并根据其属性 guid 返回响应。J

 

摘要

就是这样,我们通过 WebView 引擎创建了 C# 和 JavaScript 之间的交互。

我们可以通过创建一个标识 JavaScript 意图等的特殊类来进一步扩展此实现。当您涉及许多不同的功能时,这可能需要,但基本原理已得到解释,并且应该能够正常工作。

试试看!

 

© . All rights reserved.