'性能优化'
This commit is contained in:
@@ -80,5 +80,6 @@
|
|||||||
"typescript": "^4.1.0",
|
"typescript": "^4.1.0",
|
||||||
"webpack": "^5.78.0",
|
"webpack": "^5.78.0",
|
||||||
"webpack-obfuscator": "^3.5.1"
|
"webpack-obfuscator": "^3.5.1"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@10.25.0+sha1.2cfb3ab644446565c127f58165cc76368c9c920b"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,6 +187,11 @@
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
/* 目录 scrollIntoView 时预留固定顶栏高度(与 .book_header_div 一致) */
|
||||||
|
[data-chunk-index] {
|
||||||
|
scroll-margin-top: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 48px !important;
|
font-size: 48px !important;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -574,6 +579,10 @@
|
|||||||
max-width: 800PX;
|
max-width: 800PX;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
[data-chunk-index] {
|
||||||
|
scroll-margin-top: 56PX;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 28PX !important;
|
font-size: 28PX !important;
|
||||||
margin: 36PX 0 14PX;
|
margin: 36PX 0 14PX;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { View, Image } from "@tarojs/components";
|
|||||||
import Taro, { useRouter } from "@tarojs/taro";
|
import Taro, { useRouter } from "@tarojs/taro";
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import { AtButton, AtDrawer, AtIcon } from "taro-ui";
|
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 { chunks as infoChunks, headers as infoHeaders } from "./info.md";
|
||||||
import hackrobot from "../../images/hackrobot.jpg";
|
import hackrobot from "../../images/hackrobot.jpg";
|
||||||
|
|
||||||
@@ -13,11 +13,15 @@ function getPb() {
|
|||||||
return PB ? new PB(PB_URL) : null;
|
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 [visible, setVisible] = useState(forceVisible);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const show = forceVisible || visible;
|
const show = forceVisible || visible;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (forceVisible) setVisible(true);
|
||||||
|
}, [forceVisible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (forceVisible) return;
|
if (forceVisible) return;
|
||||||
if (visible) return;
|
if (visible) return;
|
||||||
@@ -37,9 +41,23 @@ const LazyChunk = React.memo(({ html, estimatedHeight, forceVisible }) => {
|
|||||||
}, [forceVisible, visible]);
|
}, [forceVisible, visible]);
|
||||||
|
|
||||||
if (show) {
|
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 = () => {
|
const Book = () => {
|
||||||
@@ -49,7 +67,11 @@ const Book = () => {
|
|||||||
const [paidType, setPaidType] = useState(null);
|
const [paidType, setPaidType] = useState(null);
|
||||||
const [userId, setUserId] = useState(Taro.getStorageSync("userId") || null);
|
const [userId, setUserId] = useState(Taro.getStorageSync("userId") || null);
|
||||||
const [loading, setLoading] = useState(true);
|
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 [readProgress, setReadProgress] = useState(0);
|
||||||
const [showBackTop, setShowBackTop] = useState(false);
|
const [showBackTop, setShowBackTop] = useState(false);
|
||||||
const [bookData, setBookData] = useState(null);
|
const [bookData, setBookData] = useState(null);
|
||||||
@@ -182,45 +204,94 @@ const Book = () => {
|
|||||||
if (e.target.tagName === "IMG") e.target.classList.add("loaded");
|
if (e.target.tagName === "IMG") e.target.classList.add("loaded");
|
||||||
};
|
};
|
||||||
el.addEventListener("load", onImgLoad, true);
|
el.addEventListener("load", onImgLoad, true);
|
||||||
|
let raf = 0;
|
||||||
|
let pending = false;
|
||||||
const checkCached = () => {
|
const checkCached = () => {
|
||||||
|
pending = false;
|
||||||
el.querySelectorAll("img").forEach(img => {
|
el.querySelectorAll("img").forEach(img => {
|
||||||
if (img.complete) img.classList.add("loaded");
|
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 });
|
mo.observe(el, { childList: true, subtree: true });
|
||||||
checkCached();
|
scheduleCheck();
|
||||||
return () => {
|
return () => {
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
el.removeEventListener("load", onImgLoad, true);
|
el.removeEventListener("load", onImgLoad, true);
|
||||||
mo.disconnect();
|
mo.disconnect();
|
||||||
};
|
};
|
||||||
}, [contentLoading]);
|
}, [contentLoading]);
|
||||||
|
|
||||||
const handleJump = useCallback((index) => {
|
const getMarkdownRoot = useCallback(() => {
|
||||||
setForceShowAll(true);
|
const el = contentRef.current;
|
||||||
setshow(false);
|
if (el && typeof el.querySelector === "function") return el;
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
const run = () => {
|
return document.querySelector(".Book .markdown-body");
|
||||||
const root = contentRef.current;
|
}
|
||||||
if (!root) return;
|
return null;
|
||||||
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 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 submitPayForm = (xorpayUrl, xorpayParams) => {
|
||||||
const form = document.createElement("form");
|
const form = document.createElement("form");
|
||||||
form.method = "POST";
|
form.method = "POST";
|
||||||
@@ -301,16 +372,17 @@ const Book = () => {
|
|||||||
<View className="loading-text">正在加载...</View>
|
<View className="loading-text">正在加载...</View>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View ref={contentRef} className="markdown-body">
|
<div ref={contentRef} className="markdown-body">
|
||||||
{activeChunks.map((chunk, i) => (
|
{activeChunks.map((chunk, i) => (
|
||||||
<LazyChunk
|
<LazyChunk
|
||||||
key={`${isPaid}-${i}`}
|
key={`${isPaid}-${i}`}
|
||||||
|
chunkIndex={i}
|
||||||
html={chunk.html}
|
html={chunk.html}
|
||||||
estimatedHeight={chunk.estimatedHeight}
|
estimatedHeight={chunk.estimatedHeight}
|
||||||
forceVisible={forceShowAll || i === 0}
|
forceVisible={(visibleUntilIndex !== null && i <= visibleUntilIndex) || i === 0}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AtDrawer
|
<AtDrawer
|
||||||
|
|||||||
Reference in New Issue
Block a user