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

在您的 C#/UWP 应用程序中使用 JavaScript 框架

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2015年12月2日

CPOL

9分钟阅读

viewsIcon

30470

得益于 Chakra(Microsoft Edge 使用的 JavaScript 引擎),现在可以在任何通用 Windows 平台应用程序中托管最快的 JavaScript 引擎之一(也是对 ECMAScript 6 支持度最高的引擎)。

毫无疑问,JavaScript 拥有最活跃的生态系统。每个月都有大量新框架发布(https://www.javascripting.com/)。

作为一名 C# 开发者——即使拥有一个优秀且活跃的 C# 社区——有时您可能会感到有点嫉妒。

如果我们也能将 JavaScript 语言和生态系统带入 C# 世界呢?如果 C# 开发者可以在 C# 中使用 JavaScript 呢?

别担心!我很高兴地宣布我创建了一个新的 WinRT 项目——ChakraBridge——它将让您像任何 Web 开发者一样加入派对。

事实上,得益于 Chakra(Microsoft Edge 使用的 JavaScript 引擎),现在可以在任何通用 Windows 平台应用程序中托管最快的 JavaScript 引擎之一(也是对 ECMAScript 6 支持度最高的引擎)。ChakraBridge 将 Chakra 引擎嵌入到 WinRT 应用程序中,并提供所有必需的高级工具,以便在 C# / UWP 应用程序中无缝使用它。

开发基于 HTML/JS/CSS 的 UWP 应用程序(旧世界中的 WWA 或托管应用程序)的人无需单独托管 Chakra,因为它已经是沙盒的一部分。

如何使用它?

这非常简单:只需前往 https://github.com/deltakosh/JsBridge 并将项目克隆到您的硬盘上。

现在您有两个选择:您可以将 ChakraBridge 项目(一个 WinRT 库)添加到您的解决方案中,或者您可以从 /dist 文件夹引用 ChakraBridge.winmd

初始化 Chakra

引用后,您可以调用以下代码行使 Chakra 准备就绪

host = new ChakraHost();

名为 host 的变量是您的 JavaScript 上下文。

您可能还希望能够跟踪发送到 JavaScript 控制台的消息。为此,请添加此代码

Console.OnLog += Console_OnLog;

连接后,每当 JavaScript 代码执行“console.log()”时,此事件处理程序就会被调用。

我可以使用哪些 JavaScript 框架?

在定义您可以做什么之前,您必须了解 Chakra 是一个 JavaScript 引擎,这意味着您可以在应用程序中执行 JavaScript 代码,但与 HTML 或 CSS 无关。

然后,您可以选择任何与 HTML(DOM 操作)或 CSS 无关的框架。以下是一些示例(但还有很多):

  • Facebook
  • Twitter 客户端
  • Instagram
  • Pinterest
  • PouchDB
  • CDC
  • Hello.js
  • Together.js
  • Math.js
  • Moment
  • 等等。

一旦您选择了想要使用的框架,您就必须将其注入到您的 Chakra 上下文中。在我的例子中,我想要使用 CDC(CloudDataConnector),因为我需要一种无缝连接到各种云数据提供商(Amazon、Azure、CouchDB 等)的方法。您可以下载 .js 文件并将其嵌入到您的项目中,或者每次启动应用程序时都下载它们

await ReadAndExecute("cdc.js");
await ReadAndExecute("azuremobileservices.js");
await ReadAndExecute("cdc-azuremobileservices.js");
await ReadAndExecute("sample.js");

如果您更喜欢引用实时 .js 文件,可以将 ReadAndExecute 替换为 DownloadAndExecute

现在您的 JavaScript 上下文已经编译并执行了引用的文件。

请注意,“sample.js”是一个自定义 JavaScript 文件,其中包含我应用程序的客户端代码

var CDCAzureMobileService = new CloudDataConnector.AzureDataService();

var CDCService = new CloudDataConnector.DataService(new CloudDataConnector.OfflineService(), new CloudDataConnector.ConnectivityService());
CDCAzureMobileService.addSource('https://angularpeoplev2.azure-mobile.net/', 'xxxxxxx', ['people']);

CDCService.addSource(CDCAzureMobileService);

var dataContext = {};

var onUpdateDataContext = function (data) {
    if (data && data.length) {
        syncPeople(data);
    }
}

var syncPeople = function (data) {
    sendToHost(JSON.stringify(data), "People[]");
}

CDCService.connect(function (results) {
    if (results === false) {
        console.log("CDCService must first be successfully initialized");
    } else {
        console.log("CDCService is good to go!");
    }
}, dataContext, onUpdateDataContext, 3);

这里没什么花哨的,我只是使用 CDC 连接到 Azure 移动服务以获取人员列表。

从 JavaScript 世界获取数据

接下来,我将从 JavaScript 上下文中获取数据。正如您在“sample.js”文件中可能看到的,当数据上下文更新时,我正在调用一个名为 sendToHost 的全局函数。此函数由 ChakraBridge 提供,允许您与 C# 主机通信。

要使其工作,您必须定义可以从 JavaScript 发送的类型

CommunicationManager.RegisterType(typeof(People[]));

所以现在当从 JavaScript 上下文调用 sendToHost 时,C# 端将引发一个特定事件

CommunicationManager.OnObjectReceived = (data) =>
{
    var peopleList = (People[])data;
    peopleCollection = new ObservableCollection<People>(peopleList);

    peopleCollection.CollectionChanged += PeopleCollection_CollectionChanged;

    GridView.ItemsSource = peopleCollection;
    WaitGrid.Visibility = Visibility.Collapsed;
};

显然,您负责 JavaScript 对象和 C# 类型之间的映射(相同的属性名称)

调用 JavaScript 函数

另一方面,您可能希望从 C# 代码中调用 JavaScript 上下文中的特定函数。例如,考虑提交事务或添加新对象。

所以首先让我们在“sample.js”文件中为特定任务创建一个函数

commitFunction = function () {
    CDCService.commit(function () {
        console.log('Commit successful');
    }, function (e) {
        console.log('Error during commit');
    });
}

要从 C# 调用此函数,您可以使用此代码

host.CallFunction("commitFunction");

如果您的函数接受参数,您也可以传递它们

host.CallFunction("deleteFunction", people.Id);

ChakraBridge 的当前版本接受 intdoubleboolstring 类型。

在 JavaScript 上下文中调试

得益于 Visual Studio,即使您现在在 C# 应用程序中,仍然可以调试您的 JavaScript 代码。为此,您首先必须在项目属性中启用脚本调试

然后,您可以在 JavaScript 代码中设置断点。

但有一个技巧需要知道:您无法在项目中的文件中设置此断点,因为它们在这里只是作为源。您必须在调试模式下运行时通过解决方案资源管理器的脚本文档部分来访问执行的代码

它是如何工作的?

互操作

现在让我们讨论一下其内部工作原理。

基本上,Chakra 基于一个 Win32 库,位于每个 Windows 10 桌面设备上的“C:\Windows\System32\Chakra.dll”。

所以这里的想法是提供一个内部 C# 类,它将通过 DllImport 属性嵌入到 DLL 的所有入口点

 internal static class Native
    {
        [DllImport("Chakra.dll")]
        internal static extern JavaScriptErrorCode JsCreateRuntime(JavaScriptRuntimeAttributes attributes, 
            JavaScriptThreadServiceCallback threadService, out JavaScriptRuntime runtime);

        [DllImport("Chakra.dll")]
        internal static extern JavaScriptErrorCode JsCollectGarbage(JavaScriptRuntime handle);

        [DllImport("Chakra.dll")]
        internal static extern JavaScriptErrorCode JsDisposeRuntime(JavaScriptRuntime handle);

可用函数的列表相当长。ChakraBridge 旨在封装这些函数并提供更高级别的抽象。

这里要考虑的另一个选择:您还可以使用 Rob Paveza 的优秀包装器 js-rt winrthttps://github.com/robpaveza/jsrt-winrt。它比纯 Chakra 引擎更高级,并且避免了对 P/Invoke 的需求。

提供缺失的部分

一个重要的理解点是 Chakra 只提供 JavaScript 引擎。但作为主机,您必须提供与 JavaScript 一起使用的工具。这些工具通常由浏览器提供(想想没有 .NET 的 C#)。

例如,XmlHttpRequest 对象或 setTimeout 函数不属于 JavaScript 语言。它们是 JavaScript 语言在浏览器上下文中使用的工具。

为了让您能够使用 JavaScript 框架,ChakraBridge 提供了一些此类工具。

这是一个持续进行的过程,将来我或社区将向 ChakraBridge 项目添加更多工具

现在让我们看一下 XmlHttpRequest 的实现

using System;
using System.Collections.Generic;
using System.Net.Http;

namespace ChakraBridge
{
    public delegate void XHREventHandler();

    public sealed class XMLHttpRequest
    {
        readonly Dictionary<string, string> headers = new Dictionary<string, string>();
        Uri uri;
        string httpMethod;
        private int _readyState;

        public int readyState
        {
            get { return _readyState; }
            private set
            {
                _readyState = value;

                try
                {
                    onreadystatechange?.Invoke();
                }
                catch
                {
                }
            }
        }

        public string response => responseText;

        public string responseText
        {
            get; private set;
        }

        public string responseType
        {
            get; private set;
        }

        public bool withCredentials { get; set; }

        public XHREventHandler onreadystatechange { get; set; }

        public void setRequestHeader(string key, string value)
        {
            headers[key] = value;
        }

        public string getResponseHeader(string key)
        {
            if (headers.ContainsKey(key))
            {
                return headers[key];
            }

            return null;
        }

        public void open(string method, string url)
        {
            httpMethod = method;
            uri = new Uri(url);

            readyState = 1;
        }

        public void send(string data)
        {
            SendAsync(data);
        }

        async void SendAsync(string data)
        {
            using (var httpClient = new HttpClient())
            {
                foreach (var header in headers)
                {
                    if (header.Key.StartsWith("Content"))
                    {
                        continue;
                    }
                    httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
                }

                readyState = 2;

                HttpResponseMessage responseMessage = null;

                switch (httpMethod)
                {
                    case "DELETE":
                        responseMessage = await httpClient.DeleteAsync(uri);
                        break;
                    case "PATCH":
                    case "POST":
                        responseMessage = await httpClient.PostAsync(uri, new StringContent(data));
                        break;
                    case "GET":
                        responseMessage = await httpClient.GetAsync(uri);
                        break;
                }

                if (responseMessage != null)
                {
                    using (responseMessage)
                    {
                        using (var content = responseMessage.Content)
                        {
                            responseType = "text";
                            responseText = await content.ReadAsStringAsync();
                            readyState = 4;
                        }
                    }
                }
            }
        }
    }
}

如您所见,XmlHttpRequest 类内部使用 HttpClient 并用它来模拟您在浏览器或 node.js 中可以找到的 XmlHttpRequest 对象。

然后将此类别(字面上)投射到 JavaScript 上下文

Native.JsProjectWinRTNamespace("ChakraBridge");

实际上,整个命名空间都被投射了,因为无法只投射一个类。所以然后执行一个 JavaScript,将 XmlHttpRequest 对象移动到全局对象

RunScript("XMLHttpRequest = ChakraBridge.XMLHttpRequest;");

处理垃圾回收

如果您决定扩展 ChakraBridge,您可能会面临的一个陷阱是垃圾回收。事实上,JavaScript 垃圾回收器对其自身上下文之外发生的事情一无所知。

所以例如,让我们看看 setTimeout 函数是如何开发的

internal static class SetTimeout
    {
        public static JavaScriptValue SetTimeoutJavaScriptNativeFunction(JavaScriptValue callee, bool isConstructCall, 
                                          [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] JavaScriptValue[] arguments, 
                                          ushort argumentCount, IntPtr callbackData)
        {
            // setTimeout signature is (callback, after)
            JavaScriptValue callbackValue = arguments[1];

            JavaScriptValue afterValue = arguments[2].ConvertToNumber();
            var after = Math.Max(afterValue.ToDouble(), 1);

            uint refCount;
            Native.JsAddRef(callbackValue, out refCount);
            Native.JsAddRef(callee, out refCount);

            ExecuteAsync((int)after, callbackValue, callee);

            return JavaScriptValue.True;
        }

        static async void ExecuteAsync(int delay, JavaScriptValue callbackValue, JavaScriptValue callee)
        {
            await Task.Delay(delay);
            callbackValue.CallFunction(callee);
            uint refCount;
            Native.JsRelease(callbackValue, out refCount);
            Native.JsRelease(callee, out refCount);
        }
    }

SetTimeoutJavaScriptNativeFunction 是将投射到 JavaScript 上下文内部的方法。您可以注意到每个参数都被收集为 JavaScriptValue,然后转换为期望的值。对于回调函数(callbackValue),我们必须告诉 JavaScript 垃圾回收器我们持有引用,因此即使 JavaScript 上下文中没有人持有它,它也无法释放此变量

Native.JsAddRef(callbackValue, out refCount);

一旦调用回调,必须释放引用

Native.JsRelease(callbackValue, out refCount);

另一方面,C# 垃圾回收器对 Chakra 黑盒内部发生的事情一无所知。因此,您必须注意保留您投射到 JavaScript 上下文中的对象或函数的引用。在 setTimeout 实现的特定情况下,您首先必须创建一个指向您的 C# 方法的静态字段,以保留对它的引用。

为什么不使用 Webview?

这是一个您可能会问的有效问题。仅使用 Chakra 具有一些很大的优势

  • 内存占用:无需嵌入 HTML 和 CSS 引擎,因为我们已经有了 XAML。
  • 性能:我们可以直接控制 JavaScript 上下文,例如,调用 JavaScript 函数,而无需像使用 webview 那样经历复杂的过程。
  • 简单性:webview 需要导航到一个页面才能执行 JavaScript。没有直接的方法来仅仅执行 JavaScript 代码。
  • 控制:通过提供我们自己的工具(如 XHRsetTimeout),我们可以高度精细地控制 JavaScript 可以做什么。

深入研究

得益于 Chakra 引擎,C#、XAML 和 JavaScript 之间的伟大合作才刚刚开始。根据社区的反应,我计划在 ChakraBridge 项目中添加更多功能,以便能够处理更多的 JavaScript 框架(例如,如果能增加对画布绘图的支持,以便能够使用所有可用于 JavaScript 的出色图表框架,那就太棒了)。

如果您有兴趣阅读更多关于 Chakra 本身的内容,可以访问官方的 Chakra 示例仓库:https://github.com/Microsoft/Chakra-Samples

您可能还会发现以下链接很有趣

  • 在 Windows 10 上使用 Chakra 编写脚本应用程序
  • 文档

更多 Web 开发实践

本文是 Microsoft 技术推广人员和工程师关于实用 JavaScript 学习、开源项目和互操作性最佳实践的 Web 开发系列文章的一部分,其中包括 Microsoft Edge 浏览器和新的 EdgeHTML 渲染引擎

我们鼓励您在包括 Windows 10 默认浏览器 Microsoft Edge 在内的浏览器和设备上进行测试,使用 dev.microsoftedge.com 上的免费工具

我们工程师和布道者的更深入的学习

我们的社区开源项目

  • vorlon.JS(跨设备远程 JavaScript 测试)
  • manifoldJS(部署跨平台托管 Web 应用程序)
  • babylonJS(轻松实现 3D 图形)

更多免费工具和后端 Web 开发内容

© . All rights reserved.