配合使用 ASP.NET Core SignalR 和 TypeScript 以及 Webpack
开发人员可以通过 Webpack 捆绑和生成 Web 应用的客户端资源。 本教程介绍在 ASP.NET Core SignalR Web 应用中使用 Webpack,该应用的客户端是使用 TypeScript 编写的。
在本教程中,你将了解:
- 为入门 ASP.NET Core SignalR 应用搭建基架
- 配置 SignalR TypeScript 客户端
- 使用 Webpack 配置生成管道
- 配置 SignalR 服务器
- 启用客户端和服务器之间的通信
系统必备
- 已安装“ASP.NET 和 Web 开发”工作负载的 Visual Studio 2017 版本 15.9 或更高版本
- .NET Core SDK 2.2 或更高版本
创建 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 文件:console复制npm init -y
- 将突出显示的属性添加到 package.json 文件: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 包。 从项目根执行以下命令:console复制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 将语义化版本控制范围运算符写到 package.json 的默认行为。 例如,使用 "webpack": "4.29.3" 而不是 "webpack": "^4.29.3"。 此选项防止意外升级到新的包版本。有关详细信息,请参阅官方 npm-install 文档。
- 将 package.json 文件的 scripts 属性替换为以下代码片段: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 Core CLI 的 publish 命令发布应用。
- 在项目根中创建名为 webpack.config.js 的文件,包含以下内容:JavaScript复制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。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: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 文件设计应用样式。
- 创建包含以下内容的 src/tsconfig.json:JSON复制{ "compilerOptions": { "target": "es5" } } 前面的代码配置 TypeScript 编译器,生成与 ECMAScript 5 兼容的 JavaScript。
- 创建包含以下内容的 src/index.ts:TypeScript复制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 和 UseStaticFiles 的调用。C#复制app.UseDefaultFiles(); app.UseStaticFiles(); 前面的代码允许服务器定位并提供 index.html 文件,无论用户输入完整 URL 还是 Web 应用的根 URL。
- 在 Startup.ConfigureServices 方法中调用 AddSignalR。 此操作会将 SignalR 服务添加到项目。C#复制services.AddSignalR();
- 将 /hub 路由映射到 ChatHub 中心。 在 Startup.Configure 方法的末尾添加以下行:C#复制app.UseSignalR(options => { options.MapHub<ChatHub>("/hub"); });
- 在项目根中创建名为 Hubs 的新目录。 目的是存储 SignalR 中心(在下一步中创建)。
- 创建包含以下代码的中心 Hubs/ChatHub.cs:C#复制using Microsoft.AspNetCore.SignalR; using System.Threading.Tasks; namespace SignalRWebPack.Hubs { public class ChatHub : Hub { } }
- 在 Startup.cs 文件顶部添加以下代码,解析 ChatHub 引用:C#复制using SignalRWebPack.Hubs;
启用客户端和服务器通信
应用当前显示一个发送消息的简单窗体。 尝试执行此操作时没有任何反应。 服务器正在侦听特定的路由,但是不涉及发送消息。
- 在项目根执行以下命令:console复制npm install @aspnet/signalr 前面的命令安装 SignalR TypeScript 客户端,它允许客户端向服务器发送消息。
- 将突出显示的代码添加到 src/index.ts 文件:TypeScript复制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.start().catch(err => document.write(err)); 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; }); 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 文件:TypeScript复制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.start().catch(err => document.write(err)); 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; }); 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 的消息已发送到服务器。 该消息包含用户名和文本框中的用户输入。 如果发送成功,会清空文本框。
- 将突出显示的方法添加到 ChatHub 类:C#复制using Microsoft.AspNetCore.SignalR; using System.Threading.Tasks; namespace SignalRWebPack.Hubs { public class ChatHub : Hub { public async Task NewMessage(string username, string message) { await Clients.All.SendAsync("messageReceived", username, message); } } } 服务器收到消息后,前面的代码会将这些消息播发到所有连接的用户。 没有必要使用泛型 on方法接收所有消息。 使用以消息名称命名的方法就可以了。在此示例中,TypeScript 客户端发送一条标识为 newMessage 的消息。 C# NewMessage 方法需要客户端发送的数据。 在 Clients.All 调用 SendAsync 方法。 接收的消息会发送到所有连接到中心的客户端。
测试应用
确认应用遵循以下步骤。
在 release 模式下运行 Webpack。 使用“包管理器控制台”窗口,在项目根中运行以下命令。 如果不在项目根中,请在输入该命令之前输入
cd SignalRWebPack
。consolenpm run release
此命令在运行应用时生成要提供的客户端资产。 资产位于 wwwroot 文件夹。
Webpack 完成了以下任务:
- 清除了 wwwroot 目录的内容。
- 将 TypeScript 转换为 JavaScript,该过程称为“转译”.
- 破坏生成的 JavaScript 以降低文件大小,该过程称为“缩小”。
- 将已处理的 JavaScript、CSS 和 HTML 文件从 src 复制到 wwwroot 目录。
- 将以下元素注入 wwwroot/index.html 文件:
- 一个引用 wwwroot/main.<hash>.css 文件的
<link>
标记。 此标记紧挨着</head>
结束标记之前。 - 一个引用缩小后的 wwwroot/main.<hash>.js 文件的
<script>
标记。 此标记紧挨着</body>
结束标记之前。
- 一个引用 wwwroot/main.<hash>.css 文件的
选择“调试” > “开始执行(不调试)”,在不附加调试器的情况下在浏览器中启动应用。 在
http://localhost:<port_number>
上提供 wwwroot/index.html 文件。打开另一个浏览器实例(任意浏览器)。 在地址栏中粘贴 URL。
选择一个浏览器,在“消息”文本框中键入任意内容,然后单击“发送”按钮。 两个页面上立即显示唯一的用户名和消息。