隐藏

ASP.NET Core Blazor 初探之 Blazor Server

发布:2022/1/10 17:09:03作者:管理员 来源:本站 浏览次数:923

上周初步对Blazor WebAssembly进行了初步的探索(ASP.NET Core Blazor 初探之 Blazor WebAssembly)。这次来看看Blazor Server该怎么玩。

Blazor Server

Blazor 技术又分两种:

  • Blazor WebAssembly

  • Blazor Server

Blazor WebAssembly上次已经介绍过了,这次主要来看看Blazor Server。Blazor Server 有点像WebAssembly的服务端渲染模式。页面在服务器端渲染完成之后,通过SignalR(websocket)技术传输到前端,再替换dom元素。其实不光是页面的渲染,大部分计算也是服务端完成的。Blazor Server模式可以让一些不支持WebAssembly的浏览器可以运行Blazor项目,可是问题也是显而易见的,基于SignalR的双向实时通信给网络提出了很高的要求,一旦用户量巨大,对服务端的水平扩容也带来很大的挑战,Blazor Server的用户状态都维护在服务端,这对服务端内存也造成很大的压力。
我们还是以完成一个简单的CRUD项目为目标来探究一下Blazor Server究竟是什么。因为前面Blazor Webassembly已经讲过了,相同的东西,比如数据绑定,属性绑定,事件绑定等内容就不多说了,请参见ASP.NET Core Blazor 初探之 Blazor WebAssembly。

新建Blazor Server项目

打开vs找到Blazor Server模板,看清楚了不要选成Blazor Webassembly模板。

看看生成的项目结构:

可以看到Blazor Server的项目结构跟ASP.Net Core razor pages 项目是一模一样的。看看Startup是怎么配置的:


  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. // This method gets called by the runtime. Use this method to add services to the container.
  9. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
  10. public void ConfigureServices(IServiceCollection services)
  11. {
  12. services.AddRazorPages();
  13. services.AddServerSideBlazor();
  14. services.AddSingleton<WeatherForecastService>();
  15. }
  16. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
  17. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  18. {
  19. if (env.IsDevelopment())
  20. {
  21. app.UseDeveloperExceptionPage();
  22. }
  23. else
  24. {
  25. app.UseExceptionHandler("/Error");
  26. // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
  27. app.UseHsts();
  28. }
  29. app.UseHttpsRedirection();
  30. app.UseStaticFiles();
  31. app.UseRouting();
  32. app.UseEndpoints(endpoints =>
  33. {
  34. endpoints.MapBlazorHub();
  35. endpoints.MapFallbackToPage("/_Host");
  36. });
  37. }
  38. }

主要有2个地方要注意:在ConfigureServices方法里注册了Blazor的相关service:

services.AddServerSideBlazor(); 

在Configure方法的终结点配置了Blazor相关的映射:

endpoints.MapBlazorHub(); 

上次Blazor Webassembly我们的数据服务是通过一个Webapi项目提供的,这次不用了。如果需要提供webapi服务,Blazor Server本身就可以承载,但是Blazor Server根本不需要提供webapi服务,因为他的数据交互都是通过websocket完成的。

实现数据访问

新建student类:


  1. public class Student
  2. {
  3. public int Id { get; set; }
  4. public string Name { get; set; }
  5. public string Class { get; set; }
  6. public int Age { get; set; }
  7. public string Sex { get; set; }
  8. }

上次我们实现了一个StudentRepository,我们直接搬过来:


  1. public interface IStudentRepository
  2. {
  3. List<Student> List();
  4. Student Get(int id);
  5. bool Add(Student student);
  6. bool Update(Student student);
  7. bool Delete(int id);
  8. }
  9. }

  1. public class StudentRepository : IStudentRepository
  2. {
  3. private static List<Student> Students = new List<Student> {
  4. new Student{ Id=1, Name="小红", Age=10, Class="1班", Sex="女"},
  5. new Student{ Id=2, Name="小明", Age=11, Class="2班", Sex="男"},
  6. new Student{ Id=3, Name="小强", Age=12, Class="3班", Sex="男"}
  7. };
  8. public bool Add(Student student)
  9. {
  10. Students.Add(student);
  11. return true;
  12. }
  13. public bool Delete(int id)
  14. {
  15. var stu = Students.FirstOrDefault(s => s.Id == id);
  16. if (stu != null)
  17. {
  18. Students.Remove(stu);
  19. }
  20. return true;
  21. }
  22. public Student Get(int id)
  23. {
  24. return Students.FirstOrDefault(s => s.Id == id);
  25. }
  26. public List<Student> List()
  27. {
  28. return Students;
  29. }
  30. public bool Update(Student student)
  31. {
  32. var stu = Students.FirstOrDefault(s => s.Id == student.Id);
  33. if (stu != null)
  34. {
  35. Students.Remove(stu);
  36. }
  37. Students.Add(student);
  38. return true;
  39. }
  40. }

注册一下:

 services.AddScoped<IStudentRepository, StudentRepository>(); 

实现学生列表

跟上次一样,先删除默认生成的一些内容,减少干扰,这里不多说了。在pages文件夹下新建student文件夹,新建List.razor文件:


  1. @page "/student/list"
  2. @using BlazorServerDemo.Model
  3. @using BlazorServerDemo.Data
  4. @inject IStudentRepository Repository
  5. <h1>List</h1>
  6. <p class="text-right">
  7. <a class="btn btn-primary" href="/student/add">Add</a>
  8. </p>
  9. <table class="table">
  10. <tr>
  11. <th>Id</th>
  12. <th>Name</th>
  13. <th>Age</th>
  14. <th>Sex</th>
  15. <th>Class</th>
  16. <th></th>
  17. </tr>
  18. @if (_stutdents != null)
  19. {
  20. foreach (var item in _stutdents)
  21. {
  22. <tr>
  23. <td>@item.Id</td>
  24. <td>@item.Name</td>
  25. <td>@item.Age</td>
  26. <td>@item.Sex</td>
  27. <td>@item.Class</td>
  28. <td>
  29. <a class="btn btn-primary" href="/student/modify/@item.Id">修改</a>
  30. <a class="btn btn-danger" href="/student/delete/@item.Id">删除</a>
  31. </td>
  32. </tr>
  33. }
  34. }
  35. </table>
  36. @code {
  37. private List<Student> _stutdents;
  38. protected override void OnInitialized()
  39. {
  40. _stutdents = Repository.List();
  41. }
  42. }

这个页面是从上次的WebAssembly项目上复制过来的,只改了下OnInitialized方法。上次OnInitialized里需要通过Httpclient从后台获取数据,这次不需要注入HttpClient了,只要注入Repository就可以直接获取数据。
运行一下:

F12看一下这个页面是如何工作的:

首先/student/list是一次标准的Http GET请求。返回了页面的html。从返回的html代码上来看绑定的数据已经有值了,这可以清楚的证明Blazor Server技术使用的是服务端渲染技术。

_blazor?id=Fv2IGD6CfKpQFZ-fi-e1IQ连接是个websocket长连接,用来处理服务端跟客户端的数据交互。

实现Edit组件

Edit组件直接从Webassembly项目复制过来,不用做任何改动。


  1. @using BlazorServerDemo.Model
  2. <div>
  3. <div class="form-group">
  4. <label>Id</label>
  5. <input @bind="Student.Id" class="form-control" />
  6. </div>
  7. <div class="form-group">
  8. <label>Name</label>
  9. <input @bind="Student.Name" class="form-control" />
  10. </div>
  11. <div class="form-group">
  12. <label>Age</label>
  13. <input @bind="Student.Age" class="form-control" />
  14. </div>
  15. <div class="form-group">
  16. <label>Class</label>
  17. <input @bind="Student.Class" class="form-control" />
  18. </div>
  19. <div class="form-group">
  20. <label>Sex</label>
  21. <input @bind="Student.Sex" class="form-control" />
  22. </div>
  23. <button class="btn btn-primary" @onclick="TrySave">
  24. 保存
  25. </button>
  26. <CancelBtn Name="取消"></CancelBtn>
  27. </div>
  28. @code{
  29. [Parameter]
  30. public Student Student { get; set; }
  31. [Parameter]
  32. public EventCallback<Student> OnSaveCallback { get; set; }
  33. protected override Task OnInitializedAsync()
  34. {
  35. if (Student == null)
  36. {
  37. Student = new Student();
  38. }
  39. return Task.CompletedTask;
  40. }
  41. private void TrySave()
  42. {
  43. OnSaveCallback.InvokeAsync(Student);
  44. }
  45. }

实现新增页面

同样新增页面从上次的Webassembly项目复制过来,可以复用大量的代码,只需改改保存的代码。原来保存代码是通过HttpClient提交到后台来完成的,现在只需要注入Repository调用Add方法即可。


  1. @page "/student/add"
  2. @using BlazorServerDemo.Model
  3. @using BlazorServerDemo.Data
  4. @inject NavigationManager NavManager
  5. @inject IStudentRepository Repository
  6. <h1>Add</h1>
  7. <Edit Student="Student" OnSaveCallback="OnSave"></Edit>
  8. <div class="text-danger">
  9. @_errmsg
  10. </div>
  11. @code {
  12. private Student Student { get; set; }
  13. private string _errmsg;
  14. protected override Task OnInitializedAsync()
  15. {
  16. Student = new Student()
  17. {
  18. Id = 1
  19. };
  20. return base.OnInitializedAsync();
  21. }
  22. private void OnSave(Student student)
  23. {
  24. Student = student;
  25. var result = Repository.Add(student);
  26. if (result)
  27. {
  28. NavManager.NavigateTo("/student/list");
  29. }
  30. else
  31. {
  32. _errmsg = "保存失败";
  33. }
  34. }
  35. }

这里不再多讲绑定属性,绑定事件等内容,因为跟Webassembly模式是一样的,请参见上一篇。
运行一下 :

我们的页面出来了。继续F12看看页面到底是怎么渲染出来的:

这次很奇怪并没有发生任何Http请求,那么我们的Add页面是哪里来的呢,让我们继续看Websocket的消息:

客户端通过websocket给服务端发了一个消息,里面携带了一个信息:OnLocation Changed "http://localhost:59470/student/add",服务端收到消息后把对应的页面html渲染出来通过Websocket传递到前端,然后前端进行dom的切换,展示新的页面。所以这里看不到任何传统的Http请求的过程。
点一下保存看看发生了什么:

我们可以看到点击保存的时候客户端同样没有发送任何Http请求,而是通过websocket给后台发了一个消息,这个消息表示哪个按钮被点击了,后台会根据这个信息找到需要执行的方法,方法执行完后通知前端进行页面跳转。
但是这里有个问题,我们填写的数据呢?我们在文本框里填写的数据貌似没有传递到后台,这就不符合逻辑了啊。想了下有可能是文本框编辑的时候数据就提交回去了,让我们验证下:

我们一边修改文本框的内容,一边监控websocket的消息,果然发现了,当我们修改完焦点离开文本框的时候,数据直接被传递到了服务器。厉害了我的软,以前vue,angularjs实现的是前端html跟js对象的绑定技术,而Blazor Server这样就实现了前后端的绑定技术,666啊。

实现编辑跟删除页面

这个不多说了使用上面的知识点轻松搞定。
编辑页面:


  1. @page "/student/modify/{Id:int}"
  2. @using BlazorServerDemo.Model
  3. @using BlazorServerDemo.Data
  4. @inject NavigationManager NavManager
  5. @inject IStudentRepository Repository
  6. <h1>Modify</h1>
  7. <Edit Student="Student" OnSaveCallback="OnSave"></Edit>
  8. <div class="text-danger">
  9. @_errmsg
  10. </div>
  11. @code {
  12. [Parameter]
  13. public int Id { get; set; }
  14. private Student Student { get; set; }
  15. private string _errmsg;
  16. protected override void OnInitialized()
  17. {
  18. Student = Repository.Get(Id);
  19. }
  20. private void OnSave(Student student)
  21. {
  22. Student = student;
  23. var result = Repository.Update(student);
  24. if (result)
  25. {
  26. NavManager.NavigateTo("/student/list");
  27. }
  28. else
  29. {
  30. _errmsg = "保存失败";
  31. }
  32. }
  33. }

删除页面:


  1. @page "/student/delete/{Id:int}"
  2. @using BlazorServerDemo.Model
  3. @using BlazorServerDemo.Data
  4. @inject NavigationManager NavManager
  5. @inject IStudentRepository Repository
  6. <h1>Delete</h1>
  7. <h3>
  8. 确定删除(@Student.Id)@Student.Name ?
  9. </h3>
  10. <button class="btn btn-danger" @onclick="OnDeleteAsync">
  11. 删除
  12. </button>
  13. <CancelBtn Name="取消"></CancelBtn>
  14. @code {
  15. [Parameter]
  16. public int Id { get; set; }
  17. private Student Student { get; set; }
  18. protected override void OnInitialized()
  19. {
  20. Student = Repository.Get(Id);
  21. }
  22. private void OnDeleteAsync()
  23. {
  24. var result = Repository.Delete(Id);
  25. if (result)
  26. {
  27. NavManager.NavigateTo("/student/list");
  28. }
  29. }
  30. }

运行一下:

总结

Blazor Server总体开发体验上跟Blazor Webassembly模式保持了高度一直。虽然是两种不同的渲染模式:Webassembly是客户端渲染,Server模式是服务端渲染。但是微软通过使用websocket技术作为一层代理,巧妙隐藏了两者的差异,让两种模式开发保持了高度的一致性。Blazor Server除了第一次请求使用Http外,其他数据交互全部通过websocket技术在服务端完成,包括页面渲染、事件处理、数据绑定等,这样给Blazor Server项目的网络、内存、扩展等提出了很大的要求,在项目选型上还是要慎重考虑。