深秋的周末下午,我坐在驶向山林边缘的绿皮火车上。车窗半开着,窗外是一掠而过的枯黄稻田与连绵的秋山。车厢穿行在一个个黑漆漆的穿山隧道之间,手机屏幕顶部的信号格经常猝不及防地跳成空心或者“无服务”。
在这样脱离公网束缚的旅途里,翻开随身的笔记本,就着车轮撞击铁轨的富有节奏的咔哒声写写随笔,本是一件极其浪漫的事。
然而在过去,普通的网页手账往往会在这种时刻暴露出它最致命的软肋:
不小心触碰了触控板的双指刷新,或者随手按下了一记Cmd + R,原本温润的手账页面在瞬间崩塌,取而代之的是浏览器冰冷灰暗的经典报错:“未连接到互联网(ERR_INTERNET_DISCONNECTED)”,中间还站着一只滑稽的像素小恐龙。
那一瞬间,原本安顿好的文字心流被粗暴砸碎。你被迫意识到:虽然页面上写的是你私人的生活心绪,但它的生存命脉,却始终悬在远方脆弱的网络电缆之上。
一个真正贯彻“本地优先”与“生活流”哲学的数字手账,绝不能把自己的生命力寄托在永远不确定的网络连接上。
借助现代浏览器底层的Service Worker(服务工作线程)与 Cache Storage API,我们能够用不足六十行纯净的原生 JavaScript 代码,在浏览器与网络之间架设起一座忠实自主的本地代理站,让整个手账单页实现真正意义上的**“完全断网零请求运行,离线即是永恒”**。
撕开 Workbox 的重型包装:为什么我们只需要 60 行纯原生代码?
很多前端开发者一提到 Service Worker 和渐进式网页应用(PWA),脑海里立刻浮现出 Google 的 Workbox 等大型工具库。
那些工具链为了兼容成百上千人协作的复杂商业中台,引入了繁杂的 Webpack/Vite 打包插件、深层的正则路由策略以及几十 KB 的运行时胶水代码。但在我们这个结构纯粹、旨在长期留存的静态手账单页面前,这种做法完全是本末倒置。
Service Worker 本质上只是一个独立于网页主线程运行的浏览器后台事件拦截器。
对于一个由纯 HTML、CSS、原生 JS 和 SVG 噪点构成的轻快单页,我们只需要让它恪守两项极其清晰的使命:
- 初次邂逅时,将核心骨架打包存入本地专属救生舱(Pre-caching);
- 每次资源请求时,优先从本地救生舱毫秒直出;若有网络,再在后台悄悄查验更新(Cache-First, Stale-While-Revalidate)。
没有深奥的黑魔法,不需要任何 npm 依赖,一段纯粹的原生脚本便足以抵御所有的网络风浪。
核心实现:编写专属手账的离线守护者
在手账主站根目录下,我们放置一个独立的sw.js文件:
// sw.js - 极简手账离线守护者 const CACHE_NAME = 'gentle-handnote-v1.2'; // 核心生活救生包资源清单 const STATIC_ASSETS = [ '/', '/index.html', '/css/watercolor.css', '/js/editor.js', '/js/timeline-observer.js', '/search_index.json', '/favicon.ico' ]; // 1. 安装阶段:一次性将纯净骨架写入 Cache Storage self.addEventListener('install', (e) => { e.waitUntil( caches.open(CACHE_NAME).then((cache) => { console.log('🍃 核心生活手账救生包已稳妥存入本地缓存舱。'); return cache.addAll(STATIC_ASSETS); }).then(() => self.skipWaiting()) // 强制立即激活 ); }); // 2. 激活阶段:优雅清扫陈旧历史版本的缓存桶,不留一丝数字垃圾 self.addEventListener('activate', (e) => { e.waitUntil( caches.keys().then((keys) => { return Promise.all( keys.map((key) => { if (key !== CACHE_NAME) { console.log(`🍂 告别过期的历史缓存桶: ${key}`); return caches.delete(key); } }) ); }).then(() => self.clients.claim()) ); }); // 3. 拦截请求:缓存优先,后台静默异步刷新(Stale-While-Revalidate) self.addEventListener('fetch', (e) => { // 仅处理 GET 资源请求 if (e.request.method !== 'GET') return; e.respondWith( caches.match(e.request).then((cachedResponse) => { // 若本地救生舱已收纳此文件,瞬间零延迟吐出 if (cachedResponse) { // 在后台悄悄发起网络请求,探查是否有新版本 fetch(e.request).then((networkResponse) => { if (networkResponse && networkResponse.status === 200) { caches.open(CACHE_NAME).then((cache) => { cache.put(e.request, networkResponse.clone()); }); } }).catch(() => { // 离线无网时静默掠过,绝不向控制台抛出刺眼红色警报 }); return cachedResponse; } // 本地无缓存,走常规网络请求并顺道归仓 return fetch(e.request).then((networkResponse) => { if (!networkResponse || networkResponse.status !== 200) { return networkResponse; } const responseClone = networkResponse.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(e.request, responseClone); }); return networkResponse; }); }) ); });接着,在主页面index.html的底部,加入两行极其轻柔的注册逻辑:
<script> if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(() => console.log('🌿 离线守护者已就位,山川隧道亦无所畏惧。')) .catch((err) => console.log('SW 注册轻微阻滞: ', err)); }); } </script>感官设计:一盏安详的水彩状态灯
为了让作者对当前的网络环境了然于胸,我们在手账界面的右上角安放了一盏极小的水彩微光指示灯。借助原生navigator.onLine与事件监听,不需要复杂的逻辑,就能赋予界面通人性的感知:
(() => { const statusLight = document.getElementById('network-light'); function updateNetworkMood() { if (navigator.onLine) { statusLight.className = 'light-online'; statusLight.title = '网络通畅 · 与远方温和相连'; } else { statusLight.className = 'light-offline'; statusLight.title = '处于纯净离线孤岛 · 尽享专注时刻'; } } window.addEventListener('online', updateNetworkMood); window.addEventListener('offline', updateNetworkMood); updateNetworkMood(); })();配合 CSS 的温润调色:
#network-light { width: 8px; height: 8px; border-radius: 50%; display: inline-block; transition: all 0.3s ease; } /* 在线时:若竹青微光 */ .light-online { background-color: #6b8270; box-shadow: 0 0 8px rgba(107, 130, 112, 0.4); } /* 离线时:秋菊黄沉静微光 */ .light-offline { background-color: #d4a373; box-shadow: 0 0 8px rgba(212, 163, 115, 0.4); }真正的自由与笃定
部署好这套离线守护后,火车钻进了深山最长的一个隧道。车窗外是一片漆黑,车厢顶部昏黄的顶灯轻轻晃动,手机的信号彻底熄灭成了一片虚无。
我特意按下快捷键,用力地刷新了十几次页面。
没有小恐龙,没有报错码,没有一丝一毫的迟疑。
手撕纸质感的卡片依然完好无损地悬浮在屏幕中央,水彩呼吸光影在二十秒的周期里平缓流转,甚至连站内搜索框都能秒级索引出昨天的随笔。右上角那盏小巧的秋菊黄色指示灯,像一盏在山谷雨夜里为旅人守候的马灯,安静而温柔地亮着。
在现代生活的狂澜里,我们总是被迫与形形色色的网络牢牢绑定。而在这不到六十行的纯净代码守护下,这间属于个人的数字书房,终于切断了被外部世界裹挟的引线,蜕变成了一方真正属于自己的绝对净土。
任凭车窗外天崩地裂、万籁俱寂,只要合上电脑前的灵感还在,这方寸屏幕里的文字与水色,便拥有了穿越一切动荡的永恒生命。