在 .NET 应用程序中运行 JavaScript

前几天我在做一个副业,意识到我需要使用一些 JavaScript 功能。一想到要再次处理 Node.js 和 npm,我就完全放弃了,所以我决定研究一下在 .NET 应用程序中运行 JavaScript 的可能性。很疯狂吧?实际上,这出乎意料的简单。.

1你为什么要这样做?

尽管我很喜欢 .NET 生态系统,但有些事情,JavaScript 生态系统做得更好。其中之一就是任何事情都能找到一个库,特别是涉及到网络时。

以语法高亮为例。这可以直接用 C# 来做,但这不是一个特别流畅的体验。例如,TextMateSharp 项目为 TextMate 语法提供了一个解释器。这些文件是 VS Code 用来为一种语言添加基本语法高亮的。然而,如果你想部署应用程序,它包装了一个本地依赖,这就增加了一些复杂性。

相比之下,JavaScript 有大量成熟的语法高亮库。仅举几例,有 highlight.jsPrism.js(在本博客中使用)和 shiki.js。尤其是前两个,非常成熟,有多个插件和主题,而且有简单的 API。

作为一个 .NET 开发者,JavaScript 的明显问题是,你需要学习并选择进入一个完整的独立工具链,与 Node.js 和 NPM 一起工作。这似乎是一个很大的开销,只是为了使用一个小功能。

因此,我们陷入了一个困境。我们要么走 C#(+ Native)路线,要么就得转用 JavaScript。

或者......我们直接从我们的 .NET 应用程序中调用 JavaScript 

2在 .NET 中运行 JavaScript

一旦你决定在你的 .NET 代码中运行 JavaScript,你就会考虑几个选择。你可以借用 JavaScript 引擎,让它为你运行你的 JavaScript,但你并没有真正解决问题,你仍然需要安装 Node.js。

另一个选择是在你的库中直接捆绑 JavaScript 引擎。这并不像听起来那么疯狂,有几个 NuGet 包采用了这种方法,然后暴露出一个 C# 层来与引擎进行交互。

下面是你可以使用的一些包的列表。

Jering.Javascript.NodeJS

这个库采取了上述的第一种方法。它不包括包中的 Node.js。相反,它为执行 JavaScript 代码提供了一个 C# API,并调用了安装在你机器上的 Node.js。这在你知道两者都已安装的环境中可能很有用,但它并没有真正解决我想避免的问题。

ChakraCore

ChakraCore 是 Edge 转为基于 Chromium 引擎之前最初使用的 JavaScript 引擎。根据 GitHub 项目的介绍:

ChakraCore 是一个带有 C 语言 API 的 JavaScript 引擎,你可以用它来为任何 C 语言或 C 语言兼容项目添加对 JavaScript 的支持。它可以在 Linux macOS 和 Windows 上针对 x64 处理器进行编译。而 x86 和 ARM 只适用于 Windows。

因此,ChakraCore 包括一个本地依赖,但由于 C# 可以 P/Invoke 到本地库,这本身并不是一个问题。但它会带来一些部署方面的挑战。

ClearScript (V8)

Node.JS、Chromium、Chrome 和最新的 Edge 使用的都是 V8 JavaScript 引擎。Microsoft.ClearScript 包为该库提供了一个封装,为调用 V8 库提供了一个 C# 接口。就像 ChakraCore 一样,V8 引擎本身是一个本地依赖。ClearScript 库负责 P/Invoke 调用,提供了一个很好的 C# API,但你仍然要确保你在目标平台上部署了正确的本地库。

Jint

Jint 很有意思,因为它是一个完全在 .NET 中运行的 JavaScript 解释器,没有任何本地的依赖!它完全支持 ECMAScript 5.1 (ES5),并支持 .NET Standard 2.0,所以你可以在你的所有项目中使用它!

Jurassic

Jurassic 是另一个 JavaScript 引擎的 .NET 实现,类似于 Jint。也和 Jint 类似,它支持所有的 ES5,而且似乎也部分支持 ES6。与 Jint 不同的是,Jurassic 不是一个解释器,它将 JavaScript 编译成 IL,这使得它的速度非常快,而且它没有本地的依赖性。

那么,在所有这些选择中,你应该选择哪一个?

3JavaScriptEngineSwitcher:当一个 JS 引擎不够用的时候

还有一个伟大的项目可以让你简单地尝试上面其中的任何一个库。虽然所有的库都允许你运行 JavaScript,但它们都有略微不同的 C# API 来与之交互。这可能会使比较它们变得有点痛苦,因为你必须为每个库学习不同的 API。

JavaScriptEngineSwitcher 这个库为我提到的所有库和更多的库提供了封装:

  • Jering.Javascript.NodeJS

  • ChakraCore

  • Microsoft ClearScript.V8

  • Jint

  • Jurassic

  • MSIE JavaScript Engine for .NET

  • NiL.JS

  • VroomJs

每个库都在一个单独的包中(有本地依赖关系的引擎需要一个额外的本地包),还有一个 Core 包,它提供通用的 API。即使你不打算切换 JS 引擎,我也倾向于尽可能地使用 JavaScriptEngineSwitcher 封装库,这样你就不必在以后需要切换引擎时弄清楚一个新的 API 了。

在 .NET 项目中改变使用的 JavaScript 引擎在我看来是完全可能的。例如,我开始使用 Jint,但当我需要执行更大的脚本时,我遇到了性能问题,于是换成了 JurassicJavaScriptEngineSwitcher 让这一切变得很简单,只需在我的项目中添加一个新的包并改变一些初始化代码即可。

我最近才发现 JavaScriptEngineSwitcher 这个库,但最新版本的下载量已接近一百万,它被用于 .NET 静态网站建设者 Statiq 中。在这篇文章的最后部分,我将举一个最基本用法的例子。

4案例:用 JavaScriptEngineSwitcher 在控制台应用中运行 prism.js

在这篇文章的开头,我讨论了一个特定的场景--代码块的语法高亮。在本节中,我将展示如何使用 prism.js 高亮一小段代码,并在一个控制台应用程序中运行。

开始之前请添加 JavaScriptEngineSwitcher.Jurassic NuGet 包的引用。

dotnet add package JavaScriptEngineSwitcher.Jurassic

接下来,下载你想运行的 JavaScript 文件。例如,我从 Prism.js 的官网下载了 prism.js 文件,并将 C# 添加到默认支持高亮的语言集。在把文件放到项目文件夹的根目录后,我把文件更新为嵌入资源。你可以在你的 IDE 中操作,也可以手动编辑项目文件:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="JavaScriptEngineSwitcher.Jurassic" Version="3.17.4" />
  </ItemGroup>

  <!-- Make prism.js an embedded resource -->
  <ItemGroup>
    <None Remove="prism.js" />
    <EmbeddedResource Include="prism.js" />
  </ItemGroup>

</Project>

剩下的就是编写代码,在我们的程序中运行脚本。下面的代码段设置了 JavaScript 引擎,从程序集中加载嵌入的 prism.js 库,并执行它。

using JavaScriptEngineSwitcher.Jurassic;

// Create an instance of the JavaScript engine
IJsEngine engine = new JurassicJsEngine();

// Execute the embedded resource called JsInDotnet.prism.js from the provided assembly
engine.ExecuteResource("JsInDotnet.prism.js", typeof(Program).Assembly);

现在我们可以在同一个上下文中运行我们自己的 JavaScript 命令。我们可以通过使用 SetVariableNameExecute 和 Evaluate 从 C# 向 JavaScript 引擎传递数值:

// This is the code we want to highlight
string code = @"
using System;

public class Test : ITest
{
    public int ID { get; set; }
    public string Name { get; set; }
}";

// set the JavaScript variable called "input" to the value of the c# variable "code"
engine.SetVariableValue("input", code);

// set the JavaScript variable called "lang" to the string "csharp"
engine.SetVariableValue("lang", "csharp");

// run the Prism.highlight() function, and set the result to the "highlighed" variable
engine.Execute($"highlighted = Prism.highlight(input, Prism.languages.csharp, lang)");

// "extract the value of "highlighted" from JavaScript to C#
string result = engine.Evaluate<string>("highlighted");

Console.WriteLine(result);

当你把它们放在一起运行时,高亮的代码会被打印到控制台:

<span class="token keyword">using</span> <span class="token namespace">System</span><span class="token punctuation">;</span>

<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Test</span> <span class="token punctuation">:</span> <span class="token type-list"><span class="token class-name">ITest</span></span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">int</span></span> ID <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">string</span></span> Name <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

渲染后,看起来像这样:

在 .NET 应用程序中运行 JavaScript

我对整个过程的简单程度感到惊讶。启动一个 JavaScript 引擎,加载 prism.js 文件,并执行我们的自定义代码是如此顺利。这是我面临问题的完美解决方案。

我显然不建议所有的应用程序都这样做。如果你需要运行大量的 JavaScript,那么直接使用 Node.js 生态系统及工具可能更容易。但如果你只是想利用一个小型的、独立的工具(如 prims.js),那么这是一个不错的选择。

5总结

在这篇文章中,我展示了如何使用 JavaScriptEngineSwitcher NuGet 包来在 .NET 应用程序中运行 JavaScript。这个包为许多不同的 JavaScript 引擎提供了一个一致的接口。其中一些引擎(如 Chakra Core 和 V8)需依赖一个本地组件,而其他引擎(如 Jint 和 Jurassic)只使用托管代码。最后,我展示了你如何使用 JavaScriptEngineSwitcher 在 .NET 应用程序内部运行 Prims.js 代码高亮库。