From 62ac0b3371d6c33e923fc8bcd98149190cf436df Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Sun, 21 Jul 2024 00:52:03 +0800 Subject: [PATCH] feat(theme): add `@vuepress/plugin-cache` --- .vscode/launch.json | 10 +- docs/notes/theme/config/主题配置.md | 20 +++ plugins/plugin-shikiji/src/node/highlight.ts | 15 +- .../plugin-shikiji/src/node/shikiPlugin.ts | 2 +- pnpm-lock.yaml | 35 +++-- theme/package.json | 11 ++ theme/src/node/config/resolveThemeOption.ts | 2 + theme/src/node/extendsMarkdown.ts | 134 ------------------ theme/src/node/plugins/getPlugins.ts | 7 + theme/src/node/theme.ts | 12 +- theme/src/shared/options/index.ts | 10 ++ theme/src/shared/options/plugins.ts | 5 +- 12 files changed, 97 insertions(+), 166 deletions(-) delete mode 100644 theme/src/node/extendsMarkdown.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 7d29fa0a..d4ebb81e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,13 +5,19 @@ "name": "dev", "request": "launch", "type": "node-terminal", - "command": "pnpm run dev" + "command": "pnpm dev" + }, + { + "name": "build", + "request": "launch", + "type": "node-terminal", + "command": "pnpm build" }, { "name": "docs:dev", "type": "node-terminal", "request": "launch", - "command": "pnpm run docs:dev" + "command": "pnpm docs:dev" }, { "name": "docs:build", diff --git a/docs/notes/theme/config/主题配置.md b/docs/notes/theme/config/主题配置.md index 7c818f00..7a14c0d3 100644 --- a/docs/notes/theme/config/主题配置.md +++ b/docs/notes/theme/config/主题配置.md @@ -161,6 +161,26 @@ interface BlogOptions { } ``` +### cache + +- 类型: `false | 'memory' | 'filesystem'` +- 默认值: `filesystem` +- 详情: + + 是否启用 编译缓存,或配置缓存方式 + + 此配置项用于解决 VuePress 启动速度慢的问题,在首次启动服务时,对编译结果进行缓存,二次启动时 + 直接读取缓存,跳过编译,从而加快启动速度。 + + - `false`:禁用 缓存 + - `'memory'`:使用内存缓存,此方式可获得更快的启动速度,但随着项目文件数量增加,内存占用会增加, + 适合文章数量较少的项目使用 + - `'filesystem'`:使用文件系统缓存,此方式可获得相对快且稳定的启动速度,更适合内容多的项目使用 + + ::: warning + 该字段不支持在 [主题配置文件 `plume.config.js`](./配置说明.md#主题配置文件) 中进行配置。 + ::: + ### locales - 类型: `Record` diff --git a/plugins/plugin-shikiji/src/node/highlight.ts b/plugins/plugin-shikiji/src/node/highlight.ts index 31caddf6..49e1f69e 100644 --- a/plugins/plugin-shikiji/src/node/highlight.ts +++ b/plugins/plugin-shikiji/src/node/highlight.ts @@ -19,11 +19,10 @@ import { transformerRenderWhitespace, } from '@shikijs/transformers' import type { HighlighterOptions, ThemeOptions } from './types.js' -import { LRUCache, attrsToLines, resolveLanguage } from './utils/index.js' +import { attrsToLines, resolveLanguage } from './utils/index.js' import { defaultHoverInfoProcessor, transformerTwoslash } from './twoslash/rendererTransformer.js' const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10) -const cache = new LRUCache(64) const vueRE = /-vue$/ const mustacheRE = /\{\{.*?\}\}/g @@ -32,7 +31,6 @@ const decorationsRE = /^\/\/ @decorations:(.*)\n/ export async function highlight( theme: ThemeOptions, options: HighlighterOptions, - isDev: boolean, ): Promise<(str: string, lang: string, attrs: string) => string> { const { defaultHighlightLang: defaultLang = '', @@ -95,14 +93,6 @@ export async function highlight( let lang = resolveLanguage(language) || defaultLang const vPre = vueRE.test(lang) ? '' : 'v-pre' - const key = str + language + attrs - - if (isDev) { - const rendered = cache.get(key) - if (rendered) - return rendered - } - if (lang) { const langLoaded = loadedLanguages.includes(lang as any) if (!langLoaded && !isPlainLang(lang) && !isSpecialLang(lang)) { @@ -183,9 +173,6 @@ export async function highlight( const rendered = restoreMustache(highlighted) - if (isDev) - cache.set(key, rendered) - return rendered } catch (e) { diff --git a/plugins/plugin-shikiji/src/node/shikiPlugin.ts b/plugins/plugin-shikiji/src/node/shikiPlugin.ts index bedf633c..2d961a9d 100644 --- a/plugins/plugin-shikiji/src/node/shikiPlugin.ts +++ b/plugins/plugin-shikiji/src/node/shikiPlugin.ts @@ -51,7 +51,7 @@ export function shikiPlugin({ extendsMarkdown: async (md, app) => { const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' } - md.options.highlight = await highlight(theme, options, app.env.isDev) + md.options.highlight = await highlight(theme, options) md.use(highlightLinesPlugin) md.use(preWrapperPlugin, { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad469a4c..d232ca5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -259,6 +259,9 @@ importers: theme: dependencies: + '@iconify/vue': + specifier: ^4.1.2 + version: 4.1.2(vue@3.4.33(typescript@5.5.3)) '@pengzhanbo/utils': specifier: ^1.1.2 version: 1.1.2 @@ -283,6 +286,9 @@ importers: '@vuepress/plugin-active-header-links': specifier: 2.0.0-rc.39 version: 2.0.0-rc.39(typescript@5.5.3)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3))) + '@vuepress/plugin-cache': + specifier: 2.0.0-rc.39 + version: 2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3))) '@vuepress/plugin-comment': specifier: 2.0.0-rc.39 version: 2.0.0-rc.39(typescript@5.5.3)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3))) @@ -361,6 +367,10 @@ importers: vuepress-plugin-md-power: specifier: workspace:* version: link:../plugins/plugin-md-power + devDependencies: + '@iconify/json': + specifier: ^2.2.229 + version: 2.2.229 packages: @@ -1934,6 +1944,11 @@ packages: peerDependencies: vuepress: 2.0.0-rc.14 + '@vuepress/plugin-cache@2.0.0-rc.39': + resolution: {integrity: sha512-PVsC797lGMuu8L7jtW9vv2hYM+d5qq5fbWwBJuSyRXEdpcwryhAjGWnz9F19dYe5KWLYG6EbCoANTQObmiyBag==} + peerDependencies: + vuepress: 2.0.0-rc.14 + '@vuepress/plugin-comment@2.0.0-rc.39': resolution: {integrity: sha512-/oCS+0wH/MtE4c1HUKlqH/tj70oXSz/tfR1hsHj8F8wiZ+IVJxexvtzMKk0vdRmYnH4nqeZh6dg5ggSJjrLEZQ==} peerDependencies: @@ -3981,14 +3996,13 @@ packages: resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==} engines: {node: '>=0.10.0'} - lru-cache@10.0.1: - resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} - engines: {node: 14 || >=16.14} - lru-cache@10.0.2: resolution: {integrity: sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==} engines: {node: 14 || >=16.14} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.0.0: resolution: {integrity: sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==} engines: {node: 20 || >=22} @@ -7269,6 +7283,11 @@ snapshots: - '@vue/composition-api' - typescript + '@vuepress/plugin-cache@2.0.0-rc.39(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3)))': + dependencies: + lru-cache: 10.4.3 + vuepress: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3)) + '@vuepress/plugin-comment@2.0.0-rc.39(typescript@5.5.3)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3)))': dependencies: '@vuepress/helper': 2.0.0-rc.39(typescript@5.5.3)(vuepress@2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(jiti@1.21.6)(tsx@4.16.0)(typescript@5.5.3)(yaml@2.4.2))(typescript@5.5.3)(vue@3.4.33(typescript@5.5.3))) @@ -9534,11 +9553,11 @@ snapshots: longest@2.0.1: {} - lru-cache@10.0.1: {} - lru-cache@10.0.2: dependencies: - semver: 7.6.0 + semver: 7.6.3 + + lru-cache@10.4.3: {} lru-cache@11.0.0: {} @@ -10331,7 +10350,7 @@ snapshots: path-scurry@1.10.1: dependencies: - lru-cache: 10.0.1 + lru-cache: 10.0.2 minipass: 5.0.0 path-scurry@2.0.0: diff --git a/theme/package.json b/theme/package.json index b7aee393..0b6be2f4 100644 --- a/theme/package.json +++ b/theme/package.json @@ -59,9 +59,16 @@ "tsup:watch": "tsup --config tsup.config.ts --watch" }, "peerDependencies": { + "@iconify/json": "^2", "vuepress": "2.0.0-rc.14" }, + "peerDependenciesMeta": { + "@iconify/json": { + "optional": true + } + }, "dependencies": { + "@iconify/vue": "^4.1.2", "@pengzhanbo/utils": "^1.1.2", "@vuepress-plume/plugin-content-update": "workspace:*", "@vuepress-plume/plugin-fonts": "workspace:*", @@ -70,6 +77,7 @@ "@vuepress-plume/plugin-shikiji": "workspace:*", "@vuepress/helper": "2.0.0-rc.39", "@vuepress/plugin-active-header-links": "2.0.0-rc.39", + "@vuepress/plugin-cache": "2.0.0-rc.39", "@vuepress/plugin-comment": "2.0.0-rc.39", "@vuepress/plugin-docsearch": "2.0.0-rc.39", "@vuepress/plugin-git": "2.0.0-rc.38", @@ -95,5 +103,8 @@ "vue-router": "^4.4.0", "vuepress-plugin-md-enhance": "2.0.0-rc.52", "vuepress-plugin-md-power": "workspace:*" + }, + "devDependencies": { + "@iconify/json": "^2.2.229" } } diff --git a/theme/src/node/config/resolveThemeOption.ts b/theme/src/node/config/resolveThemeOption.ts index 54648591..040da522 100644 --- a/theme/src/node/config/resolveThemeOption.ts +++ b/theme/src/node/config/resolveThemeOption.ts @@ -6,6 +6,7 @@ export function resolveThemeOptions({ plugins, hostname, configFile, + cache, ...localeOptions }: PlumeThemeOptions) { const pluginOptions = plugins ?? themePlugins ?? {} @@ -17,6 +18,7 @@ export function resolveThemeOptions({ } return { + cache, configFile, pluginOptions, hostname, diff --git a/theme/src/node/extendsMarkdown.ts b/theme/src/node/extendsMarkdown.ts deleted file mode 100644 index b1e2bbb3..00000000 --- a/theme/src/node/extendsMarkdown.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * 针对主题使用了 shiki + twoslash, 以及各种各样的对 markdown 的扩展, - * 导致了 markdown render 的速度变得越来越慢,如果每次启动都全量编译,那么时间开销会非常夸张。 - * 因此,对 markdown render 包装一层 缓存,通过 content hash 对比内容是否有更新, - * 没有更新的直接应用缓存从而跳过编译过程,加快启动速度。 - * - * 此功能计划做成独立的插件,但还不确定是放在 vuepress/ecosystem 还是在 主题插件内, - * 也有可能到 vuepress/core 仓库中进行更深度的优化。 - * 因此,先在本主题中进行 实验性验证。 - * - * 使用此功能后,本主题原本的启动耗时,由每次 13s 左右 优化到 二次启动时 1.2s 左右。 - * 基本只剩下 vuepress 本身的开销和 加载 shiki 所有语言带来 0.5s 左右的开销。 - */ -import process from 'node:process' -import { fs, path } from 'vuepress/utils' -import type { App } from 'vuepress' -import type { Markdown, MarkdownEnv } from 'vuepress/markdown' -import { hash } from './utils/index.js' - -export interface CacheData { - content: string - env: MarkdownEnv -} - -// { [filepath]: CacheDta } -export type Cache = Record - -// { [filepath]: hash } -export type Metadata = Record - -const CACHE_DIR = 'markdown/rendered' -const META_FILE = '_metadata.json' - -export async function extendsMarkdown(md: Markdown, app: App): Promise { - if (app.env.isBuild && !fs.existsSync(app.dir.cache(CACHE_DIR))) { - return - } - const basename = app.dir.cache(CACHE_DIR) - - await fs.ensureDir(basename) - - const speed = checkIOSpeed(basename) - - const metaFilepath = `${basename}/${META_FILE}` - - const metadata = (await readFile(metaFilepath)) || {} - - let timer: ReturnType | null = null - const update = (filepath: string, data: CacheData): void => { - writeFile(`${basename}/${filepath}`, data) - - if (timer) { - clearTimeout(timer) - } - timer = setTimeout(async () => writeFile(metaFilepath, metadata), 200) - } - const rawRender = md.render - md.render = (input, env: MarkdownEnv) => { - const filepath = env.filePathRelative - - if (!filepath) { - return rawRender(input, env) - } - - const key = hash(input) - const filename = normalizeFilename(filepath) - - if (metadata[filepath] === key) { - const cached = readFileSync(`${basename}/${filename}`) - if (cached) { - Object.assign(env, cached.env) - return cached.content - } - else { - metadata[filepath] = '' - } - } - const start = performance.now() - const content = rawRender(input, env) - - /** - * High-frequency I/O is also a time-consuming operation, - * therefore, for render operations with low overhead, caching is not performed. - */ - if (performance.now() - start > speed) { - metadata[filepath] = key - update(filename, { content, env }) - } - return content - } -} - -function normalizeFilename(filename: string): string { - return hash(filename).slice(0, 10) -} - -async function readFile(filepath: string): Promise { - try { - const content = await fs.readFile(filepath, 'utf-8') - return JSON.parse(content) as T - } - catch { - return null - } -} - -function readFileSync(filepath: string): T | null { - try { - const content = fs.readFileSync(filepath, 'utf-8') - return JSON.parse(content) as T - } - catch { - return null - } -} - -async function writeFile(filepath: string, data: T): Promise { - return await fs.writeFile(filepath, JSON.stringify(data), 'utf-8') -} - -export function checkIOSpeed(cwd = process.cwd()): number { - try { - const tmp = path.join(cwd, 'tmp') - fs.writeFileSync(tmp, '{}', 'utf-8') - const start = performance.now() - readFileSync(tmp) - const end = performance.now() - fs.unlinkSync(tmp) - return end - start - } - catch { - return 0.15 - } -} diff --git a/theme/src/node/plugins/getPlugins.ts b/theme/src/node/plugins/getPlugins.ts index 1be808c9..02db9c3d 100644 --- a/theme/src/node/plugins/getPlugins.ts +++ b/theme/src/node/plugins/getPlugins.ts @@ -1,5 +1,6 @@ import type { App, PluginConfig } from 'vuepress/core' import { activeHeaderLinksPlugin } from '@vuepress/plugin-active-header-links' +import { cachePlugin } from '@vuepress/plugin-cache' import { docsearchPlugin } from '@vuepress/plugin-docsearch' import { gitPlugin } from '@vuepress/plugin-git' import { photoSwipePlugin } from '@vuepress/plugin-photo-swipe' @@ -27,12 +28,14 @@ export interface SetupPluginOptions { app: App pluginOptions: PlumeThemePluginOptions hostname?: string + cache?: false | 'memory' | 'filesystem' } export function getPlugins({ app, pluginOptions, hostname, + cache, }: SetupPluginOptions): PluginConfig { const isProd = !app.env.isDev @@ -156,5 +159,9 @@ export function getPlugins({ plugins.push(seoPlugin({ hostname })) } + if (cache !== false) { + plugins.push(cachePlugin({ type: cache || 'filesystem' })) + } + return plugins } diff --git a/theme/src/node/theme.ts b/theme/src/node/theme.ts index bb05790f..77123e9c 100644 --- a/theme/src/node/theme.ts +++ b/theme/src/node/theme.ts @@ -26,7 +26,6 @@ import { } from './autoFrontmatter/index.js' import { prepareData, watchPrepare } from './prepare/index.js' import { prepareThemeData } from './prepare/prepareThemeData.js' -import { extendsMarkdown } from './extendsMarkdown.js' export function plumeTheme(options: PlumeThemeOptions = {}): Theme { const { @@ -34,6 +33,7 @@ export function plumeTheme(options: PlumeThemeOptions = {}): Theme { pluginOptions, hostname, configFile, + cache, } = resolveThemeOptions(options) return (app) => { @@ -65,7 +65,7 @@ export function plumeTheme(options: PlumeThemeOptions = {}): Theme { alias: resolveAlias(), - plugins: getPlugins({ app, pluginOptions, hostname }), + plugins: getPlugins({ app, pluginOptions, hostname, cache }), onInitialized: async (app) => { const { localeOptions } = await waitForConfigLoaded() @@ -90,14 +90,14 @@ export function plumeTheme(options: PlumeThemeOptions = {}): Theme { }, extendsPage: async (page) => { - const { localeOptions } = await waitForConfigLoaded() - await waitForAutoFrontmatter() + const { localeOptions, autoFrontmatter } = await waitForConfigLoaded() + if ((autoFrontmatter ?? pluginOptions.frontmatter) !== false) { + await waitForAutoFrontmatter() + } extendsPageData(page as Page, localeOptions) resolvePageHead(page, localeOptions) }, - extendsMarkdown, - extendsBundlerOptions, templateBuildRenderer, diff --git a/theme/src/shared/options/index.ts b/theme/src/shared/options/index.ts index fcc83290..5e080e76 100644 --- a/theme/src/shared/options/index.ts +++ b/theme/src/shared/options/index.ts @@ -23,6 +23,13 @@ export interface PlumeThemeOptions extends PlumeThemeLocaleOptions { */ hostname?: string + /** + * 是否启用编译缓存 + * + * @default 'filesystem' + */ + cache?: false | 'memory' | 'filesystem' + /** * 加密配置 */ @@ -33,6 +40,9 @@ export interface PlumeThemeOptions extends PlumeThemeLocaleOptions { */ configFile?: string + /** + * 自动插入 frontmatter + */ autoFrontmatter?: false | Omit } diff --git a/theme/src/shared/options/plugins.ts b/theme/src/shared/options/plugins.ts index 8bc1cac5..a50abc2e 100644 --- a/theme/src/shared/options/plugins.ts +++ b/theme/src/shared/options/plugins.ts @@ -60,13 +60,16 @@ export interface PlumeThemePluginOptions { * @deprecated * 请使用 [@vuepress/plugin-baidu-analytics](https://ecosystem.vuejs.press/zh/plugins/analytics/baidu-analytics.html) 代替 */ - baiduTongji?: never + baiduTongji?: false | { key: string } /** * @deprecated 使用 `autoFrontmatter` 代替 */ frontmatter?: Omit + /** + * 阅读时间、字数统计 + */ readingTime?: false | ReadingTimePluginOptions /**