基於.NetCore開發部落格專案 StarBlog

2022-05-25 06:01:35

系列文章

前言

前面 (6) 頁面開發之部落格文章列表 介紹了文章列表的開發,頁面中左側是分類列表,右側是該分類下的文章,這個佈局乍看還是不錯的,不過考慮到本專案支援多級分類,但分類列表只會機械式的把所有分類都顯示出來,無法體現分類的層級結構且佔用了很大的頁面縱向空間,因此本文將對分類列表進行改造,使之能夠體現多級分類、節省頁面空間。

關於樹形結構元件,我找了一圈,適配bootstrap(基於jQuery)的元件很難找,大都是很老的,只找到了bootstrap-treeview這個稍微好用一點的,看了下GitHub專案主頁,同樣是好久沒更新了,它適配的甚至是3.x版本的bootstrap,現在都已經2022年了,bootstrap都更新到5.x版本了,然而沒找到更好的,湊合用吧~ (實在不行還能把它程式碼clone下來魔改)

安裝

這個元件是比較老的

依賴bower,如果沒有bower的話需要先安裝

npm install -g bower

然後在StarBlog.Web目錄下執行以下命令安裝依賴

npm install bootstrap-treeview

因為我們的靜態資源都在wwwroot下,所以npm安裝的前端資源還需要通過gulp工具自動複製到wwwroot裡,這一點在前面的文章中有介紹過,忘記的同學可以看一下前面這篇:基於.NetCore開發部落格專案 StarBlog - (5) 開始搭建Web專案

編輯gulpfile.js檔案,在const libs設定中增加一行

//使用 npm 下載的前端元件包
const libs = [
    // ...
    {name: "bootstrap-treeview", dist: "./node_modules/bootstrap-treeview/dist/**/*.*"},
];

然後執行gulp任務即可

gulp move

完成之後可以看到wwwroot/lib下已經多了一個bootstrap-treeview目錄了

接下來我們就可以在頁面中參照

用法

正式開始前,先來了解一下這個元件的用法

引入依賴

<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap-treeview/dist/bootstrap-treeview.min.js"></script>

在網頁裡放一個容器

<div id="categories">

根據官方例子,使用js啟用元件

const instance = $('#categories').treeview({
    data: collections,
});

collections格式如下

const collections = [
    {
        text: 'Parent 1',
        href: '#parent1',
        nodes: [
            {
                text: 'Child 1',
                href: '#child1',
                nodes: [
                    {
                        text: 'Grandchild 1',
                        href: '#grandchild1',
                    },
                    {
                        text: 'Grandchild 2',
                        href: '#grandchild2',
                    }
                ]
            },
            {
                text: 'Child 2',
                href: '#child2',
            }
        ]
    },
    {
        text: 'Parent 2',
        href: '#parent2',
    },
    {
        text: 'Parent 3',
        href: '#parent3',
    },
    {
        text: 'Parent 4',
        href: '#parent4',
    },
    {
        text: 'Parent 5',
        href: '#parent5',
    }
];

官網的預設效果

不過經過我的測試,官網這個例子在bootstrap5下是有些問題的,預設的圖示都顯示不出來。需要我們自定義一下,加上圖示設定就行,用到的圖示是我們之前的安裝的FontAwesome Icons

const instance = $('#categories').treeview({
    data: collections,
    collapseIcon: "fa fa-caret-down",
    expandIcon: "fa fa-caret-right",
    emptyIcon: 'fa fa-circle-o',
});

處理分類資料

為了方便使用這個元件,我們需要在後端把分類層級包裝成這個元件需要的形式。

首先定義一個節點類

public class CategoryNode {
    public string text { get; set; } = "";
    public string href { get; set; } = "";
    public List<CategoryNode>? nodes { get; set; }
}

然後在Services/CategoryyService.cs裡新增一個方法,用來生成分類的樹結構,為了程式碼編寫方便,我直接用遞回來實現。

public List<CategoryNode>? GetNodes(int parentId = 0) {
    var categories = _cRepo.Select
        .Where(a => a.ParentId == parentId).ToList();

    if (categories.Count == 0) return null;

    return categories.Select(category => new CategoryNode {
        text = category.Name,
        nodes = GetNodes(category.Id)
    }).ToList();
}

這樣輸出來的資料就是這樣

[
    {
      "text": "Android開發",
      "href": "",
      "nodes": null
    },
    {
      "text": "AspNetCore",
      "href": "",
      "nodes": [
        {
          "text": "Asp-Net-Core學習筆記",
          "href": "",
          "nodes": null
        },
        {
          "text": "Asp-Net-Core開發筆記",
          "href": "",
          "nodes": null
        }
      ]
    }
]

哦差點忘了還得給每個節點加上href引數

寫死是不可能寫死的,ControllerBase範例預設帶有一個IUrlHelper型別的Url屬性,可以用其Link()方法實現地址路由解析。

不過我們這個方法是寫在Service裡,並沒有ControllerBase範例,這時只能用依賴注入的方式,不過我在Stack Overflow上看到一個說法是,AspNetCore3.x之後,用LinkGenerator更好。

上程式碼,先註冊服務

builder.Services.AddHttpContextAccessor();

然後依賴注入

private readonly IHttpContextAccessor _accessor;
private readonly LinkGenerator _generator;

public CategoryService(IHttpContextAccessor accessor, LinkGenerator generator) {
    _accessor = accessor;
    _generator = generator;
}

修改上面那個GetNodes方法,在CategoryNode初始化器里加上

href = _generator.GetUriByAction(
    _accessor.HttpContext!,
    nameof(BlogController.List),
    "Blog",
    new {categoryId = category.Id}
)

具體程式碼可以看GitHub:https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Web/Services/CategoryService.cs

生成的連結形式是這樣的:

{
    "text": "Android開發",
    "href": "http://localhost:5038/Blog/List?categoryId=2",
    "nodes": null
}

前端渲染

資料準備好了,這時遇到一個問題,資料是要放到js中處理的,那我要用fetch之類的非同步請求來獲取分類資料再顯示樹形分類嗎?這樣的好處是寫起來比較直觀,然而我們專案的部落格網站是後端渲染,現在部落格列表頁面混入了非同步請求,會導致割裂感,右邊部分的文章列表伺服器端渲染出來在瀏覽器上展示了,左側的分類還要非同步去請求。

斟酌了一下,我決定這個分類也使用後端渲染,雖然有點反直覺,但根據bootstrap-treeview元件的檔案,它可以使用json方式渲染分類,那我只需要在後端把分類資料序列化成json格式,然後在view中渲染到js程式碼中就行。

開始吧~

編輯StarBlog.Web/ViewModels/BlogListViewModel.cs檔案,新增倆欄位

public List<CategoryNode> CategoryNodes { get; set; }
// 將上面的分類層級資料轉換成Json字串
public string CategoryNodesJson => JsonSerializer.Serialize(
    CategoryNodes,
    new JsonSerializerOptions {Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping}
);

然後修改一下Controller,StarBlog.Web/Controllers/BlogController.cs,先依賴注入CategoryService

然後修改List方法

public IActionResult List(int categoryId = 0, int page = 1, int pageSize = 5) {
    var categories = _categoryRepo.Where(a => a.Visible)
        .IncludeMany(a => a.Posts).ToList();
    categories.Insert(0, new Category {Id = 0, Name = "All", Posts = _postRepo.Select.ToList()});

    return View(new BlogListViewModel {
        CurrentCategory = categoryId == 0 ? categories[0] : categories.First(a => a.Id == categoryId),
        CurrentCategoryId = categoryId,
        Categories = categories,
        // 增加這一行
        CategoryNodes = _categoryService.GetNodes(),
        Posts = _postService.GetPagedList(new PostQueryParameters {
            CategoryId = categoryId,
            Page = page,
            PageSize = pageSize,
            OnlyPublished = true
        })
    });
}

最後一步,修改View,StarBlog.Web/Views/Blog/List.cshtml,在底部加入js參照和一些js程式碼,treeview元件的設定我已經封裝成initTreeView方法,可以直接使用。

@section bottom {
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap-treeview/dist/bootstrap-treeview.min.js"></script>
    <script src="~/js/blog-list.js"></script>
    <script>
        const categories = '@Html.Raw(Model.CategoryNodesJson)'
        initTreeView(categories);
    </script>
}

View的關鍵程式碼就這幾行,完整程式碼可見:https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Web/Views/Blog/List.cshtml

最終效果

完成之後的最終效果如下,算是支援了分類層級了,不過仍然不完美,存在幾個問題:

  • 不能高亮顯示當前所選分類
  • 沒有實現分類文章數量顯示(原來的版本是有的)
  • 無法自定義list-group-item樣式,存在下劃線不美觀
  • ...

這幾個問題留著後面優化吧~ 暫時先折騰到這裡…

部落格專案的開發已經基本完成,專案程式碼完全開源,有興趣的朋友可以點個star~

參考資料