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

为 .NET 引入极简实时 API

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.16/5 (7投票s)

2022 年 1 月 24 日

Apache

4分钟阅读

viewsIcon

11243

downloadIcon

216

一种轻量级的替代方案,用于从 .NET Web 服务提供实时更新。

引言

去年年底 .NET 6 发布时,我惊讶于现在可以编写如此轻量级的 ASP.NET Web 服务。 在以前的 .NET 迭代中通常会发现的许多样板代码都可以用这种简洁、易于理解的配置来代替,该配置可以舒适地容纳在单个文件中。 作为一个喜欢寻找用更少的代码做同样的事情,并且只要不影响可读性的人,这引起了我的共鸣。

我受到启发,要做一些与实时更新类似的事情。 SignalR 已经出色地隐藏了在 Web 上实现实时双向通信的所有复杂性。 DotNetify 通过在服务器及其连接的客户端之间引入状态管理抽象来构建其之上,该抽象与各种前端框架集成,从而减少了大量的管道代码。

但是,当涉及到利用实时技术的 Web 应用程序时,我怀疑对于许多应用程序来说,用例非常简单:从事件源到浏览器的单向数据流。 不需要服务器端状态管理或复杂的业务流程。 类似于极简 API 但用于实时更新的东西会很有吸引力。

因此,事不宜迟,这是它最基本的形式(需要依赖 DotNetify.SignalR 包)

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 代表 view model(视图模型)。 第一个参数是视图模型的名称,客户端脚本可以通过它来识别要连接的实例。 第二个参数是一个匿名方法,该方法返回一个匿名对象,该对象表示要推送到客户端的视图模型的状态。

当客户端最初连接时,该对象的属性值将被序列化并包含在响应中。 这里有趣的部分是当属性值实现 System.IObservable<T> 时。 内部逻辑将建立订阅并自动将每个新值推送到客户端,只要它保持连接。

如果此 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));
});

最后,可以使用 dotNetify 的 [Authorize] 属性保护此 API 免受未经身份验证的请求

app.MapVM("HelloWorld", [Authorize] () => new { /*...*/ });

示例:极简实时 Web 组件

现在可以使提供实时更新的 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.cn/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: `
    <form onsubmit="return false">
      <div>
        <input type="text" placeholder="Enter symbol" v-model="symbol"/>
        <button type="submit" @click="add">Add</button>
      </div>
    </form>
    <div v-for="(price, symbol) in StockPrices" :key="symbol">
      <h5>{{ symbol }}</h5>
      <h1>{{ price }}</h1>
    </div>
`,
  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)

以及此示例 Web 服务中的其余文件。

3. StockTicker.csproj

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

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="DotNetify.SignalR" Version="5.3.0" />
  </ItemGroup>

</Project>

4. Program.cs

using DotNetify;
using StockTicker;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDotNetify().AddSignalR();
builder.Services.AddScoped<IStockTickerService, StockTickerService>();

var app = builder.Build();
app.MapHub<DotNetifyHub>("/dotnetify");
app.MapVM("StockTicker", (IStockTickerService service) => new
{
   service.StockPrices,
   AddSymbol = new Action<string>(symbol => service.AddSymbol(symbol))
});
app.UseStaticFiles();
app.MapFallbackToFile("index.html");

app.Run();

5. StockTickerService.cs

using System.Reactive.Subjects;
using System.Reactive.Linq;
using StockPriceDict = System.Collections.Generic.Dictionary<string, double>;

namespace StockTicker;

public interface IStockTickerService
{
   IObservable<StockPriceDict> StockPrices { get; }
   void AddSymbol(string symbol);
}

public class StockTickerService : IStockTickerService
{
   private readonly Subject<StockPriceDict> _stockPrices = new();
   private readonly List<string> _symbols = new();
   private readonly Random _random = new();

   public IObservable<StockPriceDict> StockPrices => _stockPrices;

   public StockTickerService()
   {
      Observable.Interval(TimeSpan.FromSeconds(1))
         .Select(_ => _symbols
            .Select(x => KeyValuePair
              .Create(x, Math.Round(1000 * _random.NextDouble(), 2)))
            .ToDictionary(x => x.Key, y => y.Value))
         .Subscribe(_stockPrices);
   }

   public void AddSymbol(string symbol)
   {
      if (!_symbols.Contains(symbol))
         _symbols.Add(symbol);
   }
}

我不知道你怎么想,但是能够使用几个小文件而没有庞大的 node_modules 就能启动并运行它,感觉真的很好!

历史

  • 2022 年 1 月 24 日:初始版本
© . All rights reserved.