Files
lucky_shop/common/js/config-external.js
ZF sun 82fd3839a9 feat: 新增remote-only主题模式,优化首屏包体积
1. 新增theme-override.js实现主题远端拉取逻辑
2. 新增empty-style-stub.js作为主题包占位符,减少主包~75KB
3. 在vue.config.js中添加构建期主题stub替换逻辑
4. 重构config-external.js的主题配置,新增remote-only模式支持
5. 重构store的applyThemeWithRemote方法,适配远端主题拉取
6. 在bootstrap中新增主题初始化和异步拉取逻辑
2026-07-07 14:56:13 +08:00

213 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 外置配置管理
// 用于管理需要外置的配置项,如语言包、主题配置等
// 语言包配置
export const langConfig = {
// 语言列表 — 与后端 Lang.php::overrides() 白名单对齐
// 增删需要后端同步改 LangOverrideModel::normalizeAppType 旁的 $allow_lang
langList: ['zh-cn', 'en-us'],
// 默认语言
defaultLang: 'zh-cn',
// 语言包加载方式:'lazy'(按需加载)或 'preload'(预加载)
loadMode: 'lazy',
// 数据源开关
// 数据源开关
// local : 纯本地包,忽略 remote
// remote : 本地兜底 + 后端 KV 覆写(首屏强依赖,见 bootTimeout)
// hybrid : 同 remote,后端不可用时用本地
// remote-only : 完全依赖后端 /api/lang/bundle,本地 lang/ 目录可删除
source: 'remote-only',
// 远端覆写相关
remote: {
overridesUrl: '/api/lang/overrides',
// bundle 接口(remote-only 模式下使用)
bundleUrl: '/api/lang/bundle',
// diff 接口(预留)
diffUrl: '/api/lang/diff',
// 首屏强依赖远端翻译(多租户文案强隔离),阻塞启动
// 但必须有上限避免白屏
// 2026-07-02: 从 2000 改为 5000 — 慢启动场景下,给首屏渲染更多时间
bootTimeout: 5000,
// 单次请求超时(ms)
// 2026-07-02: 从 4000 改为 60000 — 远端首次冷启动可能慢(CDN 边缘冷缓存/跨网段回源/MySQL JOIN)
// 配合 loadBundleTree 的"迟到数据补救"机制,即便超时也能拿到远端最终返回的数据
requestTimeout: 60000,
// 缓存有效期(秒),与后端 Cache::set 600s 对齐;过期后后台刷新
cacheTtl: 600,
// 本地缓存 key 前缀: <prefix><uniacid>_<appType>_<lang>
// 三维隔离,与 lang.js::getOverrideStorageKey / getOverrideMemKey 保持一致
storagePrefix: 'LANG_OVR_',
// 切租户/uniacid 时强制重拉
refreshOnUniacidChange: true,
// 签名校验密钥,留空不校验(需与后端 LANG_SIGN_SECRET 一致)
signSecret: '',
}
};
// 主题配置
export const themeConfig = {
// 主题列表 — 与 style_color.js 顶层的 preset 名严格一致(8 个)
// 关键: 这里列出的名字必须能在 style_color.js (或后端 style_color.php 镜像) 中找到对应 preset
// 历史上曾包含 'red' 但 style_color.js 从未定义 'red' preset, 切到 'red' 主题时
// loadTheme() 会返回 undefined → 走 stub default 兜底样式, 远端也无法补齐
// 2026-07-07 移除 'red' 以保持一致性
themeList: ['default', 'green', 'blue', 'pink', 'gold', 'purple', 'yellow', 'black'],
// 默认主题
defaultTheme: 'default',
// 远端 token 覆写相关(与 langConfig 同构)
// source : 'local' | 'remote' | 'hybrid' | 'remote-only'
// remote.* : 首屏拉取 + 缓存 + 三态机 + 多租户多端隔离
overrideRemote: {
overridesUrl: '/api/theme/overrides',
// 全量 preset 拉取(remote-only 模式下使用)
// 返回 8 个 preset 完整 token,替代本地 style_color.js
presetsUrl: '/api/theme/presets',
bootTimeout: 5000,
requestTimeout: 60000,
// 远端全量 preset 缓存 key 前缀
storagePresetsPrefix: 'THEME_PRESETS_',
storagePrefix: 'THEME_OVR_',
refreshOnUniacidChange: true,
// source: 'remote-only' 时,webpack 会用 NormalModuleReplacementPlugin
// 把 style_color.js 替换为 empty-style-stub.js(只含 emergency fallback)
// 节省主包 ~75KB(完整 8 preset × ~50 token = 80KB → stub 7KB)
//
// 与 lang/ 目录的 remote-only 模式完全同构:
// - 构建期: webpack 替换为 stub
// - 运行时: ThemeOverride.loadAllPresets() 异步拉 /api/theme/presets
// - 首屏: 走 stub 的 default preset,不白屏
// - 切换: 切到非 default preset 时,等远端拉完再应用(由 applyThemeWithRemote
// 订阅 theme:presets-updated 事件驱动)
source: 'remote-only',
}
};
// API配置
export const apiConfig = {
// API请求超时时间
timeout: 30000,
// 是否开启重试
retry: true,
// 重试次数
retryCount: 3,
// 重试间隔时间(毫秒)
retryDelay: 1000
};
// 配置外置管理类
class ConfigExternal {
constructor() {
this.loadedConfigs = {};
this.loadPromises = {};
}
/**
* 加载语言包
* @param {string} lang - 语言类型
* @returns {Promise} - 加载结果
*/
async loadLang(lang = langConfig.defaultLang) {
if (this.loadedConfigs[`lang_${lang}`]) {
return this.loadedConfigs[`lang_${lang}`];
}
if (this.loadPromises[`lang_${lang}`]) {
return this.loadPromises[`lang_${lang}`];
}
this.loadPromises[`lang_${lang}`] = new Promise((resolve, reject) => {
try {
// 动态加载语言包
const langData = require(`@/lang/${lang}/common.js`).lang;
this.loadedConfigs[`lang_${lang}`] = langData;
resolve(langData);
} catch (error) {
// console.error(`加载语言包 ${lang} 失败:`, error);
reject(error);
}
});
return this.loadPromises[`lang_${lang}`];
}
/**
* 加载页面语言包
* @param {string} lang - 语言类型
* @param {string} pagePath - 页面路径
* @returns {Promise} - 加载结果
*/
async loadPageLang(lang = langConfig.defaultLang, pagePath) {
const key = `page_${lang}_${pagePath}`;
if (this.loadedConfigs[key]) {
return this.loadedConfigs[key];
}
if (this.loadPromises[key]) {
return this.loadPromises[key];
}
this.loadPromises[key] = new Promise((resolve, reject) => {
try {
// 动态加载页面语言包
const pageLangData = require(`@/lang/${lang}/${pagePath}.js`).lang;
this.loadedConfigs[key] = pageLangData;
resolve(pageLangData);
} catch (error) {
console.error(`加载页面语言包 ${lang}/${pagePath} 失败:`, error);
resolve({});
}
});
return this.loadPromises[key];
}
/**
* 加载主题配置(异步方式)
* 关键: 在 remote-only 模式下, style_color.js 已被 webpack 替换为 empty-style-stub.js,
* 只含 1 个 default preset. 其它 7 个 preset 由 ThemeOverride.loadAllPresets()
* 异步拉取并合并到本地缓存. 这里的 loadTheme 优先从缓存取,缓存未命中时回退到 stub.
*
* @param {string} theme - 主题名称
* @returns {Promise} - 加载结果
*/
async loadTheme(theme = themeConfig.defaultTheme) {
// 1) 先查已加载的内存缓存(包括远端拉取后写入的)
if (this.loadedConfigs[`theme_${theme}`]) {
return this.loadedConfigs[`theme_${theme}`];
}
// 2) remote-only 模式下,从 ThemeOverride 内存缓存(由 loadAllPresets 写入)取
try {
const ThemeOverride = require('./theme-override.js').default;
const remotePresets = ThemeOverride.getAllPresets();
if (remotePresets && remotePresets[theme]) {
this.loadedConfigs[`theme_${theme}`] = remotePresets[theme];
return remotePresets[theme];
}
} catch (e) {
// ignore — ThemeOverride 不可用时继续走 require stub
}
// 3) 兜底: require style_color.js (local 模式全量, remote-only 模式 stub)
if (this.loadPromises[`theme_${theme}`]) {
return this.loadPromises[`theme_${theme}`];
}
this.loadPromises[`theme_${theme}`] = new Promise((resolve, reject) => {
try {
// 动态加载主题配置 (webpack 替换为 stub 时,只返回 1 个 default preset)
const themeData = require(`@/common/js/style_color.js`)['default'][theme];
this.loadedConfigs[`theme_${theme}`] = themeData;
resolve(themeData);
} catch (error) {
console.error(`加载主题 ${theme} 失败:`, error);
reject(error);
}
});
return this.loadPromises[`theme_${theme}`];
}
}
export default new ConfigExternal();