修阅读'一直载入中':novel.md gzip + 章节索引 + loading 状态
问题定位:
- novel.md 285KB 线上 Content-Type 被 nginx 默认识别为 application/octet-stream,gzip_types 无法命中,未压缩直传
- 用户网络慢时全本下载数秒甚至更久,一直'载入中 …'
- 附带问题:renderNovel 的 sections forEach idx 和 CHAPTERS 数组错位——'终章之后·当世界再次变聪明'这个非章节 ## heading 被 skip 掉后,之后所有第二部章节的 CHAPTERS[idx] 索引都偏移一位,映射到错误的章节图(不崩溃但显示错)
修复:
1. Dockerfile nginx conf 加 location = /novel.md 用 default_type 'text/markdown; charset=utf-8',覆盖默认 octet-stream,让 gzip_types 命中。gzip_comp_level 6 / gzip_min_length 512 / gzip_vary on。预计 285KB → ~90KB
2. renderNovel 里 ch 的解析从 CHAPTERS[idx] 改为 CHAPTERS.find(c => c.n === chN),按语义 n 精确匹配(主部 1-20 + 0 终章 / 第二部 -1 到 -8)。不再受 section 索引漂移影响
3. loadNovel 细化 loading 文案'正在下载全本 …' → '正在渲染章节 …';await setTimeout(0) 让浏览器先 paint;失败时补兜底直链'直接打开 novel.md'
4. imgPath 从 './images/${ch.img}' 加防守 (ch && ch.img),第二部 img=null 的章节不渲染 broken img src
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -489,6 +489,13 @@
|
|||||||
"message": "auto-save 2026-04-18 15:40 (~1)",
|
"message": "auto-save 2026-04-18 15:40 (~1)",
|
||||||
"hash": "d46152c",
|
"hash": "d46152c",
|
||||||
"files_changed": 1
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-04-18T15:48:56+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-04-18 15:48 (~2)",
|
||||||
|
"hash": "15b2812",
|
||||||
|
"files_changed": 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,7 @@ FROM nginx:alpine
|
|||||||
|
|
||||||
COPY . /usr/share/nginx/html/
|
COPY . /usr/share/nginx/html/
|
||||||
|
|
||||||
RUN printf 'types {\n\
|
RUN printf 'server {\n\
|
||||||
text/html html htm;\n\
|
|
||||||
text/css css;\n\
|
|
||||||
text/plain txt;\n\
|
|
||||||
text/markdown md;\n\
|
|
||||||
application/javascript js;\n\
|
|
||||||
application/json json;\n\
|
|
||||||
image/jpeg jpg jpeg;\n\
|
|
||||||
image/png png;\n\
|
|
||||||
image/webp webp;\n\
|
|
||||||
image/svg+xml svg;\n\
|
|
||||||
image/x-icon ico;\n\
|
|
||||||
font/woff woff;\n\
|
|
||||||
font/woff2 woff2;\n\
|
|
||||||
}\n\
|
|
||||||
default_type application/octet-stream;\n\
|
|
||||||
\n\
|
|
||||||
server {\n\
|
|
||||||
listen 80;\n\
|
listen 80;\n\
|
||||||
server_name _;\n\
|
server_name _;\n\
|
||||||
root /usr/share/nginx/html;\n\
|
root /usr/share/nginx/html;\n\
|
||||||
@@ -33,7 +16,13 @@ server {\n\
|
|||||||
access_log off;\n\
|
access_log off;\n\
|
||||||
}\n\
|
}\n\
|
||||||
\n\
|
\n\
|
||||||
# html / md: always revalidate\n\
|
# novel.md specifically — force text/markdown so gzip triggers\n\
|
||||||
|
location = /novel.md {\n\
|
||||||
|
default_type "text/markdown; charset=utf-8";\n\
|
||||||
|
add_header Cache-Control "public, max-age=0, must-revalidate";\n\
|
||||||
|
}\n\
|
||||||
|
\n\
|
||||||
|
# html / md catch-all: always revalidate\n\
|
||||||
location ~* \\.(html|md)$ {\n\
|
location ~* \\.(html|md)$ {\n\
|
||||||
add_header Cache-Control "public, max-age=0, must-revalidate";\n\
|
add_header Cache-Control "public, max-age=0, must-revalidate";\n\
|
||||||
}\n\
|
}\n\
|
||||||
|
|||||||
@@ -922,13 +922,21 @@ function closeLightbox() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadNovel() {
|
async function loadNovel() {
|
||||||
|
const container = document.getElementById('reader-content');
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('./novel.md');
|
container.innerHTML = '<div class="loading">正在下载全本 …</div>';
|
||||||
|
const resp = await fetch('./novel.md', { cache: 'no-cache' });
|
||||||
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||||
|
container.innerHTML = '<div class="loading">正在渲染章节 …</div>';
|
||||||
const text = await resp.text();
|
const text = await resp.text();
|
||||||
|
// Yield to the browser so the loading message paints before heavy work
|
||||||
|
await new Promise(r => setTimeout(r, 0));
|
||||||
renderNovel(text);
|
renderNovel(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('reader-content').innerHTML =
|
console.error('loadNovel failed:', e);
|
||||||
'<div class="loading" style="color: var(--red)">小说载入失败:' + e.message + '</div>';
|
container.innerHTML =
|
||||||
|
'<div class="loading" style="color: var(--red)">小说载入失败:' + e.message +
|
||||||
|
'。<a href="./novel.md" style="color: var(--gold); text-decoration: underline;">直接打开 novel.md</a></div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -944,24 +952,28 @@ function renderNovel(md) {
|
|||||||
let m = heading.match(/^(第(\d+)章|终章)\s+(.+)$/);
|
let m = heading.match(/^(第(\d+)章|终章)\s+(.+)$/);
|
||||||
let isPart2 = false;
|
let isPart2 = false;
|
||||||
let num, title;
|
let num, title;
|
||||||
|
const cnMap = { '一': '01', '二': '02', '三': '03', '四': '04', '五': '05', '六': '06', '七': '07', '八': '08', '九': '09', '十': '10' };
|
||||||
|
const cnToInt = { '一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10 };
|
||||||
|
let kicker, chN;
|
||||||
if (m) {
|
if (m) {
|
||||||
const isEpilogue = !m[2];
|
const isEpilogue = !m[2];
|
||||||
num = isEpilogue ? '终' : m[2].padStart(2, '0');
|
num = isEpilogue ? '终' : m[2].padStart(2, '0');
|
||||||
title = m[3];
|
title = m[3];
|
||||||
// kicker assigned below
|
kicker = isEpilogue ? 'EPILOGUE' : 'CHAPTER ' + num;
|
||||||
var kicker = isEpilogue ? 'EPILOGUE' : 'CHAPTER ' + num;
|
chN = isEpilogue ? 0 : parseInt(m[2]);
|
||||||
} else {
|
} else {
|
||||||
const m2 = heading.match(/^第二部\s*·\s*第(.+?)章\s*·\s*(.+)$/);
|
const m2 = heading.match(/^第二部\s*·\s*第(.+?)章\s*·\s*(.+)$/);
|
||||||
if (!m2) return;
|
if (!m2) return;
|
||||||
isPart2 = true;
|
isPart2 = true;
|
||||||
const cnMap = { '一': '01', '二': '02', '三': '03', '四': '04', '五': '05', '六': '06', '七': '07', '八': '08', '九': '09', '十': '10' };
|
|
||||||
const arabic = cnMap[m2[1]] || m2[1];
|
const arabic = cnMap[m2[1]] || m2[1];
|
||||||
num = 'II · ' + arabic;
|
num = 'II · ' + arabic;
|
||||||
title = m2[2];
|
title = m2[2];
|
||||||
var kicker = 'PART II · CHAPTER ' + arabic;
|
kicker = 'PART II · CHAPTER ' + arabic;
|
||||||
|
chN = -(cnToInt[m2[1]] || parseInt(m2[1])); // -1 = Part II Ch1, -2 = Part II Ch2, ...
|
||||||
}
|
}
|
||||||
const ch = CHAPTERS[idx];
|
// Match chapter by semantic n (immune to section index drift caused by non-chapter ## headings)
|
||||||
const imgPath = ch ? `./images/${ch.img}` : '';
|
const ch = CHAPTERS.find(c => c.n === chN);
|
||||||
|
const imgPath = (ch && ch.img) ? `./images/${ch.img}` : '';
|
||||||
const paragraphs = body.split(/\n\s*\n/).map(p => {
|
const paragraphs = body.split(/\n\s*\n/).map(p => {
|
||||||
const line = p.trim();
|
const line = p.trim();
|
||||||
if (!line) return '';
|
if (!line) return '';
|
||||||
|
|||||||
Reference in New Issue
Block a user