Skip to content

Commit b44fdc0

Browse files
committed
feat(remove-redirect): transform 支持 fallbackSelector 配置项
1 parent dc9fa7a commit b44fdc0

8 files changed

Lines changed: 123 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'remove-redirect': minor
3+
---
4+
5+
`transform` 支持 `fallbackSelector` 配置项

packages/remove-redirect/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from '@femm/shared-utils'
99

1010
import * as sites from 'src/sites'
11-
import { getSearchParamsValue } from 'src/utils'
11+
import { getSearchParamsValue, requestOriginalLink } from 'src/utils'
1212

1313
const hostname = formatHostname()
1414
const formatSites = Object.values(sites).flat()
@@ -46,6 +46,7 @@ function handleTransform<T extends AllHTMLElementTypes>({
4646
attribute,
4747
queryName,
4848
separator = '?target=',
49+
fallbackSelector,
4950
customTransform = (node: T) => {
5051
let originUrl = ''
5152

@@ -70,6 +71,10 @@ function handleTransform<T extends AllHTMLElementTypes>({
7071
}: Site.Transform<T>) {
7172
const observer = new MutationObserver(() => {
7273
document.querySelectorAll<T>(selector).forEach(customTransform)
74+
75+
if (fallbackSelector && document.querySelectorAll(fallbackSelector).length) {
76+
document.querySelectorAll<HTMLAnchorElement>(fallbackSelector).forEach(requestOriginalLink)
77+
}
7378
})
7479

7580
document.body.setAttribute(FEMM_ATTR_KEY, 'remove-redirect')

packages/remove-redirect/src/utils/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { observeElementEnterViewport } from '@femm/shared-utils'
1+
import { observeElementEnterViewport } from '@femm/shared-utils/dom/'
22
import { GMCachedRequest } from '@femm/shared-utils/gm/'
33

44
export function defineSite<T extends AllHTMLElementTypes = HTMLAnchorElement>(
@@ -43,3 +43,13 @@ export function getSearchParamsValue(
4343

4444
return result || ''
4545
}
46+
47+
export function requestOriginalLink(element: HTMLAnchorElement) {
48+
observeElementEnterViewport(element, () => {
49+
GMCachedRequest({ url: element.href }).then((res) => {
50+
if (res.finalUrl) {
51+
element.href = res.finalUrl
52+
}
53+
})
54+
})
55+
}

packages/remove-redirect/types/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ declare namespace Site {
1717
/** 分隔符 */
1818
separator?: Separator
1919

20+
/**
21+
* 需要进行兜底处理的链接元素的 CSS 选择器
22+
* 用于处理 transform 规则无法处理的链接,通过 GM.xmlHttpRequest 请求获取原始链接,并替换为解析后的 URL
23+
*/
24+
fallbackSelector?: `a${string}`
25+
2026
/**
2127
* 自定义转换规则函数
2228
* 用于定义如何处理 DOM 元素的逻辑,如果定义了该规则,将覆盖默认行为

shared/utils/src/dom/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './attr'
22
export * from './constant'
3+
export * from './intersection-observer'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { FEMM_ATTR_KEY } from './constant'
2+
3+
const map = new WeakMap()
4+
5+
/** 监听元素进入可视区域 */
6+
export function observeElementEnterViewport(element: Element, callback: () => void) {
7+
if (map.has(element) || element.getAttribute(FEMM_ATTR_KEY)) {
8+
return
9+
}
10+
map.set(element, 1)
11+
const ob = new IntersectionObserver((entries) => {
12+
for (const entry of entries) {
13+
if (entry.isIntersecting) {
14+
element.setAttribute(FEMM_ATTR_KEY, 'observered')
15+
callback()
16+
ob.unobserve(element)
17+
}
18+
}
19+
})
20+
21+
ob.observe(element)
22+
}

shared/utils/src/gm/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createCachedRequest } from '../request'
2+
3+
export const GMCachedRequest = createCachedRequest(GM.xmlHttpRequest)

shared/utils/src/request/index.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
interface CachedRequestOptions {
2+
/** 过期时间,如果为 0 则表示永不过期 */
3+
cacheTime?: number
4+
shouldCacheError?: boolean
5+
}
6+
interface CacheEntry<R> {
7+
data?: R
8+
time?: number
9+
error?: any
10+
requestPromise?: Promise<R>
11+
}
12+
13+
type RequestFunctionWithCacheOptions<TContext = any> = (
14+
details: Tampermonkey.Request<TContext> & CachedRequestOptions,
15+
) => Promise<Tampermonkey.Response<TContext>>
16+
17+
export function createCachedRequest(requestFunction: RequestFunctionWithCacheOptions) {
18+
const cache = new Map<string, CacheEntry<any>>()
19+
20+
return function cachedRequest<TContext extends any>(
21+
options: Tampermonkey.Request<TContext> & CachedRequestOptions,
22+
): Promise<Tampermonkey.Response<TContext>> {
23+
if (
24+
options.cacheTime !== undefined &&
25+
(typeof options.cacheTime !== 'number' || options.cacheTime < 0)
26+
) {
27+
throw new Error('无效的 cacheTime 选项')
28+
}
29+
30+
const cacheKey = JSON.stringify(options)
31+
const cachedData = cache.get(cacheKey)
32+
33+
if (cachedData) {
34+
const { data, time, error, requestPromise } = cachedData
35+
36+
if (requestPromise) {
37+
return requestPromise
38+
}
39+
40+
const cacheTime = options.cacheTime ?? 0
41+
42+
if (!error && time && cacheTime && Date.now() - time < cacheTime) {
43+
return Promise.resolve(data as Tampermonkey.Response<TContext>)
44+
} else {
45+
cache.delete(cacheKey)
46+
}
47+
}
48+
49+
const shouldCacheError = options.shouldCacheError ?? false
50+
51+
const requestPromise = requestFunction(options)
52+
.then((data) => {
53+
const time = Date.now()
54+
cache.set(cacheKey, { data, time })
55+
return data
56+
})
57+
.catch((error) => {
58+
if (shouldCacheError) {
59+
const time = Date.now()
60+
cache.set(cacheKey, { error, time })
61+
}
62+
throw error
63+
})
64+
65+
cache.set(cacheKey, { requestPromise })
66+
67+
return requestPromise
68+
}
69+
}

0 commit comments

Comments
 (0)