diff --git a/common/js/empty-lang-stub.js b/common/js/empty-lang-stub.js new file mode 100644 index 0000000..55aa1a2 --- /dev/null +++ b/common/js/empty-lang-stub.js @@ -0,0 +1,15 @@ +// ============================================================ +// 空 lang 包占位 +// +// 在 source === 'remote-only' 模式下, lang/ 目录下的所有 .js 会被 +// webpack.NormalModuleReplacementPlugin 替换为这个 stub. +// +// 设计要点: +// - 导出 default 对象, 结构与真实 lang 包一致: { lang: {...} } +// - lang 字段是空对象, 让 lang() 函数 fallback 路径不报错 +// - 不导出 named export, 避免 tree-shaking 把整个 stub 干掉 +// ============================================================ + +export default { + lang: {} +}; diff --git a/vue.config.js b/vue.config.js index d956438..5b19f59 100644 --- a/vue.config.js +++ b/vue.config.js @@ -90,6 +90,66 @@ function _readManifestBase(manifestPath) { return ''; } +/** + * 读取 langConfig.source, 用于构建期判断是否启用 lang/ 排除策略 + * 用 fs 静态解析, 避免 webpack 把整个 config-external.js 链入 vue.config 编译路径 + */ +function _readLangSource() { + try { + const content = fs.readFileSync(path.join(__dirname, 'common/js/config-external.js'), 'utf-8'); + const match = content.match(/source:\s*['"]([\w-]+)['"]/); + return match ? match[1] : null; + } catch (e) { + return null; + } +} + +/** + * 通用Webpack的资源替换插件 + */ +class NormalModuleReplacementPlugin { + constructor(resourceRegExp, newResource) { + this.resourceRegExp = resourceRegExp; + this.newResource = newResource; + } + + apply(compiler) { + const resourceRegExp = this.resourceRegExp; + const newResource = this.newResource; + + compiler.hooks.normalModuleFactory.tap( + "NormalModuleReplacementPlugin", + nmf => { + nmf.hooks.beforeResolve.tap("NormalModuleReplacementPlugin", result => { + if (!result) return; + if (resourceRegExp.test(result.request)) { + if (typeof newResource === "function") { + newResource(result); + } else { + result.request = newResource; + } + } + return result; + }); + nmf.hooks.afterResolve.tap("NormalModuleReplacementPlugin", result => { + if (!result) return; + if (resourceRegExp.test(result.resource)) { + if (typeof newResource === "function") { + newResource(result); + } else { + result.resource = path.resolve( + path.dirname(result.resource), + newResource + ); + } + } + return result; + }); + } + ); + } +} + /** * 自定义 webpack 插件:在通用产物(vendor.js)中内联注入版本数据 * 利用 emit 钩子在资源写入磁盘前处理,支持 HMR(每次重编译都会自动注入) @@ -352,9 +412,36 @@ class VersionInjectPlugin { // 导出配置 module.exports = { - productionSourceMap: false, - configureWebpack: config => { - console.log('config.mode = ', config.mode); + productionSourceMap: false, + configureWebpack: config => { + console.log('config.mode = ', config.mode); + + // lang/ 目录排除: 仅在 remote-only 模式下生效 + // 原理: webpack 把 require(`@/lang/${lang}/${path}.js`) 解析为 ContextModule, + // 会扫描并打包 lang/ 下所有 .js. 替换为 stub 后, 真正的 lang 文件不进 bundle. + // 条件: source === 'remote-only' (由 common/js/config-external.js 配置) + const langSource = _readLangSource(); + if (langSource === 'remote-only') { + try { + const webpack = require('webpack'); + const stubPath = path.resolve(__dirname, 'common/js/empty-lang-stub.js'); + console.log(`[vue.config] config.plugins.length = ${config.plugins.length}`); + if (webpack?.NormalModuleReplacementPlugin){ + config.plugins.push( + new NormalModuleReplacementPlugin( + // 匹配解析后的绝对路径: .../lang/xxx/yyy.js + /[\\/]lang[\\/].+\.js$/, + stubPath + ) + ); + console.log(`[vue.config] ✅ lang/ 目录已替换为 stub (source=remote-only) — 节省 lang 包体积`); + } + } catch (e) { + console.error('[vue.config] 配置 lang/ 排除失败:', e.message); + } + } else { + console.log(`[vue.config] lang/ 目录保留 (source=${langSource || 'unknown'})`); + } if (config.mode === 'production') { console.log('开始尝试配置极限压缩');