Files
lucky_shop/vue.config.js
ZF sun 63789124f4 build(vue.config): 打包后自动打开目录增加错误捕获
新增try-catch捕获打开输出目录失败的异常,避免打包流程因目录打开失败中断,同时添加失败提示日志
2026-05-25 11:56:50 +08:00

380 lines
15 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.
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] 已加载');
// 确保 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) {}
}
// 输出 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) {
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 ?? '';
// 所有 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');
// 去掉 /* */ 和 // 注释(HBuilder 生成的 JSON 允许注释)
const clean = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
const manifest = JSON.parse(clean);
if (manifest?.h5?.router?.base) {
return manifest.h5.router.base.replace(/^\/+|\/+$/g, '').replace(/\//g, '.');
}
} catch (e) {
console.warn('[manifest] 读取 h5.router.base 失败:', e.message);
}
return '';
}
/**
* 自定义 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().toISOString() };
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 生产构建时注入
const lines = [`const __app_version = ${inlineVersion};`];
if (platform === 'mp-weixin' && 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();
// 在 </head> 前注入 script 标签
const scriptTag = `<script>var __app_version = ${inlineVersion};</script>`;
if (!html.includes('__app_version')) {
html = html.replace('</head>', `${scriptTag}\n</head>`);
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 === '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);
}
}
}
// 导出配置
module.exports = {
productionSourceMap: false,
configureWebpack: config => {
console.log('config.mode = ', config.mode);
if (config.mode === 'production') {
console.log('开始尝试配置极限压缩');
// 配置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 = {};
}
// 设置terserOptions - 极限压缩配置
config.optimization.minimizer[terserPluginIndex].options.terserOptions = {
compress: {
drop_console: true, // 去除所有console语句
drop_debugger: true, // 去除debugger语句
dead_code: true, // 移除死代码
unused: true, // 移除未使用的变量
toplevel: true, // 清理顶层作用域未使用的变量
passes: 5, // 增加压缩次数以获得更好的压缩效果
sequences: true, // 合并连续的变量声明
evaluate: true, // 提前计算常量表达式
reduce_vars: true, // 合并或移除变量
join_vars: true, // 合并变量声明
conditionals: true, // 优化条件表达式
comparisons: true, // 优化比较操作
booleans: true, // 优化布尔表达式
typeofs: true, // 优化typeof操作
collapse_vars: true, // 折叠定义后不再修改的变量
reduce_funcs: true, // 合并或移除未使用的函数
inline: true, // 内联简单函数
side_effects: true, // 移除有副作用的代码
keep_fargs: false, // 移除未使用的函数参数
keep_fnames: false, // 不保留函数名称
pure_funcs: ['console.log', 'console.warn', 'console.error', 'console.info'], // 标记为纯函数的console方法
pure_getters: true // 假设getter函数没有副作用
},
format: {
ascii_only: true, // 确保输出ASCII字符
comments: false, // 去除所有注释
beautify: false, // 不美化输出
ecma: 5, // 输出兼容ECMAScript 5
wrap_func_args: false, // 不包裹函数参数
bracketize: true, // 使用大括号包裹所有块
quote_style: 1 // 使用单引号
},
parse: {
ecma: 8, // 使用ECMAScript 8解析
bare_returns: true // 允许顶级return语句
},
mangle: {
toplevel: true, // 混淆顶层作用域的变量名
keep_fnames: false, // 不保留函数名称
keep_classnames: false // 不保留类名称
},
module: false, // 不是ES模块
sourceMap: false // 不生成source map
};
console.log('配置极限压缩完成');
}
}
}
// 所有模式下添加版本注入插件(适用于所有平台)
config.plugins.push(new VersionInjectPlugin());
}
};