ASP.NET Core Blazor 初探之 Blazor WebAssembly


最近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,不要選錯了。
YmBx8P.md.png
先看一下項目結構:
YmrQQf.png
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的單文件組件,笑哭。
讓我們運行一下吧:
YmIGvQ.md.png

實現新增學生頁面(/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");

讓我們運行一下看看吧:
YnDgXT.md.png

實現修改學生信息頁面(/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 來存儲學生列表信息,對外提供幾個Set,Get方法來存儲數據跟獲取數據。這里我並沒有手工實現為單例,直接在框架的容器上注冊為單例生命周期。

改造列表頁面

現在我們有了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);
    }
}

我們的改造完成了,運行一下看看吧:
YnTaRK.md.png

實現刪除頁面(/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");

我們在取消按鈕的事件代碼里調用以上代碼,這樣就能順利后退了。
YuZE5R.md.png

總結

通過以上,我們使用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

關注我的公眾號一起玩轉技術


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM