托管 Javascript-WinRT 双工通信






4.86/5 (8投票s)
在本文中,我将解释如何在 Winrt 平台上创建本地 WebView 托管的 Html/Javascript 应用程序,以及如何在两者之间建立通信。
托管的 Javascript 到 WinRT 双向通信
来源
在 GitHub 上: https://github.com/Pavel-Durov/CodeProject-Hosted-Javascript-WinRT-Duplex
直接
引言
在本文中,我将讨论如何在 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 意图等的特殊类来进一步扩展此实现。当您涉及许多不同的功能时,这可能需要,但基本原理已得到解释,并且应该能够正常工作。
试试看!