通过


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

Note

此版本不是本文的最新版本。 有关当前版本,请参阅 本文的 .NET 10 版本

Warning

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅 本文的 .NET 10 版本

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

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

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

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

Prerequisites

  • 最新版本的 Visual Studio 具有 ASP.NET 和 Web 开发 工作负载。

    VS26 安装程序工作负载

创建 ASP.NET Core Web 应用

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

启动最新版本的 Visual Studio。 在“启动”窗口中,选择“继续但无需代码”。

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

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

    Visual Studio 配置。

新建 ASP.NET Core Web 应用:

  1. 使用 “文件>新建>项目/解决方案...” 菜单选项。
  2. 在“ 创建新项目 ”对话框中,选择 ASP.NET 核心空 模板。 然后选择下一步
  3. 在“配置新项目”对话框中,为“项目名称”SignalRWebpack输入 。 选择“下一步”。
  4. “其他信息”对话框中,从框架下拉列表中选择 .NET 10.0(长期支持)。 选择 创建

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

  1. 解决方案资源管理器中,右键单击项目节点并选择“ 管理 NuGet 包...”
  2. 在“浏览”选项卡中,搜索 ,然后选择右侧的“安装”来安装包。
  3. “预览更改 ”对话框中,选择“ 应用”。
  4. 在“许可接受”对话框中,选择“我接受”

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 方法需要客户端发送的数据。
    • SendAsync 上发起对 Clients.All 的调用。
    • 接收的消息会发送到所有连接到中心的客户端。
  5. using 顶部添加以下 Program.cs 语句来解析 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": {
        "build": "webpack --mode=development --watch",
        "release": "webpack --mode=production",
        "publish": "npm run release && dotnet publish -c Release"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "type": "commonjs",
      "devDependencies": {
        "clean-webpack-plugin": "4.0.0",
        "css-loader": "7.1.3",
        "html-webpack-plugin": "5.6.6",
        "mini-css-extract-plugin": "2.10.0",
        "ts-loader": "9.5.4",
        "typescript": "5.9.3",
        "webpack": "5.104.1",
        "webpack-cli": "6.0.1"
      },
      "dependencies": {
        "@microsoft/signalr": "^10.0.0",
        "@types/node": "^25.0.10"
      }
    }
    

    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.76.1" 而不是 "webpack": "^5.76.1"。 此选项防止意外升级到新的包版本。

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

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

    "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 中,为客户端代码创建名为 SignalRWebpack/ 的新目录。

  7. 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");
      
        const author = document.createElement("div");
        author.className = "message-author";
        author.textContent = username;
      
        const content = document.createElement("div");
        content.textContent = message;
      
        m.append(author, content);
      
        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:当用户选择“发送”按钮并调用 函数时触发。

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

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

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

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

    npm i @microsoft/signalr @types/node
    

    前面的命令会安装:

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

测试应用

确认应用遵循以下步骤:

  1. release 模式下运行 Webpack。 使用“包管理器控制台”窗口,在项目根目录中运行以下命令。

    npm run release
    

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

    Webpack 完成了以下任务:

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

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

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

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

这两个浏览器窗口中显示的消息。

后续步骤

其他资源

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

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

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

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

Prerequisites

创建 ASP.NET Core Web 应用

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

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

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

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

    Visual Studio 配置

新建 ASP.NET Core Web 应用:

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

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

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

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 方法需要客户端发送的数据。
    • SendAsync 上发起对 Clients.All 的调用。
    • 接收的消息会发送到所有连接到中心的客户端。
  5. using 顶部添加以下 Program.cs 语句来解析 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.76.1" 而不是 "webpack": "^5.76.1"。 此选项防止意外升级到新的包版本。

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

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

    "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 中,为客户端代码创建名为 SignalRWebpack/ 的新目录。

  7. 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:当用户选择“发送”按钮并调用 函数时触发。

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

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

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

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

    npm i @microsoft/signalr @types/node
    

    前面的命令会安装:

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

测试应用

确认应用遵循以下步骤:

  1. release 模式下运行 Webpack。 使用“包管理器控制台”窗口,在项目根目录中运行以下命令。

    npm run release
    

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

    Webpack 完成了以下任务:

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

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

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

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

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

后续步骤

其他资源

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

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

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

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

Prerequisites

创建 ASP.NET Core Web 应用

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

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

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

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

    Visual Studio 配置

新建 ASP.NET Core Web 应用:

  1. 使用文件>新建>项目菜单选项,然后选择ASP.NET Core 空模板。 选择“下一步”。
  2. 将项目命名为 SignalRWebpack 并选择“创建”。
  3. 框架下拉列表中选择 .NET 7.0(标准术语支持)。 选择 创建

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

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

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 方法需要客户端发送的数据。
    • SendAsync 上发起对 Clients.All 的调用。
    • 接收的消息会发送到所有连接到中心的客户端。
  5. using 顶部添加以下 Program.cs 语句来解析 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.76.1" 而不是 "webpack": "^5.76.1"。 此选项防止意外升级到新的包版本。

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

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

    "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:当用户选择“发送”按钮并调用 函数时触发。

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

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

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

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

    npm i @microsoft/signalr @types/node
    

    前面的命令会安装:

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

测试应用

确认应用遵循以下步骤:

  1. release 模式下运行 Webpack。 使用“包管理器控制台”窗口,在项目根目录中运行以下命令。

    npm run release
    

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

    Webpack 完成了以下任务:

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

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

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

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

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

后续步骤

其他资源

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

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

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

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

Prerequisites

创建 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(长期支持)。 选择 创建

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

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

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 方法需要客户端发送的数据。 在 SendAsync 上发起对 Clients.All 的调用。 接收的消息会发送到所有连接到中心的客户端。

  5. using 顶部添加以下 Program.cs 语句来解析 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. scripts 文件的 package.json 属性替换为以下代码:

    "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:当用户选择“发送”按钮并调用 函数时触发。

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

    SignalR 启用客户端和服务器之间的消息交换。 每个消息都有特定的名称。 例如,名为 messageReceived 的消息可以运行负责在消息区域显示新消息的逻辑。 可以通过 on 函数完成对特定消息的侦听。 可以侦听任意数量的消息名称。 还可以将参数传递到消息,例如所接收消息的作者姓名和内容。 客户端收到消息后,会创建一个新的 div 元素,并使用 textContent 将作者姓名和消息内容追加为子元素。 它被添加到显示消息的主要 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 文件:
      • 一个 <link> 标记,引用 wwwroot/main.<hash>.css 文件。 此标记放置在紧靠</head>结束标记之前。
      • 一个 <script> 标记,用于引用缩小后的 wwwroot/main.<hash>.js 文件。 此标记紧接在 </title> 结束标记之后。
  2. 选择调试>开始执行(不调试),在不附加调试器的情况下在浏览器中启动应用。 wwwroot/index.html 文件在 https://localhost:<port> 提供。

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

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

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

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

后续步骤

其他资源

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

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

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

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

Prerequisites

创建 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 包” 。 在“浏览”选项卡中,搜索 ,然后单击右侧的“安装”来安装包 。

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. scripts 文件的 package.json 属性替换为以下代码:

    "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 目录,以存储项目的客户端资产。

  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. using 文件的顶部添加以下 Startup.cs 语句来解析 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");
    
        const author = document.createElement("div");
        author.className = "message-author";
        author.textContent = username;
    
        const content = document.createElement("div");
        content.textContent = message;
    
        m.append(author, content);
    
        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 元素,并使用 textContent 将作者姓名和消息内容追加为子元素。 它被添加到显示消息的主要 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");
    
        const author = document.createElement("div");
        author.className = "message-author";
        author.textContent = username;
    
        const content = document.createElement("div");
        content.textContent = message;
    
        messages.append(author, content);
    
        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 方法需要客户端发送的数据。 在 SendAsync 上发起对 Clients.All 的调用。 接收的消息会发送到所有连接到中心的客户端。

测试应用

确认应用遵循以下步骤。

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

    npm run release
    

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

    Webpack 完成了以下任务:

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

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

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

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

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

其他资源

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

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

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

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

Prerequisites

创建 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. scripts 文件的 package.json 属性替换为以下代码:

    "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(["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. AddSignalR 中调用 Startup.ConfigureServices。 此操作会将 SignalR 服务添加到项目。

    services.AddSignalR();
    
  3. /hub 路由映射到 ChatHub Hub。 在 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");
    
        const author = document.createElement("div");
        author.className = "message-author";
        author.textContent = username;
    
        const content = document.createElement("div");
        content.textContent = message;
    
        m.append(author, content);
    
        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 元素,并使用 textContent 将作者姓名和消息内容追加为子元素。 新消息将添加到显示消息的主 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");
    
        const author = document.createElement("div");
        author.className = "message-author";
        author.textContent = username;
    
        const content = document.createElement("div");
        content.textContent = message;
    
        messageContainer.append(author, content);
    
        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 方法需要客户端发送的数据。 在 SendAsync 上发起对 Clients.All 的调用。 接收的消息会发送到所有连接到中心的客户端。

测试应用

确认应用遵循以下步骤。

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

    npm run release
    

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

    Webpack 完成了以下任务:

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

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

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

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

其他资源