教程:使用 TypeScript 和 Webpack 开始使用 ASP.NET Core SignalR
注意
此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
本教程演示如何在 ASP.NET Core SignalR Web 应用中使用 Webpack 来捆绑和构建用 TypeScript 编写的客户端。 开发人员可以通过 Webpack 捆绑和生成 Web 应用的客户端资源。
在本教程中,你将了解如何执行以下操作:
- 创建 ASP.NET Core SignalR 应用
- 配置 SignalR 服务器
- 使用 Webpack 配置生成管道
- 配置 SignalR TypeScript 客户端
- 启用客户端和服务器之间的通信
先决条件
带有 ASP.NET 和 Web 开发工作负载的 Visual Studio 2022。
创建 ASP.NET Core Web 应用
默认情况下,Visual Studio 使用在安装目录中找到的 npm 版本。 配置 Visual Studio,在 PATH
环境变量中查找 npm:
启动 Visual Studio。 在“启动”窗口中,选择“继续但无需代码”。
导航到“工具”>“选项”>“项目和解决方案”>“Web 包管理”>“外部 Web 工具”。
在列表中选择
$(PATH)
项。 选择向上键将项移动列表中的第二个位置,然后选择“确定”:。
新建 ASP.NET Core Web 应用:
- 使用“文件”>“新建”>“项目”菜单选项,然后选择“ASP.NET Core 空”模板。 选择“下一页”。
- 将项目命名为
SignalRWebpack
并选择“创建”。 - 从“框架”下拉列表中选择“.NET 8.0(长期支持)”。 选择创建。
将 Microsoft.TypeScript.MSBuild NuGet 包添加到项目:
- 在“解决方案资源管理器”中,右键单击项目节点,然后选择“管理 NuGet 包”。 在“浏览”选项卡中,搜索
Microsoft.TypeScript.MSBuild
,然后选择右侧的“安装”来安装包。
Visual Studio 会将 NuGet 包添加到解决方案资源管理器中的“依赖项”节点下,从而在项目中启用 TypeScript 编译 。
配置服务器
在本部分,配置 ASP.NET Core Web 应用以发送和接收 SignalR 消息。
在
Program.cs
中,调用 AddSignalR:var builder = WebApplication.CreateBuilder(args); builder.Services.AddSignalR();
同样,在
Program.cs
中,调用 UseDefaultFiles 和 UseStaticFiles:var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles();
上述代码允许服务器查找和处理
index.html
文件。 无论用户输入完整 URL 还是 Web 应用的根 URL,都会提供该文件。在项目根目录
SignalRWebpack/
中,为 SignalR类创建名为Hubs
的新目录。创建包含以下代码的新文件
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 进行调用。
- 接收的消息会发送到所有连接到中心的客户端。
- TypeScript 客户端发送一条标识为
在
Program.cs
顶部添加以下using
语句来解析ChatHub
引用:using SignalRWebpack.Hubs;
在
Program.cs
中,将/hub
路由映射到ChatHub
中心。 将显示Hello World!
的代码替换为以下代码:app.MapHub<ChatHub>("/hub");
配置客户端
在本部分,创建一个 Node.js 项目,以使用 Webpack 将 TypeScript 转换为 JavaScript 并捆绑客户端资源,包括 HTML 和 CSS。
在项目根目录中运行以下命令,创建
package.json
文件:npm init -y
将突出显示的属性添加到
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
,防止下一步出现包安装警告。安装所需的 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 文档。
将
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 命令发布应用。
在项目根目录中创建名为
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。
在项目根目录
SignalRWebpack/
中,为客户端代码创建名为src
的新目录。将
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
的消息已发送到服务器。 该消息包含用户名和文本框中的用户输入。 如果发送成功,会清空文本框。
在项目根目录运行以下命令:
npm i @microsoft/signalr @types/node
上述的代码会安装:
- SignalR TypeScript 客户端,它允许客户端向服务器发送消息。
- 用于 node.js 的 TypeScript 类型定义,支持 Node.js 类型的编译时检查。
测试应用程序
确认应用遵循以下步骤:
在
release
模式下运行 Webpack。 使用“包管理器控制台”窗口,在项目根目录中运行以下命令。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>
标记。 此标记紧挨着</title>
结束标记之后。
- 一个引用
- 清除了
选择“调试”>“开始执行(不调试)”,在不附加调试器的情况下在浏览器中启动应用 。 在
https://localhost:<port>
上提供wwwroot/index.html
文件。如果存在编译错误,请尝试关闭并重新打开解决方案。
打开另一个浏览器实例(任何浏览器),然后在地址栏中粘贴该 URL。
选择一个浏览器,在“消息”文本框中键入任意内容,然后选择“发送”按钮。 两个页面上立即显示唯一的用户名和消息。
后续步骤
其他资源
本教程演示如何在 ASP.NET Core SignalR Web 应用中使用 Webpack 来捆绑和构建用 TypeScript 编写的客户端。 开发人员可以通过 Webpack 捆绑和生成 Web 应用的客户端资源。
在本教程中,你将了解如何执行以下操作:
- 创建 ASP.NET Core SignalR 应用
- 配置 SignalR 服务器
- 使用 Webpack 配置生成管道
- 配置 SignalR TypeScript 客户端
- 启用客户端和服务器之间的通信
先决条件
带有 ASP.NET 和 Web 开发工作负载的 Visual Studio 2022。
创建 ASP.NET Core Web 应用
默认情况下,Visual Studio 使用在安装目录中找到的 npm 版本。 配置 Visual Studio,在 PATH
环境变量中查找 npm:
启动 Visual Studio。 在“启动”窗口中,选择“继续但无需代码”。
导航到“工具”>“选项”>“项目和解决方案”>“Web 包管理”>“外部 Web 工具”。
在列表中选择
$(PATH)
项。 选择向上键将项移动列表中的第二个位置,然后选择“确定”:。
新建 ASP.NET Core Web 应用:
- 使用“文件”>“新建”>“项目”菜单选项,然后选择“ASP.NET Core 空”模板。 选择“下一页”。
- 将项目命名为
SignalRWebpack
并选择“创建”。 - 从“框架”下拉列表中,选择
.NET 7.0 (Standard Term Support)
。 选择“创建”。
将 Microsoft.TypeScript.MSBuild NuGet 包添加到项目:
- 在“解决方案资源管理器”中,右键单击项目节点,然后选择“管理 NuGet 包”。 在“浏览”选项卡中,搜索
Microsoft.TypeScript.MSBuild
,然后选择右侧的“安装”来安装包。
Visual Studio 会将 NuGet 包添加到解决方案资源管理器中的“依赖项”节点下,从而在项目中启用 TypeScript 编译 。
配置服务器
在本部分,配置 ASP.NET Core Web 应用以发送和接收 SignalR 消息。
在
Program.cs
中,调用 AddSignalR:var builder = WebApplication.CreateBuilder(args); builder.Services.AddSignalR();
同样,在
Program.cs
中,调用 UseDefaultFiles 和 UseStaticFiles:var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles();
上述代码允许服务器查找和处理
index.html
文件。 无论用户输入完整 URL 还是 Web 应用的根 URL,都会提供该文件。在项目根目录
SignalRWebpack/
中,为 SignalR类创建名为Hubs
的新目录。创建包含以下代码的新文件
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 进行调用。
- 接收的消息会发送到所有连接到中心的客户端。
- TypeScript 客户端发送一条标识为
在
Program.cs
顶部添加以下using
语句来解析ChatHub
引用:using SignalRWebpack.Hubs;
在
Program.cs
中,将/hub
路由映射到ChatHub
中心。 将显示Hello World!
的代码替换为以下代码:app.MapHub<ChatHub>("/hub");
配置客户端
在本部分,创建一个 Node.js 项目,以使用 Webpack 将 TypeScript 转换为 JavaScript 并捆绑客户端资源,包括 HTML 和 CSS。
在项目根目录中运行以下命令,创建
package.json
文件:npm init -y
将突出显示的属性添加到
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
,防止下一步出现包安装警告。安装所需的 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 文档。
将
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 命令发布应用。
在项目根目录中创建名为
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。
将
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
的消息已发送到服务器。 该消息包含用户名和文本框中的用户输入。 如果发送成功,会清空文本框。
在项目根目录运行以下命令:
npm i @microsoft/signalr @types/node
上述的代码会安装:
- SignalR TypeScript 客户端,它允许客户端向服务器发送消息。
- 用于 node.js 的 TypeScript 类型定义,支持 Node.js 类型的编译时检查。
测试应用程序
确认应用遵循以下步骤:
在
release
模式下运行 Webpack。 使用“包管理器控制台”窗口,在项目根目录中运行以下命令。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>
标记。 此标记紧挨着</title>
结束标记之后。
- 一个引用
- 清除了
选择“调试”>“开始执行(不调试)”,在不附加调试器的情况下在浏览器中启动应用 。 在
https://localhost:<port>
上提供wwwroot/index.html
文件。如果存在编译错误,请尝试关闭并重新打开解决方案。
打开另一个浏览器实例(任何浏览器),然后在地址栏中粘贴该 URL。
选择一个浏览器,在“消息”文本框中键入任意内容,然后选择“发送”按钮。 两个页面上立即显示唯一的用户名和消息。
后续步骤
其他资源
本教程演示如何在 ASP.NET Core SignalR Web 应用中使用 Webpack 来捆绑和构建用 TypeScript 编写的客户端。 开发人员可以通过 Webpack 捆绑和生成 Web 应用的客户端资源。
在本教程中,你将了解如何执行以下操作:
- 创建 ASP.NET Core SignalR 应用
- 配置 SignalR 服务器
- 使用 Webpack 配置生成管道
- 配置 SignalR TypeScript 客户端
- 启用客户端和服务器之间的通信
先决条件
- 带有 ASP.NET 和 Web 开发工作负载的 Visual Studio 2022。
- .NET 6.0 SDK
创建 ASP.NET Core Web 应用
默认情况下,Visual Studio 使用在安装目录中找到的 npm 版本。 配置 Visual Studio,在 PATH
环境变量中查找 npm:
启动 Visual Studio。 在“启动”窗口中,选择“继续但无需代码”。
导航到“工具”>“选项”>“项目和解决方案”>“Web 包管理”>“外部 Web 工具”。
在列表中选择
$(PATH)
项。 选择向上键将项移动列表中的第二个位置,然后选择“确定”:。
新建 ASP.NET Core Web 应用:
- 使用“文件”>“新建”>“项目”菜单选项,然后选择“ASP.NET Core 空”模板。 选择“下一页”。
- 将项目命名为
SignalRWebpack
并选择“创建”。 - 从“框架”下拉列表中,选择
.NET 6.0 (Long Term Support)
。 选择“创建”。
将 Microsoft.TypeScript.MSBuild NuGet 包添加到项目:
- 在“解决方案资源管理器”中,右键单击项目节点,然后选择“管理 NuGet 包”。 在“浏览”选项卡中,搜索
Microsoft.TypeScript.MSBuild
,然后选择右侧的“安装”来安装包。
Visual Studio 会将 NuGet 包添加到解决方案资源管理器中的“依赖项”节点下,从而在项目中启用 TypeScript 编译 。
配置服务器
在本部分,配置 ASP.NET Core Web 应用以发送和接收 SignalR 消息。
在
Program.cs
中,调用 AddSignalR:var builder = WebApplication.CreateBuilder(args); builder.Services.AddSignalR();
同样,在
Program.cs
中,调用 UseDefaultFiles 和 UseStaticFiles:var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles();
上述代码允许服务器查找和处理
index.html
文件。 无论用户输入完整 URL 还是 Web 应用的根 URL,都会提供该文件。在项目根目录
SignalRWebpack/
中,为 SignalR类创建名为Hubs
的新目录。创建包含以下代码的新文件
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 进行调用。 接收的消息会发送到所有连接到中心的客户端。在
Program.cs
顶部添加以下using
语句来解析ChatHub
引用:using SignalRWebpack.Hubs;
在
Program.cs
中,将/hub
路由映射到ChatHub
中心。 将显示Hello World!
的代码替换为以下代码:app.MapHub<ChatHub>("/hub");
配置客户端
在本部分,创建一个 Node.js 项目,以使用 Webpack 将 TypeScript 转换为 JavaScript 并捆绑客户端资源,包括 HTML 和 CSS。
在项目根目录中运行以下命令,创建
package.json
文件:npm init -y
将突出显示的属性添加到
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
,防止下一步出现包安装警告。安装所需的 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 文档。
将
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 命令发布应用。
在项目根目录中创建名为
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。
将
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
的消息已发送到服务器。 该消息包含用户名和文本框中的用户输入。 如果发送成功,会清空文本框。在项目根目录运行以下命令:
npm i @microsoft/signalr @types/node
上述的代码会安装:
- SignalR TypeScript 客户端,它允许客户端向服务器发送消息。
- 用于 node.js 的 TypeScript 类型定义,支持 Node.js 类型的编译时检查。
测试应用程序
确认应用遵循以下步骤:
在
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>
标记。 此标记紧挨着</title>
结束标记之后。
- 一个引用
- 清除了
选择“调试”>“开始执行(不调试)”,在不附加调试器的情况下在浏览器中启动应用 。 在
https://localhost:<port>
上提供wwwroot/index.html
文件。如果遇到编译错误,请尝试关闭并重新打开解决方案。
打开另一个浏览器实例(任何浏览器),然后在地址栏中粘贴该 URL。
选择一个浏览器,在“消息”文本框中键入任意内容,然后选择“发送”按钮。 两个页面上立即显示唯一的用户名和消息。
后续步骤
其他资源
本教程演示如何在 ASP.NET Core SignalR Web 应用中使用 Webpack 来捆绑和构建用 TypeScript 编写的客户端。 开发人员可以通过 Webpack 捆绑和生成 Web 应用的客户端资源。
在本教程中,你将了解:
- 为入门 ASP.NET Core SignalR 应用搭建基架
- 配置 SignalR TypeScript 客户端
- 使用 Webpack 配置生成管道
- 配置 SignalR 服务器
- 启用客户端和服务器之间的通信
先决条件
- Visual Studio 2019 与 ASP.NET 和 Web 开发工作负载
- .NET Core SDK 3.0 或更高版本
- 带 npm 的 Node.js
创建 ASP.NET Core Web 应用
配置 Visual Studio,在 PATH 环境变量中查找 npm。 默认情况下,Visual Studio 使用在安装目录中找到的 npm 版本。 在 Visual Studio 中按照以下说明执行操作:
启动 Visual Studio。 在“启动”窗口中,选择“继续但无需代码”。
导航到“工具”>“选项”>“项目和解决方案”>“Web 包管理”>“外部 Web 工具”。
在列表中选择 $(PATH) 项。 选择向上键将项移动列表中的第二个位置,然后选择“确定”。
。
Visual Studio 配置完成。
- 使用“文件”>“新建”>“项目”菜单选项,然后选择“ASP.NET Core Web 应用程序”模板 。 选择“下一步”。
- 将项目命名为 *SignalRWebPac`` 并选择“创建”。
- 从目标框架下拉列表选择 .NET Core 并从框架选择器下拉列表选择 ASP.NET Core 3.1 。 选择“空白”模板并选择“创建” 。
将 Microsoft.TypeScript.MSBuild
包添加到项目:
- 在“解决方案资源管理器”(右侧窗格)中,右键单击项目节点,然后选择“管理 NuGet 包” 。 在“浏览”选项卡中,搜索
Microsoft.TypeScript.MSBuild
,然后单击右侧的“安装”来安装包 。
Visual Studio 会将 NuGet 包添加到解决方案资源管理器中的“依赖项”节点下,从而在项目中启用 TypeScript 编译 。
配置 Webpack 和 TypeScript
以下步骤配置 TypeScript 到 JavaScript 的转换和客户端资源的捆绑。
在项目根目录中运行以下命令,创建
package.json
文件:npm init -y
将突出显示的属性添加到
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
,防止下一步出现包安装警告。安装所需的 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 文档。
- 每个包名称中
将
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 命令发布应用。
在项目根目录中创建名为
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。
在项目根目录中创建新的 src 目录,以存储项目的客户端资产。
使用以下标记创建
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 定义主页的样板标记。
创建新的 src/css 目录。 目的是存储项目的
.css
文件。使用以下 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
文件设计应用样式。使用以下 JSON 创建
src/tsconfig.json
:{ "compilerOptions": { "target": "es5" } }
前面的代码配置 TypeScript 编译器,生成与 ECMAScript 5 兼容的 JavaScript。
使用以下代码创建
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
函数。
配置应用
在
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,都会提供该文件。在
Startup.Configure
的末尾,将 /hub 路由映射到ChatHub
中心。 将显示 Hello World! 的代码替换为以下行:app.UseEndpoints(endpoints => { endpoints.MapHub<ChatHub>("/hub"); });
在
Startup.ConfigureServices
中,调用 AddSignalR。services.AddSignalR();
在项目根目录 SignalRWebPack/ 中创建名为 Hubs 的新目录,以存储 SignalR 中心 。
使用以下代码创建中心
Hubs/ChatHub.cs
:using Microsoft.AspNetCore.SignalR; using System.Threading.Tasks; namespace SignalRWebPack.Hubs { public class ChatHub : Hub { } }
在
Startup.cs
文件的顶部添加以下using
语句来解析ChatHub
引用:using SignalRWebPack.Hubs;
启用客户端和服务器通信
应用目前显示用于发送消息的基本窗体,但尚不能正常工作。 服务器正在侦听特定的路由,但是不涉及发送消息。
在项目根目录运行以下命令:
npm i @microsoft/signalr @types/node
上述的代码会安装:
- SignalR TypeScript 客户端,它允许客户端向服务器发送消息。
- 用于 node.js 的 TypeScript 类型定义,支持 Node.js 类型的编译时检查。
将突出显示的代码添加到
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
元素。客户端可以接收消息后,将它配置为发送消息。 将突出显示的代码添加到
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
的消息已发送到服务器。 该消息包含用户名和文本框中的用户输入。 如果发送成功,会清空文本框。将
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 进行调用。 接收的消息会发送到所有连接到中心的客户端。
测试应用
确认应用遵循以下步骤。
在 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>
标记。 此标记紧挨着</title>
结束标记之后。
- 一个引用
- 清除了
选择“调试”>“开始执行(不调试)”,在不附加调试器的情况下在浏览器中启动应用 。 在
http://localhost:<port_number>
上提供wwwroot/index.html
文件。如果遇到编译错误,请尝试关闭并重新打开解决方案。
打开另一个浏览器实例(任意浏览器)。 在地址栏中粘贴 URL。
选择一个浏览器,在“消息”文本框中键入任意内容,然后选择“发送”按钮。 两个页面上立即显示唯一的用户名和消息。
其他资源
本教程演示如何在 ASP.NET Core SignalR Web 应用中使用 Webpack 来捆绑和构建用 TypeScript 编写的客户端。 开发人员可以通过 Webpack 捆绑和生成 Web 应用的客户端资源。
在本教程中,你将了解:
- 为入门 ASP.NET Core SignalR 应用搭建基架
- 配置 SignalR TypeScript 客户端
- 使用 Webpack 配置生成管道
- 配置 SignalR 服务器
- 启用客户端和服务器之间的通信
先决条件
- Visual Studio 2019 与 ASP.NET 和 Web 开发工作负载
- .NET Core SDK 2.2 或更高版本
- 带 npm 的 Node.js
创建 ASP.NET Core Web 应用
配置 Visual Studio,在 PATH 环境变量中查找 npm。 默认情况下,Visual Studio 使用在安装目录中找到的 npm 版本。 在 Visual Studio 中按照以下说明执行操作:
导航到“工具”>“选项”>“项目和解决方案”>“Web 包管理”>“外部 Web 工具”。
在列表中选择 $(PATH) 项。 选择向上键将项移动列表第二个位置。
已完成 Visual Studio 配置。 可以开始创建项目了。
- 使用“文件”>“新建”>“项目”菜单选项,然后选择“ASP.NET Core Web 应用程序”模板 。
- 将项目命名为 *SignalRWebPack` 并选择“创建”。
- 从目标框架下拉列表选择 .NET Core 并从框架选择器下拉列表选择 ASP.NET Core 2.2 。 选择“空白”模板并选择“创建” 。
配置 Webpack 和 TypeScript
以下步骤配置 TypeScript 到 JavaScript 的转换和客户端资源的捆绑。
在项目根目录中运行以下命令,创建
package.json
文件:npm init -y
将突出显示的属性添加到
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
,防止下一步出现包安装警告。安装所需的 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 文档。
- 每个包名称中
将
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 命令发布应用。
在项目根目录中创建名为
*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。
在项目根目录中创建新的 src 目录,以存储项目的客户端资产。
使用以下标记创建
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 定义主页的样板标记。
创建新的 src/css 目录。 目的是存储项目的
.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
文件设计应用样式。使用以下 JSON 创建
src/tsconfig.json
:{ "compilerOptions": { "target": "es5" } }
前面的代码配置 TypeScript 编译器,生成与 ECMAScript 5 兼容的 JavaScript。
使用以下代码创建
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 应用
Startup.Configure
方法中提供的代码显示 Hello World!。 将app.Run
方法调用替换为对 UseDefaultFiles(IApplicationBuilder) 和 UseStaticFiles(IApplicationBuilder) 的调用。app.UseDefaultFiles(); app.UseStaticFiles();
前面的代码允许服务器定位并提供
index.html
文件,无论用户输入完整 URL 还是 Web 应用的根 URL。在
Startup.ConfigureServices
中调用 AddSignalR。 此操作会将 SignalR 服务添加到项目。services.AddSignalR();
将 /hub 路由映射到
ChatHub
中心。 在Startup.Configure
的末尾添加以下行:app.UseSignalR(options => { options.MapHub<ChatHub>("/hub"); });
在项目根中创建名为 Hubs 的新目录。 目的是存储 SignalR 中心(在下一步中创建)。
使用以下代码创建中心
Hubs/ChatHub.cs
:using Microsoft.AspNetCore.SignalR; using System.Threading.Tasks; namespace SignalRWebPack.Hubs { public class ChatHub : Hub { } }
在
Startup.cs
文件的顶部添加以下代码来解析ChatHub
引用:using SignalRWebPack.Hubs;
启用客户端和服务器通信
应用当前显示一个发送消息的简单窗体。 尝试执行此操作时没有任何反应。 服务器正在侦听特定的路由,但是不涉及发送消息。
在项目根目录运行以下命令:
npm install @aspnet/signalr
前面的命令将安装 SignalR TypeScript 客户端,它允许客户端向服务器发送消息。
将突出显示的代码添加到
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
元素中。客户端可以接收消息后,将它配置为发送消息。 将突出显示的代码添加到
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
的消息已发送到服务器。 该消息包含用户名和文本框中的用户输入。 如果发送成功,会清空文本框。将
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 进行调用。 接收的消息会发送到所有连接到中心的客户端。
测试应用
确认应用遵循以下步骤。
在 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>
标记。 此标记紧挨着</title>
结束标记之后。
- 一个引用
- 清除了
选择“调试”>“开始执行(不调试)”,在不附加调试器的情况下在浏览器中启动应用 。 在
http://localhost:<port_number>
上提供wwwroot/index.html
文件。打开另一个浏览器实例(任意浏览器)。 在地址栏中粘贴 URL。
选择一个浏览器,在“消息”文本框中键入任意内容,然后选择“发送”按钮。 两个页面上立即显示唯一的用户名和消息。
其他资源
反馈
https://aka.ms/ContentUserFeedback。
即将发布:在整个 2024 年,我们将逐步淘汰作为内容反馈机制的“GitHub 问题”,并将其取代为新的反馈系统。 有关详细信息,请参阅:提交和查看相关反馈