系列文章
- 基於 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">https://meowv.com/post@_post.Url</a></span>
</p>
<p class="copyright-item">
<span>License:</span>
<span>本文采用<a target="_blank" href="http://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
試試。
完美,比較簡單的實現了驗證是否登入的元件。其中還有許多地方可以優化,就交給大家去自行完成了?。