9.1 应用打包概述
9.1.1 Sciter 应用打包原理
graph TD
A[源代码] --> B[资源收集]
B --> C[依赖分析]
C --> D[代码压缩]
D --> E[资源打包]
E --> F[可执行文件]
F --> G[安装包]
H[配置文件] --> B
I[静态资源] --> B
J[第三方库] --> C
9.1.2 打包工具配置
// build-config.js
class BuildConfig {
constructor() {
this.config = {
// 应用信息
app: {
name: 'MySciterApp',
version: '1.0.0',
description: 'A Sciter-JS Application',
author: 'Your Name',
icon: 'assets/icon.ico'
},
// 构建选项
build: {
target: 'windows', // windows, macos, linux
architecture: 'x64', // x86, x64, arm64
compression: true,
minify: true,
obfuscate: false
},
// 输出配置
output: {
directory: 'dist',
filename: 'app.exe',
installer: true
},
// 资源配置
resources: {
include: ['src/**/*', 'assets/**/*'],
exclude: ['**/*.tmp', '**/*.log'],
compress: ['**/*.js', '**/*.css', '**/*.html']
}
};
}
// 获取配置
getConfig() {
return this.config;
}
// 更新配置
updateConfig(updates) {
Object.assign(this.config, updates);
}
// 验证配置
validate() {
const required = ['app.name', 'app.version', 'build.target'];
const missing = required.filter(key => {
const value = this.getNestedValue(this.config, key);
return !value;
});
if (missing.length > 0) {
throw new Error(`Missing required config: ${missing.join(', ')}`);
}
return true;
}
// 获取嵌套值
getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => {
return current && current[key];
}, obj);
}
}
// 导出配置实例
const buildConfig = new BuildConfig();
export default buildConfig;
9.2 资源管理与优化
9.2.1 资源收集器
// resource-collector.js
import { promises as fs } from 'fs';
import path from 'path';
import glob from 'glob';
class ResourceCollector {
constructor(config) {
this.config = config;
this.resources = new Map();
}
// 收集所有资源
async collectResources() {
console.log('开始收集资源...');
// 收集包含的文件
for (const pattern of this.config.resources.include) {
await this.collectByPattern(pattern, true);
}
// 排除指定文件
for (const pattern of this.config.resources.exclude) {
await this.collectByPattern(pattern, false);
}
console.log(`收集完成,共 ${this.resources.size} 个资源`);
return this.resources;
}
// 按模式收集
async collectByPattern(pattern, include) {
const files = await this.globPromise(pattern);
for (const file of files) {
if (include) {
await this.addResource(file);
} else {
this.removeResource(file);
}
}
}
// 添加资源
async addResource(filePath) {
try {
const stats = await fs.stat(filePath);
if (stats.isFile()) {
const content = await fs.readFile(filePath);
this.resources.set(filePath, {
path: filePath,
size: stats.size,
content: content,
type: this.getFileType(filePath),
compressed: false
});
}
} catch (error) {
console.warn(`无法读取文件: ${filePath}`, error.message);
}
}
// 移除资源
removeResource(filePath) {
this.resources.delete(filePath);
}
// 获取文件类型
getFileType(filePath) {
const ext = path.extname(filePath).toLowerCase();
const typeMap = {
'.js': 'javascript',
'.css': 'stylesheet',
'.html': 'markup',
'.htm': 'markup',
'.json': 'data',
'.png': 'image',
'.jpg': 'image',
'.jpeg': 'image',
'.gif': 'image',
'.svg': 'image',
'.ico': 'icon',
'.ttf': 'font',
'.woff': 'font',
'.woff2': 'font'
};
return typeMap[ext] || 'binary';
}
// Glob Promise 包装
globPromise(pattern) {
return new Promise((resolve, reject) => {
glob(pattern, (error, files) => {
if (error) reject(error);
else resolve(files);
});
});
}
// 获取资源统计
getStats() {
const stats = {
total: this.resources.size,
types: {},
totalSize: 0
};
for (const resource of this.resources.values()) {
stats.totalSize += resource.size;
stats.types[resource.type] = (stats.types[resource.type] || 0) + 1;
}
return stats;
}
}
export default ResourceCollector;
9.2.2 资源压缩器
// resource-compressor.js
import { minify as minifyJS } from 'terser';
import CleanCSS from 'clean-css';
import { minify as minifyHTML } from 'html-minifier-terser';
import zlib from 'zlib';
import { promisify } from 'util';
const gzip = promisify(zlib.gzip);
const deflate = promisify(zlib.deflate);
class ResourceCompressor {
constructor(config) {
this.config = config;
this.cssMinifier = new CleanCSS({
level: 2,
returnPromise: true
});
}
// 压缩资源
async compressResources(resources) {
console.log('开始压缩资源...');
const compressPromises = [];
for (const [path, resource] of resources) {
if (this.shouldCompress(path)) {
compressPromises.push(this.compressResource(resource));
}
}
await Promise.all(compressPromises);
console.log('资源压缩完成');
return resources;
}
// 判断是否需要压缩
shouldCompress(filePath) {
return this.config.resources.compress.some(pattern => {
return this.matchPattern(filePath, pattern);
});
}
// 压缩单个资源
async compressResource(resource) {
try {
const originalSize = resource.size;
switch (resource.type) {
case 'javascript':
resource.content = await this.compressJavaScript(resource.content);
break;
case 'stylesheet':
resource.content = await this.compressCSS(resource.content);
break;
case 'markup':
resource.content = await this.compressHTML(resource.content);
break;
default:
// 对于其他类型,使用通用压缩
if (resource.size > 1024) { // 只压缩大于1KB的文件
resource.content = await gzip(resource.content);
resource.compressed = 'gzip';
}
break;
}
const newSize = Buffer.byteLength(resource.content);
const ratio = ((originalSize - newSize) / originalSize * 100).toFixed(2);
console.log(`${resource.path}: ${originalSize} -> ${newSize} bytes (${ratio}% 减少)`);
resource.size = newSize;
resource.originalSize = originalSize;
resource.compressionRatio = ratio;
} catch (error) {
console.warn(`压缩失败: ${resource.path}`, error.message);
}
}
// 压缩 JavaScript
async compressJavaScript(content) {
const result = await minifyJS(content.toString(), {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info']
},
mangle: {
toplevel: true
},
format: {
comments: false
}
});
return Buffer.from(result.code);
}
// 压缩 CSS
async compressCSS(content) {
const result = await this.cssMinifier.minify(content.toString());
return Buffer.from(result.styles);
}
// 压缩 HTML
async compressHTML(content) {
const result = await minifyHTML(content.toString(), {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
minifyCSS: true,
minifyJS: true
});
return Buffer.from(result);
}
// 模式匹配
matchPattern(str, pattern) {
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
return regex.test(str);
}
}
export default ResourceCompressor;
9.3 构建流程管理
9.3.1 构建管理器
// build-manager.js
import BuildConfig from './build-config.js';
import ResourceCollector from './resource-collector.js';
import ResourceCompressor from './resource-compressor.js';
import { promises as fs } from 'fs';
import path from 'path';
class BuildManager {
constructor() {
this.config = BuildConfig.getConfig();
this.collector = new ResourceCollector(this.config);
this.compressor = new ResourceCompressor(this.config);
this.buildSteps = [];
}
// 执行完整构建
async build() {
try {
console.log('开始构建应用...');
const startTime = Date.now();
// 验证配置
BuildConfig.validate();
// 执行构建步骤
await this.executeBuildSteps();
const duration = Date.now() - startTime;
console.log(`构建完成,耗时: ${duration}ms`);
return true;
} catch (error) {
console.error('构建失败:', error.message);
throw error;
}
}
// 执行构建步骤
async executeBuildSteps() {
const steps = [
{ name: '清理输出目录', fn: () => this.cleanOutput() },
{ name: '收集资源', fn: () => this.collectResources() },
{ name: '压缩资源', fn: () => this.compressResources() },
{ name: '生成清单', fn: () => this.generateManifest() },
{ name: '打包应用', fn: () => this.packageApp() },
{ name: '创建安装包', fn: () => this.createInstaller() }
];
for (const step of steps) {
console.log(`执行步骤: ${step.name}`);
await step.fn();
console.log(`✓ ${step.name} 完成`);
}
}
// 清理输出目录
async cleanOutput() {
const outputDir = this.config.output.directory;
try {
await fs.rmdir(outputDir, { recursive: true });
} catch (error) {
// 目录不存在,忽略错误
}
await fs.mkdir(outputDir, { recursive: true });
}
// 收集资源
async collectResources() {
this.resources = await this.collector.collectResources();
const stats = this.collector.getStats();
console.log(`收集到 ${stats.total} 个资源,总大小: ${this.formatBytes(stats.totalSize)}`);
return this.resources;
}
// 压缩资源
async compressResources() {
if (this.config.build.compression) {
await this.compressor.compressResources(this.resources);
}
return this.resources;
}
// 生成应用清单
async generateManifest() {
const manifest = {
name: this.config.app.name,
version: this.config.app.version,
description: this.config.app.description,
author: this.config.app.author,
buildTime: new Date().toISOString(),
resources: Array.from(this.resources.keys()),
totalSize: Array.from(this.resources.values())
.reduce((sum, resource) => sum + resource.size, 0)
};
const manifestPath = path.join(this.config.output.directory, 'manifest.json');
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
return manifest;
}
// 打包应用
async packageApp() {
const outputPath = path.join(
this.config.output.directory,
this.config.output.filename
);
// 这里应该调用 Sciter 的打包工具
// 示例:使用 sciter-sdk 的打包功能
await this.createSciterPackage(outputPath);
return outputPath;
}
// 创建 Sciter 包
async createSciterPackage(outputPath) {
// 创建资源包
const resourcesData = this.serializeResources();
// 写入可执行文件
// 这里需要根据实际的 Sciter SDK 进行调整
const executableTemplate = await this.getExecutableTemplate();
const packagedApp = Buffer.concat([executableTemplate, resourcesData]);
await fs.writeFile(outputPath, packagedApp);
console.log(`应用已打包到: ${outputPath}`);
}
// 序列化资源
serializeResources() {
const resourcesArray = Array.from(this.resources.entries()).map(([path, resource]) => ({
path,
content: resource.content.toString('base64'),
type: resource.type,
size: resource.size
}));
return Buffer.from(JSON.stringify(resourcesArray));
}
// 获取可执行文件模板
async getExecutableTemplate() {
const templatePath = `templates/${this.config.build.target}-${this.config.build.architecture}.exe`;
return await fs.readFile(templatePath);
}
// 创建安装包
async createInstaller() {
if (!this.config.output.installer) {
return;
}
const installerConfig = {
appName: this.config.app.name,
appVersion: this.config.app.version,
appDescription: this.config.app.description,
appIcon: this.config.app.icon,
executablePath: path.join(
this.config.output.directory,
this.config.output.filename
),
outputPath: path.join(
this.config.output.directory,
`${this.config.app.name}-${this.config.app.version}-setup.exe`
)
};
await this.generateInstaller(installerConfig);
}
// 生成安装程序
async generateInstaller(config) {
// 这里可以使用 NSIS、Inno Setup 或其他安装包生成工具
console.log(`创建安装包: ${config.outputPath}`);
// 示例:生成 NSIS 脚本
const nsisScript = this.generateNSISScript(config);
const scriptPath = path.join(this.config.output.directory, 'installer.nsi');
await fs.writeFile(scriptPath, nsisScript);
// 调用 NSIS 编译器
// await this.runNSISCompiler(scriptPath);
}
// 生成 NSIS 脚本
generateNSISScript(config) {
return `
!define APP_NAME "${config.appName}"
!define APP_VERSION "${config.appVersion}"
!define APP_DESCRIPTION "${config.appDescription}"
!define APP_EXECUTABLE "${path.basename(config.executablePath)}"
Name "\${APP_NAME}"
OutFile "${config.outputPath}"
InstallDir "$PROGRAMFILES\\\${APP_NAME}"
Section "Main"
SetOutPath "$INSTDIR"
File "${config.executablePath}"
CreateDirectory "$SMPROGRAMS\\\${APP_NAME}"
CreateShortCut "$SMPROGRAMS\\\${APP_NAME}\\\${APP_NAME}.lnk" "$INSTDIR\\\${APP_EXECUTABLE}"
CreateShortCut "$DESKTOP\\\${APP_NAME}.lnk" "$INSTDIR\\\${APP_EXECUTABLE}"
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\\${APP_NAME}" "DisplayName" "\${APP_NAME}"
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\\${APP_NAME}" "UninstallString" "$INSTDIR\\uninstall.exe"
WriteUninstaller "$INSTDIR\\uninstall.exe"
SectionEnd
Section "Uninstall"
Delete "$INSTDIR\\\${APP_EXECUTABLE}"
Delete "$INSTDIR\\uninstall.exe"
RMDir "$INSTDIR"
Delete "$SMPROGRAMS\\\${APP_NAME}\\\${APP_NAME}.lnk"
RMDir "$SMPROGRAMS\\\${APP_NAME}"
Delete "$DESKTOP\\\${APP_NAME}.lnk"
DeleteRegKey HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\\${APP_NAME}"
SectionEnd
`;
}
// 格式化字节数
formatBytes(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
}
export default BuildManager;
9.4 部署策略
9.4.1 部署配置管理
// deployment-config.js
class DeploymentConfig {
constructor() {
this.environments = {
development: {
name: 'Development',
url: 'http://localhost:3000',
apiUrl: 'http://localhost:8080/api',
debug: true,
logLevel: 'debug'
},
staging: {
name: 'Staging',
url: 'https://staging.example.com',
apiUrl: 'https://staging-api.example.com',
debug: false,
logLevel: 'info'
},
production: {
name: 'Production',
url: 'https://app.example.com',
apiUrl: 'https://api.example.com',
debug: false,
logLevel: 'error'
}
};
this.currentEnvironment = 'development';
}
// 设置环境
setEnvironment(env) {
if (!this.environments[env]) {
throw new Error(`Unknown environment: ${env}`);
}
this.currentEnvironment = env;
return this.getCurrentConfig();
}
// 获取当前配置
getCurrentConfig() {
return {
environment: this.currentEnvironment,
...this.environments[this.currentEnvironment]
};
}
// 生成环境配置文件
generateConfigFile() {
const config = this.getCurrentConfig();
return `
// 自动生成的环境配置文件
// 环境: ${config.environment}
// 生成时间: ${new Date().toISOString()}
window.APP_CONFIG = ${JSON.stringify(config, null, 2)};
// 全局配置访问器
window.getConfig = function(key) {
return key ? window.APP_CONFIG[key] : window.APP_CONFIG;
};
// 环境检查函数
window.isDevelopment = function() {
return window.APP_CONFIG.environment === 'development';
};
window.isProduction = function() {
return window.APP_CONFIG.environment === 'production';
};
window.isStaging = function() {
return window.APP_CONFIG.environment === 'staging';
};
`;
}
}
export default DeploymentConfig;
9.4.2 自动部署脚本
// auto-deploy.js
import BuildManager from './build-manager.js';
import DeploymentConfig from './deployment-config.js';
import { promises as fs } from 'fs';
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
class AutoDeploy {
constructor() {
this.buildManager = new BuildManager();
this.deployConfig = new DeploymentConfig();
this.deploymentSteps = [];
}
// 执行自动部署
async deploy(environment, options = {}) {
try {
console.log(`开始部署到 ${environment} 环境...`);
const startTime = Date.now();
// 设置环境
this.deployConfig.setEnvironment(environment);
// 执行部署步骤
await this.executeDeploymentSteps(options);
const duration = Date.now() - startTime;
console.log(`部署完成,耗时: ${duration}ms`);
return true;
} catch (error) {
console.error('部署失败:', error.message);
throw error;
}
}
// 执行部署步骤
async executeDeploymentSteps(options) {
const steps = [
{ name: '生成环境配置', fn: () => this.generateEnvironmentConfig() },
{ name: '构建应用', fn: () => this.buildApplication() },
{ name: '运行测试', fn: () => this.runTests(), skip: options.skipTests },
{ name: '上传文件', fn: () => this.uploadFiles(), skip: options.skipUpload },
{ name: '更新服务', fn: () => this.updateService(), skip: options.skipService },
{ name: '验证部署', fn: () => this.verifyDeployment() }
];
for (const step of steps) {
if (step.skip) {
console.log(`跳过步骤: ${step.name}`);
continue;
}
console.log(`执行步骤: ${step.name}`);
await step.fn();
console.log(`✓ ${step.name} 完成`);
}
}
// 生成环境配置
async generateEnvironmentConfig() {
const configContent = this.deployConfig.generateConfigFile();
const configPath = path.join('src', 'config', 'environment.js');
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(configPath, configContent);
console.log(`环境配置已生成: ${configPath}`);
}
// 构建应用
async buildApplication() {
await this.buildManager.build();
}
// 运行测试
async runTests() {
console.log('运行单元测试...');
try {
const { stdout, stderr } = await execAsync('npm test');
console.log('测试输出:', stdout);
if (stderr) {
console.warn('测试警告:', stderr);
}
} catch (error) {
console.error('测试失败:', error.message);
throw new Error('测试未通过,部署中止');
}
}
// 上传文件
async uploadFiles() {
const config = this.deployConfig.getCurrentConfig();
if (config.environment === 'development') {
console.log('开发环境,跳过文件上传');
return;
}
// 这里可以集成各种上传方式
await this.uploadToServer();
}
// 上传到服务器
async uploadToServer() {
const config = this.deployConfig.getCurrentConfig();
// 示例:使用 SCP 上传
const localPath = 'dist/*';
const remotePath = `/var/www/${config.name.toLowerCase()}/`;
const serverHost = this.extractHostFromUrl(config.url);
console.log(`上传文件到服务器: ${serverHost}`);
try {
const command = `scp -r ${localPath} user@${serverHost}:${remotePath}`;
const { stdout, stderr } = await execAsync(command);
if (stderr) {
console.warn('上传警告:', stderr);
}
console.log('文件上传完成');
} catch (error) {
console.error('文件上传失败:', error.message);
throw error;
}
}
// 更新服务
async updateService() {
const config = this.deployConfig.getCurrentConfig();
if (config.environment === 'development') {
console.log('开发环境,跳过服务更新');
return;
}
console.log('更新远程服务...');
// 示例:重启服务
const serverHost = this.extractHostFromUrl(config.url);
const restartCommand = `ssh user@${serverHost} 'sudo systemctl restart myapp'`;
try {
await execAsync(restartCommand);
console.log('服务重启完成');
} catch (error) {
console.error('服务更新失败:', error.message);
throw error;
}
}
// 验证部署
async verifyDeployment() {
const config = this.deployConfig.getCurrentConfig();
console.log('验证部署状态...');
// 检查应用是否可访问
const isAccessible = await this.checkUrlAccessibility(config.url);
if (!isAccessible) {
throw new Error(`应用无法访问: ${config.url}`);
}
// 检查 API 是否正常
const isApiHealthy = await this.checkApiHealth(config.apiUrl);
if (!isApiHealthy) {
console.warn(`API 健康检查失败: ${config.apiUrl}`);
}
console.log('部署验证通过');
}
// 检查 URL 可访问性
async checkUrlAccessibility(url) {
try {
const response = await fetch(url, { method: 'HEAD' });
return response.ok;
} catch (error) {
console.error(`URL 检查失败: ${url}`, error.message);
return false;
}
}
// 检查 API 健康状态
async checkApiHealth(apiUrl) {
try {
const healthUrl = `${apiUrl}/health`;
const response = await fetch(healthUrl);
if (response.ok) {
const data = await response.json();
return data.status === 'healthy';
}
return false;
} catch (error) {
console.error(`API 健康检查失败: ${apiUrl}`, error.message);
return false;
}
}
// 从 URL 提取主机名
extractHostFromUrl(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (error) {
throw new Error(`无效的 URL: ${url}`);
}
}
}
export default AutoDeploy;
9.5 版本管理与发布
9.5.1 版本管理器
// version-manager.js
import { promises as fs } from 'fs';
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
class VersionManager {
constructor() {
this.packageJsonPath = 'package.json';
this.changelogPath = 'CHANGELOG.md';
}
// 获取当前版本
async getCurrentVersion() {
try {
const packageJson = await this.readPackageJson();
return packageJson.version;
} catch (error) {
throw new Error('无法读取当前版本');
}
}
// 更新版本
async updateVersion(type = 'patch') {
const currentVersion = await this.getCurrentVersion();
const newVersion = this.incrementVersion(currentVersion, type);
console.log(`版本更新: ${currentVersion} -> ${newVersion}`);
// 更新 package.json
await this.updatePackageVersion(newVersion);
// 更新 changelog
await this.updateChangelog(newVersion);
// 创建 Git 标签
await this.createGitTag(newVersion);
return newVersion;
}
// 版本号递增
incrementVersion(version, type) {
const parts = version.split('.').map(Number);
switch (type) {
case 'major':
parts[0]++;
parts[1] = 0;
parts[2] = 0;
break;
case 'minor':
parts[1]++;
parts[2] = 0;
break;
case 'patch':
default:
parts[2]++;
break;
}
return parts.join('.');
}
// 读取 package.json
async readPackageJson() {
const content = await fs.readFile(this.packageJsonPath, 'utf8');
return JSON.parse(content);
}
// 更新 package.json 版本
async updatePackageVersion(version) {
const packageJson = await this.readPackageJson();
packageJson.version = version;
const content = JSON.stringify(packageJson, null, 2);
await fs.writeFile(this.packageJsonPath, content);
console.log(`package.json 版本已更新为: ${version}`);
}
// 更新 changelog
async updateChangelog(version) {
const date = new Date().toISOString().split('T')[0];
const newEntry = `\n## [${version}] - ${date}\n\n### Added\n- 新功能描述\n\n### Changed\n- 变更描述\n\n### Fixed\n- 修复描述\n`;
try {
let changelog = await fs.readFile(this.changelogPath, 'utf8');
// 在第一个版本条目前插入新条目
const insertIndex = changelog.indexOf('## [');
if (insertIndex !== -1) {
changelog = changelog.slice(0, insertIndex) + newEntry + changelog.slice(insertIndex);
} else {
changelog += newEntry;
}
await fs.writeFile(this.changelogPath, changelog);
} catch (error) {
// 如果 changelog 不存在,创建新的
const newChangelog = `# Changelog\n\nAll notable changes to this project will be documented in this file.${newEntry}`;
await fs.writeFile(this.changelogPath, newChangelog);
}
console.log(`CHANGELOG.md 已更新`);
}
// 创建 Git 标签
async createGitTag(version) {
try {
// 添加文件到 Git
await execAsync('git add package.json CHANGELOG.md');
// 提交更改
await execAsync(`git commit -m "chore: bump version to ${version}"`);
// 创建标签
await execAsync(`git tag -a v${version} -m "Release version ${version}"`);
console.log(`Git 标签 v${version} 已创建`);
} catch (error) {
console.warn('Git 操作失败:', error.message);
}
}
// 获取版本历史
async getVersionHistory() {
try {
const { stdout } = await execAsync('git tag --sort=-version:refname');
const tags = stdout.trim().split('\n').filter(tag => tag.startsWith('v'));
const history = [];
for (const tag of tags) {
const { stdout: commitInfo } = await execAsync(`git log -1 --format="%ci|%s" ${tag}`);
const [date, message] = commitInfo.trim().split('|');
history.push({
version: tag.substring(1), // 移除 'v' 前缀
tag: tag,
date: new Date(date),
message: message
});
}
return history;
} catch (error) {
console.warn('无法获取版本历史:', error.message);
return [];
}
}
// 生成发布说明
async generateReleaseNotes(version) {
try {
const previousVersion = await this.getPreviousVersion(version);
const { stdout } = await execAsync(`git log v${previousVersion}..v${version} --pretty=format:"- %s (%h)"`);
const commits = stdout.trim().split('\n').filter(line => line.length > 0);
const releaseNotes = `
# Release Notes - v${version}
## Changes
${commits.join('\n')}
## Installation
\`\`\`bash
npm install myapp@${version}
\`\`\`
## Download
- [Windows Installer](releases/v${version}/myapp-${version}-setup.exe)
- [Portable Version](releases/v${version}/myapp-${version}-portable.zip)
`;
return releaseNotes;
} catch (error) {
console.warn('无法生成发布说明:', error.message);
return `# Release Notes - v${version}\n\n发布说明生成失败。`;
}
}
// 获取上一个版本
async getPreviousVersion(currentVersion) {
const history = await this.getVersionHistory();
const currentIndex = history.findIndex(item => item.version === currentVersion);
if (currentIndex > 0) {
return history[currentIndex + 1].version;
}
return '0.0.0';
}
}
export default VersionManager;
9.5.2 发布管理器
// release-manager.js
import VersionManager from './version-manager.js';
import BuildManager from './build-manager.js';
import AutoDeploy from './auto-deploy.js';
import { promises as fs } from 'fs';
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
class ReleaseManager {
constructor() {
this.versionManager = new VersionManager();
this.buildManager = new BuildManager();
this.autoDeploy = new AutoDeploy();
this.releaseDir = 'releases';
}
// 执行完整发布流程
async release(options = {}) {
try {
console.log('开始发布流程...');
const startTime = Date.now();
// 执行发布步骤
const releaseInfo = await this.executeReleaseSteps(options);
const duration = Date.now() - startTime;
console.log(`发布完成,耗时: ${duration}ms`);
return releaseInfo;
} catch (error) {
console.error('发布失败:', error.message);
throw error;
}
}
// 执行发布步骤
async executeReleaseSteps(options) {
const steps = [
{ name: '版本检查', fn: () => this.checkVersion(options) },
{ name: '更新版本', fn: () => this.updateVersion(options), skip: options.skipVersionUpdate },
{ name: '构建应用', fn: () => this.buildApplication() },
{ name: '运行测试', fn: () => this.runTests(), skip: options.skipTests },
{ name: '创建发布包', fn: () => this.createReleasePackage() },
{ name: '生成发布说明', fn: () => this.generateReleaseNotes() },
{ name: '发布到仓库', fn: () => this.publishToRepository(), skip: options.skipPublish },
{ name: '部署到生产', fn: () => this.deployToProduction(), skip: options.skipDeploy }
];
let releaseInfo = {};
for (const step of steps) {
if (step.skip) {
console.log(`跳过步骤: ${step.name}`);
continue;
}
console.log(`执行步骤: ${step.name}`);
const result = await step.fn();
if (result) {
Object.assign(releaseInfo, result);
}
console.log(`✓ ${step.name} 完成`);
}
return releaseInfo;
}
// 检查版本
async checkVersion(options) {
const currentVersion = await this.versionManager.getCurrentVersion();
console.log(`当前版本: ${currentVersion}`);
if (options.version) {
console.log(`目标版本: ${options.version}`);
}
return { currentVersion };
}
// 更新版本
async updateVersion(options) {
const versionType = options.versionType || 'patch';
const newVersion = await this.versionManager.updateVersion(versionType);
return { newVersion };
}
// 构建应用
async buildApplication() {
await this.buildManager.build();
return { buildCompleted: true };
}
// 运行测试
async runTests() {
console.log('运行完整测试套件...');
try {
// 单元测试
await execAsync('npm run test:unit');
console.log('✓ 单元测试通过');
// 集成测试
await execAsync('npm run test:integration');
console.log('✓ 集成测试通过');
// E2E 测试
await execAsync('npm run test:e2e');
console.log('✓ E2E 测试通过');
return { testsCompleted: true };
} catch (error) {
console.error('测试失败:', error.message);
throw new Error('测试未通过,发布中止');
}
}
// 创建发布包
async createReleasePackage() {
const version = await this.versionManager.getCurrentVersion();
const releaseVersionDir = path.join(this.releaseDir, `v${version}`);
// 创建发布目录
await fs.mkdir(releaseVersionDir, { recursive: true });
// 复制构建产物
await this.copyBuildArtifacts(releaseVersionDir);
// 创建压缩包
const packagePath = await this.createZipPackage(releaseVersionDir, version);
console.log(`发布包已创建: ${packagePath}`);
return {
releasePackage: packagePath,
releaseDir: releaseVersionDir
};
}
// 复制构建产物
async copyBuildArtifacts(targetDir) {
const artifacts = [
{ src: 'dist', dest: path.join(targetDir, 'dist') },
{ src: 'package.json', dest: path.join(targetDir, 'package.json') },
{ src: 'README.md', dest: path.join(targetDir, 'README.md') },
{ src: 'CHANGELOG.md', dest: path.join(targetDir, 'CHANGELOG.md') }
];
for (const artifact of artifacts) {
try {
await execAsync(`cp -r ${artifact.src} ${artifact.dest}`);
} catch (error) {
console.warn(`无法复制 ${artifact.src}:`, error.message);
}
}
}
// 创建 ZIP 包
async createZipPackage(sourceDir, version) {
const zipPath = path.join(this.releaseDir, `myapp-${version}.zip`);
try {
await execAsync(`cd ${sourceDir} && zip -r ../${path.basename(zipPath)} .`);
return zipPath;
} catch (error) {
console.error('创建 ZIP 包失败:', error.message);
throw error;
}
}
// 生成发布说明
async generateReleaseNotes() {
const version = await this.versionManager.getCurrentVersion();
const releaseNotes = await this.versionManager.generateReleaseNotes(version);
const notesPath = path.join(this.releaseDir, `v${version}`, 'RELEASE_NOTES.md');
await fs.writeFile(notesPath, releaseNotes);
console.log(`发布说明已生成: ${notesPath}`);
return { releaseNotes: notesPath };
}
// 发布到仓库
async publishToRepository() {
console.log('发布到 Git 仓库...');
try {
// 推送代码和标签
await execAsync('git push origin main');
await execAsync('git push origin --tags');
console.log('代码和标签已推送到远程仓库');
return { repositoryPublished: true };
} catch (error) {
console.error('仓库发布失败:', error.message);
throw error;
}
}
// 部署到生产环境
async deployToProduction() {
console.log('部署到生产环境...');
await this.autoDeploy.deploy('production', {
skipTests: true // 测试已经在发布流程中运行过
});
return { productionDeployed: true };
}
// 回滚发布
async rollback(version) {
console.log(`回滚到版本: ${version}`);
try {
// 检查版本是否存在
const history = await this.versionManager.getVersionHistory();
const targetVersion = history.find(item => item.version === version);
if (!targetVersion) {
throw new Error(`版本 ${version} 不存在`);
}
// 回滚代码
await execAsync(`git checkout v${version}`);
// 重新部署
await this.autoDeploy.deploy('production', {
skipTests: true,
skipUpload: false
});
console.log(`回滚到版本 ${version} 完成`);
return { rolledBack: true, version };
} catch (error) {
console.error('回滚失败:', error.message);
throw error;
}
}
}
export default ReleaseManager;
9.6 实践练习
9.6.1 创建完整的构建和部署流程
// build-deploy-example.js
import BuildManager from './build-manager.js';
import AutoDeploy from './auto-deploy.js';
import ReleaseManager from './release-manager.js';
// 示例:完整的构建和部署流程
async function fullBuildAndDeploy() {
try {
console.log('=== 开始完整构建和部署流程 ===');
// 1. 创建发布管理器
const releaseManager = new ReleaseManager();
// 2. 执行发布流程
const releaseInfo = await releaseManager.release({
versionType: 'minor', // 版本类型:major, minor, patch
skipTests: false, // 是否跳过测试
skipPublish: false, // 是否跳过发布到仓库
skipDeploy: false // 是否跳过部署
});
console.log('发布信息:', releaseInfo);
console.log('=== 构建和部署流程完成 ===');
} catch (error) {
console.error('构建和部署失败:', error.message);
process.exit(1);
}
}
// 示例:仅构建应用
async function buildOnly() {
try {
console.log('=== 开始构建应用 ===');
const buildManager = new BuildManager();
await buildManager.build();
console.log('=== 构建完成 ===');
} catch (error) {
console.error('构建失败:', error.message);
process.exit(1);
}
}
// 示例:仅部署到指定环境
async function deployOnly(environment) {
try {
console.log(`=== 开始部署到 ${environment} 环境 ===`);
const autoDeploy = new AutoDeploy();
await autoDeploy.deploy(environment);
console.log('=== 部署完成 ===');
} catch (error) {
console.error('部署失败:', error.message);
process.exit(1);
}
}
// 根据命令行参数执行不同操作
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case 'build':
buildOnly();
break;
case 'deploy':
const environment = args[1] || 'development';
deployOnly(environment);
break;
case 'release':
fullBuildAndDeploy();
break;
default:
console.log('使用方法:');
console.log(' node build-deploy-example.js build # 仅构建');
console.log(' node build-deploy-example.js deploy [env] # 仅部署');
console.log(' node build-deploy-example.js release # 完整发布流程');
break;
}
9.6.2 package.json 脚本配置
{
"name": "sciter-js-app",
"version": "1.0.0",
"description": "A Sciter-JS Application",
"scripts": {
"build": "node scripts/build-deploy-example.js build",
"deploy:dev": "node scripts/build-deploy-example.js deploy development",
"deploy:staging": "node scripts/build-deploy-example.js deploy staging",
"deploy:prod": "node scripts/build-deploy-example.js deploy production",
"release": "node scripts/build-deploy-example.js release",
"release:patch": "npm version patch && npm run release",
"release:minor": "npm version minor && npm run release",
"release:major": "npm version major && npm run release",
"test": "jest",
"test:unit": "jest --testPathPattern=unit",
"test:integration": "jest --testPathPattern=integration",
"test:e2e": "playwright test",
"lint": "eslint src/",
"format": "prettier --write src/"
},
"devDependencies": {
"terser": "^5.0.0",
"clean-css": "^5.0.0",
"html-minifier-terser": "^7.0.0",
"glob": "^8.0.0",
"jest": "^29.0.0",
"playwright": "^1.0.0",
"eslint": "^8.0.0",
"prettier": "^2.0.0"
}
}
9.7 本章小结
9.7.1 核心概念
- 应用打包:将源代码、资源和依赖打包成可执行文件
- 资源优化:压缩和优化应用资源以减小体积
- 部署自动化:自动化部署流程提高效率和可靠性
- 版本管理:系统化管理应用版本和发布历史
- 发布流程:标准化的发布流程确保质量
9.7.2 技术要点
- 构建配置管理和验证
- 资源收集、压缩和打包
- 多环境部署策略
- 自动化测试和验证
- 版本控制和标签管理
- 发布包创建和分发
9.7.3 最佳实践
- 配置管理:使用配置文件管理不同环境的设置
- 自动化测试:在部署前运行完整的测试套件
- 渐进式部署:先部署到测试环境再到生产环境
- 版本标记:为每个发布版本创建 Git 标签
- 回滚准备:保持回滚到上一版本的能力
- 监控验证:部署后验证应用状态和功能
9.7.4 下一章预告
下一章我们将学习「实战项目案例」,通过完整的项目实例来综合运用前面学到的所有知识和技能。