近期一直在寫程式碼,給部落格新增了一些功能,本來想寫一篇文章一起介紹的,不過好像篇幅會太長,想想還是分開寫好了~
本文介紹主題切換功能,StarBlog部落格頁面基於Bootstrap5實現,Bootstrap本身的設計是比較簡潔大方的,不過看久了也會膩,自己調css太麻煩,好在Bootstrap世界有個東西叫bootswatch,提供了幾十套主題,我只需要npm install
然後就可以實現主題切換功能了~
同時所有專案程式碼已經上傳GitHub,歡迎各位大佬Star/Fork!
照例先看看實現的效果
預設主題 | quartz主題 |
---|---|
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管理前端靜態檔案
在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;
}
}
主題的後端部分完成了,前端需要完成三部分功能
為了方便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的雙向繫結,把主題渲染在頂部選單上(同時高亮當前主題)
也就是這個地方
<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
搞定~