const path = require('path'); const fs = require('fs'); const { execSync } = require('child_process'); const { zionUniMpLoadPackagePlugin } = require('zion-uniapp-mp-load-package/webpack'); console.log('[vue.config] 已加载'); // Build-time: 给所有 page 注入 diy componentPlaceholder // 见 scripts/inject-placeholder.js require('./scripts/inject-placeholder.js'); // 确保 UNI_APP_DOMAIN 有值(从 package.json 脚本配置中读取) if (!process.env.UNI_APP_DOMAIN && process.env.UNI_SCRIPT) { try { const pkg = require('./package.json'); const scriptCfg = pkg['uni-app']?.scripts?.[process.env.UNI_SCRIPT]; if (scriptCfg?.env?.UNI_APP_DOMAIN) { process.env.UNI_APP_DOMAIN = scriptCfg.env.UNI_APP_DOMAIN; } } catch (e) { } } // 输出当前构建的平台 console.log(`🏠 当前构建的平台: [${process.env.UNI_PLATFORM || '(未知)'}]\n`); // 输出 H5 域名信息到控制台 if (process.env.UNI_PLATFORM === 'h5') { console.log(`\n[H5 发行] 脚本: ${process.env.UNI_SCRIPT || '(默认)'}`); console.log(`[H5 发行] 域名: ${process.env.UNI_APP_DOMAIN || '(未配置)'}\n`); } // 打印出process.env // for (let key in process.env) { // console.log(key, process.env[key]); // } // 定义UNI_OUTPUT_DIR环境变量(H5 自定义发行脚本使用/及其他构建处理) try { if (!process.env.NODE_ENV) { console.error(`❌ NODE_ENV 环境变量未设置`); throw new Error('NODE_ENV 环境变量未设置'); } const buildDir = process.env.NODE_ENV === 'production' ? 'build' : 'dev'; const platform = process.env.UNI_PLATFORM || ''; const UNI_SCRIPT = process.env.UNI_SCRIPT ?? ''; // 针对鸿蒙元服务的发布,先清理构建输出目录 if (platform == 'mp-harmony') { try { const assetDir = path.join(__dirname, `unpackage/dist/${buildDir}/.mp-harmony`); console.log(`🧹 清理构建目录 ${assetDir}\n`); fs.rmSync(assetDir, { recursive: true, force: true }); } catch (e) {} } // 所有 H5 自定义脚本统一处理,无需在 vue.config.js 中硬编码脚本名 if (platform === 'h5' && UNI_SCRIPT) { // 从 manifest.json 读取 h5.router.base,拼接到输出路径 let baseSuffix = ''; try { const manifestPath = path.join(__dirname, 'manifest.json'); baseSuffix = _readManifestBase(manifestPath); } catch (e) { } // 从脚本 env 中获取域名(优先使用 UNI_APP_DOMAIN,否则从 UNI_SCRIPT 提取) let domain = ''; if (process.env.UNI_APP_DOMAIN) { try { domain = new URL(process.env.UNI_APP_DOMAIN).hostname; } catch (e) { domain = process.env.UNI_APP_DOMAIN.replace(/^https?:\/\//, '').replace(/\/.*$/, ''); } } else { domain = UNI_SCRIPT.replace(/^h5-/, ''); } // 格式: {域名}-{base用.连接} const outputDirName = baseSuffix ? `${domain}-${baseSuffix}` : domain; process.env.UNI_OUTPUT_DIR = path.join(__dirname, `unpackage/dist/${buildDir}/h5/${outputDirName}`); console.log('UNI_OUTPUT_DIR = ', process.env.UNI_OUTPUT_DIR); } } catch (e) { console.log('NODE_ENV 环境变量未设置,默认使用开发模式'); } /** * 读取 manifest.json 中的 h5.router.base,去掉注释后解析 * 返回 base 路径的 . 连接形式(如 /hwappx/common/ → hwappx.common) */ function _readManifestBase(manifestPath) { try { const raw = fs.readFileSync(manifestPath, 'utf8'); const clean = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, ''); const manifest = JSON.parse(clean); if (manifest?.h5?.router?.base) { let base = manifest.h5.router.base; if (!base.endsWith('/')) { console.warn(`[manifest] h5.router.base "${base}" 不规范,已自动补全 /`); base = base + '/'; } return base.replace(/^\/+|\/+$/g, '').replace(/\//g, '.'); } } catch (e) { console.warn('[manifest] 读取 h5.router.base 失败:', e.message); } 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; } } /** * 读取 themeConfig.overrideRemote.source, 用于构建期判断是否启用 style_color 排除策略 * remote-only 模式: style_color.js 替换为 empty-style-stub.js(只含 emergency fallback) * 节省主包 ~75KB */ function _readThemeSource() { try { const content = fs.readFileSync(path.join(__dirname, 'common/js/config-external.js'), 'utf-8'); // 找到 overrideRemote 块中的 source 字段(可能有多个 source 字段,取 overrideRemote 块内的) const orBlock = content.match(/overrideRemote\s*:\s*\{([\s\S]*?)\n\s*\}/); if (!orBlock) return null; const match = orBlock[1].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(每次重编译都会自动注入) * 适用于所有平台(mp-weixin / h5 等) */ class VersionInjectPlugin { constructor() { this._cachedCommitId = null; } apply(compiler) { compiler.hooks.emit.tapAsync('VersionInjectPlugin', (compilation, callback) => { const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development'; const platform = process.env.UNI_PLATFORM || ''; // 获取 Git 提交 ID(缓存,避免每次 HMR 都执行) if (!this._cachedCommitId) { try { this._cachedCommitId = execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim(); } catch (e) { this._cachedCommitId = 'unknown'; } } const versionData = { commitId: this._cachedCommitId, buildTime: new Date(new Date().getTime() + 8 * 3600 * 1000).toISOString().slice(0, 16).replace('T', ' ') }; const inlineVersion = JSON.stringify(versionData); // 尝试注入到 vendor.js(mp-weixin / mp-* 等平台) const vendorKey = 'common/vendor.js'; const vendorAsset = compilation.assets[vendorKey]; if (vendorAsset) { let content = typeof vendorAsset.source === 'function' ? vendorAsset.source() : vendorAsset._value; if (typeof content !== 'string') content = content.toString(); // 清理旧注入 content = content.replace(/^\s*const\s+site\s*=\s*require\s*\(['"]\.\.\/site(\.js)?['"]\);?\s*\r?\n/gm, ''); content = content.replace(/^\s*const\s+__app_version\s*=\s*(require\s*\(['"]\.\.\/version(\.json|\.js)?['"]\)|.+);?\s*\r?\n/gm, ''); // 只注入版本信息(通用),site.js require 仅在 mp-weixin/mp-harmony 生产构建时注入 const lines = [`const __app_version = ${inlineVersion};`]; if ((platform === 'mp-weixin' || platform === 'mp-harmony') && mode === 'production') { lines.unshift('const site = require("../site.js");'); } const eol = content.includes('\r\n') ? '\r\n' : '\n'; let prefix = ''; if (content.charCodeAt(0) === 0xfeff) { prefix = '\uFEFF'; content = content.slice(1); } content = prefix + lines.join(eol) + eol + content; compilation.assets[vendorKey] = { source: () => content, size: () => content.length }; console.log(`[VersionInject] ✅ vendor.js 已注入 (platform=${platform}, mode=${mode}, commit=${this._cachedCommitId})`); } else { // H5 等平台可能没有 common/vendor.js,改为尝试注入到 index.html // #ifdef H5 const htmlKey = 'index.html'; const htmlAsset = compilation.assets[htmlKey]; if (htmlAsset) { let html = typeof htmlAsset.source === 'function' ? htmlAsset.source() : htmlAsset._value; if (typeof html !== 'string') html = html.toString(); // 在 前注入 script 标签 const scriptTag = ``; if (!html.includes('__app_version')) { html = html.replace('', `${scriptTag}\n`); compilation.assets[htmlKey] = { source: () => html, size: () => html.length }; console.log(`[VersionInject] ✅ index.html 已注入 (platform=${platform}, commit=${this._cachedCommitId})`); } } // #endif } callback(); }); // 生产构建后处理:mp-weixin 拷贝 site.js + ZIP;H5 自动打包 ZIP if (process.env.NODE_ENV === 'production') { compiler.hooks.afterEmit.tapAsync('ProductionZipPack', (compilation, callback) => { const platform = process.env.UNI_PLATFORM || ''; const UNI_SCRIPT = process.env.UNI_SCRIPT ?? ''; if (platform === 'mp-weixin') { console.log('\n🚀 mp-weixin 生产构建完成,运行补丁脚本(site.js 拷贝 + ZIP 打包)...'); try { execSync(`"${process.execPath}" scripts/mp-weixin.patch.js --mode production`, { cwd: __dirname, stdio: 'inherit' }); } catch (e) { console.error('补丁脚本执行失败:', e.message); } } else if (platform === 'mp-harmony') { console.log('\n🚀 mp-harmony 生产构建完成,运行补丁脚本(配置拷贝 + ZIP 打包)...'); try { execSync(`"${process.execPath}" scripts/harmony.patch.js --mode production`, { cwd: __dirname, stdio: 'inherit' }); } catch (e) { console.error('补丁脚本执行失败:', e.message); } // 拷贝构建产物到 ascf_src console.log('\n🚀 mp-harmony 生产构建完成,拷贝产物到 ascf_src 目录...'); this._copyMpHarmonyBuild(); } else if (platform === 'h5' && UNI_SCRIPT) { this._zipH5Build(compilation); } callback(); }); } } _computeH5OutputName() { // 计算输出目录名: {域名}-{base用.连接} let baseSuffix = _readManifestBase(path.join(__dirname, 'manifest.json')); let domain = ''; if (process.env.UNI_APP_DOMAIN) { try { domain = new URL(process.env.UNI_APP_DOMAIN).hostname; } catch (e) { domain = process.env.UNI_APP_DOMAIN.replace(/^https?:\/\//, '').replace(/\/.*$/, ''); } } else { domain = (process.env.UNI_SCRIPT || '').replace(/^h5-/, ''); } const result = baseSuffix ? `${domain}-${baseSuffix}` : domain; console.log('[H5Zip] 输出目录名:', result); return result; } _zipH5Build(compilation) { const outputDirName = this._computeH5OutputName(); const buildDir = process.env.NODE_ENV === 'production' ? 'build' : 'dev'; const h5Dir = path.join(__dirname, 'unpackage', 'dist', buildDir, 'h5'); // 实际构建输出目录(由 webpack 决定) const actualOutput = compilation.compiler.outputPath; const targetDir = path.join(h5Dir, outputDirName); // 如果路径一致,跳过重命名 if (actualOutput !== targetDir) { try { if (fs.existsSync(targetDir)) { fs.rmSync(targetDir, { recursive: true }); } fs.renameSync(actualOutput, targetDir); console.log(`[H5Zip] 输出目录已重命名为: ${outputDirName}`); } catch (e) { console.error('[H5Zip] 目录重命名失败:', e.message); return; } } else { console.log(`[H5Zip] 输出目录名正确: ${outputDirName}`); } // 标准时区时间戳格式: 20260511_1058 const fmt = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }); const parts = fmt.formatToParts(new Date()); const get = (t) => parts.find(p => p.type === t).value; const ts = `${get('year')}${get('month')}${get('day')}_${get('hour')}${get('minute')}`; const zipFileName = `${outputDirName}-${this._cachedCommitId}-${ts}.zip`; const zipFilePath = path.join(h5Dir, zipFileName); console.log(`\n📦 H5 ZIP: ${zipFileName}...`); let command; const sourcePath = targetDir.replace(/'/g, "''"); const destPath = zipFilePath.replace(/'/g, "''"); if (process.platform === 'win32') { command = `powershell -Command "Compress-Archive -Path '${sourcePath}\\*' -DestinationPath '${destPath}' -Force"`; } else { const sourceBase = path.dirname(targetDir); const sourceName = path.basename(targetDir); command = `cd '${sourceBase}' && zip -r '${zipFilePath}' '${sourceName}' -x '*/node_modules/*' '*.git/*' '*.DS_Store'`; } try { execSync(command, { stdio: 'inherit' }); console.log(`✅ H5 ZIP: ${zipFileName}`); try { const openDir = h5Dir.replace(/'/g, "''"); if (process.platform === 'win32') { execSync(`explorer "${openDir}"`, { stdio: 'ignore' }); } else { execSync(`open "${openDir}"`, { stdio: 'ignore' }); } } catch (openErr) { console.warn(`⚠️ 打开目录失败(不影响 ZIP 打包): ${openErr.message}`); } // 删除已打包的源目录 // fs.rmSync(targetDir, { recursive: true, force: true }); // console.log(`[H5Zip] 已删除源目录: ${outputDirName}`); } catch (e) { console.error('H5 ZIP 打包失败:', e.message); } } _copyMpHarmonyBuild() { const sourceDir = path.join(__dirname, 'unpackage', 'dist', 'build', '.mp-harmony'); const destDir = path.join(__dirname, 'stub-app-projects', 'mp-harmony', 'ascf', 'ascf_src'); console.log(`\n📋 拷贝鸿蒙构建产物...`); console.log(` 源: ${sourceDir}`); console.log(` 目标: ${destDir}`); // 检查源目录是否存在 if (!fs.existsSync(sourceDir)) { console.error(`❌ 源目录不存在: ${sourceDir}`); throw new Error(`鸿蒙构建产物目录不存在: ${sourceDir}`); } try { // 清空目标目录(失败不影响后续拷贝) if (fs.existsSync(destDir)) { try { fs.rmSync(destDir, { recursive: true, force: true }); if (fs.existsSync(destDir)) { fs.rmSync(destDir, { recursive: true, force: true }); } console.log(` ✅ 已清空目标目录 ${destDir}`); } catch (cleanErr) { console.warn(` ⚠️ 清空目标目录 ${destDir} 失败,将尝试覆盖: ${cleanErr.message}`); } } // 创建目标目录 fs.mkdirSync(destDir, { recursive: true }); // 拷贝函数 const copyDir = (src, dest) => { if (!fs.existsSync(src)) return; fs.mkdirSync(dest, { recursive: true }); const entries = fs.readdirSync(src, { withFileTypes: true }); for (const entry of entries) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { copyDir(srcPath, destPath); } else { fs.copyFileSync(srcPath, destPath); } } }; copyDir(sourceDir, destDir); console.log(' ✅ 拷贝完成'); } catch (e) { console.error(` ❌ 拷贝失败: ${e.message}`); throw e; } } } // 导出配置 module.exports = { 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'); 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'})`); } // style_color.js 排除: 仅在 themeConfig.overrideRemote.source === 'remote-only' 模式下生效 // 原理: config-external.js 的 loadTheme() 用 require('@/common/js/style_color.js') 同步加载 // 整个文件. 替换为 stub 后, 主包只包含 1 个 default preset (~5KB), 其他 7 个 preset // 由远端 /api/theme/presets 异步补齐. 节省主包 ~75KB. const themeSource = _readThemeSource(); if (themeSource === 'remote-only') { try { const webpack = require('webpack'); const stubPath = path.resolve(__dirname, 'common/js/empty-style-stub.js'); if (webpack?.NormalModuleReplacementPlugin) { config.plugins.push( new NormalModuleReplacementPlugin( // 精确匹配: 解析后的绝对路径是 .../common/js/style_color.js // 不能匹配 style_color 目录里其它可能存在的同名文件 /[\\/]common[\\/]js[\\/]style_color\.js$/, stubPath ) ); console.log(`[vue.config] ✅ style_color.js 已替换为 stub (source=remote-only) — 节省 ~75KB`); } } catch (e) { console.error('[vue.config] 配置 style_color 排除失败:', e.message); } } else { console.log(`[vue.config] style_color.js 保留 (source=${themeSource || 'unknown'})`); } if (config.mode === 'production') { console.log('开始尝试配置极限压缩'); if (process.env.UNI_PLATFORM === 'mp-weixin') { // 配置zion-uniapp-mp-load-package插件 config.plugins.push(new zionUniMpLoadPackagePlugin({ optimization: { minimize: true, moduleIds: 'named' }, packages: [ 'pages', 'pages_tool', 'pages_goods', 'pages_order', 'pages_promotion' ] })); } // 确保minimizer数组存在 if (config.optimization && config.optimization.minimizer && Array.isArray(config.optimization.minimizer)) { // 查找TerserWebpackPlugin实例,而不是依赖索引 const terserPluginIndex = config.optimization.minimizer.findIndex(plugin => { return plugin.constructor && plugin.constructor.name === 'TerserWebpackPlugin' || (plugin.options && plugin.options.terserOptions); }); if (terserPluginIndex !== -1) { // 确保options对象存在 if (!config.optimization.minimizer[terserPluginIndex].options) { config.optimization.minimizer[terserPluginIndex].options = {}; } // 区分平台配置压缩选项 const isMpHarmony = process.env.UNI_PLATFORM === 'mp-harmony'; // 基础压缩配置(通用) const terserOptions = { compress: { drop_console: !isMpHarmony, // 鸿蒙保留 console 用于调试 drop_debugger: !isMpHarmony, dead_code: true, unused: true, toplevel: true, passes: 10, sequences: true, evaluate: true, reduce_vars: true, join_vars: true, conditionals: true, comparisons: true, booleans: true, typeofs: true, collapse_vars: true, reduce_funcs: true, inline: true, side_effects: true, keep_fargs: false, keep_fnames: false, pure_funcs: isMpHarmony ? [] : ['console.log', 'console.warn', 'console.error', 'console.info'], pure_getters: true, unsafe_math: true, }, format: { ascii_only: true, comments: false, beautify: false, ecma: 5, wrap_func_args: false, bracketize: true, quote_style: 1 }, parse: { ecma: 8, bare_returns: true }, mangle: { toplevel: true, keep_fnames: false, keep_classnames: false }, module: false, sourceMap: false }; config.optimization.minimizer[terserPluginIndex].options.terserOptions = terserOptions; console.log('配置极限压缩完成'); } } } // 所有模式下添加版本注入插件(适用于所有平台) config.plugins.push(new VersionInjectPlugin()); } };