基於 abp vNext 和 .NET Core 開發部落格專案 - Blazor 實戰系列(三)

阿星Plus發表於2020-06-11

系列文章

  1. 基於 abp vNext 和 .NET Core 開發部落格專案 - 使用 abp cli 搭建專案
  2. 基於 abp vNext 和 .NET Core 開發部落格專案 - 給專案瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發部落格專案 - 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發部落格專案 - 資料訪問和程式碼優先
  5. 基於 abp vNext 和 .NET Core 開發部落格專案 - 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發部落格專案 - 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發部落格專案 - 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發部落格專案 - 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發部落格專案 - 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發部落格專案 - 使用Redis快取資料
  11. 基於 abp vNext 和 .NET Core 開發部落格專案 - 整合Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發部落格專案 - 用AutoMapper搞定物件對映
  13. 基於 abp vNext 和 .NET Core 開發部落格專案 - 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發部落格專案 - 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發部落格專案 - 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發部落格專案 - 部落格介面實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發部落格專案 - 部落格介面實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發部落格專案 - 部落格介面實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發部落格專案 - 部落格介面實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發部落格專案 - 部落格介面實戰篇(五)
  21. 基於 abp vNext 和 .NET Core 開發部落格專案 - Blazor 實戰系列(一)
  22. 基於 abp vNext 和 .NET Core 開發部落格專案 - Blazor 實戰系列(二)

上一篇完成了部落格的主題切換,選單和二維碼的顯示與隱藏功能,本篇繼續完成分頁查詢文章列表的資料展示。

新增頁面

現在點選頁面上的連結,都會提示錯誤訊息,因為沒有找到對應的路由地址。先在Pages下建立五個資料夾:Posts、Categories、Tags、Apps、FriendLinks。

然後在對應的資料夾下新增Razor元件。

  • Posts資料夾:文章列表頁面Posts.razor、根據分類查詢文章列表頁面Posts.Category.razor、根據標籤查詢文章列表頁面Posts.Tag.razor、文章詳情頁Post.razor
  • Categories資料夾:分類列表頁面Categories.razor
  • Tags資料夾:標籤列表頁面Tags.razor
  • Apps資料夾:Apps.razor準備將友情連結入口放在裡面
  • FriendLinks資料夾:友情連結列表頁面FriendLinks.razor

先分別建立上面這些Razor元件,差不多除了後臺CURD的頁面就這些了,現在來逐個突破。

不管三七二十一,先把所有頁面的路由給確定了,指定頁面路由使用 @page 指令,官方文件說不支援可選引數,但是可以支援多個路由規則。

預設先什麼都不顯示,可以將之前的載入中圈圈寫成一個元件,供每個頁面使用。

在Shared資料夾新增元件Loading.razor

<!--Loading.razor-->
<div class="loader"></div>
//Posts.razor
@page "/posts/"
@page "/posts/page/{page:int}"
@page "/posts/{page:int}"

<Loading />

@code {
    /// <summary>
    /// 當前頁碼
    /// </summary>
    [Parameter]
    public int? page { get; set; }
}

這裡我加了三條,可以匹配沒有page引數,帶page引數的,/posts/page/{page:int}這個大家可以不用加,我是用來相容目前線上的部落格路由的。總的來說可以匹配到:/posts/posts/1/posts/page/1這樣的路由。

//Posts.Category.razor
@page "/category/{name}"

<Loading />

@code {
    /// <summary>
    /// 分類名稱引數
    /// </summary>
    [Parameter]
    public string name { get; set; }
}

根據分類名稱查詢文章列表頁面,name當作分類名稱引數,可以匹配到類似於:/category/aaa/category/bbb這樣的路由。

//Posts.Tag.razor
@page "/tag/{name}"

<Loading />

@code {
    /// <summary>
    /// 標籤名稱引數
    /// </summary>
    [Parameter]
    public string name { get; set; }
}

這個根據標籤名稱查詢文章列表頁面和上面差不多一樣,可以匹配到:/tag/aaa/tag/bbb這樣的路由。

//Post.razor
@page "/post/{year:int}/{month:int}/{day:int}/{name}"

<Loading />

@code {
    [Parameter]
    public int year { get; set; }

    [Parameter]
    public int month { get; set; }

    [Parameter]
    public int day { get; set; }

    [Parameter]
    public string name { get; set; }
}

文章詳情頁面的路由有點點複雜,以/post/開頭,加上年月日和當前文章的語義化名稱組成。分別新增了四個引數年月日和名稱,用來接收URL的規則,使用int來設定路由的約束,最終可以匹配到路由:/post/2020/06/09/aaa/post/2020/06/9/bbb這樣的。

//Categories.razor
@page "/categories"

<Loading />

//Tags.razor
@page "/tags"

<Loading />

//FriendLinks.razor
@page "/friendlinks"

<Loading />

分類、標籤、友情連結都是固定的路由,像上面這樣就不多說了,然後還剩一個Apps.razor

//Apps.razor
@page "/apps"

<div class="container">
    <div class="post-wrap">
        <h2 class="post-title">-&nbsp;Apps&nbsp;-</h2>
        <ul>
            <li>
                <a target="_blank" href="https://support.qq.com/products/75616"><h3>吐個槽_留言板</h3></a>
            </li>
            <li>
                <NavLink href="/friendlinks"><h3>友情連結</h3></NavLink>
            </li>
        </ul>
    </div>
</div>

在裡面新增了一個友情連結的入口,和一個 騰訊兔小巢 的連結,歡迎大家吐槽留言噢。

1

現在可以執行一下看看,點選所有的連結都不會提示錯誤,只要路由匹配正確就會出現載入中的圈圈了。

文章列表

在做文章列表的資料繫結的時候遇到了大坑,有前端開發經驗的都知道,JavaScript弱型別語言中接收json資料隨便玩,但是在Blazor中我試了下動態接受傳遞過來的JSON資料,一直報錯壓根執行不起來。所以在請求api接收資料的時候需要指定接收物件,那就好辦了我就直接引用API中的.Application.Contracts就行了啊,但是緊接著坑又來了,目標框架對不上,引用之後也執行不起來,這裡應該是之前沒有設計好。

於是,我就想了一個折中的辦法吧,將API中的返回物件可以用到的DTO先手動拷貝一份到Blazor專案中,後續可以考慮將公共的返回模型做成Nuget包,方便使用。

那麼,最終就是在Blazor中新增一個Response資料夾,用來放接收物件,裡面的內容看圖:

2

有點傻,先這樣解決,後面在做進一步的優化吧。

將我們複製進來的東東,在_Imports.razor中新增引用。

//_Imports.razor
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Meowv.Blog.BlazorApp.Shared
@using Response.Base
@using Response.Blog

@inject HttpClient Http
@inject Commons.Common Common

@inject HttpClient Http:注入HttpClient,用它來請求API資料。

現在有了接收物件,接下來就好辦了,來實現分頁查詢文章列表吧。

先新增三個私有變數,限制條數,就是一次載入文章的數量,總頁碼用來計算分頁,還有就是API的返回資料的接收型別引數。

/// <summary>
/// 限制條數
/// </summary>
private int Limit = 15;

/// <summary>
/// 總頁碼
/// </summary>
private int TotalPage;

/// <summary>
/// 文章列表資料
/// </summary>
private ServiceResult<PagedList<QueryPostDto>> posts;

然後當頁面初始化的時候,去載入資料,渲染頁面,因為page引數可能存在為空的情況,所以要考慮進去,當為空的時候給他一個預設值1。

/// <summary>
/// 初始化
/// </summary>
protected override async Task OnInitializedAsync()
{
    // 設定預設值
    page = page.HasValue ? page : 1;

    await RenderPage(page);
}

/// <summary>
/// 點選頁碼重新渲染資料
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
private async Task RenderPage(int? page)
{
    // 獲取資料
    posts = await Http.GetFromJsonAsync<ServiceResult<PagedList<QueryPostDto>>>($"/blog/posts?page={page}&limit={Limit}");

    // 計算總頁碼
    TotalPage = (int)Math.Ceiling((posts.Result.Total / (double)Limit));
}

在初始化方法中設定預設值,呼叫RenderPage(...)獲取到API返回來的資料,並根據返回資料計算出頁碼,這樣就可以繫結資料了。

@if (posts == null)
{
    <Loading />
}
else
{
    <div class="post-wrap archive">
        @if (posts.Success && posts.Result.Item.Any())
        {
            @foreach (var item in posts.Result.Item)
            {
                <h3>@item.Year</h3>
                @foreach (var post in item.Posts)
                {
                    <article class="archive-item">
                        <NavLink href="@("/post" + post.Url)">@post.Title</NavLink>
                        <span class="archive-item-date">@post.CreationTime</span>
                    </article>
                }
            }
            <nav class="pagination">
                @for (int i = 1; i <= TotalPage; i++)
                {
                    var _page = i;

                    if (page == _page)
                    {
                        <span class="page-number current">@_page</span>
                    }
                    else
                    {
                        <a class="page-number" @onclick="@(() => RenderPage(_page))" href="/posts/@_page">@_page</a>
                    }
                }
            </nav>
        }
        else
        {
            <ErrorTip />
        }
    </div>
}

在載入資料的時候肯定是需要一個等待時間的,因為不可抗拒的原因資料還沒載入出來的時候,可以讓它先轉一會圈圈,當posts不為空的時候,再去繫結資料。

在繫結資料,for迴圈頁碼的時候我又遇到了一個坑?,這裡不能直接去使用變數i,必須新建一個變數去接受它,不然我傳遞給RenderPage(...)的引數就會是錯的,始終會取到最後一次迴圈的i值。

當判斷資料出錯或者沒有資料的時候,在把錯誤提示<ErrorTip />扔出來顯示。

做到這裡,可以去執行看看了,肯定會報錯,因為還有一個重要的東西沒有改,就是我們介面的BaseAddress,在Program.cs中,預設是當前Blazor專案的執行地址。

我們需要先將API專案執行起來,拿到地址配置在Program.cs中,因為現在還是本地開發,有多種辦法可以解決,可以將.HttpApi.Hosting設為啟動專案直接執行起來,也可以使用命令直接dotnet run

我這裡為了方便,直接釋出在IIS中,後續只要電腦開啟就可以訪問了,你甚至選擇其它任何你能想到的方式。

關於如何釋出這裡先不做展開,有機會的話寫一篇將.net core開發的專案釋出到 Windows、Linux、Docker 的教程吧。

3

所以我的Program.cs中配置如下:

//Program.cs
using Meowv.Blog.BlazorApp.Commons;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace Meowv.Blog.BlazorApp
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            var baseAddress = "https://localhost";

            if (builder.HostEnvironment.IsProduction())
                baseAddress = "https://api.meowv.com";

            builder.Services.AddTransient(sp => new HttpClient
            {
                BaseAddress = new Uri(baseAddress)
            });

            builder.Services.AddSingleton(typeof(Common));

            await builder.Build().RunAsync();
        }
    }
}

baseAddress預設為本地開發地址,使用builder.HostEnvironment.IsProduction()判斷是否為線上正式生產環境,改變baseAddress地址。

4

現在可以看到已經可以正常獲取資料,並且翻頁也是OK的,然後又出現了一個新的BUG?。

解決BUG

細心的可以發現,當我點選頭部元件的Postsa 標籤選單時候,頁面沒有發生變化,只是路由改變了。

思來想去,我決定使用NavigationManager這個URI和導航狀態幫助程式來解決,當點選頭部的Postsa 標籤選單直接重新整理頁面得了。

Common.cs中使用建構函式注入NavigationManager,然後新增一個跳轉指定URL的方法。

/// <summary>
/// 跳轉指定URL
/// </summary>
/// <param name="uri"></param>
/// <param name="forceLoad">true,繞過路由重新整理頁面</param>
/// <returns></returns>
public async Task RenderPage(string url, bool forceLoad = true)
{
    _navigationManager.NavigateTo(url, forceLoad);

    await Task.CompletedTask;
}

forceLoad = true的時候,將會繞過路由直接強制重新整理頁面,如果forceLoad = false,則不會重新整理頁面。

緊接著在Header.razor中修改程式碼,新增點選事件。

@*<NavLink class="menu-item" href="posts">Posts</NavLink>*@

<NavLink class="menu-item" href="posts" @onclick="@(async () => await Common.RenderPage("posts"))">Posts</NavLink>

總算是搞定,完成了分頁查詢文章列表的資料繫結,今天就到這裡吧,未完待續...

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

相關文章