feat(theme): add @vuepress/plugin-cache

This commit is contained in:
pengzhanbo 2024-07-21 00:52:03 +08:00
parent 688c96452e
commit 62ac0b3371
12 changed files with 97 additions and 166 deletions

10
.vscode/launch.json vendored
View File

@ -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",

View File

@ -161,6 +161,26 @@ interface BlogOptions {
}
```
### cache
- 类型: `false | 'memory' | 'filesystem'`
- 默认值: `filesystem`
- 详情:
是否启用 编译缓存,或配置缓存方式
此配置项用于解决 VuePress 启动速度慢的问题,在首次启动服务时,对编译结果进行缓存,二次启动时
直接读取缓存,跳过编译,从而加快启动速度。
- `false`:禁用 缓存
- `'memory'`:使用内存缓存,此方式可获得更快的启动速度,但随着项目文件数量增加,内存占用会增加,
适合文章数量较少的项目使用
- `'filesystem'`:使用文件系统缓存,此方式可获得相对快且稳定的启动速度,更适合内容多的项目使用
::: warning
该字段不支持在 [主题配置文件 `plume.config.js`](./配置说明.md#主题配置文件) 中进行配置。
:::
### locales
- 类型: `Record<string, PlumeThemeLocaleConfig>`

View File

@ -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<string, string>(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) {

View File

@ -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<PreWrapperOptions>(preWrapperPlugin, {

35
pnpm-lock.yaml generated
View File

@ -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:

View File

@ -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"
}
}

View File

@ -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,

View File

@ -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<string, CacheData>
// { [filepath]: hash }
export type Metadata = Record<string, string>
const CACHE_DIR = 'markdown/rendered'
const META_FILE = '_metadata.json'
export async function extendsMarkdown(md: Markdown, app: App): Promise<void> {
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<Metadata>(metaFilepath)) || {}
let timer: ReturnType<typeof setTimeout> | 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<CacheData>(`${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<T = any>(filepath: string): Promise<T | null> {
try {
const content = await fs.readFile(filepath, 'utf-8')
return JSON.parse(content) as T
}
catch {
return null
}
}
function readFileSync<T = any>(filepath: string): T | null {
try {
const content = fs.readFileSync(filepath, 'utf-8')
return JSON.parse(content) as T
}
catch {
return null
}
}
async function writeFile<T = any>(filepath: string, data: T): Promise<void> {
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
}
}

View File

@ -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
}

View File

@ -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<PlumeThemePageData>, localeOptions)
resolvePageHead(page, localeOptions)
},
extendsMarkdown,
extendsBundlerOptions,
templateBuildRenderer,

View File

@ -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<AutoFrontmatter, 'frontmatter'>
}

View File

@ -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<AutoFrontmatter, 'frontmatter'>
/**
*
*/
readingTime?: false | ReadingTimePluginOptions
/**