fix: caching and reactivity

This commit is contained in:
hypercross 2026-02-26 14:51:26 +08:00
parent 9bb48e0388
commit 588ae49f5f
3 changed files with 49 additions and 52 deletions

View File

@ -1,4 +1,4 @@
import { Component, createSignal, onMount } from "solid-js"; import { Component, createMemo, createSignal } from "solid-js";
import { useLocation } from "@solidjs/router"; import { useLocation } from "@solidjs/router";
// 导入组件以注册自定义元素 // 导入组件以注册自定义元素
@ -7,10 +7,9 @@ import { Article, Sidebar } from "./components";
const App: Component = () => { const App: Component = () => {
const location = useLocation(); const location = useLocation();
const [currentPath, setCurrentPath] = createSignal("");
const [isSidebarOpen, setIsSidebarOpen] = createSignal(false); const [isSidebarOpen, setIsSidebarOpen] = createSignal(false);
onMount(() => { const currentPath = createMemo(() => {
// 根据路由加载对应的 markdown 文件 // 根据路由加载对应的 markdown 文件
let path = decodeURIComponent(location.pathname); let path = decodeURIComponent(location.pathname);
@ -18,7 +17,7 @@ const App: Component = () => {
if (path.endsWith("/")) path += "index"; if (path.endsWith("/")) path += "index";
if (!path.endsWith(".md")) path += ".md"; if (!path.endsWith(".md")) path += ".md";
setCurrentPath(path); return path;
}); });
return ( return (

View File

@ -1,4 +1,4 @@
import { Component, createSignal, onMount, onCleanup, Show } from 'solid-js'; import { Component, createSignal, createEffect, onCleanup, Show, createResource } from 'solid-js';
import { parseMarkdown } from '../markdown'; import { parseMarkdown } from '../markdown';
import { fetchData, extractSection } from '../data-loader'; import { fetchData, extractSection } from '../data-loader';
@ -9,51 +9,45 @@ export interface ArticleProps {
onError?: (error: Error) => void; onError?: (error: Error) => void;
} }
async function fetchArticleContent(params: { src: string; section?: string }): Promise<string> {
const text = await fetchData(params.src);
// 如果指定了 section提取对应内容
return params.section ? extractSection(text, params.section) : text;
}
/** /**
* Article * Article
* src md markdown * src md markdown
*/ */
export const Article: Component<ArticleProps> = (props) => { export const Article: Component<ArticleProps> = (props) => {
const [content, setContent] = createSignal(''); const [content, { refetch }] = createResource(
const [loading, setLoading] = createSignal(true); () => ({ src: props.src, section: props.section }),
const [error, setError] = createSignal<Error | null>(null); fetchArticleContent
);
let articleRef: HTMLArticleElement | undefined; let articleRef: HTMLArticleElement | undefined;
onMount(async () => { createEffect(() => {
setLoading(true); const data = content();
try { if (data) {
const text = await fetchData(props.src);
// 如果指定了 section提取对应内容
const finalContent = props.section
? extractSection(text, props.section)
: text;
setContent(finalContent);
setLoading(false);
props.onLoaded?.(); props.onLoaded?.();
} catch (err) {
const errorObj = err instanceof Error ? err : new Error(String(err));
setError(errorObj);
setLoading(false);
props.onError?.(errorObj);
} }
}); });
onCleanup(() => { onCleanup(() => {
// 清理时清空内容,触发内部组件的销毁 // 清理时清空内容,触发内部组件的销毁
setContent('');
}); });
return ( return (
<article ref={articleRef} class="prose" data-src={props.src}> <article ref={articleRef} class="prose" data-src={props.src}>
<Show when={loading()}> <Show when={content.loading}>
<div class="text-gray-500">...</div> <div class="text-gray-500">...</div>
</Show> </Show>
<Show when={error()}> <Show when={content.error}>
<div class="text-red-500">{error()?.message}</div> <div class="text-red-500">{content.error?.message}</div>
</Show> </Show>
<Show when={!loading() && !error()}> <Show when={!content.loading && !content.error && content()}>
<div innerHTML={parseMarkdown(content())} /> <div innerHTML={parseMarkdown(content()!)} />
</Show> </Show>
</article> </article>
); );

View File

@ -1,5 +1,5 @@
import { customElement, noShadowDOM } from 'solid-element'; import { customElement, noShadowDOM } from 'solid-element';
import { createSignal, For, Show, createEffect, createMemo } from 'solid-js'; import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js';
import { parse } from 'csv-parse/browser/esm/sync'; import { parse } from 'csv-parse/browser/esm/sync';
import { marked } from '../markdown'; import { marked } from '../markdown';
@ -10,12 +10,14 @@ interface TableRow {
[key: string]: string; [key: string]: string;
} }
// 全局缓存已加载的 CSV 内容
const csvCache = new Map<string, TableRow[]>();
customElement('md-table', { roll: false, remix: false }, (props, { element }) => { customElement('md-table', { roll: false, remix: false }, (props, { element }) => {
noShadowDOM(); noShadowDOM();
const [rows, setRows] = createSignal<TableRow[]>([]); const [rows, setRows] = createSignal<TableRow[]>([]);
const [activeTab, setActiveTab] = createSignal(0); const [activeTab, setActiveTab] = createSignal(0);
const [activeGroup, setActiveGroup] = createSignal<string | null>(null); const [activeGroup, setActiveGroup] = createSignal<string | null>(null);
const [loaded, setLoaded] = createSignal(false);
const [bodyHtml, setBodyHtml] = createSignal(''); const [bodyHtml, setBodyHtml] = createSignal('');
// 从 element 的 textContent 获取 CSV 路径 // 从 element 的 textContent 获取 CSV 路径
@ -41,34 +43,36 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
const resolvedSrc = resolvePath(articlePath, src); const resolvedSrc = resolvePath(articlePath, src);
// 解析 CSV 内容 // 加载 CSV 文件的函数
const parseCSV = (content: string) => { const loadCSV = async (path: string): Promise<TableRow[]> => {
// 先从缓存获取
if (csvCache.has(path)) {
return csvCache.get(path)!;
}
const response = await fetch(path);
const content = await response.text();
const records = parse(content, { const records = parse(content, {
columns: true, columns: true,
comment: '#', comment: '#',
trim: true, trim: true,
skipEmptyLines: true skipEmptyLines: true
}); });
setRows(records as TableRow[]); const result = records as TableRow[];
setLoaded(true); // 缓存结果
csvCache.set(path, result);
return result;
}; };
// 加载 CSV 文件 // 使用 createResource 加载 CSV自动响应路径变化并避免重复加载
const loadCSV = async () => { const [csvData] = createResource(() => resolvedSrc, loadCSV);
try {
const response = await fetch(resolvedSrc);
const content = await response.text();
parseCSV(content);
} catch (error) {
console.error('Failed to load CSV:', error);
setLoaded(true);
}
};
// 初始化加载 // 当数据加载完成后更新 rows
if (!loaded()) { createEffect(() => {
loadCSV(); const data = csvData();
if (data) {
setRows(data);
} }
});
// 检测是否有 group 列 // 检测是否有 group 列
const hasGroup = createMemo(() => { const hasGroup = createMemo(() => {
@ -119,7 +123,7 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
// 更新 body 内容 // 更新 body 内容
const updateBodyContent = () => { const updateBodyContent = () => {
const filtered = filteredRows(); const filtered = filteredRows();
if (loaded() && filtered.length > 0) { if (!csvData.loading && filtered.length > 0) {
const index = Math.min(activeTab(), filtered.length - 1); const index = Math.min(activeTab(), filtered.length - 1);
const currentRow = filtered[index]; const currentRow = filtered[index];
setBodyHtml(processBody(currentRow.body, currentRow)); setBodyHtml(processBody(currentRow.body, currentRow));
@ -207,7 +211,7 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
</div> </div>
</div> </div>
<div class="p-1 prose max-w-none"> <div class="p-1 prose max-w-none">
<Show when={loaded() && filteredRows().length > 0}> <Show when={!csvData.loading && filteredRows().length > 0}>
<div innerHTML={bodyHtml()} /> <div innerHTML={bodyHtml()} />
</Show> </Show>
</div> </div>