无需网络也能用!SystemJS离线功能全攻略:Service Worker缓存实战
【免费下载链接】systemjs Dynamic ES module loader 项目地址: https://gitcode.com/gh_mirrors/sy/systemjs
你还在为SystemJS应用离线时模块加载失败而烦恼吗?当用户网络不稳定或完全断网时,动态加载的ES模块常常成为应用崩溃的"重灾区"。本文将通过Service Worker(服务工作线程)与SystemJS的深度整合,手把手教你实现三级缓存策略,让应用在无网络环境下依然流畅运行。读完本文你将掌握:Service Worker注册与生命周期管理、SystemJS加载钩子拦截技术、三级缓存策略设计(内存/HTTP/IndexedDB),以及完整的离线功能测试流程。
Service Worker与SystemJS架构基础
Service Worker作为浏览器后台运行的脚本,能拦截网络请求并实现资源缓存,是PWA(渐进式Web应用)离线功能的核心。与SystemJS的动态模块加载机制结合时,需要特别注意两者的生命周期协同。
技术架构图
SystemJS加载流程关键节点
SystemJS的加载机制通过多个可扩展钩子函数实现,其中与离线功能相关的核心钩子包括:
- resolve钩子:处理模块ID解析,对应src/features/resolve.js
- fetch钩子:控制资源获取方式,对应src/features/fetch-load.js
- shouldFetch钩子:决定是否使用fetch API加载模块,默认实现返回false(使用script标签加载)
通过重写这些钩子,我们可以实现自定义缓存逻辑。例如修改shouldFetch使所有JS模块通过fetch加载:
// 强制所有模块使用fetch加载以便缓存
System.constructor.prototype.shouldFetch = function(url) {
// 排除Worker脚本等特殊资源
return !url.includes('worker.js');
};
实现步骤:从注册到缓存
1. Service Worker注册
首先在应用入口文件注册Service Worker,建议使用examples/目录下的注册模板:
// src/main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW注册成功:', registration.scope);
})
.catch(err => {
console.log('SW注册失败:', err);
});
});
}
Service Worker文件sw.js需放在项目根目录,其作用域默认包含所有子目录。
2. SystemJS加载钩子改造
通过重写SystemJS的fetch钩子实现缓存逻辑,关键代码位于src/features/fetch-load.js第94行:
// 重写fetch钩子实现缓存优先策略
const originalFetch = System.constructor.prototype.fetch;
System.constructor.prototype.fetch = async function(url, options) {
// 1. 检查内存缓存
if (self.__systemjs_memory_cache[url]) {
return new Response(self.__systemjs_memory_cache[url]);
}
try {
// 2. 尝试网络请求
const response = await originalFetch.call(this, url, options);
// 3. 缓存成功响应
if (response.ok) {
const clone = response.clone();
caches.open('systemjs-v1').then(cache => {
cache.put(url, clone);
});
}
return response;
} catch (e) {
// 4. 网络失败时返回缓存
return caches.match(url) || new Response(JSON.stringify({error: 'offline'}), {
headers: {'Content-Type': 'application/json'}
});
}
};
3. 三级缓存策略设计
| 缓存级别 | 存储位置 | 优势 | 适用场景 |
|---|---|---|---|
| 一级缓存 | 内存对象 | 速度最快,同步访问 | 频繁访问的核心模块 |
| 二级缓存 | CacheStorage | 持久化,请求拦截 | 所有静态资源 |
| 三级缓存 | IndexedDB | 大容量存储 | 大型WASM文件、JSON数据 |
具体实现可参考src/extras/module-types.js中对JSON和WASM模块的处理方式,该文件通过fetch钩子实现了非JS模块的加载逻辑。
代码示例:核心功能实现
Service Worker缓存逻辑
创建sw.js文件实现请求拦截和缓存管理:
// sw.js
const CACHE_NAME = 'systemjs-offline-v1';
const ASSETS_TO_CACHE = [
'/',
'/src/system.js',
'/docs/import-maps.md' // 缓存文档以便离线查阅
];
// 安装阶段缓存核心资源
self.addEventListener('install', event => {
self.skipWaiting(); // 强制激活新SW
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(ASSETS_TO_CACHE))
);
});
// 激活阶段清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(names => Promise.all(
names.filter(name => name !== CACHE_NAME).map(name => caches.delete(name))
)).then(() => self.clients.claim())
);
});
// 拦截网络请求
self.addEventListener('fetch', event => {
// 只缓存GET请求和SystemJS相关资源
if (event.request.method !== 'GET' || !event.request.url.includes('src/')) {
return fetch(event.request);
}
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
// 返回缓存同时更新缓存
const fetchPromise = fetch(event.request).then(networkResponse => {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
});
return cachedResponse || fetchPromise;
})
);
});
SystemJS配置优化
结合import maps功能预先定义模块路径,减少离线时的解析错误:
<!-- 定义模块映射关系 -->
<script type="systemjs-importmap">
{
"imports": {
"lodash": "/src/extras/lodash.js",
"utils": "/src/common.js"
},
"depcache": {
"/src/common.js": ["./err-msg.js"] // 预定义依赖关系加速加载
}
}
</script>
测试与验证
离线功能测试流程
- 部署测试服务器:使用
npx serve启动本地服务器 - 首次加载:访问应用并确认Service Worker注册成功
- 模拟离线:在Chrome DevTools的Network面板勾选"Offline"
- 验证功能:刷新页面确认所有模块加载正常
- 缓存更新:修改模块内容后验证缓存失效机制
常见问题排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| SW注册失败 | 不在HTTPS环境 | 使用localhost或配置HTTPS证书 |
| 模块缓存不更新 | 缓存策略过于激进 | 实现版本化缓存或Cache-Control管理 |
| SystemJS钩子冲突 | 第三方库修改了相同钩子 | 使用钩子链确保所有逻辑执行:const original = System.hook; System.hook = function() { ... original.call(this) ... } |
高级优化与最佳实践
动态缓存策略调整
根据模块类型实现差异化缓存策略,参考docs/module-types.md中对不同模块类型的处理建议:
// 根据文件类型调整缓存策略
System.constructor.prototype.fetch = async function(url) {
const response = await fetch(url);
// JSON模块使用IndexedDB长期缓存
if (url.endsWith('.json')) {
const data = await response.clone().json();
await idbKeyval.set(url, data);
}
// WASM模块只缓存到CacheStorage
else if (url.endsWith('.wasm')) {
caches.open('systemjs-wasm').then(cache => cache.put(url, response.clone()));
}
return response;
};
监控与统计实现
通过SystemJS的onload钩子实现模块加载监控,代码位于src/features/registry.js:
// 监控模块加载状态用于缓存统计
System.constructor.prototype.onload = function(err, id) {
if (err) {
console.error(`模块${id}加载失败:`, err);
// 实现失败重试逻辑
} else {
// 记录成功加载的模块用于预缓存优化
navigator.serviceWorker.controller.postMessage({
type: 'CACHE_MODULE',
url: id
});
}
};
总结与展望
通过Service Worker与SystemJS加载钩子的深度整合,我们构建了一套 robust 的离线模块加载系统。核心要点包括:
- 钩子拦截:通过shouldFetch和fetch钩子控制资源加载流程
- 分级缓存:结合内存缓存、CacheStorage和IndexedDB实现全面覆盖
- 版本管理:使用带版本号的缓存名称实现平滑更新
- 监控反馈:通过onload钩子实现加载状态跟踪
随着Web标准的发展,未来可探索更先进的离线方案:
- 结合Import Maps的动态更新特性实现缓存映射动态调整
- 使用Background Sync API实现网络恢复后的数据同步
- 集成Streams API实现大型模块的流式缓存
立即动手改造你的SystemJS应用,为用户提供无缝的离线体验吧!关注本系列下一篇《SystemJS性能优化:模块预加载与依赖树分析》。
官方文档:docs/api.md | 完整示例:examples/offline/ | 问题反馈:项目Issues
【免费下载链接】systemjs Dynamic ES module loader 项目地址: https://gitcode.com/gh_mirrors/sy/systemjs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



