ASP.NET Core Blazor 初探之 Blazor Server
上周初步對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是怎麼配置的:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit //go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see //aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
主要有2個地方要注意:
在ConfigureServices方法里註冊了Blazor的相關service:
services.AddServerSideBlazor();
在Configure方法的終結點配置了Blazor相關的映射:
endpoints.MapBlazorHub();
上次Blazor Webassembly我們的數據服務是通過一個Webapi項目提供的,這次不用了。如果需要提供webapi服務,Blazor Server本身就可以承載,但是Blazor Server根本不需要提供webapi服務,因為他的數據交互都是通過websocket完成的。
實現數據訪問
新建student類:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public string Class { get; set; }
public int Age { get; set; }
public string Sex { get; set; }
}
上次我們實現了一個StudentRepository,我們直接搬過來:
public interface IStudentRepository
{
List<Student> List();
Student Get(int id);
bool Add(Student student);
bool Update(Student student);
bool Delete(int id);
}
}
public class StudentRepository : IStudentRepository
{
private static List<Student> Students = new List<Student> {
new Student{ Id=1, Name="小紅", Age=10, Class="1班", Sex="女"},
new Student{ Id=2, Name="小明", Age=11, Class="2班", Sex="男"},
new Student{ Id=3, Name="小強", Age=12, Class="3班", Sex="男"}
};
public bool Add(Student student)
{
Students.Add(student);
return true;
}
public bool Delete(int id)
{
var stu = Students.FirstOrDefault(s => s.Id == id);
if (stu != null)
{
Students.Remove(stu);
}
return true;
}
public Student Get(int id)
{
return Students.FirstOrDefault(s => s.Id == id);
}
public List<Student> List()
{
return Students;
}
public bool Update(Student student)
{
var stu = Students.FirstOrDefault(s => s.Id == student.Id);
if (stu != null)
{
Students.Remove(stu);
}
Students.Add(student);
return true;
}
}
註冊一下:
services.AddScoped<IStudentRepository, StudentRepository>();
實現學生列表
跟上次一樣,先刪除默認生成的一些內容,減少干擾,這裡不多說了。在pages文件夾下新建student文件夾,新建List.razor文件:
@page "/student/list"
@using BlazorServerDemo.Model
@using BlazorServerDemo.Data
@inject IStudentRepository Repository
<h1>List</h1>
<p class="text-right">
<a class="btn btn-primary" href="/student/add">Add</a>
</p>
<table class="table">
<tr>
<th>Id</th>
<th>Name</th>
<th>Age</th>
<th>Sex</th>
<th>Class</th>
<th></th>
</tr>
@if (_stutdents != null)
{
foreach (var item in _stutdents)
{
<tr>
<td>@item.Id</td>
<td>@item.Name</td>
<td>@item.Age</td>
<td>@item.Sex</td>
<td>@item.Class</td>
<td>
<a class="btn btn-primary" href="/student/modify/@item.Id">修改</a>
<a class="btn btn-danger" href="/student/delete/@item.Id">刪除</a>
</td>
</tr>
}
}
</table>
@code {
private List<Student> _stutdents;
protected override void OnInitialized()
{
_stutdents = Repository.List();
}
}
這個頁面是從上次的WebAssembly項目上複製過來的,只改了下OnInitialized方法。上次OnInitialized里需要通過Httpclient從後台獲取數據,這次不需要注入HttpClient了,只要注入Repository就可以直接獲取數據。
運行一下:
F12看一下這個頁面是如何工作的:
首先/student/list是一次標準的Http GET請求。返回了頁面的html。從返回的html程式碼上來看綁定的數據已經有值了,這可以清楚的證明Blazor Server技術使用的是服務端渲染技術。
_blazor?id=Fv2IGD6CfKpQFZ-fi-e1IQ連接是個websocket長連接,用來處理服務端跟客戶端的數據交互。
實現Edit組件
Edit組件直接從Webassembly項目複製過來,不用做任何改動。
@using BlazorServerDemo.Model
<div>
<div class="form-group">
<label>Id</label>
<input @bind="Student.Id" class="form-control" />
</div>
<div class="form-group">
<label>Name</label>
<input @bind="Student.Name" class="form-control" />
</div>
<div class="form-group">
<label>Age</label>
<input @bind="Student.Age" class="form-control" />
</div>
<div class="form-group">
<label>Class</label>
<input @bind="Student.Class" class="form-control" />
</div>
<div class="form-group">
<label>Sex</label>
<input @bind="Student.Sex" class="form-control" />
</div>
<button class="btn btn-primary" @onclick="TrySave">
保存
</button>
<CancelBtn Name="取消"></CancelBtn>
</div>
@code{
[Parameter]
public Student Student { get; set; }
[Parameter]
public EventCallback<Student> OnSaveCallback { get; set; }
protected override Task OnInitializedAsync()
{
if (Student == null)
{
Student = new Student();
}
return Task.CompletedTask;
}
private void TrySave()
{
OnSaveCallback.InvokeAsync(Student);
}
}
實現新增頁面
同樣新增頁面從上次的Webassembly項目複製過來,可以復用大量的程式碼,只需改改保存的程式碼。原來保存程式碼是通過HttpClient提交到後台來完成的,現在只需要注入Repository調用Add方法即可。
@page "/student/add"
@using BlazorServerDemo.Model
@using BlazorServerDemo.Data
@inject NavigationManager NavManager
@inject IStudentRepository Repository
<h1>Add</h1>
<Edit Student="Student" OnSaveCallback="OnSave"></Edit>
<div class="text-danger">
@_errmsg
</div>
@code {
private Student Student { get; set; }
private string _errmsg;
protected override Task OnInitializedAsync()
{
Student = new Student()
{
Id = 1
};
return base.OnInitializedAsync();
}
private void OnSave(Student student)
{
Student = student;
var result = Repository.Add(student);
if (result)
{
NavManager.NavigateTo("/student/list");
}
else
{
_errmsg = "保存失敗";
}
}
}
這裡不再多講綁定屬性,綁定事件等內容,因為跟Webassembly模式是一樣的,請參見上一篇。
運行一下 :
我們的頁面出來了。繼續F12看看頁面到底是怎麼渲染出來的:
這次很奇怪並沒有發生任何Http請求,那麼我們的Add頁面是哪裡來的呢,讓我們繼續看Websocket的消息:
客戶端通過websocket給服務端發了一個消息,裡面攜帶了一個資訊:OnLocation Changed “//localhost:59470/student/add“,服務端收到消息後把對應的頁面html渲染出來通過Websocket傳遞到前端,然後前端進行dom的切換,展示新的頁面。所以這裡看不到任何傳統的Http請求的過程。
點一下保存看看發生了什麼:
我們可以看到點擊保存的時候客戶端同樣沒有發送任何Http請求,而是通過websocket給後台發了一個消息,這個消息表示哪個按鈕被點擊了,後台會根據這個資訊找到需要執行的方法,方法執行完後通知前端進行頁面跳轉。
但是這裡有個問題,我們填寫的數據呢?我們在文本框里填寫的數據貌似沒有傳遞到後台,這就不符合邏輯了啊。想了下有可能是文本框編輯的時候數據就提交回去了,讓我們驗證下:
我們一邊修改文本框的內容,一邊監控websocket的消息,果然發現了,當我們修改完焦點離開文本框的時候,數據直接被傳遞到了伺服器。厲害了我的軟,以前vue,angularjs實現的是前端html跟js對象的綁定技術,而Blazor Server這樣就實現了前後端的綁定技術,666啊。
實現編輯跟刪除頁面
這個不多說了使用上面的知識點輕鬆搞定。
編輯頁面:
@page "/student/modify/{Id:int}"
@using BlazorServerDemo.Model
@using BlazorServerDemo.Data
@inject NavigationManager NavManager
@inject IStudentRepository Repository
<h1>Modify</h1>
<Edit Student="Student" OnSaveCallback="OnSave"></Edit>
<div class="text-danger">
@_errmsg
</div>
@code {
[Parameter]
public int Id { get; set; }
private Student Student { get; set; }
private string _errmsg;
protected override void OnInitialized()
{
Student = Repository.Get(Id);
}
private void OnSave(Student student)
{
Student = student;
var result = Repository.Update(student);
if (result)
{
NavManager.NavigateTo("/student/list");
}
else
{
_errmsg = "保存失敗";
}
}
}
刪除頁面:
@page "/student/delete/{Id:int}"
@using BlazorServerDemo.Model
@using BlazorServerDemo.Data
@inject NavigationManager NavManager
@inject IStudentRepository Repository
<h1>Delete</h1>
<h3>
確定刪除(@Student.Id)@Student.Name ?
</h3>
<button class="btn btn-danger" @onclick="OnDeleteAsync">
刪除
</button>
<CancelBtn Name="取消"></CancelBtn>
@code {
[Parameter]
public int Id { get; set; }
private Student Student { get; set; }
protected override void OnInitialized()
{
Student = Repository.Get(Id);
}
private void OnDeleteAsync()
{
var result = Repository.Delete(Id);
if (result)
{
NavManager.NavigateTo("/student/list");
}
}
}
總結
Blazor Server總體開發體驗上跟Blazor Webassembly模式保持了高度一直。雖然是兩種不同的渲染模式:Webassembly是客戶端渲染,Server模式是服務端渲染。但是微軟通過使用websocket技術作為一層代理,巧妙隱藏了兩者的差異,讓兩種模式開發保持了高度的一致性。Blazor Server除了第一次請求使用Http外,其他數據交互全部通過websocket技術在服務端完成,包括頁面渲染、事件處理、數據綁定等,這樣給Blazor Server項目的網路、記憶體、擴展等提出了很大的要求,在項目選型上還是要慎重考慮。
最後demo的源碼:BlazorServerDemo