'性能优化'
This commit is contained in:
@@ -80,5 +80,6 @@
|
||||
"typescript": "^4.1.0",
|
||||
"webpack": "^5.78.0",
|
||||
"webpack-obfuscator": "^3.5.1"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.25.0+sha1.2cfb3ab644446565c127f58165cc76368c9c920b"
|
||||
}
|
||||
|
||||
@@ -187,6 +187,11 @@
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
/* 目录 scrollIntoView 时预留固定顶栏高度(与 .book_header_div 一致) */
|
||||
[data-chunk-index] {
|
||||
scroll-margin-top: 88px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 48px !important;
|
||||
font-weight: 800;
|
||||
@@ -574,6 +579,10 @@
|
||||
max-width: 800PX;
|
||||
margin: 0 auto;
|
||||
|
||||
[data-chunk-index] {
|
||||
scroll-margin-top: 56PX;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28PX !important;
|
||||
margin: 36PX 0 14PX;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { View, Image } from "@tarojs/components";
|
||||
import Taro, { useRouter } from "@tarojs/taro";
|
||||
import "./index.scss";
|
||||
import { AtButton, AtDrawer, AtIcon } from "taro-ui";
|
||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||
import React, { useEffect, useState, useRef, useCallback, useLayoutEffect } from "react";
|
||||
import { chunks as infoChunks, headers as infoHeaders } from "./info.md";
|
||||
import hackrobot from "../../images/hackrobot.jpg";
|
||||
|
||||
@@ -13,11 +13,15 @@ function getPb() {
|
||||
return PB ? new PB(PB_URL) : null;
|
||||
}
|
||||
|
||||
const LazyChunk = React.memo(({ html, estimatedHeight, forceVisible }) => {
|
||||
const LazyChunk = React.memo(({ html, estimatedHeight, forceVisible, chunkIndex }) => {
|
||||
const [visible, setVisible] = useState(forceVisible);
|
||||
const ref = useRef(null);
|
||||
const show = forceVisible || visible;
|
||||
|
||||
useEffect(() => {
|
||||
if (forceVisible) setVisible(true);
|
||||
}, [forceVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (forceVisible) return;
|
||||
if (visible) return;
|
||||
@@ -37,9 +41,23 @@ const LazyChunk = React.memo(({ html, estimatedHeight, forceVisible }) => {
|
||||
}, [forceVisible, visible]);
|
||||
|
||||
if (show) {
|
||||
return <div className="chunk-enter" dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
const animate = !forceVisible;
|
||||
return (
|
||||
<div
|
||||
data-chunk-index={chunkIndex}
|
||||
className={animate ? "chunk-enter" : undefined}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <div ref={ref} className="chunk-placeholder" style={{ minHeight: estimatedHeight }} />;
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-chunk-index={chunkIndex}
|
||||
className="chunk-placeholder"
|
||||
style={{ minHeight: estimatedHeight }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const Book = () => {
|
||||
@@ -49,7 +67,11 @@ const Book = () => {
|
||||
const [paidType, setPaidType] = useState(null);
|
||||
const [userId, setUserId] = useState(Taro.getStorageSync("userId") || null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [forceShowAll, setForceShowAll] = useState(false);
|
||||
/** 目录跳转时至少渲染到该索引的 chunk;null 表示仅首块 + 懒加载 */
|
||||
const [visibleUntilIndex, setVisibleUntilIndex] = useState(null);
|
||||
/** 每次点击目录递增,驱动 useLayoutEffect 在 DOM 提交后滚动(不依赖 visibleUntilIndex 是否变化) */
|
||||
const [jumpSeq, setJumpSeq] = useState(0);
|
||||
const pendingJumpIndexRef = useRef(null);
|
||||
const [readProgress, setReadProgress] = useState(0);
|
||||
const [showBackTop, setShowBackTop] = useState(false);
|
||||
const [bookData, setBookData] = useState(null);
|
||||
@@ -182,45 +204,94 @@ const Book = () => {
|
||||
if (e.target.tagName === "IMG") e.target.classList.add("loaded");
|
||||
};
|
||||
el.addEventListener("load", onImgLoad, true);
|
||||
let raf = 0;
|
||||
let pending = false;
|
||||
const checkCached = () => {
|
||||
pending = false;
|
||||
el.querySelectorAll("img").forEach(img => {
|
||||
if (img.complete) img.classList.add("loaded");
|
||||
});
|
||||
};
|
||||
const mo = new MutationObserver(checkCached);
|
||||
const scheduleCheck = () => {
|
||||
if (pending) return;
|
||||
pending = true;
|
||||
raf = requestAnimationFrame(checkCached);
|
||||
};
|
||||
const mo = new MutationObserver(() => scheduleCheck());
|
||||
mo.observe(el, { childList: true, subtree: true });
|
||||
checkCached();
|
||||
scheduleCheck();
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
el.removeEventListener("load", onImgLoad, true);
|
||||
mo.disconnect();
|
||||
};
|
||||
}, [contentLoading]);
|
||||
|
||||
const handleJump = useCallback((index) => {
|
||||
setForceShowAll(true);
|
||||
setshow(false);
|
||||
|
||||
const run = () => {
|
||||
const root = contentRef.current;
|
||||
if (!root) return;
|
||||
const h1s = root.querySelectorAll("h1");
|
||||
const el = h1s[index];
|
||||
if (el) {
|
||||
try {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
} catch (_) {
|
||||
const top = el.getBoundingClientRect().top + (window.pageYOffset || document.documentElement.scrollTop) - 60;
|
||||
window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
setTimeout(run, 100);
|
||||
setTimeout(run, 300);
|
||||
setTimeout(run, 600);
|
||||
const getMarkdownRoot = useCallback(() => {
|
||||
const el = contentRef.current;
|
||||
if (el && typeof el.querySelector === "function") return el;
|
||||
if (typeof document !== "undefined") {
|
||||
return document.querySelector(".Book .markdown-body");
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const scrollToChapterIndex = useCallback((index, attempt = 0) => {
|
||||
const root = getMarkdownRoot();
|
||||
if (!root) return;
|
||||
const el = root.querySelector(`[data-chunk-index="${index}"]`);
|
||||
if (el?.classList.contains("chunk-placeholder") && attempt < 48) {
|
||||
requestAnimationFrame(() => scrollToChapterIndex(index, attempt + 1));
|
||||
return;
|
||||
}
|
||||
const h1list = root.querySelectorAll("h1");
|
||||
const target = el || h1list[index];
|
||||
if (!target) return;
|
||||
try {
|
||||
target.scrollIntoView({ block: "start", behavior: "auto" });
|
||||
} catch (_) {
|
||||
const rect = target.getBoundingClientRect();
|
||||
const st = window.pageYOffset || document.documentElement.scrollTop || 0;
|
||||
const dest = Math.max(0, rect.top + st - 60);
|
||||
try {
|
||||
window.scrollTo({ top: dest, behavior: "auto" });
|
||||
} catch (__) {
|
||||
window.scrollTo(0, dest);
|
||||
}
|
||||
}
|
||||
}, [getMarkdownRoot]);
|
||||
|
||||
const handleJump = useCallback((rawIndex) => {
|
||||
const index =
|
||||
typeof rawIndex === "number" && Number.isFinite(rawIndex)
|
||||
? rawIndex
|
||||
: parseInt(String(rawIndex), 10);
|
||||
if (!Number.isFinite(index) || index < 0) return;
|
||||
pendingJumpIndexRef.current = index;
|
||||
setVisibleUntilIndex(prev => Math.max(prev ?? -1, index));
|
||||
setshow(false);
|
||||
setJumpSeq(s => s + 1);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (jumpSeq === 0) return;
|
||||
const index = pendingJumpIndexRef.current;
|
||||
if (index === null || index === undefined) return;
|
||||
const run = () => scrollToChapterIndex(index);
|
||||
run();
|
||||
const t1 = setTimeout(run, 50);
|
||||
const t2 = setTimeout(run, 200);
|
||||
const t3 = setTimeout(run, 450);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(run);
|
||||
});
|
||||
return () => {
|
||||
clearTimeout(t1);
|
||||
clearTimeout(t2);
|
||||
clearTimeout(t3);
|
||||
};
|
||||
}, [jumpSeq, scrollToChapterIndex]);
|
||||
|
||||
const submitPayForm = (xorpayUrl, xorpayParams) => {
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
@@ -301,16 +372,17 @@ const Book = () => {
|
||||
<View className="loading-text">正在加载...</View>
|
||||
</View>
|
||||
) : (
|
||||
<View ref={contentRef} className="markdown-body">
|
||||
<div ref={contentRef} className="markdown-body">
|
||||
{activeChunks.map((chunk, i) => (
|
||||
<LazyChunk
|
||||
key={`${isPaid}-${i}`}
|
||||
chunkIndex={i}
|
||||
html={chunk.html}
|
||||
estimatedHeight={chunk.estimatedHeight}
|
||||
forceVisible={forceShowAll || i === 0}
|
||||
forceVisible={(visibleUntilIndex !== null && i <= visibleUntilIndex) || i === 0}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AtDrawer
|
||||
|
||||
Reference in New Issue
Block a user