e50502d8f6
chat.js 首屏 308KB → 100KB(↓68%) 44 个重型模块改为 Vite 动态 import() Alpine 组件通过 $watch 监听实现真懒加载 新增 createLazyAlpineComponent 工具 + Proxy has 陷阱修复 补充 userCardComponent 全部 28 个属性默认值 vendor 依赖独立分包(108KB) 生产环境关闭 sourcemap
186 lines
7.7 KiB
JavaScript
186 lines
7.7 KiB
JavaScript
/**
|
||
* 懒加载工具模块
|
||
*
|
||
* 提供按需动态导入辅助函数,支持:
|
||
* 1. 首次加载时自动调用初始化函数(如 bind*Controls)
|
||
* 2. 包裹目标函数,实现 window.xxx = (...args) => 自动加载并调用
|
||
* 3. 模块缓存机制,避免重复加载
|
||
* 4. Alpine 组件懒加载:返回同步 stub,在 $watch 触发时异步加载真实组件
|
||
*/
|
||
|
||
/**
|
||
* 创建一个可延迟加载的模块引用。
|
||
*
|
||
* @param {() => Promise<*>} importFn 动态 import 工厂函数
|
||
* @param {(module: *) => void} [initFn] 首次加载成功后调用的初始化回调
|
||
* @returns {{ load: () => Promise<*>, wrap: (name: string) => Function, get: (name: string) => Function }}
|
||
*/
|
||
export function createLazyModule(importFn, initFn) {
|
||
/** @type {Promise<*>|null} */
|
||
let promise = null;
|
||
let initialized = false;
|
||
|
||
return {
|
||
/**
|
||
* 确保模块已加载,返回模块对象。
|
||
* @returns {Promise<*>}
|
||
*/
|
||
async load() {
|
||
if (!promise) {
|
||
promise = importFn().then((mod) => {
|
||
if (!initialized && initFn) {
|
||
initialized = true;
|
||
initFn(mod);
|
||
}
|
||
return mod;
|
||
});
|
||
}
|
||
return promise;
|
||
},
|
||
|
||
/**
|
||
* 创建一个延迟执行的目标函数包装器。
|
||
* 首次调用时自动加载模块,之后直接调用缓存。
|
||
*
|
||
* @param {string} fnName 模块中导出的函数名
|
||
* @returns {Function}
|
||
*/
|
||
wrap(fnName) {
|
||
return async (...args) => {
|
||
const mod = await this.load();
|
||
const fn = mod[fnName];
|
||
if (typeof fn !== "function") {
|
||
throw new Error(
|
||
`懒加载模块中找不到函数 "${fnName}"`,
|
||
);
|
||
}
|
||
return fn(...args);
|
||
};
|
||
},
|
||
|
||
/**
|
||
* 返回一个返回模块导出的 getter 函数。
|
||
* 适用于返回非函数值(如 Alpine 组件对象)。
|
||
*
|
||
* @param {string} name 模块中导出的名称
|
||
* @returns {Function}
|
||
*/
|
||
get(name) {
|
||
return async () => {
|
||
const mod = await this.load();
|
||
return mod[name];
|
||
};
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 创建 Alpine 组件延迟加载包装器。
|
||
*
|
||
* Alpine 的 x-data="ComponentName()" 要求工厂函数返回一个同步对象。
|
||
* 本函数返回一个函数,该函数:
|
||
* 1. 立即返回一个包含安全默认值的 stub 对象
|
||
* 2. 通过 Alpine 的 $watch 监听显示状态变化
|
||
* 3. 仅当组件变为可见(show/showUserModal 变为 true)时,才异步加载真实模块
|
||
* 4. 通过 Alpine 的响应式代理(this)写入真实数据,触发模板重新渲染
|
||
*
|
||
* 这实现了"真懒加载"——用户若不打开面板,对应代码块永远不会下载。
|
||
*
|
||
* @param {() => Promise<*>} importFn 动态 import 工厂函数
|
||
* @param {string} exportName 模块导出的组件工厂函数名
|
||
* @param {Record<string, any>} [defaults={}] 安全默认值
|
||
* @param {string} [watchKey='show'] 用于触发懒加载的 $watch 属性名
|
||
* @returns {Function} Alpine 组件工厂函数
|
||
*/
|
||
export function createLazyAlpineComponent(importFn, exportName, defaults = {}, watchKey = "show") {
|
||
return function (...args) {
|
||
const stub = {
|
||
[watchKey]: false,
|
||
...defaults,
|
||
init() {
|
||
const proxy = this;
|
||
let loaded = false;
|
||
|
||
// 使用 Alpine 的 $watch 监听显示状态变化
|
||
// 仅在组件变为可见时才加载真实模块
|
||
if (
|
||
watchKey in proxy &&
|
||
typeof proxy.$watch === "function"
|
||
) {
|
||
proxy.$watch(watchKey, (value, oldValue) => {
|
||
if (value && !loaded) {
|
||
loaded = true;
|
||
importFn()
|
||
.then((mod) => {
|
||
const componentFn = mod[exportName];
|
||
const realData =
|
||
typeof componentFn === "function"
|
||
? componentFn(...args)
|
||
: componentFn;
|
||
|
||
// 通过 Alpine 响应式代理写入所有属性
|
||
Object.keys(realData).forEach((key) => {
|
||
if (key !== "init") {
|
||
proxy[key] = realData[key];
|
||
}
|
||
});
|
||
|
||
// 处理继承属性
|
||
for (const key in realData) {
|
||
if (!(key in proxy)) {
|
||
proxy[key] = realData[key];
|
||
}
|
||
}
|
||
|
||
// 调用真实组件的 init(如果存在)
|
||
if (
|
||
typeof proxy.init === "function" &&
|
||
proxy.init !== stub.init
|
||
) {
|
||
proxy.init.call(proxy);
|
||
}
|
||
})
|
||
.catch((err) => {
|
||
console.error(
|
||
`[懒加载] Alpine 组件 "${exportName}" 加载失败:`,
|
||
err,
|
||
);
|
||
loaded = false; // 允许重试
|
||
});
|
||
}
|
||
});
|
||
}
|
||
},
|
||
};
|
||
|
||
// 使用 Proxy 包裹 stub,对任何未在 defaults 中定义的属性/方法提供安全兜底
|
||
// 这样组件模板中的所有表达式(方法调用、属性访问)都不会抛出 "not defined" 错误
|
||
return new Proxy(stub, {
|
||
get(target, prop, receiver) {
|
||
// 已存在的属性直接返回
|
||
if (prop in target) {
|
||
return Reflect.get(target, prop, receiver);
|
||
}
|
||
// 对于未定义的属性/方法,返回一个安全的、支持调用的兜底值
|
||
// - 作为值访问(如 targetMarriage?.status): 返回 '',保证 .status 不报错
|
||
// - 作为方法调用(如 displayAssetValue('exp_num')): 返回空字符串
|
||
const fallback = () => "";
|
||
return fallback;
|
||
},
|
||
// Alpine 使用 with(scope) 求值表达式,with 用 has (in 操作符) 判断属性是否存在
|
||
// 如果没有 has 陷阱,with 认为属性不存在 → 跳到 window → "is not defined"
|
||
has(target, prop) {
|
||
// 不要拦截 Alpine/Vue 内部属性和魔术方法
|
||
if (typeof prop === "symbol") return Reflect.has(target, prop);
|
||
if (String(prop).startsWith("__v_")) return Reflect.has(target, prop);
|
||
if (String(prop).startsWith("$")) return Reflect.has(target, prop);
|
||
// 所有其他属性都报告存在,让 with 继续用 get 获取兜底值
|
||
return true;
|
||
},
|
||
set(target, prop, value, receiver) {
|
||
return Reflect.set(target, prop, value, receiver);
|
||
},
|
||
});
|
||
};
|
||
}
|