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

阿星Plus發表於2020-06-10

系列文章

  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 實戰系列(一)

上一篇搭建了 Blazor 專案並將整體框架改造了一下,本篇將完成用 C# 程式碼代替 JavaScript 實現幾個小功能,說是代替但並不能完全不用 JavaScript,應該說是儘量不用吧。

二維碼顯示與隱藏

0

可以看到,當我滑鼠移入的時候顯示二維碼,移出的時候隱藏二維碼。

這個功能如果是用JavaScript來完成的話,肯定首先想到的是HTML的 Mouse 事件屬性,那麼在Blazor中也是一樣的,給我們實現了各種on*事件。

開啟index.razor頁面,給微信圖示那個 NavLink 標籤新增兩個事件,@onmouseover@onmouseout

...
<NavLink class="link-item weixin" title="掃碼關注微信公眾號:『阿星Plus』檢視更多。" @onmouseover="Hover" @onmouseout="Hover">
    <i class="iconfont iconweixin"></i>
</NavLink>
...

當滑鼠移入移出的時候都執行我們自定義的一個方法Hover()

C# 程式碼寫在@code{}花括號中,實現顯示和隱藏原理是利用css,預設是隱藏的,當顯示的時候將具有隱藏屬性的class值去掉就可以了。

所以,可以新增兩個欄位,一個用於判斷當前是否處於隱藏狀態,一個用來儲存class的值。

/// <summary>
/// 是否隱藏
/// </summary>
private bool IsHidden = true;

/// <summary>
/// 二維碼CSS
/// </summary>
private string QrCodeCssClass => IsHidden ? "hidden" : null;

IsHidden = trueQrCodeCssClass = "hidden",當IsHidden = falseQrCodeCssClass = null

那麼在Hover()方法中,不斷修改IsHidden的值就可以實現效果了。

/// <summary>
/// 滑鼠移入移出操作
/// </summary>
private void Hover() => IsHidden = !IsHidden;

最後將QrCodeCssClass變數賦值給二維碼圖片所在的div上。

...
<div class="qrcode @QrCodeCssClass">
    <img src="https://static.meowv.com/images/wx_qrcode.jpg" />
</div>
...

大功告成,index.razor完整程式碼如下:

@page "/"

<div class="main">
    <div class="container">
        <div class="intro">
            <div class="avatar">
                <a href="javascript:;"><img src="https://static.meowv.com/images/avatar.jpg"></a>
            </div>
            <div class="nickname">阿星Plus</div>
            <div class="description">
                <p>
                    生命不息,奮鬥不止
                    <br>Cease to struggle and you cease to live
                </p>
            </div>
            <div class="links">
                <NavLink class="link-item" title="Posts" href="posts">
                    <i class="iconfont iconread"></i>
                </NavLink>
                <NavLink target="_blank" class="link-item" title="Notes" href="https://notes.meowv.com/">
                    <i class="iconfont iconnotes"></i>
                </NavLink>
                <NavLink target="_blank" class="link-item" title="API" href="https://api.meowv.com/">
                    <i class="iconfont iconapi"></i>
                </NavLink>
                <NavLink class="link-item" title="Manage" href="/account/auth">
                    <i class="iconfont iconcode"></i>
                </NavLink>
                <NavLink target="_blank" class="link-item" title="Github" href="https://github.com/Meowv/">
                    <i class="iconfont icongithub"></i>
                </NavLink>
                <NavLink class="link-item weixin" title="掃碼關注微信公眾號:『阿星Plus』檢視更多。" @onmouseover="Hover" @onmouseout="Hover">
                    <i class="iconfont iconweixin"></i>
                </NavLink>
                <div class="qrcode @QrCodeCssClass">
                    <img src="https://static.meowv.com/images/wx_qrcode.jpg" />
                </div>
            </div>
        </div>
    </div>
</div>

@code {
    /// <summary>
    /// 是否隱藏
    /// </summary>
    private bool IsHidden = true;

    /// <summary>
    /// 二維碼CSS
    /// </summary>
    private string QrCodeCssClass => IsHidden ? "hidden" : null;

    /// <summary>
    /// 滑鼠移入移出操作
    /// </summary>
    private void Hover() => IsHidden = !IsHidden;
}

選單顯示與隱藏

2

選單是在小螢幕上才會出現的,相信看完了二維碼的顯示與隱藏,這個選單的顯示與隱藏就好辦了吧,實現方法是一樣的,選單按鈕是在頭部元件Header.razor中的,包括主題切換功能,所以下面程式碼都在Header.razor裡面。

@code {
    /// <summary>
    /// 下拉選單是否開啟
    /// </summary>
    private bool collapseNavMenu = false;

    /// <summary>
    /// 導航選單CSS
    /// </summary>
    private string NavMenuCssClass => collapseNavMenu ? "active" : null;

    /// <summary>
    /// 顯示/隱藏 選單
    /// </summary>
    private void ToggleNavMenu() => collapseNavMenu = !collapseNavMenu;
}

預設是不開啟的,collapseNavMenu = false。然後根據collapseNavMenu值為NavMenuCssClass給定不同的class。

...
<nav class="navbar-mobile">
    <div class="container">
        <div class="navbar-header">
            <div>
                <NavLink class="menu-item" href="" Match="NavLinkMatch.All">?阿星Plus</NavLink>
                <NavLink>&nbsp;·&nbsp;Light</NavLink>
            </div>
            <div class="menu-toggle" @onclick="ToggleNavMenu">&#9776; Menu</div>
        </div>
        <div class="menu @NavMenuCssClass">
            <NavLink class="menu-item" href="posts">Posts</NavLink>
            <NavLink class="menu-item" href="categories">Categories</NavLink>
            <NavLink class="menu-item" href="tags">Tags</NavLink>
            <NavLink class="menu-item apps" href="apps">Apps</NavLink>
        </div>
    </div>
</nav>
...

與二維碼顯示與隱藏唯一區別就是這裡是點選按鈕,不是移入移出,所以選單顯示與隱藏需要用到@onclick方法。

主題切換

3

哇,這個主題切換真的是一言難盡,當切換主題的時候需要記住當前的主題是什麼,當重新整理頁面或者跳轉其他頁面的時候,主題狀態是需要一致的,預設是白色主題,當切換暗黑色主題後其實是在body上加了一個class。

在Blazor實在是不知道用什麼辦法去動態控制body的樣式,所以這裡我想到了一個辦法,寫幾個全域性的JavaScript方法,然後再Blazor中呼叫,要知道,他們是可以互相呼叫的,於是問題迎刃而解。

新增app.js檔案,放在 /wwwroot/js/ 下面。

var func = window.func || {};

func = {
    setStorage: function (name, value) {
        localStorage.setItem(name, value);
    },
    getStorage: function (name) {
        return localStorage.getItem(name);
    },
    switchTheme: function () {
        var currentTheme = this.getStorage('theme') || 'Light';
        var isDark = currentTheme === 'Dark';

        if (isDark) {
            document.querySelector('body').classList.add('dark-theme');
        } else {
            document.querySelector('body').classList.remove('dark-theme');
        }
    }
};

這裡寫了三個方法,設定localStorage:setStorage(name,value),獲取localStorage:getStorage(name),切換主題:switchTheme(),localStorage 是瀏覽器以 name:value 形式的本地儲存物件。

switchTheme主要做的事情就是,判斷當前主題如果是暗黑,就給body加上對應的class,如果不是就去掉。

然後在 index.html 中引用。

...
<body>
    <app>
        <div class="loader"></div>
    </app>
    <script src="js/app.js"></script>
    <script src="_framework/blazor.webassembly.js"></script>
</body>
...

有了這個三個全域性的JavaScript方法,切換主題就變得簡單多了,看程式碼。

...
/// <summary>
/// 當前主題
/// </summary>
private string currentTheme;

/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
    currentTheme = await JSRuntime.InvokeAsync<string>("window.func.getStorage", "theme") ?? "Light";

    await JSRuntime.InvokeVoidAsync("window.func.switchTheme");
}
...

注意在Blazor呼叫JavaScript方法需要注入IJSRuntime介面,@inject IJSRuntime JSRuntime

新建一個變數currentTheme,在生命週期函式初始化的時候去呼叫JavaScript中的getStorage方法,獲取當前主題,考慮到第一次訪問的情況,可以給一個預設值為Light,表示白色主題,然後再去呼叫switchTheme,執行切換主題的方法。這樣頁面就會根據localStorage的值來確定當前的主題。

...
/// <summary>
/// 切換主題
/// </summary>
private async Task SwitchTheme()
{
    currentTheme = currentTheme == "Light" ? "Dark" : "Light";

    await JSRuntime.InvokeVoidAsync("window.func.setStorage", "theme", currentTheme);

    await JSRuntime.InvokeVoidAsync("window.func.switchTheme");
}
...

SwitchTheme()是切換主題的方法,當我們點選input按鈕時可以任意切換,並且主題還要實時跟著變化。

當點選按鈕執行SwitchTheme()時候改變currentTheme的值,然後將currentTheme傳遞給JavaScript方法setStorage,最後再次執行切換主題的JavaScript方法即可。

此時變數currentTheme也發揮了不少作用,在小螢幕下會顯示當前主題的名稱,Dark or Light,可以直接將currentTheme在HTML中賦值即可。

並且我們input是checkbox型別,當是黑色主題的時候需要時選中的狀態,白色主題的時候不選中,這裡就可以利用checked屬性這樣寫:checked="@(currentTheme == "Dark")"

<nav class="navbar">
    <div class="container">
       ...
        <div class="menu navbar-right">
           ...
            <input id="switch_default" type="checkbox" class="switch_default" @onchange="SwitchTheme" checked="@(currentTheme == "Dark")" />
            <label for="switch_default" class="toggleBtn"></label>
        </div>
    </div>
</nav>
<nav class="navbar">
    <div class="container">
       ...
        <div class="menu navbar-right">
           ...
            <input id="switch_default" type="checkbox" class="switch_default" @onchange="SwitchTheme" checked="@(currentTheme == "Dark")" />
            <label for="switch_default" class="toggleBtn"></label>
        </div>
    </div>
</nav>
<nav class="navbar-mobile">
    <div class="container">
        <div class="navbar-header">
            <div>
                <NavLink class="menu-item" href="" Match="NavLinkMatch.All">?阿星Plus</NavLink>
                <NavLink @onclick="SwitchTheme">&nbsp;·&nbsp;@currentTheme</NavLink>
            </div>
            <div class="menu-toggle" @onclick="ToggleNavMenu">&#9776; Menu</div>
        </div>
        <div class="menu @NavMenuCssClass">
            ...
        </div>
    </div>
</nav>

OK,搞定,快去試試吧。

優化程式碼

現在看起來亂亂的,並且設定獲取localStorage屬於公共的方法,說不定以後也能用到,我們將其封裝一下,便於日後的呼叫,不然要寫好多重複的程式碼。

在Blazor專案根目錄新增資料夾Commons,在資料夾下新增一個Common.cs,目前用到了IJSRuntime,用建構函式注入,然後寫幾個公共的方法。

//Common.cs
using Microsoft.JSInterop;
using System.Threading.Tasks;

namespace Meowv.Blog.BlazorApp.Commons
{
    public class Common
    {
        private readonly IJSRuntime _jsRuntime;

        public Common(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }

        /// <summary>
        /// 執行無返回值方法
        /// </summary>
        /// <param name="identifier"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public async ValueTask InvokeAsync(string identifier, params object[] args)
        {
            await _jsRuntime.InvokeVoidAsync(identifier, args);
        }

        /// <summary>
        /// 執行帶返回值的方法
        /// </summary>
        /// <typeparam name="TValue"></typeparam>
        /// <param name="identifier"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public async ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object[] args)
        {
            return await _jsRuntime.InvokeAsync<TValue>(identifier, args);
        }

        /// <summary>
        /// 設定localStorage
        /// </summary>
        /// <param name="name"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public async Task SetStorageAsync(string name, string value)
        {
            await InvokeAsync("window.func.setStorage", name, value);
        }

        /// <summary>
        /// 獲取localStorage
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public async Task<string> GetStorageAsync(string name)
        {
            return await InvokeAsync<string>("window.func.getStorage", name);
        }
    }
}

然後需要在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");

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

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

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

緊接著在_Imports.razor中注入使用Common@inject Commons.Common Common

改造一下Header.razor,全部程式碼如下:

<header>
    <nav class="navbar">
        <div class="container">
            <div class="navbar-header header-logo">
                <NavLink class="menu-item" href="/" Match="NavLinkMatch.All">
                    ?阿星Plus
                </NavLink>
            </div>
            <div class="menu navbar-right">
                <NavLink class="menu-item" href="posts">Posts</NavLink>
                <NavLink class="menu-item" href="categories">Categories</NavLink>
                <NavLink class="menu-item" href="tags">Tags</NavLink>
                <NavLink class="menu-item apps" href="apps">Apps</NavLink>
                <input id="switch_default" type="checkbox" class="switch_default" @onchange="SwitchTheme" checked="@(currentTheme == "Dark")" />
                <label for="switch_default" class="toggleBtn"></label>
            </div>
        </div>
    </nav>
    <nav class="navbar-mobile">
        <div class="container">
            <div class="navbar-header">
                <div>
                    <NavLink class="menu-item" href="" Match="NavLinkMatch.All">?阿星Plus</NavLink>
                    <NavLink @onclick="SwitchTheme">&nbsp;·&nbsp;@currentTheme</NavLink>
                </div>
                <div class="menu-toggle" @onclick="ToggleNavMenu">&#9776; Menu</div>
            </div>
            <div class="menu @NavMenuCssClass">
                <NavLink class="menu-item" href="posts">Posts</NavLink>
                <NavLink class="menu-item" href="categories">Categories</NavLink>
                <NavLink class="menu-item" href="tags">Tags</NavLink>
                <NavLink class="menu-item apps" href="apps">Apps</NavLink>
            </div>
        </div>
    </nav>
</header>

@code {
    /// <summary>
    /// 下拉選單是否開啟
    /// </summary>
    private bool collapseNavMenu = false;

    /// <summary>
    /// 導航選單CSS
    /// </summary>
    private string NavMenuCssClass => collapseNavMenu ? "active" : null;

    /// <summary>
    /// 顯示/隱藏 選單
    /// </summary>
    private void ToggleNavMenu() => collapseNavMenu = !collapseNavMenu;

    /// <summary>
    /// 當前主題
    /// </summary>
    private string currentTheme;

    /// <summary>
    /// 初始化
    /// </summary>
    /// <returns></returns>
    protected override async Task OnInitializedAsync()
    {
        currentTheme = await Common.GetStorageAsync("theme") ?? "Light";

        await Common.InvokeAsync("window.func.switchTheme");
    }

    /// <summary>
    /// 切換主題
    /// </summary>
    private async Task SwitchTheme()
    {
        currentTheme = currentTheme == "Light" ? "Dark" : "Light";

        await Common.SetStorageAsync("theme", currentTheme);

        await Common.InvokeAsync("window.func.switchTheme");
    }
}

實現過程比較簡單,相信你絕對學會了。本篇就到這裡了,未完待續...

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

相關文章