-
Notifications
You must be signed in to change notification settings - Fork 672
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cache): add
CacheInterceptor
(#1576)
- Loading branch information
Showing
12 changed files
with
302 additions
and
17 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './user'; | ||
export * from './chart'; | ||
export * from './upload'; | ||
export * from './test'; |
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,6 @@ | ||
import { HttpHeaders, HttpResponse } from "@angular/common/http"; | ||
|
||
export const TESTS = { | ||
'/test': { ok: true }, | ||
'/test/cache-control': () => new HttpResponse({ body: 'cache-control', headers: new HttpHeaders({ 'cache-control': 'max-age=10' }) }), | ||
}; |
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,22 @@ | ||
--- | ||
order: 3 | ||
title: Interceptor | ||
type: Documents | ||
--- | ||
|
||
# 写在前面 | ||
|
||
搭配 `httpCacheInterceptor` Http 拦截器,可以将缓存应用到 Http 请求当中。它只有几个特征: | ||
|
||
- 支持缓存过期时间 | ||
- 支持自定义缓存 KEY | ||
- 支持任何 Http 请求、任何数据格式 | ||
- 符合 Http 缓存响应标准 `Cache-Control` | ||
|
||
# 如何使用 | ||
|
||
在 `withInterceptors` 中引入 `httpCacheInterceptor`: | ||
|
||
```ts | ||
provideHttpClient(withInterceptors([httpCacheInterceptor])) | ||
``` |
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,3 +1,4 @@ | ||
export * from './src/interface'; | ||
export * from './src/cache.service'; | ||
export * from './src/cache.module'; | ||
export * from './src/cache.interceptor'; | ||
export * from './src/token'; |
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,118 @@ | ||
import { HttpClient, HttpContext, HttpResponse, provideHttpClient, withInterceptors } from '@angular/common/http'; | ||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; | ||
import { Type } from '@angular/core'; | ||
import { TestBed } from '@angular/core/testing'; | ||
|
||
import { httpCacheInterceptor } from './cache.interceptor'; | ||
import { CacheService } from './cache.service'; | ||
import { CACHE } from './token'; | ||
|
||
describe('cache: interceptor', () => { | ||
let http: HttpClient; | ||
let httpBed: HttpTestingController; | ||
let cacheSrv: CacheService; | ||
|
||
function genModule(): void { | ||
TestBed.configureTestingModule({ | ||
imports: [], | ||
providers: [provideHttpClient(withInterceptors([httpCacheInterceptor])), provideHttpClientTesting()] | ||
}); | ||
http = TestBed.inject(HttpClient); | ||
httpBed = TestBed.inject(HttpTestingController as Type<HttpTestingController>); | ||
cacheSrv = TestBed.inject(CacheService); | ||
cacheSrv.set('a', 'a', { type: 'm' }); | ||
} | ||
|
||
beforeEach(genModule); | ||
|
||
it('should be working', () => { | ||
const logSpy = spyOn(console, 'log'); | ||
http.get('/test', { responseType: 'text', context: new HttpContext().set(CACHE, { key: 'a' }) }).subscribe(); | ||
expect(logSpy).toHaveBeenCalled(); | ||
expect(logSpy.calls.first().args[0]).toBe(`%c👽GET->/test->from cache(onle in development)`); | ||
}); | ||
|
||
it('should be truth request and cache data of response when is not cache', done => { | ||
const key = 'b'; | ||
const res = 'ok'; | ||
http | ||
.get('/test', { responseType: 'text', context: new HttpContext().set(CACHE, { key, expire: 60 }) }) | ||
.subscribe(res => { | ||
expect(res).toBe(res); | ||
expect((cacheSrv.getNone(key) as HttpResponse<string>)?.body).toBe(res); | ||
done(); | ||
}); | ||
httpBed.expectOne('/test').flush(res); | ||
}); | ||
|
||
it('should be support cache-control', done => { | ||
const key = 'b'; | ||
const res = 'ok'; | ||
http.get('/test', { responseType: 'text', context: new HttpContext().set(CACHE, { key }) }).subscribe(res => { | ||
expect(res).toBe(res); | ||
expect((cacheSrv.getNone(key) as HttpResponse<string>)?.body).toBe(res); | ||
done(); | ||
}); | ||
httpBed.expectOne('/test').flush(res, { headers: { 'cache-control': 'max-age=60' } }); | ||
}); | ||
|
||
it('should be support POST data', done => { | ||
const key = 'b'; | ||
const res = 'ok'; | ||
http.post(key, { responseType: 'text', context: new HttpContext().set(CACHE, { key }) }).subscribe(() => { | ||
expect((cacheSrv.getNone(key) as HttpResponse<string>)?.body).toBe(res); | ||
done(); | ||
}); | ||
httpBed.expectOne(key).flush(res, { headers: { 'cache-control': 'max-age=60' } }); | ||
}); | ||
|
||
describe('Ignore cache', () => { | ||
it('when response cache-control', done => { | ||
const key = 'b'; | ||
const res = 'ok'; | ||
http.get(key, { responseType: 'text' }).subscribe(res => { | ||
expect(res).toBe(res); | ||
expect((cacheSrv.getNone(key) as HttpResponse<string>)?.body).toBe(res); | ||
done(); | ||
}); | ||
httpBed.expectOne(key).flush(res, { headers: { 'cache-control': 'max-age=60' } }); | ||
}); | ||
|
||
it('when is not set CACHE', done => { | ||
const key = 'b'; | ||
const res = 'ok'; | ||
http.get('/test', { responseType: 'text' }).subscribe(res => { | ||
expect(res).toBe(res); | ||
expect(cacheSrv.has(key)).toBe(false); | ||
done(); | ||
}); | ||
httpBed.expectOne('/test').flush(res); | ||
}); | ||
|
||
it('when enabled is false', done => { | ||
const key = 'b'; | ||
const res = 'ok'; | ||
http | ||
.get(key, { responseType: 'text', context: new HttpContext().set(CACHE, { enabled: false }) }) | ||
.subscribe(res => { | ||
expect(res).toBe(res); | ||
expect(cacheSrv.has(key)).toBe(false); | ||
done(); | ||
}); | ||
httpBed.expectOne(key).flush(res, { headers: { 'cache-control': 'max-age=60' } }); | ||
}); | ||
|
||
it('when expire is 0', done => { | ||
const key = 'b'; | ||
const res = 'ok'; | ||
http | ||
.get('/test', { responseType: 'text', context: new HttpContext().set(CACHE, { expire: 0 }) }) | ||
.subscribe(res => { | ||
expect(res).toBe(res); | ||
expect(cacheSrv.has(key)).toBe(false); | ||
done(); | ||
}); | ||
httpBed.expectOne('/test').flush(res); | ||
}); | ||
}); | ||
}); |
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,69 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import { HttpEvent, HttpInterceptorFn, HttpResponseBase } from '@angular/common/http'; | ||
import { inject } from '@angular/core'; | ||
import { map, of, OperatorFunction } from 'rxjs'; | ||
|
||
import { AlainConfigService } from '@delon/util/config'; | ||
|
||
import { CacheService } from './cache.service'; | ||
import { CacheOptions, CACHE } from './token'; | ||
|
||
/** | ||
* Cache interceptor | ||
* | ||
* 缓存拦截器 | ||
* | ||
* @example | ||
* provideHttpClient(withInterceptors([httpCacheInterceptor])), | ||
*/ | ||
export const httpCacheInterceptor: HttpInterceptorFn = (req, next) => { | ||
const cog = inject(AlainConfigService).merge('cache', {})!.interceptor; | ||
const options: CacheOptions = { | ||
enabled: true, | ||
emitNotify: true, | ||
saveType: 'm', | ||
...cog, | ||
...req.context.get(CACHE) | ||
}; | ||
const srv = inject(CacheService); | ||
const mapPipe: OperatorFunction<HttpEvent<any>, HttpEvent<any>> = map(ev => save(srv, ev, options)); | ||
if (options.enabled === false) { | ||
return next(req).pipe(mapPipe); | ||
} | ||
|
||
if (options.key == null) { | ||
options.key = req.urlWithParams; | ||
} | ||
|
||
const cacheData = srv.getNone<HttpEvent<any>>(options.key); | ||
if (cacheData != null) { | ||
if (typeof ngDevMode === 'undefined' || ngDevMode) { | ||
console.log( | ||
`%c👽${req.method}->${req.urlWithParams}->from cache(onle in development)`, | ||
'background:#000;color:#1890ff', | ||
req, | ||
cacheData | ||
); | ||
} | ||
return of(cacheData); | ||
} | ||
|
||
return next(req).pipe(mapPipe); | ||
}; | ||
|
||
function save(srv: CacheService, ev: HttpEvent<any>, options: CacheOptions): HttpEvent<any> { | ||
if (!(ev instanceof HttpResponseBase) || !(ev.status >= 200 && ev.status < 300)) return ev; | ||
let expire = options.expire; | ||
if (expire == null) { | ||
const ageMatch = /max-age=(\d+)/g.exec(ev.headers.get('cache-control')?.toLowerCase() ?? ''); | ||
if (ageMatch == null) return ev; | ||
expire = +ageMatch[1]; | ||
} | ||
if (expire > 0) { | ||
srv.set(options.key!!, ev, { | ||
type: options.saveType!!, | ||
expire: expire | ||
}); | ||
} | ||
return ev; | ||
} |
This file was deleted.
Oops, something went wrong.
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
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,46 @@ | ||
import { HttpContextToken } from '@angular/common/http'; | ||
|
||
export interface CacheOptions { | ||
/** | ||
* Whether to enable it, if `Cache-control: max-age=xxx` is found when the request returns, it will be automatically cached, and the next request will be forced to obtain data from the cache unless `enabled: false` is specified; default `true` | ||
* | ||
* 是否启用,当启用后若请求返回时发现 `Cache-control: max-age=xxx` 时自动缓存,下一次请求时除非指定 `enabled: false` 否则强制从缓存中获取数据;默认 `true` | ||
*/ | ||
enabled?: boolean; | ||
/** | ||
* Specify the cache unique key, which is used to distinguish cache entries, and the default is based on the requested URL | ||
* | ||
* 指定缓存唯一键,用于区分缓存条目,默认以请求的 URL 为准 | ||
*/ | ||
key?: string; | ||
/** | ||
* Specify the storage method, `m` means memory, `s` means persistence; default: `m` | ||
* | ||
* 指定存储方式,`m` 表示内存,`s` 表示持久化;默认:`m` | ||
*/ | ||
saveType?: 'm' | 's'; | ||
/** | ||
* Expire time, the highest priority when returning `Cache-control: max-age=xxx`, unit `second` | ||
* | ||
* 过期时间,当返回 `Cache-control: max-age=xxx` 时优先级最高,单位 `秒` | ||
*/ | ||
expire?: number; | ||
/** | ||
* Whether to trigger a notification, default: `true` | ||
* | ||
* 是否触发通知,默认:`true` | ||
*/ | ||
emitNotify?: boolean; | ||
} | ||
|
||
/** | ||
* Cache options (Don't forget to register `CacheInterceptor`) | ||
* | ||
* 缓存配置项(不要忘记注册 `CacheInterceptor`) | ||
* | ||
* @example | ||
* this.http.get(`my`, { | ||
* context: new HttpContext().set(CACHE, { key: 'user-data' }) | ||
* }) | ||
*/ | ||
export const CACHE = new HttpContextToken<CacheOptions>(() => ({})); |
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 |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
"rootDir": ".", | ||
"paths": { | ||
"@delon/*": ["../../dist/@delon/*"] | ||
} | ||
}, | ||
"types": ["../dev-mode-types"] | ||
} | ||
} |
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