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; # 密码文件路径 try_files $uri $uri/ /index.html; } location = /logout { # 生成动态 realm 使浏览器认为这是新认证域 add_header WWW-Authenticate 'Basic realm="Restricted_$request_time"'; # 阻止浏览器缓存认证状态 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; }