#!/usr/bin/env node /** * 前端开发服务启动脚本 * * 用法: * node scripts/dev.js # 同时启动 web + mall * node scripts/dev.js web # 仅启动健康APP (web) * node scripts/dev.js mall # 仅启动商城 (mall) * node scripts/dev.js --kill # 仅关闭占用端口的进程 * * 启动前会自动检查端口占用并清理。 */ const { execSync, spawn } = require('child_process'); const path = require('path'); const os = require('os'); // ─── 配置 ──────────────────────────────────────────── const PROJECTS = { web: { name: '健康APP (web)', dir: 'web', port: 5173, color: '\x1b[36m', // cyan url: 'http://localhost:5173' }, mall: { name: '健康商城 (mall)', dir: 'mall', port: 5174, color: '\x1b[32m', // green url: 'http://localhost:5174' } }; const RESET = '\x1b[0m'; const BOLD = '\x1b[1m'; const RED = '\x1b[31m'; const YELLOW = '\x1b[33m'; const GREEN = '\x1b[32m'; const CYAN = '\x1b[36m'; const DIM = '\x1b[2m'; // ─── 工具函数 ──────────────────────────────────────── function log(msg) { console.log(msg); } function info(msg) { log(`${CYAN}ℹ${RESET} ${msg}`); } function success(msg) { log(`${GREEN}✓${RESET} ${msg}`); } function warn(msg) { log(`${YELLOW}⚠${RESET} ${msg}`); } function error(msg) { log(`${RED}✗${RESET} ${msg}`); } function banner() { log(''); log(`${BOLD}╔═══════════════════════════════════════════════╗${RESET}`); log(`${BOLD}║ 健康AI助手 · 前端开发服务启动脚本 ║${RESET}`); log(`${BOLD}╚═══════════════════════════════════════════════╝${RESET}`); log(''); } /** * 获取占用指定端口的进程 PID * 使用 netstat(Windows/Linux 通用)进行精确检测 * 返回 PID 或 null */ function getPortPid(port) { try { const isWin = os.platform() === 'win32'; if (isWin) { // Windows: netstat -ano,查找 LISTENING 状态 const result = execSync(`netstat -ano`, { encoding: 'utf-8', timeout: 5000 }); const lines = result.split('\n'); for (const line of lines) { // 匹配精确端口(避免 51730 匹配 5173) const match = line.match(new RegExp(`:\\s*${port}\\s+.*LISTENING\\s+(\\d+)`)); if (match) { const pid = parseInt(match[1], 10); if (pid > 0) return pid; } } // 兜底:用 findstr try { const result2 = execSync(`netstat -ano | findstr ":${port} "`, { encoding: 'utf-8', timeout: 5000 }); const lines2 = result2.trim().split('\n'); for (const line of lines2) { if (line.includes('LISTENING')) { const parts = line.trim().split(/\s+/); const pid = parseInt(parts[parts.length - 1], 10); if (pid > 0) return pid; } } } catch { /* no match */ } } else { const result = execSync(`lsof -ti:${port}`, { encoding: 'utf-8', timeout: 5000 }); const pid = parseInt(result.trim().split('\n')[0], 10); if (pid > 0) return pid; } } catch { // 没有找到进程 } return null; } /** * 获取进程名称 */ function getProcessName(pid) { try { const isWin = os.platform() === 'win32'; if (isWin) { const result = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf-8', timeout: 5000 }); const match = result.match(/"([^"]+)"/); return match ? match[1] : '未知进程'; } else { return execSync(`ps -p ${pid} -o comm=`, { encoding: 'utf-8', timeout: 5000 }).trim(); } } catch { return '未知进程'; } } /** * 终止进程 */ function killProcess(pid) { try { const isWin = os.platform() === 'win32'; if (isWin) { // 先尝试终止进程树 execSync(`taskkill /PID ${pid} /T /F`, { encoding: 'utf-8', timeout: 5000, stdio: 'ignore' }); } else { execSync(`kill -9 ${pid}`, { encoding: 'utf-8', timeout: 5000, stdio: 'ignore' }); } return true; } catch { return false; } } /** * 检查并释放端口 */ async function ensurePortFree(port, projectName) { const pid = getPortPid(port); if (!pid) { success(`端口 ${BOLD}${port}${RESET} 空闲 ${DIM}(${projectName})${RESET}`); return true; } const procName = getProcessName(pid); warn(`端口 ${BOLD}${port}${RESET} 被占用 → PID: ${pid} (${procName}) ${DIM}[${projectName}]${RESET}`); if (killProcess(pid)) { // 等待端口释放(最多重试 3 次) for (let i = 0; i < 3; i++) { await new Promise(resolve => setTimeout(resolve, 1000)); if (!getPortPid(port)) { success(`已终止进程 PID ${pid},端口 ${port} 已释放`); return true; } } error(`端口 ${port} 仍被占用,无法释放`); return false; } else { error(`无法终止进程 PID ${pid}`); return false; } } /** * 检查项目依赖是否已安装 */ function checkNodeModules(projectDir) { const nmPath = path.join(projectDir, 'node_modules'); try { require('fs').accessSync(nmPath); return true; } catch { return false; } } /** * 安装项目依赖 */ function installDeps(projectDir, projectName) { info(`${projectName}: 正在安装依赖 (npm install)...`); try { execSync('npm install', { cwd: projectDir, stdio: 'inherit', timeout: 120000 }); success(`${projectName}: 依赖安装完成`); return true; } catch { error(`${projectName}: 依赖安装失败`); return false; } } /** * 启动项目开发服务 */ function startProject(project, rootDir) { const projectDir = path.join(rootDir, project.dir); const label = `${project.color}[${project.name}]${RESET}`; info(`${label} 启动中...`); const child = spawn('npm', ['run', 'dev'], { cwd: projectDir, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env }, shell: true }); child.stdout.on('data', (data) => { const lines = data.toString().split('\n').filter(l => l.trim()); for (const line of lines) { log(` ${label} ${line}`); } }); child.stderr.on('data', (data) => { const lines = data.toString().split('\n').filter(l => l.trim()); for (const line of lines) { // Vite 的 deprecation warning 降级显示 if (line.includes('Deprecation Warning') || line.includes('More info:')) { log(` ${label} ${DIM}${line}${RESET}`); } else { log(` ${label} ${YELLOW}${line}${RESET}`); } } }); child.on('error', (err) => { error(`${label} 启动失败: ${err.message}`); }); child.on('close', (code) => { if (code !== null && code !== 0) { warn(`${label} 进程退出,退出码: ${code}`); } }); return child; } // ─── 主流程 ────────────────────────────────────────── async function main() { banner(); const args = process.argv.slice(2); const killOnly = args.includes('--kill'); const targets = []; if (args.includes('web')) targets.push('web'); if (args.includes('mall')) targets.push('mall'); if (targets.length === 0 && !killOnly) { targets.push('web', 'mall'); } const rootDir = path.resolve(__dirname, '..'); // ── 步骤 1:显示计划 ── if (killOnly) { info('模式: 仅关闭端口占用进程'); } else if (targets.length === 2) { info('模式: 同时启动 健康APP + 商城'); } else { info(`模式: 仅启动 ${PROJECTS[targets[0]].name}`); } log(''); // ── 步骤 2:检查端口 ── log(`${BOLD}[1/3] 检查端口占用${RESET}`); log('─'.repeat(48)); const portProjects = killOnly ? Object.values(PROJECTS) : targets.map(t => PROJECTS[t]); for (const proj of portProjects) { const freed = await ensurePortFree(proj.port, proj.name); if (!freed && !killOnly) { error(`无法释放端口 ${proj.port},请手动处理后重试`); process.exit(1); } } log(''); if (killOnly) { success('端口清理完成'); process.exit(0); } // ── 步骤 3:检查依赖 ── log(`${BOLD}[2/3] 检查项目依赖${RESET}`); log('─'.repeat(48)); for (const key of targets) { const proj = PROJECTS[key]; const projDir = path.join(rootDir, proj.dir); if (checkNodeModules(projDir)) { success(`${proj.name}: node_modules 已就绪`); } else { warn(`${proj.name}: 未安装依赖`); const ok = installDeps(projDir, proj.name); if (!ok) { error('依赖安装失败,退出'); process.exit(1); } } } log(''); // ── 步骤 4:启动服务 ── log(`${BOLD}[3/3] 启动开发服务${RESET}`); log('─'.repeat(48)); const children = []; for (const key of targets) { const child = startProject(PROJECTS[key], rootDir); children.push(child); } // 等待所有服务就绪后显示汇总 await new Promise(resolve => setTimeout(resolve, 3000)); log(''); log(`${BOLD}╔═══════════════════════════════════════════════╗${RESET}`); log(`${BOLD}║ 开发服务已启动 ║${RESET}`); log(`${BOLD}╠═══════════════════════════════════════════════╣${RESET}`); for (const key of targets) { const proj = PROJECTS[key]; log(`${BOLD}║${RESET} ${proj.color}${proj.name}${RESET}: ${BOLD}${proj.url}${RESET}`); } log(`${BOLD}╠═══════════════════════════════════════════════╣${RESET}`); log(`${BOLD}║${RESET} 后端 API: ${BOLD}http://localhost:8080${RESET}`); log(`${BOLD}║${RESET} 测试账号: ${DIM}13800138000 / 123456${RESET}`); log(`${BOLD}╠═══════════════════════════════════════════════╣${RESET}`); log(`${BOLD}║${RESET} 按 ${RED}Ctrl+C${RESET} 停止所有服务`); log(`${BOLD}╚═══════════════════════════════════════════════╝${RESET}`); log(''); // ── 优雅退出处理 ── function cleanup() { log(''); info('正在停止所有服务...'); for (const child of children) { try { const isWin = os.platform() === 'win32'; if (isWin) { execSync(`taskkill /PID ${child.pid} /T /F`, { stdio: 'ignore', timeout: 5000 }); } else { child.kill('SIGTERM'); } } catch { // 进程可能已退出 } } success('所有服务已停止'); process.exit(0); } process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); // 保持主进程运行 await new Promise(() => {}); } main().catch(err => { error(`脚本执行出错: ${err.message}`); process.exit(1); });