1. 调整manifest.json中router.base默认路径补全末尾斜杠 2. 在vue.config.js中增加base路径校验,自动补全缺失的末尾斜杠并打印警告
392 lines
16 KiB
JavaScript
392 lines
16 KiB
JavaScript
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');
|
||
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 '';
|
||
}
|
||
|
||
/**
|
||
* 自定义 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 === '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);
|
||
}
|
||
} 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: 10, // 增加压缩次数以获得更好的压缩效果
|
||
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函数没有副作用
|
||
unsafe_math: true, // 优化数学表达式如 Math.sin(3) → 预计算值
|
||
},
|
||
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());
|
||
}
|
||
}; |