docsify是一个很神奇的文档生成工具,利用markdown文档动态生成文档网站。与 GitBook 不同,它不会生成静态的 HTML 文件。相反,它会智能地加载并解析Markdown 文件,并将它们展示为一个网站。这里简要介绍一下一个实例,在官方配置的基础上增加一个手动切换主题的功能,官方文档(https://docsify.js.org/#/zh-cn/)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>第三方API接口收集文档</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="第三方API接口收集文档" />
<!-- 禁止搜索引擎索引 -->
<meta name="robots" content="noindex, nofollow" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0"
/>
<!-- 默认主题 -->
<link
id="theme-style"
rel="stylesheet"
href="https://cdn.staticfile.net/docsify/4.13.1/themes/vue.min.css"
/>
<style>
/* 自定义样式:确保主题切换按钮与 navbar 在同一行 */
.app-nav {
display: flex;
align-items: center;
justify-content: space-between;
}
.theme-switcher {
cursor: pointer;
font-size: 16px;
text-decoration: none;
border-radius: 4px;
transition: all 0.3s ease;
margin: 0 1rem;
}
.theme-switcher:hover {
opacity: 0.8; /* 悬停时透明度变化 */
}
.logout-btn {
color: #ff4d4f !important; /* 使用醒目的红色 */
transition: opacity 0.3s ease;
}
.logout-btn:hover {
opacity: 0.8;
text-decoration: underline; /* 悬停下划线提示 */
}
</style>
</head>
<body>
<div id="app"></div>
<script>
// 动态生成时间戳
const timestamp = new Date().getTime();
const themeLink = document.getElementById("theme-style");
// 加载用户上次选择的主题
let currentTheme = localStorage.getItem("docsify-theme") || "vue"; // 默认主题为 vue
themeLink.href = `https://cdn.staticfile.net/docsify/4.13.1/themes/${currentTheme}.min.css?timestamp=${timestamp}`;
// Docsify 配置
window.$docsify = {
name: "第三方API接口收集文档", // 文档站点名称
loadSidebar: true, // 启用侧边栏
loadNavbar: true, // 启用导航栏
autoHeader: true, // 自动生成标题
subMaxLevel: 6, // 自动生成标题链接的最大层级
search: "auto", // 启用搜索功能
auto2top: true, // 启用自动滚动到顶部
mergeNavbar: true, // 合并导航栏
externalLinkTarget: "_blank", // 外部链接打开方式
search: {
maxAge: 86400000, // 缓存时间(毫秒)
paths: "auto", // 搜索范围
placeholder: "Type to Search", // 搜索框占位符
noData: "No Results!",
},
// 启用 lastUpdated 插件
lastUpdated: true, // 显示文档更新时间
formatUpdated: "{YYYY}-{MM}-{DD} {HH}:{mm}:{ss}", // 时间格式化
// 自定义插件:添加主题切换功能
plugins: [
function (hook, vm) {
hook.init(function () {
// 监听路由变化
// 使用原生路由监听方式
window.addEventListener("hashchange", function () {
setTimeout(addThemeSwitcher, 100);
});
});
hook.doneEach(function () {
// 确保主题切换按钮只添加一次
addThemeSwitcher();
});
hook.mounted(function () {
// 页面首次加载时添加主题切换按钮
addThemeSwitcher();
});
},
],
};
// 添加 主题切换 和 注销 按钮
function addThemeSwitcher() {
const checkNavbar = () => {
const navbar = document.querySelector(".app-nav");
if (navbar) {
// 添加主题切换按钮
const existingSwitcher = navbar.querySelector(".theme-switcher");
if (!existingSwitcher) {
const switcher = document.createElement("div");
switcher.className = "theme-switcher";
switcher.textContent = "切换主题";
switcher.onclick = toggleTheme;
navbar.appendChild(switcher);
updateThemeSwitcherColor();
}
// 新增注销按钮
const existingLogout = navbar.querySelector(".logout-btn");
if (!existingLogout) {
const logoutBtn = document.createElement("a");
logoutBtn.className = "logout-btn theme-switcher"; // 复用主题切换按钮样式
logoutBtn.textContent = "注销";
logoutBtn.href = "/logout";
logoutBtn.style.marginLeft = "10px"; // 增加间距
// 添加点击事件处理
logoutBtn.addEventListener("click", function (e) {
e.preventDefault();
// 强制清除Service Worker
if (navigator.serviceWorker) {
navigator.serviceWorker
.getRegistrations()
.then(function (registrations) {
registrations.forEach((registration) =>
registration.unregister(),
);
});
}
// 清除缓存后跳转
caches.keys().then(function (names) {
names.forEach((name) => caches.delete(name));
// 添加随机参数强制刷新
window.location.href = `/logout?cache-bust=${Date.now()}`;
setTimeout(() => {
window.location.replace(window.location.origin);
}, 100);
});
});
navbar.appendChild(logoutBtn);
}
} else {
setTimeout(checkNavbar, 10);
console.log("导航栏未加载,导航容器未生成,请检查配置");
}
};
checkNavbar();
}
// 主题切换逻辑
const themes = ["vue", "dark"]; // 可选主题列表
function toggleTheme() {
const nextTheme =
themes[(themes.indexOf(currentTheme) + 1) % themes.length];
currentTheme = nextTheme;
// 更新主题样式
themeLink.href = `https://cdn.staticfile.net/docsify/4.13.1/themes/${nextTheme}.min.css?timestamp=${timestamp}`;
// 保存用户选择的主题
localStorage.setItem("docsify-theme", nextTheme);
// 更新按钮文字颜色
updateThemeSwitcherColor();
}
// 更新按钮文字颜色以匹配当前主题
function updateThemeSwitcherColor() {
const switcher = document.querySelector(".theme-switcher");
if (switcher) {
// 根据当前主题设置文字颜色
switch (currentTheme) {
case "vue":
switcher.style.color = "#34495e"; // 默认主题文字颜色
break;
case "dark":
switcher.style.color = "#c8c8c8"; // 深色主题文字颜色
break;
default:
switcher.style.color = "#34495e"; // 默认回退颜色
}
}
}
// 页面加载完成后初始化按钮颜色
document.addEventListener("DOMContentLoaded", function () {
updateThemeSwitcherColor();
});
</script>
<!-- Docsify v4 -->
<script src="https://cdn.staticfile.net/docsify/4.13.1/docsify.min.js"></script>
<!-- 引入 Docsify 搜索插件 -->
<script src="https://cdn.staticfile.net/docsify/4.13.1/plugins/search.min.js"></script>
<!-- 引入 Docsify 复制到剪切板插件 -->
<script src="https://cdn.staticfile.net/docsify-copy-code/3.0.0/docsify-copy-code.min.js"></script>
<!-- 引入时间更新插件 -->
<script src="./assets/time-updater.min.js"></script>
</body>
</html>
<script>
if (typeof navigator.serviceWorker !== "undefined") {
navigator.serviceWorker.register("sw.js");
console.log("register serviceWorker successed!");
}
</script>再在文档根目录创建sw.js文件,增强离线功能:
/* ===========================================================
* docsify sw.js
* ===========================================================
* Copyright 2016 @huxpro
* Licensed under Apache 2.0
* Register service worker.
* ========================================================== */
const RUNTIME = 'docsify'
const HOSTNAME_WHITELIST = [
self.location.hostname,
'fonts.gstatic.com',
'fonts.googleapis.com',
'cdn.jsdelivr.net',
'cdn.staticfile.net'
]
// The Util Function to hack URLs of intercepted requests
const getFixedUrl = (req) => {
var now = Date.now()
var url = new URL(req.url)
// 1. fixed http URL
// Just keep syncing with location.protocol
// fetch(httpURL) belongs to active mixed content.
// And fetch(httpRequest) is not supported yet.
url.protocol = self.location.protocol
// 2. add query for caching-busting.
// Github Pages served with Cache-Control: max-age=600
// max-age on mutable content is error-prone, with SW life of bugs can even extend.
// Until cache mode of Fetch API landed, we have to workaround cache-busting with query string.
// Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190
if (url.hostname === self.location.hostname) {
url.search += (url.search ? '&' : '?') + 'cache-bust=' + now
}
return url.href
}
/**
* @Lifecycle Activate
* New one activated when old isnt being used.
*
* waitUntil(): activating ====> activated
*/
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim())
})
/**
* @Functional Fetch
* All network requests are being intercepted here.
*
* void respondWith(Promise<Response> r)
*/
self.addEventListener('fetch', event => {
if (event.request.url.includes('/logout')) {
return; // 跳过缓存
}
//
// Skip some of cross-origin requests, like those for Google Analytics.
if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) {
// Stale-while-revalidate
// similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale
// Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1
const cached = caches.match(event.request)
const fixedUrl = getFixedUrl(event.request)
const fetched = fetch(fixedUrl, { cache: 'no-store' })
const fetchedCopy = fetched.then(resp => resp.clone())
// Call respondWith() with whatever we get first.
// If the fetch fails (e.g disconnected), wait for the cache.
// If there’s nothing in cache, wait for the fetch.
// If neither yields a response, return offline pages.
event.respondWith(
Promise.race([fetched.catch(_ => cached), cached])
.then(resp => resp || fetched)
.catch(_ => { /* eat any errors */ })
)
// Update the cache with the version we fetched (only for ok status)
event.waitUntil(
Promise.all([fetchedCopy, caches.open(RUNTIME)])
.then(([response, cache]) => response.ok && cache.put(event.request, response))
.catch(_ => { /* eat any errors */ })
)
}
})因为配置了密码登陆,所以需要后端nginx配置一下:
yum install httpd-tools htpasswd -c /etc/nginx/.htpasswd your_username
注意目录/etc/nginx要存在,your_username是账户,执行上述命令后,系统会提示你输入密码,并将其加密存储到 .htpasswd 文件中。
然后是具体的nginx配置,http段:
http{
# 其他配置
map $status $auth_realm {
~^4 "Restricted_$request_time"; # 动态生成 realm
}
}server段:
add_header Vary "Cookie,Authorization"; # 确保不同凭证得到不同缓存副本
add_header X-Content-Type-Options "nosniff"; # 阻止 MIME 类型嗅探
auth_basic $auth_realm; # 使用动态 realm
location / {
auth_basic "Restricted Access"; # 提示信息
auth_basic_user_file /etc/nginx/.htpasswd; # 密码文件路径
if($arg_auth_required){
return 401;
}
try_files $uri $uri/ /index.html;
}
# 文档页面退出登录URL
location = /logout {
# 生成动态 realm 使浏览器认为这是新认证域
# add_header WWW-Authenticate 'Basic realm="Restricted_$request_time"';
# 强制清除认证缓存
add_header WWW-Authenticate 'Basic realm="Restricted Area" charset="UTF-8"';
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
# 清除浏览器数据(部分浏览器生效)
add_header Clear-Site-Data '"cache", "storage"';
# 返回 401 触发浏览器认证弹窗
return 401;
}
error_page 401 /401.html;
location = /401.html{
auth_basic off; # 关键设置:确保错误页面不需要认证
internal;
}


