From 27bc2fc74741285703c3ad732662b429d0a752ab Mon Sep 17 00:00:00 2001 From: hypercross Date: Fri, 13 Mar 2026 15:50:50 +0800 Subject: [PATCH] refactor: cached and reloaded file-index --- src/components/stores/yarnStore.ts | 23 ++----- src/components/utils/csv-loader.ts | 26 +++----- src/data-loader/file-index.ts | 88 ++++++++++++++++++++++++ src/data-loader/index.ts | 104 ++--------------------------- 4 files changed, 107 insertions(+), 134 deletions(-) create mode 100644 src/data-loader/file-index.ts diff --git a/src/components/stores/yarnStore.ts b/src/components/stores/yarnStore.ts index 4c0c2fd..e61e9b6 100644 --- a/src/components/stores/yarnStore.ts +++ b/src/components/stores/yarnStore.ts @@ -5,6 +5,7 @@ import {loadElementSrc, resolvePath} from "../utils/path"; import {createStore} from "solid-js/store"; import {RunnerOptions} from "../../yarn-spinner/runtime/runner"; import {getTtrpgFunctions} from "./ttrpgRunner"; +import { getIndexedData } from "../../data-loader/file-index"; type YarnSpinnerStore = { dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[], @@ -43,8 +44,8 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){ startAt: props.start, }); const {commands, functions} = getTtrpgFunctions(runner); - runner.registerFunctions(functions); runner.registerCommands(commands); + runner.registerFunctions(functions); return runner; } catch (error) { console.error('Failed to initialize YarnRunner:', error); @@ -77,7 +78,7 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){ const processRunnerOutput = () => { const runner = store.runnerInstance; if(!runner)return; - + const result = runner.currentResult; if (!result) return; @@ -100,7 +101,7 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){ }); processRunnerOutput(); }; - + return { store, advance, @@ -108,22 +109,8 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){ } } -// 缓存已加载的 yarn 文件内容 -const yarnCache = new Map(); - -// 加载 yarn 文件内容 -async function loadYarnFile(path: string): Promise { - if (yarnCache.has(path)) { - return yarnCache.get(path)!; - } - const response = await fetch(path); - const content = await response.text(); - yarnCache.set(path, content); - return content; -} - // 加载多个 yarn 文件并拼接 async function loadYarnFiles(paths: string[]): Promise { - const contents = await Promise.all(paths.map(path => loadYarnFile(path))); + const contents = await Promise.all(paths.map(path => getIndexedData(path))); return contents.join('\n'); } diff --git a/src/components/utils/csv-loader.ts b/src/components/utils/csv-loader.ts index edb142f..dd372a4 100644 --- a/src/components/utils/csv-loader.ts +++ b/src/components/utils/csv-loader.ts @@ -1,10 +1,6 @@ import { parse } from 'csv-parse/browser/esm/sync'; import yaml from 'js-yaml'; - -/** - * 全局缓存已加载的 CSV 内容 - */ -const csvCache = new Map[]>(); +import { getIndexedData } from '../../data-loader/file-index'; /** * 解析 front matter @@ -19,7 +15,7 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin // 分割内容 const parts = content.split(/(?:^|\n)---\s*\n/); - + // 至少需要三个部分:空字符串、front matter、剩余内容 if (parts.length < 3) { return { remainingContent: content }; @@ -29,10 +25,10 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin // 解析 YAML front matter const frontmatterStr = parts[1].trim(); const frontmatter = yaml.load(frontmatterStr) as JSONObject; - + // 剩余内容是第三部分及之后的所有内容 const remainingContent = parts.slice(2).join('---\n').trimStart(); - + return { frontmatter, remainingContent }; } catch (error) { console.warn('Failed to parse front matter:', error); @@ -45,16 +41,12 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin * @template T 返回数据的类型,默认为 Record */ export async function loadCSV>(path: string): Promise> { - if (csvCache.has(path)) { - return csvCache.get(path)! as CSV; - } + // 尝试从索引获取 + const content = await getIndexedData(path); - const response = await fetch(path); - const content = await response.text(); - // 解析 front matter const { frontmatter, remainingContent } = parseFrontMatter(content); - + const records = parse(remainingContent, { columns: true, comment: '#', @@ -72,8 +64,6 @@ export async function loadCSV>(path: string): Promise } } csvResult.sourcePath = path; - - csvCache.set(path, result); return csvResult; } @@ -100,4 +90,4 @@ export function processVariables (body: string, currentRow } return body?.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`) || ''; -} \ No newline at end of file +} diff --git a/src/data-loader/file-index.ts b/src/data-loader/file-index.ts new file mode 100644 index 0000000..08f2b83 --- /dev/null +++ b/src/data-loader/file-index.ts @@ -0,0 +1,88 @@ +/** + * 文件索引管理器 + * 支持任意文件类型的索引加载和缓存 + */ + +type FileIndex = Record; + +let fileIndex: FileIndex | null = null; +let indexLoadPromise: Promise | null = null; + +/** + * 加载文件索引(只加载一次) + * 支持 CLI 环境(从 JSON 加载)和 Dev 环境(使用 webpackContext 仅加载 .md 文件) + */ +function ensureIndexLoaded(): Promise { + if (indexLoadPromise) return indexLoadPromise; + + indexLoadPromise = (async () => { + // 尝试 CLI 环境:从 /__FILE_INDEX.json 加载 + try { + const response = await fetch("/__FILE_INDEX.json"); + if (response.ok) { + const index = await response.json(); + fileIndex = { ...fileIndex, ...index }; + return; + } + } catch (e) { + // CLI 索引不可用时尝试 dev 环境 + } + + // Dev 环境:使用 import.meta.webpackContext + raw loader 加载 .md, .csv, .yarn 文件 + try { + const context = import.meta.webpackContext("../../content", { + recursive: true, + regExp: /\.md|\.yarn|\.csv$/i, + }); + const keys = context.keys(); + const index: FileIndex = {}; + for (const key of keys) { + // context 返回的是模块,需要访问其 default 导出(raw-loader 处理后的内容) + const module = context(key) as { default?: string } | string; + const content = typeof module === "string" ? module : module.default ?? ""; + const normalizedPath = "/content" + key.slice(1); + index[normalizedPath] = content; + } + fileIndex = { ...fileIndex, ...index }; + } catch (e) { + // webpackContext 不可用时忽略 + } + })(); + + return indexLoadPromise; +} + +/** + * 从索引获取文件内容 + */ +export async function getIndexedData(path: string): Promise { + await ensureIndexLoaded(); + if (fileIndex && fileIndex[path]) { + return fileIndex[path]; + } + const res = await fetch(path); + const content = await res.text(); + fileIndex = fileIndex || {}; + fileIndex[path] = content; + return content; +} + +/** + * 获取指定扩展名的文件路径 + */ +export async function getPathsByExtension(ext: string): Promise { + await ensureIndexLoaded(); + if (!fileIndex) return []; + const normalizedExt = ext.startsWith(".") ? ext : `.${ext}`; + return Object.keys(fileIndex).filter(path => + path.toLowerCase().endsWith(normalizedExt.toLowerCase()) + ); +} + +/** + * 清除索引(用于测试或重新加载) + */ +export function clearIndex(): void { + fileIndex = null; + indexLoadPromise = null; +} diff --git a/src/data-loader/index.ts b/src/data-loader/index.ts index 612982b..098ab27 100644 --- a/src/data-loader/index.ts +++ b/src/data-loader/index.ts @@ -1,83 +1,19 @@ import { buildFileTree, extractHeadings, extractSection, type TocNode, type FileNode } from "./toc"; +import {getIndexedData, getPathsByExtension} from "./file-index"; export { TocNode, FileNode, extractHeadings, buildFileTree, extractSection }; -let dataIndex: Record | null = null; -let indexLoadPromise: Promise | null = null; - -/** - * 从索引获取数据 - */ -function getIndexedData(path: string): string | null { - if (dataIndex && dataIndex[path]) { - return dataIndex[path]; - } - return null; -} - -/** - * 获取所有索引路径 - */ -export function getIndexedPaths(): string[] { - if (!dataIndex) return []; - return Object.keys(dataIndex); -} - -/** - * 加载索引(只加载一次) - */ -export function ensureIndexLoaded(): Promise { - if (indexLoadPromise) return indexLoadPromise; - - indexLoadPromise = (async () => { - // 尝试 CLI 环境:从 /__CONTENT_INDEX.json 加载 - try { - const response = await fetch("/__CONTENT_INDEX.json"); - if (response.ok) { - const index = await response.json(); - dataIndex = { ...dataIndex, ...index }; - return; - } - } catch (e) { - // CLI 索引不可用时尝试 dev 环境 - } - - // Dev 环境:使用 import.meta.webpackContext + raw loader 加载 - try { - const context = import.meta.webpackContext("../../content", { - recursive: true, - regExp: /\.md$/, - }); - const keys = context.keys(); - const index: Record = {}; - for (const key of keys) { - // context 返回的是模块,需要访问其 default 导出(raw-loader 处理后的内容) - const module = context(key) as { default?: string } | string; - const content = typeof module === "string" ? module : module.default ?? ""; - const normalizedPath = "/content" + key.slice(1); - index[normalizedPath] = content; - } - dataIndex = { ...dataIndex, ...index }; - } catch (e) { - // webpackContext 不可用时忽略 - } - })(); - - return indexLoadPromise; -} - /** * 生成目录树(文件树 + 标题结构) + * 仅处理 .md 文件 */ export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeadings: Record }> { - await ensureIndexLoaded(); - - const paths = getIndexedPaths(); - const fileTree = buildFileTree(paths); + const mdPaths = await getPathsByExtension("md"); + const fileTree = buildFileTree(mdPaths); const pathHeadings: Record = {}; - for (const path of paths) { - const content = getIndexedData(path); + for (const path of mdPaths) { + const content = await getIndexedData(path); if (content) { pathHeadings[path] = extractHeadings(content); } @@ -85,31 +21,3 @@ export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeading return { fileTree, pathHeadings }; } - -/** - * 异步加载数据 - * @param path 数据路径 - * @returns 数据内容 - */ -export async function fetchData(path: string): Promise { - await ensureIndexLoaded(); - - // 首先尝试从索引获取 - const indexedData = getIndexedData(path); - if (indexedData) { - return indexedData; - } - - // 索引不存在时,使用 fetch 加载 - if (dataIndex !== null) throw new Error(`no data in index: ${path}`); - try { - const response = await fetch(path); - if (!response.ok) { - throw new Error(`Failed to fetch: ${path}`); - } - return await response.text(); - } catch (error) { - console.error("fetchData error:", error); - throw error; - } -}