Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add basic features #1

Merged
merged 8 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ jobs:
uses: halo-sigs/reusable-workflows/.github/workflows/plugin-ci.yaml@v1
with:
ui-path: "ui"
node-version: "20"
pnpm-version: "9"
80 changes: 14 additions & 66 deletions README.md
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)
7 changes: 4 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
id "run.halo.plugin.devtools" version "0.0.9"
}

group 'run.halo.starter'
group 'run.halo.cache.page'
sourceCompatibility = JavaVersion.VERSION_17

repositories {
Expand All @@ -16,7 +16,7 @@ repositories {
}

dependencies {
implementation platform('run.halo.tools.platform:plugin:2.11.0-SNAPSHOT')
implementation platform('run.halo.tools.platform:plugin:2.17.0-SNAPSHOT')
compileOnly 'run.halo.app:api'

testImplementation 'run.halo.app:api'
Expand Down Expand Up @@ -52,5 +52,6 @@ build {
}

halo {
version = '2.11'
version = '2.17.0-beta.1'
debug = true
}
Binary file added images/plugin-page-cache-preview-after.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/plugin-page-cache-preview-before.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ pluginManagement {
gradlePluginPortal()
}
}
rootProject.name = 'plugin-starter'
rootProject.name = 'plugin-page-cache'

54 changes: 54 additions & 0 deletions src/main/java/run/halo/cache/page/CacheEndpoint.java
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");
}
}
18 changes: 18 additions & 0 deletions src/main/java/run/halo/cache/page/CachedResponse.java
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) {

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package run.halo.starter;
package run.halo.cache.page;

import org.springframework.stereotype.Component;
import run.halo.app.plugin.BasePlugin;
Expand All @@ -13,19 +13,9 @@
* @since 1.0.0
*/
@Component
public class StarterPlugin extends BasePlugin {
public class PageCachePlugin extends BasePlugin {

public StarterPlugin(PluginContext pluginContext) {
public PageCachePlugin(PluginContext pluginContext) {
super(pluginContext);
}

@Override
public void start() {
System.out.println("插件启动成功!");
}

@Override
public void stop() {
System.out.println("插件停止!");
}
}
151 changes: 151 additions & 0 deletions src/main/java/run/halo/cache/page/PageCacheWebFilter.java
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);
}
}
}
9 changes: 9 additions & 0 deletions src/main/resources/extensions/extension.yaml
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 removed src/main/resources/logo.png
Binary file not shown.
1 change: 1 addition & 0 deletions src/main/resources/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading