ddb4c8175f
packages/embed-loader:手写零依赖加载器(esbuild bundle+minify→dist/v1/loader.js, IIFE target es2018 4.7KB)。自发现 EMBED_ORIGIN(currentScript 兜底扫描)、 data-slot 必填缺失静默 no-op、__photoliveEmbedV1 标记去重+同槽去重、 窄屏 <480px 强制 sm+默认收起、postMessage 双向白名单(source+origin 严格 校验,resize 夹取 60-640)、sessionStorage 收起记忆、零全局零 console 全静默。sync 脚本携 sha256 校验拷贝至 viewer publicDir(两份产物入库); src/params.js 为 loader/单测共用参数源(11 vitest 用例)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
80 lines
2.8 KiB
JavaScript
80 lines
2.8 KiB
JavaScript
// packages/embed-loader/src/params.js
|
||
// 纯函数参数模块:loader(esbuild 打包内联)与 vitest 单测共用的唯一参数源。
|
||
// 取值域与缺省严格对齐契约 EmbedSlotSettings(packages/contracts/openapi.yaml),
|
||
// 非法/缺省一律静默回退——loader 对宿主零报错(A11「不破坏宿主」)。
|
||
|
||
export const POSITION_VALUES = ['left', 'right'];
|
||
export const SIZE_VALUES = ['sm', 'md'];
|
||
export const LANG_VALUES = ['auto', 'zh-CN', 'en'];
|
||
|
||
export const DEFAULTS = {
|
||
position: 'right',
|
||
size: 'md',
|
||
intervalMs: 5000,
|
||
lang: 'auto',
|
||
collapsedDefault: false,
|
||
};
|
||
|
||
/** size → 宿主侧 wrapper 宽度(px)。sm 为窄屏降级档。 */
|
||
export const SIZE_WIDTH = { sm: 280, md: 340 };
|
||
|
||
export const INTERVAL_MIN = 3000;
|
||
export const INTERVAL_MAX = 30000;
|
||
|
||
/** resize 消息高度夹取范围(px),越界/非数值返回 null 表示忽略。 */
|
||
export const HEIGHT_MIN = 60;
|
||
export const HEIGHT_MAX = 640;
|
||
|
||
function oneOf(allowed, value, fallback) {
|
||
return allowed.indexOf(value) >= 0 ? value : fallback;
|
||
}
|
||
|
||
/** 轮播间隔(ms):解析失败取缺省,超界夹取到 [3000, 30000]。 */
|
||
export function normalizeInterval(value) {
|
||
const n = parseInt(value, 10);
|
||
if (isNaN(n)) return DEFAULTS.intervalMs;
|
||
return Math.min(INTERVAL_MAX, Math.max(INTERVAL_MIN, n));
|
||
}
|
||
|
||
/**
|
||
* 解析宿主 script 标签 data-* 配置(dataset 形如 {slot, lang, position, interval})。
|
||
* opts.narrow=true(viewport <480px)时强制 sm 尺寸 + 默认收起(计划评审缺口:移动端可用)。
|
||
* data-slot 缺失/空白 → slot 为 null,调用方须静默 no-op。
|
||
*/
|
||
export function parseEmbedConfig(dataset, opts) {
|
||
const ds = dataset || {};
|
||
const slot = typeof ds.slot === 'string' ? ds.slot.replace(/^\s+|\s+$/g, '') : '';
|
||
const narrow = !!(opts && opts.narrow);
|
||
return {
|
||
slot: slot || null,
|
||
lang: oneOf(LANG_VALUES, ds.lang, DEFAULTS.lang),
|
||
position: oneOf(POSITION_VALUES, ds.position, DEFAULTS.position),
|
||
intervalMs: normalizeInterval(ds.interval),
|
||
size: narrow ? 'sm' : oneOf(SIZE_VALUES, ds.size, DEFAULTS.size),
|
||
collapsedDefault: narrow ? true : DEFAULTS.collapsedDefault,
|
||
};
|
||
}
|
||
|
||
/** iframe src:{origin}/embed/{slot}?lang=&pos=&interval=(槽 token 编码防注入)。 */
|
||
export function buildEmbedUrl(origin, slot, config) {
|
||
return (
|
||
origin +
|
||
'/embed/' +
|
||
encodeURIComponent(slot) +
|
||
'?lang=' +
|
||
encodeURIComponent(config.lang) +
|
||
'&pos=' +
|
||
encodeURIComponent(config.position) +
|
||
'&interval=' +
|
||
String(config.intervalMs)
|
||
);
|
||
}
|
||
|
||
/** resize 高度夹取:非有限数值/缺失 → null(忽略该消息)。 */
|
||
export function clampHeight(value) {
|
||
if (value === null || value === undefined) return null;
|
||
const n = Number(value);
|
||
if (isNaN(n) || !isFinite(n)) return null;
|
||
return Math.min(HEIGHT_MAX, Math.max(HEIGHT_MIN, Math.round(n)));
|
||
}
|