主页 > 手机  > 

vitePress实现原理(三)

vitePress实现原理(三)
(二)createVitePressPlugin函数

源码位置:src/node/plugins.ts

import path from 'path' // 导入路径模块 import c from 'picocolors' // 导入颜色模块用于控制台输出着色 import { mergeConfig, // 合并配置函数 searchForWorkspaceRoot, // 查找工作区根目录函数 type ModuleNode, // Vite模块节点类型 type Plugin, // Vite插件类型 type ResolvedConfig, // 解析后的Vite配置类型 type Rollup, // Rollup打包工具类型 type UserConfig // 用户自定义Vite配置类型 } from 'vite' import { APP_PATH, // 应用程序路径常量 DIST_CLIENT_PATH, // 客户端分发路径常量 SITE_DATA_REQUEST_PATH, // 站点数据请求路径常量 resolveAliases // 解析别名函数 } from './alias' import { resolvePages, resolveUserConfig, type SiteConfig } from './config' // 解析页面和用户配置函数及站点配置类型 import { disposeMdItInstance } from './markdown/markdown' // 清理Markdown实例函数 import { clearCache, // 清除缓存函数 createMarkdownToVueRenderFn, // 创建Markdown转Vue渲染函数 type MarkdownCompileResult // Markdown编译结果类型 } from './markdownToVue' import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin' // 动态路由插件 import { localSearchPlugin } from './plugins/localSearchPlugin' // 本地搜索插件 import { rewritesPlugin } from './plugins/rewritesPlugin' // 重写插件 import { staticDataPlugin } from './plugins/staticDataPlugin' // 静态数据插件 import { webFontsPlugin } from './plugins/webFontsPlugin' // Web字体插件 import { slash, type PageDataPayload } from './shared' // 斜杠处理函数及页面数据负载类型 import { deserializeFunctions, serializeFunctions } from './utils/fnSerialize' // 函数序列化和反序列化工具 declare module 'vite' { interface UserConfig { vitepress?: SiteConfig // 扩展Vite用户配置接口,添加vitepress字段 } } const themeRE = /\/\.vitepress\/theme\/index\.(m|c)?(j|t)s$/ // 主题文件正则表达式 const hashRE = /\.([-\w]+)\.js$/ // 哈希值正则表达式 const staticInjectMarkerRE = /\b(const _hoisted_\d+ = \/\*(?:#|@)__PURE__\*\/\s*createStaticVNode)\("(.*)", (\d+)\)/g // 静态注入标记正则表达式 const staticStripRE = /['"`]__VP_STATIC_START__[^]*?__VP_STATIC_END__['"`]/g // 静态剥离正则表达式 const staticRestoreRE = /__VP_STATIC_(START|END)__/g // 静态恢复正则表达式 // 匹配MPA模式下的客户端JavaScript块。 const scriptClientRE = /<script\b[^>]*client\b[^>]*>([^]*?)<\/script>/ const isPageChunk = ( chunk: Rollup.OutputAsset | Rollup.OutputChunk ): chunk is Rollup.OutputChunk & { facadeModuleId: string } => !!( chunk.type === 'chunk' && // 检查是否为chunk类型 chunk.isEntry && // 检查是否为主入口 chunk.facadeModuleId && // 检查是否有facadeModuleId chunk.facadeModuleId.endsWith('.md') // 检查facadeModuleId是否以.md结尾 ) const cleanUrl = (url: string): string => url.replace(/#.*$/s, '').replace(/\?.*$/s, '') // 移除URL中的哈希和查询参数 export async function createVitePressPlugin( siteConfig: SiteConfig, ssr = false, pageToHashMap?: Record<string, string>, clientJSMap?: Record<string, string>, recreateServer?: () => Promise<void> ) { const { srcDir, configPath, configDeps, markdown, site, vue: userVuePluginOptions, vite: userViteConfig, pages, lastUpdated, cleanUrls } = siteConfig let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>> // Markdown转Vue渲染函数的结果 const userCustomElementChecker = userVuePluginOptions?.template? pilerOptions?.isCustomElement // 用户自定义元素检查器 let isCustomElement = userCustomElementChecker // 初始化自定义元素检查器 if (markdown?.math) { isCustomElement = (tag) => { if (tag.startsWith('mjx-')) { return true } return userCustomElementChecker?.(tag) ?? false // 使用用户自定义元素检查器或默认返回false } } // 懒加载导入plugin-vue以尊重NODE_ENV在@vue/compiler-x中的设置 const vuePlugin = await import('@vitejs/plugin-vue').then((r) => r.default({ include: [/\.vue$/, /\.md$/], // 包含.vue和.md文件 ...userVuePluginOptions, // 合并用户Vue插件选项 template: { ...userVuePluginOptions?.template, // 合并模板选项 compilerOptions: { ...userVuePluginOptions?.template? pilerOptions, // 合并编译器选项 isCustomElement // 设置自定义元素检查器 } } }) ) const processClientJS = (code: string, id: string) => { return scriptClientRE.test(code) ? code.replace(scriptClientRE, (_, content) => { if (ssr && clientJSMap) clientJSMap[id] = content // 如果是SSR并且有clientJSMap,则存储内容 return `\n`.repeat(_.split('\n').length - 1) // 替换为相同数量的新行 }) : code // 如果不匹配则返回原始代码 } let siteData = site // 初始化站点数据 let allDeadLinks: MarkdownCompileResult['deadLinks'] = [] // 初始化所有死链数组 let config: ResolvedConfig // 解析后的Vite配置 let importerMap: Record<string, Set<string> | undefined> = {} // 导入映射 const vitePressPlugin: Plugin = { name: 'vitepress', // 插件名称 async configResolved(resolvedConfig) { // 触发时机:当Vite完成配置解析后调用 config = resolvedConfig // 设置解析后的配置 markdownToVue = await createMarkdownToVueRenderFn( srcDir, markdown, pages, config mand === 'build', config.base, lastUpdated, cleanUrls, siteConfig ) // 创建Markdown转Vue渲染函数 }, config() { // 触发时机:在Vite读取配置文件时调用 const baseConfig: UserConfig = { resolve: { alias: resolveAliases(siteConfig, ssr) // 解析别名 }, define: { __VP_LOCAL_SEARCH__: site.themeConfig?.search?.provider === 'local', // 是否启用本地搜索 __ALGOLIA__: site.themeConfig?.search?.provider === 'algolia' || !!site.themeConfig?.algolia, // 是否启用Algolia搜索 __CARBON__: !!site.themeConfig?.carbonAds, // 是否启用Carbon广告 __ASSETS_DIR__: JSON.stringify(siteConfig.assetsDir), // 资源目录 __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: !!process.env.DEBUG // Vue生产水合调试信息 }, optimizeDeps: { // 强制包含vue以避免链接和优化时出现重复副本 include: [ 'vue', 'vitepress > @vue/devtools-api', 'vitepress > @vueuse/core' ], exclude: ['@docsearch/js', 'vitepress'] // 排除某些依赖 }, server: { fs: { allow: [ DIST_CLIENT_PATH, srcDir, searchForWorkspaceRoot(process.cwd()) ] // 允许访问的文件系统路径 } }, vitepress: siteConfig // VitePress配置 } return userViteConfig ? mergeConfig(baseConfig, userViteConfig) : baseConfig // 合并用户配置和基础配置 }, resolveId(id) { // 触发时机:当Vite需要解析一个模块ID时调用 if (id === SITE_DATA_REQUEST_PATH) { return SITE_DATA_REQUEST_PATH // 返回站点数据请求路径 } }, load(id) { // 触发时机:当Vite需要加载一个模块的内容时调用 if (id === SITE_DATA_REQUEST_PATH) { let data = siteData // 生产构建中客户端不需要头部信息 if (config mand === 'build') { data = { ...siteData, head: [] } // 移除头部信息 // 在生产客户端构建中,数据内联到每个页面以避免配置更改使每个块失效 if (!ssr) { return `export default window.__VP_SITE_DATA__` // 导出全局站点数据 } } data = serializeFunctions(data) // 序列化数据 return `${deserializeFunctions};export default deserializeFunctions(JSON.parse(${JSON.stringify( JSON.stringify(data) )}))` // 反序列化数据并导出 } }, async transform(code, id) { // 触发时机:当Vite需要转换一个模块的内容时调用 if (id.endsWith('.vue')) { return processClientJS(code, id) // 处理客户端JavaScript } else if (id.endsWith('.md')) { // 将.md文件转换为vueSrc以便plugin-vue处理 const { vueSrc, deadLinks, includes } = await markdownToVue( code, id, config.publicDir ) allDeadLinks.push(...deadLinks) // 添加死链 if (includes.length) { includes.forEach((i) => { ; (importerMap[slash(i)] ??= new Set()).add(id) // 更新导入映射 this.addWatchFile(i) // 添加监视文件 }) } return processClientJS(vueSrc, id) // 处理客户端JavaScript } }, renderStart() { // 触发时机:当Vite开始渲染页面时调用 if (allDeadLinks.length > 0) { allDeadLinks.forEach(({ url, file }, i) => { siteConfig.logger.warn( c.yellow( `${i === 0 ? '\n\n' : ''}(!) 找到死链 ${c.cyan( url )} 在文件 ${c.white(c.dim(file))}` ) ) // 输出警告信息 }) siteConfig.logger.info( c.cyan( '\n如果这是预期行为,您可以通过配置禁用此检查。参考: vitepress.dev/reference/site-config#ignoredeadlinks\n' ) ) // 提示如何禁用死链检查 throw new Error(`${allDeadLinks.length} 死链(s) 被找到。`) // 抛出错误 } }, configureServer(server) { // 触发时机:当Vite启动开发服务器时调用 // 监听文件变化 configPath 项目目录默认doc if (configPath) { server.watcher.add(configPath) // 添加配置路径监听 configDeps.forEach((file) => server.watcher.add(file)) // 添加配置依赖监听 } const onFileAddDelete = async (added: boolean, _file: string) => { const file = slash(_file) // 当主题文件被创建或删除时重启服务器 if (themeRE.test(file)) { siteConfig.logger.info( c.green( `${path.relative(process.cwd(), _file)} ${added ? '已创建' : '已删除' },正在重启服务器...\n` ), { clear: true, timestamp: true } ) // 输出重启服务器信息 await recreateServer?.() // 重启服务器 } // 当Markdown文件被创建或删除时更新页面、动态路由和重写规则 if (file.endsWith('.md')) { Object.assign( siteConfig, await resolvePages( siteConfig.srcDir, siteConfig.userConfig, siteConfig.logger ) ) // 更新页面配置 } if (!added && importerMap[file]) { delete importerMap[file] // 删除导入映射 } } server.watcher .on('add', onFileAddDelete.bind(null, true)) .on('unlink', onFileAddDelete.bind(null, false)) // 绑定文件添加和删除事件 // 为服务器中间件添加自定义HTML响应 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结构 html = await server.transformIndexHtml(url, html, req.originalUrl) // 转换HTML res.end(html) // 发送响应 return } next() // 调用下一个中间件 }) } }, renderChunk(code, chunk) { // 触发时机:当Vite渲染一个代码块时调用 if (!ssr && isPageChunk(chunk as Rollup.OutputChunk)) { // 对于每个页面块, // 注入标记以标识静态字符串的开始和结束。 // 我们在这里这样做是因为在generateBundle阶段, // 块会被压缩,我们无法安全地定位字符串。 // 使用正则表达式依赖于Vue编译器核心的特定输出, // 这是一个合理的权衡,考虑到相对于完整AST解析的巨大性能提升。 code = code.replace( staticInjectMarkerRE, '$1("__VP_STATIC_START__$2__VP_STATIC_END__", $3)' ) return code } return null }, generateBundle(_options, bundle) { // 触发时机:当Vite生成最终的构建包时调用 if (ssr) { this.emitFile({ type: 'asset', fileName: 'package.json', source: '{ "private": true, "type": "module" }' }) } else { // 客户端构建: // 对于每个.md入口块,调整其名称为其正确的路径。 for (const name in bundle) { const chunk = bundle[name] if (isPageChunk(chunk)) { // 记录页面 -> 哈希关系 const hash = chunk.fileName.match(hashRE)![1] pageToHashMap![chunk.name.toLowerCase()] = hash // 注入另一个块,其中内容已被剥离 bundle[name + '-lean'] = { ...chunk, fileName: chunk.fileName.replace(/\.js$/, '.lean.js'), preliminaryFileName: chunk.preliminaryFileName.replace( /\.js$/, '.lean.js' ), code: chunk.code.replace(staticStripRE, `""`) } // 从原始代码中移除静态标记 chunk.code = chunk.code.replace(staticRestoreRE, '') } } } }, async handleHotUpdate(ctx) { // 触发时机:当Vite检测到文件热更新时调用 const { file, read, server } = ctx if (file === configPath || configDeps.includes(file)) { siteConfig.logger.info( c.green( `${path.relative( process.cwd(), file )} changed, restarting server...\n` ), { clear: true, timestamp: true } ) try { await resolveUserConfig(siteConfig.root, 'serve', 'development') } catch (err: any) { siteConfig.logger.error(err) return } disposeMdItInstance() clearCache() await recreateServer?.() return } // 热重载 .md 文件为 .vue 文件 if (file.endsWith('.md')) { const content = await read() const { pageData, vueSrc } = await markdownToVue( content, file, config.publicDir ) const relativePath = slash(path.relative(srcDir, file)) const payload: PageDataPayload = { path: `/${siteConfig.rewrites.map[relativePath] || relativePath}`, pageData } // 通知客户端更新页面数据 server.ws.send({ type: 'custom', event: 'vitepress:pageData', data: payload }) // 覆盖src以便Vue插件可以处理HMR ctx.read = () => vueSrc } } } const hmrFix: Plugin = { name: 'vitepress:hmr-fix', async handleHotUpdate({ file, server, modules }) { // 触发时机:当Vite检测到文件热更新时调用 const importers = [...(importerMap[slash(file)] || [])] if (importers.length > 0) { return [ ...modules, ...importers.map((id) => { clearCache(slash(path.relative(srcDir, id))) return server.moduleGraph.getModuleById(id) }) ].filter(Boolean) as ModuleNode[] } } } return [ vitePressPlugin, rewritesPlugin(siteConfig), vuePlugin, hmrFix, webFontsPlugin(siteConfig.useWebFonts), ...(userViteConfig?.plugins || []), await localSearchPlugin(siteConfig), staticDataPlugin, await dynamicRoutesPlugin(siteConfig) ] } 配置解析和合并: 解析用户自定义的VitePress配置。合并基础配置和用户自定义配置,生成最终的Vite配置。 主题文件处理: 监听主题文件的变化,并在发生变化时重启服务器以应用新的主题。 Markdown文件处理: 将Markdown文件转换为Vue组件以便使用Vue插件进行进一步处理。处理Markdown文件中的死链,并在检测到死链时输出警告信息。 静态资源处理: 在构建过程中对静态资源进行优化,包括注入标记以标识静态字符串的开始和结束。生成精简版本的JavaScript文件(.lean.js),剥离不必要的静态内容。 热模块替换 (HMR): 支持Markdown文件的热重载,当Markdown文件发生变化时,更新页面数据并通过WebSocket通知客户端重新加载页面。修复热更新问题,确保导入的模块能够正确地被清除和重新加载。 服务配置: 配置开发服务器,允许访问特定的文件系统路径。添加自定义中间件以处理HTML请求,并返回基本的HTML结构。 插件集成: 集成多个插件,包括动态路由插件、本地搜索插件、重写插件、静态数据插件、Web字体插件等,以增强VitePress的功能。 日志记录: 提供详细的日志记录,包括警告信息和操作步骤,便于调试和监控。 缓存管理: 清除缓存以确保每次构建都是最新的状态。

[!NOTE]

configResolved: 当Vite完成配置解析后调用,主要用于初始化和设置一些必要的变量和函数。config: 在Vite读取配置文件时调用,用于合并基础配置和用户自定义配置。resolveId: 当Vite需要解析一个模块ID时调用,主要用于处理特定路径的模块。load: 当Vite需要加载一个模块的内容时调用,主要用于加载站点数据。transform: 当Vite需要转换一个模块的内容时调用,主要用于处理Markdown文件并将其转换为Vue组件。renderStart: 当Vite开始渲染页面时调用,主要用于检查和报告死链。configureServer: 当Vite启动开发服务器时调用,主要用于配置开发服务器的行为和监听文件变化。renderChunk: 当Vite渲染一个代码块时调用,主要用于在代码块中注入静态字符串的标记。generateBundle: 当Vite生成最终的构建包时调用,主要用于生成精简版本的JavaScript文件。handleHotUpdate: 当Vite检测到文件热更新时调用,主要用于处理Markdown文件的热重载。 1.createMarkdownToVueRenderFn函数

源码位置:src/node/markdownToVue.ts

import { resolveTitleFromToken } from '@mdit-vue/shared' import _debug from 'debug' import fs from 'fs-extra' import { LRUCache } from 'lru-cache' import path from 'path' import type { SiteConfig } from './config' import { createMarkdownRenderer, type MarkdownOptions, type MarkdownRenderer } from './markdown/markdown' import { EXTERNAL_URL_RE, getLocaleForPath, slash, treatAsHtml, type HeadConfig, type MarkdownEnv, type PageData } from './shared' import { getGitTimestamp } from './utils/getGitTimestamp' import { processIncludes } from './utils/processIncludes' const debug = _debug('vitepress:md') const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 }) export interface MarkdownCompileResult { vueSrc: string // 转换后的Vue源代码 pageData: PageData // 页面数据 deadLinks: { url: string; file: string }[] // 死链信息 includes: string[] // 包含的文件路径 } export function clearCache(file?: string) { if (!file) { cache.clear() // 清除所有缓存 return } file = JSON.stringify({ file }).slice(1) cache.find((_, key) => key.endsWith(file!) && cache.delete(key)) // 清除特定文件的缓存 } export async function createMarkdownToVueRenderFn( srcDir: string, options: MarkdownOptions = {}, pages: string[], isBuild = false, base = '/', includeLastUpdatedData = false, cleanUrls = false, siteConfig: SiteConfig | null = null ) { const md = await createMarkdownRenderer( srcDir, options, base, siteConfig?.logger ) // 创建Markdown渲染器 pages = pages.map((p) => slash(p.replace(/\.md$/, ''))) // 处理页面路径 const dynamicRoutes = new Map( siteConfig?.dynamicRoutes?.routes.map((r) => [ r.fullPath, slash(path.join(srcDir, r.route)) ]) || [] ) // 处理解析动态路由 const rewrites = new Map( Object.entries(siteConfig?.rewrites.map || {}).map(([key, value]) => [ slash(path.join(srcDir, key)), slash(path.join(srcDir, value!)) ]) || [] ) // 处理解析重写规则 return async ( src: string, file: string, publicDir: string ): Promise<MarkdownCompileResult> => { const fileOrig = dynamicRoutes.get(file) || file // 获取原始文件路径 file = rewrites.get(file) || file // 获取重写后的文件路径 const relativePath = slash(path.relative(srcDir, file)) // 获取相对路径 const cacheKey = JSON.stringify({ src, file: relativePath }) if (isBuild || options.cache !== false) { const cached = cache.get(cacheKey) if (cached) { debug(`[cache hit] ${relativePath}`) // 使用缓存 return cached } } const start = Date.now() // resolve params for dynamic routes let params src = src.replace( /^__VP_PARAMS_START([^]+?)__VP_PARAMS_END__/, (_, paramsString) => { params = JSON.parse(paramsString) // 解析动态路由参数 return '' } ) // resolve includes let includes: string[] = [] src = processIncludes(srcDir, src, fileOrig, includes) // 处理包含的文件 const localeIndex = getLocaleForPath(siteConfig?.site, relativePath) // 获取语言索引 // reset env before render const env: MarkdownEnv = { path: file, relativePath, cleanUrls, includes, realPath: fileOrig, localeIndex } const html = md.render(src, env) // 渲染Markdown为HTML const { frontmatter = {}, headers = [], links = [], sfcBlocks, title = '' } = env // validate data.links const deadLinks: MarkdownCompileResult['deadLinks'] = [] const recordDeadLink = (url: string) => { deadLinks.push({ url, file: path.relative(srcDir, fileOrig) }) // 记录死链 } function shouldIgnoreDeadLink(url: string) { if (!siteConfig?.ignoreDeadLinks) { return false } if (siteConfig.ignoreDeadLinks === true) { return true } if (siteConfig.ignoreDeadLinks === 'localhostLinks') { return url.replace(EXTERNAL_URL_RE, '').startsWith('//localhost') } return siteConfig.ignoreDeadLinks.some((ignore) => { if (typeof ignore === 'string') { return url === ignore } if (ignore instanceof RegExp) { return ignore.test(url) } if (typeof ignore === 'function') { return ignore(url) } return false }) } if (links) { const dir = path.dirname(file) for (let url of links) { const { pathname } = new URL(url, 'http://a ') if (!treatAsHtml(pathname)) continue url = url.replace(/[?#].*$/, '').replace(/\.(html|md)$/, '') if (url.endsWith('/')) url += `index` let resolved = decodeURIComponent( slash( url.startsWith('/') ? url.slice(1) : path.relative(srcDir, path.resolve(dir, url)) ) ) resolved = siteConfig?.rewrites.inv[resolved + '.md']?.slice(0, -3) || resolved if ( !pages.includes(resolved) && !fs.existsSync(path.resolve(dir, publicDir, `${resolved}.html`)) && !shouldIgnoreDeadLink(url) ) { recordDeadLink(url) // 检查并记录死链 } } } let pageData: PageData = { title: inferTitle(md, frontmatter, title), // 推断标题 titleTemplate: frontmatter.titleTemplate as any, description: inferDescription(frontmatter), // 推断描述 frontmatter, headers, params, relativePath, filePath: slash(path.relative(srcDir, fileOrig)) } if (includeLastUpdatedData && frontmatter.lastUpdated !== false) { if (frontmatter.lastUpdated instanceof Date) { pageData.lastUpdated = +frontmatter.lastUpdated } else { pageData.lastUpdated = await getGitTimestamp(fileOrig) // 获取最后更新时间 } } if (siteConfig?.transformPageData) { const dataToMerge = await siteConfig.transformPageData(pageData, { siteConfig }) if (dataToMerge) { pageData = { ...pageData, ...dataToMerge } } } const vueSrc = [ ...injectPageDataCode( sfcBlocks?.scripts.map((item) => item.content) ?? [], pageData ), `<template><div>${html}</div></template>`, // 生成Vue模板 ...(sfcBlocks?.styles.map((item) => item.content) ?? []), ...(sfcBlocks?.customBlocks.map((item) => item.content) ?? []) ].join('\n') debug(`[render] ${file} in ${Date.now() - start}ms.`) // 记录渲染时间 const result = { vueSrc, pageData, deadLinks, includes } if (isBuild || options.cache !== false) { cache.set(cacheKey, result) // 设置缓存 } return result } } const scriptRE = /<\/script>/ const scriptLangTsRE = /<\s*script[^>]*\blang=['"]ts['"][^>]*/ const scriptSetupRE = /<\s*script[^>]*\bsetup\b[^>]*/ const scriptClientRE = /<\s*script[^>]*\bclient\b[^>]*/ const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/ const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)as(\s*)default/ function injectPageDataCode(tags: string[], data: PageData) { const code = `\nexport const __pageData = JSON.parse(${JSON.stringify( JSON.stringify(data) )})` const existingScriptIndex = tags.findIndex((tag) => { return ( scriptRE.test(tag) && !scriptSetupRE.test(tag) && !scriptClientRE.test(tag) ) }) const isUsingTS = tags.findIndex((tag) => scriptLangTsRE.test(tag)) > -1 if (existingScriptIndex > -1) { const tagSrc = tags[existingScriptIndex] // user has <script> tag inside markdown // if it doesn't have export default it will error out on build const hasDefaultExport = defaultExportRE.test(tagSrc) || namedDefaultExportRE.test(tagSrc) tags[existingScriptIndex] = tagSrc.replace( scriptRE, code + (hasDefaultExport ? `` : `\nexport default {name:${JSON.stringify(data.relativePath)}}`) + `</script>` ) } else { tags.unshift( `<script ${ isUsingTS ? 'lang="ts"' : '' }>${code}\nexport default {name:${JSON.stringify( data.relativePath )}}</script>` ) } return tags } const inferTitle = ( md: MarkdownRenderer, frontmatter: Record<string, any>, title: string ) => { if (typeof frontmatter.title === 'string') { const titleToken = md.parseInline(frontmatter.title, {})[0] if (titleToken) { return resolveTitleFromToken(titleToken, { shouldAllowHtml: false, shouldEscapeText: false }) } } return title // 推断标题 } const inferDescription = (frontmatter: Record<string, any>) => { const { description, head } = frontmatter if (description !== undefined) { return description } return (head && getHeadMetaContent(head, 'description')) || '' // 推断描述 } const getHeadMetaContent = ( head: HeadConfig[], name: string ): string | undefined => { if (!head || !head.length) { return undefined } const meta = head.find(([tag, attrs = {}]) => { return tag === 'meta' && attrs.name === name && attrs.content }) return meta && meta[1].content // 获取<head>中的<meta>内容 } 1.1createMarkdownRenderer函数

源码位置:src/node/markdown/markdown.ts

import { componentPlugin, type ComponentPluginOptions } from '@mdit-vue/plugin-component' import { frontmatterPlugin, type FrontmatterPluginOptions } from '@mdit-vue/plugin-frontmatter' import { headersPlugin, type HeadersPluginOptions } from '@mdit-vue/plugin-headers' import { sfcPlugin, type SfcPluginOptions } from '@mdit-vue/plugin-sfc' import { titlePlugin } from '@mdit-vue/plugin-title' import { tocPlugin, type TocPluginOptions } from '@mdit-vue/plugin-toc' import { slugify } from '@mdit-vue/shared' import type { Options } from 'markdown-it' import MarkdownIt from 'markdown-it' import anchorPlugin from 'markdown-it-anchor' import attrsPlugin from 'markdown-it-attrs' import { full as emojiPlugin } from 'markdown-it-emoji' import type { BuiltinTheme, Highlighter } from 'shiki' import type { LanguageInput, ShikiTransformer, ThemeRegistrationAny } from '@shikijs/types' import type { Logger } from 'vite' import { containerPlugin, type ContainerOptions } from './plugins/containers' import { gitHubAlertsPlugin } from './plugins/githubAlerts' import { highlight as createHighlighter } from './plugins/highlight' import { highlightLinePlugin } from './plugins/highlightLines' import { imagePlugin, type Options as ImageOptions } from './plugins/image' import { lineNumberPlugin } from './plugins/lineNumbers' import { linkPlugin } from './plugins/link' import { preWrapperPlugin } from './plugins/preWrapper' import { restoreEntities } from './plugins/restoreEntities' import { snippetPlugin } from './plugins/snippet' export type { Header } from '../shared' export type ThemeOptions = | ThemeRegistrationAny | BuiltinTheme | { light: ThemeRegistrationAny | BuiltinTheme dark: ThemeRegistrationAny | BuiltinTheme } export interface MarkdownOptions extends Options { /* ==================== General Options ==================== */ /** * Setup markdown-it instance before applying plugins */ preConfig?: (md: MarkdownIt) => void /** * Setup markdown-it instance */ config?: (md: MarkdownIt) => void /** * Disable cache (experimental) */ cache?: boolean externalLinks?: Record<string, string> /* ==================== Syntax Highlighting ==================== */ /** * Custom theme for syntax highlighting. * * You can also pass an object with `light` and `dark` themes to support dual themes. * * @example { theme: 'github-dark' } * @example { theme: { light: 'github-light', dark: 'github-dark' } } * * You can use an existing theme. * @see shiki.style/themes * Or add your own theme. * @see shiki.style/guide/load-theme */ theme?: ThemeOptions /** * Languages for syntax highlighting. * @see shiki.style/languages */ languages?: LanguageInput[] /** * Custom language aliases. * * @example { 'my-lang': 'js' } * @see shiki.style/guide/load-lang#custom-language-aliases */ languageAlias?: Record<string, string> /** * Show line numbers in code blocks * @default false */ lineNumbers?: boolean /** * Fallback language when the specified language is not available. */ defaultHighlightLang?: string /** * Transformers applied to code blocks * @see shiki.style/guide/transformers */ codeTransformers?: ShikiTransformer[] /** * Setup Shiki instance */ shikiSetup?: (shiki: Highlighter) => void | Promise<void> /** * The tooltip text for the copy button in code blocks * @default 'Copy Code' */ codeCopyButtonTitle?: string /* ==================== Markdown It Plugins ==================== */ /** * Options for `markdown-it-anchor` * @see github /valeriangalliat/markdown-it-anchor */ anchor?: anchorPlugin.AnchorOptions /** * Options for `markdown-it-attrs` * @see github /arve0/markdown-it-attrs */ attrs?: { leftDelimiter?: string rightDelimiter?: string allowedAttributes?: Array<string | RegExp> disable?: boolean } /** * Options for `markdown-it-emoji` * @see github /markdown-it/markdown-it-emoji */ emoji?: { defs?: Record<string, string> enabled?: string[] shortcuts?: Record<string, string | string[]> } /** * Options for `@mdit-vue/plugin-frontmatter` * @see github /mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter */ frontmatter?: FrontmatterPluginOptions /** * Options for `@mdit-vue/plugin-headers` * @see github /mdit-vue/mdit-vue/tree/main/packages/plugin-headers */ headers?: HeadersPluginOptions | boolean /** * Options for `@mdit-vue/plugin-sfc` * @see github /mdit-vue/mdit-vue/tree/main/packages/plugin-sfc */ sfc?: SfcPluginOptions /** * Options for `@mdit-vue/plugin-toc` * @see github /mdit-vue/mdit-vue/tree/main/packages/plugin-toc */ toc?: TocPluginOptions /** * Options for `@mdit-vue/plugin-component` * @see github /mdit-vue/mdit-vue/tree/main/packages/plugin-component */ component?: ComponentPluginOptions /** * Options for `markdown-it-container` * @see github /markdown-it/markdown-it-container */ container?: ContainerOptions /** * Math support * * You need to install `markdown-it-mathjax3` and set `math` to `true` to enable it. * You can also pass options to `markdown-it-mathjax3` here. * @default false * @see vitepress.dev/guide/markdown#math-equations */ math?: boolean | any image?: ImageOptions /** * Allows disabling the github alerts plugin * @default true * @see vitepress.dev/guide/markdown#github-flavored-alerts */ gfmAlerts?: boolean } export type MarkdownRenderer = MarkdownIt let md: MarkdownRenderer | undefined let _disposeHighlighter: (() => void) | undefined export function disposeMdItInstance() { if (md) { md = undefined _disposeHighlighter?.() } } /** * @experimental */ export async function createMarkdownRenderer( srcDir: string, options: MarkdownOptions = {}, base = '/', logger: Pick<Logger, 'warn'> = console ): Promise<MarkdownRenderer> { if (md) return md // 如果已经存在Markdown实例,则直接返回 const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' } // 设置主题,默认为github-light和github-dark const codeCopyButtonTitle = options.codeCopyButtonTitle || 'Copy Code' // 设置代码复制按钮的提示文本 const hasSingleTheme = typeof theme === 'string' || 'name' in theme // 判断是否有单一主题 let [highlight, dispose] = options.highlight ? [options.highlight, () => { }] : await createHighlighter(theme, options, logger) // 创建高亮器 _disposeHighlighter = dispose // 设置高亮器的清理函数 md = MarkdownIt({ html: true, linkify: true, highlight, ...options }) // 创建Markdown解析器实例 md.linkify.set({ fuzzyLink: false }) // 禁用模糊链接识别 md.use(restoreEntities) // 恢复实体字符 if (options.preConfig) { options.preConfig(md) // 在应用插件之前调用用户配置函数 } // 自定义插件 md.use(componentPlugin, { ...options ponent }) // 使用组件插件 .use(highlightLinePlugin) // 使用高亮行插件 .use(preWrapperPlugin, { codeCopyButtonTitle, hasSingleTheme }) // 使用预处理器包装插件 .use(snippetPlugin, srcDir) // 使用代码片段插件 .use(containerPlugin, { hasSingleTheme }, options.container) // 使用容器插件 .use(imagePlugin, options.image) // 使用图像插件 .use(linkPlugin, { target: '_blank', rel: 'noreferrer', ...options.externalLinks }, base) // 使用链接插件 .use(lineNumberPlugin, options.lineNumbers) // 使用行号插件 md.renderer.rules.table_open = function (tokens, idx, options, env, self) { return '<table tabindex="0">\n' // 为表格添加tabindex属性 } if (options.gfmAlerts !== false) { md.use(gitHubAlertsPlugin) // 使用GitHub风格警告插件 } // 第三方插件 if (!options.attrs?.disable) { md.use(attrsPlugin, options.attrs) // 使用属性插件 } md.use(emojiPlugin, { ...options.emoji }) // 使用表情符号插件 // mdit-vue 插件 md.use(anchorPlugin, { slugify, permalink: anchorPlugin.permalink.linkInsideHeader({ symbol: '&ZeroWidthSpace;', renderAttrs: (slug, state) => { // 找到与slug匹配的heading_open令牌 const idx = state.tokens.findIndex((token) => { const attrs = token.attrs const id = attrs?.find((attr) => attr[0] === 'id') return id && slug === id[1] }) // 获取实际的标题内容 const title = state.tokens[idx + 1].content return { 'aria-label': `Permalink to "${title}"` } } }), ...options.anchor } as anchorPlugin.AnchorOptions).use(frontmatterPlugin, { ...options.frontmatter } as FrontmatterPluginOptions) if (options.headers) { md.use(headersPlugin, { level: [2, 3, 4, 5, 6], slugify, ...(typeof options.headers === 'boolean' ? undefined : options.headers) } as HeadersPluginOptions) } md.use(sfcPlugin, { ...options.sfc } as SfcPluginOptions) .use(titlePlugin) .use(tocPlugin, { ...options.toc } as TocPluginOptions) if (options.math) { try { const mathPlugin = await import('markdown-it-mathjax3') md.use(mathPlugin.default ?? mathPlugin, { ...(typeof options.math === 'boolean' ? {} : options.math) }) const orig = md.renderer.rules.math_block! md.renderer.rules.math_block = (tokens, idx, options, env, self) => { return orig(tokens, idx, options, env, self).replace( /^<mjx-container /, '<mjx-container tabindex="0" ' ) } } catch (error) { throw new Error( 'You need to install `markdown-it-mathjax3` to use math support.' ) } } // 应用用户配置 if (options.config) { options.config(md) } return md } Markdown 解析器创建: 创建并配置一个 MarkdownIt 实例,用于解析 Markdown 文档。支持多种自定义选项,包括语法高亮、插件配置等。 插件集成: 集成了多个内置插件,如 componentPlugin, frontmatterPlugin, headersPlugin, sfcPlugin, tocPlugin, anchorPlugin, attrsPlugin, emojiPlugin, containerPlugin, imagePlugin, linkPlugin, preWrapperPlugin, snippetPlugin, highlightLinePlugin, lineNumberPlugin, 和 gitHubAlertsPlugin。支持第三方插件,如 markdown-it-attrs 和 markdown-it-emoji。 语法高亮: 使用 Shiki 进行语法高亮,支持多种主题和语言。提供代码行号显示和代码复制按钮功能。 扩展性: 允许用户通过 preConfig 和 config 选项自定义 Markdown 解析器。支持数学公式渲染(需要安装 markdown-it-mathjax3)。 缓存管理: 提供 disposeMdItInstance 函数来释放资源,确保每次构建都是最新的状态。 辅助功能: 提供多种辅助函数来处理 Markdown 内容,如 restoreEntities 和 highlightLinePlugin 2.resolveAliases函数

下节揭晓

标签:

vitePress实现原理(三)由讯客互联手机栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“vitePress实现原理(三)