Files
vf_react/scripts/deploy-to-cos.js
2025-12-13 11:38:19 +08:00

358 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* 腾讯云 COS 部署脚本
* 将构建产物上传到 COS 存储桶,配合 CDN 实现静态资源加速
*
* 使用方法:
* node scripts/deploy-to-cos.js
*
* 配置文件:
* .env.cos (从 .env.cos.example 复制并填写)
*/
const COS = require('cos-nodejs-sdk-v5');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// ============================================================================
// 配置加载
// ============================================================================
// 加载 .env.cos 配置
function loadConfig() {
const envPath = path.join(__dirname, '..', '.env.cos');
if (!fs.existsSync(envPath)) {
console.error('\x1b[31m[错误]\x1b[0m 配置文件不存在: .env.cos');
console.log('\n请先创建配置文件:');
console.log(' 1. 复制 .env.cos.example 为 .env.cos');
console.log(' 2. 填写腾讯云 SecretId、SecretKey 等信息\n');
process.exit(1);
}
const envContent = fs.readFileSync(envPath, 'utf-8');
const config = {};
envContent.split('\n').forEach(line => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const [key, ...valueParts] = trimmed.split('=');
if (key) {
config[key.trim()] = valueParts.join('=').trim();
}
}
});
// 验证必需配置
const required = ['COS_SECRET_ID', 'COS_SECRET_KEY', 'COS_BUCKET', 'COS_REGION'];
const missing = required.filter(key => !config[key]);
if (missing.length > 0) {
console.error('\x1b[31m[错误]\x1b[0m 配置不完整,缺少:', missing.join(', '));
process.exit(1);
}
return config;
}
// ============================================================================
// COS 客户端
// ============================================================================
let cos = null;
let config = null;
function initCOS() {
config = loadConfig();
cos = new COS({
SecretId: config.COS_SECRET_ID,
SecretKey: config.COS_SECRET_KEY,
});
console.log('\x1b[32m[✓]\x1b[0m COS 客户端初始化成功');
console.log(` 存储桶: ${config.COS_BUCKET}`);
console.log(` 地域: ${config.COS_REGION}`);
}
// ============================================================================
// 文件操作
// ============================================================================
/**
* 递归获取目录下所有文件
*/
function getAllFiles(dirPath, arrayOfFiles = []) {
const files = fs.readdirSync(dirPath);
files.forEach(file => {
const fullPath = path.join(dirPath, file);
if (fs.statSync(fullPath).isDirectory()) {
getAllFiles(fullPath, arrayOfFiles);
} else {
arrayOfFiles.push(fullPath);
}
});
return arrayOfFiles;
}
/**
* 获取文件的 Content-Type
*/
function getContentType(filePath) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.webp': 'image/webp',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.map': 'application/json',
'.txt': 'text/plain; charset=utf-8',
'.xml': 'application/xml',
'.webmanifest': 'application/manifest+json',
};
return mimeTypes[ext] || 'application/octet-stream';
}
/**
* 获取缓存控制头
*/
function getCacheControl(filePath) {
const ext = path.extname(filePath).toLowerCase();
const fileName = path.basename(filePath);
// HTML 文件不缓存(或短缓存)
if (ext === '.html') {
return 'no-cache, no-store, must-revalidate';
}
// 带 hash 的静态资源长期缓存JS/CSS 构建产物)
// 文件名格式: main.abc123.js, styles.def456.css
if (/\.[a-f0-9]{8,}\.(js|css)$/.test(fileName)) {
return 'public, max-age=31536000, immutable'; // 1 年
}
// 图片、字体缓存 30 天
if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp', '.woff', '.woff2', '.ttf', '.eot'].includes(ext)) {
return 'public, max-age=2592000'; // 30 天
}
// 其他文件缓存 1 天
return 'public, max-age=86400';
}
// ============================================================================
// 上传功能
// ============================================================================
/**
* 上传单个文件到 COS
*/
function uploadFile(localPath, remotePath) {
return new Promise((resolve, reject) => {
const contentType = getContentType(localPath);
const cacheControl = getCacheControl(localPath);
cos.putObject({
Bucket: config.COS_BUCKET,
Region: config.COS_REGION,
Key: remotePath,
Body: fs.createReadStream(localPath),
ContentType: contentType,
CacheControl: cacheControl,
}, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
/**
* 清空存储桶(可选)
*/
async function clearBucket() {
console.log('\n\x1b[36m[清理]\x1b[0m 清空存储桶旧文件...');
return new Promise((resolve, reject) => {
cos.getBucket({
Bucket: config.COS_BUCKET,
Region: config.COS_REGION,
MaxKeys: 1000,
}, async (err, data) => {
if (err) {
reject(err);
return;
}
if (!data.Contents || data.Contents.length === 0) {
console.log(' 存储桶为空,无需清理');
resolve();
return;
}
const objects = data.Contents.map(item => ({ Key: item.Key }));
console.log(` 发现 ${objects.length} 个文件`);
cos.deleteMultipleObject({
Bucket: config.COS_BUCKET,
Region: config.COS_REGION,
Objects: objects,
}, (err, data) => {
if (err) {
reject(err);
} else {
console.log(` 已删除 ${objects.length} 个文件`);
resolve();
}
});
});
});
}
/**
* 上传整个 build 目录
*/
async function uploadBuildDir() {
const buildDir = path.join(__dirname, '..', 'build');
if (!fs.existsSync(buildDir)) {
console.error('\x1b[31m[错误]\x1b[0m build 目录不存在,请先运行 npm run build');
process.exit(1);
}
const files = getAllFiles(buildDir);
console.log(`\n\x1b[36m[上传]\x1b[0m 共 ${files.length} 个文件待上传...\n`);
let uploaded = 0;
let failed = 0;
const errors = [];
// 并发上传(限制并发数)
const concurrency = 10;
const chunks = [];
for (let i = 0; i < files.length; i += concurrency) {
chunks.push(files.slice(i, i + concurrency));
}
for (const chunk of chunks) {
await Promise.all(chunk.map(async (filePath) => {
const relativePath = path.relative(buildDir, filePath).replace(/\\/g, '/');
try {
await uploadFile(filePath, relativePath);
uploaded++;
// 进度显示
const progress = Math.round((uploaded + failed) / files.length * 100);
process.stdout.write(`\r 进度: ${progress}% (${uploaded}/${files.length})`);
} catch (err) {
failed++;
errors.push({ file: relativePath, error: err.message });
}
}));
}
console.log('\n');
if (failed > 0) {
console.log('\x1b[33m[警告]\x1b[0m 部分文件上传失败:');
errors.forEach(({ file, error }) => {
console.log(` - ${file}: ${error}`);
});
}
console.log(`\x1b[32m[✓]\x1b[0m 上传完成: 成功 ${uploaded},失败 ${failed}`);
return { uploaded, failed };
}
// ============================================================================
// CDN 刷新(可选)
// ============================================================================
/**
* 刷新 CDN 缓存
* 注意:需要额外配置 CDN API 权限
*/
async function refreshCDN() {
if (!config.CDN_DOMAIN) {
console.log('\n\x1b[33m[提示]\x1b[0m 未配置 CDN 域名,跳过 CDN 刷新');
console.log(' 如需自动刷新 CDN请在 .env.cos 中配置 CDN_DOMAIN');
return;
}
console.log('\n\x1b[36m[CDN]\x1b[0m 刷新 CDN 缓存...');
console.log(` 域名: ${config.CDN_DOMAIN}`);
console.log('\n\x1b[33m[提示]\x1b[0m 由于文件名包含 hash通常不需要手动刷新 CDN');
console.log(' 如需刷新,请到腾讯云 CDN 控制台操作:');
console.log(' https://console.cloud.tencent.com/cdn/refresh');
}
// ============================================================================
// 主流程
// ============================================================================
async function main() {
console.log('\n╔════════════════════════════════════════════════════════════════╗');
console.log('║ 腾讯云 COS 部署工具 - ValueFrontier ║');
console.log('╚════════════════════════════════════════════════════════════════╝\n');
const startTime = Date.now();
try {
// 1. 初始化 COS 客户端
initCOS();
// 2. 可选:清空存储桶
const clearFirst = process.argv.includes('--clear');
if (clearFirst) {
await clearBucket();
}
// 3. 上传文件
const { uploaded, failed } = await uploadBuildDir();
// 4. CDN 刷新提示
await refreshCDN();
// 5. 完成
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
console.log('\n╔════════════════════════════════════════════════════════════════╗');
console.log('║ 🎉 部署成功! ║');
console.log('╚════════════════════════════════════════════════════════════════╝');
console.log(`\n 耗时: ${duration}`);
console.log(` 上传: ${uploaded} 个文件`);
if (config.CDN_DOMAIN) {
console.log(`\n 访问地址: https://${config.CDN_DOMAIN}`);
}
console.log('\n');
} catch (err) {
console.error('\n\x1b[31m[错误]\x1b[0m 部署失败:', err.message);
process.exit(1);
}
}
// 运行
main();