初探 Blazor WebAssembly (WASM)

2023-12-10

筆記使用 Blazor WebAssembly 開發的初體驗,想要寫一個純粹前端的簡易應用,但這次不是選擇 Vue.js 或者是 jQuery,而是直接把後端的 C# 拿來前端寫 😆 最後結合 GitHub Action 的另一個初體驗,達成 Commit 自動部署在 gh-pages 分支的功效,完成 Commit 即部署的便利 😎

logo

Blazor WebAssembly

GitHub Demo

在 .NET 7,Blazor 分為 Blazor Server 以及 Blazor WebAssembly,本次選擇的是完全部署於 Client 端的 Web Assembly。

Features Blazor Server Blazor WebAssembly
執行速度 初始執行快,因為大部分 Code 在 Server 執行 初始執行慢,需要下載整個應用 Dependency
部署複雜度 簡單,所有 Code 在 Server 執行 複雜,需要前端部署
資源利用 Server 管理資源 利用 Client Side 計算能力
數據保護 較容易保護敏感數據 需要 Client Side 安全措施
網路依賴 高度依賴網路連線品質 下載後可以 Offline Running
伺服器負擔 隨用戶增加而增加 主要在 Client Side 處理
延遲敏感 對網路延遲敏感 較少受網路延遲影響
適用場景 快速部署、頻繁 Server 交互的應用 高即時性、需要 Offlline 功能的應用

專案建立之後,馬上開寫,延續著之前寫 Vue.js 的經驗,一開始就是單檔開發,完全沒考慮 Components 設計的 😁

而在 Blazor 當中是撰寫 /Pages/Index.razor 因為有 ASP.NET MVC 寫 Razor 的經驗,之前也把玩過 Razor Pages 專案類型,因此寫起來完全沒問題。也因為 Vue.js 的經驗,大概知曉上面 Template 下面 Code 的區隔,只差沒有 Scoped CSS 😅

以往要透過 JS 的處理,延續著 Vue.js 的經驗,找到如何在 Blazor Razor 使用 Two-Way Binding,一切的工作就輕鬆的寫了起來。

Pages/Index.razor

<td>@(item.Defence - (item.IsEnhanceable ? item.Enhanced : 0))</td>
<td>
  @if (item.IsEnhanceable)
  {
      <button 
        class="btn btn-outline-info me-lg-3 btn-sm @((appRuntime.CoolDownState) ? "disabled" : "" )"
        @onclick="() => Enhance(item)">防卷</button>
  }
</td>

邏輯控制什麼的就是 Razor 習慣的語法,Click 事件 則透過 @onclick 搭配 lambda 的方式來註冊使用。

<div class="col-12 col-lg-4 d-flex align-items-center">
    <label>LV:</label>
    <input type="number" @bind="appRuntime.Level" class="form-control" />
</div>
<div class="col-12 col-lg-4 mt-3 mt-lg-0 d-flex align-items-center">
    <span>AC:</span>
    <input type="number" value="@sumDefence()" class="form-control" disabled/>
</div>
public int sumDefence()
{
    var baseDef = 10;
    var defFromLevelDex = (int)Math.Floor((int)appRuntime.Level / 4.0);
    foreach (var equip in appRuntime.Equipments)
    {
        baseDef = baseDef + equip.Defence - equip.Enhanced;
    }
    baseDef -= defFromLevelDex;
    return baseDef;
}

Two-Way Binding 根據 Input 的操作,對應的動態計算,只需要在 input 透過 @Bind 就可以輕鬆達成。

寫著寫著,因為狀態管理的處理多了起來,於是就獨立抽出來,延續 ASP.NET MVC 的習慣,用 Models 來管理資料模型,也因為應用系統不大,沒有再額外做 Service 或者 Business Logic 的分層,直接在 Models 集合起來。

/Models/AppRuntime.cs

public class AppRuntime
{
    public bool CoolDownState { get; set; } = false;

    public int Level { get; set; } = 48;
    public int Dex { get; set; } = 18;
    public int Experience { get; set; } = 0;

    public List<Equipment> Equipments { get; set; }

    public AppRuntime()
    {
        Equipments = new List<Equipment> {
            new Equipment { Name = "艾爾穆的祝福", Safe = 6, Defence = -6, Image = "helmet" },
            new Equipment { Name = "精靈金屬盔甲", Safe = 6, Defence = -6, Image = "armor" },
        };
    }
}

Program.cs

builder.Services.AddScoped<AppRuntime>();

需要共用的狀態資料,透過 IoC 的方式注入。

/Pages/Index.razor

@using LineageEnhance.Models
@inject AppRuntime appRuntime

對於需要使用的頁面直接 @inject 以及 @using 就可以使用,超級方便 😍

Async & Await

而這次實驗的 Apps,想要使用非同步的效果,在工作執行後,先暫停重複觸發工作,但同時又可以進行其他類型工作的方式。

以往暫停都是透過 Thread.Sleep,但這樣會讓所有的操作都停下來等待,不是非同步的效果。

運用共用的狀態 CoolState,當 Button 觸發,會觸發非同步的 Method,但不必 Await,而是會任由等待,於是同時就可以做其他的事情。

/Pages/Index.razor

private async Task Enhance(Equipment equipment, bool blessed = false)
{
    if (!equipment.IsEnhanceable || appRuntime.CoolDownState == true)
    {
        return;
    }

    appRuntime.CoolDownState = true;

    var enhanceSystem = new EnhanceSystem(appRuntime);
    await enhanceSystem.Enhance(equipment, blessed).ConfigureAwait(false);
}

這邊的 Enhance 類似 Controller 的用途,主要先將按鈕的狀態透過 appRuntime.CoolDownState 調整為不可使用,而 Controller 本身也會檢查如果是不允許使用的狀態會直接 return。

實際的運作則是透過已經抽出來的 EnhanceSystem.Enhance 來進行,其實是不需要使用 await 但 IDE 不喜歡,IDE 認為 Enhace 是 async Task 就應該要有 await,但可以透過 ConfigureAwait(false) 的方式來解除 😅

/Models/EnhanceSystem.cs

public async Task Enhance(Equipment equipment, bool blessed = false)
{
    ...

    await Task.Delay(25);
    _appRuntime.CoolDownState = false;
}

而因為 CoolDownState 是實作刻意的,透過 await Task.Delay 來達成,而非使用 Thread.Sleep,最後也在 EnhanceSystem.Enhance 的結尾恢復 CoolDownState

Deploy to GitHub Page with GitHub Action

  1. Settings "Workflow permissions" : "Read and write permissions" : 解決 GitHub Action 執行 發生 '/usr/bin/git' failed with exit code 128 權限不足的問題

  1. 新增 .nojekyll : 直接在 /wwwroot 加入空檔案
  2. 新增 404.html : 透過 GitHub Action 去處理
  3. 修正 index.html base href: 透過 GitHub Action
name: .NET

on:
  push:
    branches: [ master ]

jobs:
  deploy-to-github-pages:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Setup .NET
      uses: actions/setup-dotnet@v2
      with:
        dotnet-version: 7.0.x
    
    - name: Publish .NET Core Project
      run: dotnet publish LineageEnhance.csproj -c Release -o release --nologo

    - name: Modify index.html
      run: sed -i 's/\/LineageEnhance"/\/LineageEnhance\/"/' release/wwwroot/index.html
      
    - name: copy index.html to 404.html
      run: cp release/wwwroot/index.html release/wwwroot/404.html
      
    - name: Commit wwwroot to GitHub Pages
      uses: JamesIves/github-pages-deploy-[email protected]
      with:
          BRANCH: gh-pages
          FOLDER: release/wwwroot

參考 PuTaoNini 所分享的將 Blazor WebAssembly 部署到 GitHub Pages