generated from halo-dev/plugin-starter
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
添加最基本的页面静态缓存功能。 ```release-note None ```
- Loading branch information
Showing
19 changed files
with
3,734 additions
and
2,866 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,76 +1,24 @@ | ||
# plugin-starter | ||
# 页面静态缓存 | ||
|
||
Halo 2.0 插件开发快速开始模板。 | ||
为 Halo 2 提供页面的静态缓存功能,提高页面访问速度。 | ||
|
||
## 开发环境 | ||
## 使用 | ||
|
||
插件开发的详细文档请查阅:<https://docs.halo.run/developer-guide/plugin/introduction> | ||
1. 在内置的 Halo 应用市场搜索 **页面静态缓存** 或访问 <https://www.halo.run/store/apps/app-BaamQ> 手动下载并安装。 | ||
2. 启动插件之后会自动生效。 | ||
|
||
所需环境: | ||
## 注意事项 | ||
|
||
1. Java 17 | ||
2. Node 18 | ||
3. pnpm 8 | ||
4. Docker (可选) | ||
1. 当前不支持监听网站内容的变动,如果网站有内容更新并需要立即展示给访客,请点击 Console 右下角的按钮手动刷新缓存。 | ||
2. 页面缓存仅对未登录的访客有效,登录用户不会使用缓存。 | ||
3. 缓存失效策略:TODO | ||
|
||
克隆项目: | ||
## 预览 | ||
|
||
```bash | ||
git clone [email protected]:halo-sigs/plugin-starter.git | ||
启用插件前: | ||
|
||
# 或者当你 fork 之后 | ||
![Before](./images/plugin-page-cache-preview-before.jpg) | ||
|
||
git clone [email protected]:{your_github_id}/plugin-starter.git | ||
``` | ||
启用插件后: | ||
|
||
```bash | ||
cd path/to/plugin-starter | ||
``` | ||
|
||
### 运行方式 1(推荐) | ||
|
||
> 此方式需要本地安装 Docker | ||
```bash | ||
# macOS / Linux | ||
./gradlew pnpmInstall | ||
|
||
# Windows | ||
./gradlew.bat pnpmInstall | ||
``` | ||
|
||
```bash | ||
# macOS / Linux | ||
./gradlew haloServer | ||
|
||
# Windows | ||
./gradlew.bat haloServer | ||
``` | ||
|
||
执行此命令后,会自动创建一个 Halo 的 Docker 容器并加载当前的插件,更多文档可查阅:<https://docs.halo.run/developer-guide/plugin/basics/devtools> | ||
|
||
### 运行方式 2 | ||
|
||
> 此方式需要使用源码运行 Halo | ||
编译插件: | ||
|
||
```bash | ||
# macOS / Linux | ||
./gradlew build | ||
|
||
# Windows | ||
./gradlew.bat build | ||
``` | ||
|
||
修改 Halo 配置文件: | ||
|
||
```yaml | ||
halo: | ||
plugin: | ||
runtime-mode: development | ||
fixedPluginPath: | ||
- "/path/to/plugin-starter" | ||
``` | ||
最后重启 Halo 项目即可。 | ||
![After](./images/plugin-page-cache-preview-after.jpg) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package run.halo.cache.page; | ||
|
||
import static org.springframework.http.HttpStatus.NO_CONTENT; | ||
|
||
import org.springdoc.core.fn.builders.apiresponse.Builder; | ||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; | ||
import org.springframework.cache.CacheManager; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.reactive.function.server.RouterFunction; | ||
import org.springframework.web.reactive.function.server.ServerRequest; | ||
import org.springframework.web.reactive.function.server.ServerResponse; | ||
import reactor.core.publisher.Mono; | ||
import run.halo.app.core.extension.endpoint.CustomEndpoint; | ||
import run.halo.app.extension.GroupVersion; | ||
|
||
@Component | ||
public class CacheEndpoint implements CustomEndpoint { | ||
|
||
private final CacheManager cacheManager; | ||
|
||
public CacheEndpoint(CacheManager cacheManager) { | ||
this.cacheManager = cacheManager; | ||
} | ||
|
||
@Override | ||
public RouterFunction<ServerResponse> endpoint() { | ||
final var tag = "CacheV1alpha1Console"; | ||
return SpringdocRouteBuilder.route() | ||
.DELETE("/caches/page", this::evictCache, builder -> { | ||
builder | ||
.tag(tag) | ||
.operationId("EvictPageCache") | ||
.description("Evict Page cache.") | ||
.response(Builder.responseBuilder() | ||
.responseCode(String.valueOf(NO_CONTENT.value()))); | ||
}) | ||
.build(); | ||
} | ||
|
||
private Mono<ServerResponse> evictCache(ServerRequest request) { | ||
if (cacheManager.getCacheNames().contains("page")) { | ||
var cache = cacheManager.getCache("page"); | ||
if (cache != null) { | ||
cache.invalidate(); | ||
} | ||
} | ||
return ServerResponse.accepted().build(); | ||
} | ||
|
||
@Override | ||
public GroupVersion groupVersion() { | ||
return new GroupVersion("console.api.cache.halo.run", "v1alpha1"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package run.halo.cache.page; | ||
|
||
import java.nio.ByteBuffer; | ||
import java.time.Instant; | ||
import java.util.List; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.HttpStatusCode; | ||
|
||
/** | ||
* Cached response. Refer to | ||
* <a href="https://github.com/spring-cloud/spring-cloud-gateway/blob/f98aa6d47bf802019f07063f4fd7af6047f15116/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponse.java">here</a> } | ||
*/ | ||
public record CachedResponse(HttpStatusCode statusCode, | ||
HttpHeaders headers, | ||
List<ByteBuffer> body, | ||
Instant timestamp) { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 151 additions & 0 deletions
151
src/main/java/run/halo/cache/page/PageCacheWebFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package run.halo.cache.page; | ||
|
||
import static java.nio.ByteBuffer.allocateDirect; | ||
import static org.springframework.http.HttpHeaders.CACHE_CONTROL; | ||
|
||
import java.time.Instant; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.reactivestreams.Publisher; | ||
import org.springframework.cache.Cache; | ||
import org.springframework.cache.CacheManager; | ||
import org.springframework.core.io.buffer.DataBuffer; | ||
import org.springframework.core.io.buffer.DataBufferUtils; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.HttpMethod; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.http.server.reactive.ServerHttpRequest; | ||
import org.springframework.http.server.reactive.ServerHttpResponse; | ||
import org.springframework.http.server.reactive.ServerHttpResponseDecorator; | ||
import org.springframework.lang.NonNull; | ||
import org.springframework.security.web.server.context.ServerSecurityContextRepository; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.server.ServerWebExchange; | ||
import org.springframework.web.server.WebFilterChain; | ||
import reactor.core.publisher.Flux; | ||
import reactor.core.publisher.Mono; | ||
import run.halo.app.security.AdditionalWebFilter; | ||
import run.halo.app.theme.router.ModelConst; | ||
|
||
@Slf4j | ||
@Component | ||
public class PageCacheWebFilter implements AdditionalWebFilter { | ||
|
||
public static final String CACHE_NAME = "page"; | ||
|
||
private final Cache cache; | ||
|
||
private final ServerSecurityContextRepository serverSecurityContextRepository; | ||
|
||
public PageCacheWebFilter(CacheManager cacheManager, | ||
ServerSecurityContextRepository serverSecurityContextRepository) { | ||
this.cache = cacheManager.getCache(CACHE_NAME); | ||
this.serverSecurityContextRepository = serverSecurityContextRepository; | ||
} | ||
|
||
private static boolean hasRequestBody(ServerHttpRequest request) { | ||
return request.getHeaders().getContentLength() > 0; | ||
} | ||
|
||
@Override | ||
@NonNull | ||
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) { | ||
return serverSecurityContextRepository.load(exchange) | ||
.switchIfEmpty(Mono.defer(() -> { | ||
var cacheKey = generateCacheKey(exchange.getRequest()); | ||
var cachedResponse = cache.get(cacheKey, CachedResponse.class); | ||
if (cachedResponse != null) { | ||
// cache hit, then write the cached response | ||
return writeCachedResponse(exchange.getResponse(), cachedResponse).then( | ||
Mono.empty()); | ||
} | ||
// decorate the ServerHttpResponse to cache the response | ||
var decoratedExchange = exchange.mutate() | ||
.response(new CacheResponseDecorator(exchange, cacheKey)) | ||
.build(); | ||
return chain.filter(decoratedExchange).then(Mono.empty()); | ||
})) | ||
.flatMap(securityContext -> chain.filter(exchange).then(Mono.empty())); | ||
} | ||
|
||
private boolean requestCacheable(ServerHttpRequest request) { | ||
return HttpMethod.GET.equals(request.getMethod()) | ||
&& !hasRequestBody(request) | ||
&& enableCacheByCacheControl(request.getHeaders()); | ||
} | ||
|
||
private boolean enableCacheByCacheControl(HttpHeaders headers) { | ||
return headers.getOrEmpty(CACHE_CONTROL) | ||
.stream() | ||
.noneMatch(cacheControl -> | ||
"no-store".equals(cacheControl) || "private".equals(cacheControl)); | ||
} | ||
|
||
private boolean responseCacheable(ServerWebExchange exchange) { | ||
var response = exchange.getResponse(); | ||
if (!MediaType.TEXT_HTML.equals(response.getHeaders().getContentType())) { | ||
return false; | ||
} | ||
var statusCode = response.getStatusCode(); | ||
if (statusCode == null || !statusCode.isSameCodeAs(HttpStatus.OK)) { | ||
return false; | ||
} | ||
return exchange.getAttributeOrDefault(ModelConst.POWERED_BY_HALO_TEMPLATE_ENGINE, false); | ||
} | ||
|
||
private String generateCacheKey(ServerHttpRequest request) { | ||
return request.getURI().toASCIIString(); | ||
} | ||
|
||
private Mono<Void> writeCachedResponse(ServerHttpResponse response, | ||
CachedResponse cachedResponse) { | ||
response.setStatusCode(cachedResponse.statusCode()); | ||
response.getHeaders().clear(); | ||
response.getHeaders().addAll(cachedResponse.headers()); | ||
var body = Flux.fromIterable(cachedResponse.body()) | ||
.map(byteBuffer -> response.bufferFactory().wrap(byteBuffer)); | ||
return response.writeWith(body); | ||
} | ||
|
||
class CacheResponseDecorator extends ServerHttpResponseDecorator { | ||
|
||
private final ServerWebExchange exchange; | ||
|
||
private final String cacheKey; | ||
|
||
public CacheResponseDecorator(ServerWebExchange exchange, String cacheKey) { | ||
super(exchange.getResponse()); | ||
this.exchange = exchange; | ||
this.cacheKey = cacheKey; | ||
} | ||
|
||
@Override | ||
@NonNull | ||
public Mono<Void> writeWith(@NonNull Publisher<? extends DataBuffer> body) { | ||
if (responseCacheable(exchange)) { | ||
var response = getDelegate(); | ||
body = Flux.from(body) | ||
.map(dataBuffer -> { | ||
var byteBuffer = allocateDirect(dataBuffer.readableByteCount()); | ||
dataBuffer.toByteBuffer(byteBuffer); | ||
DataBufferUtils.release(dataBuffer); | ||
return byteBuffer.asReadOnlyBuffer(); | ||
}) | ||
.collectSortedList() | ||
.doOnSuccess(byteBuffers -> { | ||
var headers = new HttpHeaders(); | ||
headers.addAll(response.getHeaders()); | ||
var cachedResponse = new CachedResponse(response.getStatusCode(), | ||
headers, | ||
byteBuffers, | ||
Instant.now()); | ||
cache.put(cacheKey, cachedResponse); | ||
}) | ||
.flatMapMany(Flux::fromIterable) | ||
.map(byteBuffer -> response.bufferFactory().wrap(byteBuffer)); | ||
} | ||
// write the response | ||
return super.writeWith(body); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
apiVersion: plugin.halo.run/v1alpha1 | ||
kind: ExtensionDefinition | ||
metadata: | ||
name: page-cache-handler | ||
spec: | ||
className: run.halo.cache.page.PageCacheWebFilter | ||
extensionPointName: additional-webfilter | ||
displayName: "页面静态缓存" | ||
description: "通过拦截页面请求,以实现页面的静态缓存功能,提高页面访问速度。" |
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.