• 基于.NetCore开发博客项目 StarBlog (14) 实现主题切换功能


    系列文章

    前言

    近期一直在写代码,给博客新增了一些功能,本来想写一篇文章一起介绍的,不过好像篇幅会太长,想想还是分开写好了~

    本文介绍主题切换功能,StarBlog博客页面基于Bootstrap5实现,Bootstrap本身的设计是比较简洁大方的,不过看久了也会腻,自己调css太麻烦,好在Bootstrap世界有个东西叫bootswatch,提供了几十套主题,我只需要npm install然后就可以实现主题切换功能了~

    同时所有项目代码已经上传GitHub,欢迎各位大佬Star/Fork!

    实现效果

    照例先看看实现的效果

    默认主题 quartz主题
    image image

    PS:目前浅色主题可以比较好的适配,深色主题还在适配中,部分页面的显示效果不佳

    思路

    bootswatch切换主题的方式是引入其提供的bootstrap.css文件,覆盖Bootstrap默认的样式

    我在DjangoStarter中实现的切换主题用的是Django的TemplateTag,它可以在模板渲染的时候根据用户选择的主题,加入对应的css文件引用

    理论上使用AspNetCore MVC实现的StarBlog项目也是可以用这种方式实现主题切换的,不过当时开发时遇到一些处理起来比较麻烦的问题,所以我决定改为暴露主题API,通过JS动态加载css的方式实现。

    添加依赖

    开始代码~

    首先添加bootswatch依赖,需要和Bootstrap版本对应,本项目使用的Bootstrap版本是5.1.3,所以这个bootswatch版本也需要同步使用5.1.3

    yarn add bootswatch
    

    gulpfile.js中配置自动复制

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

    StarBlog.Web目录下执行gulp move命令,gulp会自动把bootswatch相关文件复制到wwwroot/lib目录中,方便接下来的使用

    关于使用NPM和Gulp管理静态资源的详情,可以参考前面的这篇文章:Asp-Net-Core开发笔记:使用NPM和gulp管理前端静态文件

    编写Service

    StarBlog.Web/Services中添加ThemeService.cs

    首先是定义Theme模型

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

    然后ThemeService,扫描wwwroot/lib/bootswatch中的所有主题,同时把Bootstrap默认主题也加入

    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"
                });
            }
        }
    }
    

    然后注册为单例服务就OK了

    builder.Services.AddSingleton<ThemeService>();
    

    写个接口

    然后还需要写一个接口

    StarBlog.Web/Apis目录下新增个ThemeController.cs,代码很简单,只有一个action,获取全部主题

    /// <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;
        }
    }
    

    前端实现

    主题的后端部分完成了,前端需要完成三部分功能

    • 请求接口,获取全部主题列表
    • 设置主题,动态引入css
    • 保存当前选择的主题,下次打开页面时自动引入

    为了方便DOM操作,我使用了Vue,在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>
    

    先在来写最后一个引入的site.js代码,使用vue,网页打开时通过fetch函数加载主题列表,然后显示在页面上

    还有切换主题时将当前主题的名称和css链接保存在localStorage中,下次加载页面的时候可以自动引入

    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()
            }
        }
    })
    

    这里加载了主题,通过vue的双向绑定,把主题渲染在顶部菜单上(同时高亮当前主题)

    也就是这个地方

    image

    <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>
    

    最后,还需要在页面刷新的时候读取主题配置,然后自动加载当前主题

    因为动态切换主题会导致一些样式冲突啥的,所以需要在页面还没加载完成的时候先引入

    因此我又写了个site.preload.js,放在页面的<head>部分

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

    代码如下,先读取当前主题,如果有设置过主题就读取CSS链接并且引入

    (同时前面的site.js中也有使用到这个loadStyles函数)

    // 动态加载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:动态加载CSS的代码来自:http://lengyun.github.io/js/3-2-2dynamicAddCSS.html

    搞定~

  • 相关阅读:
    react-redux
    Vue中常用的UI框架
    vue中router与route的区别
    H5新增input属性
    H5新增的input类型
    菜鸡对作用域链的理解
    自己对路由的一些理解
    浏览器缓存
    黄瓜的不定期更新面试题
    ajax封装
  • 原文地址:https://www.cnblogs.com/deali/p/16441294.html
Copyright © 2020-2023  润新知