17 KiB
17 KiB
install.js 安装脚本改进计划
概述
本文档记录 install.js 安装脚本的改进计划,涵盖 15 项改进,按优先级分为高/中/低三档。
一、高优先级改进
1. npm 依赖安装网络问题处理
问题描述:
- 直接执行
npm install,无重试机制 - 国内访问 npmjs.org 超时/速度慢
- 网络波动导致安装中断无反馈
改进方案:
1.1 自动检测并切换镜像源
async function detectAndSetRegistry() {
const registries = [
{ name: '淘宝镜像', url: 'https://registry.npmmirror.com' },
{ name: '腾讯镜像', url: 'https://mirrors.cloud.tencent.com/npm/' },
{ name: '华为镜像', url: 'https://repo.huaweicloud.com/repository/npm/' },
{ name: '官方源', url: 'https://registry.npmjs.org' },
];
for (const registry of registries.slice(0, 3)) {
const pingResult = await pingRegistry(registry.url);
if (pingResult.success && pingResult.latency < 3000) {
log.success(`使用 ${registry.name} (${pingResult.latency}ms)`);
return registry.url;
}
}
return null;
}
async function pingRegistry(url) {
const start = Date.now();
try {
await new Promise((resolve, reject) => {
const req = require('https').get(`${url}/-/ping`, (res) => {
if (res.statusCode === 200) resolve();
else reject(new Error('Not 200'));
});
req.on('error', reject);
req.setTimeout(3000, () => { req.destroy(); reject(new Error('Timeout')); });
});
return { success: true, latency: Date.now() - start };
} catch {
return { success: false, latency: Infinity };
}
}
1.2 重试机制与指数退避
async function installWithRetry(cwd, name, maxRetries = 3) {
const baseDelay = 2000;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
log.info(`安装 ${name} 依赖 (第 ${attempt}/${maxRetries} 次)...`);
const result = await runNpmInstall(cwd);
if (result.success) {
return { success: true };
}
const errorType = analyzeNpmError(result.error);
if (errorType === 'NETWORK_ERROR' && attempt < maxRetries) {
const delay = baseDelay * Math.pow(2, attempt - 1);
log.warning(`网络错误,${delay/1000} 秒后重试...`);
await new Promise(r => setTimeout(r, delay));
runCommand('npm cache clean --force', { cwd, silent: true });
} else if (attempt < maxRetries) {
await new Promise(r => setTimeout(r, baseDelay));
}
}
return { success: false };
}
function analyzeNpmError(error) {
const errorLower = (error || '').toLowerCase();
if (errorLower.includes('econnrefused') ||
errorLower.includes('econnreset') ||
errorLower.includes('etimedout') ||
errorLower.includes('network')) {
return 'NETWORK_ERROR';
}
return 'UNKNOWN_ERROR';
}
1.3 安装进度显示
function runNpmInstall(cwd) {
return new Promise((resolve) => {
const spawn = require('child_process').spawn;
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const child = spawn(npm, ['install', '--loglevel=warn'], {
cwd, shell: true, stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let lastProgress = '';
child.stdout.on('data', (data) => {
stdout += data;
const progressMatch = data.toString().match(/(\d+\/\d+|\d+%)/);
if (progressMatch) {
const progress = progressMatch[1];
if (progress !== lastProgress) {
process.stdout.write(`\r 安装进度: ${progress} `);
lastProgress = progress;
}
}
});
child.on('close', (code) => {
if (lastProgress) process.stdout.write('\n');
resolve({ success: code === 0, output: stdout });
});
setTimeout(() => { child.kill(); resolve({ success: false, error: '安装超时' }); }, 10 * 60 * 1000);
});
}
1.4 安装后验证
async function verifyDependencies(cwd, name) {
log.subStep(`验证 ${name} 依赖完整性...`);
const lsResult = runCommand('npm ls --depth=0 --json', { cwd, silent: true });
if (!lsResult.success) {
return { success: false, error: '依赖检查失败' };
}
try {
const deps = JSON.parse(lsResult.output);
if (deps.problems && deps.problems.length > 0) {
log.warning(`发现 ${deps.problems.length} 个依赖问题`);
return { success: false, problems: deps.problems };
}
return { success: true };
} catch {
return { success: false, error: '验证解析失败' };
}
}
实施位置:install.js 第 1782-1804 行 installDependencies() 函数
2. 健康检查跨平台兼容性
问题描述:
// 当前代码(Windows 无 curl)
execSync('curl -s -o /dev/null -w "%{http_code}" ...');
改进方案:
async function healthCheck() {
const http = require('http');
return new Promise((resolve) => {
const req = http.get(`http://localhost:${config.backendPort}/health`, (res) => {
resolve({ success: res.statusCode === 200, statusCode: res.statusCode });
});
req.on('error', () => resolve({ success: false }));
req.setTimeout(5000, () => { req.destroy(); resolve({ success: false }); });
});
}
实施位置:install.js 第 2039-2073 行 healthCheck() 函数
3. MySQL 密码安全传递
问题描述:
// 当前代码 - 密码暴露在命令行
execSync(`mysql -u root -p'${rootPassword}' -e "${sql}"`);
改进方案:
async function createMySQLDatabase(rootPassword) {
const createDbSql = 'CREATE DATABASE IF NOT EXISTS idc_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;';
try {
execSync(`mysql -u root -e "${createDbSql}"`, {
env: { ...process.env, MYSQL_PWD: rootPassword },
stdio: ['pipe', 'pipe', 'pipe']
});
log.success('数据库创建成功');
} catch (error) {
// 错误处理...
}
}
实施位置:install.js 第 1052-1086 行 createMySQLDatabase() 函数
4. 错误处理完善
问题描述:
- 错误后继续执行,导致级联失败
- 无重试选项
改进方案:
async function initDatabase() {
const initScript = path.join(__dirname, 'backend', 'scripts', 'init-database.js');
if (!fs.existsSync(initScript)) {
log.warning('未找到数据库初始化脚本,跳过');
return;
}
log.info('正在初始化数据库...');
const result = runCommand('node scripts/init-database.js', {
cwd: path.join(__dirname, 'backend')
});
if (!result.success) {
const retry = await ask('数据库初始化失败,是否重试? (Y/n)', 'Y');
if (retry.toLowerCase() === 'y') {
return await initDatabase();
}
throw new Error('Database initialization failed');
}
log.success('数据库初始化完成');
}
实施位置:install.js 第 1811-1833 行 initDatabase() 函数
5. 回滚机制
问题描述:
- 安装失败后无清理
- 残留半成品状态
改进方案:
const rollbackSteps = [];
async function installDependencies() {
try {
log.step('安装后端依赖...');
await installBackend();
rollbackSteps.push(() => runCommand('rmdir /s /q node_modules', { cwd: backendDir, shell: true }));
log.step('安装前端依赖...');
await installFrontend();
rollbackSteps.push(() => runCommand('rmdir /s /q node_modules', { cwd: frontendDir, shell: true }));
} catch (error) {
await rollback();
throw error;
}
}
async function rollback() {
log.warning('安装失败,正在回滚...');
for (const step of rollbackSteps.reverse()) {
try { await step(); } catch {}
}
}
实施位置:新增 rollbackSteps 数组和 rollback() 函数,在 main() 中调用
6. SQLite 编译依赖替换
已完成 ✅
| 文件 | 修改内容 |
|---|---|
backend/package.json |
sqlite3 → better-sqlite3 |
backend/db.js |
添加 dialectModule 配置 |
二、中优先级改进
7. 日志管理优化
问题描述:
- 每次安装生成新日志,无清理
- Windows 文件名不兼容冒号
改进方案:
function initLogFile() {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
// 清理 7 天前的旧日志
const oldLogs = fs.readdirSync(LOG_DIR)
.filter(f => f.startsWith('install_'))
.filter(f => {
const stat = fs.statSync(path.join(LOG_DIR, f));
return Date.now() - stat.mtimeMs > 7 * 24 * 60 * 60 * 1000;
});
oldLogs.forEach(f => fs.unlinkSync(path.join(LOG_DIR, f)));
// 使用安全的时间格式
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const logFileName = `install_${timestamp}.log`;
logFileStream = fs.createWriteStream(path.join(LOG_DIR, logFileName), { flags: 'a' });
// 重写 console.log 同时输出到文件和终端
const originalLog = console.log;
console.log = (...args) => {
const message = args.map(arg => String(arg)).join(' ');
logFileStream.write(`[${new Date().toISOString()}] ${message}\n`);
originalLog.apply(console, args);
};
}
实施位置:install.js 第 100-123 行 initLogFile() 函数
8. 配置持久化与恢复
问题描述:
- 每次安装需重新配置
改进方案:
const CONFIG_FILE = path.join(__dirname, '.install-config.json');
async function loadSavedConfig() {
if (!fs.existsSync(CONFIG_FILE)) return false;
const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
const useSaved = await ask('检测到上次配置,是否使用? (Y/n)', 'Y');
if (useSaved.toLowerCase() === 'y') {
Object.assign(config, saved);
return true;
}
return false;
}
function saveConfig() {
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
log.subStep('配置已保存');
}
// 在 confirmConfiguration() 成功后调用 saveConfig()
实施位置:新增 CONFIG_FILE 常量、loadSavedConfig() 和 saveConfig() 函数
9. 进度显示优化
问题描述:
- 只有文字输出,无进度条
改进方案:
const steps = [
{ name: '环境检测', fn: checkEnvironment },
{ name: '数据库配置', fn: configureDatabase },
{ name: '服务配置', fn: configureServices },
{ name: '安装依赖', fn: installDependencies },
{ name: '初始化数据库', fn: initDatabase },
{ name: '构建前端', fn: buildFrontend },
{ name: '启动服务', fn: startServices },
];
async function runWithProgress() {
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
const progress = `[${i + 1}/${steps.length}]`;
log.step(`${progress} ${step.name}`);
const startTime = Date.now();
await step.fn();
log.subStep(`耗时 ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
}
}
实施位置:新增 steps 数组和 runWithProgress() 函数
10. Nginx 版本动态获取
问题描述:
// 固定版本
const nginxUrl = 'https://nginx.org/download/nginx-1.24.0.zip';
改进方案:
async function getLatestNginxVersion() {
const https = require('https');
return new Promise((resolve) => {
https.get('https://nginx.org/en/download.html', (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const match = data.match(/nginx-(\d+\.\d+\.\d+)\.zip/);
resolve(match ? match[1] : '1.24.0');
});
}).on('error', () => resolve('1.24.0'));
});
}
async function autoInstallNginx() {
const version = await getLatestNginxVersion();
const nginxUrl = `https://nginx.org/download/nginx-${version}.zip`;
// 后续下载逻辑...
}
实施位置:install.js 第 1430-1483 行 autoInstallNginx() 函数
11. PM2 serve 本地化
问题描述:
// 全局安装
runCommand('npm install -g serve');
改进方案:
// 方案1:使用 npx(推荐)
const frontendResult = runCommand(
`npx serve -s dist -l ${config.frontendPort}`,
{ cwd: path.join(__dirname, 'frontend') }
);
// 方案2:本地安装
runCommand('npm install serve', { cwd: path.join(__dirname, 'frontend') });
实施位置:install.js 第 1889-1903 行 startServices() 函数
三、低优先级改进
12. 代码模块化重构
问题描述:
- 2100+ 行单文件,难以维护
改进方案:
scripts/
├── install.js # 入口
├── lib/
│ ├── logger.js # 日志模块
│ ├── prompt.js # 交互输入模块
│ ├── executor.js # 命令执行模块
│ ├── database.js # 数据库配置模块
│ ├── nginx.js # Nginx 配置模块
│ ├── pm2.js # PM2 配置模块
│ └── health-check.js # 健康检查模块
└── templates/
├── env.template # .env 模板
├── pm2.template.js # PM2 配置模板
└── nginx.template # Nginx 配置模板
13. 卸载脚本
改进方案:
// uninstall.js
async function uninstall() {
log.step('停止服务');
runCommand('pm2 stop idc-backend idc-frontend');
runCommand('pm2 delete idc-backend idc-frontend');
log.step('清理文件');
['node_modules', 'dist', 'logs'].forEach(dir => {
const cmd = process.platform === 'win32'
? `rmdir /s /q ${dir}`
: `rm -rf ${dir}`;
runCommand(cmd, { cwd: backendDir });
runCommand(cmd, { cwd: frontendDir });
});
log.step('清理配置');
['.env', '.install-config.json'].forEach(file => {
const path = `${backendDir}/${file}`;
if (fs.existsSync(path)) fs.unlinkSync(path);
});
log.success('卸载完成');
}
14. 配置验证
改进方案:
async function validateConfig() {
const errors = [];
// 端口冲突检查
const usedPorts = await checkUsedPorts([config.backendPort, config.frontendPort]);
if (usedPorts.length > 0) {
errors.push(`端口已被占用: ${usedPorts.join(', ')}`);
}
// 磁盘空间检查
const freeSpace = await checkDiskSpace(__dirname);
if (freeSpace < 500 * 1024 * 1024) {
errors.push('磁盘空间不足 500MB');
}
// 权限检查
if (config.frontendPort < 1024 && !isRootUser()) {
errors.push(`端口 ${config.frontendPort} 需要 root 权限`);
}
return errors;
}
15. 配置导入导出
改进方案:
// 导入配置
// node install.js --import=config.json
async function importConfig(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const loaded = JSON.parse(content);
Object.assign(config, loaded);
log.success(`配置已导入: ${filePath}`);
}
// 导出配置(在 saveConfig 时可选择)
// node install.js --export=config.json
四、实施优先级
| 优先级 | 编号 | 改进项 | 复杂度 | 状态 |
|---|---|---|---|---|
| 🔴高 | 1 | npm 安装网络处理 | 中 | 待实施 |
| 🔴高 | 2 | 健康检查跨平台 | 低 | 待实施 |
| 🔴高 | 3 | MySQL 密码安全 | 低 | 待实施 |
| 🔴高 | 4 | 错误处理完善 | 中 | 待实施 |
| 🔴高 | 5 | 回滚机制 | 高 | 待实施 |
| 🔴高 | 6 | SQLite 替换 | 中 | ✅ 已完成 |
| 🟡中 | 7 | 日志管理 | 低 | 待实施 |
| 🟡中 | 8 | 配置持久化 | 中 | 待实施 |
| 🟡中 | 9 | 进度显示 | 低 | 待实施 |
| 🟡中 | 10 | Nginx 版本动态 | 低 | 待实施 |
| 🟡中 | 11 | PM2 serve 本地化 | 低 | 待实施 |
| 🟢低 | 12 | 代码模块化 | 高 | 待实施 |
| 🟢低 | 13 | 卸载脚本 | 中 | 待实施 |
| 🟢低 | 14 | 配置验证 | 中 | 待实施 |
| 🟢低 | 15 | 配置导入导出 | 低 | 待实施 |
五、建议实施顺序
第一阶段(快速见效)
- Nginx 版本动态 (10) - 改动小,立即生效
- 日志管理优化 (7) - 改动小,提升调试体验
- PM2 serve 本地化 (11) - 改动小,避免全局污染
第二阶段(核心增强)
- npm 安装网络处理 (1) - 提升安装成功率
- 健康检查跨平台 (2) - Windows 兼容性
- MySQL 密码安全 (3) - 安全性
第三阶段(健壮性)
- 错误处理完善 (4) - 提升容错能力
- 回滚机制 (5) - 失败恢复
- 配置持久化 (8) - 复用配置
- 进度显示 (9) - 用户体验
第四阶段(长期)
- 代码模块化 (12) - 提升可维护性
- 卸载脚本 (13) - 功能完整性
- 配置验证 (14) - 提前发现问题
- 配置导入导出 (15) - 扩展性
六、测试清单
每项改进实施后需验证:
- Windows 环境
- Linux 环境(Ubuntu/Debian)
- Linux 环境(CentOS/RHEL)
- macOS 环境
- 非交互模式 (
-y) - 配置恢复功能
- 日志文件生成
- 回滚功能(模拟失败)