refactor: 优化项目配置与代码冗余清理

1. 新增components-diy占位组件与注入脚本,按需注入分包占位配置
2. 开启postcss编译优化
3. 移除多处重复的图标字体全局样式导入
This commit is contained in:
2026-07-09 15:13:06 +08:00
parent a7dd0b302e
commit 94be20d499
14 changed files with 1691 additions and 44 deletions

View File

@@ -0,0 +1,333 @@
/**
* Build-time: 按需注入 diy componentPlaceholder 占位
*
* 设计原则: 局部文本替换, 不破坏 pages.json 原有结构
* ============================================================
* - 不调用 JSON.parse / JSON.stringify (它们会丢失注释和预处理指令)
* - 不剥离 // 注释
* - 不剥离 // #ifdef / // #ifndef / // #endif 等 uni-app 预处理指令
* - 只对每个 page 的 "style": { ... } 块做局部修改
* - 块外的所有内容 (注释, 预处理, 缩进, 换行) 保持原样
*
* 工作流 (按需注入, 避免 vendor.js 膨胀):
* 1. 扫描 components-diy/ 目录, 收集所有 diy-*.vue 组件名 (单一数据源: 文件系统)
* 2. 逐行扫描 pages.json, 跟踪每个 page 的完整路径:
* - 顶层 pages[] 里的 page: 路径 = path (如 "pages/index/index")
* - subPackages[] 里的 page: 路径 = root + "/" + path (如 "pages_goods/category")
* 3. 对每个 "style": { ... } 块:
* a) 读取对应 .vue 文件, 检查是否包含 <diy- 引用
* b) **只有真正用 diy 组件的 page** 才注入/保留 componentPlaceholder
* c) 不用的 page: 若已有 placeholder 则移除 (含 _diy_placeholder 标记)
* d) 用的 page:
* - 已有 componentPlaceholder → 跳过
* - 有 _diy_placeholder 标记 → 展开为完整块
* - 都没有 → 注入 _diy_placeholder 标记 (带逗号处理)
* 4. 仅在文件有变化时写回
*
* 为什么按需注入: 给 113 个 page 全注入 54 项 placeholder, uni-app 会把所有 54 个
* diy 组件名打进 vendor.js, 每个出现 226 次, 让 vendor.js 膨胀 ~270KB.
* 只对真正用 <diy-*> 的 24 个 page 注入, vendor.js 维持 ~570KB, 主包减少 ~265KB.
*
* 调用时机: vue.config.js 顶部 (module.exports 之前) require 此文件自动执行.
*/
const path = require('path');
const fs = require('fs');
const PROJECT_ROOT = path.join(__dirname, '..');
const PAGES_JSON = path.join(PROJECT_ROOT, 'pages.json');
const DIY_DIR = path.join(PROJECT_ROOT, 'components-diy');
/**
* 扫描 components-diy/ 目录, 收集所有 diy-* 组件名.
* - 只匹配顶层 diy-*.vue 文件 (uni-app easycom 单文件组件)
* - 排除子目录 (css/ js/ placeholder/) 和其它文件 (minx.js 等)
* - 单一数据源: 文件系统, 无需手维护清单
*/
function collectDiyComponents() {
if (!fs.existsSync(DIY_DIR)) {
console.warn(`[InjectPlaceholder] ⚠️ ${DIY_DIR} 不存在, diy 组件清单为空`);
return [];
}
const entries = fs.readdirSync(DIY_DIR, { withFileTypes: true });
const names = [];
for (const e of entries) {
if (e.isFile() && e.name.startsWith('diy-') && e.name.endsWith('.vue')) {
names.push(e.name.slice(0, -'.vue'.length));
}
}
names.sort();
return names;
}
const DIY_LIST = collectDiyComponents();
/**
* 字符串感知的花括号计数.
* 跳过 // 行注释和 "" 字符串内的花括号.
* 返回该行净增的花括号深度.
*/
function netBraces(line) {
let d = 0;
let inStr = false;
let esc = false;
let i = 0;
while (i < line.length) {
const c = line[i];
if (inStr) {
if (esc) esc = false;
else if (c === '\\') esc = true;
else if (c === '"') inStr = false;
} else {
if (c === '/' && line[i + 1] === '/') break;
if (c === '"') inStr = true;
else if (c === '{') d++;
else if (c === '}') d--;
}
i++;
}
return d;
}
/**
* 在 body 中找最后一个 "有意义的" 行 (非空, 非纯注释).
*/
function lastMeaningfulIdx(bodyLines) {
for (let j = bodyLines.length - 1; j >= 0; j--) {
if (!/^\s*(\/\/.*)?$/.test(bodyLines[j])) return j;
}
return -1;
}
/**
* 检查 page 的 .vue 文件是否引用了 <diy-*> 组件.
* 简单的字符串检查, 包含 <diy- 即认为使用 (覆盖模板引用、动态组件等).
*/
function pageUsesDiy(fullPagePath) {
if (!fullPagePath) return false;
const vuePath = path.join(PROJECT_ROOT, fullPagePath + '.vue');
try {
const content = fs.readFileSync(vuePath, 'utf-8');
return /<diy-/.test(content);
} catch (e) {
return false; // 文件不存在时不注入
}
}
/**
* 从 body 中移除 componentPlaceholder 块和 _diy_placeholder 标记.
* 用于: page 之前注入了 placeholder, 但重构后不再使用 diy 组件.
* 自动修正尾逗号, 保证移除后 JSON 仍合法.
*/
function stripPlaceholderFromBody(bodyLines) {
let startIdx = -1, endIdx = -1, braceDepth = 0;
for (let i = 0; i < bodyLines.length; i++) {
const line = bodyLines[i];
if (startIdx === -1) {
const m = line.match(/^\s*"componentPlaceholder"\s*:\s*\{/);
if (m) {
startIdx = i;
const idx = line.indexOf('{', m.index);
for (let j = idx; j < line.length; j++) {
const c = line[j];
if (c === '{') braceDepth++;
else if (c === '}') braceDepth--;
}
if (braceDepth === 0) endIdx = i;
}
} else {
for (const c of line) {
if (c === '{') braceDepth++;
else if (c === '}') braceDepth--;
}
if (braceDepth === 0) { endIdx = i; break; }
}
}
const toRemove = new Set();
if (startIdx >= 0 && endIdx >= 0) {
for (let i = startIdx; i <= endIdx; i++) toRemove.add(i);
}
for (let i = 0; i < bodyLines.length; i++) {
if (/^\s*"_diy_placeholder"\s*:\s*true/.test(bodyLines[i])) toRemove.add(i);
}
if (toRemove.size === 0) return bodyLines;
const newBody = [];
for (let i = 0; i < bodyLines.length; i++) {
if (toRemove.has(i)) continue;
newBody.push(bodyLines[i]);
}
// 修正: 移除后, 新的最后有意义行不应有尾逗号
const lastIdx = lastMeaningfulIdx(newBody);
if (lastIdx >= 0) {
const line = newBody[lastIdx];
const commentMatch = line.match(/(\/\/.*)$/);
const comment = commentMatch ? commentMatch[1] : '';
const valuePart = line.substring(0, line.length - comment.length);
if (valuePart.trimEnd().endsWith(',')) {
newBody[lastIdx] = valuePart.trimEnd().replace(/,\s*$/, '') + comment;
}
}
return newBody;
}
/**
* 处理单个 style 块.
* needsPlaceholder=true: 注入/展开/保留 componentPlaceholder
* needsPlaceholder=false: 若已有 placeholder 则移除
* 返回 { lines, action } action: 'expand' | 'inject-marker' | 'remove' | 'skip' | 'no-change'
*/
function processStyleBlock(openLine, bodyLines, closeLine, needsPlaceholder) {
const styleIndent = (openLine.match(/^(\s*)/) || ['', ''])[1];
const innerIndent = styleIndent + '\t';
// 不需要 placeholder: 移除已有
if (!needsPlaceholder) {
const newBody = stripPlaceholderFromBody(bodyLines);
if (newBody.length === bodyLines.length) {
return { lines: [openLine, ...bodyLines, closeLine], action: 'no-change' };
}
return { lines: [openLine, ...newBody, closeLine], action: 'remove' };
}
// 需要 placeholder: 已存在 → 跳过
if (bodyLines.some(l => /^\s*"componentPlaceholder"\s*:/.test(l))) {
return { lines: [openLine, ...bodyLines, closeLine], action: 'skip' };
}
// 找 _diy_placeholder 标记, 展开
const markerIdx = bodyLines.findIndex(l => /^\s*"_diy_placeholder"\s*:\s*true/.test(l));
if (markerIdx >= 0) {
const tailCommaMatch = bodyLines[markerIdx].match(/^\s*"_diy_placeholder"\s*:\s*true\s*(,?)/);
const hasTailComma = tailCommaMatch && tailCommaMatch[1] === ',';
const newBody = [];
for (let j = 0; j < bodyLines.length; j++) {
if (j === markerIdx) {
newBody.push(`${innerIndent}"componentPlaceholder": {`);
// JSON 不允许尾逗号, 最后一个 diy 条目不能加 ","
for (let i = 0; i < DIY_LIST.length; i++) {
const name = DIY_LIST[i];
const isLast = i === DIY_LIST.length - 1;
newBody.push(`${innerIndent}\t"${name}": "view"${isLast ? '' : ','}`);
}
newBody.push(`${innerIndent}}` + (hasTailComma ? ',' : ''));
} else {
newBody.push(bodyLines[j]);
}
}
return { lines: [openLine, ...newBody, closeLine], action: 'expand' };
}
// 注入标记
const newBody = bodyLines.slice();
const lastIdx = lastMeaningfulIdx(newBody);
if (lastIdx === -1) {
newBody.push(`${innerIndent}"_diy_placeholder": true`);
} else {
const lastLine = newBody[lastIdx];
const commentMatch = lastLine.match(/(\/\/.*)$/);
const comment = commentMatch ? commentMatch[1] : '';
const valuePart = lastLine.substring(0, lastLine.length - comment.length);
if (!valuePart.trimEnd().endsWith(',')) {
newBody[lastIdx] = valuePart.trimEnd() + ',' + (comment ? ' ' + comment.trimStart() : '');
}
newBody.push(`${innerIndent}"_diy_placeholder": true`);
}
return { lines: [openLine, ...newBody, closeLine], action: 'inject-marker' };
}
function run() {
let raw;
try {
raw = fs.readFileSync(PAGES_JSON, 'utf-8');
} catch (e) {
console.error('[InjectPlaceholder] ❌ 读取 pages.json 失败:', e.message);
return false;
}
const EOL = raw.includes('\r\n') ? '\r\n' : '\n';
const lines = raw.split(/\r?\n/);
const out = [];
let i = 0;
let currentSubRoot = null; // 当前 subPackage 的 root, 顶层 page 时为 null
let currentPath = null; // 当前 page 的 path
let expanded = 0, added = 0, removed = 0, skipped = 0, noChange = 0;
while (i < lines.length) {
const line = lines[i];
// 跟踪 subPackage root
const rootMatch = line.match(/^\s*"root"\s*:\s*"([^"]+)"/);
if (rootMatch) {
currentSubRoot = rootMatch[1];
out.push(line);
i++;
continue;
}
// 跟踪 page path
const pathMatch = line.match(/^\s*"path"\s*:\s*"([^"]+)"/);
if (pathMatch) {
currentPath = pathMatch[1];
out.push(line);
i++;
continue;
}
// 处理 style 块
const styleMatch = line.match(/^(\s*)"style"\s*:\s*\{\s*,?\s*$/);
if (!styleMatch) {
out.push(line);
i++;
continue;
}
const openLine = line;
const blockStart = i;
let depth = 1;
i++;
while (i < lines.length && depth > 0) {
depth += netBraces(lines[i]);
i++;
}
const blockEnd = i;
if (depth !== 0) {
for (let j = blockStart; j < blockEnd; j++) out.push(lines[j]);
continue;
}
const blockLines = lines.slice(blockStart, blockEnd);
const closeLine = blockLines[blockLines.length - 1];
const bodyLines = blockLines.slice(1, -1);
// 关键: 按需判断. 读取 .vue 文件看是否真的用了 <diy-*>.
const fullPagePath = currentSubRoot
? `${currentSubRoot}/${currentPath}`
: currentPath;
const needsPlaceholder = pageUsesDiy(fullPagePath);
const result = processStyleBlock(openLine, bodyLines, closeLine, needsPlaceholder);
for (const l of result.lines) out.push(l);
if (result.action === 'expand') expanded++;
else if (result.action === 'inject-marker') added++;
else if (result.action === 'remove') removed++;
else if (result.action === 'skip') skipped++;
else if (result.action === 'no-change') noChange++;
}
const outText = out.join(EOL);
if (outText !== raw) {
try {
fs.writeFileSync(PAGES_JSON, outText, 'utf-8');
console.log(`[InjectPlaceholder] ✅ 展开 ${expanded}, 注入 ${added}, 移除 ${removed}, 跳过 ${skipped}, 无变化 ${noChange}, diy 组件 ${DIY_LIST.length}`);
} catch (e) {
console.error('[InjectPlaceholder] ❌ 写回 pages.json 失败:', e.message);
return false;
}
} else {
console.log(`[InjectPlaceholder] ✓ 无需改动, diy 组件 ${DIY_LIST.length}`);
}
return true;
}
run();