前端加载优化:代码分割 + 按需懒加载

chat.js 首屏 308KB → 100KB(↓68%)
44 个重型模块改为 Vite 动态 import()
Alpine 组件通过 $watch 监听实现真懒加载
新增 createLazyAlpineComponent 工具 + Proxy has 陷阱修复
补充 userCardComponent 全部 28 个属性默认值
vendor 依赖独立分包(108KB)
生产环境关闭 sourcemap
This commit is contained in:
pllx
2026-04-28 09:38:18 +08:00
parent e8b4dcc968
commit e50502d8f6
5 changed files with 1507 additions and 729 deletions
+664 -729
View File
File diff suppressed because it is too large Load Diff
+185
View File
@@ -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);
},
});
};
}