Csharp中调用JavaScript之Jint简析

C# 中的 Jint:在 .NET 世界中嵌入 JavaScript 的利器

1. 什么是 Jint?

Jint 是一个用于 .NET 的 JavaScript 解释器(Interpreter)。它完全使用 C# 编写,无任何外部依赖,并且遵循 ECMAScript 5.1 规范(同时也在不断增加对 ES6 及更高版本的支持)。

简单来说,Jint 就像一座桥梁,它允许你在 C#/.NET 应用程序中 直接执行 JavaScript 代码 ,并实现 C# 与 JavaScript 之间的数据和功能互通。

关键特性:

  • 纯 .NET 实现 :不需要安装 Node.js 或任何其他 JavaScript 运行时。
  • 无浏览器环境 :Jint 不包含浏览器中的 windowdocument 等 DOM API。它是一个纯粹的 JavaScript 语言执行引擎。
  • 高性能 :相对于其他 .NET 的 JS 解释器,Jint 性能非常出色。
  • 安全沙箱 :可以配置引擎以限制脚本的权限,例如禁止访问本地文件系统或任意 .NET 程序集,这对于执行不受信任的用户脚本至关重要。

2. 为什么以及何时使用 Jint?(核心用途)

你可能会问:我已经在用强大的 C# 了,为什么还需要 JavaScript?

Jint 在以下场景中能发挥巨大作用:

  • 动态规则引擎 :允许业务人员或用户通过编写简单的 JS 脚本来定义业务规则(如折扣计算、审批流程、数据验证),而无需修改和重新编译 C# 应用程序。这是 Jint 最常见的用途。
  • 插件化/扩展系统 :让你的应用程序支持用 JavaScript 编写的插件。第三方开发者可以用一种比 C# 更简单的语言来扩展你的应用功能。
  • 动态配置 :对于复杂的配置场景,JSON 或 XML 可能不够灵活。使用 JS 文件作为配置文件,可以在其中包含逻辑判断,动态生成最终配置。
  • 模板渲染 :执行一些轻量级的 JavaScript 模板引擎(如 Mustache.js 的某个无 DOM 依赖版本)来处理字符串和数据。
  • 遗留系统或跨平台逻辑复用 :如果你的团队有一套用 JavaScript 编写的核心业务逻辑,Jint 可以让你在 .NET 后端直接复用它,而无需用 C# 重写。

3. 快速入门:安装与基本使用

3.1. 安装 Jint

通过 NuGet 包管理器控制台安装:

Install-Package Jint

或者使用 .NET CLI:

dotnet add package Jint

3.2. 第一个 Jint 程序

Jint 的使用非常直观。核心类是 Engine

using Jint;
using Jint.Native;

public class Program
{
    public static void Main()
    {
        // 1. 创建 Jint 引擎实例
        var engine = new Engine();

        // 2. 执行 JavaScript 代码
        // Execute() 方法用于执行没有返回值的脚本
        engine.Execute(@"
            function hello(name) {
                console.log('Hello, ' + name + '!');
            }

            hello('Jint World');
        ");

        // 3. 执行并获取返回值
        // Evaluate() 方法用于执行有返回值的表达式
        var result = engine.Evaluate("1 + 1");
        Console.WriteLine($"1 + 1 = {result}"); // 输出: 1 + 1 = 2

        // 4. 获取一个在 JS 中定义的变量
        engine.Execute("var myVar = 'This is a test';");
        JsValue myVar = engine.GetValue("myVar");
        Console.WriteLine(myVar.AsString()); // 输出: This is a test
    }
}

注意console.log 在 Jint 中默认不会输出到 C# 的控制台。你需要手动将 C# 的 Console.WriteLine 暴露给 JS 环境(参见 4.1.3 节)。

4. 核心功能与高级示例

4.1. C# 与 JavaScript 的双向交互

这是 Jint 最强大的功能。

4.1.1. C# 向 JavaScript 传递变量

使用 SetValue(name, value) 方法可以将 C# 变量注入到 JS 的全局作用域中。

var engine = new Engine();

// 传递基本类型
engine.SetValue("magicNumber", 42);
engine.SetValue("message", "Hello from C#");

// 传递复杂类型(数组、字典等)
var numbers = new[] { 1, 2, 3, 4, 5 };
engine.SetValue("csharpArray", numbers);

string script = @"
    var sum = 0;
    for (var i = 0; i < csharpArray.length; i++) {
        sum += csharpArray[i];
    }
    sum += magicNumber;
    message + ' The sum is ' + sum; // 这是脚本的返回值
";

var result = engine.Evaluate(script).AsString();

Console.WriteLine(result); // 输出: Hello from C# The sum is 57

4.1.2. C# 获取 JavaScript 中的值

执行脚本后,可以使用 GetValue(name) 从 JS 全局作用域中取回变量。返回值是 JsValue 类型,你可以轻松地将其转换为对应的 C# 类型。

var engine = new Engine();

engine.Execute(@"
    var user = {
        name: 'John Doe',
        age: 30,
        isActive: true,
        roles: ['Admin', 'User']
    };
    var calculatedValue = 123.45;
");

// 获取数字
double val = engine.GetValue("calculatedValue").AsNumber();
Console.WriteLine($"Calculated Value: {val}"); // 123.45

// 获取对象并转换为 C# 对象
var userObj = engine.GetValue("user").AsObject();
string name = userObj.Get("name").AsString();
int age = (int)userObj.Get("age").AsNumber();
string[] roles = userObj.Get("roles").AsArray().Select(x => x.AsString()).ToArray();

Console.WriteLine($"User: {name}, Age: {age}, Roles: {string.Join(", ", roles)}");
// 输出: User: John Doe, Age: 30, Roles: Admin, User

4.1.3. 在 JavaScript 中调用 C# 方法

你可以将 C# 的 ActionFunc 或任何委托传递给 JS。

var engine = new Engine()
    // 将 Console.WriteLine 委托暴露为 JS 中的 log 函数
    .SetValue("log", new Action<object>(Console.WriteLine))
    // 将一个返回字符串的 Func 暴露为 JS 中的 getUserName 函数
    .SetValue("getUserName", new Func<string>(() => "C# User"));

engine.Execute(@"
    log('This message is printed by C# Console.WriteLine!');
    var name = getUserName();
    log('User from C#: ' + name);
");

4.2. 操作 CLR 对象

Jint 可以无缝地处理普通的 C# 对象(POCO)。

4.2.1. 传递和操作 C# 对象实例

Jint 会自动将 C# 对象的公共属性和方法暴露给 JavaScript 环境。

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public string Greet()
    {
        return $"Hello, my name is {Name} and I'm {Age} years old.";
    }
}

var person = new Person { Name = "Alice", Age = 25 };

var engine = new Engine()
    .SetValue("log", new Action<object>(Console.WriteLine))
    .SetValue("person", person); // 将整个 person 对象实例传入

engine.Execute(@"
    log('Initial state: ' + person.Greet());

    // 在 JS 中修改 C# 对象的属性
    person.Name = 'Bob';
    person.Age = person.Age + 10;

    // 在 JS 中调用 C# 对象的方法
    var greeting = person.Greet();
    log('Modified state: ' + greeting);
");

// 检查 C# 对象是否真的被修改了
Console.WriteLine($"Final state in C#: {person.Name} is now {person.Age}");
// 输出: Final state in C#: Bob is now 35

4.2.2. 在 JavaScript 中创建 C# 对象

通过 AllowClr() 选项,你可以授权 JS 代码访问 .NET 类型并创建实例。这是一个强大的功能,但有安全风险,请谨慎使用。

var engine = new Engine(options => {
    // 允许 JS 访问指定的命名空间下的类型
    options.AllowClr(typeof(Person).Assembly);
});

// 将 Person 类型本身暴露给 JS
engine.SetValue("Person", typeof(Person));

var newPersonJson = engine.Evaluate(@"
    // 在 JS 中创建 Person 类的实例
    var jsPerson = new Person();
    jsPerson.Name = 'Charlie';
    jsPerson.Age = 40;
    
    log(jsPerson.Greet());

    // 将 JS 对象序列化为 JSON 字符串返回
    JSON.stringify(jsPerson);
").AsString();

Console.WriteLine($"JSON from JS: {newPersonJson}");
// 输出: JSON from JS: {"Name":"Charlie","Age":40}

4.3. 安全性与资源限制

当执行外部(尤其是用户提供)的脚本时,安全性至关重要。Jint 提供了丰富的选项来创建一个安全的“沙箱”环境。

var untrustedScript = "while(true) {}"; // 恶意死循环脚本

var engine = new Engine(options =>
{
    // 1. 限制脚本执行时间,防止死循环
    options.TimeoutInterval(TimeSpan.FromSeconds(2));

    // 2. 限制递归深度
    options.MaxRecursionDepth(10);
    
    // 3. 限制内存使用
    options.MemoryLimit(4_000_000); // 4 MB

    // 4. (重要) 禁止访问 CLR,这是最安全的默认设置。
    // options.DenyClr(); // Jint 默认就是禁止的
});

try
{
    engine.Execute(untrustedScript);
}
catch (Jint.Runtime.JavaScriptException ex) when (ex.InnerException is System.TimeoutException)
{
    Console.WriteLine("Script execution timed out!");
}
catch (Jint.Runtime.JavaScriptException ex) when (ex.InnerException is Jint.Runtime.MemoryLimitExceededException)
{
    Console.WriteLine("Script memory limit exceeded!");
}

4.3.3. 限制 CLR 访问

AllowClr() 方法可以非常精细地控制 JS 对 .NET 程序集的访问权限,以降低安全风险。

// 只允许访问 System.Linq.Enumerable 类
options.AllowClr(typeof(System.Linq.Enumerable).Assembly);

// 或者只允许访问某个特定的命名空间
// options.AllowClr("My.Safe.Namespace");

5. 综合实战案例:一个简单的动态规则引擎

让我们把所有知识点结合起来,创建一个用于电商订单折扣计算的规则引擎。

5.1. 场景描述

我们希望根据订单的总价、商品数量、用户等级等信息,动态计算折扣金额。这些折扣规则应该由运营人员以 JavaScript 脚本的形式提供,而不是硬编码在 C# 中。

5.2. C# 代码实现

// 数据模型
public class Order
{
    public double TotalPrice { get; set; }
    public int ItemCount { get; set; }
    public string UserLevel { get; set; } // e.g., "Silver", "Gold"
}

// 规则引擎
public class DiscountRuleEngine
{
    private readonly Engine _engine;
    private readonly string _ruleScript;

    public DiscountRuleEngine(string ruleScript)
    {
        _ruleScript = ruleScript;
        _engine = new Engine(options => {
            // 安全起见,禁止CLR访问
            options.DenyClr(); 
        })
        .SetValue("log", new Action<object>(Console.WriteLine));
    }

    public double CalculateDiscount(Order order)
    {
        _engine.SetValue("order", order);
        
        // 执行脚本,并期望它返回一个名为 'discount' 的变量
        _engine.Execute(_ruleScript);
        
        var discount = _engine.GetValue("discount").AsNumber();
        return discount;
    }
}

5.3. JavaScript 规则脚本

运营人员可以编写如下的 JS 脚本。

规则 1: Gold 用户满 200 打 8 折:

// rule1.js
var discount = 0;
if (order.UserLevel === 'Gold' && order.TotalPrice >= 200) {
    discount = order.TotalPrice * 0.2;
    log('Applied Gold User 20% discount.');
}

规则 2: 任何用户购买超过 5 件商品,总价减 50:

// rule2.js
var discount = 0;
if (order.ItemCount > 5) {
    discount = 50;
    log('Applied "More than 5 items" discount.');
} else if (order.TotalPrice > 1000) {
    discount = 100;
    log('Applied "Total price over 1000" discount.');
}

5.4. 运行与结果

public static void Main()
{
    // 场景一:Gold 用户,高总价
    var order1 = new Order { TotalPrice = 250, ItemCount = 3, UserLevel = "Gold" };
    var rule1Script = File.ReadAllText("rule1.js");
    var engine1 = new DiscountRuleEngine(rule1Script);
    double discount1 = engine1.CalculateDiscount(order1);
    Console.WriteLine($"Order 1 Discount: {discount1:C}\n"); // $50.00

    // 场景二:普通用户,多件商品
    var order2 = new Order { TotalPrice = 180, ItemCount = 6, UserLevel = "Silver" };
    var rule2Script = File.ReadAllText("rule2.js");
    var engine2 = new DiscountRuleEngine(rule2Script);
    double discount2 = engine2.CalculateDiscount(order2);
    Console.WriteLine($"Order 2 Discount: {discount2:C}\n"); // $50.00
}

这个例子完美地展示了 Jint 的核心价值:将易变的业务逻辑从核心应用程序中分离出来 ,提高了灵活性和可维护性。

6. Jint 的优缺点总结

优点

  • 灵活性极高 :为静态类型的 C# 语言带来了动态脚本的能力。
  • 易于集成 :纯 C# 实现,通过 NuGet 即可引入,没有额外依赖。
  • 强大的互操作性 :C# 和 JS 之间的数据和方法调用非常方便。
  • 安全可控 :提供了细粒度的安全配置,可创建安全的脚本执行沙箱。
  • 标准兼容性好 :遵循 ES5.1 标准,大部分现代 JS 语法(非 DOM 相关)都能良好运行。

缺点

  • 性能瓶颈 :作为解释器,其执行速度远慢于 V8 引擎(如 Node.js)或编译后的 C# 代码。不适合执行计算密集型或性能要求极高的任务。
  • 无 DOM/BOM :无法执行任何依赖浏览器环境的 JS 库(如 jQuery, React, Vue)。
  • 调试困难 :在 C# 中调试 JS 脚本比在浏览器开发者工具中要困难得多。通常只能依赖 log 输出。

7. 结论

Jint 是 .NET生态中一个非常成熟和强大的工具库。它并非要取代 C#,而是作为 C# 的一个补充,专门用于处理那些需要 高度灵活性和动态性 的场景。当你需要构建一个规则引擎、插件系统或任何希望将部分逻辑“外置化”的应用时,Jint 绝对是一个值得优先考虑的优秀选择。

1 Like