Blazor的N种渲染模式原理和常见问题说明
2024-09-14
200 0我们从下面这幅图开始,下图显示了三种渲染模式,分别称之为静态SSR、交互式SSR(即之前的BlazorServer)、交互式CSR(即之前的BlazorWasm)。还有一种渲染模式BlazorHybrid,稍后说。
一、先浅层理解一个图例
静态SSR:经典的请求响应模式,服务器接收到客户端的请求后,找到路由组件,直接将Razor组件解析为HTML,发送给浏览器渲染。很多特性,和WebApi相似。
交互式SSR:首先,浏览器发起请求,服务器先返回JS脚本;接着,浏览器的JS脚本自动向服务器发起SignalR连接,这个过程图例没画出来。服务器和浏览器,通过SignalR(本质是WebSocket)建立双向的长连接。服务端管理着一棵与浏览器真实DOM树相对应的虚拟DOM树。服务端通过SignalR,监听着浏览器的UI交互事件。服务端接收到UI事件后,执行计算,(这里还会形成一棵虚拟DOM树,新旧两棵虚拟DOM树进行比较),将差量DOM,通过SignalR提交给浏览器的JS脚本,由JS脚本更新真实DOM。
交互式CSR:首先,浏览器发起请求,服务器先返回JS脚本;接着,浏览器的JS脚本向服务器请求Wasm负载(包括应用组件、依赖项和针对WebAssembly定制的.NET运行时等),下载到WebAssembly沙箱中。“漫长的下载”完成后,启动.NET运行时,通过Wasm和JS的交互操作,监听UI交互事件,并将差量DOM提交给JS脚本,由JS脚本更新真实DOM。Wasm负载可以AOT编译为Wasm二进制,这个很牛。
二、这套机制的诸多疑问
1、什么是状态数据:
状态数据:指为了实现差量更新,由运行时保存和管理的虚拟DOM、组件属性、依赖注入等数据对象。
静态SSR:每次请求和响应都是无状态的HTTP,服务端没有保留状态数据,和WebApi有很多相似性。
交互式SSR:使用WebSocket技术(SignalR)建立长连接,服务端为每个请求都保存着状态数据。
交互式CSR:用户浏览器的每个Tab页中,都保存着一份独立的状态数据,信息保存在WebAssembly沙箱环境中。
2、SignalR的问题:
在网络不稳定情况下,SignalR容易出现断连问题。
连接断开后,服务端会暂时保存状态数据,浏览器JS脚本尝试重连,但重连次数或时间超过阈值后,会放弃重连,服务端释放数据,浏览器提示连接失败。
每个连接都保存着一份独立的状态数据,服务器的压力会比较大。这让人想起了古老的聊天室。
3、JSInterop的问题:
不像Vue等前端框架,Blazor的事件监听和差量DOM更新,涉及到Wasm和JS两个运行时的切换,以及两个运行时之间的通信数据转换,其间无论优化的如何,都是有性能开销的。
Blazor能追回来的性能,就是差量DOM的计算上,如果计算复杂,Blazor的性能优势才能体现出来,但大多数情况下都比较简单。当然,这个问题对于BlazorWasm来说,并不是大问题,下面才是大问题。
4、交互式CSR的最大问题:
和Vue等前端框架不同,首次访问BlazorWasm网页时,不但要下载应用组件、依赖类库、CSS等文件,还要下载整个.NET运行时,启动.NET也是一项耗时工作(这个问题有好几个Issues),Wasm负载很大。
虽然现在BlazorWasm支持AOT,可以提升运行时性能,但编译后的下载包体积会更大。总之,交互式CSR的冷启动很耗时。
借助浏览器缓存,Wasm负载首次下载后可以缓存,从而大大减少初始化时间,但要解决应用升级重新发布后的更新问题。
5、什么是Auto模式:
交互式SSR和交互式CSR都有各自的优点和问题,而Auto模式将两者结合起来,取长补短。
首次请求时,先使用交互式SSR,能够快速呈现页面。然后,浏览器后台静默下载Wasm负载并启动.NET运行时,当Wasm的准备工作完成后,再次发生路由切换时,将自动切换到交互式CSR模式。
同一个浏览页面,如果一开始是交互式SSR,即使Wasm已经准备好,也不会发生自动切换。
6、什么是BlazorWeb混合渲染:
综合了静态SSR、交互式SSR、交互式CSR、Auto模式的混合渲染模式,同一个项目,不同页面、或者同一个页面内的不同组件,可以使用不同的渲染模式。充分利用不同渲染模式的优势,但也增加了复杂度。
交互式组件具有传染性,如果父组件使用了某种交互式渲染模式,则后代组件自动继承这种交互式渲染模式,且不能更改。这个规则将在.NET9中被打破,在交互式组件的后代中,可以强行启动静态SSR渲染,VeryGood!!!
静态SSR组件不具有传染性,在一个静态SSR组件(页面)中,可以使用多种交互模式的组件,比如A组件是交互式SSR,B组件是交互式CSR,C组件是Auto。
在一个静态SSR组件(页面)中,传递给交互式子组件的参数必须时JSON可序列化的,比如:
//Name参数是可序列化的
<MyComp @rendermode="InteractiveServer" Name="MC"/>
//RenderFragment类型参数Child content,是不可序列化的
<MyComp @rendermode="InteractiveServer">Child content</MyComp> //XXX报错
//像以上这种情况,可以外包一层来解决
//WrapMyComp.razor
<MyComp>Child content</MyComp>
//在WrapMyComp组件上使用交互式
<WrapMyComp @rendermode="InteractiveServer" />
7、什么是首次浏览:
当我们在地址栏中输入地址,或者手动刷新页面,或者使用a标签跳转地址,都会触发首次浏览。首次浏览,不是首“页”浏览,任何页面(路由地址)都可能触发首次浏览。
首次浏览发生后,使用Blazor框架内置的路由跳转,除静态SSR之外,不会再次触发首次浏览。
首次浏览发生时,对静态SSR没有影响,因为它的每次请求/响应,本质都是首次浏览;对于交互式SSR,将重新建立一个新的SingalR连接,状态数据丢失;对于交互式CSR,将重新下载Wasm负载并重新启动.NET运行时,状态数据丢失。
如果开通了预渲染功能(交互式SSR或者交互式CSR),只有首次浏览时会触发预渲染。但是,任意页面都有可能是首次浏览的页面,所以任何页面都要考虑预渲染的问题。
8、预渲染的问题:
首次浏览时,为加快页面的渲染速度和优化SEO(这是重点),Blazor为交互式SSR和交互式CSR,提供了预渲染机制。请求页面首先在服务端被解析为HTML,并迅速发送到浏览器渲染,这个过程类似静态SSR,爬虫可以爬到内容。然后再建立SignalR连接或者下载Wasm负载,完成后,会启动相应的运行时,并重新渲染一次。
预渲染提升了首次浏览的体验,优化了SEO,但也带来了一些难题。一是交互式组件的初始化和生命周期函数(除了AfterRender之外),会被执行两次,这不仅有性能开销,如果前后两次产生的数据不一致,还需要解决数据一致性问题;二是对于交互式CSR组件,它首先在服务端执行一次,然后在客户端重新执行一次,两次执行的环境不一样,如果组件中有访问强依赖于执行环境的数据,则需要解决数据一致性问题,有方案,但操作繁琐。
BlazorWeb项目中,预渲染是默认打开的。如果在生命周期函数中进行比较复杂操作的,建议针对这个组件,单独关闭预渲染。
9、依赖注入的生命周期问题:
静态SSR:依赖注入的表现特征,和WebApi保持一致,每次请求是一个Scoped(范围),也可以使用Transient(瞬时)和Singleton(单例)服务。
交互式SSR:如果是Singleton服务,所有SignalR连接,都是访问同一个对象,需要注意数据的安全性;每个SignalR连接,是一个Scoped服务,所以要创建购物车这种共享状态数据,适合使用Scoped;交互式SSR,也可以使用Transient服务,每次使用都是一个新的实例。
交互式CSR:首先,区别于静态SSR和交互式SSR,依赖注入对象存在于客户端浏览器中;其次,由于不存在请求响应模式,所以交互式CSR的Scoped服务,和Singleton服务基本一致;Transient服务,和SSR表现一致。
由于同一组件可能在服务端执行,也可能在客户端执行,比如使用Auto模式的组件,还或者使用预渲染的组件。如果在组件中,使用了某个依赖注入服务,就需要在服务端和客户端分别注册,且还需要考虑这两个服务是不同的对象,使用方式是否一样(比如HttpClient,服务端有HttpClientFactory,而客户端默认没有提供的),以及是否会造成数据不一致的问题。
10、状态数据的共享和持久化问题:
状态数据的共享,目前有联级值(CascadingValue)和依赖注入两个方案。如果使用Auto模式,一定要和状态数据的持久化相结合,因为当交互式SSR向交互式CSR切换时,无论是联级值、还是依赖注入,数据都会丢失。如果使用联级值的自定义组件方案,应该在自定义组件的初始化或者AfterRender后,读取持久化中的数据,并在组件销毁时保存数据。如果是使用依赖注入方案,是不是应该在根组件的生命周期里进行状态数据的持久化保存和读取?这个方案暂时还没试过。
状态数据的持久化,目前也有两个方案,一是使用浏览器的Storage,使用这种方式,一定要在AfterRender生命周期函数中进行持久化操作,因为只有它里面,调用JS或操作DOM才是安全的,并建议使用“nuget搜索Blazored”提供的API。我更推荐是用第二种方案,使用服务端的Sqlite进行持久化保存,这恰恰体现了BlazorWeb的优势。这个方案,虽然相对于浏览器的Storage会更重些,但它还是处于轻量这一档,同时它更加稳定,也不需要考虑不同渲染模式的影响。具体的实现思路:创建增删改查的服务接口>服务端实现接口,并将实现类暴露为WebApi>客户端的实现类,并通过访问WebApi来进行增删改查>服务端和客户端分别用自己的实现类来注册服务。
11、HTTP请求问题:
由于HttpClient存在Socket套接字和DNS的问题(甭管啥问题,反之有问题),AspNetCore服务端项目,推荐使用HttpClientFactory,且默认已安装。BlazorWeb的服务端项目,可以直接使用。
BlazorWasm客户端,不存在以上问题,如果是独立项目,可以直接使用HttpClient。BlazorWasm项目,默认没有安装相应类库,无法直接使用HttpClientFactory。
BlazorWeb项目中,由于同一组件可能在服务端执行,还可能在客户端执行,如果组件中需要发送HTTP请求,建议统一使用HttpClientFactory,此时BlazorWasm客户端项目需要安装Microsoft.Extensions.Http。
12、BlazorHybrid如何运行?
看图例,BlazorHybrid渲染模式的运行原理并没有什么不同,把服务器应用或者浏览器的WebAssembly换成MAUI应用、再把浏览器换成本机自带的WebView就可以。
MAUI应用托管着Blazor组件、依赖资源和虚拟DOM,然后通过MAUI应用和WebView的连接通道进行事件响应和差量更新。这个连接通道的具体实现,可以追踪一下BlazorWebView控件的源码。
Blazor托管在MAUI本机应用中,没有WebAssembly这样的沙箱,所以,在Razor组件中,可以调用所有“MAUI本机应用可以调用的API”,就像BlazorServer环境中的组件可以调用AspNetCore的API一样。
您可能感兴趣: