Blazor 机制初探以及什么是前后端分离,还不赶紧上车?

标签: Blazor .Net


上一篇文章我发了一个 BlazAdmin 的尝鲜版,这一次主要聊聊 Blazor 是如何做到用 C# 来写前端的

飚车前

需要说明的一点是,因为我深入接触 Blazor 的时间也不是多长,顶多也就半年,所以这篇文章的内容我不能保证 100% 正确,但可以保证大致原理正确

另外,具有以下条件的园友食用这篇文章会更舒服:

  • 了解 Http 请求响应模型及 Http 协议
  • 对 Web 开发足够了解
  • 有足够的微软技术栈 Web 开发经验,例如 MVC、WebApi 等
  • 有按照微软的 Blazor 官方文档进行入门的实战操作,传送门:https://docs.microsoft.com/zh-cn/aspnet/core/blazor/get-started?view=aspnetcore-3.1&tabs=visual-studio
  • 有自己研究过 Blazor 生成的代码的园友
  • 有过 SignalR 或 WebSocket 使用经验的园友

建议结合 AspNetCore 源码看这篇文章,我不能贴出所有源码,源码需要编译过才能看,不然会很麻烦,但编译这事比较难,编译源码比看源码难多了,这儿是一位园友的源码编译教程:https://www.cnblogs.com/ZaraNet/p/12001261.html
天底下没有新鲜事儿,Blazor 看着神奇,其实也没啥黑科技,它跑不掉 Http 协议,也跑不掉 Html

开始发车

Blazor 服务端渲染过程

当您打开一个服务端渲染的 Blazor 应用时:

    浏览器 -->> 服务器: 建立 WebSocket 连接
    服务器 -->> 浏览器: 发送首页 HTML 代码
    loop 连接未断开
        Note left of 浏览器: 浏览器JS捕获用户输入事件
        浏览器 -->> 服务器: 通知服务器发生了该事件
        Note right of 服务器: 服务器 .Net 处理事件
        服务器-->>浏览器: 发送有变动的 HTML 代码
        Note left of 浏览器: 浏览器JS渲染变动的 HTML 代码
    end

有以下几点需要注意:

  • WebSocket 连接采用 SignalR 来建立,如果浏览器不支持 WebSocket,SignalR 会采用其他技术建立
  • 浏览器捕获用户输入是使用 Javascript进行捕获的
  • 服务器处理客户端事件完成后,会生成新的 HTML 结构,然后将这个结构与老的结构进行对比,得到有变动的 HTML 代码
  • Blazor 服务端渲染版采用在服务器端维护一个虚拟 DOM 树来实现上述操作
  • “通知服务器发生了该事件”这一步里,从原理上来说类似于 WebForm 的 PostBack 机制,不同点在于,Blazor 只告诉服务器是哪个 DOM 节点发生了什么事件,这个传输量是极小的。

服务端渲染的基本原理就是这样,下面我们详细讨论

Blazor 路由渲染过程

当我们通过 NavigationManager 去改变路由地址时,大概流程如下

st=>start: 服务器启动
rt=>operation: 初始化 Router 组件,Router 内部注册 LocationChanged 事件
op1=>operation: LocationChanged 事件中根据路由查找对应的组件,默认触发首页组件
queue=>operation: 加入渲染队列
render=>operation: 一直进行渲染及比对,直到队列中所有的组件全部渲染完
diff=>operation: 将比对的差异结果更新至浏览器
e=>end: 等待下一次路由改变,继续触发 LocationChanged 事件

st->rt->op1->queue->render->diff->e

这里的 Router 组件,就是我们经常用到的,看看下面的代码,是不是很熟悉?

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Router 组件部分代码

public class Router : IComponent, IHandleAfterRender, IDisposable
{
     public void Attach(RenderHandle renderHandle)
        {
            _logger = LoggerFactory.CreateLogger<Router>();
            _renderHandle = renderHandle;
            _baseUri = NavigationManager.BaseUri;
            _locationAbsolute = NavigationManager.Uri;
            //注册 LocationChanged 事件
            NavigationManager.LocationChanged += OnLocationChanged;
        }
    private void OnLocationChanged(object sender, LocationChangedEventArgs args)
        {
            _locationAbsolute = args.Location;
            if (_renderHandle.IsInitialized && Routes != null)
            {
                Refresh(args.IsNavigationIntercepted);
            }
        }
    private void Refresh(bool isNavigationIntercepted)
        {
            var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute);
            locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
            var context = new RouteContext(locationPath);
            Routes.Route(context);

            ..........

            var routeData = new RouteData(
                context.Handler,
                context.Parameters ?? _emptyParametersDictionary);
            //此处开始渲染,Found 是一个 RenderFragment<RouteData> 委托,是我们在调用的时候指定的那个
            _renderHandle.Render(Found(routeData));
            ..........
        }
}

Blazor 组件渲染过程

要开始飚车了,握紧方向盘,不要翻车。
这部分可能会比较难,如果你发现你看不懂的话就先尝试自己写个组件玩玩。
在 Blazor 中,几乎一切皆组件。首先我们得提到一个 Blazor 组件的几个关键方法,部分方法也是它的生命周期

  • OnInitialized、OnInitializedAsync:仅在第一次实例化组件时,才会调用这些方法一次。注意,该方法调用时参数已经设置,但没有渲染。
  • SetParametersAsync:该方法可以让您在设置参数之前做一些事
  • OnParametersSetAsync、OnParametersSet:每一次参数设置完成之后都会调用
  • OnAfterRender、OnAfterRenderAsync:在组件渲染完成之后触发
  • ShouldRender:如果该方法返回 false,则组件在第一次渲染完成后不会执行二次渲染
  • StateHasChanged:强制渲染当前组件,如果 ShouldRender 返回的是 false,则不会强制渲染
  • BuildRenderTree: 该方法一般情况下我们用不到,它的作用是拼接 HTML 代码,由 VS 自动生成的代码去调用它

另有一个关键的结构体 EventCallBack,还有一个关键的委托RenderFragment,它俩非常重要,前者可能见得比较少,后者基本上玩过 Blazor 的园友都知道。

上面提到的关键点,有个印象即可,下面将开始飚车,我们将重点讨论那个流程图中渲染对比的那部分,但将忽略浏览器捕获事件这一步,我不能贴太多的源码,尽可能用流程图表示

主要生命周期过程

st=>start: 开始渲染
isfirst=>condition: 是否首次渲染
init=>operation: 调用 OnInitialized 方法
initAsync=>operation: 调用 OnInitializedAsync 方法
onSetParameter=>operation: 调用 OnParametersSet 方法
setParameter=>operation: 调用 SetParametersAsync 方法
stateHasChanged=>operation: 调用 StateHasChanged 方法
st->setParameter->isfirst->init->initAsync->onSetParameter
onSetParameter->stateHasChanged
isfirst(yes)->init
isfirst(no)->onSetParameter

需要注意的是这个流程中没有 OnAfterRender 方法的调用,这个将在下面讨论

StateHasChanged 方法

这个方法至关重要,就比如上图中最终只到了 StateHasChanged 方法,就没了下文,我们来看看这个方法里面有什么

st=>start: 开始
isfirst=>condition: 是否首次渲染
should=>condition: ShouldRender 为True?
queue=>operation: 进入渲染队列
render=>operation: 开始循环渲染队列的数据
after=>operation: 触发 OnAfterRender 方法
e=>end: 结束
st->isfirst
queue->render->after->e
isfirst(yes)->queue
isfirst(no)->should
should(yes)->queue
should(no)->e

至此,我们基本把一个组件的生命周期的那几个方法讨论完了,除了一些异步版本的,逻辑都差不多,没有写进来

渲染队列时都干了啥?

嗯对,这是重点

st=>start: 开始渲染队列
queue=>condition: 队列还有组件?
read=>operation: 从队列获取组件
swap=>operation: 备份当前 DOM 树及清空
render=>operation: 调用组件的 RenderFragment 委托获取新的 DOM 树
diff=>operation: 与备份的树对比
append=>operation: 将对比结果存入列表
display=>operation: 将列表中的所有对比结果发送至浏览器
e=>end: 结束
st->queue
read->swap->render->diff->append->queue
queue(yes)->read
queue(no)->display->e

为了图好看点(好吧现在其实也不好看),我把流程缩短了一点,有以下几点需要注意:

  • 渲染开始之前是将当前树赋值成了旧的树,然后再将当前树清空
  • 组件的 RenderFragment 委托在大多数情况下就是组件的 ChildContent 属性的值,玩过的都知道几乎每个组件都有自己的 ChildContent
  • 同时 RenderFragment 也有可能是 ComponentBase类中的一个私有属性,详见下面的代码。当然也有可能是其他的,限于篇幅,不细说
  • RenderFragment 委托输入的参数就是当前这颗树
  • 如果您在组件中调用了子组件,并且这个子组件还有自己的内容,那么 VS 会生成调用这个组件的代码,并且为这个组件添加 ChildContent 属性,内容就是子组件自己的内容,详见代码

下面是 ComponentBase 的部分代码,上文提到的私有属性就是 _renderFragment,这个私有属性仅在此处被赋值,可以看到这个属性内部调用了 BuildRenderTree 方法

    public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender
    {
        private readonly RenderFragment _renderFragment;

        /// <summary>
        /// Constructs an instance of <see cref="ComponentBase"/>.
        /// </summary>
        public ComponentBase()
        {
            _renderFragment = builder =>
            {
                _hasPendingQueuedRender = false;
                _hasNeverRendered = false;
                BuildRenderTree(builder);
            };
        }
    }

针对最后一点,举个例子
下面是 NavMenu.razor 组件的 Razor 代码

<BMenu>
    <BMenuItem Route="button">Button 按钮</BMenuItem>
</BMenu>

下面是 VS 生成的代码

public partial class NavMenu : Microsoft.AspNetCore.Components.ComponentBase
    {
        protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
        {
            __builder.OpenComponent<BMenu>(1);
            __builder.AddAttribute(4, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((__builder2) => {
                __builder2.OpenComponent<BMenuItem>(6);
                __builder2.AddAttribute(7, "Route", "button");
                __builder2.AddAttribute(8, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((__builder3) => {
                    __builder3.AddMarkupContent(9, "Button 按钮");
                }
                ));
                __builder2.CloseComponent();
            }
        }
    }

可以看到,NavMenu.razor 使用了 BMenu 这个组件,BMenu 又使用了 BMenuItem这个组件,共套了两层,因此生成了两个 ChildContent 的属性,而且属性类型都是 Microsoft.AspNetCore.Components.RenderFragment
到这儿为止,Blazor 的大概机制基本讨论了一半,接下来讨论上个流程图中的对比那一步,看看 Blazor 是如何进行的对比
这里不细说,因为确实太复杂我也没搞清楚,只说个大概流程,需要说明的一点是 Blazor 的对比是基于序列号的,序列号是什么?大家一定注意到上面代码中的 __builder.AddAttribute(4 中的这个 4 了,这个 4 就是序列号,然后每个序列号对应的内容称为帧,简而言之是通过判断每个序列号对应的帧是否一致来对比是否有改动

st=>start: 开始对比
seq=>operation: 循环每帧
compare=>condition: 序列号是否一致?
isComponent=>condition: 该帧是否都为组件?
render=>operation: 渲染该组件
compareParameter=>condition: 两边组件的参数是否有变化?
skip=>operation: 跳过该帧
setParameter=>operation: 设置新组件的参数,进入该组件的生命周期流程
currentSkip=>operation: 机制过于复杂,不讨论
e=>end: 对比结束
endSeq=>operation: 结束循环
st->seq->compare
compare(yes)->isComponent
compare(no)->currentSkip
isComponent(yes)->render->compareParameter
isComponent(no)->currentSkip
compareParameter(yes)->setParameter->endSeq->e
compareParameter(no)->skip

流程图总算画完了,大概有以下几点需要注意:

  • 实际的对比过程是很复杂的,流程图是简化了再简化的结果,这篇文章的几个流程图需要结合在一起理解才行
  • 当走到设置新组件的参数这一步时,继续往下其实就是进入了新组件的生命周期流程,这个流程跟上面的生命周期流程是一样的
  • 结合所有流程图来看,如果只是组件本身重新渲染,那么组件本身设置参数的方法不会被触发,必须是它的父组件被渲染,才会触发它自己的设置参数的方法
  • 对比组件参数这一步,流程图比较笼统。我们可以简单的认为,没有组件的参数是不变化的,它的对比流程过于细节,我觉得没必要写进来。

渲染到此结束

有心开发 Blazor 的园友们,我写这篇文章也是为了能让大家能够少走弯路,毕竟看源码还是挺头疼的,知道大概流程之后可以避免很多问题,下面就来谈谈 Blazor 会让你遇到的问题

Blazor 的不足

优势我们就不谈了,我们来谈谈一个比较隐藏但又不容易解决的不足,这个不足就是我们一不小心就让我们的 Blazor 应用变得卡,而且还比较不容易解决,这个问题在服务端渲染的应用中尤其严重。

结合第一张流程图,浏览器产生任何事件都会发送到服务器端,想象一下你注册了一个 onmousemove 事件的话,还要不要活了?所以,大规模触发的事件尽量少注册,这里面的网络传输成本是很大的,而且也会给你的服务端造成很大的压力。

Blazor 应用变卡一般有以下几种情况,我们只讨论服务端应用的情况

  • 服务器端已经挂了,这种情况其实浏览器端会完全失去响应,除非你刷新
  • 你的代码有问题或你引用的库的代码有问题,导致进入死循环或循环次数非常多

第一点无所谓,第二点是要命的,至少对于我来说,一旦 Blazui 或 BlazAdmin 出现了卡的情况,会非常头疼,但实际上大多数情况都是第二种中,原因在于:

结合所有流程图来看,Blazor 完成渲染才会发送至浏览器,那么完成渲染的标准就是渲染队列被清空,那如果一直无法清空呢?体现出来就是死循环,或者说发生了一次点击事件结果循环了十次,这明显不科学(你故意的例外),而渲染队列被加入新东西大多数情况下是因为调用了 StateHasChanged 并且 ShuoldRender 返回了 true,或者是因为使用了 EventCallBack,这些代码所在的地方你全都难以调试
因为这些代码不是你的代码,所以你的断点也没处打,目前的 Blazor 不会告诉你到底是哪个组件哪行代码引起的死循环

还欠了点东西

还有一个关键的东西是 EventCallBack,一次写太多了,不想写了
园友如果有兴趣的话可以继续把这个写了

什么是前后端分离?

Blazor 出来的时候一堆人说什么 WebForm 又来了,Silverlight 又来了,还有啥啥乱七八糟的,最让我不能理解的是另一种说法:

前后端分离搞得好好的,微软为什么又要把前后端合在一起?

我不敢瞎说,我找了一篇文章:https://www.jianshu.com/p/bf3fa3ba2a8f
下面是摘抄的内容

1.首先要知道所有的程序都是一数据为基础的,没有数据的程序没有实际意义,程序的本质就是对程序的增删改查。

2.前后端分离就是把数据操作和显示分离出来。前端专注做数据显示,通过文字,图片或者图标等方式让数据形象直观的显示出来。后端专注做数据的操作。前端把数据发给后端,有后端对数据进行修改。

3.后端一般用java,c#等语言,现在的node属于JavaScript也能进行后端操作,此处不意义裂解语言。后端来进行数据库的链接,并对数据进行操作。

4.后端提供接口给前端调用,来触发后端对数据的操作。

基本原理就是这样,可能语言上不准确,思想是没有问题的。

作者:前端developer 链接:https://www.jianshu.com/p/bf3fa3ba2a8f 来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

重点在于第二点,前后端分离就是把数据操作和显示分离出来,Blazor 并没有有非要让你用 .Net 写后端
第三点也说了,前端一般是 JS,那现在把 JS 换成 .Net 并没有什么不一样

bbbbbb的更多相关文章

  1. js自动提示查询添加功能(不是自动补全)

    在工作中遇到查询某些数据,并添加到一个列表里的时候,写了一个小功能. 优点: 1.纯手工JS代码,不需要任何js框架,复制下来就能测试,无毒副作用. 2.通过模糊查询快速定位数据,并添加到列表里. 缺 ...

  2. spring boot(六):如何优雅的使用mybatis

    *:first-child{margin-top: 0 !important}.markdown-body>*:last-child{margin-bottom: 0 !important}.m ...

  3. 基于WebGL 的3D呈现A* Search Algorithm

    http://www.hightopo.com/demo/astar/astar.html 最近搞个游戏遇到最短路径的常规游戏问题,一时起兴基于HT for Web写了个A*算法的WebGL 3D呈现 ...

  4. Django知识点整理

    什么是web框架 框架,即framework,特指为解决一个开放性问题而设计的具有一定约束性的支撑结构,使用框架可以帮你快速开发特定的系统,简单地说,就是你用别人搭建好的舞台来做表演. web应用 访 ...

  5. Backbone源码学习之extend

    extend函数在backbone大概就20来行代码包括注释,对学习Javascript中"类"的继承很是好的学习资料. 下面先贴出Backbone中的源码,其中涉及到unders ...

  6. MySQL 5.7 Replication 相关新功能说明

    背景: MySQL5.7在主从复制上面相对之前版本多了一些新特性,包括多源复制.基于组提交的并行复制.在线修改Replication Filter.GTID增强.半同步复制增强等.因为都是和复制相关, ...

  7. 【转】oracle中rowid的用法 (全面)

    ROWID是数据的详细地址,通过rowid,oracle可以快速的定位某行具体的数据的位置. ROWID可以分为物理rowid和逻辑rowid两种.普通的堆表中的rowid是物理rowid,索引组织表 ...

  8. Jquery垂直下拉二级菜单

    自己做了一个基于Jquery 的垂直下拉二级菜单功能,直接看图: Html的代码如下: <!DOCTYPE html> <html> <head> <meta ...

  9. .NET中的DES对称加密

    DES是一种对称加密(Data Encryption Standard)算法,于1977年得到美国政府的正式许可,是一种用56位密钥来加密64位数据的方法.一般密码长度为8个字节,其中56位加密密钥, ...

随机推荐

  1. Nginx防盗链、访问控制、解析PHP相关配置及Nginx代理

    6月11日任务 12.13 Nginx防盗链12.14 Nginx访问控制12.15 Nginx解析php相关配置12.16 Nginx代理 扩展502问题汇总 http://ask.apelearn ...

  2. Jpa支持LocalDateTime类型持久化

    package com.boldseas.porscheshop.common.config; import javax.persistence.AttributeConverter; import ...

  3. kubectl exec 在kubelet中的处理流程

    基于kuebrnetes v1.17 简单来说,一个完整的streaming请求如下: 客户端 kubectl exec -i -t ... kube-apiserver 向 Kubelet 发送流式 ...

  4. 痞子衡嵌入式:恩智浦i.MX RTxxx系列MCU启动那些事(8)- 从Serial(1-bit SPI) NOR恢复启动

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是恩智浦i.MX RTxxx系列MCU的1-bit SPI NOR恢复启动. 在前几篇里痞子衡介绍的Boot Device都属于主动启动的 ...

  5. tomcat部署项目,详细!

    一.导出war包 1.先导出项目的war包(idea为例) 点+号,选择 之后点ok,确定,关闭窗口,回到idea主页面 在弹出窗口中选择新建的war,选build 之后在war导出目录,找到这个wa ...

  6. 硬核! 逛了4年Github ,一口气把我收藏的 Java 开源项目分享给你!

    Awsome Java Great Java project on Github(Github 上非常棒的 Java 开源项目). English Version 大家都知道 Github 是一个程序 ...

  7. es6 proxy浅析

    Proxy 使用proxy,你可以把老虎伪装成猫的外表,这有几个例子,希望能让你感受到proxy的威力. proxy 用来定义自定义的基本操作行为,比如查找.赋值.枚举性.函数调用等. proxy接受 ...

  8. unrecognized selector sent to class 0x10a4ce490 我躺过的坑

    错误现象:unrecognized selector sent to class 0x102265ad8’ 错误原因: Other Linker Flags 链接设置错误导致类扩展不知道如何正确读取, ...

  9. 浏览器主页锁定之战——IE:我太难了

    精彩回顾: 我是一个explorer的线程 我是一个杀毒软件线程 我是一个IE浏览器线程 比特宇宙-TCP/IP的诞生 产品vs程序员:你知道www是怎么来的吗? Hello, World! 我是一个 ...

  10. 使用 webservice 实现 RPC 调用

    WebService 介绍 Web service 是一个平台独立的,低耦合的 web 的应用程序用于开发分布式的互操作的应用程序.Web Service 技术, 能使得运行在不同机器上的不同应用无须 ...