@norejs/prefetch-worker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # @norejs/prefetch-worker
2
+
3
+ ## 1.0.0
4
+
5
+ ### Initial Release
6
+
7
+ Service worker implementation for background prefetching.
8
+
9
+ ### Features
10
+
11
+ - 🔧 Service worker setup and installation utilities
12
+ - 📡 Background request prefetching with caching
13
+ - 🎯 Rule-based request matching and handling
14
+ - 🔐 Crypto-based request key generation
15
+ - 📝 Comprehensive logging for debugging
16
+
17
+ ### Build
18
+
19
+ - Built with Rsbuild for optimized service worker bundle
20
+ - Express server utilities for development and testing
package/README.md ADDED
@@ -0,0 +1,226 @@
1
+ # Prefetch Worker
2
+
3
+ 一个支持消息初始化的 Service Worker,用于实现智能的 API 请求缓存和预请求功能。
4
+
5
+ ## 特性
6
+
7
+ - 🔄 **请求去重**: 自动合并相同的并发请求
8
+ - 📦 **智能缓存**: 支持预请求和普通请求的统一缓存机制
9
+ - ⚡ **性能优化**: Promise 级别的请求复用
10
+ - 🎛️ **灵活配置**: 支持消息初始化和默认配置
11
+ - 🔧 **动态劫持**: fetch 事件监听器在脚本初始化时注册,通过函数变量实现动态处理
12
+ - 🐛 **调试友好**: 详细的日志输出
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ npm install @norejs/prefetch-worker
18
+ ```
19
+
20
+ ## 复制 Service Worker 文件
21
+
22
+ ```bash
23
+ # 复制到 public 目录
24
+ prefetch-worker install --dir public
25
+
26
+ # 或者复制到自定义目录
27
+ prefetch-worker install --dir assets
28
+ ```
29
+
30
+ ## 使用方法
31
+
32
+ ### 1. 基本用法(使用 @norejs/prefetch)
33
+
34
+ ```javascript
35
+ import { setup } from '@norejs/prefetch'
36
+
37
+ // 初始化 Service Worker
38
+ await setup({
39
+ serviceWorkerUrl: '/service-worker.js',
40
+ scope: '/',
41
+ apiMatcher: '\/api\/*', // API 匹配规则,默认 '/api'
42
+ defaultExpireTime: 30000, // 默认过期时间 30 秒
43
+ maxCacheSize: 100, // 最大缓存数量
44
+ debug: true // 开启调试模式
45
+ })
46
+ ```
47
+
48
+ ### 2. 手动初始化(发送消息)
49
+
50
+ ```javascript
51
+ // 注册 Service Worker
52
+ const registration = await navigator.serviceWorker.register('/service-worker.js')
53
+
54
+ // 等待激活
55
+ await new Promise((resolve) => {
56
+ if (navigator.serviceWorker.controller) {
57
+ resolve()
58
+ } else {
59
+ navigator.serviceWorker.addEventListener('controllerchange', resolve)
60
+ }
61
+ })
62
+
63
+ // 发送初始化消息
64
+ navigator.serviceWorker.controller.postMessage({
65
+ type: 'PREFETCH_INIT',
66
+ config: {
67
+ apiMatcher: '/api/v1', // 自定义 API 匹配规则
68
+ defaultExpireTime: 60000, // 60 秒过期时间
69
+ maxCacheSize: 200, // 最大缓存 200 个请求
70
+ debug: false // 关闭调试模式
71
+ }
72
+ })
73
+ ```
74
+
75
+ ### 3. 配置参数
76
+
77
+ | 参数 | 类型 | 默认值 | 说明 |
78
+ |------|------|--------|------|
79
+ | `apiMatcher` | `string \| RegExp` | `'/api'` | API 请求匹配规则 |
80
+ | `defaultExpireTime` | `number` | `0` | 默认缓存过期时间(毫秒) |
81
+ | `maxCacheSize` | `number` | `100` | 最大缓存数量 |
82
+ | `debug` | `boolean` | `false` | 是否开启调试模式 |
83
+
84
+ ## HTTP 方法支持
85
+
86
+ ### 支持缓存的方法 ✅
87
+ - **GET**: 查询操作,适合缓存
88
+ - **POST**: 提交操作,支持请求去重
89
+ - **PATCH**: 更新操作,支持缓存
90
+
91
+ ### 不支持缓存的方法 ❌
92
+ - **DELETE**: 删除操作,每次都真实执行
93
+
94
+ ## 请求复用机制
95
+
96
+ Service Worker 会自动处理并发的相同请求:
97
+
98
+ ```javascript
99
+ // 同时发起的相同请求会被合并
100
+ Promise.all([
101
+ fetch('/api/users'), // 发起真实请求
102
+ fetch('/api/users'), // 复用第一个请求的 Promise
103
+ fetch('/api/users') // 复用第一个请求的 Promise
104
+ ])
105
+ ```
106
+
107
+ ## 预请求功能
108
+
109
+ 配合 `@norejs/prefetch` 使用预请求功能:
110
+
111
+ ```javascript
112
+ import { preFetch } from '@norejs/prefetch'
113
+
114
+ // 直接预请求数据
115
+ await preFetch('/api/products', {
116
+ expireTime: 30000 // 30 秒过期时间
117
+ })
118
+
119
+ // 实际请求时会从缓存返回
120
+ const response = await fetch('/api/products')
121
+ ```
122
+
123
+ ## 调试
124
+
125
+ 开启调试模式后,可以在浏览器控制台看到详细的日志:
126
+
127
+ ```
128
+ prefetch-worker: received message {type: "PREFETCH_INIT", config: {...}}
129
+ prefetch-worker: initializing with config {apiMatcher: "/api", ...}
130
+ prefetch-worker: initialization completed
131
+ prefetch: cache hit (response) /api/products
132
+ prefetch: cache hit (promise) /api/users
133
+ ```
134
+
135
+ ## 自动初始化
136
+
137
+ 如果没有收到初始化消息,Service Worker 会在 install 事件后 1 秒自动使用默认配置初始化:
138
+
139
+ ```javascript
140
+ // 默认配置
141
+ {
142
+ apiMatcher: '\/api\/*'
143
+ }
144
+ ```
145
+
146
+ ## 消息类型
147
+
148
+ ### 发送给 Service Worker
149
+
150
+ ```javascript
151
+ // 初始化消息
152
+ {
153
+ type: 'PREFETCH_INIT',
154
+ config: {
155
+ apiMatcher: '\/api\/*',
156
+ defaultExpireTime: 30000,
157
+ maxCacheSize: 100,
158
+ debug: true
159
+ }
160
+ }
161
+ ```
162
+
163
+ ### 从 Service Worker 接收
164
+
165
+ ```javascript
166
+ // 初始化成功
167
+ {
168
+ type: 'PREFETCH_INIT_SUCCESS',
169
+ config: { /* 实际使用的配置 */ }
170
+ }
171
+
172
+ // 初始化失败
173
+ {
174
+ type: 'PREFETCH_INIT_ERROR',
175
+ error: 'Error message'
176
+ }
177
+ ```
178
+
179
+ ## 技术实现
180
+
181
+ ### 动态劫持机制
182
+
183
+ Service Worker 采用动态劫持的方式来解决 `fetch` 事件监听器必须在脚本初始评估阶段注册的限制:
184
+
185
+ ```javascript
186
+ // 在脚本加载时就注册 fetch 事件监听器
187
+ self.addEventListener('fetch', function (event) {
188
+ // 如果没有初始化或没有处理函数,直接返回(不拦截)
189
+ if (!isInitialized || !handleFetchEventImpl) {
190
+ return;
191
+ }
192
+
193
+ // 调用动态处理函数
194
+ event.respondWith(handleFetchEventImpl(event));
195
+ });
196
+
197
+ // 初始化时设置处理函数
198
+ handleFetchEventImpl = setupWorker(config);
199
+ ```
200
+
201
+ ### 处理流程
202
+
203
+ 1. **脚本加载**: 注册 `fetch` 事件监听器,但不执行任何处理逻辑
204
+ 2. **收到初始化消息**: 调用 `setupWorker` 获取处理函数
205
+ 3. **设置处理函数**: 将返回的函数赋值给 `handleFetchEventImpl`
206
+ 4. **开始拦截**: 后续请求通过动态函数进行处理
207
+
208
+ 这种设计确保了:
209
+ - 符合 Service Worker 规范要求
210
+ - 支持动态配置和初始化
211
+ - 避免了"Event handler must be added on initial evaluation"错误
212
+
213
+ ## 注意事项
214
+
215
+ 1. **首次加载**: Service Worker 首次安装时可能需要刷新页面才能拦截请求
216
+ 2. **HTTPS**: Service Worker 只能在 HTTPS 或 localhost 下运行
217
+ 3. **作用域**: Service Worker 只能拦截其作用域内的请求
218
+ 4. **缓存策略**: DELETE 请求永远不会被缓存,确保数据一致性
219
+ 5. **动态劫持**: fetch 监听器在脚本评估时注册,但处理逻辑通过函数变量动态设置
220
+
221
+ ## 兼容性
222
+
223
+ - Chrome 40+
224
+ - Firefox 44+
225
+ - Safari 11.1+
226
+ - Edge 17+
package/bin/install.js ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // 获取命令行参数
7
+ const args = process.argv.slice(2);
8
+
9
+ // 检查第一个参数必须是 install
10
+ if (args.length === 0 || args[0] !== 'install') {
11
+ console.error('错误: 必须指定 install 子命令');
12
+ console.log('');
13
+ showHelp();
14
+ }
15
+
16
+ let targetDir = 'public'; // 默认目标目录
17
+
18
+ // 显示帮助信息
19
+ function showHelp() {
20
+ console.log(`
21
+ 使用方法:
22
+ prefetch-worker install [选项]
23
+
24
+ 选项:
25
+ --dir, -d <目录> 指定复制文件的目标目录 (默认: public)
26
+ --help, -h 显示帮助信息
27
+
28
+ 示例:
29
+ prefetch-worker install # 复制到 public 目录
30
+ prefetch-worker install --dir static # 复制到 static 目录
31
+ prefetch-worker install -d assets # 复制到 assets 目录
32
+ `);
33
+ process.exit(0);
34
+ }
35
+
36
+ // 解析命令行参数(从索引1开始,跳过 install 子命令)
37
+ for (let i = 1; i < args.length; i++) {
38
+ if (args[i] === '--dir' || args[i] === '-d') {
39
+ if (i + 1 < args.length) {
40
+ targetDir = args[i + 1];
41
+ i++; // 跳过下一个参数
42
+ }
43
+ } else if (args[i] === '--help' || args[i] === '-h') {
44
+ showHelp();
45
+ }
46
+ }
47
+
48
+ // 获取当前工作目录(执行命令的项目目录)
49
+ const cwd = process.cwd();
50
+ const targetPath = path.resolve(cwd, targetDir);
51
+
52
+ // 获取 prefetch-worker 包的路径
53
+ const packageDir = path.dirname(__dirname);
54
+ const distWorkerDir = path.join(packageDir, 'dist', 'worker');
55
+
56
+ // 要复制的文件列表
57
+ const filesToCopy = ['service-worker.js'];
58
+
59
+ function copyFile(src, dest) {
60
+ try {
61
+ // 确保目标目录存在
62
+ const destDir = path.dirname(dest);
63
+ if (!fs.existsSync(destDir)) {
64
+ fs.mkdirSync(destDir, { recursive: true });
65
+ console.log(`✓ 创建目录: ${destDir}`);
66
+ }
67
+
68
+ // 复制文件
69
+ fs.copyFileSync(src, dest);
70
+ console.log(`✓ 已复制: ${path.basename(src)} -> ${dest}`);
71
+ } catch (error) {
72
+ console.error(`✗ 复制失败: ${src} -> ${dest}`);
73
+ console.error(` 错误: ${error.message}`);
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ function main() {
79
+ console.log(`🚀 安装 prefetch-worker 文件到: ${targetPath}`);
80
+ console.log('');
81
+
82
+ // 检查源文件是否存在
83
+ if (!fs.existsSync(distWorkerDir)) {
84
+ console.error(`✗ 错误: 未找到构建文件目录: ${distWorkerDir}`);
85
+ console.error(' 请先运行 "npm run build" 或 "pnpm build" 构建项目');
86
+ process.exit(1);
87
+ }
88
+
89
+ let copiedCount = 0;
90
+ let totalFiles = filesToCopy.length;
91
+
92
+ // 复制每个文件
93
+ filesToCopy.forEach(fileName => {
94
+ const srcFile = path.join(distWorkerDir, fileName);
95
+
96
+ if (!fs.existsSync(srcFile)) {
97
+ console.warn(`⚠ 警告: 文件不存在,跳过: ${srcFile}`);
98
+ totalFiles--;
99
+ return;
100
+ }
101
+
102
+ const destFile = path.join(targetPath, fileName);
103
+ copyFile(srcFile, destFile);
104
+ copiedCount++;
105
+ });
106
+
107
+ console.log('');
108
+ console.log(`🎉 安装完成! 成功复制 ${copiedCount}/${totalFiles} 个文件到 ${targetPath}`);
109
+
110
+ if (copiedCount > 0) {
111
+ console.log('');
112
+ console.log('已安装的文件:');
113
+ filesToCopy.forEach(fileName => {
114
+ const srcFile = path.join(distWorkerDir, fileName);
115
+ if (fs.existsSync(srcFile)) {
116
+ console.log(` • ${fileName}`);
117
+ }
118
+ });
119
+ }
120
+ }
121
+
122
+ // 运行主函数
123
+ main();
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@norejs/prefetch-worker",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "prefetch-worker": "./bin/install.js"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "devDependencies": {
13
+ "@rsbuild/core": "^0.6.1",
14
+ "@types/crypto-js": "^4.2.2",
15
+ "@types/express": "4.17.21"
16
+ },
17
+ "dependencies": {
18
+ "crypto-js": "^4.2.0",
19
+ "express": "4"
20
+ },
21
+ "scripts": {
22
+ "dev": "rsbuild dev",
23
+ "start": "node dist/server/index.js",
24
+ "build": "rsbuild build",
25
+ "test": ""
26
+ }
27
+ }
@@ -0,0 +1,30 @@
1
+ export default {
2
+ source: {
3
+ entry({ target }) {
4
+ if (target === 'service-worker') {
5
+ return {
6
+ index: './src/index.ts',
7
+ 'service-worker': './src/index.ts',
8
+ };
9
+ }
10
+ if (target === 'node') {
11
+ return {
12
+ index: './src/index.server.ts',
13
+ };
14
+ }
15
+ },
16
+ },
17
+ dev: {
18
+ writeToDisk: true,
19
+ },
20
+ server: {
21
+ port: 9004,
22
+ publicDir: {
23
+ name: 'dist/worker/',
24
+ },
25
+ },
26
+ output: {
27
+ minify: false,
28
+ targets: ['service-worker', 'node'],
29
+ },
30
+ };
@@ -0,0 +1,12 @@
1
+ // 静态服务器,用于提供Service Worker文件
2
+ import express from 'express';
3
+
4
+ const app = express();
5
+ const port = 8080; // 你可以选择任意未被占用的端口
6
+
7
+ // 指定静态文件目录
8
+ app.use(express.static('dist/worker'));
9
+
10
+ app.listen(port, () => {
11
+ console.log(`Server is running at http://localhost:${port}`);
12
+ });
package/src/index.ts ADDED
@@ -0,0 +1,115 @@
1
+ import setupWorker from './setup';
2
+
3
+ declare var self: ServiceWorkerGlobalScope;
4
+
5
+ // 标记是否已经初始化
6
+ let isInitialized = false;
7
+
8
+ // 默认配置
9
+ const defaultConfig = {
10
+ apiMatcher: '\/api\/*'
11
+ };
12
+
13
+ // 动态处理函数变量
14
+ let handleFetchEventImpl: ((event: FetchEvent) => Promise<Response> | undefined) | null = null;
15
+
16
+ // 在初始化阶段就注册 fetch 事件监听器
17
+ self.addEventListener('fetch', function (event) {
18
+ // 如果没有初始化或没有处理函数,直接返回(不拦截)
19
+ if (!isInitialized || !handleFetchEventImpl) {
20
+ return;
21
+ }
22
+ const response = handleFetchEventImpl(event);
23
+ if (response) {
24
+ return event.respondWith(response);
25
+ }
26
+ return;
27
+ });
28
+
29
+ // 监听来自主线程的消息
30
+ self.addEventListener('message', (event) => {
31
+ console.log('prefetch-worker: received message', event.data);
32
+
33
+ if (event.data && event.data.type === 'PREFETCH_INIT') {
34
+ try {
35
+ if (isInitialized) {
36
+ console.log('prefetch-worker: already initialized, sending success response');
37
+
38
+ // 发送已初始化成功的消息回主线程
39
+ if (event.source) {
40
+ event.source.postMessage({
41
+ type: 'PREFETCH_INIT_SUCCESS',
42
+ config: { ...defaultConfig, ...event.data.config },
43
+ message: 'Already initialized'
44
+ });
45
+ }
46
+ return;
47
+ }
48
+
49
+ const config = { ...defaultConfig, ...event.data.config };
50
+ console.log('prefetch-worker: initializing with config', config);
51
+
52
+ // 将字符串转换为正则表达式
53
+ const apiMatcher = typeof config.apiMatcher === 'string'
54
+ ? new RegExp(config.apiMatcher)
55
+ : config.apiMatcher;
56
+
57
+ // 调用 setupWorker 并获取处理函数
58
+ const handleFetchEvent = setupWorker({
59
+ apiMatcher,
60
+ ...config
61
+ });
62
+ if (handleFetchEvent) {
63
+ handleFetchEventImpl = handleFetchEvent;
64
+ }
65
+
66
+ isInitialized = true;
67
+ console.log('prefetch-worker: initialization completed');
68
+
69
+ // 发送初始化完成的消息回主线程
70
+ if (event.source) {
71
+ event.source.postMessage({
72
+ type: 'PREFETCH_INIT_SUCCESS',
73
+ config: config
74
+ });
75
+ }
76
+ } catch (error) {
77
+ console.error('prefetch-worker: initialization failed', error);
78
+
79
+ // 发送初始化失败的消息回主线程
80
+ if (event.source) {
81
+ event.source.postMessage({
82
+ type: 'PREFETCH_INIT_ERROR',
83
+ error: error instanceof Error ? error.message : String(error)
84
+ });
85
+ }
86
+
87
+ self.registration.unregister();
88
+ }
89
+ }
90
+ });
91
+
92
+ // 如果没有收到初始化消息,使用默认配置自动初始化
93
+ self.addEventListener('install', () => {
94
+ console.log('prefetch-worker: install event');
95
+
96
+ // 延迟一下,给主线程发送初始化消息的机会
97
+ setTimeout(() => {
98
+ if (!isInitialized) {
99
+ console.log('prefetch-worker: auto-initializing with default config');
100
+ try {
101
+ const apiMatcher = new RegExp(defaultConfig.apiMatcher);
102
+ handleFetchEventImpl = setupWorker({
103
+ apiMatcher
104
+ });
105
+ isInitialized = true;
106
+ console.log('prefetch-worker: auto-initialization completed');
107
+ } catch (error) {
108
+ console.error('prefetch-worker: auto-initialization failed', error);
109
+ self.registration.unregister();
110
+ }
111
+ }
112
+ }, 1000); // 1秒延迟
113
+ });
114
+
115
+ console.log('prefetch-worker: loaded, waiting for initialization message');
package/src/setup.ts ADDED
@@ -0,0 +1,247 @@
1
+ import { createLogger } from 'utils/log';
2
+ import { default as defaultRequestToKey } from './utils/requestToKey';
3
+ declare var self: ServiceWorkerGlobalScope & {
4
+ _setuped: symbol;
5
+ debug: boolean;
6
+ };
7
+
8
+ export const HeadName = 'X-Prefetch-Request-Type';
9
+ export const HeadValue = 'prefetch';
10
+ export const ExpireTimeHeadName = 'X-Prefetch-Expire-Time';
11
+
12
+ export type ICacheItem = {
13
+ expire: number;
14
+ response?: Response;
15
+ requestPromise?: Promise<Response>;
16
+ };
17
+
18
+ export type ISetupWorker = {
19
+ // 通过Url匹配是否是需要缓存的请求,匹配中的请求才有可能被缓存,是否缓存取决于请求头
20
+ apiMatcher: RegExp;
21
+ // 缓存的最大数量
22
+ requestToKey?: (request: Request) => Promise<string>;
23
+ // 默认的失效时间,单位毫秒, 默认为0
24
+ defaultExpireTime?: number;
25
+ // 是否允许跨域, 默认为false
26
+ allowCrossOrigin?: boolean;
27
+ // 是否自动跳过等待,默认为true
28
+ // 最大缓存数量, 默认为 100
29
+ maxCacheSize?: number;
30
+ debug?: boolean;
31
+ };
32
+ self.addEventListener('install', (event) => {
33
+ console.log('prefetch: install');
34
+ self.skipWaiting();
35
+ });
36
+ self.addEventListener('activate', (event) => {
37
+ // 激活阶段:清除旧缓存,并立即控制客户端
38
+ console.log('prefetch: activate');
39
+ event.waitUntil(
40
+ self.clients.claim().then(() => {
41
+ console.log(
42
+ 'Service Worker activated and now controls the clients.'
43
+ );
44
+ })
45
+ );
46
+ });
47
+ // 用于标记是否已经初始化
48
+ const setupSymbol = Symbol('setuped');
49
+ export default function setupWorker(
50
+ props: ISetupWorker
51
+ ): ((event: FetchEvent) => Promise<Response> | undefined) | undefined {
52
+ console.log('prefetch setupWorker');
53
+ if (self._setuped === setupSymbol) {
54
+ // 如果已经设置过,返回现有的处理函数
55
+ return;
56
+ }
57
+
58
+ self._setuped = setupSymbol;
59
+ const preRequestCache: Map<string, ICacheItem> = new Map();
60
+ let cachedNums = 0;
61
+ const {
62
+ apiMatcher,
63
+ requestToKey = defaultRequestToKey,
64
+ defaultExpireTime = 0,
65
+ maxCacheSize = 100,
66
+ debug = false,
67
+ } = props;
68
+ if (debug) {
69
+ self.debug = debug;
70
+ }
71
+ const logger = createLogger(debug);
72
+ logger.info('prefetch: setupWorker', {
73
+ apiMatcher,
74
+ requestToKey,
75
+ defaultExpireTime,
76
+ maxCacheSize,
77
+ });
78
+
79
+ console.log('prefetch: setupWorker complete');
80
+
81
+ // 创建处理函数
82
+ const fetchHandler = (event: FetchEvent): Promise<Response> | undefined => {
83
+ try {
84
+ const request = event.request;
85
+ // Skip cross-origin requests, like those for Google Analytics.
86
+ if (request.mode === 'navigate') {
87
+ return;
88
+ }
89
+
90
+ // Opening the DevTools triggers the "only-if-cached" request
91
+ // that cannot be handled by the worker. Bypass such requests.
92
+ if (
93
+ request.cache === 'only-if-cached' &&
94
+ request.mode !== 'same-origin'
95
+ ) {
96
+ return;
97
+ }
98
+ const url = request?.url;
99
+ const method = request?.method?.toLowerCase?.();
100
+ const isApiMetod = ['get', 'post', 'patch'].includes(method);
101
+ const isApi = url?.match(apiMatcher) || isApiMetod;
102
+ if (!url || !isApi) {
103
+ return;
104
+ }
105
+ return handleFetchEvent(event);
106
+ } catch (error) {
107
+ logger.error('fetch error', error);
108
+ return;
109
+ }
110
+ };
111
+
112
+ async function handleFetchEvent(event: FetchEvent) {
113
+ try {
114
+ const request = event.request.clone();
115
+ const headers = request.headers;
116
+ const method = request.method?.toLowerCase?.();
117
+ const isPreRequest = headers.get(HeadName) === HeadValue;
118
+ const expireTime =
119
+ Number(headers.get(ExpireTimeHeadName)) || defaultExpireTime;
120
+
121
+ // DELETE 方法不进行缓存,直接透传
122
+ if (method === 'delete') {
123
+ logger.info(
124
+ 'prefetch: DELETE method, bypass cache',
125
+ request.url
126
+ );
127
+ return fetch(event.request);
128
+ }
129
+
130
+ const cacheKey = await requestToKey(request.clone());
131
+ logger.info('prefetch: cacheKey', request.url, cacheKey);
132
+ if (!cacheKey) {
133
+ return fetch(event.request);
134
+ }
135
+
136
+ const cache = preRequestCache.get(cacheKey);
137
+
138
+ // 检查是否有有效的缓存(不管是预请求还是普通请求)
139
+ if (cache && cache.expire > Date.now()) {
140
+ // 如果有完成的响应,直接返回
141
+ if (cache.response) {
142
+ logger.info('prefetch: cache hit (response)', request.url);
143
+ return cache.response.clone();
144
+ }
145
+
146
+ // 如果有正在进行的请求,等待并复用
147
+ if (cache.requestPromise) {
148
+ logger.info('prefetch: cache hit (promise)', request.url);
149
+ try {
150
+ const response = await cache.requestPromise;
151
+ return response.clone();
152
+ } catch (error) {
153
+ // 如果正在进行的请求失败,清除缓存并重新发起请求
154
+ preRequestCache.delete(cacheKey);
155
+ cachedNums--;
156
+ logger.error('prefetch: cached promise failed', error);
157
+ return fetch(event.request.clone());
158
+ }
159
+ }
160
+ } else if (cache && cache.expire <= Date.now()) {
161
+ // 缓存过期,清除
162
+ preRequestCache.delete(cacheKey);
163
+ cachedNums--;
164
+ }
165
+
166
+ // 创建新的请求
167
+ const fetchPromise = fetch(request.clone());
168
+
169
+ // 如果缓存中没有这个请求或请求已过期,创建新的缓存项
170
+ if (!cache || cache.expire <= Date.now()) {
171
+ const newExpireTime =
172
+ isPreRequest && expireTime ? expireTime : defaultExpireTime;
173
+ if (newExpireTime > 0) {
174
+ logger.info(
175
+ 'prefetch: creating new cache entry',
176
+ request.url
177
+ );
178
+ clearCacheWhenOversize();
179
+
180
+ // 创建带有 requestPromise 的缓存项,以便其他并发请求可以复用
181
+ preRequestCache.set(cacheKey, {
182
+ expire: Date.now() + newExpireTime,
183
+ requestPromise: fetchPromise
184
+ .then((response) => {
185
+ const returnResponse = response.clone();
186
+ // 请求成功后,更新缓存为 response
187
+ if (response.status === 200) {
188
+ const existingCache =
189
+ preRequestCache.get(cacheKey);
190
+ if (
191
+ existingCache &&
192
+ existingCache.expire > Date.now()
193
+ ) {
194
+ preRequestCache.set(cacheKey, {
195
+ expire: existingCache.expire,
196
+ response: response.clone(),
197
+ });
198
+ }
199
+ }
200
+ return returnResponse;
201
+ })
202
+ .catch((error) => {
203
+ // 请求失败,清除缓存
204
+ preRequestCache.delete(cacheKey);
205
+ cachedNums--;
206
+ throw error;
207
+ }),
208
+ });
209
+ cachedNums++;
210
+ }
211
+ }
212
+
213
+ // 等待请求完成并返回响应
214
+ try {
215
+ const response = await fetchPromise;
216
+ logger.info(
217
+ 'prefetch: response received',
218
+ response.status,
219
+ request.url
220
+ );
221
+ return response;
222
+ } catch (error) {
223
+ logger.error('prefetch: fetch failed', error);
224
+ throw error;
225
+ }
226
+ } catch (error) {
227
+ logger.error('prefetch: error', error);
228
+ return fetch(event.request);
229
+ }
230
+ }
231
+
232
+ function clearCacheWhenOversize() {
233
+ if (cachedNums <= maxCacheSize) {
234
+ return;
235
+ }
236
+ logger.info('clearCache');
237
+ preRequestCache.forEach((cache, key) => {
238
+ if (cache && cache.expire < Date.now()) {
239
+ preRequestCache.delete(key);
240
+ cachedNums--;
241
+ }
242
+ });
243
+ }
244
+
245
+ // 返回处理函数
246
+ return fetchHandler;
247
+ }
@@ -0,0 +1,6 @@
1
+ const noop = () => {};
2
+ export const createLogger = (debug = false) => ({
3
+ info: debug ? console.log : noop,
4
+ warn: debug ? console.warn : noop,
5
+ error: debug ? console.error : noop,
6
+ });
@@ -0,0 +1,13 @@
1
+ import sha256 from "crypto-js/sha256";
2
+ export default async function requestToKey(_request: Request) {
3
+ const request = _request.clone();
4
+ const url = request.url;
5
+ const method = request.method;
6
+ const body = await request.text();
7
+ // 组合信息
8
+ const combinedInfo = `${method.toUpperCase()} ${url} ${JSON.stringify(body)}`;
9
+ // 使用btoa函数进行编码生成唯一键
10
+ // 注意:在实际应用中,可能需要使用更安全的哈希函数如SHA-256
11
+ const key = sha256(combinedInfo).toString();
12
+ return key;
13
+ }
@@ -0,0 +1,120 @@
1
+ import { IRule } from '@norejs/prefetch';
2
+ // 是否在SW 中可运行
3
+ import MD5 from 'crypto-js/md5';
4
+
5
+ // 通过appUrl找到rule
6
+ const AppRuleMap: {
7
+ [key: string]: {
8
+ [key: string]: IRule;
9
+ };
10
+ } = {};
11
+
12
+ // 通过key找到rule
13
+ const KeyRuleMap: {
14
+ [key: string]: IRule;
15
+ } = {};
16
+
17
+ /**
18
+ * 添加规则
19
+ * @param appUrl
20
+ * @param rule
21
+ * @returns
22
+ */
23
+ export function addRule(appUrl: string, rule: IRule) {
24
+ if (!appUrl || !rule) {
25
+ return null;
26
+ }
27
+ if (!AppRuleMap[appUrl]) {
28
+ AppRuleMap[appUrl] = {};
29
+ }
30
+ // 利用rule和appUrl 计算唯一key
31
+ const key = MD5(`${appUrl}-${JSON.stringify(rule)}`).toString();
32
+ AppRuleMap[appUrl][key] = rule;
33
+ // @ts-ignore
34
+ rule.__key__ = key;
35
+ // @ts-ignore
36
+ rule.__appUrl__ = appUrl;
37
+ // 不允许修改
38
+ Object.defineProperties(rule, {
39
+ __appUrl__: {
40
+ writable: false,
41
+ },
42
+ __key__: {
43
+ writable: false,
44
+ },
45
+ });
46
+ KeyRuleMap[key] = rule;
47
+ return key;
48
+ }
49
+
50
+ export function removeRule(key: string) {
51
+ if (!key) {
52
+ return null;
53
+ }
54
+ const rule = KeyRuleMap[key];
55
+ const appUrl = rule.__appUrl__;
56
+ if (appUrl && AppRuleMap[appUrl]) {
57
+ delete AppRuleMap[appUrl][key];
58
+ }
59
+ if (KeyRuleMap[key]) {
60
+ delete KeyRuleMap[key];
61
+ }
62
+ return true;
63
+ }
64
+
65
+ // 获取appUrl下的所有规则
66
+ export function getAppRules(appUrl: string) {
67
+ return AppRuleMap[appUrl];
68
+ }
69
+
70
+ export function clearRules(appUrl: string) {
71
+ if (appUrl && AppRuleMap[appUrl]) {
72
+ Object.keys(AppRuleMap[appUrl]).forEach((key) => {
73
+ if (KeyRuleMap[key]) {
74
+ delete KeyRuleMap[key];
75
+ }
76
+ });
77
+ delete AppRuleMap[appUrl];
78
+ }
79
+ }
80
+ /**
81
+ * 清空所有规则
82
+ */
83
+ export function clearAllRules() {
84
+ Object.keys(AppRuleMap).forEach((appUrl) => {
85
+ clearRules(appUrl);
86
+ });
87
+ }
88
+
89
+ /**
90
+ * 匹配规则
91
+ * @param appUrl
92
+ * @param request
93
+ */
94
+ export function matchRule(appUrl: string, request: Request) {
95
+ const appRules = getAppRules(appUrl);
96
+ // TODO: 这里可以通过算法优化将复杂度降到O(1)
97
+ if (appRules) {
98
+ for (const key in appRules) {
99
+ const rule = appRules[key];
100
+ if (isRuleMatched(rule, request)) {
101
+ return rule;
102
+ }
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+
108
+ /**
109
+ * 通过请求匹配规则,从而决定如何缓存
110
+ * @param rule
111
+ * @param originRequest
112
+ * @returns
113
+ */
114
+ export function isRuleMatched(rule: IRule, originRequest: Request) {
115
+ // type 相同、url 相同
116
+ const request = originRequest.clone();
117
+ const url = request.url;
118
+ const method = request.method;
119
+ return rule.type === method && rule.apiUrl === url;
120
+ }
@@ -0,0 +1 @@
1
+ // 测试请求变成唯一key
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "baseUrl": "./src",
5
+ "declaration": true,
6
+ "emitDeclarationOnly": true,
7
+ "esModuleInterop": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "isolatedModules": true,
10
+ "lib": ["ESNext", "WebWorker"],
11
+ "moduleResolution": "Node",
12
+ "paths": {
13
+ "@/*": ["./src/*"]
14
+ },
15
+ "resolveJsonModule": true,
16
+ "rootDir": "src",
17
+ "skipLibCheck": true,
18
+ "strict": true
19
+ },
20
+ "exclude": ["**/node_modules"],
21
+ "include": ["src"]
22
+ }