.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N191121302
出刊日期: 2019/11/27
本站2018年《初探Blazor》文章中第一次介紹了「Blazor」,它是從這句話衍生出來的:
Browser + Razor = Blazor
「Blazor」是一個新的.NET網站框架(.NET web framework),以WebAssembly標準為基礎,可以取代以往使用JavaScript語言,改用C# / Razor語法與HTML標籤建立執行在瀏覽器上的用戶端應用程式,有了「Blazor」就可以讓程式設計師專注在一種程式語言,直接使用C# 語言進行全端開發(full stack web development)。隨著.net core 3.0的問市,現在我們可以真正開始使用ASP.NET Core Blazor 來開發互動式的用戶端網頁介面(Web UI)程式,當然《初探Blazor》文章中介紹的程式架構與語法也不太適用、需要改寫了。在這篇文章中,將要介紹如何在Visual Studio 2019開發工具中建立第一個Blazor應用程式。
ASP.NET Core Blazor
「Blazor」是一個開發用戶端網頁介面(Web UI)的框架,使用.NET程式庫(Library)與C#程式語言進行開發,可以享受到.NET帶來的效能、可靠性與安全性的好處,並且可跨Windows、Linux與macOS平台來運行。
「Blazor」應用程式目前分為兩類:
- l 執行在用戶端瀏覽器,目前為預覽版(preview),參考下圖,以WebAssembly為基礎,使用C#撰寫的 *.razor檔案的程式都會編譯成組件(Assembly),這些組件與.NET runtime和相依的檔案將會下載到瀏覽器端執行。目前大部分的現代化瀏覽器都支援WebAssembly標準,但Microsoft Internet Explorer除外。
圖 1:執行在用戶端瀏覽器的WebAssembly。
- l 執行在伺服端(Blazor Server),ASP.NET Core 3版及以上版本才有支援。Blazor Server應用程式運作方式是將Razor元件(Razor Component)裝載在伺服端上的ASP.NET Core app中執行,瀏覽器中的UI與伺服端將通過SignalR連線進行通訊,瀏覽器中的UI事件觸發時,將通知伺服端做對應處理,再於瀏覽器更新UI。所有的現代化瀏覽器都可支援Blazor Server,Microsoft Internet Explorer需要11版以上搭配一些Polyfill程式才可以支援,請參考下圖所示:
圖 2:執行在伺服端的Blazor Server。
不管是上述哪一類的「Blazor」應用程式,都是以「元件 (Component) 」為基礎,「元件」 是一個附檔名為「.razor」檔案,其中可以使用Razor標籤來定義UI元素,也稱做「Razor Component」,例如頁面、對話方塊或資料輸入表單等等,並透過C#程式語法來設計UI渲染(UI rendering)邏輯,或處理事件。當編譯應用程式時,「元件」將會編譯成 一個.NET 類別。
使用Visual Studio 2019開發工具建立Blazor Server App
首先利用Visual Studio 2019開發工具來建立一個新專案,啟動 Visual Studio 2019之後,可以看到以下畫面,點選「Create a new Project」項目,然後按「Next」按鈕進入下一個畫面,請參考下圖所示:
圖 3:啟動 Visual Studio 2019。
在「Create a New Project」對話盒中,選取「Blazor App」項目,然後按「Next」按鈕進入下一個畫面,請參考下圖所示:
圖 4:選取「Blazor App」樣版專案。
在下一個步驟可以讓你設定專案名稱,如「BlazorApp1」,以及專案存放路徑後按下「Create」按鈕,請參考下圖所示:
圖 5:設定專案。
下一個畫面將可設定是否使用驗證功能,或啟用HTTPS或加裝Docker功能來建立專案,請參考下圖所示,目前直接使用預設值,按下「Create」按鈕之後將開始建立專案:
圖 6:建立專案。
新建立的範本網站結構如下圖所示,「wwwroot」資料夾存放網站靜態資料,例如圖示檔、樣式表。「Data」資料夾存放模型定義以及存取資料的服務程式;「Pages」資料夾用來存放元件(Component)程式碼;「Shared」資料夾用來存放共用的元件程式碼;「App.razor」檔案用來設定路由;「appsettings.json」用來進行組態設定;「Program.cs」檔案中包含一個「Main」方法,定義程式進入點:
圖 7:「Blazor App」範本網站檔案結構。
基本上第一個Blazor Server App便已經建立完成,「Pages」資料夾下的「Index.razor」是一個元件,定義網站首頁要顯示的內容。在Visual Studio開發工具,按CTRL+F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同),Visual Studio開發工具便會自動啟動一個開發階段用的網站伺服器IIS Express,接著會啟動瀏覽器,可看到首頁如下圖所示:
圖 8:網站首頁執行結果。
我們回頭過來看一下程式,檢視專案根目錄隙的「Startup.cs」檔案,其中「Startup」類別的「ConfigureServices」方法,叫用了「AddServerSideBlazor」方法在專案中加入了「Blazor」的服務。「Configure」方法中則設定的請求處理管理(Request Pipeline),並叫用「UseEndpoints」方法設定「SignalR」端點。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using BlazorApp1.Data;
namespace BlazorApp1 {
public class Startup {
public Startup( IConfiguration configuration ) {
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices( IServiceCollection services ) {
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
}
public void Configure( IApplicationBuilder app , IWebHostEnvironment env ) {
if ( env.IsDevelopment() ) {
app.UseDeveloperExceptionPage();
}
else {
app.UseExceptionHandler( "/Error" );
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints( endpoints => {
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage( "/_Host" );
} );
}
}
}
Blazor Server整合了ASP.NET Core Endpoint Routing功能,你可以看到「UseEndpoints」方法中叫用了「MapBlazorHub」方法,以接受連線。若找不到請求的路由,就會執行「_Host.cshtml」,從程式中得知,預設將會渲染「App」元件(App.razor)。
按照慣例裝載程式的檔案名稱為「_Host.cshtml」,參考以下列表,其中的 <app> 標籤表示它將裝載一個Blazor App。而「RenderComponentAsync」方法,是用來啟用伺服端預渲染(prerender)功能,可在用戶端尚未建立連線時,預先在伺服端進行渲染的動作。檔案下方則引用了「blazor.server.js」,其中的JavaScript程式碼用於建立用戶端連線。將「RenderMode」設定為「ServerPrerendered」表示元件渲染的結果是靜態的HTML。
@page "/"
@namespace BlazorApp1.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlazorApp1</title>
<base href="~/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
</app>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
「App.razor」檔案包含了啟用路由元件(Router component)的程式碼,使用<Router>標籤來定義路由。「RouteView」元件的「DefaultLayout」設定預設的版面頁為「Shared」資料夾中的「MainLayout」元件,「RouteView」元件是一個定位點,若有找到相符的路由,便將對應的自訂元件渲染完的結果套用版面頁呈現在此。若找不到相符的路由,則會顯示「<NotFound>」樣版定義的內容,透過「LayoutView」套用指定的版面頁顯示自訂錯誤訊息:「<p>Sorry, there's nothing at this address.</p>」。
<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>
「MainLayout.razor」檔案定義網頁版面配置(layout),需繼承「LayoutComponentBase」類別,此類別定義了「Body」屬性,程式中使用「@Body」將路由相符的自訂元件渲染的結果插入這個位置。
@inherits LayoutComponentBase
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="https://docs.microsoft.com/en-us/aspnet/" target="_blank">About</a>
</div>
<div class="content px-4">
@Body
</div>
</div>
如果想要蓋過預設版面配置頁的設定,你可以在「_Imports.razor」設定版面配置頁,這個檔案也可以加入using的語法,引用元件程式碼所需的命名空間,以下程式列表是專案根目錄下的「_Imports.razor」檔案:
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using BlazorApp1
@using BlazorApp1.Shared
專案內每一個資料夾中都可以選擇性的包含一個「_Imports.razor」檔案,若要蓋掉預設版面配置頁,我們可以這樣做,例如在「Pages」資料夾中加入一個「_Imports.razor」檔案,設定版面配置頁,程式參考如下:
@layout BlazorApp1.Shared.MyLayout
然後在「Shared」資料夾中,加入一個「MyLayout」檔案,程式參考如下:
@inherits LayoutComponentBase
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<h1>
my layout
</h1>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/en-us/aspnet/" target="_blank">About</a>
</div>
<div class="content px-4">
@Body
</div>
</div>
這樣「Pages」資料夾中的元件就會自動套用「MyLayout.razor」檔案做版面配置,執行結果請參考下圖所示:
圖 9:套用版面配置頁。
「Pages」資料夾下的「Index.razor」檔案實作了Blazor元件(Blazor Component),首頁的內容只包含靜態HTML標籤如下列表:
@page "/"
<h1> Hello, world! </h1>
Welcome to your new app.
第一行程式碼使用「@page」指示詞定義了路由。因此只要執行網站首頁,「Index.razor」元件會便執行渲染(Rendering)動作在記憶體中建立渲染樹(render tree),用來更新DOM。
「Pages」資料夾下的「Counter.razor」與「FetchData.razor」元件除了包含HTML標籤之外,還包含了使用C#語言撰寫的程式邏輯。
若在檢視「Index.razor」在瀏覽器中執行的結果,可以看到瀏覽器接收到以下標籤與程式碼,透過JavaScript(blazor.server.js)來運行:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlazorApp1</title>
<base href="/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>
<!--Blazor:{"sequence":0,"type":"server","prerenderId":"fbdab358cbae4687b63d36820179ef00",
"descriptor":"CfDJ8NM4coV2aRBOmlH3B1B3ovM-qwAItkU-H7OtMCPj6GZOCtGF-5FvWJqaFA5S7S6VBkI3TRz5IzbtEOPSig4Kn4_ILMucUNZwv04DNOCo56nngdV38AyoNDZZehcj6EEsKfsOKZZhb6ID-rwTP_VHMUGxzdkqGczhLkddXi7pb33HhfgXBNjqIWHkGMDY0yrHqSpon1MjHBi7pPeA4kEIYJolLIj4uu0-e7WGCwVPH9JF9xLVOpxWrlUjFdYU6bpG6z1Vx6r8hhOHDuw-9suLIwC4lUf0NTmyCxKLkFcmhjka"}-->
<div class="sidebar">
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href>BlazorApp1</a>
<button class="navbar-toggler">
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="collapse">
<ul class="nav flex-column">
<li class="nav-item px-3">
<a href="" class="nav-link active">
<span class="oi oi-home" aria-hidden="true"></span> Home
</a>
</li>
<li class="nav-item px-3">
<a href="counter" class="nav-link">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</a>
</li>
<li class="nav-item px-3">
<a href="fetchdata" class="nav-link">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</a>
</li>
</ul>
</div>
</div>
<div class="main">
<div class="top-row px-4">
<a href="https://docs.microsoft.com/en-us/aspnet/" target="_blank">About</a>
</div>
<div class="content px-4">
<h1>Hello, world!</h1>
Welcome to your new app.
</div>
</div>
<!--Blazor:{"prerenderId":"fbdab358cbae4687b63d36820179ef00"}-->
</app>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
關於版面配置頁的設定,我們再做一下說明,若是只有個別的元件要套用版面配置頁,則可以直接在元件的程式使用「@layout」設定,例如:
@layout BlazorApp1.Shared.MyLayout
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
建立Hello元件
「Blazor」中的元件(Component)也稱為「Razor」元件(Razor Component),一個「*.razor」檔案定義一個「Blazor」元件。一個「Blazor」元件是一個.NET類別,定義一個可以在網頁中重複使用的Web使用者介面(Web UI)。
讓我們開始來試寫一個自訂的「Hello」元件,首先在專案中「Pages」資料夾加入一個「Razor Component」範本,從「Solution Explorer」視窗 -「Pages」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」項目,請參考下圖所示:
圖 10:建立新項目。
從「Add New Item」對話盒中,選取「Visual C#」-「ASP.NET Core」分類下「Razor Component」項目,然後在下方將「Name」設定為「Hello.razor」最後按下「Add」按鈕,請參考下圖所示:
圖 11:在專案中加入「Razor Component」項目。
特別注意,元件的名稱必需以大寫的英文字開始,不可以使用小寫,例如「Hello.razor」是有效的名稱,而「hello.razor」則是無效的名稱。接著在「Hello.razor」檔案中加入以下程式碼:
@page "/hello"
<h1> Hello </h1>
<p>
Name :
<input placeholder="Enter Your Name " @bind="myName" />
</p>
<br />
<p>
Message : @msg
</p>
<button class="btn btn-primary" @onclick="SayHello"> Click me </button>
@code {
private string myName;
private string msg;
private void SayHello() {
msg = $"Hello {myName}";
myName = string.Empty;
}
}
「Hello.razor」檔案第一行以「@page」指示詞開始。其後的字串定義了路由。也就是說「Hello」元件會負則處理瀏覽器送過來的「/hello」請求。元件可以不需要「@page」指示詞來處理路由,這樣的元件可以插入別的元件之中使用。
「Hello」元件使用標準的HTML標籤定義UI介面,程式處理邏輯則是使用Razor語法(使用C#語言)。HTML標籤與程式邏輯將會在編譯階段轉換成一個元件類別(Component Class),「Hello.razor」檔案的名稱就被拿來當做類別的名稱(不含附檔名)。以此例而言「Hello」元件的完整類別名稱為「BlazorApp1.Pages.Hello」,此類別將會自動繼承自「Microsoft.AspNetCore.Components.ComponentBase」類別。
「@code」區塊中定義了「Hello」類別的成員與元件的邏輯,其中「myName」與「msg」將編譯成「private」欄位(Field),「SayHello」則變成方法,當然你也可以在其中撰寫事件處理程式碼。
「@屬性名稱」或「@欄位名稱」語法可以用來設定資料繫結,例如<input>欄位之中透過「@bind="myName"」attribute繫結到「myName」欄位:
<input placeholder="Enter Your Name " @bind="myName" />
事件註冊的語法有點類似JavaScript,使用HTML attribute,例如以下範例程式碼註冊按鈕的「Click」事件觸發後,將會叫用「Hello」元件的「SayHello」方法:
<button class="btn btn-primary" @onclick="SayHello"> Click me </button>
元件測試
選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。在Visual Studio開發工具,按CTRL+F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同,請修改為實際的埠號),然後在瀏覽器輸入以下URL:
https://localhost:44369/hello
這個範例程式的執行結果參考如下圖所示,網頁中將會包含一個文字方塊,與一個按鈕:
圖 12:「Hello」元件。
只要在文字方塊中輸入名字,再按下按鈕就可以在下方看到歡迎訊息,請參考下圖所示:
圖 13:「Hello」元件執行結果。
當按下「Click me」按鈕,便叫用「SayHello」方法,「Hello」元件會重新產生渲染樹(render tree),接著拿新產生的渲染樹與舊的渲染樹做比對,將兩者之間的差異套用到DOM,接著畫面中就會顯示「Hello mary」歡迎的訊息。
使用元件
若有一個「ServerTime.razor」元件程式如下列表:
<p>
Server Time is : @t
</p>
@code {
string t = DateTime.Now.ToLongTimeString();
}
「ServerTime」元件不需要路由,而是提供功能讓其它元件來重複叫用,因此不需要在檔案上方加上「@page」指示詞來定義路由。同時,為了讓網站所有元件都可以使用到它,我們將「ServerTime.razor」檔案放在網站中「Shared」資料夾下。
接著修改「Hello.razor」檔案,加入「< ServerTime>」標籤,便可以在「Hello」組件中使用「ServerTime」元件:
@page "/hello"
<h1> Hello </h1>
<p>
Name :
<input placeholder="Enter Your Name " @bind="myName" />
</p>
<br />
<p>
Message : @msg
</p>
<p>
<ServerTime />
</p>
<button class="btn btn-primary" @onclick="SayHello"> Click me </button>
@code {
private string myName;
private string msg;
private void SayHello() {
msg = $"Hello {myName}";
myName = string.Empty;
}
}
選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。
在Visual Studio開發工具,按CTRL+F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同,請修改為實際的埠號),然後在瀏覽器輸入以下URL:
https://localhost:44369/hello
這個範例程式的執行結果參考如下圖所示:
圖 14:重複使用元件。
使用參數
元件可以設計參數,如此便可以在父元件傳遞資料到子元件。只要在子元件將參數定義成「public」的屬性,並在屬性前方套用「Parameter」Attribute,例如修改「ServerTime.razor」檔案,加入一個「public」的「format」屬性,用於設定時間顯示格式:
<p>
Server Time is : @GetTime()
</p>
@code {
[Parameter]
public string format { get; set; }
private string GetTime() {
return DateTime.Now.ToString( format );
}
}
修改「Hello.razor」檔案,使用「ServerTime」元件時,利用HTML attribute「format="tt hh:mm:ss"」設定參數:
@page "/hello"
<h1> Hello </h1>
<p>
Name :
<input placeholder="Enter Your Name " @bind="myName" />
</p>
<br />
<p>
Message : @msg
</p>
<p>
<ServerTime format="tt hh:mm:ss" />
</p>
<button class="btn btn-primary" @onclick="SayHello"> Click me </button>
@code {
private string myName;
private string msg;
private void SayHello() {
msg = $"Hello {myName}";
myName = string.Empty;
}
}
當瀏覽器未關閉的情況下,當你修改了Blazor App的程式碼,用戶端會自動跳出提示,是否重新連接到伺服器執行新程式,請參考下圖所示,這對開發來說省了很多功夫。
圖 15:程式自動重載執行。
這個範例程式的執行結果參考如下圖所示:
圖 16:使用參數。
使用導覽功能
「<NavLink>」元件會產生HTML <a>標籤,建立超連結。範本網站的導覽功能定義在「NavMenu.razor」檔案之中,而「<NavLink>」元件套用的css類別則是來自於「Bootstrap」套件:
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">BlazorApp1</a>
<button class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="hello">
<span class="oi oi-list-rich" aria-hidden="true"></span> Hello
</NavLink>
</li>
</ul>
</div>
@code {
bool collapseNavMenu = true;
string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
「<NavLink>」元件的「Match」attribute設定為「NavLinkMatch.All」表示請求的URL要完全相符「<NavLink>」才會有作用(Active),修改完成之後,網站首頁看起來如下:
圖 17:使用導覽功能。