build: add remote-only mode lang package stub and replacement logic

add empty-lang-stub.js to stub lang package files in remote-only mode,
add webpack plugin logic to replace lang files with stub to reduce bundle size
This commit is contained in:
2026-07-01 16:50:01 +08:00
parent 5cfee417be
commit 563ab9274f
2 changed files with 105 additions and 3 deletions

View File

@@ -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: {}
};

View File

@@ -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('开始尝试配置极限压缩');