返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
StarBlog博客开发笔记

StarBlog Development Notes (14): Implementing Theme Switching

Series articles Building a Blog Project with .NetCore: StarBlog - (1) Why Write Your Own Blog? Building a Blog Project with .NetCore: StarBlog - (2) Environment Setup and Creating the Project Building a Blog Project with .NetCore: StarBlog - (3) Model Design Building a Blog Project with .NetCore: StarBlog - (4) Batch Import of Markdown Blog Posts Building a Blog Project with .NetCore: StarBl

曦远 (DealiAxy)约 27 分钟
目录8 个章节

Series Articles

Preface

Recently I've been writing code and adding some new features to the blog. I originally wanted to cover them all in one article, but it would probably be too long, so I figured it's better to split them up~

This article covers the theme switching feature. StarBlog's pages are built on Bootstrap 5. Bootstrap's own design is fairly clean and elegant, but it can get boring after a while, and tweaking CSS yourself is a hassle. Luckily, in the Bootstrap world there's something called Bootswatch that provides dozens of themes. I just need to npm install and then I can implement theme switching~

The Result

As usual, let's look at the result first

Default ThemeQuartz Theme
image-20220703230951905image-20220703231019745

PS: Light themes currently adapt fairly well; dark themes are still being adapted, and some pages don't display well yet

Approach

Bootswatch switches themes by including the bootstrap.css file it provides, which overrides Bootstrap's default styles

The theme switching I implemented in DjangoStarter used Django's TemplateTag, which can add the corresponding CSS file reference based on the user's selected theme when the template is rendered

In theory, the StarBlog project built with ASP.NET Core MVC could implement theme switching the same way, but I ran into some issues that were a bit of a hassle to deal with during development, so I decided instead to expose a theme API and load CSS dynamically via JS.

Adding Dependencies

Let's start coding~

First, add the bootswatch dependency. It needs to match the Bootstrap version. This project uses Bootstrap 5.1.3, so this Bootswatch version also needs to be 5.1.3

yarn add bootswatch

Configure automatic copying in gulpfile.js

//使用 npm 下载的前端组件包
const libs = [
    {name: "bootswatch", dist: "./node_modules/bootswatch/dist/**/*.*"},
];

Run the gulp move command in the StarBlog.Web directory. Gulp will automatically copy the Bootswatch-related files into the wwwroot/lib directory for later use

For details on managing static assets with NPM and Gulp, see this earlier article: ASP.NET Core Development Notes: Managing Frontend Static Files with NPM and Gulp

Writing the Service

Add ThemeService.cs under StarBlog.Web/Services

First, define the Theme model

public class Theme {
    public string Name { get; set; }
    public string Path { get; set; }
    public string CssUrl { get; set; }
}

Then ThemeService scans all themes in wwwroot/lib/bootswatch and also adds Bootstrap's default theme

public class ThemeService {
    public const string BootstrapTheme = "Bootstrap";
    private const string CssUrlPrefix = "/lib/bootswatch/dist";

    public List<Theme> Themes { get; set; } = new() {
        new Theme {Name = BootstrapTheme, Path = "", CssUrl = ""}
    };

    public ThemeService(IWebHostEnvironment env) {
        var themePath = Path.Combine(env.WebRootPath, "lib", "bootswatch", "dist");
        foreach (var item in Directory.GetDirectories(themePath)) {
            var name = Path.GetFileName(item);
            Themes.Add(new Theme {
                Name = name,
                Path = item,
                CssUrl = $"{CssUrlPrefix}/{name}/bootstrap.min.css"
            });
        }
    }
}

Then register it as a singleton service and you're good

builder.Services.AddSingleton<ThemeService>();

Writing an API

Next we also need to write an API

Add a ThemeController.cs under the StarBlog.Web/Apis directory. The code is simple — just one action that gets all themes

/// <summary>
/// 页面主题
/// </summary>
[ApiController]
[Route("Api/[controller]")]
[ApiExplorerSettings(GroupName = "common")]
public class ThemeController : ControllerBase {
    private readonly ThemeService _themeService;

    public ThemeController(ThemeService themeService) {
        _themeService = themeService;
    }

    [HttpGet]
    public List<Theme> GetAll() {
        return _themeService.Themes;
    }
}

Frontend Implementation

The backend part of themes is done. The frontend needs to complete three parts:

  • Request the API to get the full theme list
  • Set the theme and dynamically include CSS
  • Save the currently selected theme so it's automatically applied the next time the page is opened

To make DOM operations easier, I used Vue, included at the bottom of Views/Shared/_Layout.cshtml

<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/vue/dist/vue.js"></script>
<script src="~/js/site.js"></script>

First, let's write the code for the last included file, site.js. Using Vue, when the page opens it loads the theme list via the fetch function and then displays it on the page

Also, when switching themes, save the current theme's name and CSS URL in localStorage so they can be automatically applied the next time the page loads

let app = new Vue({
    el: '#vue-header',
    data: {
        currentTheme: '',
        themes: []
    },
    created: function () {
        fetch('/Api/Theme')
            .then(res => res.json())
            .then(res => {
                this.themes = res.data
            })

        // 读取本地主题配置
        let theme = localStorage.getItem('currentTheme')
        if (theme != null) this.currentTheme = theme
    },
    methods: {
        setTheme(themeName) {
            let theme = this.themes.find(t => t.name === themeName)
            loadStyles(theme.cssUrl)
            this.currentTheme = themeName
            localStorage.setItem('currentTheme', themeName)
            localStorage.setItem('currentThemeCssUrl', theme.cssUrl)
            // 换主题之后最好要刷新页面,不然可能样式冲突
            location.reload()
        }
    }
})

Here the themes are loaded, and through Vue's two-way binding they're rendered in the top menu (with the current theme highlighted)

Which is this place

image-20220703230232127

<div class="px-3 py-2 border-bottom mb-3">
    <div class="container d-flex flex-wrap justify-content-center">
        <form class="col-12 col-lg-auto mb-2 mb-lg-0 me-lg-auto" asp-controller="Search" asp-action="Blog">
            <input type="search" class="form-control" placeholder="Search..." aria-label="Search" name="keyword">
        </form>

        <div class="text-end">
            <span class="dropdown me-2">
                <a class="btn btn-secondary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-bs-toggle="dropdown" aria-expanded="false">
                    Themes
                </a>
                <ul class="dropdown-menu" aria-labelledby="dropdownMenuLink">
                    <li v-for="theme in themes">
                        <a v-if="theme.name===currentTheme" class="dropdown-item active">{{theme.name}}</a>
                        <a v-else class="dropdown-item" v-on:click="setTheme(theme.name)">{{theme.name}}</a>
                    </li>
                </ul>
            </span>
        </div>
    </div>
</div>

Finally, we also need to read the theme configuration when the page refreshes and automatically load the current theme

Because dynamically switching themes can cause some style conflicts and such, we need to include it before the page has finished loading

So I also wrote a site.preload.js and placed it in the page's <head> section

<script src="~/js/site.preload.js"></script>

The code is as follows: first read the current theme; if a theme has been set, read the CSS URL and include it

(The loadStyles function is also used in the earlier site.js)

// 动态加载CSS
function loadStyles(url) {
    let link = document.createElement("link");
    link.rel = "stylesheet";
    link.type = "text/css";
    link.href = url;
    let head = document.getElementsByTagName("head")[0];
    head.appendChild(link);
}

let currentTheme = localStorage.getItem('currentTheme')
if (currentTheme !== 'Bootstrap') {
    let themeCssUrl = localStorage.getItem('currentThemeCssUrl')
    if (themeCssUrl != null) loadStyles(themeCssUrl)
}

PS: The code for dynamically loading CSS comes from: http://lengyun.github.io/js/3-2-2dynamicAddCSS.html

Done~

_EOF_

StarBlog Development Notes (14): Implementing Theme Switching

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

StarBlog博客开发笔记(24):统一接口数据返回格式

前言 开发接口,是给客户端(Web前端、App)用的,前面说的RESTFul,是接口的规范,有了统一的接口风格,客户端开发人员在访问后端功能的时候能更快找到需要的接口,能写出可维护性更高的代码。 而接口的数据返回格式也是接口规范的重要一环,不然一个接口返回JSON,一个返回纯字符串,客户端对接到数据时一脸懵逼,没法处理啊。 合格的接口返回值应该包括状态码、提示信息和数据。 就像这样: { st

2023-01-02 · StarBlog博客开发笔记 · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…