在当今的软件开发领域,经常追求代码的简洁性和高效性。ASP.NET作为微软推出的一个强大的Web开发框架,其最新版本已经能够支持创建非常轻量级的Web服务。这主要得益于.NET框架的迭代更新,使得许多之前版本中常见的模板代码(boilerplate code)可以被更简洁、易于理解的配置所替代。这种配置可以舒适地放置在单个文件中,大大减少了代码量,同时不影响代码的可读性。
对于实时更新的需求,SignalR已经很好地隐藏了实现Web上实时双向通信的复杂性。DotNetify在此基础上引入了服务器和客户端之间状态管理的抽象,并且可以与多种前端框架集成,从而进一步减少了大量管道代码。
然而,对于许多Web应用程序来说,如果只是需要实现简单的用例,比如从事件源到浏览器的单向数据流,那么可能不需要服务器端的状态管理或复杂的编排。因此,类似于最小API但用于实时更新的解决方案将非常有吸引力。
接下来,将展示如何使用DotNetify.SignalR包创建一个基本的实时更新API。以下是在Program.cs文件中的实现方式:
using DotNetify;
using System.Reactive.Linq;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDotNetify().AddSignalR();
var app = builder.Build();
app.MapHub<DotNetifyHub>("dotnetify");
app.MapVM("HelloWorld", () => new {
Greetings = "Hello World",
ServerTime = Observable.Interval(TimeSpan.FromSeconds(1)).Select(_ => DateTime.Now)
});
app.Run();
新的API是MapVM,其中VM代表视图模型。第一个参数是客户端脚本用于识别连接实例的视图模型名称。第二个参数是一个匿名方法,返回一个匿名对象,表示要推送到客户端的视图模型的状态。
对象的属性值将在客户端初始连接时被序列化并包含在响应中。有趣的部分是,当属性值实现了System.IObservable
如果API无法访问依赖注入容器,那么它将没有多大用处。因此,逻辑还处理了服务注入,并支持异步操作:
app.MapVM("HelloWorld", async (IDateTimeService service) => new {
ServerTime = await service.GetDateTimeObservableAsync()
});
如果想让客户端能够将命令发送回服务器,这也得到了支持。将属性值设置为具有零个或一个参数的动作方法,然后可以使用客户端上的vm.$dispatch调用来调用该动作:
app.MapVM("HelloWorld", async (IDateTimeService service) => new {
ServerTime = await service.GetDateTimeObservableAsync(),
SetTimeZone = new Action<string>(zone => service.SetTimeZone(zone))
});
最后,这个API可以使用dotNetify的[Authorize]属性来保护,以防止未经认证的请求:
app.MapVM("HelloWorld", [Authorize] () => new {
/* ... */
});
现在,已经能够创建一个非常轻量级的Web服务来提供实时更新,让将注意力转向前端。假设目标是创建一个UI组件来显示这些更新,并且它可以轻松地嵌入到现有的网站中,无论它们使用的是哪个UI框架。本着尽可能简化的精神,也希望它不需要使用Node.js来构建。
共享UI组件的最便携方式是将其制作成原生HTML自定义元素。通常,制作一个需要很多步骤,但幸运的是,从3.2版本开始,Vue提供了一个内置API,可以将Vue组件转换为一个。Vue是一个很棒的UI框架,如果只关注现代浏览器,那么使用最新的JavaScript语法编写代码是完全可能的,而不需要转译。
举了一个例子,模拟了一个基本的股票行情应用。它有一个输入字段用于查找股票符号,以及一个区域用于显示符号及其当前价格,每秒钟更新一次。以下是实现方式:
只需要添加两个前端文件到服务中:
1. index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Stock Ticker</title>
</head>
<body>
<stock-ticker/>
<script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@5/dist/browser/signalr.min.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/dotnetify@latest/dist/dotnetify-vue.min.js"></script>
<script src="/stock-ticker.js"></script>
</body>
</html>
2. stocker-ticker.js
const StockTicker = Vue.defineCustomElement({
template: `
`,
created() {
this.vm = dotnetify.vue.connect("StockTicker", this)
},
unmounted() {
this.vm.$destroy()
},
data() {
return { symbol: "", StockPrices: [] }
},
methods: {
add() {
this.vm.$dispatch({ AddSymbol: this.symbol })
this.symbol = ""
},
}
})
customElements.define("stock-ticker", StockTicker)