本文从比较高的位置俯瞰一下 .NET Blazor 技术方向,主要是给大家介绍一下“什么是 Blazor”

文章后半部分会给出一个 Blazor 中的 Hello World 示例

1. 概览

1.1 什么是 Blazor?

Blazor 是一个.NET 全家桶里的一个 Web UI 开发框架,简单来说,你可以把它理解为 React/Angular/Vue 的替代品:这是一个用于开发 Web UI 的框架。

从框架使用者的角度来说,最直接的使用体验,就是使用 C#替换掉了 JavaScript 或 TypeScript

那除了使用 C#而非 JS这一点外, Blazor 和如今流行的三大前端框架 React/Angular/Vue 相比较,还有什么差异呢?

在聊这些差异之前,我们需要先明确一个基础知识点:UI = 视觉 + 交互。这个等式并不严谨,但你应当明白我说的是什么意思,所有前端 UI 框架其实都在做两件核心的事

  1. 指挥 DOM 去渲染视觉样式(调用 DOM API)。这其中框架开发者不可避免的要去学习 HTML 和 CSS 的相关知识
  2. 响应用户输入,然后完成一些交互计算,再根据交互计算去高效的更新视觉样式。而交互计算又分为两部分
    1. 直接在浏览器中,用 JavaScript 做的相关计算
    2. 浏览器没法算的,则是通过通信方式(HTTP 请求),去调用服务端 API

简化一下,其实就是做了三件事:

  1. 调用 DOM API
  2. 使用浏览器能运行的代码,来完成交互计算
  3. 使用 HTTP 协议调用服务端 API

Blazor 其实也是在做这三件事,但 Blazor 有两种做法:

方法一 : Blazor WebAssembly

  1. 把 C#编译成 WebAssembly,然后放在浏览器上去执行:去调用 DOM API 渲染视觉,以及完成交互计算
  2. 依然使用 HTTP 协议调用服务端 API

这种做法其实和 React/Angular/Vue 的做法是一样的,唯一的区别就是为了使用 C#,使用了 WebAssembly 来做中间层。如下图所示:

代价也是有的,就是 WebAssembly 虽然干其它事很牛逼,但调用 DOM API 时,只能间接的去使用 interop 方式去调用 JavaScript 的 DOM API。并且初次载入页面时,慢到令人窒息。

方法二 : Blazor Server

除了第一次访问,后续访问均摒弃 HTTP 协议,使用 WebSocket 在服务端和浏览器之间维持一个长连接,浏览器上的代码只做一件事:根据 WebSocket 消息来调用 DOM API。

  1. 当需要更新 DOM 时,服务端向浏览器发送二进制消息。浏览器按指令调用 DOM API 更新视觉就行
  2. 当有用户输入,需要进行交互计算的时候,浏览器不算,而是把用户输入以二进制发送给服务端,服务端来做交互计算,然后回传计算结果,浏览器只管更新 DOM
  3. 交互计算代码需要访问业务逻辑代码时,就不存在所谓的“调用 API”了,而是直接在服务端调用函数即可

如下图所示:

这种做法非常激进,你以为你点开的网页是网页,是 B/S 架构的,其实是一个需要维持长连接的 C/S 构架的应用:浏览器只是一个样式渲染器而已,用户本质上使用的是一个远程应用,所有计算,无论大小,都运行在服务端。

总结

  1. 边缘 Web 开发技术,不流行,缺点相当难以忽视
  2. 优点也相当难以忽视:使用 C#,而不是 JavaScript。配合.NET 服务端技术,一人全栈相当舒服。

    你要认识到,当我们说不使用 JavaScript时,不光是抛开了 JS 或 TS,更重要的是抛开了传统前端的所有工具链: node, webpack, babel 等等等等,这些东西对于一个专业的前端开发工程师来说不算什么,但对于一个只是想攒一个小应用,小网站,小博客的个人来说,这些玩意是巨大的心智负担。
  3. 但你依然无法逃脱 HTML 和 CSS
  4. 目前没有比较好的,有说服力,广为人知的商业互联网产品使用 Blazor 技术栈

1.2 Blazor 中的组件:Razor Components

Blazor 其它 UI 框架一样,也是分治于“组件”的思想,一个组件可以是一个大到页面,小到对话框、按钮、输入表单这样的 UI 元素。

上面介绍了 Blazor 有两种工作模式,Blazor 中的组件比较神奇的点在于:组件和具体的工作方式是无关的。配套的工具链会帮你去处理两种工作方式之间的差异,但对于框架开发者来说,组件就是组件,开发组件的时候不需要考虑是 Blazor Server 还是 Blazor WebAssembly。

从代码角度来看,React 中的组件是一个 JS 函数,而 Blazor 中的组件则是一个 C#类。在组件内部,要写上下面的东西:

  1. 组件内部要写上 UI 渲染逻辑
  2. 组件应当在必要的时候处理用户的输入,即处理“事件”
  3. 组件应当是可嵌套的,可复用的
  4. 组件可以被打成一个类库,或者打成一个 NuGet 包分发在互联网平台上,供他人“借用”

虽然本质上,每个 Blazor 组件都是一个 C#的类,但如果全然用public class xxxx去写这样的类,是非常不直观的,于是 Blazor 的解决方式有点类似于上古时期的 JSP 技术:给你一套四不像的标记语言,让你去写一种看起来像是 HTML 但又不是 HTML 的东西,然后这个东西再被工具链在编译的时候转换成*.cs文件,再编译成一个.NET 类

这种四不像的文件以*.razor结尾,在这种文件里,你可以写 HTML,可以写内嵌的 CSS,然后还可以用一套别扭的语法去再写一些 C#代码,这个玩意大概长下面这样:

<div class="card" style="width:22rem">
<div class="card-body">
<h3 class="card-title">@Title</h3>
<p class="card-text">@ChildContent</p>
<button @onclick="OnYes">Yes!</button>
</div>
</div> @code {
[Parameter]
public RenderFragment ChildContent { get; set; } [Parameter]
public string Title { get; set; } private void OnYes()
{
Console.WriteLine("Write to the console in C#! 'Yes' button selected.");
}
}

在上面的代码示例中,你可以理解为,整个@code {xxx}包起来的区域,相当于你写了一个partial class,你在里面可以定义属性,定义方法,定义字段等。这样理解起来,OnYes就是一个方法,而在上面的类 HTML 代码中,使用了@onclick="OnYes"这种奇怪的写法,将一个按钮的点击回调,绑定到了一个 C#方法上,而且用@Title@ChildContent这样奇怪的语法,将类中的属性引用到了 HTML 中。

那么虽然目前我们并不明白所有有关*.razor的书写规范以及工作原理,但我们已经能大致猜出来它的工作流程了:在运行时,这个特殊的类会被实例化,它内部的两样东西:

  1. 它要控制页面如何渲染,或者换句话说,这个类内部最终还是要输出一种能被浏览器直接渲染的东西的,我们暂时可以认为这个类内部有一个方法可以输出 HTML+CSS 代码(但实际并不是这样
  2. 它本身可以拥有一些属性与方法来表达业务逻辑,这些属性与方法可以在代码中用@属性@onclick=方法这种语法被我们使用

假如上面这个代码文件叫做Dialog.razor的话,按照组件应当被复用嵌套的要求,这个组件可以被嵌套在另外一个组件里,比如我们下面将它嵌套在一个叫Index.razor的文件中:

@page "/"

<h1>Hello, world!</h1>

<p>
Welcome to your new app.
</p> <Dialog Title="Learn More">
Do you want to <i>learn more</i> about Blazor?
</Dialog>

这时,我们就可以理解为,在最终的编译出来的.NET 类中,当Index类要输出渲染时,它会实例化一个Dialog类的实例,并且将dialog.Title的值设置为Learn More, 将diaglog.ChildContent的值设置为Do you want to <i>learn more</i> about Blazor?,然后在必要的位置将两个diaglog渲染出来

如果 DOM 是渲染的目标的话,那么这个Index的实例,会被渲染成对应的一颗 DOM 树,而其中的一颗子树,其实是Dialog的样子

2. Hello Blazor

这一节,我们将创建两个项目,分别先快速体验一下 Blazor 技术。到今天为止(2022-03-15),.NET 的版本号已经到 6 了,但我们这里依然使用 5.0 版本做演示,原因嘛,没有原因,就是倔强。

另外,显然,你需要在电脑上安装 .Net Core SDK,建议你先安装个版本号为 5 开头的与本文同步。。不要紧张,.NetCore SDK 可以同时安装 N 多个版本,可以安全共存,最终,在命令行中输入dotnet --list-sdks时,你要能找到一个以 5 开头的版本,如下:

2.1 Hello Blazor Server

命令: dotnet new blazorserver -o HelloBlazorServer --framework 'net5.0'

默认创建出了这样的一个项目:

这其实是一个非常典型的 ASP 项目,如果你点开Program.csStartup.cs看的话,它与普通的 ASP 项目基本没什么区别,核心的点在Start.cs中的ConfigureServicesConfigure两个方法中:

再介绍一下其它目录:

  1. Program.cs, Startup.cs : 经典的 ASP .Net Core 项目入口
  2. appsettings.jsonappsettings.Development.json : 项目配置文件
  3. HelloBlazorServer.csproj : 项目编译脚本
  4. _Imports.razor : 相当于所有*.razor文件的公共头,点开看,里面全是@using指令
  5. App.razor : 这是整个项目的 UI 根组件,它内部其实只写了一个<Router>元素,逻辑是:如果路由匹配成功了,则去把匹配 UI 组件包在MainLayout组件里面进行渲染,否则将一个提示字符串包在MainLayout组件里渲染出来

  6. wwwroot 目录:里面包含了静态资源,与普通 的 ASP .Net Core 项目一样。需要注意的是,默认创建的项目中,这里面给你自带了bootstrap样式库和open-iconic图标库
  7. Properties 目录:与经典的 ASP 项目一样,里面一个launchSettings.json描述了一些配置项
  8. Data 目录:这里面包含两个类,一个是默认自带的WeatherForecast.cs,是一个 Model 类,另外一个是WeatherForecastService.cs类,里面写着真正的“业务逻辑”,并且这个SeatherForecastService还被在Startup.cs中注册成了一个全局单例的服务
  9. Shared目录:这里面包含了一些自带的 Blazor 组件,这些组件都是被广泛使用的公共组件,所以被放在这个目录中。你会注意到MainLayoutNavMenu组件除了本身的*.razor定义文件,还有单独的描述样式的css文件
  10. Pages目录:这里就包含了所有的页面 Blazor 组件,如果你要开发一个网站的话,这里应该是你最常工作的地方。这个目录中除了Counter, FetchDataIndex三个 Blazor 组件,还有_Host.cshtml, Error.cshtml, Error.cshtml.cs三个文件,你可以暂时理解为,*.cshtml*.cshtml.cs是 Blazor 组件的传统写法

我们再打包一下,观察一个 Blazor Server App 在打包后会变成什么,我们将当前这个项目打包到 Linux 平台上,然后观察一下输出:

可以看到,整个应用其实被打包成了一个典型的 ASP 应用:你只需要将这个目录部署在某台 Linux 机器上,然后运行./HelloBlazorServer就行了。。你所定义的所有 Blazor 组件都被编译进了HelloBlazorServer.dll这个 Assembly 中

接下来我们再来验证一下东西:我们要来验证,在 Blazor Server App 中,所有的计算都是在服务端完成的。默认创建的这个 App 有个页面,在本地启动的话地址是https://localhost:5001/counter,这个页面上有一个计数器,按一下按钮,计数器就+1

通过查看Pages/Counter.razor我们得知,这个计数器中的数值,其实就是Counter类中的一个属性,每次点击按钮,其实调用的是Counter实例下的IncrementCount()方法

按照上面我们介绍 Blazor Server App 运行模式的说法,那么每次客户端用户在浏览器中点击这个按钮时,其实都会发送一个网络消息给服务端,然后服务端去执行counter.IncrementCount(),执行结束后,服务端的counter实例去分析 UI 上有哪些地方需要更新,然后再通过网络消息,指挥浏览器更新页面上渲染的数字。

如果这套理论是正确的,那意味着每次用户点击这个按钮,背后都会有数据在浏览器与服务端之间来回传递。我们用dotnet run命令将整个项目在本地启动起来,然后打开浏览器,按 F12 打开调试控制台,切换到网络选项卡,然后刷新地址去访问这个计数器页面,我们会看到一个奇怪的网络请求:

这其实是一个全双工的 WebSocket 连接,点进去,切换到消息选项卡,然后随着你每一次点击页面上的按钮,你都会看到有二进制数据在背后一来一回

2.2 Hello Blazor WebAssembly

命令: dotnet new blazorwasm -o HelloBlazorWASM --framework 'net5.0'

创建出这样一个项目:

这个项目,就和 ASP 没关系了,我们打开它的Program.cs去看一眼,如果你对.NET 稍微熟悉的话,你就会立即明白:这个东西,不正常,这个东西,不寻常:

namespace HelloBlazorWASM
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); await builder.Build().RunAsync();
}
}
}

这个程序入口创建了一个WebAssemblyHost,然后把这个 Host 运行了起来。而其实呢,这个WebAssemblyHost就是个静态的 WebServer 而已,跟你用create-react-app创建一个 react 项目,然后npm start起来的原理是差不多一样的。

另外,在Blazor Server的例子中,FetchData页面的数据,是通过在服务端随机生成的,服务端的逻辑写在WeatherForecastService.cs中。而现在在Blazor WebAssembly中,有两个显著的区别

  1. 没有了Data目录,也没有了WeatherForecast.csWeatherForecastService.cs
  2. FetchData.razor
    1. 就地定义了结构体WeatherForecast
    2. 数据是从一个静态的,托管在服务端上的json文件中获取的
@page "/fetchdata"
@inject HttpClient Http <h1>Weather forecast</h1> <p>This component demonstrates fetching data from the server.</p> @if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
} @code {
private WeatherForecast[] forecasts; protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
} public class WeatherForecast
{
public DateTime Date { get; set; } public int TemperatureC { get; set; } public string Summary { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

再结合我们上面讲的Blazor WebAssembly的工作方式,我们就能得出一个结论:在这个样例Blazor WebAssembly程序中,所有的计算负载,其实都是运行在客户端的浏览器上的。

服务端纯粹就是一个静态文件托管 Web Server,然后我们把这个项目打包一下,看看它长什么样子:

打包后这玩意就长这样:

其实所有的东西都在那个wwwroot目录里,而外边那个web.config,其实是写给 IIS 看的。

看到没?这玩意纯粹就是个前端项目!跟webpack后的前端项目不能说一模一样吧,但至少是有异曲同工之妙,把整个dist目录现在随便放在一个 Nginx 里托管起来,它就能跑起来。而我们在Program.cs中看到的所谓WebAssemblyHost,其实就他妈是一个 Kestrel Server,用来托管静态文件而已

你不信?行,我下载一个 Windows 版本的 Nginx,给你跑一下你就信了,我把上面的wwwroot目录拷到 Nginx 目录下的HelloBlazorWASM目录中,然后把 Nginx 的配置文件改成下面这样:

# ...
server {
listen 9438;
server_name localhost; location / {
root HelloBlazorWASM/wwwroot;
index index.html index.htm;
}
# ...
}
# ...

然后给你跑起来,你看,一点毛病没有!

2.3 再看 Blazor 的优缺点

通过上面两个例子,我们对 Blazor 到底是什么有了一个更直观的认识,无论是 Blazor Server 还是 Blazor Webassembly,其实都不会革现在流行的前端框架的命,整个 Blazor 技术的优势和缺陷都相当激进,它的应用范围也相当受限。

优势主要来源于 C#和 .NET 工具链,如果你对 Azure 熟悉的话,也会知道微软在云这方面,真的是给 .NET 做了无缝对接。从感观上来说,Blazor 的技术,适合于个人与小团队创业者,做一些负载有限的 Web 应用。

缺陷处十分明显,对于 Blazor Server 来说,一直需要客户端浏览器通过SingalR与服务端建立一个 WebSocket 连接,所有计算都在服务端,长连接还一直得保持,并且,每新开一个 Tab,就相当于新开了一个连接,性能问题十分堪忧。甚至我们不能说这是一个 Web 应用,这本质上其实是一个以浏览器为客户端的远程应用

在 Blazor WebAssembly 这边,你也看到了,即便是我们创建的样例程序 ,在加载时都非常能明显的看到一个Loading字样,如果你再细心的去翻浏览器的调试面板的话,你会看到这个网页啥啥干不干,先给你加载个 70 多 KB 的blazor.webassembly.js,再给你加载一个 200 多 KB 的dotnet.5.0.12.js,你品,你细品。再网页点开了,业务逻辑计算代码被编成了 WebAssembly,UI 更新还得脱裤子放屁间接调 JS 去操控 DOM,你品,你细品。

但是,话说回来,话说说说回来,各位,扪心自问一下,如果一个 Web 产品,做到了峰值并发上万,规模得有多大?而这样的数据并发规模,我不知道你的后台业务逻辑得有多复杂,即使复杂的要死,说难听点,也就是加几台机器的问题,瓶颈如果有,也一定是在存储层。

Blazor 生态第二大的问题是:没有 UI 库,没有一个类似于 ant-d 的库可以用,这是一个非常大的问题。对于创业团队或者个人开发者来说,这个问题要比什么狗屁性能问题大一万倍。

最新文章

  1. Eclipse设置和必装插件
  2. pyenv的使用
  3. SQL merge into 表合并
  4. 数位DP HDU3652
  5. [AX]AX2012 Number sequence framework :(三)再谈Number sequence
  6. golang 图片处理,剪切,base64数据转换,文件存储
  7. [转载]浅谈组策略设置IE受信任站点
  8. Json格式理解
  9. EventHandler委托的使用
  10. jetty-distribution-7.6.x 部署
  11. 在PYTHON中,用cx_Oracle连接ORACLE数据库简单示例
  12. Android打开系统的Document文档图片选择
  13. 第2课 Linux操作系统简介
  14. Echarts数据可视化polar极坐标系,开发全解+完美注释
  15. 笔记:Struts 2.3.31 配置说明
  16. Scrapy爬虫框架(实战篇)【Scrapy框架对接Splash抓取javaScript动态渲染页面】
  17. SpringMVC和Struts2的区别及优势
  18. 【C语言基础】循环体系
  19. animate.css –齐全的CSS3动画库--- 学习笔记
  20. ie8不支持currentTarget的解决办法

热门文章

  1. PHP面试常考内容之面向对象(2)
  2. Solution Set -「LOCAL」冲刺省选 Round XXIV
  3. Solution -「FJWC 2020」人生
  4. 【软件实施面试】MySQL和Oracle联合查询以及聚合函数面试总结
  5. node(s) didn‘t match node selector.
  6. 一文带你解读Spring5源码解析 IOC之开启Bean的加载,以及FactoryBean和BeanFactory的区别。
  7. Spring Boot 自定义配置文件异常&quot;expected single matching bean but found 2&quot;
  8. VUE项目二级菜单刷新时404 nginx
  9. swagger 2.0
  10. DNS中的FQDN