先说结论:blazor
做Web开发绝对好东西。对于我个人来说,有两个重要的原因:
C#
;JavaScript
;在了解到blazor
居然可以使用c#
做Web开发且完全可以不用编写JavaScript
代码时,第一感受是,居然有这么懂自己的东西,果断尝鲜体验!
网上大概也了解了一下,基本上都说得过于官方和理论,不够简单直接,这里直接说调研的结论。
blazor
做web开发非常地高效(包括前后端),而且还是在资料不多的情况下(生态不太好,但绝对是好东西)。blazor
提供两种模式:WebAssembly
、Server
;Server
模式,WebAssembly
慢的受不了,一个简单的demo首次启动和每次刷新时都要将近 10秒钟时间,而相同demo项目Server
模式基本是瞬间显示的。WebAssembly
模式还有一个弊端:目前只有一个线程,容易出现 UI 界面卡死的问题。据说以后会改,谁知道啥时候呢。Server
模式就可以完全前后端一锅端了,非常的简单高效。笔者本身不是做web开发的,但是对全栈比较感兴趣,在前几年也接触和体验了一下SpringBoot
开发框架,参见:SpringBoot极简教程,因为个人不喜欢Java
更青睐c#
,所以体验之后弄清楚如何使用及流程也就作罢了,日后并没有再接触使用。但是有了之前的了解,至少有了web开发的基础知识储备,因此此次使用blazor
也会比较地快和顺利。这一次也会以一个具体的demo项目来做介绍:搭建一个局域网环境使用的APP管理平台
。
背景是这样的:在很久之前我们团队就想搭建一个这样的内部平台,但是我们都是客户端开发(非前端开发),对这块没有经验也不擅长,也就作罢了。我们是做安卓安全研究的,日常会遇到各种各样的样本,具有重复使用的价值,但是都散落在组内各人自己的电脑里,没有汇总起来,时间久了也容易找不到。如果能在内网搭建一个这样的样本管理平台就好了,遇到新样本的时候上传上去,需要的时候检索下载下来。我们也想过用共享文件夹
或ftp服务器
的形式搭建,但是这都是单纯的文件上传和下载,没有样本的基础信息,并没有使用开来。
但这个需求也不算是刚需,只能说有了更好。但是在遇到blazor
的时候,我就想大胆试一下,本身C#
的开发效率非常高,心想应该花不了多少时间,于是开始构思:
本着快速编程实现的想法,就想着如果能找到现成的代码改一改就行了,于是从blazor文件上传
的关键词找到了这个demo:File Upload with Blazor WebAssembly and ASP.NET Core Web API,GitHub地址:CodeMazeBlog/blazor-series at blazor-file-upload。工程里面有两个项目,分别是server(服务端)和client(客户端)。这是一个前后端分离的项目,结构比较清晰,如果打算上手学习blazor,是一个非常好的入门练手项目。这是一个管理产品(product)的demo,基本上和我的需求类似,所以就拿这个开刀修改起来。遇到了一些坑:
WebAssembly
模式,问题很严重,就是前面提的速度超级慢和界面卡死问题;SQL Server
的,这个遇到问题不好解决花了一些时间,网上MySQL
的资料多一些,好在及时换成了MySQL
;全程是以逆向工程的思路去实现的,遇到了非常多的问题,基本就是遇到一个解决一个,最终把项目跑起来了,效果图如下:
主要功能介绍:
http://[ip地址]:5000/apps
即可显示样本列表;后面的教程会以正向工程的顺序讲解,然后穿插一些实际修改demo项目的经验和问题。
SQL
命令,方面快捷;VisualStudio
默认使用的数据库是SQL Server
,先改为链接MySQL
的数据库,这里使用开源的MariaDB
。VisualStudio
链接MariaDB
数据库需要通过ODBC方式,因此需要安装ODBC Connector - MariaDB Knowledge Base,然后进行ODBC数据源配置:
ODBC数据源管理程序
(Win + R 输入命令:odbcad32
);MariaDB ODBC 3.0 Driver
,Name填:MariaDB,下一步;工具
- 连接到数据库
,选择:Microsoft ODBC 数据源
,刚刚的MariaDB
名称,就能在服务器资源管理器
里看到数据库链接了。参考:
MariaDB + Visual Studio 2017 环境下的 ODBC 入门开发
参考:How to Use MySQL Database With .Net Core And Entity Framework?
MariaDB
控制台,创建数据库:CREATE DATABASE appdb;
然后source apps.sql
;,apps.sql
内容如下(可以在Navicat
里设计,比较方便):/*
Navicat Premium Data Transfer
Source Server : 127.0.0.1
Source Server Type : MySQL
Source Server Version : 101100 (10.11.0-MariaDB)
Source Host : localhost:3306
Source Schema : appdb
Target Server Type : MySQL
Target Server Version : 101100 (10.11.0-MariaDB)
File Encoding : 65001
Date: 18/11/2022 00:16:42
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for apps
-- ----------------------------
DROP TABLE IF EXISTS `apps`;
CREATE TABLE `apps` (
`Id` bigint NOT NULL AUTO_INCREMENT,
`Name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`Package` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`Icon` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
`VersionName` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`VersionCode` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`ShellName` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`Tags` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`Memo` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`Operator` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`dbPath` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
`Url` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
`CreationDate` datetime(6) NOT NULL DEFAULT current_timestamp(6),
`ModifiedDate` datetime(6) NOT NULL DEFAULT current_timestamp(6) ON UPDATE CURRENT_TIMESTAMP(6),
`Md5` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',
`FileSize` int UNSIGNED NULL DEFAULT 0,
PRIMARY KEY (`Id`) USING BTREE,
UNIQUE INDEX `index_id`(`Id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 41 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of apps
-- ----------------------------
INSERT INTO `apps` VALUES (36, '测试App', 'com.test', 'https://appimg.dbankcdn.com/application/icon144/629336112d984dc1950f3992e0efdae0.png', '1.1', '2133', '360', NULL, '这个包有xxx的问题,版本发布的时候可以测一下', 'zhangsan', 'share\\app\\dytt8.apk', 'file:\\\\192.168.1.2\\share\\app\\dytt8.apk', '2022-11-17 23:44:00.872375', '2022-11-17 23:55:03.068345', NULL, 1973292);
INSERT INTO `apps` VALUES (38, '测试App', 'com.test', 'https://appimg.dbankcdn.com/application/icon144/629336112d984dc1950f3992e0efdae0.png', '1.1', '2133', '无', '加固 隐私', '这个包有xxx的问题,版本发布的时候可以测一下', 'zhangsan', 'share\\app\\184543_3.apk', 'file:\\\\192.168.1.2\\share\\app\\184543_3.apk', '2022-11-17 23:47:57.061125', '2022-11-18 00:10:47.843875', NULL, 46668230);
INSERT INTO `apps` VALUES (39, '测试App', 'com.test', 'https://appimg.dbankcdn.com/application/icon144/629336112d984dc1950f3992e0efdae0.png', '1.1', '2133', '无', NULL, '这个包有xxx的问题,版本发布的时候可以测一下', 'zhangsan', 'share\\app\\dytt8.apk', 'file:\\\\192.168.1.2\\share\\app\\dytt8.apk', '2022-11-18 00:02:13.713289', '2022-11-18 00:02:13.715084', NULL, 1973292);
SET FOREIGN_KEY_CHECKS = 1;
创建一个名为AppInfo
的Model:
public class AppInfo {
//[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
// App名称
//[Required(ErrorMessage = "Name is required field")]
[StringLength(maximumLength: 64)]
public string Name { get; set; }
// 包名
//[Required(ErrorMessage = "Package is required field")]
[StringLength(maximumLength: 100, MinimumLength = 3)]
public string Package { get; set; }
// 图标
public string Icon { get; set; }
// 版本号
[StringLength(maximumLength: 32)]
public string VersionName { get; set; }
[StringLength(maximumLength: 32)]
public string VersionCode { get; set; }
// 壳特征名称
[StringLength(maximumLength: 32)]
public string ShellName { get; set; } = "null";
// 标签
[StringLength(maximumLength: 64)]
public string Tags { get; set; }
// 备注
[StringLength(maximumLength: 128)]
public string Memo { get; set; }
// 操作人员
[StringLength(maximumLength: 32)]
public string Operator { get; set; }
// 数据库存储路径(相对路径)
public string dbPath { get; set; }
public string Url { get; set; }
public DateTime CreationDate { get; set; }
// 对应的是自动更新时间的数据库字段
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime ModifiedDate { get; set; }
public string Md5 { get; set; }
public long FileSize { get; set; }
}
服务端项目(BlazorDemo.Server
)添加三方库:Pomelo.EntityFrameworkCore.MySql
,然后修改Startup.cs
函数ConfigureServices
的代码为:
var connectionString = Configuration.GetConnectionString("sqlConnection");
services.AddDbContext(opt => opt.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
服务端appsettings.json
修改为:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore": "Information"
}
},
"ConnectionStrings": {
"sqlConnection": "SERVER=127.0.0.1;UID=root;PWD=123; database=appdb;"
},
"AllowedHosts": "*"
}
在VisualStudio的PM控制台下输入命令。
Add-Migration Initial
Update-Database
Remove-Migration
一共需要设置 4 个服务器,分别是:
部署前端的服务器;
部署API接口的服务器;
数据库服务器;
文件下载服务器;
其实前两个服务器可以合并为一个。因为我们是内网使用,所有的服务器最后都用了同一台Windows普通电脑来完成。
先说最简单的文件服务器,直接使用Python
的一句话命令就可以完成了。在电脑上选择一个目录作为存储样本文件使用,例如E:\share
,然后在该目录下创建一个批处理文件httpServer.bat
,内容:python -m http.server
,每次需要启动文件服务器的时候运行下httpServer.bat
即可。如果全部使用dotnet
技术栈,内容也可以是:
Rem python -m http.server
Rem dotnet tool install --global dotnet-serve
dotnet-serve -p 8000 -a 0.0.0.0
前后端可以分离开发,也可以混合开发。一开始借鉴的开源代码是前后端分离的,但是后来因为修改成了server模式,变成了混合模式。但是无论哪种都可以把前后端的项目部署在同一台服务器上,仅仅是开放的端口不一样罢了。最终配置如下:
服务端(BlazorDemo.Server
)的launchSettings.json
内容:
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:52497",
"sslPort": 0
}
},
"profiles": {
"BlazorDemo.Server": {
"commandName": "Project",
"launchBrowser": false,
"applicationUrl": "http://localhost:5010;http://*:5010",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
前端(BlazorDemo.Client
)的appsettings.json
内容:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
https的配置比较麻烦,而且第一次玩这个,不想太花时间折腾,本着先跑通的想法,先简单弄起来。如果是新创建项目的话,就不要勾选https
即可。
server项目里面Startup.cs
文件Configure
函数注释掉这两句调用:
//app.UseHttpsRedirection();
//app.UseAuthorization();
server项目launchSettings.json
去掉对https的支持,修改为:
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:52497",
"sslPort": 0
}
},
"profiles": {
"BlazorProducts.Server": {
"commandName": "Project",
"launchBrowser": false,
"applicationUrl": "http://localhost:5010;http://*:5010",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
client项目的launchSettings.json
去掉对https的支持,修改为:
{
"profiles": {
"BlazorProducts.Client": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5000;http://*:5000;",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
http://作为服务器的电脑的具体IP地址:5010
千万不要自己从零去写,没经验的话比较花时间,强烈推荐使用 Bootstrap Blazor - 企业级 UI 组件库 ,是个开源项目,GitHub地址见:BootstrapBlazor,总体使用比较方便,官网有详细使用教程和示例代码,直接拿来使用即可。文件上传功能就是直接用了这个 上传组件 Upload ,很方便,开发效率很高,使用的组件情况如下。
删除确认弹框:
文件上传:
private async Task OnClickToUpload(UploadFile file) {
await SaveToFile(file);
}
消息通知(消息提示):
[Inject]
[NotNull]
private MessageService? MessageService { get; set; }
await MessageService.Show(new MessageOption() {
Content = e.Message
});
这个可以借鉴 深入浅出Blazor webassembly之异常错误显示,也可以直接使用 全局异常 - Bootstrap Blazor - 企业级 UI 组件库。
两种方法:
dotnet
命令:dotnet publish -c Release
选择一台Windows电脑作为服务器使用,记录其 IP 地址;
安装:dotnet、MariaDB(账号密码弄个测试的简单一点的:root/123)、ODBC Connector - MariaDB Knowledge Base
配置好数据库;
选择一个文件夹作为http服务器,目录下放好httpServer.bat
,一键运行开启http服务。
为保证局域网的其他设备能够访问,客户端和服务端的appsettings.json
做好类似如下配置:
"AllowedHosts": "*",
"Urls": "http://*:5010",
配置ApkInfoTool
工具;
启动服务端:dotnet run
启动前端:dotnet run
其他设备访问地址:http://IP地址:5000
服务器项目里的接口函数一律要用anync Task
返回类型,否则容易出现错误:Cannot access a disposed context instance
,参考:Resolved- DBContext ObjectDisposedException:Cannot access a disposed of object
WebAssembly
的缺点:不支持多线程容易UI卡死;启动和刷新速度特别慢;
新建项目的时候勾选ASP.NET Core托管
自动生成的解决方案包含三个项目,服务端也一并包含;
回调函数用await InvokeAsync
,例如:
_timer = new Timer(async (sender) => {
await InvokeAsync(() => {
SearchCallback.InvokeAsync(SearchText);
});
_timer.Dispose();
}, null, 500, 0);
UI的响应函数类型要用async Task
,如果内部没有调用异步函数,可以在最后调用下:await Task.CompletedTask;
,例如:
public partial class Search {
private Timer _timer;
public string SearchText { get; set; }
[Parameter]
public EventCallback SearchCallback { get; set; }
private async Task OnSearch() {
if (_timer != null)
_timer.Dispose();
_timer = new Timer(async (sender) => {
await InvokeAsync(() => {
SearchCallback.InvokeAsync(SearchText);
});
_timer.Dispose();
}, null, 500, 0);
await Task.CompletedTask;
}
}
开源项目参考: