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; /* 悬停时透明度变化 */ } </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(); // 同步颜色 } } else { // 如果导航栏未加载,延迟重试 setTimeout(checkNavbar, 50); 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 => { // 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 */ }) ) } })