ClearScript 略述
几年前,我对在 Visual Basic 应用程序中托管 Active Server Page 的整个 VBScript 引擎的前景很感兴趣。我为一个希望以 CD 形式销售编辑内容的公司创建了令人惊叹的概念验证,即仅在本地或远程 Web 服务器外部重用现有的 Active Server Page 内容。
那是上世纪 90 年代末,还没有 Microsoft .NET Framework,也没有 HTML5。只有少数几个人在积极探索动态 HTML,但托管脚本引擎是再容易不过的了。我必须引用 ActiveX 控件,在脚本环境中发布 ActiveX 对象,我已经做好了准备。
最近,一位客户问我从 SQL Server 查询中提取文本文件最有效的方法。这个问题超出了我负责的主题范围,因此我试图用„抱歉,我不知道”这样的话来回答。但是,我知道这位客户在数据库操作方面非常在行。我怀疑这个问题的背后有更多的背景信息。
这位客户定期从存储在 SQL Server 实例内的数据库表中的内容生成纯文本文件(主要是 CSV 和 XML 文件)。这让数据库工作人员感到厌恶,因为这些请求大多来自业务问题总是很紧急的企业。没有重复的逻辑可以帮助创建可重复的例程 — 至少在 SQL Server 环境中是这样的。
因此,这位客户想要寻找一个工具,以便业务人员可以使用简单的脚本语言(如 VBScript)来进行编程。用户需要对数据库的受控访问,确保其为只读权限。不用说,该工具必须为用户提供轻松创建文本文件的机会。这让我想起了使用 ActiveX 和 VBScript 的快乐日子。在我听说有一个相对较新的名为 ClearScript 的库 (clearscript.codeplex.com) 时,我几乎感到一丝遗憾。
将 ClearScript 集成到 Windows Presentation Foundation
ClearScript 使您可以将脚本功能添加到 .NET 应用程序(只要它使用 .NET Framework 4 或更高版本)。ClearScript 支持 VBScript、JavaScript 和 V8。V8 是由 Google 创建的开源 JavaScript 引擎,并且与 Chrome 集成。V8 是高性能的 JavaScript 引擎,且非常适用于多线程和异步操作方案。
将 ClearScript 添加到 .NET 应用程序的实际效果是,您可以将 JavaScript 或 VBScript 表达式传递到引擎,它们将得到处理并运行。有趣的是,您并不会被限制为使用纯脚本对象,如数组、JSON 对象和基元类型。您可以集成外部 JavaScript 库和脚本托管的 .NET 对象。
在您将 ClearScript 集成到应用程序后,剩下的就是让库了解它能编写脚本的对象。这意味着您在 ClearScript 的上下文中发布自己的对象,允许授权用户加载并运行现有脚本或编写新脚本。
如果您想要添加自定义层,以便用户添加自己的逻辑片段,而不产生更改请求成本,那么 ClearScript 是必要的,但可能还不够。ClearScript 只是解决方案的一部分。您可能希望为用户提供一种可以管理他自己的脚本的方法。此外,您应创建一些可简化常规任务(如创建文件)的临时对象。
为了帮助客户从由 Web API 后端公开的大量服务生成文本和 XML 报告,我做了以下工作。主要的功能要求是让用户创建文本文件。为了进行概念验证,我需要一个外壳应用程序来托管 ClearScript。我选择了带有文本框的 Windows Presentation Foundation (WPF) 应用程序来手动输入脚本代码。连续迭代增加了对默认输入文件夹和用于打开/导入现有脚本文件的 UI 的支持。图 1 显示了正在运行的 WPF 示例应用程序。
图 1 托管 ClearScript 引擎的示例 Windows Presentation Foundation 应用程序
此外,ClearScript 是可以通过链接程序集直接在项目中引用的开源项目。您还可以通过第三方 NuGet 程序包进行操作,如图 2 中所示。
图 2 通过 NuGet 安装 ClearScript
初始化 ClearScript
在以编程的方式使用脚本引擎之前,您还必须完成一些工作。但是在结束之时,一旦完全进行了设置,图 3 中的代码就是触发脚本代码执行所需的代码。
图 3 触发脚本代码的代码
public void Confirm()
{
try
{
SonoraConsole.ScriptEngine.Execute(Command);
OutputText = SonoraConsole.Output.ToString();
}
catch(Exception e)
{
OutputText = e.Message;
}
}
此确认方法属于支持示例应用程序主要视图的表示器类。通过单击“运行”按钮触发此方法(在图 1 中可见)。您所看到的列表中引用的 SonoraConsole 类只是我自己围绕 ClearScript 库核心类的包装。
在应用程序启动并绑定到 XAML 应用程序类的 Startup 事件时,ClearScript 引擎会进行初始化:
public partial class App : Application
{
void Application_Startup(Object sender, StartupEventArgs e)
{
SonoraConsole.Initialize();
}
}
初始化的复杂程度取决于您的喜好,但是它必须至少初始化所选语言的脚本引擎。您必须使脚本引擎的实例可用于应用程序的其他部分。下面是一种可行的方法:
public class SonoraConsole
{
public static void Initialize()
{
ScriptEngine = new VBScriptEngine()
}
public static ScriptEngine ScriptEngine { get; private set; }
...
}
您可能想要从配置文件中进行读取,因此应在应用程序中启用该配置文件的脚本语言。这是一种可行的配置架构:
<appSettings>
<add key="language" value="vb" />
</appSettings>
在所选脚本引擎实例就绪之后,您可以运行任何有效的 JavaScript(或 VBScript)代码。您必须了解这些基本功能,否则您在实际应用中所能执行的操作非常有限。
添加可编写脚本的对象
所有 ClearScript 引擎均提供一个可编程接口,您可以通过该接口将可编写脚本的对象添加到运行时环境。尤其是,您使用 AddHostObject 方法,如下所示:
ScriptEngine.AddHostObject("out", new SonoraOutput(settings));
ScriptEngine.AddHostObject("xml", new XmlFacade());
此方法需要两个参数。第一个参数是脚本编者用于引用发布对象的公共名称。第二个参数就是对象实例。在任何 JavaScript 或 VBScript 中查看以前的代码段,您可以使用名称“out”来调用 SonoraOutput 接口上的任何可用的公共方法。下面是 JavaScript 中的一个示例,引用了图 1 中所示的内容:
var x = 4;
out.print(x + 1);
众所周知,在 JavaScript 中根据 camelCase 约定命名对象是一种常见做法。在 .NET 编程中,PascalCase 约定更为常见,也是推荐的约定。在 SonoraOutput 类的实现过程中,我故意选择遵循 JavaScript 约定,调用了 print 方法而不是 Print,因为在纯 C# 编程中会出现这种情况。
根据我的经验,您无需了解更多内容即可开始在 ClearScript 上执行操作。大多数情况下,出于提供应用程序特定对象这一主要目的,您可以在宿主应用程序内配置 ClearScript 环境。通常情况下,有定制对象包装现有业务对象,并且在脚本环境中更易于使用。
ClearScript 环境的主要用户通常不是全职开发人员。他们最有可能是这样一些人:具有一些软开发技能,觉得处理 .NET 类的详细信息无需如此复杂和烦人。ClearScript 使您将相当大一部分 .NET Framework 直接提供给 JavaScript 和 VBScript。我选择了设计得极其简单的定制对象。以下是如何在 ClearScript 中发布类型而非对象的方法:
ScriptEngine.AddHostType("dt", typeof(DateTime));
在引用类型时,您授予用户以编程方式创建此类实例的权限。例如,上一行代码将 .NET DateTime 对象的权限添加到脚本环境。现在,以下 JavaScript 代码成为可能:
var date = new dt(1998, 5, 20);
date = date.AddDays(1000);
out.print(date)
在 JavaScript 代码中,您充分利用了此方法的全部功能,如 AddDays 和 AddHours。如果想要了解两个日期之间的区别,该怎么操作?您可以执行如下操作:
var date1 = new dt(1998, 5, 20);
var date2 = date1.AddDays(1000);
var span = date2.Subtract(date1);
out.print(span.Days)
正确地处理了 TimeSpan 对象,表达式 span.Days 仅返回 1000。这是由于 JavaScript 语言的动态特性造成的,它会以动态方式确定名为“span”的对象提供名为 Days 的成员。相反,如果您想创建 TimeSpan 实例,则首先需要让引擎知道它的存在。
要避免提供数量众多的不同类型,ClearScript 允许您托管整个程序集。这是一种可行的方法:
ScriptEngine.AddHostObject("dotnet",
new HostTypeCollection("mscorlib", "System.Core"));
现在关键字 dotnet 成为访问 mscorlib 和 System.Core 中任何类型和静态成员的密钥。创建新的日期对象需要更长时间,但创建好之后,您就可以明确地使用 TimeSpan 对象:
var date1 = new dotnet.System.DateTime(1998, 5, 20);
var ts1 = new dotnet.System.TimeSpan(24, 0, 0);
var ts2 = ts1.Add(new dotnet.System.TimeSpan(24, 0, 0));
out.print(ts2.Days);
JavaScript 代码段输出数字 2,这是两个不同的 TimeSpan 对象(每个计数 24 小时)之和。ClearScript 无法正常运行的一种情况就是运算符重载,而这根本不存在。这意味着要累加日期或时间戳,您必须使用 Add 或 Subtract 之类的方法。它还支持反射。
生成输出
您在图 1 中看到的工具必须能够向用户显示一些结果。默认情况下,添加到 ClearScript 引擎的 SonoraOutput 对象只保留一个内部 StringWriter 对象。由 print 方法处理的所有文本实际上是写入基础编写器。编写器的内容通过 SonoraConsole 类向外界公开。该类是 ClearScript 引擎和宿主应用程序之间的唯一联系点。宿主应用程序表示器仅通过属性返回字符串编写器的内容。然后该属性将绑定到 WPF UI 中的 TextBlock。方法 print 通过字符串编写器写入 UI。方法 clr 清除缓冲区和 UI。
另存为文本文件
我的客户只需创建文本文件,大多为 CSV 文件。这相对比较容易实现。我所做的只是创建文件方法,直接向它传递一些文本内容。我还可以让它抓取任何已经打印到屏幕上的内容,并保存在内部缓冲区。对于文件而言,最成问题的方面就是命名和位置。要大幅提升脚本的速度,创建和检索文件必须非常容易。我设法创建了两个默认文件夹,一个用于输入,一个用于输出。我还假定所有文件都是 TXT 格式。如果没有指定文件名,则文件假设一个默认名称。
对于某些情况,这些假设可能过于严格,但是我的项目只是生成文件而非存储文件的工具的概念验证。如图 4 所示,我可以轻松地将 XmlWriter 对象包装成一个美观的组件,并通过脚本创建 XML 文件。
图 4 通过脚本创建 XML 文件
总结
通过脚本创建 XML 文件的目的是什么?这实际上就相当于拥有一些企业级应用程序中的脚本功能。您需要脚本,因为您希望实现任务的自动化。在某些情况下,您需要做的只是创建临时文本或 XML 文件。或许您可以在 SQL Server 上运行查询,并将其导入 CSV,但这需要生产数据库的管理访问权限,更重要的是,需要相应的技能。我会让自己很麻烦地使用 xp_cmdshell 从 SQL Server 查询中抓取文本文件。但对于开发人员而言,为脚本设置一些现成的临时、易用对象并不困难。
我的客户非常喜欢这个想法,就像我喜欢使用 ClearScript 一样。他让我将更多的对象添加到动态环境。最终,我添加了一个控制反转层来配置在启动时加载的对象。他还想在整个公司进行 ClickOnce 部署,以便在新工具发布时使用。
Dino Esposito 是《Microsoft .NET:Architecting Mobile Applications Solutions for the Enterprise》(Microsoft Press,2014 年)和即将出版的《Programming ASP.NET MVC 5》(Microsoft Press,2014 年)的合著者。作为 JetBrains 的 .NET Framework 和 Android 平台的技术推广人员,Esposito 经常在全球行业活动中发表演讲,并在 software2cents.wordpress.com 上以及 twitter.com/despos 上的推文中分享他对于软件的愿景。
衷心感谢以下技术专家对本文的审阅:Microsoft ClearScript 团队