ogMeta.js
8.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
/**
* 统一管理 Open Graph 元标签。
*
* 设计目标:
* 1. 任意路由切换时都能自动同步 OG 信息。
* 2. 兼容 index.html 中已经存在的静态 meta,例如 og:description。
* 3. 页面只负责更新 document.title;OG 的写入和恢复统一放在这里处理。
*/
const MANAGED_OG_TAGS = [
{ id: 'og-title', property: 'og:title', key: 'title' },
{ id: 'og-description', property: 'og:description', key: 'description' },
{ id: 'og-image', property: 'og:image', key: 'image' },
{ id: 'og-url', property: 'og:url', key: 'url' },
]
// 首次接管 OG 标签时,记录页面原始状态,便于 teardown/reset 时恢复。
let originalOgMetaState = null
const getHead = documentObj =>
documentObj?.head || documentObj?.getElementsByTagName?.('head')?.[0] || null
const getMetaByProperty = (documentObj, property) =>
getHead(documentObj)?.querySelector(`meta[property="${property}"]`) || null
const captureOriginalOgMetaState = documentObj => {
if (originalOgMetaState) {
return originalOgMetaState
}
// 只在首次接管时做快照,避免后续多次同步把“运行时状态”误当成“初始状态”。
originalOgMetaState = MANAGED_OG_TAGS.reduce((state, item) => {
const meta = getMetaByProperty(documentObj, item.property)
state[item.property] = {
existed: !!meta,
content: meta?.getAttribute('content') || '',
id: meta?.getAttribute('id') || '',
}
return state
}, {})
return originalOgMetaState
}
const ensureManagedMeta = (documentObj, item) => {
const head = getHead(documentObj)
if (!head) return null
// 优先按 property 查找,兼容原本就写在 index.html 里的静态节点。
// 找不到时再按约定 id 查找我们自己创建的节点。
let meta = getMetaByProperty(documentObj, item.property) || head.querySelector(`meta#${item.id}`)
if (!meta) {
meta = documentObj.createElement('meta')
const titleEl = head.querySelector('title')
if (titleEl) {
head.insertBefore(meta, titleEl)
} else if (head.firstChild) {
head.insertBefore(meta, head.firstChild)
} else {
head.appendChild(meta)
}
}
meta.setAttribute('id', item.id)
meta.setAttribute('property', item.property)
return meta
}
const resolveMetaSource = route => route?.meta?.og || route?.meta || {}
const resolveRouteTitle = (route, documentObj, { preferDocumentTitle = false } = {}) => {
const meta = route?.meta || {}
const documentTitle = documentObj?.title || ''
// 某些详情页会在异步数据返回后通过 useTitle 更新标题。
// 这种情况下优先读取 document.title,才能拿到页面最终标题,而不是路由默认标题。
if (preferDocumentTitle && documentTitle) {
return documentTitle
}
return meta.ogTitle || meta.title || documentTitle || ''
}
const createTitleObserver = (documentObj, onChange) => {
const titleEl = getHead(documentObj)?.querySelector('title')
if (!titleEl || typeof MutationObserver === 'undefined') {
return () => {}
}
const observer = new MutationObserver(() => {
onChange()
})
observer.observe(titleEl, {
childList: true,
subtree: true,
characterData: true,
})
return () => {
observer.disconnect()
}
}
/**
* 统一处理项目里的图片压缩规则。
* 目前仅对 cdn.ipadbiz.cn 做 imageMogr2 参数补充。
*/
export function buildOgImageUrl(src) {
if (!src) return ''
if (src.includes('cdn.ipadbiz.cn')) {
const compressParam = 'imageMogr2/thumbnail/400x/strip/quality/70'
if (src.includes('?')) {
return src.includes(compressParam) ? src : `${src}&${compressParam}`
}
return `${src}?${compressParam}`
}
return src
}
/**
* 写入或更新 OG 标签。
*
* 注意:
* - 所有标签都走统一入口,避免各页面各自 append/remove。
* - description 也会被统一接管,这样恢复逻辑才是对称的。
*/
export function applyOgMeta(payload = {}, { documentObj = document, windowObj = window } = {}) {
const head = getHead(documentObj)
if (!head) return
captureOriginalOgMetaState(documentObj)
const normalizedPayload = {
title: payload.title || documentObj?.title || '',
// description 没传时退化为 title,至少保证分享卡片不是空文案。
description: payload.description || payload.title || documentObj?.title || '',
image: buildOgImageUrl(payload.image || ''),
url: payload.url || windowObj?.location?.href || '',
}
MANAGED_OG_TAGS.forEach(item => {
const meta = ensureManagedMeta(documentObj, item)
if (!meta) return
meta.setAttribute('content', normalizedPayload[item.key] || '')
})
}
/**
* 恢复接管前的 OG 状态。
*
* 规则:
* - 原本存在的节点恢复 content/id。
* - 原本不存在的节点直接删除。
*/
export function resetOgMeta({ documentObj = document } = {}) {
const head = getHead(documentObj)
if (!head || !originalOgMetaState) return
MANAGED_OG_TAGS.forEach(item => {
const snapshot = originalOgMetaState[item.property]
const meta =
getMetaByProperty(documentObj, item.property) || head.querySelector(`meta#${item.id}`)
if (!meta) return
if (snapshot?.existed) {
meta.setAttribute('property', item.property)
meta.setAttribute('content', snapshot.content || '')
if (snapshot.id) {
meta.setAttribute('id', snapshot.id)
} else {
meta.removeAttribute('id')
}
return
}
meta.parentNode?.removeChild(meta)
})
}
export function resetOgMetaManagerState() {
// 仅用于测试或明确需要重置管理器内部状态的场景。
originalOgMetaState = null
}
/**
* 从当前路由解析出应写入的 OG 数据。
*
* 支持两种写法:
* - meta.ogDescription / meta.ogImage / meta.ogTitle
* - meta.og = { description, image, title, url }
*/
export function resolveRouteOgMeta(
route,
{ documentObj = document, windowObj = window, preferDocumentTitle = false } = {}
) {
const metaSource = resolveMetaSource(route)
const title = resolveRouteTitle(route, documentObj, { preferDocumentTitle })
return {
title,
description: metaSource.description || metaSource.ogDescription || title,
image: metaSource.image || metaSource.ogImage || '',
url: metaSource.url || metaSource.ogUrl || windowObj?.location?.href || '',
}
}
const mergeResolvedOgMeta = (
basePayload,
resolvedPayload,
documentObj,
{ preferDocumentTitle = false } = {}
) => {
const mergedPayload = {
...basePayload,
...(resolvedPayload || {}),
}
if (resolvedPayload?.desc && !resolvedPayload?.description) {
mergedPayload.description = resolvedPayload.desc
}
if (resolvedPayload?.imgUrl && !resolvedPayload?.image) {
mergedPayload.image = resolvedPayload.imgUrl
}
if (resolvedPayload?.link && !resolvedPayload?.url) {
mergedPayload.url = resolvedPayload.link
}
if (preferDocumentTitle && documentObj?.title) {
mergedPayload.title = documentObj.title
}
return mergedPayload
}
/**
* 把 OG 同步器挂到 router 上。
*
* 同步时机有两个:
* 1. 路由切换完成后:更新当前页面的默认 OG。
* 2. document.title 发生变化时:把异步详情页最终标题同步回 OG。
*/
export function installOgMetaSync(
router,
{ documentObj = document, windowObj = window, resolver } = {}
) {
if (!router || !documentObj) {
return () => {}
}
let syncToken = 0
const buildPayload = async (route, { preferDocumentTitle = false } = {}) => {
const basePayload = resolveRouteOgMeta(route, { documentObj, windowObj, preferDocumentTitle })
const resolvedPayload =
typeof resolver === 'function'
? await resolver(route, { documentObj, windowObj, preferDocumentTitle })
: null
return mergeResolvedOgMeta(basePayload, resolvedPayload, documentObj, { preferDocumentTitle })
}
const syncFromRoute = async (route, options = {}) => {
const currentToken = ++syncToken
const payload = await buildPayload(route, options)
if (currentToken !== syncToken) {
return
}
applyOgMeta(payload, {
documentObj,
windowObj,
})
}
const syncFromDocumentTitle = () => {
// 这里显式偏向 document.title,确保课程详情、学习详情这类异步标题能覆盖路由默认标题。
syncFromRoute(router.currentRoute?.value, {
preferDocumentTitle: true,
})
}
const removeAfterEach = router.afterEach?.(to => {
syncFromRoute(to)
})
const stopObserveTitle = createTitleObserver(documentObj, syncFromDocumentTitle)
// 首屏也立即同步一次,避免只有路由跳转后才有 OG。
syncFromDocumentTitle()
return () => {
stopObserveTitle()
removeAfterEach?.()
resetOgMeta({ documentObj })
}
}