116 lines
4.6 KiB
TypeScript
116 lines
4.6 KiB
TypeScript
/**
|
||
* 淘宝/天猫 SSR 状态提取 —— 通过 MAIN world 桥读取页面全局变量。
|
||
*
|
||
* 候选键(selectors-taobao.md §5.3 列出的待评估项):
|
||
* __ICE_APP_CONTEXT__ ICE 框架上下文(loaderData.home.data.res,v1 已知结构)★主
|
||
* __general_skupanel_cache_data 疑似完整 SKU 面板缓存(结构未知,容错深搜)
|
||
* __ICE_DATA_LOADER__ ICE 框架数据层(容错深搜)
|
||
*
|
||
* 输出对齐 ssr.ts 的 SSRData,供 buildFromSSR 消费。
|
||
*/
|
||
import type { SSRData } from './ssr';
|
||
|
||
/** 从 __ICE_APP_CONTEXT__ 结构映射(原 v1 extractSSRData 的对象版) */
|
||
function fromIceContext(ctx: unknown): SSRData | null {
|
||
const res = (ctx as any)?.loaderData?.home?.data?.res;
|
||
if (!res?.item?.title || !res?.item?.itemId) return null;
|
||
const industryParams = res.plusViewVO?.industryParamVO;
|
||
const extensionParams = res.componentsVO?.extensionInfoVO?.infos?.find(
|
||
(i: any) => i.type === 'BASE_PROPS'
|
||
);
|
||
return {
|
||
item: {
|
||
title: res.item.title,
|
||
itemId: res.item.itemId,
|
||
images: res.item.images || [],
|
||
videos: res.item.videos,
|
||
},
|
||
skuBase: res.skuBase,
|
||
params: {
|
||
basicParamList: industryParams?.basicParamList || extensionParams?.items || [],
|
||
enhanceParamList: industryParams?.enhanceParamList || [],
|
||
},
|
||
price: res.componentsVO?.priceVO?.price || res.componentsVO?.priceVO?.extraPrice,
|
||
};
|
||
}
|
||
|
||
/** 容错:未知结构里深搜「SKU props 数组」(元素含 name + values/props 嵰 name/imageUrl) */
|
||
function skuPropsFromUnknown(root: unknown): SSRData['skuBase'] | null {
|
||
const candidates: any[] = [];
|
||
const collect = (node: unknown, depth = 0) => {
|
||
if (!node || typeof node !== 'object' || depth > 6 || candidates.length) return;
|
||
if (Array.isArray(node)) {
|
||
if (
|
||
node.length >= 1 && node.every((x: any) => x && typeof x === 'object' &&
|
||
typeof (x.prop ?? x.name) === 'string' && Array.isArray(x.values ?? x.props))
|
||
) { candidates.push(node.map((x: any) => ({
|
||
pid: String(x.pid ?? ''),
|
||
name: x.prop ?? x.name,
|
||
values: (x.values ?? x.props).map((v: any) => ({
|
||
vid: String(v.vid ?? ''),
|
||
name: v.name ?? v.valueName ?? '',
|
||
image: v.image ?? v.imageUrl,
|
||
})),
|
||
}))); return;
|
||
}
|
||
node.forEach(n => collect(n, depth + 1));
|
||
return;
|
||
}
|
||
for (const v of Object.values(node as Record<string, unknown>)) collect(v, depth + 1);
|
||
};
|
||
collect(root);
|
||
return candidates.length ? { props: candidates[0] } : null;
|
||
}
|
||
|
||
/** 容错:深搜参数数组(元素含 propertyName/valueName) */
|
||
function paramsFromUnknown(root: unknown): Array<{ propertyName: string; valueName: string }> {
|
||
const out: Array<{ propertyName: string; valueName: string }> = [];
|
||
const seen = new Set<string>();
|
||
const walk = (node: unknown, depth = 0) => {
|
||
if (!node || typeof node !== 'object' || depth > 6 || out.length > 60) return;
|
||
if (Array.isArray(node)) {
|
||
if (node.length >= 2 && node.every((x: any) => x && typeof x === 'object' &&
|
||
typeof x.propertyName === 'string' && typeof x.valueName === 'string')) {
|
||
for (const p of node) {
|
||
const k = `${p.propertyName}=${p.valueName}`;
|
||
if (!seen.has(k)) { seen.add(k); out.push({ propertyName: p.propertyName, valueName: p.valueName }); }
|
||
}
|
||
}
|
||
node.forEach(n => walk(n, depth + 1));
|
||
return;
|
||
}
|
||
for (const v of Object.values(node as Record<string, unknown>)) walk(v, depth + 1);
|
||
};
|
||
walk(root);
|
||
return out;
|
||
}
|
||
|
||
export function taobaoStateFromBridge(keys: Record<string, any>): SSRData | null {
|
||
// 主路径:ICE 上下文
|
||
const ice = fromIceContext(keys['__ICE_APP_CONTEXT__']);
|
||
if (ice) {
|
||
// 主路径缺 SKU 时用面板缓存补
|
||
if (!ice.skuBase?.props?.length) {
|
||
const fromCache = skuPropsFromUnknown(keys['__general_skupanel_cache_data'] ?? keys['__ICE_DATA_LOADER__']);
|
||
if (fromCache?.props?.length) ice.skuBase = fromCache;
|
||
}
|
||
return ice;
|
||
}
|
||
|
||
// 降级:只有面板缓存/数据层 —— 尽力拼一个最小 SSRData(标题给空,DOM 会补)
|
||
const cacheRoot = keys['__general_skupanel_cache_data'] ?? keys['__ICE_DATA_LOADER__'];
|
||
if (cacheRoot) {
|
||
const skuBase = skuPropsFromUnknown(cacheRoot);
|
||
const params = paramsFromUnknown(cacheRoot);
|
||
if (skuBase?.props?.length || params.length) {
|
||
return {
|
||
item: { title: '', itemId: '', images: [], videos: [] },
|
||
skuBase: skuBase ?? undefined,
|
||
params: { basicParamList: params, enhanceParamList: [] },
|
||
price: undefined,
|
||
};
|
||
}
|
||
}
|
||
return null;
|
||
}
|