教程:使用 TypeScript 和 Webpack 开始使用 ASP.NET Core SignalR

作者:Sébastien SougnezScott Addie

本教程演示如何在 ASP.NET Core SignalR Web 应用中使用 Webpack 来捆绑和构建用 TypeScript 编写的客户端。 开发人员可以通过 Webpack 捆绑和生成 Web 应用的客户端资源。

在本教程中,你将了解如何执行以下操作:

  • 创建 ASP.NET Core SignalR 应用
  • 配置 SignalR 服务器
  • 使用 Webpack 配置生成管道
  • 配置 SignalR TypeScript 客户端
  • 启用客户端和服务器之间的通信

查看或下载示例代码如何下载

先决条件

创建 ASP.NET Core Web 应用

默认情况下,Visual Studio 使用在安装目录中找到的 npm 版本。 配置 Visual Studio,在 PATH 环境变量中查找 npm:

  1. 启动 Visual Studio。 在“启动”窗口中,选择“继续但无需代码”。

  2. 导航到“工具”>“选项”>“项目和解决方案”>“Web 包管理”>“外部 Web 工具”

  3. 在列表中选择 $(PATH) 项。 选择向上键将项移动列表中的第二个位置,然后选择“确定”:

    Visual Studio 配置

新建 ASP.NET Core Web 应用:

  1. 使用“文件”>“新建”>“项目”菜单选项,然后选择“ASP.NET Core 空”模板。 选择“下一步”。
  2. 将项目命名为 SignalRWebpack 并选择“创建”。
  3. 从“框架”下拉列表中,选择 .NET 6.0 (Long-term support)。 选择“创建”。

Microsoft.TypeScript.MSBuild NuGet 包添加到项目:

  1. 在“解决方案资源管理器”中,右键单击项目节点,然后选择“管理 NuGet 包”。 在“浏览”选项卡中,搜索 Microsoft.TypeScript.MSBuild,然后选择右侧的“安装”来安装包。

Visual Studio 会将 NuGet 包添加到解决方案资源管理器中的“依赖项”节点下,从而在项目中启用 TypeScript 编译 。

配置服务器

在本部分,配置 ASP.NET Core Web 应用以发送和接收 SignalR 消息。

  1. Program.cs 中,调用 AddSignalR

    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddSignalR();
    
  2. 同样,在 Program.cs 中,调用 UseDefaultFilesUseStaticFiles

    var app = builder.Build();
    
    app.UseDefaultFiles();
    app.UseStaticFiles();
    

    上述代码允许服务器查找和处理 index.html 文件。 无论用户输入完整 URL 还是 Web 应用的根 URL,都会提供该文件。

  3. 在项目根目录 SignalRWebpack/ 中,为 SignalR类创建名为 Hubs 的新目录。

  4. 创建包含以下代码的新文件 Hubs/ChatHub.cs

    using Microsoft.AspNetCore.SignalR;
    
    namespace SignalRWebpack.Hubs;
    
    public class ChatHub : Hub
    {
        public async Task NewMessage(long username, string message) =>
            await Clients.All.SendAsync("messageReceived", username, message);
    }
    

    服务器收到消息后,前面的代码会将这些消息播发到所有连接的用户。 没有必要使用泛型 on 方法接收所有消息。 使用以消息名称命名的方法就可以了。

    在此示例中,TypeScript 客户端发送一条标识为 newMessage 的消息。 C# NewMessage 方法需要客户端发送的数据。 在 Clients.All 上对 SendAsync 进行调用。 接收的消息会发送到所有连接到中心的客户端。

  5. Program.cs 顶部添加以下 using 语句来解析 ChatHub 引用:

    using SignalRWebpack.Hubs;
    
  6. Program.cs 中,将 /hub 路由映射到 ChatHub 中心。 将显示 Hello World! 的代码替换为以下代码:

    app.MapHub<ChatHub>("/hub");
    

配置客户端

在本部分,创建一个 Node.js 项目,以使用 Webpack 将 TypeScript 转换为 JavaScript 并捆绑客户端资源,包括 HTML 和 CSS。

  1. 在项目根目录中运行以下命令,创建 package.json 文件:

    npm init -y
    
  2. 将突出显示的属性添加到 package.json 文件并保存文件更改:

    {
      "name": "SignalRWebpack",
      "version": "1.0.0",
      "private": true,
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
    

    private 属性设置为 true,防止下一步出现包安装警告。

  3. 安装所需的 npm 包。 从项目根目录运行以下命令:

    npm i -D -E clean-webpack-plugin css-loader html-webpack-plugin mini-css-extract-plugin ts-loader typescript webpack webpack-cli
    

    -E 选项禁用 npm 将语义化版本控制范围运算符写到 package.json 的默认行为。 例如,使用 "webpack": "5.70.0" 而不是 "webpack": "^5.70.0"。 此选项防止意外升级到新的包版本。

    有关详细信息,请参阅 npm-install 文档。

  4. package.json 文件的 scripts 属性替换为以下代码:

    "scripts": {
      "build": "webpack --mode=development --watch",
      "release": "webpack --mode=production",
      "publish": "npm run release && dotnet publish -c Release"
    },
    

    定义以下脚本:

    • build:在开发模式下捆绑客户端资源并观察文件更改。 文件观察程序使捆绑在每次项目文件发生更改时重新生成。 mode 选项可禁用生产优化,例如摇树优化和缩小优化。 仅在开发中使用 build
    • release:在生产模式下捆绑客户端资源。
    • publish:运行 release 脚本,在生产模式下捆绑客户端资源。 它调用 .NET CLI 的 publish 命令发布应用。
  5. 在项目根目录中创建名为 webpack.config.js 的文件,使其包含以下代码:

    const path = require("path");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    const { CleanWebpackPlugin } = require("clean-webpack-plugin");
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    
    module.exports = {
      entry: "./src/index.ts",
      output: {
        path: path.resolve(__dirname, "wwwroot"),
        filename: "[name].[chunkhash].js",
        publicPath: "/",
      },
      resolve: {
        extensions: [".js", ".ts"],
      },
      module: {
        rules: [
          {
            test: /\.ts$/,
            use: "ts-loader",
          },
          {
            test: /\.css$/,
            use: [MiniCssExtractPlugin.loader, "css-loader"],
          },
        ],
      },
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          template: "./src/index.html",
        }),
        new MiniCssExtractPlugin({
          filename: "css/[name].[chunkhash].css",
        }),
      ],
    };
    

    前面的文件配置 Webpack 编译过程:

    • output 属性替代 dist 的默认值。 捆绑反而在 wwwroot 目录中发出。
    • resolve.extensions 数组包含 .js,以便导入 SignalR 客户端 JavaScript。
  6. src 目录从示例项目复制到项目根目录中。 src 包含以下文件:

    • index.html,用于定义主页的样板标记:

      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="utf-8" />
          <title>ASP.NET Core SignalR with TypeScript and Webpack</title>
        </head>
        <body>
          <div id="divMessages" class="messages"></div>
          <div class="input-zone">
            <label id="lblMessage" for="tbMessage">Message:</label>
            <input id="tbMessage" class="input-zone-input" type="text" />
            <button id="btnSend">Send</button>
          </div>
        </body>
      </html>
      
    • css/main.css,用于为主页提供 CSS 样式:

      *,
      *::before,
      *::after {
        box-sizing: border-box;
      }
      
      html,
      body {
        margin: 0;
        padding: 0;
      }
      
      .input-zone {
        align-items: center;
        display: flex;
        flex-direction: row;
        margin: 10px;
      }
      
      .input-zone-input {
        flex: 1;
        margin-right: 10px;
      }
      
      .message-author {
        font-weight: bold;
      }
      
      .messages {
        border: 1px solid #000;
        margin: 10px;
        max-height: 300px;
        min-height: 300px;
        overflow-y: auto;
        padding: 5px;
      }
      
    • tsconfig.json,用于配置 TypeScript 编译器以生成与 ECMAScript 5 兼容的 JavaScript:

      {
        "compilerOptions": {
          "target": "es5"
        }
      }
      
    • index.ts:

      import * as signalR from "@microsoft/signalr";
      import "./css/main.css";
      
      const divMessages: HTMLDivElement = document.querySelector("#divMessages");
      const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
      const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
      const username = new Date().getTime();
      
      const connection = new signalR.HubConnectionBuilder()
          .withUrl("/hub")
          .build();
      
      connection.on("messageReceived", (username: string, message: string) => {
        const m = document.createElement("div");
      
        m.innerHTML = `<div class="message-author">${username}</div><div>${message}</div>`;
      
        divMessages.appendChild(m);
        divMessages.scrollTop = divMessages.scrollHeight;
      });
      
      connection.start().catch((err) => document.write(err));
      
      tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.key === "Enter") {
          send();
        }
      });
      
      btnSend.addEventListener("click", send);
      
      function send() {
        connection.send("newMessage", username, tbMessage.value)
          .then(() => (tbMessage.value = ""));
      }
      

      前面的代码检索对 DOM 元素的引用并附加两个事件处理程序:

      • keyup:当用户在 tbMessage 文本框中键入并在用户按“Enter”键调用 send 函数时触发。
      • click:当用户选择“发送”按钮并调用 send 函数时触发。

      HubConnectionBuilder 类创建新的生成器,用于配置服务器连接。 withUrl 函数配置中心 URL。

      SignalR 启用客户端和服务器之间的消息交换。 每个消息都有特定的名称。 例如,名为 messageReceived 的消息可以运行负责在消息区域显示新消息的逻辑。 可以通过 on 函数完成对特定消息的侦听。 可以侦听任意数量的消息名称。 还可以将参数传递到消息,例如所接收消息的作者姓名和内容。 客户端收到一条消息后,会创建一个新的 div 元素并在其 innerHTML 属性中显示作者姓名和消息内容。 它添加到显示消息的主要 div 元素。

      通过 WebSockets 连接发送消息需要调用 send 方法。 该方法的第一个参数是消息名称。 消息数据包含其他参数。 在此示例中,一条标识为 newMessage 的消息已发送到服务器。 该消息包含用户名和文本框中的用户输入。 如果发送成功,会清空文本框。

  7. 在项目根目录运行以下命令:

    npm i @microsoft/signalr @types/node
    

    上述的代码会安装:

    • SignalR TypeScript 客户端,它允许客户端向服务器发送消息。
    • 用于 node.js 的 TypeScript 类型定义,支持 Node.js 类型的编译时检查。

测试应用程序

确认应用遵循以下步骤:

  1. release 模式下运行 Webpack。 使用“包管理器控制台”窗口,在项目根目录中运行以下命令。 如果不在项目根中,请在输入该命令之前输入 cd SignalRWebpack

    npm run release
    

    此命令在运行应用时生成要提供的客户端资产。 资产位于 wwwroot 文件夹。

    Webpack 完成了以下任务:

    • 清除了 wwwroot 目录的内容。
    • 将 TypeScript 转换为 JavaScript,该过程称为“转译” 。
    • 破坏生成的 JavaScript 以降低文件大小,该过程称为“缩小” 。
    • 将已处理的 JavaScript、CSS 和 HTML 文件从 src 复制到 wwwroot 目录。
    • 将以下元素注入 wwwroot/index.html 文件:
      • 一个引用 wwwroot/main.<hash>.css 文件的 <link> 标记。 此标记紧挨着 </head> 结束标记之前。
      • 一个引用缩小后的 wwwroot/main.<hash>.js 文件的 <script> 标记。 此标记紧挨着 </body> 结束标记之前。
  2. 选择“调试”>“开始执行(不调试)”,在不附加调试器的情况下在浏览器中启动应用 。 在 https://localhost:<port> 上提供 wwwroot/index.html 文件。

    如果遇到编译错误,请尝试关闭并重新打开解决方案。

  3. 打开另一个浏览器实例(任何浏览器),然后在地址栏中粘贴该 URL。

  4. 选择一个浏览器,在“消息”文本框中键入任意内容,然后选择“发送”按钮。 两个页面上立即显示唯一的用户名和消息。

两个浏览器窗口都显示的消息

其他资源

本教程演示如何在 ASP.NET Core SignalR Web 应用中使用 Webpack 来捆绑和构建用 TypeScript 编写的客户端。 开发人员可以通过 Webpack 捆绑和生成 Web 应用的客户端资源。

在本教程中,你将了解:

  • 为入门 ASP.NET Core SignalR 应用搭建基架
  • 配置 SignalR TypeScript 客户端
  • 使用 Webpack 配置生成管道
  • 配置 SignalR 服务器
  • 启用客户端和服务器之间的通信

查看或下载示例代码如何下载

先决条件

创建 ASP.NET Core Web 应用

配置 Visual Studio,在 PATH 环境变量中查找 npm。 默认情况下,Visual Studio 使用在安装目录中找到的 npm 版本。 在 Visual Studio 中按照以下说明执行操作:

  1. 启动 Visual Studio。 在“启动”窗口中,选择“继续但无需代码”。

  2. 导航到“工具”>“选项”>“项目和解决方案”>“Web 包管理”>“外部 Web 工具”

  3. 在列表中选择 $(PATH) 项。 选择向上键将项移动列表中的第二个位置,然后选择“确定”。

    Visual Studio 配置

Visual Studio 配置完成。

  1. 使用“文件”>“新建”>“项目”菜单选项,然后选择“ASP.NET Core Web 应用程序”模板 。 选择“下一步”。
  2. 将项目命名为 *SignalRWebPac`` 并选择“创建”。
  3. 从目标框架下拉列表选择 .NET Core 并从框架选择器下拉列表选择 ASP.NET Core 3.1 。 选择“空白”模板并选择“创建” 。

Microsoft.TypeScript.MSBuild 包添加到项目:

  1. 在“解决方案资源管理器”(右侧窗格)中,右键单击项目节点,然后选择“管理 NuGet 包” 。 在“浏览”选项卡中,搜索 Microsoft.TypeScript.MSBuild,然后单击右侧的“安装”来安装包 。

Visual Studio 会将 NuGet 包添加到解决方案资源管理器中的“依赖项”节点下,从而在项目中启用 TypeScript 编译 。

配置 Webpack 和 TypeScript

以下步骤配置 TypeScript 到 JavaScript 的转换和客户端资源的捆绑。

  1. 在项目根目录中运行以下命令,创建 package.json 文件:

    npm init -y
    
  2. 将突出显示的属性添加到 package.json 文件并保存文件更改:

    {
      "name": "SignalRWebPack",
      "version": "1.0.0",
      "private": true,
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
    

    private 属性设置为 true,防止下一步出现包安装警告。

  3. 安装所需的 npm 包。 从项目根目录运行以下命令:

    npm i -D -E clean-webpack-plugin@3.0.0 css-loader@3.4.2 html-webpack-plugin@3.2.0 mini-css-extract-plugin@0.9.0 ts-loader@6.2.1 typescript@3.7.5 webpack@4.41.5 webpack-cli@3.3.10
    

    需要注意的一些命令细节:

    • 每个包名称中 @ 符号后是版本号。 npm 安装这些特定的包版本。
    • -E 选项禁用 npm 将语义化版本控制范围运算符写到 *packagejson 的默认行为。 例如,使用 "webpack": "4.41.5" 而不是 "webpack": "^4.41.5"。 此选项防止意外升级到新的包版本。

    有关详细信息,请参阅 npm-install 文档。

  4. package.json 文件的 scripts 属性替换为以下代码:

    "scripts": {
      "build": "webpack --mode=development --watch",
      "release": "webpack --mode=production",
      "publish": "npm run release && dotnet publish -c Release"
    },
    

    脚本的一些解释:

    • build:在开发模式下捆绑客户端资源并观察文件更改。 文件观察程序使捆绑在每次项目文件发生更改时重新生成。 mode 选项可禁用生产优化,例如摇树优化和缩小优化。 仅在开发中使用 build
    • release:在生产模式下捆绑客户端资源。
    • publish:运行 release 脚本,在生产模式下捆绑客户端资源。 它调用 .NET Core CLI 的 publish 命令发布应用。
  5. 在项目根目录中创建名为 webpack.config.js 的文件,使其包含以下代码:

    const path = require("path");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    const { CleanWebpackPlugin } = require("clean-webpack-plugin");
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    module.exports = {
        entry: "./src/index.ts",
        output: {
            path: path.resolve(__dirname, "wwwroot"),
            filename: "[name].[chunkhash].js",
            publicPath: "/"
        },
        resolve: {
            extensions: [".js", ".ts"]
        },
        module: {
            rules: [
                {
                    test: /\.ts$/,
                    use: "ts-loader"
                },
                {
                    test: /\.css$/,
                    use: [MiniCssExtractPlugin.loader, "css-loader"]
                }
            ]
        },
        plugins: [
            new CleanWebpackPlugin(),
            new HtmlWebpackPlugin({
                template: "./src/index.html"
            }),
            new MiniCssExtractPlugin({
                filename: "css/[name].[chunkhash].css"
            })
        ]
    };
    

    前面的文件配置 Webpack 编译。 需要注意的一些配置细节:

    • output 属性替代 dist 的默认值。 捆绑反而在 wwwroot 目录中发出。
    • resolve.extensions 数组包含 .js,以便导入 SignalR 客户端 JavaScript。
  6. 在项目根目录中创建新的 src 目录,以存储项目的客户端资产。

  7. 使用以下标记创建 src/index.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title>ASP.NET Core SignalR</title>
    </head>
    <body>
        <div id="divMessages" class="messages">
        </div>
        <div class="input-zone">
            <label id="lblMessage" for="tbMessage">Message:</label>
            <input id="tbMessage" class="input-zone-input" type="text" />
            <button id="btnSend">Send</button>
        </div>
    </body>
    </html>
    

    前面的 HTML 定义主页的样板标记。

  8. 创建新的 src/css 目录。 目的是存储项目的 .css 文件。

  9. 使用以下 CSS 创建 src/css/main.css

    *, *::before, *::after {
        box-sizing: border-box;
    }
    
    html, body {
        margin: 0;
        padding: 0;
    }
    
    .input-zone {
        align-items: center;
        display: flex;
        flex-direction: row;
        margin: 10px;
    }
    
    .input-zone-input {
        flex: 1;
        margin-right: 10px;
    }
    
    .message-author {
        font-weight: bold;
    }
    
    .messages {
        border: 1px solid #000;
        margin: 10px;
        max-height: 300px;
        min-height: 300px;
        overflow-y: auto;
        padding: 5px;
    }
    

    前面的 main.css 文件设计应用样式。

  10. 使用以下 JSON 创建 src/tsconfig.json

    {
      "compilerOptions": {
        "target": "es5"
      }
    }
    

    前面的代码配置 TypeScript 编译器,生成与 ECMAScript 5 兼容的 JavaScript。

  11. 使用以下代码创建 src/index.ts

    import "./css/main.css";
    
    const divMessages: HTMLDivElement = document.querySelector("#divMessages");
    const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
    const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
    const username = new Date().getTime();
    
    tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.key === "Enter") {
            send();
        }
    });
    
    btnSend.addEventListener("click", send);
    
    function send() {
    }
    

    前面的 TypeScript 检索对 DOM 元素的引用并附加两个事件处理程序:

    • keyup:用户在 tbMessage 文本框中键入时触发此事件。 用户按 Enter 时调用 send 函数。
    • click:用户选择“发送”按钮时触发此事件。 调用 send 函数。

配置应用

  1. Startup.Configure 中,添加对 UseDefaultFiles(IApplicationBuilder)UseStaticFiles(IApplicationBuilder) 的调用。

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        
        app.UseRouting();
        app.UseDefaultFiles();
        app.UseStaticFiles();
        
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHub<ChatHub>("/hub");
        });
            
    }
    

    上述代码允许服务器查找和处理 index.html 文件。 无论用户输入完整 URL 还是 Web 应用的根 URL,都会提供该文件。

  2. Startup.Configure 的末尾,将 /hub 路由映射到 ChatHub 中心。 将显示 Hello World! 的代码替换为以下行:

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/hub");
    });
    
  3. Startup.ConfigureServices 中,调用 AddSignalR

    services.AddSignalR();
    
  4. 在项目根目录 SignalRWebPack/ 中创建名为 Hubs 的新目录,以存储 SignalR 中心 。

  5. 使用以下代码创建中心 Hubs/ChatHub.cs

    using Microsoft.AspNetCore.SignalR;
    using System.Threading.Tasks;
    
    namespace SignalRWebPack.Hubs
    {
        public class ChatHub : Hub
        {
        }
    }
    
  6. Startup.cs 文件的顶部添加以下 using 语句来解析 ChatHub 引用:

    using SignalRWebPack.Hubs;
    

启用客户端和服务器通信

应用目前显示用于发送消息的基本窗体,但尚不能正常工作。 服务器正在侦听特定的路由,但是不涉及发送消息。

  1. 在项目根目录运行以下命令:

    npm i @microsoft/signalr @types/node
    

    上述的代码会安装:

    • SignalR TypeScript 客户端,它允许客户端向服务器发送消息。
    • 用于 node.js 的 TypeScript 类型定义,支持 Node.js 类型的编译时检查。
  2. 将突出显示的代码添加到 src/index.ts 文件:

    import "./css/main.css";
    import * as signalR from "@microsoft/signalr";
    
    const divMessages: HTMLDivElement = document.querySelector("#divMessages");
    const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
    const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
    const username = new Date().getTime();
    
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/hub")
        .build();
    
    connection.on("messageReceived", (username: string, message: string) => {
        let m = document.createElement("div");
    
        m.innerHTML =
            `<div class="message-author">${username}</div><div>${message}</div>`;
    
        divMessages.appendChild(m);
        divMessages.scrollTop = divMessages.scrollHeight;
    });
    
    connection.start().catch(err => document.write(err));
    
    tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.key === "Enter") {
            send();
        }
    });
    
    btnSend.addEventListener("click", send);
    
    function send() {
    }
    

    前面的代码支持从服务器接收消息。 HubConnectionBuilder 类创建新的生成器,用于配置服务器连接。 withUrl 函数配置中心 URL。

    SignalR 启用客户端和服务器之间的消息交换。 每个消息都有特定的名称。 例如,名为 messageReceived 的消息可以运行负责在消息区域显示新消息的逻辑。 可以通过 on 函数完成对特定消息的侦听。 可以侦听任意数量的消息名称。 还可以将参数传递到消息,例如所接收消息的作者姓名和内容。 客户端收到一条消息后,会创建一个新的 div 元素并在其 innerHTML 属性中显示作者姓名和消息内容。 它添加到显示消息的主要 div 元素。

  3. 客户端可以接收消息后,将它配置为发送消息。 将突出显示的代码添加到 src/index.ts 文件:

    import "./css/main.css";
    import * as signalR from "@microsoft/signalr";
    
    const divMessages: HTMLDivElement = document.querySelector("#divMessages");
    const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
    const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
    const username = new Date().getTime();
    
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/hub")
        .build();
    
    connection.on("messageReceived", (username: string, message: string) => {
        let messages = document.createElement("div");
    
        messages.innerHTML =
            `<div class="message-author">${username}</div><div>${message}</div>`;
    
        divMessages.appendChild(messages);
        divMessages.scrollTop = divMessages.scrollHeight;
    });
    
    connection.start().catch(err => document.write(err));
    
    tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.key === "Enter") {
            send();
        }
    });
    
    btnSend.addEventListener("click", send);
    
    function send() {
        connection.send("newMessage", username, tbMessage.value)
            .then(() => tbMessage.value = "");
    }
    

    通过 WebSockets 连接发送消息需要调用 send 方法。 该方法的第一个参数是消息名称。 消息数据包含其他参数。 在此示例中,一条标识为 newMessage 的消息已发送到服务器。 该消息包含用户名和文本框中的用户输入。 如果发送成功,会清空文本框。

  4. NewMessage 方法添加到 ChatHub 类:

    using Microsoft.AspNetCore.SignalR;
    using System.Threading.Tasks;
    
    namespace SignalRWebPack.Hubs
    {
        public class ChatHub : Hub
        {
            public async Task NewMessage(long username, string message)
            {
                await Clients.All.SendAsync("messageReceived", username, message);
            }
        }
    }
    

    服务器收到消息后,前面的代码会将这些消息播发到所有连接的用户。 没有必要使用泛型 on 方法接收所有消息。 使用以消息名称命名的方法就可以了。

    在此示例中,TypeScript 客户端发送一条标识为 newMessage 的消息。 C# NewMessage 方法需要客户端发送的数据。 在 Clients.All 上对 SendAsync 进行调用。 接收的消息会发送到所有连接到中心的客户端。

测试应用

确认应用遵循以下步骤。

  1. 在 release 模式下运行 Webpack。 使用“包管理器控制台”窗口,在项目根目录中运行以下命令。 如果不在项目根中,请在输入该命令之前输入 cd SignalRWebPack

    npm run release
    

    此命令在运行应用时生成要提供的客户端资产。 资产位于 wwwroot 文件夹。

    Webpack 完成了以下任务:

    • 清除了 wwwroot 目录的内容。
    • 将 TypeScript 转换为 JavaScript,该过程称为“转译” 。
    • 破坏生成的 JavaScript 以降低文件大小,该过程称为“缩小” 。
    • 将已处理的 JavaScript、CSS 和 HTML 文件从 src 复制到 wwwroot 目录。
    • 将以下元素注入 wwwroot/index.html 文件:
      • 一个引用 wwwroot/main.<hash>.css 文件的 <link> 标记。 此标记紧挨着 </head> 结束标记之前。
      • 一个引用缩小后的 wwwroot/main.<hash>.js 文件的 <script> 标记。 此标记紧挨着 </body> 结束标记之前。
  2. 选择“调试”>“开始执行(不调试)”,在不附加调试器的情况下在浏览器中启动应用 。 在 http://localhost:<port_number> 上提供 wwwroot/index.html 文件。

    如果遇到编译错误,请尝试关闭并重新打开解决方案。

  3. 打开另一个浏览器实例(任意浏览器)。 在地址栏中粘贴 URL。

  4. 选择一个浏览器,在“消息”文本框中键入任意内容,然后选择“发送”按钮。 两个页面上立即显示唯一的用户名和消息。

两个浏览器窗口都显示的消息

其他资源

本教程演示如何在 ASP.NET Core SignalR Web 应用中使用 Webpack 来捆绑和构建用 TypeScript 编写的客户端。 开发人员可以通过 Webpack 捆绑和生成 Web 应用的客户端资源。

在本教程中,你将了解:

  • 为入门 ASP.NET Core SignalR 应用搭建基架
  • 配置 SignalR TypeScript 客户端
  • 使用 Webpack 配置生成管道
  • 配置 SignalR 服务器
  • 启用客户端和服务器之间的通信

查看或下载示例代码如何下载

先决条件

创建 ASP.NET Core Web 应用

配置 Visual Studio,在 PATH 环境变量中查找 npm。 默认情况下,Visual Studio 使用在安装目录中找到的 npm 版本。 在 Visual Studio 中按照以下说明执行操作:

  1. 导航到“工具”>“选项”>“项目和解决方案”>“Web 包管理”>“外部 Web 工具”

  2. 在列表中选择 $(PATH) 项。 选择向上键将项移动列表第二个位置。

    Visual Studio 配置

已完成 Visual Studio 配置。 可以开始创建项目了。

  1. 使用“文件”>“新建”>“项目”菜单选项,然后选择“ASP.NET Core Web 应用程序”模板 。
  2. 将项目命名为 *SignalRWebPack` 并选择“创建”。
  3. 从目标框架下拉列表选择 .NET Core 并从框架选择器下拉列表选择 ASP.NET Core 2.2 。 选择“空白”模板并选择“创建” 。

配置 Webpack 和 TypeScript

以下步骤配置 TypeScript 到 JavaScript 的转换和客户端资源的捆绑。

  1. 在项目根目录中运行以下命令,创建 package.json 文件:

    npm init -y
    
  2. 将突出显示的属性添加到 package.json 文件:

    {
      "name": "SignalRWebPack",
      "version": "1.0.0",
      "private": true,
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
    

    private 属性设置为 true,防止下一步出现包安装警告。

  3. 安装所需的 npm 包。 从项目根目录运行以下命令:

    npm install -D -E clean-webpack-plugin@1.0.1 css-loader@2.1.0 html-webpack-plugin@4.0.0-beta.5 mini-css-extract-plugin@0.5.0 ts-loader@5.3.3 typescript@3.3.3 webpack@4.29.3 webpack-cli@3.2.3
    

    需要注意的一些命令细节:

    • 每个包名称中 @ 符号后是版本号。 npm 安装这些特定的包版本。
    • -E 选项禁用 npm 将语义化版本控制范围运算符写到 *packagejson 的默认行为。 例如,使用 "webpack": "4.29.3" 而不是 "webpack": "^4.29.3"。 此选项防止意外升级到新的包版本。

    有关详细信息,请参阅 npm-install 文档。

  4. package.json 文件的 scripts 属性替换为以下代码:

    "scripts": {
      "build": "webpack --mode=development --watch",
      "release": "webpack --mode=production",
      "publish": "npm run release && dotnet publish -c Release"
    },
    

    脚本的一些解释:

    • build:在开发模式下捆绑客户端资源并观察文件更改。 文件观察程序使捆绑在每次项目文件发生更改时重新生成。 mode 选项可禁用生产优化,例如摇树优化和缩小优化。 仅在开发中使用 build
    • release:在生产模式下捆绑客户端资源。
    • publish:运行 release 脚本,在生产模式下捆绑客户端资源。 它调用 .NET Core CLI 的 publish 命令发布应用。
  5. 在项目根目录中创建名为 *webpack.config.js 的文件,使其包含以下代码:

    const path = require("path");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    const CleanWebpackPlugin = require("clean-webpack-plugin");
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    
    module.exports = {
        entry: "./src/index.ts",
        output: {
            path: path.resolve(__dirname, "wwwroot"),
            filename: "[name].[chunkhash].js",
            publicPath: "/"
        },
        resolve: {
            extensions: [".js", ".ts"]
        },
        module: {
            rules: [
                {
                    test: /\.ts$/,
                    use: "ts-loader"
                },
                {
                    test: /\.css$/,
                    use: [MiniCssExtractPlugin.loader, "css-loader"]
                }
            ]
        },
        plugins: [
            new CleanWebpackPlugin(["wwwroot/*"]),
            new HtmlWebpackPlugin({
                template: "./src/index.html"
            }),
            new MiniCssExtractPlugin({
                filename: "css/[name].[chunkhash].css"
            })
        ]
    };
    

    前面的文件配置 Webpack 编译。 需要注意的一些配置细节:

    • output 属性替代 dist 的默认值。 捆绑反而在 wwwroot 目录中发出。
    • resolve.extensions 数组包含 .js,以便导入 SignalR 客户端 JavaScript。
  6. 在项目根目录中创建新的 src 目录,以存储项目的客户端资产。

  7. 使用以下标记创建 src/index.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title>ASP.NET Core SignalR</title>
    </head>
    <body>
        <div id="divMessages" class="messages">
        </div>
        <div class="input-zone">
            <label id="lblMessage" for="tbMessage">Message:</label>
            <input id="tbMessage" class="input-zone-input" type="text" />
            <button id="btnSend">Send</button>
        </div>
    </body>
    </html>
    

    前面的 HTML 定义主页的样板标记。

  8. 创建新的 src/css 目录。 目的是存储项目的 .css 文件。

  9. 使用以下标记创建 src/css/main.css

    *, *::before, *::after {
        box-sizing: border-box;
    }
    
    html, body {
        margin: 0;
        padding: 0;
    }
    
    .input-zone {
        align-items: center;
        display: flex;
        flex-direction: row;
        margin: 10px;
    }
    
    .input-zone-input {
        flex: 1;
        margin-right: 10px;
    }
    
    .message-author {
        font-weight: bold;
    }
    
    .messages {
        border: 1px solid #000;
        margin: 10px;
        max-height: 300px;
        min-height: 300px;
        overflow-y: auto;
        padding: 5px;
    }
    

    前面的 main.css 文件设计应用样式。

  10. 使用以下 JSON 创建 src/tsconfig.json

    {
      "compilerOptions": {
        "target": "es5"
      }
    }
    

    前面的代码配置 TypeScript 编译器,生成与 ECMAScript 5 兼容的 JavaScript。

  11. 使用以下代码创建 src/index.ts

    import "./css/main.css";
    
    const divMessages: HTMLDivElement = document.querySelector("#divMessages");
    const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
    const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
    const username = new Date().getTime();
    
    tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.keyCode === 13) {
            send();
        }
    });
    
    btnSend.addEventListener("click", send);
    
    function send() {
    }
    

    前面的 TypeScript 检索对 DOM 元素的引用并附加两个事件处理程序:

    • keyup:用户在 tbMessage 文本框中键入时触发此事件。 用户按 Enter 时调用 send 函数。
    • click:用户选择“发送”按钮时触发此事件。 调用 send 函数。

配置 ASP.NET Core 应用

  1. Startup.Configure 方法中提供的代码显示 Hello World!。 将 app.Run 方法调用替换为对 UseDefaultFiles(IApplicationBuilder)UseStaticFiles(IApplicationBuilder) 的调用。

    app.UseDefaultFiles();
    app.UseStaticFiles();
    

    前面的代码允许服务器定位并提供 index.html 文件,无论用户输入完整 URL 还是 Web 应用的根 URL。

  2. Startup.ConfigureServices 中调用 AddSignalR。 此操作会将 SignalR 服务添加到项目。

    services.AddSignalR();
    
  3. 将 /hub 路由映射到 ChatHub 中心。 在 Startup.Configure 的末尾添加以下行:

    app.UseSignalR(options =>
    {
        options.MapHub<ChatHub>("/hub");
    });
    
  4. 在项目根中创建名为 Hubs 的新目录。 目的是存储 SignalR 中心(在下一步中创建)。

  5. 使用以下代码创建中心 Hubs/ChatHub.cs

    using Microsoft.AspNetCore.SignalR;
    using System.Threading.Tasks;
    
    namespace SignalRWebPack.Hubs
    {
        public class ChatHub : Hub
        {
        }
    }
    
  6. Startup.cs 文件的顶部添加以下代码来解析 ChatHub 引用:

    using SignalRWebPack.Hubs;
    

启用客户端和服务器通信

应用当前显示一个发送消息的简单窗体。 尝试执行此操作时没有任何反应。 服务器正在侦听特定的路由,但是不涉及发送消息。

  1. 在项目根目录运行以下命令:

    npm install @aspnet/signalr
    

    前面的命令将安装 SignalR TypeScript 客户端,它允许客户端向服务器发送消息。

  2. 将突出显示的代码添加到 src/index.ts 文件:

    import "./css/main.css";
    import * as signalR from "@aspnet/signalr";
    
    const divMessages: HTMLDivElement = document.querySelector("#divMessages");
    const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
    const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
    const username = new Date().getTime();
    
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/hub")
        .build();
    
    connection.on("messageReceived", (username: string, message: string) => {
        let m = document.createElement("div");
    
        m.innerHTML =
            `<div class="message-author">${username}</div><div>${message}</div>`;
    
        divMessages.appendChild(m);
        divMessages.scrollTop = divMessages.scrollHeight;
    });
    
    connection.start().catch(err => document.write(err));
    
    tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.keyCode === 13) {
            send();
        }
    });
    
    btnSend.addEventListener("click", send);
    
    function send() {
    }
    

    前面的代码支持从服务器接收消息。 HubConnectionBuilder 类创建新的生成器,用于配置服务器连接。 withUrl 函数配置中心 URL。

    SignalR 启用客户端和服务器之间的消息交换。 每个消息都有特定的名称。 例如,名为 messageReceived 的消息可以运行负责在消息区域显示新消息的逻辑。 可以通过 on 函数完成对特定消息的侦听。 可以侦听任意数量的消息名称。 还可以将参数传递到消息,例如所接收消息的作者姓名和内容。 客户端收到一条消息后,会创建一个新的 div 元素并在其 innerHTML 属性中显示作者姓名和消息内容。 新消息将添加到显示消息的主 div 元素中。

  3. 客户端可以接收消息后,将它配置为发送消息。 将突出显示的代码添加到 src/index.ts 文件:

    import "./css/main.css";
    import * as signalR from "@aspnet/signalr";
    
    const divMessages: HTMLDivElement = document.querySelector("#divMessages");
    const tbMessage: HTMLInputElement = document.querySelector("#tbMessage");
    const btnSend: HTMLButtonElement = document.querySelector("#btnSend");
    const username = new Date().getTime();
    
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/hub")
        .build();
    
    connection.on("messageReceived", (username: string, message: string) => {
        let messageContainer = document.createElement("div");
    
        messageContainer.innerHTML =
            `<div class="message-author">${username}</div><div>${message}</div>`;
    
        divMessages.appendChild(messageContainer);
        divMessages.scrollTop = divMessages.scrollHeight;
    });
    
    connection.start().catch(err => document.write(err));
    
    tbMessage.addEventListener("keyup", (e: KeyboardEvent) => {
        if (e.keyCode === 13) {
            send();
        }
    });
    
    btnSend.addEventListener("click", send);
    
    function send() {
        connection.send("newMessage", username, tbMessage.value)
                  .then(() => tbMessage.value = "");
    }
    

    通过 WebSockets 连接发送消息需要调用 send 方法。 该方法的第一个参数是消息名称。 消息数据包含其他参数。 在此示例中,一条标识为 newMessage 的消息已发送到服务器。 该消息包含用户名和文本框中的用户输入。 如果发送成功,会清空文本框。

  4. NewMessage 方法添加到 ChatHub 类:

    using Microsoft.AspNetCore.SignalR;
    using System.Threading.Tasks;
    
    namespace SignalRWebPack.Hubs
    {
        public class ChatHub : Hub
        {
            public async Task NewMessage(long username, string message)
            {
                await Clients.All.SendAsync("messageReceived", username, message);
            }
        }
    }
    

    服务器收到消息后,前面的代码会将这些消息播发到所有连接的用户。 没有必要使用泛型 on 方法接收所有消息。 使用以消息名称命名的方法就可以了。

    在此示例中,TypeScript 客户端发送一条标识为 newMessage 的消息。 C# NewMessage 方法需要客户端发送的数据。 在 Clients.All 上对 SendAsync 进行调用。 接收的消息会发送到所有连接到中心的客户端。

测试应用

确认应用遵循以下步骤。

  1. 在 release 模式下运行 Webpack。 使用“包管理器控制台”窗口,在项目根目录中运行以下命令。 如果不在项目根中,请在输入该命令之前输入 cd SignalRWebPack

    npm run release
    

    此命令在运行应用时生成要提供的客户端资产。 资产位于 wwwroot 文件夹。

    Webpack 完成了以下任务:

    • 清除了 wwwroot 目录的内容。
    • 将 TypeScript 转换为 JavaScript,该过程称为“转译” 。
    • 破坏生成的 JavaScript 以降低文件大小,该过程称为“缩小” 。
    • 将已处理的 JavaScript、CSS 和 HTML 文件从 src 复制到 wwwroot 目录。
    • 将以下元素注入 wwwroot/index.html 文件:
      • 一个引用 wwwroot/main.<hash>.css 文件的 <link> 标记。 此标记紧挨着 </head> 结束标记之前。
      • 一个引用缩小后的 wwwroot/main.<hash>.js 文件的 <script> 标记。 此标记紧挨着 </body> 结束标记之前。
  2. 选择“调试”>“开始执行(不调试)”,在不附加调试器的情况下在浏览器中启动应用 。 在 http://localhost:<port_number> 上提供 wwwroot/index.html 文件。

  3. 打开另一个浏览器实例(任意浏览器)。 在地址栏中粘贴 URL。

  4. 选择一个浏览器,在“消息”文本框中键入任意内容,然后选择“发送”按钮。 两个页面上立即显示唯一的用户名和消息。

两个浏览器窗口都显示的消息

其他资源