前端加载优化:代码分割 + 按需懒加载
chat.js 首屏 308KB → 100KB(↓68%) 44 个重型模块改为 Vite 动态 import() Alpine 组件通过 $watch 监听实现真懒加载 新增 createLazyAlpineComponent 工具 + Proxy has 陷阱修复 补充 userCardComponent 全部 28 个属性默认值 vendor 依赖独立分包(108KB) 生产环境关闭 sourcemap
This commit is contained in:
+664
-729
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 懒加载工具模块
|
||||
*
|
||||
* 提供按需动态导入辅助函数,支持:
|
||||
* 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);
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user