8.1 多平台打包配置
8.1.1 项目配置文件
manifest.json 配置
{
"name": "MyUniApp",
"appid": "__UNI__XXXXXXXX",
"description": "一个跨平台应用",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"networkTimeout": {
"request": 60000,
"connectSocket": 60000,
"uploadFile": 60000,
"downloadFile": 60000
},
"debug": false,
"uniStatistics": {
"enable": false
},
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\" />",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\" />",
"<uses-permission android:name=\"android.permission.VIBRATE\" />",
"<uses-permission android:name=\"android.permission.READ_LOGS\" />",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />",
"<uses-feature android:name=\"android.hardware.camera.autofocus\" />",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />",
"<uses-permission android:name=\"android.permission.CAMERA\" />",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\" />",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\" />",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\" />",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\" />",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\" />"
],
"abiFilters": ["armeabi-v7a", "arm64-v8a"],
"targetSdkVersion": 30,
"minSdkVersion": 21
},
"ios": {
"deploymentTarget": "9.0",
"dSYMs": false
},
"sdkConfigs": {
"ad": {},
"oauth": {},
"payment": {},
"push": {},
"share": {},
"statics": {}
}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "wxXXXXXXXXXXXXXXXX",
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": false,
"coverView": true,
"nodeModules": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"scopeDataCheck": false,
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"compileHotReLoad": false,
"lazyloadPlaceholderEnable": false,
"useMultiFrameRuntime": true,
"useApiHook": true,
"useApiHostProcess": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"enableEngineNative": false,
"useIsolateContext": true,
"userConfirmedBundleSwitch": false,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"disableUseStrict": false,
"minifyWXML": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
},
"requiredPrivateInfos": ["getLocation"]
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3"
}
pages.json 配置
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/user/user",
"style": {
"navigationBarTitleText": "个人中心"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "MyUniApp",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tab-home.png",
"selectedIconPath": "static/tab-home-current.png",
"text": "首页"
},
{
"pagePath": "pages/user/user",
"iconPath": "static/tab-user.png",
"selectedIconPath": "static/tab-user-current.png",
"text": "我的"
}
]
},
"condition": {
"current": 0,
"list": [
{
"name": "首页",
"path": "pages/index/index",
"query": ""
}
]
}
}
8.4 版本管理和发布策略
8.4.1 语义化版本控制
// utils/versionManager.js
class VersionManager {
constructor() {
this.versionPattern = /^(\d+)\.(\d+)\.(\d+)(?:-(\w+)(?:\.(\d+))?)?$/;
this.packageJsonPath = 'package.json';
this.manifestJsonPath = 'manifest.json';
}
// 解析版本号
parseVersion(version) {
const match = version.match(this.versionPattern);
if (!match) {
throw new Error(`无效的版本号格式: ${version}`);
}
return {
major: parseInt(match[1]),
minor: parseInt(match[2]),
patch: parseInt(match[3]),
prerelease: match[4] || null,
prereleaseNumber: match[5] ? parseInt(match[5]) : null,
raw: version
};
}
// 比较版本号
compareVersions(version1, version2) {
const v1 = this.parseVersion(version1);
const v2 = this.parseVersion(version2);
// 比较主版本号
if (v1.major !== v2.major) {
return v1.major - v2.major;
}
// 比较次版本号
if (v1.minor !== v2.minor) {
return v1.minor - v2.minor;
}
// 比较修订版本号
if (v1.patch !== v2.patch) {
return v1.patch - v2.patch;
}
// 比较预发布版本
if (v1.prerelease && !v2.prerelease) return -1;
if (!v1.prerelease && v2.prerelease) return 1;
if (v1.prerelease && v2.prerelease) {
const prereleaseOrder = { alpha: 1, beta: 2, rc: 3 };
const order1 = prereleaseOrder[v1.prerelease] || 0;
const order2 = prereleaseOrder[v2.prerelease] || 0;
if (order1 !== order2) {
return order1 - order2;
}
if (v1.prereleaseNumber !== v2.prereleaseNumber) {
return (v1.prereleaseNumber || 0) - (v2.prereleaseNumber || 0);
}
}
return 0;
}
// 增加版本号
bumpVersion(currentVersion, type = 'patch', prerelease = null) {
const version = this.parseVersion(currentVersion);
switch (type) {
case 'major':
version.major++;
version.minor = 0;
version.patch = 0;
version.prerelease = null;
version.prereleaseNumber = null;
break;
case 'minor':
version.minor++;
version.patch = 0;
version.prerelease = null;
version.prereleaseNumber = null;
break;
case 'patch':
version.patch++;
version.prerelease = null;
version.prereleaseNumber = null;
break;
case 'prerelease':
if (version.prerelease === prerelease) {
version.prereleaseNumber = (version.prereleaseNumber || 0) + 1;
} else {
version.prerelease = prerelease;
version.prereleaseNumber = 1;
}
break;
default:
throw new Error(`未知的版本类型: ${type}`);
}
return this.formatVersion(version);
}
// 格式化版本号
formatVersion(version) {
let versionString = `${version.major}.${version.minor}.${version.patch}`;
if (version.prerelease) {
versionString += `-${version.prerelease}`;
if (version.prereleaseNumber) {
versionString += `.${version.prereleaseNumber}`;
}
}
return versionString;
}
// 获取当前版本
getCurrentVersion() {
const fs = require('fs');
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8'));
return packageJson.version;
}
// 更新版本号
updateVersion(newVersion) {
const fs = require('fs');
// 更新 package.json
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8'));
packageJson.version = newVersion;
fs.writeFileSync(this.packageJsonPath, JSON.stringify(packageJson, null, 2));
// 更新 manifest.json
if (fs.existsSync(this.manifestJsonPath)) {
const manifestJson = JSON.parse(fs.readFileSync(this.manifestJsonPath, 'utf8'));
manifestJson.versionName = newVersion;
// 更新 versionCode (用于 Android)
const version = this.parseVersion(newVersion);
manifestJson.versionCode = version.major * 10000 + version.minor * 100 + version.patch;
fs.writeFileSync(this.manifestJsonPath, JSON.stringify(manifestJson, null, 2));
}
console.log(`版本已更新为: ${newVersion}`);
}
// 创建发布标签
createReleaseTag(version, message = null) {
const { execSync } = require('child_process');
const tagName = `v${version}`;
const tagMessage = message || `Release ${version}`;
try {
// 创建标签
execSync(`git tag -a ${tagName} -m "${tagMessage}"`, { stdio: 'inherit' });
// 推送标签
execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
console.log(`发布标签已创建: ${tagName}`);
} catch (error) {
console.error('创建发布标签失败:', error.message);
throw error;
}
}
// 生成变更日志
generateChangelog(fromVersion, toVersion) {
const { execSync } = require('child_process');
try {
const fromTag = fromVersion ? `v${fromVersion}` : '';
const toTag = `v${toVersion}`;
const gitLogCommand = fromTag
? `git log ${fromTag}..${toTag} --pretty=format:"%h %s" --no-merges`
: `git log ${toTag} --pretty=format:"%h %s" --no-merges`;
const commits = execSync(gitLogCommand, { encoding: 'utf8' })
.split('\n')
.filter(line => line.trim());
const changelog = {
version: toVersion,
date: new Date().toISOString().split('T')[0],
changes: this.categorizeCommits(commits)
};
return changelog;
} catch (error) {
console.error('生成变更日志失败:', error.message);
return null;
}
}
// 分类提交记录
categorizeCommits(commits) {
const categories = {
features: [],
fixes: [],
improvements: [],
others: []
};
commits.forEach(commit => {
const [hash, ...messageParts] = commit.split(' ');
const message = messageParts.join(' ');
if (message.match(/^(feat|feature)/i)) {
categories.features.push({ hash, message });
} else if (message.match(/^(fix|bugfix)/i)) {
categories.fixes.push({ hash, message });
} else if (message.match(/^(improve|enhancement|perf)/i)) {
categories.improvements.push({ hash, message });
} else {
categories.others.push({ hash, message });
}
});
return categories;
}
// 格式化变更日志
formatChangelog(changelog) {
let output = `## ${changelog.version} (${changelog.date})\n\n`;
if (changelog.changes.features.length > 0) {
output += '### ✨ 新功能\n\n';
changelog.changes.features.forEach(item => {
output += `- ${item.message} (${item.hash})\n`;
});
output += '\n';
}
if (changelog.changes.fixes.length > 0) {
output += '### 🐛 问题修复\n\n';
changelog.changes.fixes.forEach(item => {
output += `- ${item.message} (${item.hash})\n`;
});
output += '\n';
}
if (changelog.changes.improvements.length > 0) {
output += '### 🚀 性能优化\n\n';
changelog.changes.improvements.forEach(item => {
output += `- ${item.message} (${item.hash})\n`;
});
output += '\n';
}
if (changelog.changes.others.length > 0) {
output += '### 📝 其他变更\n\n';
changelog.changes.others.forEach(item => {
output += `- ${item.message} (${item.hash})\n`;
});
output += '\n';
}
return output;
}
// 发布流程
async release(type = 'patch', prerelease = null, message = null) {
const currentVersion = this.getCurrentVersion();
const newVersion = this.bumpVersion(currentVersion, type, prerelease);
console.log(`准备发布版本: ${currentVersion} -> ${newVersion}`);
try {
// 1. 更新版本号
this.updateVersion(newVersion);
// 2. 生成变更日志
const changelog = this.generateChangelog(currentVersion, newVersion);
if (changelog) {
const changelogText = this.formatChangelog(changelog);
console.log('\n变更日志:');
console.log(changelogText);
// 更新 CHANGELOG.md
this.updateChangelogFile(changelogText);
}
// 3. 提交变更
const { execSync } = require('child_process');
execSync('git add .', { stdio: 'inherit' });
execSync(`git commit -m "chore: release ${newVersion}"`, { stdio: 'inherit' });
// 4. 创建标签
this.createReleaseTag(newVersion, message);
console.log(`\n✅ 版本 ${newVersion} 发布成功!`);
} catch (error) {
console.error('发布失败:', error.message);
throw error;
}
}
// 更新变更日志文件
updateChangelogFile(newChangelog) {
const fs = require('fs');
const changelogPath = 'CHANGELOG.md';
let existingChangelog = '';
if (fs.existsSync(changelogPath)) {
existingChangelog = fs.readFileSync(changelogPath, 'utf8');
} else {
existingChangelog = '# 变更日志\n\n';
}
// 在现有变更日志前插入新的变更
const lines = existingChangelog.split('\n');
const headerIndex = lines.findIndex(line => line.startsWith('# '));
if (headerIndex !== -1) {
lines.splice(headerIndex + 1, 0, '', newChangelog.trim());
} else {
lines.unshift(newChangelog.trim(), '');
}
fs.writeFileSync(changelogPath, lines.join('\n'));
}
}
export default VersionManager;
8.4.2 分支管理策略
// utils/branchManager.js
class BranchManager {
constructor() {
this.branches = {
main: 'main',
develop: 'develop',
feature: 'feature/',
release: 'release/',
hotfix: 'hotfix/'
};
}
// 获取当前分支
getCurrentBranch() {
const { execSync } = require('child_process');
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
}
// 检查分支是否存在
branchExists(branchName) {
const { execSync } = require('child_process');
try {
execSync(`git rev-parse --verify ${branchName}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
// 创建功能分支
createFeatureBranch(featureName) {
const { execSync } = require('child_process');
const branchName = `${this.branches.feature}${featureName}`;
if (this.branchExists(branchName)) {
throw new Error(`分支 ${branchName} 已存在`);
}
try {
// 切换到 develop 分支
execSync(`git checkout ${this.branches.develop}`, { stdio: 'inherit' });
// 拉取最新代码
execSync('git pull origin develop', { stdio: 'inherit' });
// 创建并切换到功能分支
execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
console.log(`功能分支 ${branchName} 创建成功`);
return branchName;
} catch (error) {
console.error('创建功能分支失败:', error.message);
throw error;
}
}
// 完成功能分支
finishFeatureBranch(featureName) {
const { execSync } = require('child_process');
const branchName = `${this.branches.feature}${featureName}`;
if (!this.branchExists(branchName)) {
throw new Error(`分支 ${branchName} 不存在`);
}
try {
// 切换到功能分支
execSync(`git checkout ${branchName}`, { stdio: 'inherit' });
// 推送功能分支
execSync(`git push origin ${branchName}`, { stdio: 'inherit' });
// 切换到 develop 分支
execSync(`git checkout ${this.branches.develop}`, { stdio: 'inherit' });
// 拉取最新代码
execSync('git pull origin develop', { stdio: 'inherit' });
// 合并功能分支
execSync(`git merge --no-ff ${branchName}`, { stdio: 'inherit' });
// 推送合并结果
execSync('git push origin develop', { stdio: 'inherit' });
// 删除本地功能分支
execSync(`git branch -d ${branchName}`, { stdio: 'inherit' });
// 删除远程功能分支
execSync(`git push origin --delete ${branchName}`, { stdio: 'inherit' });
console.log(`功能分支 ${branchName} 已完成并合并到 develop`);
} catch (error) {
console.error('完成功能分支失败:', error.message);
throw error;
}
}
// 创建发布分支
createReleaseBranch(version) {
const { execSync } = require('child_process');
const branchName = `${this.branches.release}${version}`;
if (this.branchExists(branchName)) {
throw new Error(`分支 ${branchName} 已存在`);
}
try {
// 切换到 develop 分支
execSync(`git checkout ${this.branches.develop}`, { stdio: 'inherit' });
// 拉取最新代码
execSync('git pull origin develop', { stdio: 'inherit' });
// 创建并切换到发布分支
execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
// 更新版本号
const versionManager = new (require('./versionManager.js').default)();
versionManager.updateVersion(version);
// 提交版本更新
execSync('git add .', { stdio: 'inherit' });
execSync(`git commit -m "chore: bump version to ${version}"`, { stdio: 'inherit' });
// 推送发布分支
execSync(`git push origin ${branchName}`, { stdio: 'inherit' });
console.log(`发布分支 ${branchName} 创建成功`);
return branchName;
} catch (error) {
console.error('创建发布分支失败:', error.message);
throw error;
}
}
// 完成发布分支
finishReleaseBranch(version) {
const { execSync } = require('child_process');
const branchName = `${this.branches.release}${version}`;
if (!this.branchExists(branchName)) {
throw new Error(`分支 ${branchName} 不存在`);
}
try {
// 切换到发布分支
execSync(`git checkout ${branchName}`, { stdio: 'inherit' });
// 合并到 main 分支
execSync(`git checkout ${this.branches.main}`, { stdio: 'inherit' });
execSync('git pull origin main', { stdio: 'inherit' });
execSync(`git merge --no-ff ${branchName}`, { stdio: 'inherit' });
// 创建发布标签
const versionManager = new (require('./versionManager.js').default)();
versionManager.createReleaseTag(version);
// 推送 main 分支
execSync('git push origin main', { stdio: 'inherit' });
// 合并回 develop 分支
execSync(`git checkout ${this.branches.develop}`, { stdio: 'inherit' });
execSync('git pull origin develop', { stdio: 'inherit' });
execSync(`git merge --no-ff ${branchName}`, { stdio: 'inherit' });
execSync('git push origin develop', { stdio: 'inherit' });
// 删除发布分支
execSync(`git branch -d ${branchName}`, { stdio: 'inherit' });
execSync(`git push origin --delete ${branchName}`, { stdio: 'inherit' });
console.log(`发布分支 ${branchName} 已完成`);
} catch (error) {
console.error('完成发布分支失败:', error.message);
throw error;
}
}
// 创建热修复分支
createHotfixBranch(version) {
const { execSync } = require('child_process');
const branchName = `${this.branches.hotfix}${version}`;
if (this.branchExists(branchName)) {
throw new Error(`分支 ${branchName} 已存在`);
}
try {
// 切换到 main 分支
execSync(`git checkout ${this.branches.main}`, { stdio: 'inherit' });
// 拉取最新代码
execSync('git pull origin main', { stdio: 'inherit' });
// 创建并切换到热修复分支
execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
console.log(`热修复分支 ${branchName} 创建成功`);
return branchName;
} catch (error) {
console.error('创建热修复分支失败:', error.message);
throw error;
}
}
// 完成热修复分支
finishHotfixBranch(version) {
const { execSync } = require('child_process');
const branchName = `${this.branches.hotfix}${version}`;
if (!this.branchExists(branchName)) {
throw new Error(`分支 ${branchName} 不存在`);
}
try {
// 切换到热修复分支
execSync(`git checkout ${branchName}`, { stdio: 'inherit' });
// 更新版本号
const versionManager = new (require('./versionManager.js').default)();
versionManager.updateVersion(version);
// 提交版本更新
execSync('git add .', { stdio: 'inherit' });
execSync(`git commit -m "chore: bump version to ${version}"`, { stdio: 'inherit' });
// 合并到 main 分支
execSync(`git checkout ${this.branches.main}`, { stdio: 'inherit' });
execSync('git pull origin main', { stdio: 'inherit' });
execSync(`git merge --no-ff ${branchName}`, { stdio: 'inherit' });
// 创建发布标签
versionManager.createReleaseTag(version);
// 推送 main 分支
execSync('git push origin main', { stdio: 'inherit' });
// 合并回 develop 分支
execSync(`git checkout ${this.branches.develop}`, { stdio: 'inherit' });
execSync('git pull origin develop', { stdio: 'inherit' });
execSync(`git merge --no-ff ${branchName}`, { stdio: 'inherit' });
execSync('git push origin develop', { stdio: 'inherit' });
// 删除热修复分支
execSync(`git branch -d ${branchName}`, { stdio: 'inherit' });
execSync(`git push origin --delete ${branchName}`, { stdio: 'inherit' });
console.log(`热修复分支 ${branchName} 已完成`);
} catch (error) {
console.error('完成热修复分支失败:', error.message);
throw error;
}
}
}
export default BranchManager;
8.5 应用商店上架指南
8.5.1 Android 应用商店
Google Play Store
// utils/playStoreUploader.js
class PlayStoreUploader {
constructor(config) {
this.config = {
packageName: config.packageName,
serviceAccountKey: config.serviceAccountKey,
apkPath: config.apkPath,
track: config.track || 'internal', // internal, alpha, beta, production
...config
};
}
// 上传 APK 到 Google Play
async uploadApk() {
const { google } = require('googleapis');
const fs = require('fs');
try {
// 认证
const auth = new google.auth.GoogleAuth({
keyFile: this.config.serviceAccountKey,
scopes: ['https://www.googleapis.com/auth/androidpublisher']
});
const androidpublisher = google.androidpublisher({
version: 'v3',
auth
});
// 创建编辑会话
const editResponse = await androidpublisher.edits.insert({
packageName: this.config.packageName
});
const editId = editResponse.data.id;
// 上传 APK
const apkResponse = await androidpublisher.edits.apks.upload({
packageName: this.config.packageName,
editId,
media: {
mimeType: 'application/vnd.android.package-archive',
body: fs.createReadStream(this.config.apkPath)
}
});
const versionCode = apkResponse.data.versionCode;
// 分配到轨道
await androidpublisher.edits.tracks.update({
packageName: this.config.packageName,
editId,
track: this.config.track,
requestBody: {
releases: [{
versionCodes: [versionCode],
status: 'completed'
}]
}
});
// 提交编辑
await androidpublisher.edits.commit({
packageName: this.config.packageName,
editId
});
console.log(`APK 已成功上传到 ${this.config.track} 轨道`);
return { success: true, versionCode };
} catch (error) {
console.error('上传 APK 失败:', error.message);
throw error;
}
}
// 更新应用信息
async updateListing(listingData) {
const { google } = require('googleapis');
try {
const auth = new google.auth.GoogleAuth({
keyFile: this.config.serviceAccountKey,
scopes: ['https://www.googleapis.com/auth/androidpublisher']
});
const androidpublisher = google.androidpublisher({
version: 'v3',
auth
});
// 创建编辑会话
const editResponse = await androidpublisher.edits.insert({
packageName: this.config.packageName
});
const editId = editResponse.data.id;
// 更新应用信息
for (const [language, data] of Object.entries(listingData)) {
await androidpublisher.edits.listings.update({
packageName: this.config.packageName,
editId,
language,
requestBody: data
});
}
// 提交编辑
await androidpublisher.edits.commit({
packageName: this.config.packageName,
editId
});
console.log('应用信息已更新');
} catch (error) {
console.error('更新应用信息失败:', error.message);
throw error;
}
}
// 获取发布状态
async getReleaseStatus() {
const { google } = require('googleapis');
try {
const auth = new google.auth.GoogleAuth({
keyFile: this.config.serviceAccountKey,
scopes: ['https://www.googleapis.com/auth/androidpublisher']
});
const androidpublisher = google.androidpublisher({
version: 'v3',
auth
});
const response = await androidpublisher.edits.tracks.list({
packageName: this.config.packageName
});
return response.data.tracks;
} catch (error) {
console.error('获取发布状态失败:', error.message);
throw error;
}
}
}
export default PlayStoreUploader;
华为应用市场
// utils/huaweiStoreUploader.js
class HuaweiStoreUploader {
constructor(config) {
this.config = {
clientId: config.clientId,
clientSecret: config.clientSecret,
appId: config.appId,
apkPath: config.apkPath,
baseUrl: 'https://connect-api.cloud.huawei.com/api/publish/v2',
...config
};
this.accessToken = null;
}
// 获取访问令牌
async getAccessToken() {
const axios = require('axios');
try {
const response = await axios.post(
'https://oauth-login.cloud.huawei.com/oauth2/v3/token',
{
grant_type: 'client_credentials',
client_id: this.config.clientId,
client_secret: this.config.clientSecret
},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
this.accessToken = response.data.access_token;
return this.accessToken;
} catch (error) {
console.error('获取访问令牌失败:', error.message);
throw error;
}
}
// 上传 APK
async uploadApk() {
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
if (!this.accessToken) {
await this.getAccessToken();
}
try {
const formData = new FormData();
formData.append('file', fs.createReadStream(this.config.apkPath));
formData.append('appId', this.config.appId);
const response = await axios.post(
`${this.config.baseUrl}/upload-url`,
formData,
{
headers: {
...formData.getHeaders(),
'Authorization': `Bearer ${this.accessToken}`
}
}
);
console.log('APK 上传成功');
return response.data;
} catch (error) {
console.error('上传 APK 失败:', error.message);
throw error;
}
}
// 提交审核
async submitForReview() {
const axios = require('axios');
if (!this.accessToken) {
await this.getAccessToken();
}
try {
const response = await axios.post(
`${this.config.baseUrl}/app-submit`,
{
appId: this.config.appId,
releaseType: 1 // 1: 全量发布, 2: 分阶段发布
},
{
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
}
}
);
console.log('已提交审核');
return response.data;
} catch (error) {
console.error('提交审核失败:', error.message);
throw error;
}
}
}
export default HuaweiStoreUploader;
8.5.2 iOS App Store
// utils/appStoreUploader.js
class AppStoreUploader {
constructor(config) {
this.config = {
apiKey: config.apiKey,
apiKeyId: config.apiKeyId,
issuerId: config.issuerId,
bundleId: config.bundleId,
ipaPath: config.ipaPath,
...config
};
}
// 生成 JWT 令牌
generateJWT() {
const jwt = require('jsonwebtoken');
const fs = require('fs');
const privateKey = fs.readFileSync(this.config.apiKey, 'utf8');
const payload = {
iss: this.config.issuerId,
exp: Math.floor(Date.now() / 1000) + (20 * 60), // 20 分钟过期
aud: 'appstoreconnect-v1'
};
return jwt.sign(payload, privateKey, {
algorithm: 'ES256',
header: {
kid: this.config.apiKeyId
}
});
}
// 上传 IPA 到 App Store Connect
async uploadIpa() {
const { execSync } = require('child_process');
try {
// 使用 altool 上传
const command = `xcrun altool --upload-app -f "${this.config.ipaPath}" --type ios --apiKey ${this.config.apiKeyId} --apiIssuer ${this.config.issuerId}`;
execSync(command, { stdio: 'inherit' });
console.log('IPA 上传成功');
} catch (error) {
console.error('上传 IPA 失败:', error.message);
throw error;
}
}
// 创建新版本
async createVersion(versionString) {
const axios = require('axios');
try {
const token = this.generateJWT();
// 获取应用信息
const appResponse = await axios.get(
`https://api.appstoreconnect.apple.com/v1/apps?filter[bundleId]=${this.config.bundleId}`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
const appId = appResponse.data.data[0].id;
// 创建新版本
const versionResponse = await axios.post(
'https://api.appstoreconnect.apple.com/v1/appStoreVersions',
{
data: {
type: 'appStoreVersions',
attributes: {
platform: 'IOS',
versionString
},
relationships: {
app: {
data: {
type: 'apps',
id: appId
}
}
}
}
},
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log(`版本 ${versionString} 创建成功`);
return versionResponse.data.data;
} catch (error) {
console.error('创建版本失败:', error.message);
throw error;
}
}
// 提交审核
async submitForReview(versionId) {
const axios = require('axios');
try {
const token = this.generateJWT();
const response = await axios.post(
'https://api.appstoreconnect.apple.com/v1/appStoreVersionSubmissions',
{
data: {
type: 'appStoreVersionSubmissions',
relationships: {
appStoreVersion: {
data: {
type: 'appStoreVersions',
id: versionId
}
}
}
}
},
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
console.log('已提交 App Store 审核');
return response.data.data;
} catch (error) {
console.error('提交审核失败:', error.message);
throw error;
}
}
}
export default AppStoreUploader;
8.5.3 小程序平台
微信小程序
// utils/wechatMiniUploader.js
class WechatMiniUploader {
constructor(config) {
this.config = {
appId: config.appId,
privateKey: config.privateKey,
projectPath: config.projectPath,
version: config.version,
desc: config.desc || '',
...config
};
}
// 上传小程序代码
async uploadCode() {
const ci = require('miniprogram-ci');
try {
const project = new ci.Project({
appid: this.config.appId,
type: 'miniProgram',
projectPath: this.config.projectPath,
privateKeyPath: this.config.privateKey,
ignores: ['node_modules/**/*']
});
const uploadResult = await ci.upload({
project,
version: this.config.version,
desc: this.config.desc,
setting: {
es6: true,
es7: true,
minifyJS: true,
minifyWXML: true,
minifyWXSS: true,
minify: true,
codeProtect: true,
autoPrefixWXSS: true
},
onProgressUpdate: (progress) => {
console.log(`上传进度: ${progress}%`);
}
});
console.log('小程序代码上传成功');
return uploadResult;
} catch (error) {
console.error('上传小程序代码失败:', error.message);
throw error;
}
}
// 预览小程序
async preview() {
const ci = require('miniprogram-ci');
try {
const project = new ci.Project({
appid: this.config.appId,
type: 'miniProgram',
projectPath: this.config.projectPath,
privateKeyPath: this.config.privateKey
});
const previewResult = await ci.preview({
project,
desc: this.config.desc,
setting: {
es6: true,
minifyJS: true,
minifyWXML: true,
minifyWXSS: true
},
qrcodeFormat: 'image',
qrcodeOutputDest: './preview-qrcode.jpg',
onProgressUpdate: (progress) => {
console.log(`预览生成进度: ${progress}%`);
}
});
console.log('小程序预览二维码已生成');
return previewResult;
} catch (error) {
console.error('生成预览失败:', error.message);
throw error;
}
}
// 提交审核
async submitAudit() {
const axios = require('axios');
try {
// 获取访问令牌
const tokenResponse = await this.getAccessToken();
const accessToken = tokenResponse.access_token;
// 提交审核
const auditResponse = await axios.post(
`https://api.weixin.qq.com/wxa/submit_audit?access_token=${accessToken}`,
{
item_list: [
{
address: 'pages/index/index',
tag: '首页',
first_class: '工具',
second_class: '效率',
title: '应用首页'
}
]
}
);
if (auditResponse.data.errcode === 0) {
console.log('小程序已提交审核');
return auditResponse.data;
} else {
throw new Error(auditResponse.data.errmsg);
}
} catch (error) {
console.error('提交审核失败:', error.message);
throw error;
}
}
// 获取访问令牌
async getAccessToken() {
const axios = require('axios');
try {
const response = await axios.get(
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.config.appId}&secret=${this.config.appSecret}`
);
if (response.data.access_token) {
return response.data;
} else {
throw new Error(response.data.errmsg);
}
} catch (error) {
console.error('获取访问令牌失败:', error.message);
throw error;
}
}
}
export default WechatMiniUploader;
支付宝小程序
// utils/alipayMiniUploader.js
class AlipayMiniUploader {
constructor(config) {
this.config = {
appId: config.appId,
privateKey: config.privateKey,
toolId: config.toolId,
projectPath: config.projectPath,
version: config.version,
...config
};
}
// 上传小程序
async uploadMini() {
const { execSync } = require('child_process');
try {
// 使用支付宝开发者工具命令行上传
const command = `miniu upload --project ${this.config.projectPath} --appId ${this.config.appId} --privateKey ${this.config.privateKey} --toolId ${this.config.toolId} --clientType alipay --version ${this.config.version}`;
execSync(command, { stdio: 'inherit' });
console.log('支付宝小程序上传成功');
} catch (error) {
console.error('上传支付宝小程序失败:', error.message);
throw error;
}
}
// 预览小程序
async preview() {
const { execSync } = require('child_process');
try {
const command = `miniu preview --project ${this.config.projectPath} --appId ${this.config.appId} --privateKey ${this.config.privateKey} --toolId ${this.config.toolId} --clientType alipay`;
execSync(command, { stdio: 'inherit' });
console.log('支付宝小程序预览二维码已生成');
} catch (error) {
console.error('生成预览失败:', error.message);
throw error;
}
}
}
export default AlipayMiniUploader;
8.6 发布后的维护和监控
8.6.1 应用监控
// utils/appMonitor.js
class AppMonitor {
constructor(config) {
this.config = {
appId: config.appId,
apiKey: config.apiKey,
platforms: config.platforms || ['android', 'ios', 'h5'],
...config
};
this.metrics = {
crashes: [],
performance: [],
userBehavior: [],
errors: []
};
}
// 初始化监控
init() {
this.setupCrashReporting();
this.setupPerformanceMonitoring();
this.setupUserBehaviorTracking();
this.setupErrorTracking();
}
// 崩溃监控
setupCrashReporting() {
// 全局错误捕获
if (typeof window !== 'undefined') {
window.addEventListener('error', (event) => {
this.reportCrash({
type: 'javascript_error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
});
});
window.addEventListener('unhandledrejection', (event) => {
this.reportCrash({
type: 'unhandled_promise_rejection',
reason: event.reason,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
});
});
}
// Vue 错误捕获
if (typeof uni !== 'undefined') {
const originalOnError = uni.onError;
uni.onError = (error) => {
this.reportCrash({
type: 'vue_error',
message: error.message,
stack: error.stack,
timestamp: Date.now(),
platform: uni.getSystemInfoSync().platform
});
if (originalOnError) {
originalOnError(error);
}
};
}
}
// 性能监控
setupPerformanceMonitoring() {
// 页面加载性能
if (typeof window !== 'undefined' && window.performance) {
window.addEventListener('load', () => {
setTimeout(() => {
const timing = window.performance.timing;
const metrics = {
type: 'page_load',
loadTime: timing.loadEventEnd - timing.navigationStart,
domReady: timing.domContentLoadedEventEnd - timing.navigationStart,
firstPaint: this.getFirstPaint(),
timestamp: Date.now(),
url: window.location.href
};
this.reportPerformance(metrics);
}, 0);
});
}
// API 请求性能
this.monitorApiPerformance();
}
// 用户行为追踪
setupUserBehaviorTracking() {
// 页面访问
this.trackPageView = (pageName, params = {}) => {
this.reportUserBehavior({
type: 'page_view',
pageName,
params,
timestamp: Date.now(),
sessionId: this.getSessionId()
});
};
// 用户点击
this.trackClick = (element, data = {}) => {
this.reportUserBehavior({
type: 'click',
element,
data,
timestamp: Date.now(),
sessionId: this.getSessionId()
});
};
// 自定义事件
this.trackEvent = (eventName, properties = {}) => {
this.reportUserBehavior({
type: 'custom_event',
eventName,
properties,
timestamp: Date.now(),
sessionId: this.getSessionId()
});
};
}
// 错误追踪
setupErrorTracking() {
this.trackError = (error, context = {}) => {
this.reportError({
type: 'custom_error',
message: error.message,
stack: error.stack,
context,
timestamp: Date.now(),
sessionId: this.getSessionId()
});
};
}
// 上报崩溃
reportCrash(crashData) {
this.metrics.crashes.push(crashData);
this.sendToServer('crash', crashData);
}
// 上报性能数据
reportPerformance(performanceData) {
this.metrics.performance.push(performanceData);
this.sendToServer('performance', performanceData);
}
// 上报用户行为
reportUserBehavior(behaviorData) {
this.metrics.userBehavior.push(behaviorData);
this.sendToServer('behavior', behaviorData);
}
// 上报错误
reportError(errorData) {
this.metrics.errors.push(errorData);
this.sendToServer('error', errorData);
}
// 发送数据到服务器
async sendToServer(type, data) {
try {
const payload = {
appId: this.config.appId,
type,
data,
deviceInfo: this.getDeviceInfo(),
appVersion: this.getAppVersion()
};
// 使用 uni.request 发送数据
if (typeof uni !== 'undefined') {
uni.request({
url: `${this.config.apiEndpoint}/monitor/${type}`,
method: 'POST',
header: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json'
},
data: payload,
success: (res) => {
console.log(`监控数据上报成功: ${type}`);
},
fail: (err) => {
console.error(`监控数据上报失败: ${type}`, err);
// 存储到本地,稍后重试
this.storeForRetry(type, payload);
}
});
}
} catch (error) {
console.error('发送监控数据失败:', error);
this.storeForRetry(type, data);
}
}
// 获取设备信息
getDeviceInfo() {
if (typeof uni !== 'undefined') {
return uni.getSystemInfoSync();
}
if (typeof window !== 'undefined') {
return {
platform: 'h5',
userAgent: navigator.userAgent,
screenWidth: window.screen.width,
screenHeight: window.screen.height,
windowWidth: window.innerWidth,
windowHeight: window.innerHeight
};
}
return {};
}
// 获取应用版本
getAppVersion() {
// 从 manifest.json 或 package.json 获取版本信息
return this.config.version || '1.0.0';
}
// 获取会话ID
getSessionId() {
if (!this.sessionId) {
this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
return this.sessionId;
}
// 获取首次绘制时间
getFirstPaint() {
if (typeof window !== 'undefined' && window.performance) {
const paintEntries = window.performance.getEntriesByType('paint');
const firstPaint = paintEntries.find(entry => entry.name === 'first-paint');
return firstPaint ? firstPaint.startTime : null;
}
return null;
}
// 监控 API 性能
monitorApiPerformance() {
// 拦截 uni.request
if (typeof uni !== 'undefined') {
const originalRequest = uni.request;
uni.request = (options) => {
const startTime = Date.now();
const originalSuccess = options.success;
const originalFail = options.fail;
options.success = (res) => {
const endTime = Date.now();
this.reportPerformance({
type: 'api_request',
url: options.url,
method: options.method || 'GET',
duration: endTime - startTime,
statusCode: res.statusCode,
success: true,
timestamp: startTime
});
if (originalSuccess) {
originalSuccess(res);
}
};
options.fail = (err) => {
const endTime = Date.now();
this.reportPerformance({
type: 'api_request',
url: options.url,
method: options.method || 'GET',
duration: endTime - startTime,
success: false,
error: err,
timestamp: startTime
});
if (originalFail) {
originalFail(err);
}
};
return originalRequest(options);
};
}
}
// 存储失败的数据以便重试
storeForRetry(type, data) {
try {
const key = `monitor_retry_${type}`;
let retryData = [];
if (typeof uni !== 'undefined') {
const stored = uni.getStorageSync(key);
if (stored) {
retryData = JSON.parse(stored);
}
}
retryData.push({
data,
timestamp: Date.now(),
retryCount: 0
});
// 限制存储数量
if (retryData.length > 100) {
retryData = retryData.slice(-100);
}
if (typeof uni !== 'undefined') {
uni.setStorageSync(key, JSON.stringify(retryData));
}
} catch (error) {
console.error('存储重试数据失败:', error);
}
}
// 重试发送失败的数据
async retryFailedData() {
const types = ['crash', 'performance', 'behavior', 'error'];
for (const type of types) {
try {
const key = `monitor_retry_${type}`;
let retryData = [];
if (typeof uni !== 'undefined') {
const stored = uni.getStorageSync(key);
if (stored) {
retryData = JSON.parse(stored);
}
}
if (retryData.length > 0) {
const successfulItems = [];
for (const item of retryData) {
if (item.retryCount < 3) {
try {
await this.sendToServer(type, item.data);
successfulItems.push(item);
} catch (error) {
item.retryCount++;
}
} else {
// 超过重试次数,丢弃
successfulItems.push(item);
}
}
// 移除成功发送的数据
const remainingData = retryData.filter(item => !successfulItems.includes(item));
if (typeof uni !== 'undefined') {
if (remainingData.length > 0) {
uni.setStorageSync(key, JSON.stringify(remainingData));
} else {
uni.removeStorageSync(key);
}
}
}
} catch (error) {
console.error(`重试 ${type} 数据失败:`, error);
}
}
}
// 生成监控报告
generateReport(timeRange = 24 * 60 * 60 * 1000) { // 默认24小时
const now = Date.now();
const startTime = now - timeRange;
const filterByTime = (items) => {
return items.filter(item => item.timestamp >= startTime);
};
const crashes = filterByTime(this.metrics.crashes);
const performance = filterByTime(this.metrics.performance);
const userBehavior = filterByTime(this.metrics.userBehavior);
const errors = filterByTime(this.metrics.errors);
return {
timeRange: {
start: new Date(startTime).toISOString(),
end: new Date(now).toISOString()
},
summary: {
totalCrashes: crashes.length,
totalErrors: errors.length,
totalPageViews: userBehavior.filter(item => item.type === 'page_view').length,
averageLoadTime: this.calculateAverageLoadTime(performance)
},
crashes: this.groupCrashesByType(crashes),
performance: this.analyzePerformance(performance),
userBehavior: this.analyzeUserBehavior(userBehavior),
errors: this.groupErrorsByType(errors)
};
}
// 计算平均加载时间
calculateAverageLoadTime(performanceData) {
const loadTimes = performanceData
.filter(item => item.type === 'page_load' && item.loadTime)
.map(item => item.loadTime);
if (loadTimes.length === 0) return 0;
return loadTimes.reduce((sum, time) => sum + time, 0) / loadTimes.length;
}
// 按类型分组崩溃
groupCrashesByType(crashes) {
const groups = {};
crashes.forEach(crash => {
const type = crash.type || 'unknown';
if (!groups[type]) {
groups[type] = [];
}
groups[type].push(crash);
});
return groups;
}
// 分析性能数据
analyzePerformance(performanceData) {
const apiRequests = performanceData.filter(item => item.type === 'api_request');
const pageLoads = performanceData.filter(item => item.type === 'page_load');
return {
apiRequests: {
total: apiRequests.length,
successful: apiRequests.filter(item => item.success).length,
failed: apiRequests.filter(item => !item.success).length,
averageDuration: apiRequests.length > 0
? apiRequests.reduce((sum, item) => sum + item.duration, 0) / apiRequests.length
: 0
},
pageLoads: {
total: pageLoads.length,
averageLoadTime: pageLoads.length > 0
? pageLoads.reduce((sum, item) => sum + item.loadTime, 0) / pageLoads.length
: 0
}
};
}
// 分析用户行为
analyzeUserBehavior(userBehaviorData) {
const pageViews = userBehaviorData.filter(item => item.type === 'page_view');
const clicks = userBehaviorData.filter(item => item.type === 'click');
const customEvents = userBehaviorData.filter(item => item.type === 'custom_event');
return {
pageViews: {
total: pageViews.length,
uniquePages: [...new Set(pageViews.map(item => item.pageName))].length
},
clicks: {
total: clicks.length
},
customEvents: {
total: customEvents.length,
uniqueEvents: [...new Set(customEvents.map(item => item.eventName))].length
}
};
}
// 按类型分组错误
groupErrorsByType(errors) {
const groups = {};
errors.forEach(error => {
const type = error.type || 'unknown';
if (!groups[type]) {
groups[type] = [];
}
groups[type].push(error);
});
return groups;
}
}
export default AppMonitor;
8.6.2 用户反馈收集
// utils/feedbackCollector.js
class FeedbackCollector {
constructor(config) {
this.config = {
apiEndpoint: config.apiEndpoint,
appId: config.appId,
...config
};
}
// 收集用户反馈
async collectFeedback(feedback) {
const feedbackData = {
appId: this.config.appId,
userId: feedback.userId,
type: feedback.type, // bug, suggestion, complaint, praise
title: feedback.title,
content: feedback.content,
rating: feedback.rating,
screenshots: feedback.screenshots || [],
deviceInfo: this.getDeviceInfo(),
appVersion: this.getAppVersion(),
timestamp: Date.now(),
contact: feedback.contact
};
try {
if (typeof uni !== 'undefined') {
const result = await new Promise((resolve, reject) => {
uni.request({
url: `${this.config.apiEndpoint}/feedback`,
method: 'POST',
data: feedbackData,
success: resolve,
fail: reject
});
});
console.log('反馈提交成功');
return result;
}
} catch (error) {
console.error('反馈提交失败:', error);
// 存储到本地,稍后重试
this.storeFeedbackLocally(feedbackData);
throw error;
}
}
// 本地存储反馈
storeFeedbackLocally(feedbackData) {
try {
const key = 'pending_feedback';
let pendingFeedback = [];
if (typeof uni !== 'undefined') {
const stored = uni.getStorageSync(key);
if (stored) {
pendingFeedback = JSON.parse(stored);
}
}
pendingFeedback.push(feedbackData);
if (typeof uni !== 'undefined') {
uni.setStorageSync(key, JSON.stringify(pendingFeedback));
}
} catch (error) {
console.error('存储反馈失败:', error);
}
}
// 重试发送本地反馈
async retryPendingFeedback() {
try {
const key = 'pending_feedback';
let pendingFeedback = [];
if (typeof uni !== 'undefined') {
const stored = uni.getStorageSync(key);
if (stored) {
pendingFeedback = JSON.parse(stored);
}
}
if (pendingFeedback.length > 0) {
const successfulItems = [];
for (const feedback of pendingFeedback) {
try {
await this.collectFeedback(feedback);
successfulItems.push(feedback);
} catch (error) {
console.error('重试反馈失败:', error);
}
}
// 移除成功发送的反馈
const remainingFeedback = pendingFeedback.filter(item => !successfulItems.includes(item));
if (typeof uni !== 'undefined') {
if (remainingFeedback.length > 0) {
uni.setStorageSync(key, JSON.stringify(remainingFeedback));
} else {
uni.removeStorageSync(key);
}
}
}
} catch (error) {
console.error('重试反馈失败:', error);
}
}
// 获取设备信息
getDeviceInfo() {
if (typeof uni !== 'undefined') {
return uni.getSystemInfoSync();
}
return {};
}
// 获取应用版本
getAppVersion() {
return this.config.version || '1.0.0';
}
}
export default FeedbackCollector;
8.7 本章总结
本章详细介绍了 UniApp 应用的打包发布流程,涵盖了从开发到上线的完整过程。
学习要点回顾
多平台打包配置
- manifest.json 和 pages.json 配置
- Android 和 iOS 平台特定配置
- 小程序平台配置
应用签名和证书
- Android 签名流程和工具
- iOS 证书配置和管理
- 证书安全管理最佳实践
发布流程和注意事项
- 发布前检查清单
- 自动化构建脚本
- 持续集成配置
版本管理和发布策略
- 语义化版本控制
- Git 分支管理策略
- 变更日志生成
应用商店上架指南
- Google Play Store 上架流程
- iOS App Store 上架流程
- 各大应用商店上架要求
- 小程序平台发布流程
发布后的维护和监控
- 应用监控和错误追踪
- 用户反馈收集
- 性能监控和优化
实践练习
配置多平台打包环境
- 配置 Android 开发环境
- 配置 iOS 开发环境
- 设置小程序开发工具
实现自动化发布流程
- 编写构建脚本
- 配置 CI/CD 流水线
- 实现版本自动管理
搭建应用监控系统
- 集成错误监控
- 实现性能监控
- 建立用户反馈机制
常见问题解答
Q: 如何处理不同平台的兼容性问题? A: 使用条件编译、平台特定代码、充分测试各平台功能。
Q: 应用签名证书丢失怎么办? A: 建立证书备份机制,使用密钥管理服务,定期检查证书有效期。
Q: 如何提高应用商店审核通过率? A: 遵循平台规范,完善应用信息,提供清晰的隐私政策,确保功能完整。
Q: 发布后发现严重 Bug 怎么处理? A: 立即修复并发布热修复版本,通知用户更新,建立应急响应机制。
Q: 如何进行灰度发布? A: 使用平台提供的分阶段发布功能,监控关键指标,逐步扩大发布范围。
最佳实践建议
发布准备
- 建立完整的测试流程
- 制定发布检查清单
- 准备回滚方案
- 建立发布文档
版本管理
- 使用语义化版本号
- 维护详细的变更日志
- 建立分支管理规范
- 定期清理过期分支
质量保证
- 实施代码审查
- 建立自动化测试
- 进行性能测试
- 验证安全性
监控运维
- 建立全面的监控体系
- 设置关键指标告警
- 定期分析用户反馈
- 持续优化用户体验
团队协作
- 建立发布流程规范
- 明确角色和职责
- 建立沟通机制
- 定期总结和改进
通过本章的学习,你应该能够熟练掌握 UniApp 应用的完整发布流程,建立规范的版本管理和发布体系,确保应用的稳定发布和持续运营。
下一章预告:第九章将介绍 UniApp 的进阶开发技巧,包括原生插件开发、混合开发模式、第三方 SDK 集成等高级主题。
8.1.2 平台特定配置
Android 配置
// utils/androidConfig.js
class AndroidConfig {
constructor() {
this.config = {
// 应用基本信息
packageName: 'com.example.myuniapp',
versionName: '1.0.0',
versionCode: 1,
// 权限配置
permissions: [
'android.permission.INTERNET',
'android.permission.ACCESS_NETWORK_STATE',
'android.permission.ACCESS_WIFI_STATE',
'android.permission.CAMERA',
'android.permission.WRITE_EXTERNAL_STORAGE',
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.ACCESS_FINE_LOCATION',
'android.permission.ACCESS_COARSE_LOCATION',
'android.permission.VIBRATE',
'android.permission.WAKE_LOCK'
],
// 签名配置
signing: {
keystore: 'android.keystore',
alias: 'myapp',
storePassword: 'your_store_password',
keyPassword: 'your_key_password'
},
// 构建配置
build: {
targetSdkVersion: 30,
minSdkVersion: 21,
compileSdkVersion: 30,
buildToolsVersion: '30.0.3'
},
// 混淆配置
proguard: {
enabled: true,
rules: [
'-keep class io.dcloud.** { *; }',
'-keep class uni.** { *; }',
'-keep class com.example.myuniapp.** { *; }',
'-dontwarn okhttp3.**',
'-dontwarn retrofit2.**'
]
}
};
}
// 生成 build.gradle 配置
generateBuildGradle() {
return `
apply plugin: 'com.android.application'
android {
compileSdkVersion ${this.config.build.compileSdkVersion}
buildToolsVersion "${this.config.build.buildToolsVersion}"
defaultConfig {
applicationId "${this.config.packageName}"
minSdkVersion ${this.config.build.minSdkVersion}
targetSdkVersion ${this.config.build.targetSdkVersion}
versionCode ${this.config.versionCode}
versionName "${this.config.versionName}"
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
}
}
signingConfigs {
release {
keyAlias '${this.config.signing.alias}'
keyPassword '${this.config.signing.keyPassword}'
storeFile file('${this.config.signing.keystore}')
storePassword '${this.config.signing.storePassword}'
}
}
buildTypes {
release {
minifyEnabled ${this.config.proguard.enabled}
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug {
debuggable true
jniDebuggable true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation 'androidx.multidex:multidex:2.0.1'
}
`;
}
// 生成 AndroidManifest.xml
generateManifest() {
const permissions = this.config.permissions
.map(permission => ` <uses-permission android:name="${permission}" />`)
.join('\n');
return `
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="${this.config.packageName}"
android:versionCode="${this.config.versionCode}"
android:versionName="${this.config.versionName}">
${permissions}
<application
android:name="io.dcloud.application.DCloudApplication"
android:allowBackup="true"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:theme="@style/TranslucentTheme"
android:hardwareAccelerated="true"
android:largeHeap="true">
<activity
android:name="io.dcloud.PandoraEntry"
android:configChanges="orientation|keyboardHidden|keyboard|navigation"
android:label="@string/app_name"
android:launchMode="singleTask"
android:screenOrientation="user"
android:theme="@style/TranslucentTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
`;
}
// 生成混淆规则
generateProguardRules() {
return this.config.proguard.rules.join('\n');
}
}
export default AndroidConfig;
iOS 配置
// utils/iosConfig.js
class IOSConfig {
constructor() {
this.config = {
// 应用基本信息
bundleId: 'com.example.myuniapp',
version: '1.0.0',
buildNumber: '1',
displayName: 'MyUniApp',
// 部署目标
deploymentTarget: '9.0',
// 权限配置
permissions: {
'NSCameraUsageDescription': '需要访问相机来拍照',
'NSPhotoLibraryUsageDescription': '需要访问相册来选择图片',
'NSLocationWhenInUseUsageDescription': '需要获取位置信息',
'NSLocationAlwaysAndWhenInUseUsageDescription': '需要获取位置信息',
'NSMicrophoneUsageDescription': '需要访问麦克风来录音',
'NSContactsUsageDescription': '需要访问通讯录',
'NSCalendarsUsageDescription': '需要访问日历',
'NSRemindersUsageDescription': '需要访问提醒事项'
},
// URL Schemes
urlSchemes: [
'myuniapp',
'weixin',
'alipay'
],
// 应用传输安全
ats: {
allowArbitraryLoads: true,
exceptionDomains: {
'example.com': {
exceptionAllowsInsecureHTTPLoads: true,
exceptionMinimumTLSVersion: 'TLSv1.0'
}
}
},
// 后台模式
backgroundModes: [
'background-fetch',
'background-processing'
]
};
}
// 生成 Info.plist
generateInfoPlist() {
const permissions = Object.entries(this.config.permissions)
.map(([key, value]) => ` <key>${key}</key>\n <string>${value}</string>`)
.join('\n');
const urlSchemes = this.config.urlSchemes
.map(scheme => ` <string>${scheme}</string>`)
.join('\n');
const backgroundModes = this.config.backgroundModes
.map(mode => ` <string>${mode}</string>`)
.join('\n');
return `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>zh_CN</string>
<key>CFBundleDisplayName</key>
<string>${this.config.displayName}</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>${this.config.bundleId}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${this.config.version}</string>
<key>CFBundleVersion</key>
<string>${this.config.buildNumber}</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<!-- 权限配置 -->
${permissions}
<!-- URL Schemes -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>${this.config.bundleId}</string>
<key>CFBundleURLSchemes</key>
<array>
${urlSchemes}
</array>
</dict>
</array>
<!-- 应用传输安全 -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<${this.config.ats.allowArbitraryLoads}/>
</dict>
<!-- 后台模式 -->
<key>UIBackgroundModes</key>
<array>
${backgroundModes}
</array>
<!-- 支持的设备方向 -->
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
`;
}
// 生成 Podfile
generatePodfile() {
return `
platform :ios, '${this.config.deploymentTarget}'
target 'MyUniApp' do
use_frameworks!
# UniApp 核心依赖
pod 'DCUniApp'
# 第三方 SDK
pod 'AFNetworking', '~> 4.0'
pod 'SDWebImage', '~> 5.0'
pod 'MJRefresh', '~> 3.7'
# 微信 SDK
pod 'WechatOpenSDK'
# 支付宝 SDK
pod 'AlipaySDK-iOS'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '${this.config.deploymentTarget}'
end
end
end
`;
}
}
export default IOSConfig;
8.1.3 小程序配置
微信小程序配置
// utils/wechatConfig.js
class WechatConfig {
constructor() {
this.config = {
appid: 'wxXXXXXXXXXXXXXXXX',
projectname: 'MyUniApp',
// 编译设置
setting: {
urlCheck: false,
es6: true,
enhance: true,
postcss: true,
preloadBackgroundData: false,
minified: true,
newFeature: false,
coverView: true,
nodeModules: false,
autoAudits: false,
showShadowRootInWxmlPanel: true,
scopeDataCheck: false,
uglifyFileName: false,
checkInvalidKey: true,
checkSiteMap: true,
uploadWithSourceMap: true,
compileHotReLoad: false,
lazyloadPlaceholderEnable: false,
useMultiFrameRuntime: true,
useApiHook: true,
useApiHostProcess: true,
babelSetting: {
ignore: [],
disablePlugins: [],
outputPath: ''
},
enableEngineNative: false,
useIsolateContext: true,
userConfirmedBundleSwitch: false,
packNpmManually: false,
packNpmRelationList: [],
minifyWXSS: true,
disableUseStrict: false,
minifyWXML: true
},
// 权限配置
permission: {
'scope.userLocation': {
desc: '你的位置信息将用于小程序位置接口的效果展示'
},
'scope.userInfo': {
desc: '你的用户信息将用于小程序个性化服务'
},
'scope.camera': {
desc: '你的摄像头将用于拍照功能'
},
'scope.album': {
desc: '你的相册将用于选择图片功能'
}
},
// 隐私设置
requiredPrivateInfos: [
'getLocation',
'chooseLocation',
'chooseAddress'
],
// 云开发配置
cloudfunctionRoot: 'cloudfunctions/',
// 分包配置
subpackages: [
{
root: 'pages/user',
name: 'user',
pages: [
'profile/profile',
'settings/settings'
]
},
{
root: 'pages/shop',
name: 'shop',
pages: [
'list/list',
'detail/detail'
]
}
],
// 预下载分包
preloadRule: {
'pages/index/index': {
network: 'all',
packages: ['user']
}
}
};
}
// 生成 project.config.json
generateProjectConfig() {
return JSON.stringify({
description: '项目配置文件',
packOptions: {
ignore: []
},
setting: this.config.setting,
compileType: 'miniprogram',
libVersion: '2.19.4',
appid: this.config.appid,
projectname: this.config.projectname,
debugOptions: {
hidedInDevtools: []
},
scripts: {},
isGameTourist: false,
simulatorType: 'wechat',
simulatorPluginLibVersion: {},
condition: {
search: {
list: []
},
conversation: {
list: []
},
game: {
list: []
},
plugin: {
list: []
},
gamePlugin: {
list: []
},
miniprogram: {
list: []
}
}
}, null, 2);
}
// 生成 sitemap.json
generateSitemap() {
return JSON.stringify({
desc: '关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html',
rules: [
{
action: 'allow',
page: '*'
}
]
}, null, 2);
}
// 生成云函数配置
generateCloudConfig() {
return JSON.stringify({
envId: 'your-env-id',
functionRoot: './cloudfunctions',
functions: [
{
name: 'login',
timeout: 5,
envVariables: {},
runtime: 'Nodejs12.16'
},
{
name: 'getUser',
timeout: 5,
envVariables: {},
runtime: 'Nodejs12.16'
}
]
}, null, 2);
}
}
export default WechatConfig;
8.2 应用签名和证书
8.2.1 Android 签名
生成密钥库
# 生成 Android 密钥库
keytool -genkey -v -keystore android.keystore -alias myapp -keyalg RSA -keysize 2048 -validity 10000
# 查看密钥库信息
keytool -list -v -keystore android.keystore
# 导出证书
keytool -export -alias myapp -keystore android.keystore -file myapp.crt
签名工具类
// utils/androidSigning.js
class AndroidSigning {
constructor() {
this.keystoreConfig = {
keystore: 'android.keystore',
alias: 'myapp',
storePassword: '',
keyPassword: ''
};
}
// 设置签名配置
setSigningConfig(config) {
this.keystoreConfig = { ...this.keystoreConfig, ...config };
}
// 验证签名配置
validateSigningConfig() {
const required = ['keystore', 'alias', 'storePassword', 'keyPassword'];
const missing = required.filter(key => !this.keystoreConfig[key]);
if (missing.length > 0) {
throw new Error(`Missing signing config: ${missing.join(', ')}`);
}
return true;
}
// 生成签名配置文件
generateSigningConfig() {
this.validateSigningConfig();
return `
signingConfigs {
release {
keyAlias '${this.keystoreConfig.alias}'
keyPassword '${this.keystoreConfig.keyPassword}'
storeFile file('${this.keystoreConfig.keystore}')
storePassword '${this.keystoreConfig.storePassword}'
}
debug {
keyAlias 'androiddebugkey'
keyPassword 'android'
storeFile file('debug.keystore')
storePassword 'android'
}
}
`;
}
// 签名 APK
signApk(apkPath, outputPath) {
const command = `
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
-keystore ${this.keystoreConfig.keystore} \
-storepass ${this.keystoreConfig.storePassword} \
-keypass ${this.keystoreConfig.keyPassword} \
${apkPath} ${this.keystoreConfig.alias}
# 对齐 APK
zipalign -v 4 ${apkPath} ${outputPath}
`;
return command;
}
// 验证 APK 签名
verifyApkSignature(apkPath) {
return `jarsigner -verify -verbose -certs ${apkPath}`;
}
// 获取 APK 签名信息
getApkSignatureInfo(apkPath) {
return `
# 获取签名信息
aapt dump badging ${apkPath}
# 获取证书指纹
keytool -printcert -jarfile ${apkPath}
`;
}
}
export default AndroidSigning;
8.2.2 iOS 证书配置
证书管理工具
// utils/iosCertificate.js
class IOSCertificate {
constructor() {
this.config = {
teamId: 'YOUR_TEAM_ID',
bundleId: 'com.example.myuniapp',
// 证书类型
certificateTypes: {
development: 'iOS Development',
distribution: 'iOS Distribution',
adhoc: 'Ad Hoc'
},
// 描述文件
provisioningProfiles: {
development: 'MyApp_Development.mobileprovision',
distribution: 'MyApp_Distribution.mobileprovision',
adhoc: 'MyApp_AdHoc.mobileprovision'
}
};
}
// 生成 Xcode 项目配置
generateXcodeConfig(buildType = 'release') {
const isRelease = buildType === 'release';
return `
// Xcode Build Settings
CODE_SIGN_IDENTITY = "${isRelease ? this.config.certificateTypes.distribution : this.config.certificateTypes.development}";
DEVELOPMENT_TEAM = "${this.config.teamId}";
PRODUCT_BUNDLE_IDENTIFIER = "${this.config.bundleId}";
PROVISIONING_PROFILE_SPECIFIER = "${isRelease ? this.config.provisioningProfiles.distribution : this.config.provisioningProfiles.development}";
CODE_SIGN_STYLE = Manual;
// 其他设置
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
TARGETED_DEVICE_FAMILY = "1,2";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
`;
}
// 生成 ExportOptions.plist
generateExportOptions(method = 'app-store') {
const methods = {
'app-store': 'app-store',
'ad-hoc': 'ad-hoc',
'enterprise': 'enterprise',
'development': 'development'
};
return `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>${methods[method]}</string>
<key>teamID</key>
<string>${this.config.teamId}</string>
<key>uploadBitcode</key>
<${method === 'app-store' ? 'true' : 'false'}/>
<key>uploadSymbols</key>
<${method === 'app-store' ? 'true' : 'false'}/>
<key>compileBitcode</key>
<${method === 'app-store' ? 'true' : 'false'}/>
<key>stripSwiftSymbols</key>
<true/>
<key>thinning</key>
<string><none></string>
<key>provisioningProfiles</key>
<dict>
<key>${this.config.bundleId}</key>
<string>${this.getProvisioningProfile(method)}</string>
</dict>
</dict>
</plist>
`;
}
// 获取对应的描述文件
getProvisioningProfile(method) {
switch (method) {
case 'app-store':
return this.config.provisioningProfiles.distribution;
case 'ad-hoc':
return this.config.provisioningProfiles.adhoc;
case 'development':
return this.config.provisioningProfiles.development;
default:
return this.config.provisioningProfiles.distribution;
}
}
// 验证证书
validateCertificate() {
return `
# 查看已安装的证书
security find-identity -v -p codesigning
# 验证描述文件
security cms -D -i ${this.config.provisioningProfiles.distribution}
# 检查证书有效期
openssl x509 -in certificate.pem -text -noout
`;
}
// 自动化签名脚本
generateSigningScript() {
return `
#!/bin/bash
# iOS 自动签名脚本
set -e
# 配置变量
PROJECT_PATH="MyUniApp.xcodeproj"
SCHEME="MyUniApp"
CONFIGURATION="Release"
ARCHIVE_PATH="build/MyUniApp.xcarchive"
EXPORT_PATH="build/export"
EXPORT_OPTIONS="ExportOptions.plist"
echo "开始构建 iOS 应用..."
# 清理构建目录
rm -rf build
mkdir -p build
# 构建 Archive
echo "正在创建 Archive..."
xcodebuild -project "$PROJECT_PATH" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-archivePath "$ARCHIVE_PATH" \
archive
# 导出 IPA
echo "正在导出 IPA..."
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_PATH" \
-exportOptionsPlist "$EXPORT_OPTIONS"
echo "构建完成!IPA 文件位于: $EXPORT_PATH"
`;
}
}
export default IOSCertificate;
8.2.3 证书管理最佳实践
// utils/certificateManager.js
class CertificateManager {
constructor() {
this.certificates = new Map();
this.expirationWarningDays = 30;
}
// 添加证书信息
addCertificate(platform, type, config) {
const key = `${platform}_${type}`;
this.certificates.set(key, {
platform,
type,
...config,
addedAt: new Date()
});
}
// 检查证书有效期
checkCertificateExpiration() {
const warnings = [];
const now = new Date();
for (const [key, cert] of this.certificates) {
if (cert.expirationDate) {
const expirationDate = new Date(cert.expirationDate);
const daysUntilExpiration = Math.ceil((expirationDate - now) / (1000 * 60 * 60 * 24));
if (daysUntilExpiration <= this.expirationWarningDays) {
warnings.push({
platform: cert.platform,
type: cert.type,
daysUntilExpiration,
expirationDate: cert.expirationDate
});
}
}
}
return warnings;
}
// 生成证书报告
generateCertificateReport() {
const report = {
totalCertificates: this.certificates.size,
platforms: {},
expirationWarnings: this.checkCertificateExpiration(),
generatedAt: new Date().toISOString()
};
for (const [key, cert] of this.certificates) {
if (!report.platforms[cert.platform]) {
report.platforms[cert.platform] = [];
}
report.platforms[cert.platform].push({
type: cert.type,
status: this.getCertificateStatus(cert),
expirationDate: cert.expirationDate
});
}
return report;
}
// 获取证书状态
getCertificateStatus(cert) {
if (!cert.expirationDate) {
return 'unknown';
}
const now = new Date();
const expirationDate = new Date(cert.expirationDate);
const daysUntilExpiration = Math.ceil((expirationDate - now) / (1000 * 60 * 60 * 24));
if (daysUntilExpiration < 0) {
return 'expired';
} else if (daysUntilExpiration <= this.expirationWarningDays) {
return 'expiring_soon';
} else {
return 'valid';
}
}
// 自动续期提醒
setupRenewalReminders() {
const checkInterval = 24 * 60 * 60 * 1000; // 每天检查一次
setInterval(() => {
const warnings = this.checkCertificateExpiration();
if (warnings.length > 0) {
this.sendRenewalNotification(warnings);
}
}, checkInterval);
}
// 发送续期通知
sendRenewalNotification(warnings) {
const message = warnings.map(warning =>
`${warning.platform} ${warning.type} 证书将在 ${warning.daysUntilExpiration} 天后过期`
).join('\n');
console.warn('证书过期提醒:', message);
// 这里可以集成邮件、短信或其他通知服务
// this.emailService.send({
// subject: '证书过期提醒',
// body: message
// });
}
}
export default CertificateManager;
8.3 发布流程和注意事项
8.3.1 发布前检查清单
// utils/releaseChecker.js
class ReleaseChecker {
constructor() {
this.checkItems = {
code: [
{ name: '代码审查完成', required: true },
{ name: '单元测试通过', required: true },
{ name: '集成测试通过', required: true },
{ name: '性能测试通过', required: false },
{ name: '安全扫描通过', required: true },
{ name: '代码覆盖率达标', required: false }
],
config: [
{ name: '生产环境配置正确', required: true },
{ name: 'API 地址配置正确', required: true },
{ name: '第三方服务配置正确', required: true },
{ name: '调试模式已关闭', required: true },
{ name: '日志级别设置正确', required: true }
],
assets: [
{ name: '应用图标完整', required: true },
{ name: '启动页图片完整', required: true },
{ name: '多分辨率图片准备', required: true },
{ name: '字体文件检查', required: false },
{ name: '音频文件检查', required: false }
],
legal: [
{ name: '隐私政策更新', required: true },
{ name: '用户协议更新', required: true },
{ name: '第三方许可证检查', required: true },
{ name: '版权信息完整', required: true }
],
platform: [
{ name: 'Android 权限声明', required: true },
{ name: 'iOS 权限描述', required: true },
{ name: '小程序类目选择', required: true },
{ name: '应用商店描述', required: true },
{ name: '关键词优化', required: false }
]
};
this.results = {};
}
// 执行检查
async runChecks() {
console.log('开始发布前检查...');
for (const [category, items] of Object.entries(this.checkItems)) {
console.log(`检查类别: ${category}`);
this.results[category] = await this.checkCategory(category, items);
}
return this.generateReport();
}
// 检查单个类别
async checkCategory(category, items) {
const results = [];
for (const item of items) {
const result = await this.checkItem(category, item);
results.push(result);
}
return results;
}
// 检查单个项目
async checkItem(category, item) {
try {
let passed = false;
// 根据类别和项目名称执行具体检查
switch (category) {
case 'code':
passed = await this.checkCodeItem(item.name);
break;
case 'config':
passed = await this.checkConfigItem(item.name);
break;
case 'assets':
passed = await this.checkAssetsItem(item.name);
break;
case 'legal':
passed = await this.checkLegalItem(item.name);
break;
case 'platform':
passed = await this.checkPlatformItem(item.name);
break;
}
return {
name: item.name,
required: item.required,
passed,
message: passed ? '检查通过' : '检查失败'
};
} catch (error) {
return {
name: item.name,
required: item.required,
passed: false,
message: `检查出错: ${error.message}`
};
}
}
// 检查代码相关项目
async checkCodeItem(itemName) {
switch (itemName) {
case '调试模式已关闭':
// 检查是否还有 console.log 或调试代码
return !this.hasDebugCode();
case '单元测试通过':
// 运行单元测试
return await this.runUnitTests();
default:
return true; // 默认通过,需要手动确认
}
}
// 检查配置相关项目
async checkConfigItem(itemName) {
switch (itemName) {
case '生产环境配置正确':
return this.checkProductionConfig();
case 'API 地址配置正确':
return this.checkApiConfig();
default:
return true;
}
}
// 检查资源相关项目
async checkAssetsItem(itemName) {
switch (itemName) {
case '应用图标完整':
return this.checkAppIcons();
case '启动页图片完整':
return this.checkSplashScreens();
default:
return true;
}
}
// 检查法律相关项目
async checkLegalItem(itemName) {
// 这些通常需要手动确认
return true;
}
// 检查平台相关项目
async checkPlatformItem(itemName) {
switch (itemName) {
case 'Android 权限声明':
return this.checkAndroidPermissions();
case 'iOS 权限描述':
return this.checkIOSPermissions();
default:
return true;
}
}
// 检查是否有调试代码
hasDebugCode() {
// 这里应该扫描代码文件
// 简化实现
return false;
}
// 运行单元测试
async runUnitTests() {
try {
// 这里应该执行实际的测试命令
// 简化实现
return true;
} catch (error) {
return false;
}
}
// 检查生产环境配置
checkProductionConfig() {
// 检查环境变量、配置文件等
return true;
}
// 检查 API 配置
checkApiConfig() {
// 检查 API 地址是否为生产环境
return true;
}
// 检查应用图标
checkAppIcons() {
const requiredIcons = {
android: ['48x48', '72x72', '96x96', '144x144', '192x192'],
ios: ['29x29', '40x40', '58x58', '60x60', '80x80', '87x87', '120x120', '180x180']
};
// 检查图标文件是否存在
return true;
}
// 检查启动页
checkSplashScreens() {
// 检查启动页图片是否存在
return true;
}
// 检查 Android 权限
checkAndroidPermissions() {
// 检查 manifest.json 中的权限配置
return true;
}
// 检查 iOS 权限
checkIOSPermissions() {
// 检查权限描述是否完整
return true;
}
// 生成检查报告
generateReport() {
const report = {
timestamp: new Date().toISOString(),
summary: {
total: 0,
passed: 0,
failed: 0,
requiredFailed: 0
},
categories: {},
canRelease: true
};
for (const [category, results] of Object.entries(this.results)) {
const categoryReport = {
total: results.length,
passed: 0,
failed: 0,
requiredFailed: 0,
items: results
};
results.forEach(result => {
report.summary.total++;
categoryReport.total++;
if (result.passed) {
report.summary.passed++;
categoryReport.passed++;
} else {
report.summary.failed++;
categoryReport.failed++;
if (result.required) {
report.summary.requiredFailed++;
categoryReport.requiredFailed++;
report.canRelease = false;
}
}
});
report.categories[category] = categoryReport;
}
return report;
}
// 打印报告
printReport(report) {
console.log('\n=== 发布前检查报告 ===');
console.log(`检查时间: ${report.timestamp}`);
console.log(`总计: ${report.summary.total} 项`);
console.log(`通过: ${report.summary.passed} 项`);
console.log(`失败: ${report.summary.failed} 项`);
console.log(`必需项失败: ${report.summary.requiredFailed} 项`);
console.log(`可以发布: ${report.canRelease ? '是' : '否'}`);
for (const [category, categoryReport] of Object.entries(report.categories)) {
console.log(`\n--- ${category} ---`);
categoryReport.items.forEach(item => {
const status = item.passed ? '✓' : '✗';
const required = item.required ? '[必需]' : '[可选]';
console.log(`${status} ${required} ${item.name}: ${item.message}`);
});
}
}
}
export default ReleaseChecker;
8.3.2 自动化构建脚本
// scripts/build.js
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
class BuildScript {
constructor() {
this.config = {
platforms: ['h5', 'mp-weixin', 'app-plus'],
outputDir: 'dist',
backupDir: 'backup',
version: this.getVersion()
};
}
// 获取版本号
getVersion() {
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
return packageJson.version;
}
// 主构建流程
async build(platforms = this.config.platforms) {
console.log('开始构建流程...');
try {
// 1. 预构建检查
await this.preBuildCheck();
// 2. 清理输出目录
this.cleanOutputDir();
// 3. 构建各平台
for (const platform of platforms) {
await this.buildPlatform(platform);
}
// 4. 后构建处理
await this.postBuildProcess();
console.log('构建完成!');
} catch (error) {
console.error('构建失败:', error.message);
process.exit(1);
}
}
// 预构建检查
async preBuildCheck() {
console.log('执行预构建检查...');
// 检查依赖
if (!fs.existsSync('node_modules')) {
console.log('安装依赖...');
execSync('npm install', { stdio: 'inherit' });
}
// 检查配置文件
const requiredFiles = ['manifest.json', 'pages.json'];
for (const file of requiredFiles) {
if (!fs.existsSync(file)) {
throw new Error(`缺少必需文件: ${file}`);
}
}
// 运行测试
try {
execSync('npm run test', { stdio: 'inherit' });
} catch (error) {
console.warn('测试失败,但继续构建');
}
}
// 清理输出目录
cleanOutputDir() {
console.log('清理输出目录...');
if (fs.existsSync(this.config.outputDir)) {
// 备份之前的构建
const backupPath = path.join(this.config.backupDir, `build_${Date.now()}`);
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
fs.renameSync(this.config.outputDir, backupPath);
}
fs.mkdirSync(this.config.outputDir, { recursive: true });
}
// 构建指定平台
async buildPlatform(platform) {
console.log(`构建 ${platform} 平台...`);
const startTime = Date.now();
try {
// 设置环境变量
process.env.UNI_PLATFORM = platform;
process.env.NODE_ENV = 'production';
// 执行构建命令
const buildCommand = this.getBuildCommand(platform);
execSync(buildCommand, { stdio: 'inherit' });
// 平台特定的后处理
await this.platformPostProcess(platform);
const duration = Date.now() - startTime;
console.log(`${platform} 构建完成,耗时: ${duration}ms`);
} catch (error) {
throw new Error(`${platform} 构建失败: ${error.message}`);
}
}
// 获取构建命令
getBuildCommand(platform) {
const commands = {
'h5': 'npm run build:h5',
'mp-weixin': 'npm run build:mp-weixin',
'mp-alipay': 'npm run build:mp-alipay',
'mp-baidu': 'npm run build:mp-baidu',
'mp-toutiao': 'npm run build:mp-toutiao',
'app-plus': 'npm run build:app-plus'
};
return commands[platform] || `npm run build:${platform}`;
}
// 平台特定后处理
async platformPostProcess(platform) {
switch (platform) {
case 'h5':
await this.processH5Build();
break;
case 'mp-weixin':
await this.processWechatBuild();
break;
case 'app-plus':
await this.processAppBuild();
break;
}
}
// 处理 H5 构建
async processH5Build() {
const h5Dir = path.join(this.config.outputDir, 'build', 'h5');
if (fs.existsSync(h5Dir)) {
// 压缩静态资源
await this.compressAssets(h5Dir);
// 生成部署脚本
this.generateDeployScript('h5', h5Dir);
}
}
// 处理微信小程序构建
async processWechatBuild() {
const mpDir = path.join(this.config.outputDir, 'build', 'mp-weixin');
if (fs.existsSync(mpDir)) {
// 检查文件大小
await this.checkMiniProgramSize(mpDir);
// 生成上传脚本
this.generateUploadScript('mp-weixin', mpDir);
}
}
// 处理 App 构建
async processAppBuild() {
const appDir = path.join(this.config.outputDir, 'build', 'app-plus');
if (fs.existsSync(appDir)) {
// 复制原生资源
await this.copyNativeAssets(appDir);
// 生成打包脚本
this.generatePackageScript('app-plus', appDir);
}
}
// 压缩静态资源
async compressAssets(dir) {
console.log('压缩静态资源...');
// 这里可以集成 gzip、brotli 等压缩
// 简化实现
}
// 检查小程序大小
async checkMiniProgramSize(dir) {
const maxSize = 2 * 1024 * 1024; // 2MB
const totalSize = this.getDirectorySize(dir);
if (totalSize > maxSize) {
console.warn(`警告: 小程序包大小 ${(totalSize / 1024 / 1024).toFixed(2)}MB 超过建议大小`);
}
}
// 获取目录大小
getDirectorySize(dir) {
let totalSize = 0;
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
totalSize += this.getDirectorySize(filePath);
} else {
totalSize += stats.size;
}
}
return totalSize;
}
// 复制原生资源
async copyNativeAssets(dir) {
const nativeDir = 'src/native';
if (fs.existsSync(nativeDir)) {
const targetDir = path.join(dir, 'native');
this.copyDirectory(nativeDir, targetDir);
}
}
// 复制目录
copyDirectory(src, dest) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const files = fs.readdirSync(src);
for (const file of files) {
const srcPath = path.join(src, file);
const destPath = path.join(dest, file);
if (fs.statSync(srcPath).isDirectory()) {
this.copyDirectory(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
// 生成部署脚本
generateDeployScript(platform, buildDir) {
const script = `
#!/bin/bash
# ${platform} 部署脚本
echo "开始部署 ${platform}..."
# 上传到服务器
# rsync -avz ${buildDir}/ user@server:/var/www/html/
# 或者上传到 CDN
# aws s3 sync ${buildDir} s3://your-bucket/
echo "部署完成!"
`;
const scriptPath = path.join(this.config.outputDir, `deploy-${platform}.sh`);
fs.writeFileSync(scriptPath, script);
fs.chmodSync(scriptPath, '755');
}
// 生成上传脚本
generateUploadScript(platform, buildDir) {
const script = `
#!/bin/bash
# ${platform} 上传脚本
echo "开始上传 ${platform}..."
# 微信开发者工具命令行上传
# /Applications/wechatwebdevtools.app/Contents/MacOS/cli upload --project ${buildDir}
echo "上传完成!"
`;
const scriptPath = path.join(this.config.outputDir, `upload-${platform}.sh`);
fs.writeFileSync(scriptPath, script);
fs.chmodSync(scriptPath, '755');
}
// 生成打包脚本
generatePackageScript(platform, buildDir) {
const script = `
#!/bin/bash
# ${platform} 打包脚本
echo "开始打包 ${platform}..."
# Android 打包
# cd android && ./gradlew assembleRelease
# iOS 打包
# xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp archive
echo "打包完成!"
`;
const scriptPath = path.join(this.config.outputDir, `package-${platform}.sh`);
fs.writeFileSync(scriptPath, script);
fs.chmodSync(scriptPath, '755');
}
// 后构建处理
async postBuildProcess() {
console.log('执行后构建处理...');
// 生成构建报告
this.generateBuildReport();
// 清理临时文件
this.cleanupTempFiles();
// 发送通知
await this.sendBuildNotification();
}
// 生成构建报告
generateBuildReport() {
const report = {
version: this.config.version,
timestamp: new Date().toISOString(),
platforms: this.config.platforms,
outputDir: this.config.outputDir,
buildSizes: this.getBuildSizes()
};
const reportPath = path.join(this.config.outputDir, 'build-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
}
// 获取构建大小
getBuildSizes() {
const sizes = {};
const buildDir = path.join(this.config.outputDir, 'build');
if (fs.existsSync(buildDir)) {
const platforms = fs.readdirSync(buildDir);
for (const platform of platforms) {
const platformDir = path.join(buildDir, platform);
if (fs.statSync(platformDir).isDirectory()) {
sizes[platform] = this.getDirectorySize(platformDir);
}
}
}
return sizes;
}
// 清理临时文件
cleanupTempFiles() {
// 清理构建过程中产生的临时文件
}
// 发送构建通知
async sendBuildNotification() {
// 发送邮件、Slack 消息等
console.log('构建通知已发送');
}
}
// 命令行接口
if (require.main === module) {
const args = process.argv.slice(2);
const platforms = args.length > 0 ? args : undefined;
const builder = new BuildScript();
builder.build(platforms).catch(console.error);
}
module.exports = BuildScript;
8.3.3 持续集成配置
GitHub Actions 配置
# .github/workflows/build.yml
name: Build and Deploy
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
release:
types: [ published ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
- name: Run linting
run: npm run lint
- name: Check code coverage
run: npm run coverage
build:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'release'
strategy:
matrix:
platform: [h5, mp-weixin, app-plus]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build ${{ matrix.platform }}
run: npm run build:${{ matrix.platform }}
env:
NODE_ENV: production
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-${{ matrix.platform }}
path: dist/build/${{ matrix.platform }}
- name: Deploy to staging
if: github.ref == 'refs/heads/develop'
run: |
echo "Deploy to staging environment"
# 部署到测试环境的脚本
- name: Deploy to production
if: github.event_name == 'release'
run: |
echo "Deploy to production environment"
# 部署到生产环境的脚本
android-build:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'release'
steps:
- uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Download app-plus build
uses: actions/download-artifact@v3
with:
name: build-app-plus
path: dist/build/app-plus
- name: Build Android APK
run: |
cd android
./gradlew assembleRelease
- name: Sign APK
run: |
echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 -d > android.keystore
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
-keystore android.keystore \
-storepass ${{ secrets.KEYSTORE_PASSWORD }} \
-keypass ${{ secrets.KEY_PASSWORD }} \
android/app/build/outputs/apk/release/app-release-unsigned.apk \
${{ secrets.KEY_ALIAS }}
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: android-apk
path: android/app/build/outputs/apk/release/
ios-build:
needs: build
runs-on: macos-latest
if: github.event_name == 'release'
steps:
- uses: actions/checkout@v3
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Download app-plus build
uses: actions/download-artifact@v3
with:
name: build-app-plus
path: dist/build/app-plus
- name: Install certificates
run: |
echo "${{ secrets.IOS_CERTIFICATE }}" | base64 -d > certificate.p12
echo "${{ secrets.IOS_PROVISIONING_PROFILE }}" | base64 -d > profile.mobileprovision
# 安装证书
security create-keychain -p "" build.keychain
security import certificate.p12 -k build.keychain -P "${{ secrets.CERTIFICATE_PASSWORD }}" -A
security list-keychains -s build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
# 安装描述文件
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
- name: Build iOS
run: |
cd ios
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath MyApp.xcarchive \
archive
xcodebuild -exportArchive \
-archivePath MyApp.xcarchive \
-exportPath export \
-exportOptionsPlist ExportOptions.plist
- name: Upload IPA
uses: actions/upload-artifact@v3
with:
name: ios-ipa
path: ios/export/
Jenkins 配置
// Jenkinsfile
pipeline {
agent any
environment {
NODE_VERSION = '16'
ANDROID_HOME = '/opt/android-sdk'
JAVA_HOME = '/usr/lib/jvm/java-11-openjdk'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Setup') {
steps {
script {
// 安装 Node.js
sh "nvm use ${NODE_VERSION}"
// 安装依赖
sh 'npm ci'
}
}
}
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'npm run test'
}
post {
always {
publishTestResults testResultsPattern: 'test-results.xml'
}
}
}
stage('Lint') {
steps {
sh 'npm run lint'
}
}
stage('Security Scan') {
steps {
sh 'npm audit'
}
}
}
}
stage('Build') {
parallel {
stage('H5') {
steps {
sh 'npm run build:h5'
archiveArtifacts artifacts: 'dist/build/h5/**', fingerprint: true
}
}
stage('WeChat Mini Program') {
steps {
sh 'npm run build:mp-weixin'
archiveArtifacts artifacts: 'dist/build/mp-weixin/**', fingerprint: true
}
}
stage('App Plus') {
steps {
sh 'npm run build:app-plus'
archiveArtifacts artifacts: 'dist/build/app-plus/**', fingerprint: true
}
}
}
}
stage('Package') {
when {
anyOf {
branch 'main'
tag pattern: 'v\\d+\\.\\d+\\.\\d+', comparator: 'REGEXP'
}
}
parallel {
stage('Android') {
steps {
script {
// 构建 Android APK
sh '''
cd android
./gradlew clean assembleRelease
'''
// 签名 APK
withCredentials([
file(credentialsId: 'android-keystore', variable: 'KEYSTORE_FILE'),
string(credentialsId: 'keystore-password', variable: 'KEYSTORE_PASSWORD'),
string(credentialsId: 'key-password', variable: 'KEY_PASSWORD'),
string(credentialsId: 'key-alias', variable: 'KEY_ALIAS')
]) {
sh '''
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
-keystore $KEYSTORE_FILE \
-storepass $KEYSTORE_PASSWORD \
-keypass $KEY_PASSWORD \
android/app/build/outputs/apk/release/app-release-unsigned.apk \
$KEY_ALIAS
zipalign -v 4 \
android/app/build/outputs/apk/release/app-release-unsigned.apk \
android/app/build/outputs/apk/release/app-release.apk
'''
}
archiveArtifacts artifacts: 'android/app/build/outputs/apk/release/*.apk', fingerprint: true
}
}
}
stage('iOS') {
agent {
label 'macos'
}
steps {
script {
// 构建 iOS
withCredentials([
file(credentialsId: 'ios-certificate', variable: 'CERTIFICATE_FILE'),
file(credentialsId: 'ios-provisioning-profile', variable: 'PROVISIONING_PROFILE'),
string(credentialsId: 'certificate-password', variable: 'CERTIFICATE_PASSWORD')
]) {
sh '''
# 安装证书和描述文件
security create-keychain -p "" build.keychain
security import $CERTIFICATE_FILE -k build.keychain -P $CERTIFICATE_PASSWORD -A
security list-keychains -s build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PROVISIONING_PROFILE ~/Library/MobileDevice/Provisioning\ Profiles/
# 构建
cd ios
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath MyApp.xcarchive \
archive
xcodebuild -exportArchive \
-archivePath MyApp.xcarchive \
-exportPath export \
-exportOptionsPlist ExportOptions.plist
'''
}
archiveArtifacts artifacts: 'ios/export/*.ipa', fingerprint: true
}
}
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
script {
// 部署到生产环境
sh 'npm run deploy:production'
// 发送通知
slackSend(
channel: '#deployments',
color: 'good',
message: "✅ 部署成功: ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
)
}
}
}
}
post {
always {
// 清理工作空间
cleanWs()
}
failure {
// 发送失败通知
slackSend(
channel: '#deployments',
color: 'danger',
message: "❌ 构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
)
}
}
}