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

标签: Blazor .Nethtml


上一篇文章发了一个 BlazAdmin 的尝鲜版,这一次主要聊聊 Blazor 是如何作到用 C# 来写前端的,传送门:https://www.cnblogs.com/wzxinchen/p/12057171.html前端

飚车前

须要说明的一点是,由于我深刻接触 Blazor 的时间也不是多长,顶多也就半年,因此这篇文章的内容我不能保证 100% 正确,但能够保证大体原理正确java

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

  • 了解 Http 请求响应模型及 Http 协议
  • 有足够的微软技术栈 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 源码看这篇文章,我不能贴出全部源码,源码须要编译过才能看,否则会很麻烦,但编译这事比较难,编译源码比看源码难多了,这儿是一位园友的源码编译教程:http://www.javashuo.com/article/p-enqpoaps-t.html
天底下没有新鲜事儿,Blazor 看着神奇,其实也没啥黑科技,它跑不掉 Http 协议,也跑不掉 Html数据库

开始发车

Blazor 服务端渲染过程

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

浏览器 -->> 服务器: 创建 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 应用变得卡,并且还比较不容易解决,这个问题在服务端渲染的应用中尤为严重。

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

Blazor 应用变卡通常有如下几种状况,咱们只讨论服务端应用的状况

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

第一点无所谓,第二点是要命的,至少对于我来讲,一旦 Blazui 或 BlazAdmin 出现了卡的状况,会很是头疼,但实际上大多数状况都是第二种中,缘由在于:

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

还欠了点东西

还有一个关键的东西是 EventCallBack,一次写太多了,不想写了
园友若是有兴趣的话能够继续把这个写了
有任何问题可进QQ群交流:74522853

什么是先后端分离?

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 并无什么不同

相关文章
相关标签/搜索