基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
系列文章
- 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
- 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
- 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
- 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
- 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
- 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
- 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
- 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
- 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
- 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
- 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
- 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
- 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
- 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
- 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
- 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
- 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
- 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
- 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
- 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
- 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
- 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
- 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
- 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
上一篇完成了分類標籤友鏈的列表查詢頁面數據綁定,還剩下一個文章詳情頁的數據沒有綁,現在簡單的解決掉。
文章詳情
之前已經添加了四個參數:year、month、day、name,用來組成我們最終的URL,繼續添加一個參數用來接收API返回的數據。
[Parameter]
public int year { get; set; }
[Parameter]
public int month { get; set; }
[Parameter]
public int day { get; set; }
[Parameter]
public string name { get; set; }
/// <summary>
/// URL
/// </summary>
private string url => $"/{year}/{(month >= 10 ? month.ToString() : $"0{month}")}/{(day >= 10 ? day.ToString() : $"0{day}")}/{name}/";
/// <summary>
/// 文章詳情數據
/// </summary>
private ServiceResult<PostDetailDto> post;
然後在初始化方法OnInitializedAsync()
中請求數據。
/// <summary>
/// 初始化
/// </summary>
protected override async Task OnInitializedAsync()
{
// 獲取數據
post = await Http.GetFromJsonAsync<ServiceResult<PostDetailDto>>($"/blog/post?url={url}");
}
現在拿到了post數據,然後在HTML中綁定即可。
@if (post == null)
{
<Loading />
}
else
{
@if (post.Success)
{
var _post = post.Result;
<article class="post-wrap">
<header class="post-header">
<h1 class="post-title">@_post.Title</h1>
<div class="post-meta">
Author: <a itemprop="author" rel="author" href="javascript:;">@_post.Author</a>
<span class="post-time">
Date: <a href="javascript:;">@_post.CreationTime</a>
</span>
<span class="post-category">
Category:<a href="/category/@_post.Category.DisplayName/">@_post.Category.CategoryName</a>
</span>
</div>
</header>
<div class="post-content" id="content">
@((MarkupString)_post.Html)
</div>
<section class="post-copyright">
<p class="copyright-item">
<span>Author:</span>
<span>@_post.Author</span>
</p>
<p class="copyright-item">
<span>Permalink:</span>
<span><a href="/post@_post.Url">//meowv.com/post@_post.Url</a></span>
</p>
<p class="copyright-item">
<span>License:</span>
<span>本文採用<a target="_blank" href="//creativecommons.org/licenses/by-nc-nd/4.0/"> 知識共享 署名-非商業性使用-禁止演繹(CC BY-NC-ND)國際許可協議 </a>進行許可</span>
</p>
</section>
<section class="post-tags">
<div>
<span>Tag(s):</span>
<span class="tag">
@if (_post.Tags.Any())
{
@foreach (var tag in _post.Tags)
{
<a href="/tag/@tag.DisplayName/"># @tag.TagName</a>
}
}
</span>
</div>
<div>
<a @onclick="async () => await Common.BaskAsync()">back</a>
<span>· </span>
<a href="/">home</a>
</div>
</section>
<section class="post-nav">
@if (_post.Previous != null)
{
<a class="prev"
rel="prev"
@onclick="@(async () => await Common.NavigateTo($"/post{_post.Previous.Url}, true))"
href="/post@_post.Previous.Url">@_post.Previous.Title</a>
}
@if (_post.Next != null)
{
<a class="next"
rel="next"
@onclick="@(async () => await Common.NavigateTo($"/post{_post.Next.Url}", true))"
href="/post@_post.Next.Url">
@_post.Next.Title
</a>
}
</section>
</article>
}
else
{
<ErrorTip />
}
}
其中有幾個地方需要注意一下:
我們從post對象中取到的文章內容HTML,直接顯示是不行了,需要將其解析為HTML標籤,需要用到MarkupString
。
然後頁面上有一個後退按鈕,這裡我在Common.cs
中寫了一個方法來實現。
/// <summary>
/// 後退
/// </summary>
/// <returns></returns>
public async Task BaskAsync()
{
await InvokeAsync("window.history.back");
}
還有就是上一篇和下一篇的問題,將具體的URL傳遞給NavigateTo()
方法,然後跳轉過去即可。
在Common.cs
中將之前文章創建RenderPage()
方法修改成NavigateTo()
。這個命名更好一點。
/// <summary>
/// 跳轉指定URL
/// </summary>
/// <param name="uri"></param>
/// <param name="forceLoad">true,繞過路由刷新頁面</param>
/// <returns></returns>
public async Task NavigateTo(string url, bool forceLoad = false)
{
_navigationManager.NavigateTo(url, forceLoad);
await Task.CompletedTask;
}
現在數據算是綁定完了,但是遇到了一個大問題,就是詳情頁面的樣式問題,因為用到了Markdown,所以之前是加載了許多JS文件來處理的。那麼現在肯定行不通了,所以關於詳情頁的樣式問題暫時擱淺,讓我尋找一下好多解決方式。
現在顯示是沒有問題了,就是不太好看,還有關於添加文章的功能,不知道有什麼好的 Markdown 編輯器可以推薦我使用。
到這裡Blazor的前端展示頁面已經全部弄完了,接下來開始寫後台相關的頁面。
後台首頁
關於後台管理的所有頁面都放在Admin文件夾下,在Pages文件夾下新建Admin文件夾,然後先添加兩個組件頁面:Admin.razor
、Auth.razor
。
Admin.razor
為後台管理的首頁入口,我們在裏面直接添加幾個預知的鏈接並設置其路由。
@page "/admin"
<div class="post-wrap">
<h2 class="post-title">- 博客內容管理 -</h2>
<ul>
<li>
<a href="/admin/post"><h3>📝~~~ 新增文章 ~~~📝</h3></a>
</li>
<li>
<a href="/admin/posts"><h3>📗~~~ 文章管理 ~~~📗</h3></a>
</li>
<li>
<a href="/admin/categories"><h3>📕~~~ 分類管理 ~~~📕</h3></a>
</li>
<li>
<a href="/admin/tags"><h3>📘~~~ 標籤管理 ~~~📘</h3></a>
</li>
<li>
<a href="/admin/friendlinks"><h3>📒~~~ 友鏈管理 ~~~📒</h3></a>
</li>
</ul>
</div>
裏面的a標籤所對應的頁面還沒有添加,等做到的時候再加,先手動訪問這個頁面看看,當成功授權後就跳到這個頁面來。
認證授權
關於授權,因為之前在API中已經完成了基於Github的JWT模式的認證授權模式,所以這裡我想做一個無感的授權功能,為什麼說無感呢,因為在我使用GitHub登錄的過程中,如果之前已經登錄過且沒有清除瀏覽器cookie數據,下次再登錄的時候會默認直接登錄成功,從而達到無感的。
實現邏輯其實也很簡單,我這裡用到了Common.cs
中之前添加的公共方法設置和獲取localStorage
的方法,我會將token等信息放入localStorage
中。
我設置的路由是:/auth
。這個路由需要和 GitHub OAuth App 的回調地址一致,當登錄成功,會回調跳到配置的頁面並攜帶code參數。
在獲取請求參數這塊需要引用一個包:Microsoft.AspNetCore.WebUtilities
,添加好後在_Imports.razor
添加引用:@using Meowv.Blog.BlazorApp.Shared
。
默認還是顯示加載中的組件:<Loading />
。
然後在@code{}
中編寫代碼,添加頁面初始化函數。
/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
// localStorage中access_token值
var access_token = await Common.GetStorageAsync("access_token");
// access_token有值
if (!string.IsNullOrEmpty(access_token))
{
// 獲取token
var _token = await Http.GetFromJsonAsync<ServiceResult<string>>($"/auth/token?access_token={access_token}");
if (_token.Success)
{
// 將token存入localStorage
await Common.SetStorageAsync("token", _token.Result);
// 跳轉至後台首頁
await Common.NavigateTo("/admin");
}
else
{
// access_token失效,或者請求失敗的情況下,重新執行一次驗證流程
await AuthProcessAsync();
}
}
else //access_token為空
{
await AuthProcessAsync();
}
}
先去獲取localStorage
中的access_token值,肯定會有兩種情況,有或者沒有,然後分別去走不同的邏輯。
當access_token有值,就可以直接拿access_token去取token的值,理想情況請求成功拿到了token,這時候可以將token存到瀏覽器中,然後正常跳轉至後台管理首頁,還有就是取token失敗了,失敗了就有可能是access_token過期了或者出現異常情況,這時候我們不去提示錯誤,直接拋棄所有,重新來一遍認證授權的流程,放在一個單獨的方法中AuthProcessAsync()
。
而當access_token沒值那就好辦了,也去來一遍認證授權的流程即可。
驗證流程AuthProcessAsync()
的代碼。
/// <summary>
/// 驗證流程
/// </summary>
/// <returns></returns>
private async Task AuthProcessAsync()
{
// 當前URI對象
var uri = await Common.CurrentUri();
// 是否回調攜帶了code參數
bool hasCode = QueryHelpers.ParseQuery(uri.Query).TryGetValue("code", out Microsoft.Extensions.Primitives.StringValues code);
if (hasCode)
{
var access_token = await Http.GetFromJsonAsync<ServiceResult<string>>($"/auth/access_token?code={code}");
if (access_token.Success)
{
// 將access_token存入localStorage
await Common.SetStorageAsync("access_token", access_token.Result);
var token = await Http.GetFromJsonAsync<ServiceResult<string>>($"/auth/token?access_token={access_token.Result}");
if (token.Success)
{
// 將token存入localStorage
await Common.SetStorageAsync("token", token.Result);
// 成功認證授權,跳轉至後台管理首頁
await Common.NavigateTo("/admin");
}
else
{
// 沒有權限的人,回到首頁去吧
await Common.NavigateTo("/");
// 輸出提示信息
Console.WriteLine(token.Message);
}
}
else
{
// 出錯了,回到首頁去吧
await Common.NavigateTo("/");
// 輸出提示信息
Console.WriteLine(access_token.Message);
}
}
else
{
// 獲取第三方登錄地址
var loginAddress = await Http.GetFromJsonAsync<ServiceResult<string>>("/auth/url");
// 跳轉到登錄頁面
await Common.NavigateTo(loginAddress.Result);
}
}
驗證流程的邏輯先獲取當前URI對象,判斷URI中是否攜帶了code參數,從而可以知道當前頁面是回調的過來的還是直接請求的,獲取當前URI對象放在Common.cs
中。
/// <summary>
/// 獲取當前URI對象
/// </summary>
/// <returns></returns>
public async Task<Uri> CurrentUri()
{
var uri = _navigationManager.ToAbsoluteUri(_navigationManager.Uri);
return await Task.FromResult(uri);
}
在剛才添加的包Microsoft.AspNetCore.WebUtilities
中為我們封裝好了解析URI參數的方法。
使用QueryHelpers.ParseQuery(...)
獲取code參數的值。
當沒有值的時候,直接取請求登錄地址,然後如果登錄成功就會跳轉到攜帶code參數的回調頁面。這樣流程就又回到了 驗證流程 開始的地方了。
登錄成功,此時code肯定就有值了,那麼直接根據code獲取access_token,存入localStorage
,正常情況拿到access_token就去生成token,然後也存入localStorage
,成功授權可以跳到後台管理首頁了。
其中如果有任何一個環節出現問題,直接跳轉到網站首頁去。如果授權不成功肯定是你在瞎搞(不接受任何反駁🤣🤣),趕緊回到首頁去吧。
現在流程走完,去看看效果。
GitHub在國內的情況大家知道,有時候慢甚至打不開,有時候還是挺快的,還好今天沒掉鏈子,我遇到過好幾次壓根打不開的情況,獲取可以針對網絡不好的時候我們換成其它的驗證方式,這個以後有機會再優化吧。
驗證組件
這個時候會發現,其實我們壓根不需要打開/auth
走驗證流程,直接訪問/admin
就可以進來管理首頁,這是極其不合理的。那豈不是誰知道地址誰都能進來瞎搞了。所以我們可以在 Shared 文件夾下添加一個權限驗證的組件:AdminLayout.razor
。用來判斷是否真的登錄了。
新建一個bool類型的變量 isLogin。默認肯定是false,此時可以讓頁面轉圈圈,使用<Loading />
組件。當isLogin = true
的時候我們才展示具體的HTML內容。
那麼就需要用到服務端組件RenderFragment
,他有一個固定的參數名稱ChildContent
。
判斷是否登錄的方法可以寫在初始化方法中,這裡還少了一個API,就是判斷當前token的值是否合法,合法就表示已經成功執行了驗證流程了。token不存在或者不合法,直接拒絕請求返回到首頁去吧。
整個代碼如下:
@if (!isLogin)
{
<Loading />
}
else
{
@ChildContent
}
@code {
/// <summary>
/// 展示內容
/// </summary>
[Parameter]
public RenderFragment ChildContent { get; set; }
/// <summary>
/// 是否登錄
/// </summary>
private bool isLogin { get; set; }
/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
var token = await Common.GetStorageAsync("token");
if (string.IsNullOrEmpty(token))
{
isLogin = false;
await Common.NavigateTo("/");
}
else
{
// TODO:判斷token是否合法,先默認都是正確的
isLogin = true;
}
}
}
使用這個組件也很方便了,我們後台所有頁面都引用AdminLayout
,將展示內容傳遞給就行了,成功驗證後就會展示HTM內容。
在Admin.razor
中使用。
@page "/admin"
<AdminLayout>
<div class="post-wrap">
<h2 class="post-title">- 博客內容管理 -</h2>
<ul>
<li>
<a href="/admin/post"><h3>📝~~~ 新增文章 ~~~📝</h3></a>
</li>
<li>
<a href="/admin/posts"><h3>📗~~~ 文章管理 ~~~📗</h3></a>
</li>
<li>
<a href="/admin/categories"><h3>📕~~~ 分類管理 ~~~📕</h3></a>
</li>
<li>
<a href="/admin/tags"><h3>📘~~~ 標籤管理 ~~~📘</h3></a>
</li>
<li>
<a href="/admin/friendlinks"><h3>📒~~~ 友鏈管理 ~~~📒</h3></a>
</li>
</ul>
</div>
</AdminLayout>
現在清除掉瀏覽器緩存,去請求/admin
試試。
完美,比較簡單的實現了驗證是否登錄的組件。其中還有許多地方可以優化,就交給大家去自行完成了😎。
開源地址://github.com/Meowv/Blog/tree/blog_tutorial