最近Blazor熱度很高,傳說馬上就要發布正式版了,做為微軟腦殘粉,趕緊也來湊個熱鬧,學習一下。
Blazor
Blazor是微軟在ASP.NET Core框架下開發的一種全新的Web開發框架。Blazor利用WebAssembly使得開發者可以拋開JavaScript而使用優雅的C#來開發web單頁應用。微軟利用WebAssembly在瀏覽器里實現了一個.NET Runtime,任何.NET STANDARD 2.1的代碼都可以在瀏覽器上運行,真的是屌炸了。Blazor強化了Razor模板引擎,並且借鑒了當前熱門前端框架的優點,比如雙向綁定技術,組件化,使前端開發敏捷高效。如果你對NG,VUE等框架熟悉那么很容易找到其中的共通點。
Blazor WebAssembly
Blazor 技術又分兩種:
- Blazor WebAssembly
- Blazor Server
Blazor WebAssembly 是真正的SPA,頁面的渲染在前端實現,可以實現真正的前后端分離設計。而Blazor Server可以認為是前者的服務端渲染版本,它使用SignalR實現了客戶端的實時通訊,它的計算跟渲染都在服務端處理。本次咱先研究WebAssembly技術,因為我覺得它的應用前景可能更適合一般項目。廢話不多說,直接開干吧,我們的目標還是完成一個標准的對學員進行CRUD的並且前后端分離的小項目。
安裝Blazor WebAssembly模板
dotnet new -i Microsoft.AspNetCore.Components.WebAssembly.Templates::3.2.0-preview5.20216.8
因為Blzor WebAssembly還在預覽階段所以要手工安裝模板,在控制台運行以上命令來安裝最新的模板。
新建Blazor WebAssembly項目
打開vs找到Blazor的項目模板,就是那個特別像火影標志的那個圖標。新建一個項目名叫BlazorWebAssemblyApp。點下一步,這里會讓選是Blazor Server還是Blazor WebAssembly,不要選錯了。
先看一下項目結構:
Blazor Webassembly的項目結構比較簡單,跟Razor Page的項目結構比較類似。
新建ASP.NET CORE WebApi項目
我們的目標是打造一個前后端分離的項目,那么自然還要建一個Api項目。並且這個項目對外提供一個Student的Restful API。在vs里新建ASP.NET CORE WebApi項目,名為BlazorWebassemblyApisite。
為了演示方便,使用靜態變量實現一個StudentRepository。
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; }
}
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;
}
}
在Startup里注冊這個Repository:
services.AddScoped<IStudentRepository, StudentRepository>();
實現StudentController用來暴露API:
[ApiController]
[Route("[controller]")]
public class StudentController : ControllerBase
{
private IStudentRepository _studentRepository;
public StudentController(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
[HttpGet]
public List<Student> Get()
{
return _studentRepository.List();
}
[HttpGet("{id}")]
public Student Get(int id)
{
return _studentRepository.Get(id);
}
[HttpPost]
public Student Post(Student model)
{
_studentRepository.Add(model);
return model;
}
[HttpPut]
public Student Put(Student model)
{
_studentRepository.Update(model);
return model;
}
[HttpDelete("{id}")]
public void Delete(int id)
{
_studentRepository.Delete(id);
}
}
因為我們的前后端項目會分兩個網址部署,所以肯定需要配置CORS的問題:
app.UseCors(config =>
{
config.AllowAnyOrigin();
config.AllowAnyMethod();
config.AllowAnyHeader();
});
這樣我們的后端API網站就完成了,接來下就是真正的Blazor環節了。
配置HttpClient與注入
讓我們切換回BlazorWebAssemblyApp項目。我們的Blazor項目需要通過Http與API站點進行通信,所以肯定需要一個訪問Http的類庫。如果是JavaScript我們平時使用如axios等庫,但是Blazor可以使用C#實現的HttpClient,在前端由C#發起Http請求,Cool!當然最后HttpClient發出的請求會還是會轉換為瀏覽器的Fetch請求。Blazor項目支持依賴注入,這個用法跟ASP.NET Core項目的體驗是一致的,通過IServiceCollection配置注入的生命周期:
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri("https://localhost:6001") });
Blazor的注入同樣分Transient、Scope、Singleton等生命周期。這里我們注冊HttpClient為Transient,並且配置baseAddress為https://localhost:6001,這是ApiSite的地址。
實現學生列表(student/list)
因為新建成的項目會自動生成一些頁面,為了減少干擾,先刪掉點內容。
簡化MainLayout.razor,刪除一些不必要的東西:
@inherits LayoutComponentBase
<div class="main">
<div class="content px-4">
@Body
</div>
</div>
刪除Index.razor的內容,就留一個page指令:
@page "/"
新建Model文件夾,用來存放Student模型,這里其實可以把Api網站的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; }
}
新建一個student文件夾,在這個文件夾內新建一個List.razor文件:
@page "/student/list"
@using BlazorWebAssemblyApp.Model
@inject HttpClient Http
<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 async Task OnInitializedAsync()
{
var students = await Http.GetFromJsonAsync<List<Student>>("/student");
this._stutdents = students;
}
}
這個文件大體上看跟RazorPages的頁面差不多,Html主體使用razor語法渲染。但是還是有很大的不同,讓我們從頭開始一個個的解釋:
@page "/student/list"
@page指令指示這個頁面的路由,當用戶訪問/student/list時就會路由到這個頁面
@using BlazorWebAssemblyApp.Model
@using指令不多說了,引用namespace,這個跟Razor Pages是一樣的
@inject HttpClient Http
@inject指令從字面看就很容易理解,注入。上面的意思就是注入HttpClient對象,並且命名為Http。后面就可以使用這個Http對象了,當然前提是在Program里注冊好。
@code {
private List<Student> _stutdents;
protected override async Task OnInitializedAsync()
{
var students = await Http.GetFromJsonAsync<List<Student>>("/student");
this._stutdents = students;
}
}
@code指令指示這個scope里的內容為C#代碼。雖然沒有明確定義為class,但是顯然這個代碼塊最后會被編譯成一個類。這個類里的變量可以作為razor模板的數據源,可以進行綁定或者for循環。OnInitializedAsync方法為初始化方法,可以在這里處理一些初始化工作,比如我們這里就是通過一次Http請求獲取學生的列表數據。如果是同步方法請使用OnInitialized。這個文件的結構看起是不是很像VUE的單文件組件,笑哭。
讓我們運行一下吧:
實現新增學生頁面(/student/add)
當點擊列表頁面的Add按鈕的時候,需要導航至新增頁面,導航直接使用a標簽沒有任何問題。
<a class="btn btn-primary" href="/student/add">Add</a>
考慮到后面還有編輯頁面,新增跟編輯頁面整體是一樣的,只是后台處理的邏輯不一樣。既然Blazor支持組件化,那么這種重復的東西既然是封裝為一個組件為好了。
封裝Edit組件
我們把對學生信息編輯的功能抽象成一個組件叫做Edit。在student文件夾下新建一個component文件夾,在文件夾內新建Edit.razor文件:
@using BlazorWebAssemblyApp.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>
</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);
}
}
繼續解釋下這個文件:
數據綁定
<input @bind="Student.Id" class="form-control" />
使用@bind指令可以跟某個對象實現的屬性實現雙向綁定。@bind指令本質上是通過對value跟onchange這個屬性的綁定配合來實現雙向綁定,這個套路怎么那么熟悉?對了VUE也是這么干的,笑哭。@bind="Student.Id"翻譯過來等效於:
<input value="@Student.Id"
@onchange="@((ChangeEventArgs __e) => Student.Id =
__e.Value.ToString())" />
事件綁定
除了對數據的綁定,Blazor還支持對事件的綁定:
<button class="btn btn-primary" @onclick="TrySave">
保存
</button>
@onclick="TrySave" 表示這個button的click事件指向TrySave這個方法。
組件屬性
我們封裝組件經常對外暴露屬性,以便接受外部傳入的數據,比如我們這個Edit組件就需要外部傳入一個Student對象才能正常工作。
[Parameter]
public Student Student { get; set; }
我們在@code代碼里的屬性上打上[Parameter]標簽。這里叫做Parameter,估計是為了跟C#里的屬性(property)進行區分。這樣的話,這個屬性就可以接受父組件的傳參,注意這個屬性是單項數據流,組件內對Student修改並不會修改外部組件的數據源,這個也很VUE啊,笑哭。
組件事件
我們除了需要對外暴露屬性,常常還需要對外暴露事件,用來通知外部組件。當外部組件接受到事件的時候可以進行相應的處理。比如這個Edit組件點擊保存的時候並沒有進行真正的保存操作,而是對外拋一個事件,當外部組件接受這個事件的時候進行真正的處理,比如是調用新增API還是更新API。
[Parameter]
public EventCallback<Student> OnSaveCallback { get; set; }
我們在@code代碼里的EventCallback事件上打上[Parameter]標簽。這樣外部組件就可以注冊這個事件了。當我們在這個組件上點擊保存的時候激發這個事件,並且把修改過的Student對象傳遞出去。
OnSaveCallback.InvokeAsync(Student);
使用Edit組件
Edit組件封裝完成了,讓我們開始使用它。新建一個Add.razor文件,並且在這里使用Edit組件。組件的使用跟VUE等一樣,使用一個自定義的Tag插入到html的里。
@page "/student/add"
@using BlazorWebAssemblyApp.Model
@inject HttpClient Http
@inject NavigationManager NavManager
<h1>Add</h1>
<Edit OnSaveCallback="OnSaveAsync"></Edit>
<div class="text-danger">
@_errmsg
</div>
@code {
private Student Student { get; set; }
private string _errmsg;
protected override Task OnInitializedAsync()
{
return base.OnInitializedAsync();
}
private async Task OnSaveAsync(Student student)
{
Student = student;
var result = await Http.PostAsJsonAsync("/student", Student);
if (result.IsSuccessStatusCode)
{
NavManager.NavigateTo("/student/list");
}
else
{
_errmsg = "保存失敗";
}
}
}
Add.razor的邏輯很簡單,接受Edit組件的保存事件,然后把Student通過Http提交到后台。
<Edit OnSaveCallback="OnSaveAsync"></Edit>
通過OnSaveCallback="OnSaveAsync"設置Edit組件的OnSaveCallback事件回調為OnSaveAsync方法。
當我們保存功能的時候,需要跳轉到列表頁面。Blazor提供了一個簡單的導航框架:NavigationManager。NavigationManager是默認注冊到IoC容器的,所以可以直接使用@inject注入到需要的地方:
@inject NavigationManager NavManager
調用NavigateTo方法進行頁面跳轉。
NavManager.NavigateTo("/student/list");
實現修改學生信息頁面(/student/modify)
修改界面相對新增頁面會多涉及一個知識點,url傳參。當我們需要修改學生信息的時候,需要傳遞一個id參數過去,告訴頁面需要修改哪一個學生。
@page "/student/modify/{Id:int}"
@using BlazorWebAssemblyApp.Model
@using BlazorWebAssemblyApp.Data
@inject HttpClient Http
@inject NavigationManager NavManager
@inject Store Store
<h1>Modify</h1>
<Edit Student="Student" OnSaveCallback="OnSaveAsync"></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 = Store.GetStudentById(Id);
}
private async Task OnSaveAsync(Student student)
{
Student = student;
var result = await Http.PutAsJsonAsync("/student", Student);
if (result.IsSuccessStatusCode)
{
NavManager.NavigateTo("/student/list");
}
else
{
_errmsg = "保存失敗";
}
}
}
@page指令配置的路由模板可以支持參數匹配
@page "/student/modify/{Id:int}"
我們在列表頁面使用a標簽進行跳轉,url組合成/student/modify/1樣式,其中1會匹配給屬性Id,並且這里限制了Id的類型為int。
<Edit Student="Student" OnSaveCallback="OnSaveAsync"></Edit>
對Edit組件的使用,修改頁面跟新增頁面不同的是,修改頁面需要傳遞一個Student對象到Edit組件內部,以便顯示學員信息。
實現一個Store
修改頁面顯然需要顯示學生當前的信息。我們通過url傳遞過來的參數只有id,那么需要一次Http請求去后台獲取學生信息,這沒什么問題。但是如果是SPA應用,其實學生的信息本身已經在列表頁面了,對於那些不是高頻更新的數據,我們沒有必要每次都去數據庫里獲取最新的數據,況且即使你從數據庫里獲取到了最新的數據,也可能在你修改的過程中被別人修改。因為SPA跟傳統的Web項目不同,它可以完整的維護狀態,所以如果我們把列表的數據存起來,那么其他地方可以很方便直接在內存里查詢到,高效又便捷。通常使用Angularjs的時候這種場景會使用一個單例的Service來完成。這里我也簡單使用C#來實現一個Service來存儲頁面的數據,名稱就借鑒一下VUE的Vuex吧,叫Store。
public class Store
{
private List<Student> _students;
public void SetStudents(List<Student> list)
{
_students = list;
}
public List<Student> GetStudents()
{
return _students;
}
public Student GetStudentById(int id)
{
var stu = _students?.FirstOrDefault(s => s.Id == id);
return stu;
}
}
builder.Services.AddSingleton<Store>();
這個service很簡單,就是一個簡單的class。使用List
改造列表頁面
現在我們有了Store,所以當列表獲取到數據后需要存儲到Store里,這樣我們在修改頁面或者其他地方就能根據id直接獲取數據了。
@inject Store Store
@code {
private List<Student> _stutdents => Store.GetStudents();
protected override async Task OnInitializedAsync()
{
var students = await Http.GetFromJsonAsync<List<Student>>("/student");
Store.SetStudents(students);
}
}
實現刪除頁面(/student/delete)
刪除頁面比較簡單,使用前面的知識點輕松可以搞定。同樣通過Url傳遞一個Id到刪除頁面,頁面上獲取學生數據后進行顯示,並且提示用戶是否確定刪除這個學生信息。如果點擊確定就調用刪除API進行刪除操作,如果點擊取消則回退到前一頁。為了增加樂趣,這里會增加C#跟JavaScript交互的內容。
@page "/student/delete/{id:int}"
@using BlazorWebAssemblyApp.Model
@using BlazorWebAssemblyApp.Data
@inject HttpClient Http
@inject Store Store
@inject NavigationManager NavManager
@inject IJSRuntime JSRuntime
<h1>Delete</h1>
<h3>
確定刪除(@Student.Id)@Student.Name ?
</h3>
<button class="btn btn-danger" @onclick="OnDeleteAsync">
刪除
</button>
<button class="btn btn-info" @onclick="OnCancel">取消</button>
@code {
[Parameter]
public int Id { get; set; }
private Student Student { get; set; }
protected override void OnInitialized()
{
Student = Store.GetStudentById(Id);
}
private async Task OnDeleteAsync()
{
var result = await Http.DeleteAsync("/student/" + Id);
if (result.IsSuccessStatusCode)
{
NavManager.NavigateTo("/student/list");
}
}
private void OnCancel()
{
JSRuntime.InvokeVoidAsync("history.back");
}
}
IJSRuntime
當用戶點擊取消的時候我們需要回退到前一個頁面,但是Blazor的NavigationManager並沒有提供GoBack這種操作。這個我實在是想不明白,不管是WPF的導航框架、還是VUE的路由服務都有這種機制,以至於我還得通過JavaScript的能力去調用瀏覽器的原生后退功能來實現。Blazor中想要跟JavaScript交互需要注入JSRuntime對象:
JSRuntime.InvokeVoidAsync("history.back");
我們在取消按鈕的事件代碼里調用以上代碼,這樣就能順利后退了。
總結
通過以上,我們使用Blazor實現了一個簡單的前后端分離的SPA。總體涉及了Blazor的幾個重要知識點,比如:數據綁定,事件處理,封裝組件,JavaScript交互等。其中每個知識點都可以再深入展開來寫一篇。我們使用Blazor,在幾乎沒用JavaScript的情況下順利的完成了一個SPA,總體感覺還是比較良好的。雖然不用JavaScript,但是顯然它借鑒了熱門JavaScript框架的一些特點,如果你有一點前端基礎跟.NET基礎很容易就能上手。但是,我不想在這神吹Blazor,畢竟它也沒有到讓人驚艷的地步,比如我熟悉Angular,熟悉VUE,說真的,目前來說,我沒有什么動力切換到Blazor上來。如果Blazor早出現那么幾年,或許一切都不一樣了。但是,又要但是。。。但是我還是會學習Blazor,就像我當年學習Silverlight一樣。沒錯,我就是那個被微軟傷害兩次(Silverlight,Windows Phone)依然待他如初戀的男人,笑哭。微軟的東西雖然不流行,但是不代表它不先進,有的時候或許是過於先進。比如MVVM、雙向綁定、前后端分離,這些概念都是當年Silverlight RIA應用早就有的。雖然Silverlight后來黃了,但是它里面的一些設計理念,開發模式並不落后,甚至是超前的。這些經驗對后來我學習Angularjs,VUE來說有非常大的幫助,學起來得心應手,因為套路都是那個套路。所以哪天說不定WebAssembly大行其道,Blazor又成了開山鼻祖,學習它的經驗一定是有用的。
最后demo的源碼:BlazorWebAssemblyAppDemo
關注我的公眾號一起玩轉技術