mirror of https://github.com/halo-dev/halo
Add support for caching template rendering result (#4091)
#### What type of PR is this? /kind feature /area core #### What this PR does / why we need it: This PR adds dependency [spring-boot-starter-cache](https://docs.spring.io/spring-boot/docs/current/reference/html/io.html#io.caching) as cache framework and [caffeine](https://github.com/ben-manes/caffeine/wiki) as cache implementation to cache template rendering result. By default, we disable the cache feature. If you want to enable it, please try to configure properties like this: ```yaml halo: cache: disabled: false ``` #### Which issue(s) this PR fixes: Fixes #2827 #### Special notes for your reviewer: 1. Start Halo 2. Browse any page twice 3. See the difference in request times #### Does this PR introduce a user-facing change? ```release-note 支持模板渲染结果缓存 ```pull/4104/head^2 v2.7.0-rc.1
parent
2791d2f0e5
commit
d0526ec592
|
@ -29,6 +29,10 @@ dependencies {
|
|||
api 'org.springframework.security:spring-security-oauth2-client'
|
||||
api 'org.springframework.security:spring-security-oauth2-resource-server'
|
||||
|
||||
// Cache
|
||||
api "org.springframework.boot:spring-boot-starter-cache"
|
||||
api "com.github.ben-manes.caffeine:caffeine"
|
||||
|
||||
api "org.springdoc:springdoc-openapi-starter-webflux-ui"
|
||||
api 'org.openapi4j:openapi-schema-validator'
|
||||
api "net.bytebuddy:byte-buddy"
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package run.halo.app.cache;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH;
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
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.ServerResponse;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
|
||||
@Component
|
||||
public class CacheEndpoint implements CustomEndpoint {
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
public CacheEndpoint(CacheManager cacheManager) {
|
||||
this.cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
return SpringdocRouteBuilder
|
||||
.route()
|
||||
.POST("/caches/{name}/invalidation", request -> {
|
||||
var cacheName = request.pathVariable("name");
|
||||
if (cacheManager.getCacheNames().contains(cacheName)) {
|
||||
var cache = cacheManager.getCache(cacheName);
|
||||
if (cache != null) {
|
||||
cache.invalidate();
|
||||
}
|
||||
}
|
||||
return ServerResponse.noContent().build();
|
||||
}, builder -> builder
|
||||
.tag("v1alpha1/Cache")
|
||||
.operationId("InvalidCache")
|
||||
.description("Invalidate a cache.")
|
||||
.parameter(parameterBuilder()
|
||||
.name("name")
|
||||
.in(PATH)
|
||||
.required(true)
|
||||
.description("Cache name"))
|
||||
.response(Builder.responseBuilder()
|
||||
.responseCode(String.valueOf(NO_CONTENT.value())))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package run.halo.app.cache;
|
||||
|
||||
import static java.nio.ByteBuffer.allocateDirect;
|
||||
import static org.springframework.http.HttpHeaders.CACHE_CONTROL;
|
||||
import static run.halo.app.infra.AnonymousUserConst.isAnonymousUser;
|
||||
|
||||
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.Ordered;
|
||||
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.core.Authentication;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Slf4j
|
||||
public class CacheWebFilter implements WebFilter, Ordered {
|
||||
|
||||
public static final String REQUEST_TO_CACHE = "RequestCacheWebFilterToCache";
|
||||
|
||||
public static final String CACHE_NAME = "page-cache";
|
||||
|
||||
private final Cache cache;
|
||||
|
||||
public CacheWebFilter(CacheManager cacheManager) {
|
||||
this.cache = cacheManager.getCache(CACHE_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Authentication::getName)
|
||||
.filter(name -> isAnonymousUser(name) && requestCacheable(exchange.getRequest()))
|
||||
.switchIfEmpty(Mono.defer(() -> chain.filter(exchange).then(Mono.empty())))
|
||||
.flatMap(name -> {
|
||||
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);
|
||||
}
|
||||
// decorate the ServerHttpResponse to cache the response
|
||||
var decoratedExchange = exchange.mutate()
|
||||
.response(new CacheResponseDecorator(exchange, cacheKey))
|
||||
.build();
|
||||
return chain.filter(decoratedExchange);
|
||||
});
|
||||
}
|
||||
|
||||
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(REQUEST_TO_CACHE, false);
|
||||
}
|
||||
|
||||
private static boolean hasRequestBody(ServerHttpRequest request) {
|
||||
return request.getHeaders().getContentLength() > 0;
|
||||
}
|
||||
|
||||
private String generateCacheKey(ServerHttpRequest request) {
|
||||
return request.getURI().toASCIIString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
// The filter should be after org.springframework.security.web.server.WebFilterChainProxy
|
||||
return Ordered.LOWEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package run.halo.app.cache;
|
||||
|
||||
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) {
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package run.halo.app.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import run.halo.app.cache.CacheWebFilter;
|
||||
|
||||
@EnableCaching
|
||||
@Configuration
|
||||
public class CacheConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "halo.cache.disabled", havingValue = "false")
|
||||
WebFilter cacheWebFilter(CacheManager cacheManager) {
|
||||
return new CacheWebFilter(cacheManager);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package run.halo.app.infra.properties;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CacheProperties {
|
||||
|
||||
private boolean disabled = true;
|
||||
|
||||
}
|
|
@ -58,6 +58,9 @@ public class HaloProperties implements Validator {
|
|||
@Valid
|
||||
private final AttachmentProperties attachment = new AttachmentProperties();
|
||||
|
||||
@Valid
|
||||
private final CacheProperties cache = new CacheProperties();
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> clazz) {
|
||||
return HaloProperties.class.isAssignableFrom(clazz);
|
||||
|
|
|
@ -15,7 +15,7 @@ import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveView;
|
|||
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveViewResolver;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.cache.CacheWebFilter;
|
||||
import run.halo.app.theme.finders.FinderRegistry;
|
||||
|
||||
@Component("thymeleafReactiveViewResolver")
|
||||
|
@ -53,8 +53,8 @@ public class HaloViewResolver extends ThymeleafReactiveViewResolver {
|
|||
return themeResolver.getTheme(exchange).flatMap(theme -> {
|
||||
// calculate the engine before rendering
|
||||
setTemplateEngine(engineManager.getTemplateEngine(theme));
|
||||
return super.render(model, contentType, exchange)
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
exchange.getAttributes().put(CacheWebFilter.REQUEST_TO_CACHE, true);
|
||||
return super.render(model, contentType, exchange);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ server:
|
|||
enabled: true
|
||||
error:
|
||||
whitelabel:
|
||||
enabled: false
|
||||
enabled: false
|
||||
spring:
|
||||
output:
|
||||
ansi:
|
||||
|
@ -27,6 +27,10 @@ spring:
|
|||
cache:
|
||||
cachecontrol:
|
||||
max-age: 365d
|
||||
cache:
|
||||
type: caffeine
|
||||
caffeine:
|
||||
spec: expireAfterAccess=1h, maximumSize=10000
|
||||
|
||||
halo:
|
||||
work-dir: ${user.home}/.halo2
|
||||
|
@ -56,7 +60,7 @@ management:
|
|||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: ["health", "info", "startup", "globalinfo", "logfile"]
|
||||
include: ["health", "info", "startup", "globalinfo", "logfile"]
|
||||
endpoint:
|
||||
health:
|
||||
probes:
|
||||
|
|
|
@ -45,6 +45,7 @@ api/storage-halo-run-v1alpha1-policy-api.ts
|
|||
api/storage-halo-run-v1alpha1-policy-template-api.ts
|
||||
api/theme-halo-run-v1alpha1-theme-api.ts
|
||||
api/v1alpha1-annotation-setting-api.ts
|
||||
api/v1alpha1-cache-api.ts
|
||||
api/v1alpha1-config-map-api.ts
|
||||
api/v1alpha1-menu-api.ts
|
||||
api/v1alpha1-menu-item-api.ts
|
||||
|
|
|
@ -56,6 +56,7 @@ export * from "./api/storage-halo-run-v1alpha1-policy-api";
|
|||
export * from "./api/storage-halo-run-v1alpha1-policy-template-api";
|
||||
export * from "./api/theme-halo-run-v1alpha1-theme-api";
|
||||
export * from "./api/v1alpha1-annotation-setting-api";
|
||||
export * from "./api/v1alpha1-cache-api";
|
||||
export * from "./api/v1alpha1-config-map-api";
|
||||
export * from "./api/v1alpha1-menu-api";
|
||||
export * from "./api/v1alpha1-menu-item-api";
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { Configuration } from "../configuration";
|
||||
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import globalAxios from "axios";
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import {
|
||||
DUMMY_BASE_URL,
|
||||
assertParamExists,
|
||||
setApiKeyToObject,
|
||||
setBasicAuthToObject,
|
||||
setBearerAuthToObject,
|
||||
setOAuthToObject,
|
||||
setSearchParams,
|
||||
serializeDataIfNeeded,
|
||||
toPathString,
|
||||
createRequestFunction,
|
||||
} from "../common";
|
||||
// @ts-ignore
|
||||
import {
|
||||
BASE_PATH,
|
||||
COLLECTION_FORMATS,
|
||||
RequestArgs,
|
||||
BaseAPI,
|
||||
RequiredError,
|
||||
} from "../base";
|
||||
/**
|
||||
* V1alpha1CacheApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const V1alpha1CacheApiAxiosParamCreator = function (
|
||||
configuration?: Configuration
|
||||
) {
|
||||
return {
|
||||
/**
|
||||
* Invalidate a cache.
|
||||
* @param {string} name Cache name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
invalidCache: async (
|
||||
name: string,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists("invalidCache", "name", name);
|
||||
const localVarPath =
|
||||
`/apis/api.console.halo.run/v1alpha1/caches/{name}/invalidation`.replace(
|
||||
`{${"name"}}`,
|
||||
encodeURIComponent(String(name))
|
||||
);
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "POST",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* V1alpha1CacheApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const V1alpha1CacheApiFp = function (configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator =
|
||||
V1alpha1CacheApiAxiosParamCreator(configuration);
|
||||
return {
|
||||
/**
|
||||
* Invalidate a cache.
|
||||
* @param {string} name Cache name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async invalidCache(
|
||||
name: string,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.invalidCache(
|
||||
name,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* V1alpha1CacheApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const V1alpha1CacheApiFactory = function (
|
||||
configuration?: Configuration,
|
||||
basePath?: string,
|
||||
axios?: AxiosInstance
|
||||
) {
|
||||
const localVarFp = V1alpha1CacheApiFp(configuration);
|
||||
return {
|
||||
/**
|
||||
* Invalidate a cache.
|
||||
* @param {V1alpha1CacheApiInvalidCacheRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
invalidCache(
|
||||
requestParameters: V1alpha1CacheApiInvalidCacheRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<void> {
|
||||
return localVarFp
|
||||
.invalidCache(requestParameters.name, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for invalidCache operation in V1alpha1CacheApi.
|
||||
* @export
|
||||
* @interface V1alpha1CacheApiInvalidCacheRequest
|
||||
*/
|
||||
export interface V1alpha1CacheApiInvalidCacheRequest {
|
||||
/**
|
||||
* Cache name
|
||||
* @type {string}
|
||||
* @memberof V1alpha1CacheApiInvalidCache
|
||||
*/
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* V1alpha1CacheApi - object-oriented interface
|
||||
* @export
|
||||
* @class V1alpha1CacheApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class V1alpha1CacheApi extends BaseAPI {
|
||||
/**
|
||||
* Invalidate a cache.
|
||||
* @param {V1alpha1CacheApiInvalidCacheRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof V1alpha1CacheApi
|
||||
*/
|
||||
public invalidCache(
|
||||
requestParameters: V1alpha1CacheApiInvalidCacheRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return V1alpha1CacheApiFp(this.configuration)
|
||||
.invalidCache(requestParameters.name, options)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
|
@ -30,10 +30,10 @@ export interface Secret {
|
|||
apiVersion: string;
|
||||
/**
|
||||
*
|
||||
* @type {{ [key: string]: string; }}
|
||||
* @type {{ [key: string]: Array<string>; }}
|
||||
* @memberof Secret
|
||||
*/
|
||||
data?: { [key: string]: string };
|
||||
data?: { [key: string]: Array<string> };
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
|
|
@ -124,6 +124,11 @@ core:
|
|||
dialog_title: Do you want to refresh the search engine index?
|
||||
dialog_content: This operation will recreate search engine indexes for all published posts.
|
||||
success_message: Refresh search engine index successfully.
|
||||
refresh_page_cache:
|
||||
title: Refresh Page Cache
|
||||
dialog_title: Do you want to refresh the page cache?
|
||||
dialog_content: This operation will clear the cache for all pages.
|
||||
success_message: Refresh page cache successfully.
|
||||
user_stats:
|
||||
title: Users
|
||||
comment_stats:
|
||||
|
|
|
@ -124,6 +124,11 @@ core:
|
|||
dialog_title: 确定要刷新搜索引擎索引吗?
|
||||
dialog_content: 此操作会对所有已发布的文章重新创建搜索引擎索引。
|
||||
success_message: 刷新成功
|
||||
refresh_page_cache:
|
||||
title: 刷新页面缓存
|
||||
dialog_title: 确定要刷新页面缓存吗?
|
||||
dialog_content: 此操作会清空所有页面的缓存。
|
||||
success_message: 刷新成功
|
||||
user_stats:
|
||||
title: 用户
|
||||
comment_stats:
|
||||
|
|
|
@ -124,6 +124,11 @@ core:
|
|||
dialog_title: 確定要刷新搜尋引擎索引嗎?
|
||||
dialog_content: 此操作會對所有已發布的文章重新創建搜尋引擎索引。
|
||||
success_message: 刷新成功
|
||||
refresh_page_cache:
|
||||
title: 重新整理頁面快取
|
||||
dialog_title: 確定要重新整理頁面快取嗎?
|
||||
dialog_content: 這個操作會清空所有頁面的快取。
|
||||
success_message: 刷新成功
|
||||
user_stats:
|
||||
title: 用戶
|
||||
comment_stats:
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
IconPalette,
|
||||
IconWindowLine,
|
||||
IconSearch,
|
||||
IconDatabase2Line,
|
||||
VCard,
|
||||
IconUserLine,
|
||||
Dialog,
|
||||
|
@ -156,6 +157,32 @@ const actions: Action[] = [
|
|||
},
|
||||
permissions: ["system:posts:manage"],
|
||||
},
|
||||
{
|
||||
icon: markRaw(IconDatabase2Line),
|
||||
title: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.refresh_page_cache.title"
|
||||
),
|
||||
action: () => {
|
||||
Dialog.warning({
|
||||
title: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.refresh_page_cache.dialog_title"
|
||||
),
|
||||
description: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.refresh_page_cache.dialog_content"
|
||||
),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await apiClient.cache.invalidCache({ name: "page-cache" });
|
||||
Toast.success(
|
||||
t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.refresh_page_cache.success_message"
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
V1alpha1SettingApi,
|
||||
V1alpha1UserApi,
|
||||
V1alpha1AnnotationSettingApi,
|
||||
V1alpha1CacheApi,
|
||||
LoginApi,
|
||||
AuthHaloRunV1alpha1AuthProviderApi,
|
||||
AuthHaloRunV1alpha1UserConnectionApi,
|
||||
|
@ -218,6 +219,7 @@ function setupApiClient(axios: AxiosInstance) {
|
|||
common: {
|
||||
user: new ApiHaloRunV1alpha1UserApi(undefined, baseURL, axios),
|
||||
},
|
||||
cache: new V1alpha1CacheApi(undefined, baseURL, axios),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# 页面缓存
|
||||
|
||||
Halo 的主要应用以博客为主,页面更新不会特别频繁,大多数情况下,实时渲染的结果都是没有变化的。如果能够缓存这些不经常变更的页面,可减少数据库访问,加快访问速度。
|
||||
|
||||
页面缓存包括缓存响应体、响应头和响应状态。页面缓存规则如下:
|
||||
|
||||
1. 仅缓存模板引擎所渲染的页面。
|
||||
2. 仅缓存 `Content-Type` 为 `text/html` 的页面。
|
||||
3. 仅缓存响应状态为 `HTTP 200(OK)`。
|
||||
4. 请求访问为 `GET`。
|
||||
|
||||
默认缓存失效时间:距最新一次访问 `1` 小时。 默认最大缓存数量:`10,000` 个
|
Loading…
Reference in New Issue