什么是 Vitepress?
Vitepress 是由 Vite 和 Vue 驱动的动态站点生成器,通过获取 Markdown 编写的内容,并能够生成对应的动态 HTML 页面。咱们常常应用 Vitepress 构建博客等动态网站,本文次要解析一下 Vitepress 的实现原理,上面就开始吧!
原理
初始化我的项目
依据官网文档举荐,咱们执行以下命令初始化我的项目:
npx vitepress init
执行完命令便会进入一个设置界面,通过设置我的项目名等参数,最终生成一个 vitepress 我的项目。
咱们都晓得,npx vitepress init
实际上等同于:
npm i -g vitepressvitepress init
很好了解,先全局装置 vitepress,再执行 vitepress init
命令:
先通过 @clack/prompts
开启命令行 UI 界面,用户进行初始化配置:
// src/node/init/init.tsimport { group } from '@clack/prompts'const options: ScaffoldOptions = await group( { root: () => text({ message: 'Where should VitePress initialize the config?', initialValue: './', validate(value) { // TODO make sure directory is inside } }), title: () => text({ message: 'Site title:', placeholder: 'My Awesome Project' }), // ...以下省略)
再依据配置项从 template 文件夹中拉取模板文件,实现我的项目的初始化。
启动服务
在 Vitepress 我的项目中,咱们通过执行以下命令启动文档服务:
vitepress dev
执行完命令,咱们便能够在浏览器拜访文档网站!
启动服务次要分为两步:
- 创立 Vite 服务;
- 执行 Vite 插件;
创立 Vite 服务
// src/node/server.tsimport { createServer as createViteServer, type ServerOptions } from 'vite'import { resolveConfig } from './config'import { createVitePressPlugin } from './plugin'export async function createServer( root: string = process.cwd(), serverOptions: ServerOptions & { base?: string } = {}, recreateServer?: () => Promise<void>) { // 读取 vitepress 配置 const config = await resolveConfig(root) if (serverOptions.base) { config.site.base = serverOptions.base delete serverOptions.base } // 创立 vite 服务 return createViteServer({ root: config.srcDir, base: config.site.base, cacheDir: config.cacheDir, plugins: await createVitePressPlugin(config, false, {}, {}, recreateServer), server: serverOptions, customLogger: config.logger, configFile: config.vite?.configFile })}
上述代码创立并启动了一个 Vite 服务:首先,通过调用 resolveConfig
,读取用户的 Vitepress 配置并整合为一个 config 对象(配置门路默认为:.vitepress/config/index.js
),再将局部配置传入 createViteServer
,创立并启动 Vite 服务。
执行 Vite 插件
看完下面的内容,你可能会有点纳闷,失常来说,Vite 须要一个 HTML 作为入口文件,但咱们找遍 Vitepress 也未发现咱们想要的 HTML 文件……其实这部分工作由 Vite 插件实现,在下面的代码片段中,咱们创立了 Vite 服务,同时配置了插件:
// src/node/server.tsreturn createViteServer({ // 省略代码 plugins: await createVitePressPlugin(config, false, {}, {}, recreateServer), // 省略代码})
createVitePressPlugin
函数返回了一个插件列表,其中有一个名为 vitepress
的插件:
// src/node/plugin.tsconst vitePressPlugin: Plugin = { name: 'vitepress', // 省略代码 configureServer(server) { // 省略代码 return () => { server.middlewares.use(async (req, res, next) => { const url = req.url && cleanUrl(req.url) if (url?.endsWith('.html')) { res.statusCode = 200 res.setHeader('Content-Type', 'text/html') let html = `<!DOCTYPE html><html> <head> <title></title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="description" content=""> </head> <body> <div id="app"></div> <script type="module" src="/@fs/${APP_PATH}/index.js"></script> </body></html>` html = await server.transformIndexHtml(url, html, req.originalUrl) res.end(html) return } next() }) } }, // 省略代码 }
vitepress 插件中定义了 configureServer
生命周期,并在 configureServer
中返回一个 HTML 文件,作为 Vite 服务的入口 HTML 文件,当咱们拜访服务时,浏览器渲染网页,执行 HTML 中引入的 Script 文件(<script type="module" src="/@fs/${APP_PATH}/index.js"></script>
,其中 APP_PATH
为 src/client/app/index.ts
),网页失常展现在咱们眼前,至此,服务失常启动!
文档渲染
在下面的局部,咱们整顿了启动服务的大抵步骤,接下来咱们将接着整顿 Markdown 文件和路由的映射关系!
创立路由
Vitepress 并没有应用 Vuejs 的官网路由计划(Vue Router),而是本人实现了一个简略的路由模块:首先通过监听 window 的点击事件,当用户点击超链接元素时,执行跳转函数 go
:
// src/client/app/router.tsasync function go(href: string = inBrowser ? location.href : '/') { href = normalizeHref(href) if ((await router.onBeforeRouteChange?.(href)) === false) return updateHistory(href) await loadPage(href) await router.onAfterRouteChanged?.(href)}function updateHistory(href: string) { if (inBrowser && normalizeHref(href) !== normalizeHref(location.href)) { // save scroll position before changing url history.replaceState({ scrollPosition: window.scrollY }, document.title) history.pushState(null, '', href) }}
通过执行 updateHistory
,先调用 history.replaceState
,将以后页面的地位信息 scrollY
保留到 history state 中;再调用 history.pushState
,更新 url;最初再调用 loadPage
加载 url 对应的页面,外围代码如下:
// src/client/app.tslet pageFilePath = pathToFile(path)let pageModule = null// 省略代码pageModule = import(/*@vite-ignore*/ pageFilePath + '?t=' + Date.now())// 省略代码return pageModule
pathToFile
函数将传入的 url 转成 md 后缀的门路,也就是对应的 Markdown 文件,再通过 import
导入对应门路的文件;举个例子,假如 url 为 /ruofee
,那么最终后果为:import(/*@vite-ignore*/ 'ruofee.md?t=以后的工夫戳')
;
同时监听 popstate 事件,当用户应用浏览器返回、后退等操作时,调用 loadPage
办法,加载 url 对应的 md 文件,并依据 history state 中保留的页面地位信息进行定位:
// src/client/app/router.tswindow.addEventListener('popstate', async (e) => { await loadPage( normalizeHref(location.href), (e.state && e.state.scrollPosition) || 0 ) router.onAfterRouteChanged?.(location.href)})// 省略代码 - loadPagewindow.scrollTo(0, scrollPosition)
创立 Vue 利用
// src/client/app.tsimport { createApp, type App} from 'vue'// 省略代码function newApp(): App { // 省略代码 return createApp(VitePressApp)}const app = newApp()
首先通过执行 createApp(VitePressApp)
创立 Vue 利用,VitePressApp
是以后主题的 Layout 组件(@theme
是别名配置,指向以后主题,若是没有设置,则默认为 src/client/theme-default
):
// src/client/app.tsimport RawTheme from '@theme/index'const Theme = resolveThemeExtends(RawTheme)const VitePressApp = defineComponent({ name: 'VitePressApp', setup() { // 省略代码 return () => h(Theme.Layout!) }})
再将下面的路由对象注册到 Vue 利用中,并注册两个全局组件:Content
和 ClientOnly
:
// src/client/app.ts// 将路由注入 appapp.provide(RouterSymbol, router)const data = initData(router.route)app.provide(dataSymbol, data)// 注册全局组件app.component('Content', Content)app.component('ClientOnly', ClientOnly)
Markdown 渲染
直到目前为止,咱们曾经启动了 Vite 服务,咱们能够在浏览器中拜访 HTML,并执行 Script 创立 Vue 利用,实现了路由零碎,当咱们拜访对应链接时,便会加载对应的 Markdown 文件,但你必定会有纳闷:咱们的 Markdown 文件如何被解析渲染到页面中呢?
其实在启动服务的局部中,咱们提到了一个名为 vitepress 的 vite 插件,Markdown 渲染工作便是在这个插件的 transform
生命周期中实现:
// src/node/plugin.ts{ async transform(code, id) { if (id.endsWith('.vue')) { return processClientJS(code, id) } else if (id.endsWith('.md')) { // transform .md files into vueSrc so plugin-vue can handle it const { vueSrc, deadLinks, includes } = await markdownToVue( code, id, config.publicDir ) // 省略代码 const res = processClientJS(vueSrc, id) return res } }}
当咱们应用 import
加载 md 文件时,便会调用 transform
函数,对文件内容进行转换:执行 markdownToVue
,将 markdown 内容转成 Vue SFC,再通过 @vitejs/plugin-vue
插件将 Vue 组件渲染到页面;那么 markdownToVue
做了什么工作呢?具体如下:
// src/node/markdownToVue.tsconst html = md.render(src, env)const vueSrc = [ // 省略代码 `<template><div>${html}</div></template>`, // 省略代码].join('\n')
这部分比较简单,md 是一个 markdown-it 对象,通过调用 md.render
函数,将 markdown 内容转成 HTML 格局,再输入到页面;
值得一提的是,若是你在 markdown 中书写 Vue 组件语法,因为是非 markdown 语法,因而 markdown-it 不会对其进行转换,那么 Vue 语法将在页面中得以执行,官网中的例子便是利用这个原理!
总结
以上便是 Vitepress 大抵的原理,Vitepress 是一个十分优良的文档构建工具,其中有很多设计上的细节文章没提到,具体大家能够自行去 Github 上查看源码!