Skip to content

Commit a492a14

Browse files
chaxusclaude
andcommitted
fix(pwa,landing): survive deploys in the service worker; restore per-format create entries
Production broke after deploy for returning visitors: the SW-cached stale index.html referenced hashed assets that no longer exist, and the 404's HTML body was handed to the CSS parser ("Refused to apply style… text/html") — layout collapsed (body height 100% gone → editor iframe at 0px) while the still-cached JS kept working. sw.js rewrite: - HTML navigations fetch with cache: 'no-cache' (revalidate with the server; a stale HTTP-cache HTML is exactly what pinned old asset hashes) - Split core (precached shell + HTML, never trimmed) from runtime cache (SWR, 600-item cap — the old single 100-cap cache was evicted constantly by OnlyOffice's hundreds of assets, including its own shell) - /assets/* 404s return Response.error() instead of an HTML body, and re-fetch a fresh shell so the next navigation self-heals - activate() clears every previous version's caches Guards & gaps: - lang-select change handler only navigates on a real locale switch — a same-locale (or init-time) change no longer reloads the page and wipes ?new=/?locale= deep links - Homepage regained create entries for every format: a quiet mono "start blank: Word · Excel · PowerPoint" row under the CTAs (EN + zh-CN), using the existing ?new= deep links (the FAB menu only exists once a document is open, so xlsx/pptx creation had no homepage entry at all) Verified: ?new=xlsx boots the spreadsheet editor at full height; clean-env production check confirms current deploy is intact (the breakage is the old SW's cache state, which this sw.js version replaces on next visit); format/lint/240 tests/build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a31245b commit a492a14

6 files changed

Lines changed: 127 additions & 17 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# 线上三问题:SW 跨部署缓存混乱、语言切换假 change、新建入口缺失
2+
3+
## 问题一:部署后页面样式全塌(用户报告,附控制台报错)
4+
5+
**症状**:线上打开/新建文档后,控制面板按钮无样式堆在左上角、编辑器画布高度
6+
塌陷(PPT 显示 Zoom 5%)。控制台:`Refused to apply style from
7+
'/assets/index-<hash>.css' because its MIME type ('text/html')`。
8+
9+
**根因**:旧 Service Worker 时代缓存的旧版 index.html 引用上一次部署的
10+
hashed 资产;新部署后该文件已不存在,请求 404 回退成 HTML 页面,被当作
11+
CSS 解析拒绝。JS 恰好还能命中缓存(所以按钮能点、能开文档),CSS 拿不到
12+
(`html,body{height:100%}` 丢失 → #app/iframe 高度塌陷;r-button 样式全无)。
13+
两处放大器:
14+
1. 旧 SW 的 HTML network-first 用裸 `fetch(event.request)`,可能命中 HTTP
15+
缓存里的旧 HTML(状态 200,被当"最新"缓存下来);
16+
2. 单缓存 100 条上限,OnlyOffice 一次加载几百个资产,precache 的外壳文件
17+
(index.html 等)被 LRU 挤出,离线兜底随机失效。
18+
19+
**修复**(public/sw.js 重写):
20+
- HTML 导航请求 `fetch(request, { cache: 'no-cache' })` 强制与服务器再验证,
21+
永远拿到当前部署的 HTML → hashed 引用永远匹配;
22+
- 拆双缓存:core(precache 外壳 + HTML,不参与修剪)与 runtime(SWR,上限
23+
600);activate 清理所有旧版本缓存;
24+
- `/assets/*` hashed 资产网络 404 时返回 `Response.error()`(绝不把 HTML
25+
体交给 CSS/JS 解析器),并顺手用 no-cache 重取 index.html 刷新外壳,下次
26+
导航自愈。
27+
- 用户侧恢复路径:新 sw.js 部署后(文件名固定),浏览器导航时检查到更新 →
28+
install(skipWaiting) → activate(清旧缓存+claim) → 下一次导航即恢复正常,
29+
无需用户手动清缓存。
30+
31+
## 问题二:语言切换假 change 冲掉深链参数(防御性修复)
32+
33+
在带状态的环境观察到 `/?locale=zh-CN&new=docx` 被跳回 `/`。index.ts 的
34+
lang-select change 监听器只要收到事件就整页跳转——任何来源的初始化/同值
35+
change 都会把 `?new=` `?locale=` 深链冲掉。修复:与当前页面语言相同的值不
36+
跳转(en 页收到 'en' 忽略、zh 页收到 'zh-CN' 忽略)。已核查 ranui select
37+
源码:`syncSelectedFromValue`/`setDefaultValue` 都以 shouldDispatch=false
38+
调用,初始化不派发 change——防御仍保留,成本为零、对任何回归免疫。
39+
(顺带发现 CLAUDE.md 里的 lib/i18n.ts 已不存在,文档待刷新。)
40+
41+
## 问题三:首页缺新建 Excel/PPT 入口(用户报告的功能缺失)
42+
43+
改版后 hero 只有 "New document"(仅 docx);新建 xlsx/pptx 的入口只在文档
44+
打开后的 FAB 菜单里——首页彻底没有。修复:CTA 下加一行安静的 mono 快捷行
45+
"start blank: Word · Excel · PowerPoint"(zh:"新建空白:…"),走已有的
46+
`?new=docx|xlsx|pptx` 深链(EN 直链,zh 带 `locale=zh-CN`)。样式
47+
`.new-row`(12.5px mono,链接用 accent 蓝)。
48+
49+
## 验证
50+
51+
- 干净环境实测线上:静态页组件/样式正常(问题一确系用户浏览器旧 SW 状态);
52+
- 本地 `?new=xlsx` 深链:URL 保持、landing 隐藏、编辑器 iframe 全高;
53+
- format/lint/240 单测/生产构建全绿。

‎index.html‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,12 @@ <h1 class="reveal d2">
206206
<r-button type="contrast" id="hero-open">Open a file</r-button>
207207
<r-button id="hero-new">New document</r-button>
208208
</div>
209+
<!-- Every format keeps a create entry on the homepage (the FAB menu
210+
only exists once a document is open) — ?new= deep-links. -->
211+
<p class="new-row reveal d5">
212+
start blank: <a href="/?new=docx">Word</a><span class="sep">·</span><a href="/?new=xlsx">Excel</a
213+
><span class="sep">·</span><a href="/?new=pptx">PowerPoint</a>
214+
</p>
209215
<div class="trust reveal d5">
210216
<span>0 bytes uploaded</span><span class="sep">·</span><span>no account</span><span class="sep">·</span
211217
><span>works offline</span><span class="sep">·</span><span>.docx .xlsx .pptx .csv</span>

‎index.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,12 @@ const langSelect = document.querySelector('#landing-hero r-select.lang-select');
8484
if (langSelect) {
8585
langSelect.addEventListener('change', (event) => {
8686
const value = (event as CustomEvent<{ value?: string }>).detail?.value;
87-
if (value === 'zh-CN') location.href = '/zh-CN/';
88-
else if (value === 'en') location.href = '/';
87+
// Guard: r-select can emit `change` while initializing its value on load.
88+
// Navigating on a same-as-current-locale event reloads the page and wipes
89+
// deep-link params (?new=docx, ?locale=…), so only act on a real switch.
90+
const onZhPage = location.pathname.startsWith('/zh-CN');
91+
if (value === 'zh-CN' && !onZhPage) location.href = '/zh-CN/';
92+
else if (value === 'en' && onZhPage) location.href = '/';
8993
});
9094
}
9195

‎public/home.css‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,23 @@
248248
gap: var(--ran-space-3);
249249
margin-top: clamp(24px, 2.8vw, 36px);
250250
}
251+
/* quiet per-format create row under the CTAs (the FAB menu only exists once
252+
a document is open, so the homepage keeps an entry for every format) */
253+
#landing-hero .new-row {
254+
margin: var(--ran-space-3) 0 0;
255+
font: 500 12.5px/1.6 var(--mono);
256+
color: var(--muted);
257+
}
258+
#landing-hero .new-row a {
259+
color: var(--accent);
260+
}
261+
#landing-hero .new-row a:hover {
262+
text-decoration: underline;
263+
}
264+
#landing-hero .new-row .sep {
265+
margin: 0 var(--ran-space-2);
266+
color: var(--rule);
267+
}
251268
#landing-hero .trust {
252269
margin-top: var(--ran-space-6);
253270
font: 500 12.5px/1.6 var(--mono);

‎public/sw.js‎

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
const CACHE_VERSION = 'SW_VERSION_PLACEHOLDER'.includes('PLACEHOLDER') ? 'dev-' + Date.now() : 'SW_VERSION_PLACEHOLDER';
2-
const CACHE_NAME = `document-editor-${CACHE_VERSION}`;
2+
// Two caches: core (precached shell + HTML) survives trimming; runtime holds
3+
// the long tail (OnlyOffice sdkjs/web-apps assets — hundreds of files, so the
4+
// old single 100-item cache was constantly evicting its own shell).
5+
const CORE_CACHE = `document-editor-core-${CACHE_VERSION}`;
6+
const RUNTIME_CACHE = `document-editor-runtime-${CACHE_VERSION}`;
37
const ASSETS_TO_CACHE = ['./', './index.html', './manifest.json', './img/64.png'];
48

5-
// Cache limits and clean-up configuration
6-
const MAX_CACHE_ITEMS = 100;
9+
const MAX_RUNTIME_ITEMS = 600;
710

811
// Helper: Trim cache to a certain size
912
const limitCacheSize = (name, maxItems) => {
@@ -19,20 +22,20 @@ const limitCacheSize = (name, maxItems) => {
1922
// Install event: Pre-cache core UI assets
2023
self.addEventListener('install', (event) => {
2124
event.waitUntil(
22-
caches.open(CACHE_NAME).then((cache) => {
25+
caches.open(CORE_CACHE).then((cache) => {
2326
return cache.addAll(ASSETS_TO_CACHE);
2427
}),
2528
);
2629
self.skipWaiting();
2730
});
2831

29-
// Activate event: Clean up old caches
32+
// Activate event: Clean up caches from every previous version
3033
self.addEventListener('activate', (event) => {
3134
event.waitUntil(
3235
caches.keys().then((cacheNames) => {
3336
return Promise.all(
3437
cacheNames.map((cacheName) => {
35-
if (cacheName !== CACHE_NAME) {
38+
if (cacheName !== CORE_CACHE && cacheName !== RUNTIME_CACHE) {
3639
return caches.delete(cacheName);
3740
}
3841
}),
@@ -61,26 +64,33 @@ self.addEventListener('fetch', (event) => {
6164
// causes a crash in OnlyOffice v7.5's fallback font code path.
6265
if (/\.(ttf|woff2?|otf|eot)(\?.*)?$/.test(url.pathname)) return;
6366

64-
// 4. Determine Strategy
67+
// 5. Determine Strategy
6568
const isHtml =
6669
event.request.mode === 'navigate' ||
6770
url.pathname.endsWith('.html') ||
6871
url.pathname === '/' ||
6972
url.pathname.endsWith('/');
7073

74+
// Hashed build outputs (/assets/index-<hash>.css|js). Their filenames change
75+
// on every deploy, so a stale HTML + missing asset = broken page. Treat any
76+
// non-OK answer for them as an error instead of handing an HTML 404 fallback
77+
// to the CSS/JS parser ("Refused to apply style… MIME type text/html").
78+
const isHashedAsset = url.pathname.startsWith('/assets/');
79+
7180
if (isHtml) {
72-
// Strategy: Network-First for HTML/Navigation
73-
// Ensuring the user always gets the latest version if online,
74-
// but can still access the app when offline.
81+
// Strategy: Network-First for HTML/Navigation.
82+
// `cache: 'no-cache'` forces revalidation with the server instead of
83+
// accepting a possibly-stale HTTP-cache copy — a stale HTML references
84+
// hashed assets that no longer exist after a deploy (the exact broken
85+
// state this rewrite fixes). Offline still falls back to the SW cache.
7586
event.respondWith(
76-
fetch(event.request)
87+
fetch(event.request, { cache: 'no-cache' })
7788
.then((networkResponse) => {
7889
// If network is ok, cache and return
7990
if (networkResponse && networkResponse.status === 200) {
8091
const responseToCache = networkResponse.clone();
81-
caches.open(CACHE_NAME).then((cache) => {
92+
caches.open(CORE_CACHE).then((cache) => {
8293
cache.put(event.request, responseToCache);
83-
limitCacheSize(CACHE_NAME, MAX_CACHE_ITEMS);
8494
});
8595
return networkResponse;
8696
}
@@ -101,10 +111,24 @@ self.addEventListener('fetch', (event) => {
101111
// Only cache valid 200 responses
102112
if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
103113
const responseToCache = networkResponse.clone();
104-
caches.open(CACHE_NAME).then((cache) => {
114+
caches.open(RUNTIME_CACHE).then((cache) => {
105115
cache.put(event.request, responseToCache);
106-
limitCacheSize(CACHE_NAME, MAX_CACHE_ITEMS);
116+
limitCacheSize(RUNTIME_CACHE, MAX_RUNTIME_ITEMS);
117+
});
118+
} else if (isHashedAsset && networkResponse && networkResponse.status === 404) {
119+
// A hashed asset that 404s means the page HTML is from another
120+
// deploy. Surface a network error (never an HTML body) so the
121+
// browser reports a clean failure, and refresh the cached shell
122+
// so the next navigation picks up the current HTML.
123+
caches.open(CORE_CACHE).then((cache) => {
124+
fetch('./index.html', { cache: 'no-cache' }).then((fresh) => {
125+
if (fresh && fresh.status === 200) {
126+
cache.put('./index.html', fresh.clone());
127+
cache.put('./', fresh);
128+
}
129+
});
107130
});
131+
return Response.error();
108132
}
109133
return networkResponse;
110134
})

‎public/zh-CN/index.html‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ <h1 class="reveal d2">在浏览器里直接打开 Word、Excel 和 PPT,<span cla
164164
>
165165
</a>
166166
</div>
167+
<!-- 每种格式在首页都保留新建入口(FAB 菜单要等文档打开后才出现)——走 ?new= 深链。 -->
168+
<p class="new-row reveal d5">
169+
新建空白:<a href="/?locale=zh-CN&amp;new=docx">Word</a><span class="sep">·</span
170+
><a href="/?locale=zh-CN&amp;new=xlsx">Excel</a><span class="sep">·</span
171+
><a href="/?locale=zh-CN&amp;new=pptx">PowerPoint</a>
172+
</p>
167173
<div class="trust reveal d5">
168174
<span>不传服务器</span><span class="sep">·</span><span>免注册</span><span class="sep">·</span
169175
><span>可离线</span><span class="sep">·</span><span>.docx .xlsx .pptx .csv</span>

0 commit comments

Comments
 (0)