Blazor 002 : 一种开历史倒车的UI描述语言 -- Razor
Razor是一门相当怪异丑陋的标记语言,但在实际使用中却十分高效灵活。本文主要介绍了Razor是什么,以及Razor引擎的一些浅薄的背后机理。
写文章前我本想一口气把Razor的基本语法,以及Blazor Server App的编译过程都介绍出来的,奈何文章到了这个长度博客园的Markdown编辑器实在不堪重负了。就只能将这些零碎的、无聊的基础语法知识,Blazor Server App与Blazor WASM App 编译过程的差别,放在下一篇文章再去讲了。
1. 什么是 Razor,它和 Blazor 有什么关系?
我们上文提到了 Web UI 框架三大重点:
- 调 DOM API
- 描述交互逻辑
- 调用服务端函数或 API
我们也介绍了 Blazor 的两种工作方式:Blazor Server 和 Blazor WebAssembly。虽然 Blazor 有两套工作方式,但都逃不脱一个问题:如何用代码描述视觉和交互逻辑。
描述交互逻辑,就必然要用一种程序设计语言去表达这些逻辑。
主流前端框架选择了 JavaScript,这出于两点考虑:
- 因为浏览器天然的有 JS 的运行环境。
- 因为交互逻辑要放在浏览器中执行
Blazor 选择了 C#,由于浏览器不支持 C#的运行环境,所以 Blazor 有以下妥协
- 其中一条路就是把 C#编译成 WebAssembly,这就是 Blazor Assembly 工作方式
- 另一条路就是,既然浏览器没有 C#的运行环境,那就不要把交互逻辑放在浏览器中执行,直接放在服务端算了,这就是 Blazor Server
视觉和交互之间最好要互相融合起来,这样框架的使用者用起来会更直观
在 React/Angular/Vue 之前,HTML 天然的就支持在其内部引用 JavaScript 代码
在服务端渲染流行的年代,JSP 和 ASP .NET 这种技术,就是发明了一种四不像的语言,用来在 HTML 文档中嵌入 Java 代码段或 C#代码段,处理过程分两部分
- 第一步,编译时将这种四不像文档转译成 Java 或 C#的类,
- 第二步,浏览器的每次请求其实都是在调用这种类中的一个方法,这个方法会给客户端返回生成的 HTML+CSS 文档
主流前端框架摒弃了服务端渲染,进一步融合了 JavaScript 或 TypeScript,HTML 和 CSS,典型的就是 React 主推的*.jsx
和*.tsx
。这些特殊的脚本并不能直接跑在浏览器中,最终会被工具链转换成 HTML 文档、JS 代码文件和 CSS 文件
Blazor 则开了一点历史的倒车:它把服务端渲染的那套四不像的东西又拉出来了,就是 Razor。
- 第一步依然是相同的,Blazor 依然会把这种四不像脚本语言先转译成一个 C#的类
- 第二步是不同的
- Blazor WebAssembly 会将这个 C#类进一步转译成 WebAssembly 代码跑在浏览器上
- Blazor Server 虽然会在服务端像 ASP .NET 一样直接跑类中的方法,但最终返回给客户端的并不是渲染好的全新的 HTML+CSS 文档,而是发送更新 UI 的指令
而 Blazor 使用的这套,将视觉和交互逻辑融合起来的四不像脚本语言,就叫 Razor。我们上面也说了,Razor 其实是在服务端渲染时代就存在的一个东西,这个东西其实就俩使命:
- 把 HTML&CSS 和 C#嵌合在一起,使用上更像是在 HTML&CSS 中嵌 C#,而不像现在的前端框架,在 JS/TS 中嵌 HTML 标签
- 它最终会被转译成一个类。换句话说,Razor 虽然写着像是标记语言,像是在 HTML&CSS 中嵌了一些 C#代码,但实际上它是一个 C#类
Razor 脚本的历史其实很长,ASP .NET 时代它就是 UI 描述语言,那时候大家用*.cshtml
来做脚本文件的后缀,也很好理解嘛,把 html 和 CSharp 结合在一起,叫*.cshtml
是非常河鲤的。最近,特别是在 Blazor 框架下,大概是微软的人觉得用*.cshtml
太土了,所以又启用了一个新的文件后缀,就叫*.razor
,其实就是喵叫了个咪,没有什么本质区别。
最重要的要谨记以下两点:
- Razor 是一门四不像语言,在 HTML 中掺 C#
- Razor 文件虽然看起来像是 HTML,但其实是个 C#的类
特别是第二点,不清晰的认识到第二点,就很难理解 Razor 语法中很多奇怪的地方
2. Razor 是怎么被转译成 C#类的?
上面我们介绍了什么是 Razor,按常理来说,我们接下来应该介绍 Razor 怎么写,即 Razor 的语法。但我觉得有必要,在讲解 Razor 的语法之前,探究一下 Razor 文件是怎么被转译成一个 C#类 的这个过程。
虽然从框架的使用者的视角来说,并没有必要去了解、理解框架的工作方式,只需要掌握使用方法就行了。但 Razor 太拧巴了,就像上面说的,这是一门四不像的标记语言,如果不了解、理解它背后的工作原理,那么 Razor 中很多奇怪的语法、用法,使用者就无法理解。并且当代码出错时,就完全没有调试纠错的思路。
而更要命的是,上一篇文章我们仅是走马观花的介绍了,使用默认的.Net Core 项目模板创建出来的 BlazorWASM 和 BlazorServer 项目,如果你回过头去看上一篇文章我们介绍的项目中的目录与文件明细,会发现很多不明所以的内容(特别是对之前完全不了解.Net 框架的人来说)。所以这里还得先给大家介绍,如何一步步的纯手动的创建一个 Blazor 项目。
所以这个小节有两个主要任务:
- 介绍如何以最原始的方式创建一个最简单的 BlazorWASM 项目。
- 再介绍如何从命令行编译这个项目,以及编译的过程中都发生了什么,以及最终这个项目是怎么 run 起来的。
在上面两部分内容介绍完毕后,我们会再简短的介绍一下如何创建一个类似的 BlazorServer 项目
2.1 徒手创建并运行一个 BlazorWASM 项目
现在,让我们抛开dotnet new
这个命令行工具,我们直接徒手开始搓一个项目。这里你不需要用到 Visual Studio,甚至不需要用到 VSCode,你需要的只是一个文本编辑器。
step 1 : 新建目录,创建csproj
文件
显然,我们需要先创建一个目录(文件夹,我在系列文章中将使用目录这个术语,后续不再说明),我们打开 powershell,或者如果你在 Linux 平台或 Mac OS 平台,打开 Terminal,如下使用mkdir
命令创建一个目录:
>> mkdir HelloRazor
首先,所有的.Net 项目都由一个*.csproj
文件声明,这个文件里,以 XML 的形式描述了这个项目的类型、结构、包含多少源代码,你可以把这个文件理解为项目的声明文件+项目的编译脚本。一般来说,像 Visual Studio,以及dotnet new
这种工具,创建项目时会为你自动生成一个*.csproj
文件,但这里我们决定,在HelloRazor
目录下新建一个名为HelloRazor.csproj
的文件,其内容如下:
net6.0
上面的文件内容主要说了三件事:
- 这个项目面向.Net 6.0
- 这个项目依赖两个包:
M.A.C.WebAssembly
和M.A.C.WebAssembly.DevServer
- 虽然没有明说,但这个项目,会把当前目录下的所有
*.cs
文件视为项目的源代码文件,即所有的*.cs
文件都会参与编译
step 2 : 创建入口类
如所有程序设计语言一样,.Net 项目也需要一个入口类,一个入口函数。这种函数在 C 语言中叫int main(int argc, char ** argv)
,在 C#中叫static void Main(string[] args)
。现在我们将在HelloRazor
目录下与HelloRazor.csproj
平齐,再创建一个文件Program.cs
,它的内容如下:
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace HelloRazor;
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
}
在上一篇文章中我们讲过,Blazor WebAssembly 的工作方式和 Angular、React、Vue 是类似的。那么类比一下:
- 一个 React 的前端项目:
- 开发人员要在本地把它 run 起来一边开发一边调试,就需要把它塞给 Webpack dev server,也就是一个本地的 Web Server
- 而实际部署到生产环境时,打包后的前端编译产物会被拷贝到 Nginx 中托管起来,此时 Nginx 充当了 Web Server 的角色
- 一个 Blazor WebAssembly 项目:
- 开发人员要在本地把它 run 起来一边开发一边调试,就需要把它塞给一个类似于 Webpack dev server 的东西中去。
- 而实际部署到生产环境时,编译,或者叫打包后的产物,也一样是会被拷贝到 Nginx 中托管起来
.Net 工具链中,有没有一个类似于 webpack dev server,或者 Nginx 的东西呢?答案是:有,也没有
webpack dev server 和 Nginx,以及其它的 Web Server 软件,它们本质上都是一个现成的、可执行的二进制,然后通过配置文件中的信息去寻找应当如何处理 Http 请求。
.Net 中没有这样现成的、可执行的二进制,但有一个库,叫 Kestrel,这个库的作用,就是用来处理Http 请求里有关网络收发的繁杂工作:比如网络层的连接管理、将 TCP 解析为 HTTP Request,再将 HTTP Request 解析为.Net 技术栈中相应的对象,以及在回包时,将.Net 技术栈中相应的对象,再翻译成 HTTP 回应报文,再通过网络层发回去。
在.Net 技术栈中,把这个名为 Kestrel 的库,也称为 Web Server,但它和 Nginx、Apache、以及 webpack dev server 有着本质的区别:
- Kestrel 只是一个库,你需要进行编译、编译、链接后才能得到一个可执行的二进制
- 相较于 webpack dev server, Nginx, Apache 这种只能直接托管静态资源的 Web Server,.Net 的开发人员可以在 Kestrel 库的基础上,自己编写能动态处理 HTTP 请求的应用程序 -- 这,其实就是 ASP .NET Core 整个技术栈的工作方式
所以现在回过头再去看上面Program.cs
的代码,以下的注释你就能稍微理解了:
// 创建一个hostBuilder,它用来创建一个Host实例,这个Host
// 1. 在服务端会打包一个叫 `App` 的Blazor Component,类似于React中的根组件,以WebAssembly的方式返回给浏览器
// 2. 而这个 `App` 的Blazor Component在渲染完成之后,会用渲染成果替换掉HTML文档中那个叫 `app` 的元素
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
上面这段代码,和 React 的下面这段代码有异曲同工之妙,但工作方式完全不同:
ReactDOM.render( , document.getElementById("app"));
step 3 : 编写根 Blazor Component :App
和 React 一样的是,Blazor 项目都由一个根组件一层层嵌套渲染起来。Blazor 的根组件,一般是一个前端路由器。现在,我们在HelloRazor
目录中再创建一个名为App.razor
的文件,其内容如下:
@using Microsoft.AspNetCore.Components.Routing
Page Not Found
目前我们还没有学习 Razor 中的语法以及特殊元素,所以上面的代码我们并看不懂。但根据单词基本能猜个大概,这个前端路由器的功能是:
- 如果用户访问的路径能被路由表匹配,那么就去渲染
@routeData
,也就是渲染对应的子 Blazor Component - 如果不能,则渲染
Page Not Found
step 4 : 编写一个 Hello 页面
上面我们说了,根组件(后续文章中,Component和组件将频繁出现,其实它俩是同一个意思)其实就是个前端路由器,我们当然不能只写个路由器,不然用户访问哪都是
,我们现在就来编写一个真正意义的组件,一个真正意义上的Page Not Found
Razor Page
。
在HelloBlazor
目录中新建一个文件叫Index.Razor
,其内容如下:
@page "/"
Hello, Razor!
This is a Razor page, but only contains standard HTML code.
这个文件包含两部分内容:
- 脑门上的
@page "/"
,其实是路由声明:声明这个组件仅匹配路由路径"/"
,也就是根目录 - 余下两行就是标准的 HTML 代码,没有任何魔法
step 5 : 编写一个默认 HTML 文档
如同 React 一样,前端渲染框架都要有一个默认的 HTML 文档,这个文档中一般有一个 ID 为 Blazor 也一样,我们依然也需要创建这样一个默认文档。而这个文档也是一个纯纯的静态文件,而 Kestrel 库默认情况下,会把项目目录下一个名为 鉴于我们在 可以看到除了那个 ID 为 现在,你的 现在,打开命令行,在 其中 效果大致如下: 然后在浏览器中打开 现在我们已经手动,from scratch 的创建了一个 Blazor 项目,简单的总结一下: 实际项目运行时,我们运行的是编译链接后的 Web Server,浏览器访问 整体流程故事就是这样,但这里有一个核心点需要我们关注:服务端是怎么生成 过程分如下三步走 这其中我们要重点关注第一步,即要关注一个我们从上一篇文章就强调的概念:Blazor 组件,其实就是 C#类,只不过书写成了 至于第二步,我们作为框架的使用者,没必要去过分关心。有两个理由: 在执行了 对于我们这个项目来说,有两个编译产物需要关注 我们上面说了, 没有中间的 下面是 首先是整个 dll 中包含了三个类: 以下是 下面是反编译的 通过对比,不难看出一些一一对应的行。我们也可以简单的总结一些规律 比如很明显的: 除了这种组件,还有一行特别瞩目: 根据这个,我们目前可以简单的认为, 关于这一点,我们可以在 以下是 This is a Razor page, but only contains standard HTML code. 以下是反编译的 This is a Razor page, but only contains standard HTML code. 这就简直没什么悬念了,我们可以暂时总结出下面三条规律: 另外,虽然我们不清楚 在了解了一些浅薄的 Razor -> C#的知识后,我们终于可以开始介绍 Razor 这套标记语言的语法了。本小节将在上一小节的示例项目的基础上,循序渐进的讲解一些基础的 Razor 语法 在 Razor 页面中可以书写 C#表达式,最终呈现的渲染结果将对表达式进行求值,比如我们可以把 Current Time Is @DateTime.Now.ToString() 最终呈现效果如下: 语法很简单,就是在一个 表达式最终会被隐式的调用 而除过记住这个语法,更重要的是去理解,这种语法在 C#类那边,被转译成了什么样子。下面是更改后,对应的 C#类在 ILSpy 中的样子( 这里我们接触到了新的方法: 一些额外知识点: @DateTime. Now. ToString() @(DateTime. Now. ToString()) @GenericMethod @(GenericMethod @(" 鉴于博客园的markdown编辑器已经开始卡顿了,并且这篇文章已经足够长了,我们就把其它Razor基础语法放在下一篇文章中再介绍吧。 有好奇心的同学其实已经可以顺着这个思路去官网查文档学习Razor Syntax了。没必要非得等我写教程 总结一下: razor是先进的,我个人对其有着不正确的评价。描述UI势必就要把JS/TS或其它什么程序设计语言,和HTML&CSS掺在一起写。。。到底是面里掺水还是水里掺面,其实大家掺完都长一个逼样。。要说丑,大家都丑。 文中最大的错误在于对 关于Kestrel和Web Server。 在.net 5及以前的版本中, 位置是 一个默认模板项目中的root
或app
的wwwroot
的子目录中的所有东西都托管为静态资源的。所以,这次,我们要在HelloRazor
目录下创建一个名为wwwroot
的子目录,然后在子目录下创建一个名为index.html
的文档。Program.cs
中已经写明了,那个占位符元素的 ID 是app
,所以这个index.html
的内容应当如下:
app
的空_framework/blazor.webassembly.js
这个文件:这其实就是 Blazor 组件打包后生成的文件
step 6 : 编译、启动项目
HelloRazor
目录中的文件结构应当如下所示:HelloRazor
目录中,执行以下命令:>> dotnet restore
...
...
>> dotnet build
...
...
>> dotnet run
...
...
dotnet restore
是下载项目编译所需要的依赖包dotnet build
是编译项目dotnet run
是运行项目http://localhost:5000
或https://localhost:5001
即可看到如下效果:2.2 Blazor WASM 编译、运行背后的一些浅层机理
App.razor
: 没有视觉,是一个前端路由器Index.razor
: 一个视觉组件,绑定在路径"/"
,即根目录上
wwwroot/index.html
,这个文档内部有两个重点
_framework/blazor.webassembly.js
,这实际上是 Blazor 组件编译打包后的产物Program.cs
中的几行代码,让这个 Web Server 对 Blazor 组件进行打包,也就是生成上面所谓的blazor.webassembly.js
localhost:5000
时,同时下载了index.html
和服务端生成的blazor.webassembly.js
。之后,浏览器执行blazor.webassembly.js
渲染了两个组件,并最终将浏览器中的blazor.webassembly.js
的?
*.razor
文件,都被转译成了 C#文件,然后进一步的,编译成了 dll 中的 IL 代码。也就是一个名为HelloRazor.dll
的可执行二进制
*.dll
。。
dotnet *.dll
方式运行,但对于一些复杂应用,比如 Blazor App,直接以dotnet *.dll
试图运行时可能会找不到对应的运行时依赖库*.exe
),或 Linux 下标准的 ELF 可执行二进制,需要进一步的使用dotnet publish
命令进行发布*.dll
文件包装成了一个 PE 文件或 ELF 文件,就是套了一层壳子,外加把依赖库集中放置在身边 -- 或者self-contained
形式的话整体打成一个二进制blazor.webassembly.js
,并放置在运行目录的wwwroot
子目录中blazor.webassembly.js
就变成了一个在wwwroot
目录下托管的普通静态资源,变得与index.html
别无二致*.razor
这种形式。我们要重点关注,这个从*.razor
到*.cs
的转译过程中,发生了什么。了解这背后的机理,有助于我们理解*.razor
中一些奇怪的语法。
dotnet build
后,项目目录下就会默认的生成两个子目录:obj
和bin
,这两个目录下有海茫茫的文件与目录,目录你可以这样简单的理解这两个目录:
obj
: 存放编译产物与中间产物,包括编译前必要的一些准备工作所需的临时文件等bin
: 可执行二进制。比如在bin
目录下除了会有可执行二进制外,还会有wwwroot
目录存储着运行时需要托管的静态资源(包括生成的blazor.webassembly.js
,以及运行时所需的相关库文件)
obj/Debug/net6.0/HelloRazor.dll
和bin/Debug/net6.0/HelloRazor.dll
,可以认为这两个文件是同一个文件。后续文章将直接以HelloRazor.dll
来描述bin/Debug/net6.0/wwwroot/_framework/blazor.webassembly.js
。这个是最终生成的 Web Assembly 代码*.razor
文件会被先转成*.cs
,然后再编进二进制中。但在 Blazor WASM 工作模式下,中间那一步是不可见的,即obj
目录下是没有App.cs
和Index.cs
这样的中间文件的:它们被一步到位的编译进了HelloRazor.dll
中Razor 代码与 IL 代码
*.cs
文件,意味着我们没法直接观察中间的*.cs
长什么样子。但有个比较曲折的方式:我们可以使用反编译工具ILSpy,去查看由 IL 代码反编译生成的 C#代码长什么样。HelloRazor.dll
的反编译结果:Program
是我们写的入口类,App
和Index
则是 Razor 代码生成的类:App
类App.razor
的内容@using Microsoft.AspNetCore.Components.Routing
Page Not Found
App
的类// HelloRazor.App
using HelloRazor;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.CompilerServices;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
public class App : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
__builder.OpenComponent
Page Not Found
");
});
__builder.CloseComponent();
}
}
App.razor
是一个前端路由器,虽然书写上均是 XML 元素+属性的形式,但用到的元素和属性都不属于 HTML 的范畴。这里,我们可以把非 HTML 范畴的 XML 元素标签或属性简单的理解为Blazor 框架内部为我们已经实现的组件。
就对应着OpenComponent
,我们可以理解为 Blazor 框架内部为我们实现了一个名为Router
的组件
虽然也是 XML 元素,但对应的 C#代码其实是Router
组件中的一个属性
对应着OpenComponent
,可以理解这是一个名为RouteView
的组件
被转译成了Page Not Found
AddMarkupContent(.., "
Page Not Found
")*.razor
中原生的 HTML 代码其实是被直接以字符串的形式转译过去的。Index.razor
中得到验证Index
类Index.razor
的内容:@page "/"
Hello, Razor!
Index
类的代码:// HelloRazor.Index
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
[Route("/")]
public class Index : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
__builder.AddMarkupContent(0, "
Hello, Razor!
\r\n\r\n");
__builder.AddMarkupContent(1, "
AddMarkupContent
,以字符串的形式喂给__builder
OpenComponent
*.razor
中以 XML 属性的形式出现,一部分以子元素的形式出现__builder
的具体实现,也没必要去过分纠结,但有一点可以肯定的是:它内部是一个树型的数据结构,它就是对标于 React 框架中的 Virtual Dom 概念的一个东西,它最终的渲染结果,其实就是代表着最终视觉效果的 DOM3. 基础的 Razor 语法
3.1 Razor 表达式 = 在 Razor 页面中写 C#表达式 :
@xxx
与@(xxx)
Index.razor
改写成如下模样:@page "/"
Hello, Razor!
@
后加一个合法的 C#表达式,即可。ToString()
转成字符串(也就是说上面显式的调用ToString()
是不必要的),并且为了避免注入,也会对字符串进行转义处理。这都没什么好说的,比较容易理解。using
语句已略,后方不再说明):[Route("/")]
public class Index : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
__builder.AddMarkupContent(1, "
Hello, Razor!
\r\n\r\n");
__builder.OpenElement(1, "p");
__builder.AddContent(2, "Current Time Is ");
__builder.AddContent(3, DateTime.Now.ToString());
__builder.CloseElement();
}
}
OpenElement
,显然它是用来转译 HTML 原生元素用的。而 C#表达式则被转译成了AddContent
。如果你仔细阅读了上一章节的内容,这里你应该相当豁然开朗,甚至如果你有相关编译原理知识的功底,知道如何写一个 Parser 的话,你大致已经有了一个“自己写一个 Razor 引擎转译器”的思路了
@
代表着 Razor 引擎会把后续当作是一个 C#表达式去处理。而如果你真的想输入一个@
字符的话,连续写两个@@
就可以了@
字符后面的后续当成 C#表达式去处理,一些场景它会智能分析,比如像damn@wtf.com
这种情形,它就能自动分析出来这是电子邮件地址,而不做表达式求值
是非法的,引擎仅会把DateTime
当成表达式,而由于这是一个类型,不是一个合法的表达式,编译期就会把这种错误检查出来。 但有时候恰巧加个空格导致一个残疾的表达式在语法上是合法的,这种错误可能就只能等到运行期才可能报错了。
就是合法的。这种由@(xxx)
将表达式整个括起来的写法,被称为Explicit Razor Expression
,我将称其为显式表达式,而不加括号的简便写法,叫Implicit Razor Expressions
,我将称其为隐式表达式
,这是非法的。这是由于 Razor 引擎无法区分泛型表达式中的尖括号,与 HTML 元素、Blazor 组件的尖括号。这时你只能使用显式表达式,如ToString()
转成字符串,再被脱敏进行防注入。意味着
最终求值的结果其实是Header?
")"<h1>Header?</h1>"
。而如果你真的想作大死,就是要输出 HTML 标签,那么你可以使用@((MarkupString)("
这种方式。其中Header?
"))MarkupString
是一个类型,全名为Microsoft.AspNetCore.Components.MarkupString
,其实就是加一个强制类型转换。。但强烈建议不要这么作死。3.2 待续
4. 指正与更正
4.1 评论指正总结
Razor也只是长的像JSP,其实比JSP这种货色不知道高到哪里去了。webassembly.js
的解释是完全错误的:它并不是前端那种打包后的bundle.js
,它里面没有Blazor组件的代码,它也不是WASM代码。
WASM项目打包后其实有三层:webassembly.js
->dotnet.wasm->*.dll,这三层摞在一起才相当于传统前端打包后的那个bundle.js
我可以用是,我知道。我这是在简化问题,先隐去一些细节,目的是之后展示更大的图景。
这句来源于《C# in depth》的话来狡辩,但这个错误确实太硬了,没法洗。
WASM项目打包后,和其它前端项目一样,就是一堆静态文件。
但在开发时,本地势必要起个dev server,前端那边用的是webpack dev server,Blazor这边用的是自己写的web server:一个内部使用Kestrel库的web server。4.2 工具链生成的C#代码
obj
中间目录会有Blazor组件对应的C#代码存在,.net 6后就把这些中间产物移除了(不让我们直接看了),可能是工具链做了某些调整。obj/Debug/net5.0/Razor/...
,生成的C#代码掺杂了大量的注释,可读性不高,不如直接用ILSpy反着看紧凑。App.razor
会被转译成下面这样:相关