stepper_cache 是一个Flarum的缓存插件。我们将分几个部分来实现这个插件:
1. 创建插件的基本结构:
stepper_cache/
├── composer.json
├── extend.php
├── js/
│ ├── dist/
│ │ └── forum.js
│ ├── src/
│ │ ├── forum/
│ │ │ ├── index.js
│ │ │ └── serviceWorker.js
│ └── package.json
└── src/
└── Listeners/
└── AddAssets.php
2. 首先是 composer.json:
{
"name": "stepper/cache",
"description": "Browser-side caching plugin for Flarum using Service Worker",
"type": "flarum-extension",
"require": {
"flarum/core": "^1.2.0"
},
"autoload": {
"psr-4": {
"Stepper\\Cache\\": "src/"
}
},
"extra": {
"flarum-extension": {
"title": "Stepper Cache",
"icon": {
"name": "fas fa-bolt",
"backgroundColor": "#238c59",
"color": "#fff"
}
}
}
}
3. extend.php 文件:
<?php
use Flarum\Extend;
use Stepper\Cache\Listeners\AddAssets;
return [
(new Extend\Frontend('forum'))
->js(__DIR__.'/js/dist/forum.js')
->content(AddAssets::class),
];
4. src/Listeners/AddAssets.php:
<?php
namespace Stepper\Cache\Listeners;
use Flarum\Frontend\Document;
use Illuminate\Support\Str;
class AddAssets
{
public function __invoke(Document [imath:0]document)
{
// 添加Service Worker注册脚本
$document->head[] = '<script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", function() {
navigator.serviceWorker.register("/sw.js")
.then(function(registration) {
console.log("ServiceWorker registered");
})
.catch(function(err) {
console.log("ServiceWorker registration failed: ", err);
});
});
}
</script>';
}
}
5. js/src/forum/serviceWorker.js:
const CACHE_VERSION = 'v1';
const STATIC_CACHE = `stepper-static-[/imath:0]{CACHE_VERSION}`;
const RESPONSE_CACHE = `stepper-response-${CACHE_VERSION}`;
const CACHE_DURATION = 28 * 60 * 60 * 1000; // 28 hours in milliseconds
// 需要缓存的静态资源类型
const STATIC_EXTENSIONS = [
'.css', '.js', '.woff2', '.woff', '.ttf', '.eot', '.png', '.jpg',
'.jpeg', '.gif', '.svg', '.ico'
];
// 判断是否为静态资源
function isStaticResource(url) {
return STATIC_EXTENSIONS.some(ext => url.toLowerCase().endsWith(ext));
}
// 判断是否为写操作请求
function isWriteRequest(request) {
return ['POST', 'PUT', 'DELETE', 'PATCH'].includes(request.method);
}
// 判断缓存是否过期
function isCacheExpired(cachedResponse) {
const dateHeader = cachedResponse.headers.get('date');
if (!dateHeader) return true;
const cachedDate = new Date(dateHeader).getTime();
return Date.now() - cachedDate > CACHE_DURATION;
}
// 异步刷新缓存
async function refreshCache(request, cacheName) {
try {
const response = await fetch(request.clone());
const cache = await caches.open(cacheName);
await cache.put(request, response);
} catch (error) {
console.error('Cache refresh failed:', error);
}
}
// 获取页面中的所有静态资源URL
async function extractStaticUrls(response) {
const text = await response.clone().text();
const staticUrls = new Set();
// 提取<link>标签中的资源
const linkMatches = text.match(/<link[^>]*href=["']([^"']+)["'][^>]*>/g) || [];
linkMatches.forEach(match => {
const url = match.match(/href=["']([^"']+)["']/)?.[1];
if (url && isStaticResource(url)) {
staticUrls.add(new URL(url, location.origin).href);
}
});
// 提取<script>标签中的资源
const scriptMatches = text.match(/<script[^>]*src=["']([^"']+)["'][^>]*>/g) || [];
scriptMatches.forEach(match => {
const url = match.match(/src=["']([^"']+)["']/)?.[1];
if (url && isStaticResource(url)) {
staticUrls.add(new URL(url, location.origin).href);
}
});
// 提取<img>标签中的资源
const imgMatches = text.match(/<img[^>]*src=["']([^"']+)["'][^>]*>/g) || [];
imgMatches.forEach(match => {
const url = match.match(/src=["']([^"']+)["']/)?.[1];
if (url && isStaticResource(url)) {
staticUrls.add(new URL(url, location.origin).href);
}
});
return Array.from(staticUrls);
}
// Service Worker安装事件
self.addEventListener('install', event => {
event.waitUntil(
Promise.all([
caches.open(STATIC_CACHE),
caches.open(RESPONSE_CACHE)
])
);
});
// Service Worker激活事件
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(cacheName =>
cacheName.startsWith('stepper-') &&
!cacheName.endsWith(CACHE_VERSION)
)
.map(cacheName => caches.delete(cacheName))
);
})
);
});
// 拦截请求
self.addEventListener('fetch', event => {
const request = event.request;
// 忽略写操作请求
if (isWriteRequest(request)) {
return;
}
event.respondWith(
(async function() {
const url = new URL(request.url);
const cacheName = isStaticResource(url.pathname) ?
STATIC_CACHE : RESPONSE_CACHE;
// 尝试从缓存中获取响应
const cachedResponse = await caches.match(request);
if (cachedResponse) {
// 检查缓存是否过期
if (isCacheExpired(cachedResponse)) {
// 异步刷新缓存
refreshCache(request, cacheName);
}
return cachedResponse;
}
// 如果缓存中没有,发起网络请求
try {
const response = await fetch(request);
if (!response || response.status !== 200) {
return response;
}
// 缓存响应
const clonedResponse = response.clone();
const cache = await caches.open(cacheName);
cache.put(request, clonedResponse);
// 如果是HTML响应,提取并缓存静态资源
if (response.headers.get('content-type')?.includes('text/html')) {
const staticUrls = await extractStaticUrls(response);
const staticCache = await caches.open(STATIC_CACHE);
staticUrls.forEach(async url => {
try {
const staticResponse = await fetch(url);
if (staticResponse.ok) {
staticCache.put(url, staticResponse);
}
} catch (error) {
console.error('Failed to cache static resource:', url);
}
});
}
return response;
} catch (error) {
// 如果网络请求失败,返回缓存的响应(如果有的话)
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
})()
);
});
6. js/src/forum/index.js:
app.initializers.add('stepper-cache', () => {
console.log('Stepper Cache initialized');
});
使用说明:
将插件安装到Flarum后,它会自动注册Service Worker并开始缓存静态资源和响应。
插件的主要功能:
- 自动缓存所有静态资源(CSS、JS、字体、图片等)
- 缓存GET请求的响应
- 缓存时间为28小时
- 当缓存命中时,异步刷新缓存内容
- 自动分析HTML页面并缓存其中引用的所有静态资源
- 不缓存POST、PUT、DELETE等写操作请求
- 支持离线访问已缓存的内容
缓存策略:
- 优先使用缓存内容
- 如果缓存过期,返回缓存内容的同时异步刷新
- 如果缓存不存在,从网络获取并缓存
- 网络请求失败时回退到缓存(如果有)
安装步骤:
composer require stepper/cache
注意事项:
- Service Worker只能在HTTPS环境下运行(localhost除外)
- 需要确保服务器正确设置了CORS和缓存相关的响应头
- 建议在生产环境中使用
这个插件会显著提升Flarum论坛的访问速度和用户体验,特别是对重复访问的用户来说。