662 lines
17 KiB
Markdown
662 lines
17 KiB
Markdown
# install.js 安装脚本改进计划
|
||
|
||
## 概述
|
||
|
||
本文档记录 `install.js` 安装脚本的改进计划,涵盖 **15 项改进**,按优先级分为高/中/低三档。
|
||
|
||
---
|
||
|
||
## 一、高优先级改进
|
||
|
||
### 1. npm 依赖安装网络问题处理
|
||
|
||
**问题描述**:
|
||
- 直接执行 `npm install`,无重试机制
|
||
- 国内访问 npmjs.org 超时/速度慢
|
||
- 网络波动导致安装中断无反馈
|
||
|
||
**改进方案**:
|
||
|
||
#### 1.1 自动检测并切换镜像源
|
||
|
||
```javascript
|
||
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 重试机制与指数退避
|
||
|
||
```javascript
|
||
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 安装进度显示
|
||
|
||
```javascript
|
||
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 安装后验证
|
||
|
||
```javascript
|
||
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. 健康检查跨平台兼容性
|
||
|
||
**问题描述**:
|
||
```javascript
|
||
// 当前代码(Windows 无 curl)
|
||
execSync('curl -s -o /dev/null -w "%{http_code}" ...');
|
||
```
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
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 密码安全传递
|
||
|
||
**问题描述**:
|
||
```javascript
|
||
// 当前代码 - 密码暴露在命令行
|
||
execSync(`mysql -u root -p'${rootPassword}' -e "${sql}"`);
|
||
```
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
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. 错误处理完善
|
||
|
||
**问题描述**:
|
||
- 错误后继续执行,导致级联失败
|
||
- 无重试选项
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
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. 回滚机制
|
||
|
||
**问题描述**:
|
||
- 安装失败后无清理
|
||
- 残留半成品状态
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
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 文件名不兼容冒号
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
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. 配置持久化与恢复
|
||
|
||
**问题描述**:
|
||
- 每次安装需重新配置
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
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. 进度显示优化
|
||
|
||
**问题描述**:
|
||
- 只有文字输出,无进度条
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
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 版本动态获取
|
||
|
||
**问题描述**:
|
||
```javascript
|
||
// 固定版本
|
||
const nginxUrl = 'https://nginx.org/download/nginx-1.24.0.zip';
|
||
```
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
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 本地化
|
||
|
||
**问题描述**:
|
||
```javascript
|
||
// 全局安装
|
||
runCommand('npm install -g serve');
|
||
```
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
// 方案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. 卸载脚本
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
// 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. 配置验证
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
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. 配置导入导出
|
||
|
||
**改进方案**:
|
||
|
||
```javascript
|
||
// 导入配置
|
||
// 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 | 配置导入导出 | 低 | 待实施 |
|
||
|
||
---
|
||
|
||
## 五、建议实施顺序
|
||
|
||
### 第一阶段(快速见效)
|
||
|
||
1. **Nginx 版本动态** (10) - 改动小,立即生效
|
||
2. **日志管理优化** (7) - 改动小,提升调试体验
|
||
3. **PM2 serve 本地化** (11) - 改动小,避免全局污染
|
||
|
||
### 第二阶段(核心增强)
|
||
|
||
4. **npm 安装网络处理** (1) - 提升安装成功率
|
||
5. **健康检查跨平台** (2) - Windows 兼容性
|
||
6. **MySQL 密码安全** (3) - 安全性
|
||
|
||
### 第三阶段(健壮性)
|
||
|
||
7. **错误处理完善** (4) - 提升容错能力
|
||
8. **回滚机制** (5) - 失败恢复
|
||
9. **配置持久化** (8) - 复用配置
|
||
10. **进度显示** (9) - 用户体验
|
||
|
||
### 第四阶段(长期)
|
||
|
||
11. **代码模块化** (12) - 提升可维护性
|
||
12. **卸载脚本** (13) - 功能完整性
|
||
13. **配置验证** (14) - 提前发现问题
|
||
14. **配置导入导出** (15) - 扩展性
|
||
|
||
---
|
||
|
||
## 六、测试清单
|
||
|
||
每项改进实施后需验证:
|
||
|
||
- [ ] Windows 环境
|
||
- [ ] Linux 环境(Ubuntu/Debian)
|
||
- [ ] Linux 环境(CentOS/RHEL)
|
||
- [ ] macOS 环境
|
||
- [ ] 非交互模式 (`-y`)
|
||
- [ ] 配置恢复功能
|
||
- [ ] 日志文件生成
|
||
- [ ] 回滚功能(模拟失败)
|