1. 打包配置
1.1 manifest.json配置
{
"name": "MyUniApp",
"appid": "__UNI__XXXXXXX",
"description": "我的UniApp应用",
"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": {
"umeng": {
"appkey_ios": "",
"appkey_android": "",
"channel": "umeng"
}
}
},
"icons": {
"android": {
"hdpi": "unpackage/res/icons/72x72.png",
"xhdpi": "unpackage/res/icons/96x96.png",
"xxhdpi": "unpackage/res/icons/144x144.png",
"xxxhdpi": "unpackage/res/icons/192x192.png"
},
"ios": {
"appstore": "unpackage/res/icons/1024x1024.png",
"ipad": {
"app": "unpackage/res/icons/76x76.png",
"app@2x": "unpackage/res/icons/152x152.png",
"notification": "unpackage/res/icons/20x20.png",
"notification@2x": "unpackage/res/icons/40x40.png",
"proapp@2x": "unpackage/res/icons/167x167.png",
"settings": "unpackage/res/icons/29x29.png",
"settings@2x": "unpackage/res/icons/58x58.png",
"spotlight": "unpackage/res/icons/40x40.png",
"spotlight@2x": "unpackage/res/icons/80x80.png"
},
"iphone": {
"app@2x": "unpackage/res/icons/120x120.png",
"app@3x": "unpackage/res/icons/180x180.png",
"notification@2x": "unpackage/res/icons/40x40.png",
"notification@3x": "unpackage/res/icons/60x60.png",
"settings@2x": "unpackage/res/icons/58x58.png",
"settings@3x": "unpackage/res/icons/87x87.png",
"spotlight@2x": "unpackage/res/icons/80x80.png",
"spotlight@3x": "unpackage/res/icons/120x120.png"
}
}
}
}
},
"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,
"showES6CompileOption": false,
"useCompilerPlugins": false
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
},
"requiredPrivateInfos": ["getLocation"]
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"h5": {
"template": "index.html",
"router": {
"mode": "hash",
"base": "/"
},
"optimization": {
"treeShaking": {
"enable": true
}
},
"title": "MyUniApp",
"devServer": {
"https": false,
"port": 8080,
"disableHostCheck": true
}
}
}
1.2 环境配置
// config/env.js
const ENV_CONFIG = {
development: {
API_BASE_URL: 'http://localhost:3000/api',
WS_BASE_URL: 'ws://localhost:3000',
APP_NAME: 'MyApp(开发)',
DEBUG: true,
LOG_LEVEL: 'debug'
},
testing: {
API_BASE_URL: 'https://test-api.example.com/api',
WS_BASE_URL: 'wss://test-api.example.com',
APP_NAME: 'MyApp(测试)',
DEBUG: true,
LOG_LEVEL: 'info'
},
staging: {
API_BASE_URL: 'https://staging-api.example.com/api',
WS_BASE_URL: 'wss://staging-api.example.com',
APP_NAME: 'MyApp(预发布)',
DEBUG: false,
LOG_LEVEL: 'warn'
},
production: {
API_BASE_URL: 'https://api.example.com/api',
WS_BASE_URL: 'wss://api.example.com',
APP_NAME: 'MyApp',
DEBUG: false,
LOG_LEVEL: 'error'
}
}
// 获取当前环境
function getCurrentEnv() {
// #ifdef H5
const hostname = window.location.hostname
if (hostname === 'localhost' || hostname === '127.0.0.1') {
return 'development'
} else if (hostname.includes('test')) {
return 'testing'
} else if (hostname.includes('staging')) {
return 'staging'
} else {
return 'production'
}
// #endif
// #ifdef APP-PLUS
// 根据打包配置或其他方式判断环境
return process.env.NODE_ENV === 'production' ? 'production' : 'development'
// #endif
// #ifdef MP
// 小程序环境判断
const accountInfo = wx.getAccountInfoSync()
const envVersion = accountInfo.miniProgram.envVersion
switch (envVersion) {
case 'develop':
return 'development'
case 'trial':
return 'testing'
case 'release':
return 'production'
default:
return 'development'
}
// #endif
}
// 获取环境配置
function getEnvConfig() {
const env = getCurrentEnv()
return ENV_CONFIG[env] || ENV_CONFIG.development
}
export { getCurrentEnv, getEnvConfig }
export default getEnvConfig()
1.3 构建脚本
// package.json
{
"name": "my-uniapp",
"version": "1.0.0",
"description": "My UniApp Project",
"main": "main.js",
"scripts": {
"serve": "npm run dev:h5",
"build": "npm run build:h5",
"build:app-plus": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus vue-cli-service uni-build",
"build:custom": "cross-env NODE_ENV=production uniapp-cli custom",
"build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build",
"build:mp-360": "cross-env NODE_ENV=production UNI_PLATFORM=mp-360 vue-cli-service uni-build",
"build:mp-alipay": "cross-env NODE_ENV=production UNI_PLATFORM=mp-alipay vue-cli-service uni-build",
"build:mp-baidu": "cross-env NODE_ENV=production UNI_PLATFORM=mp-baidu vue-cli-service uni-build",
"build:mp-kuaishou": "cross-env NODE_ENV=production UNI_PLATFORM=mp-kuaishou vue-cli-service uni-build",
"build:mp-lark": "cross-env NODE_ENV=production UNI_PLATFORM=mp-lark vue-cli-service uni-build",
"build:mp-qq": "cross-env NODE_ENV=production UNI_PLATFORM=mp-qq vue-cli-service uni-build",
"build:mp-toutiao": "cross-env NODE_ENV=production UNI_PLATFORM=mp-toutiao vue-cli-service uni-build",
"build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build",
"build:quickapp-native": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-native vue-cli-service uni-build",
"build:quickapp-webview": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview vue-cli-service uni-build",
"dev:app-plus": "cross-env NODE_ENV=development UNI_PLATFORM=app-plus vue-cli-service uni-build --watch",
"dev:custom": "cross-env NODE_ENV=development uniapp-cli custom",
"dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve",
"dev:mp-360": "cross-env NODE_ENV=development UNI_PLATFORM=mp-360 vue-cli-service uni-build --watch",
"dev:mp-alipay": "cross-env NODE_ENV=development UNI_PLATFORM=mp-alipay vue-cli-service uni-build --watch",
"dev:mp-baidu": "cross-env NODE_ENV=development UNI_PLATFORM=mp-baidu vue-cli-service uni-build --watch",
"dev:mp-kuaishou": "cross-env NODE_ENV=development UNI_PLATFORM=mp-kuaishou vue-cli-service uni-build --watch",
"dev:mp-lark": "cross-env NODE_ENV=development UNI_PLATFORM=mp-lark vue-cli-service uni-build --watch",
"dev:mp-qq": "cross-env NODE_ENV=development UNI_PLATFORM=mp-qq vue-cli-service uni-build --watch",
"dev:mp-toutiao": "cross-env NODE_ENV=development UNI_PLATFORM=mp-toutiao vue-cli-service uni-build --watch",
"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch",
"dev:quickapp-native": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-native vue-cli-service uni-build --watch",
"dev:quickapp-webview": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview vue-cli-service uni-build --watch",
"info": "node node_modules/@dcloudio/vue-cli-plugin-uni/commands/info.js",
"serve:quickapp-native": "node node_modules/@dcloudio/uni-quickapp-native/bin/serve.js",
"test:android": "cross-env UNI_PLATFORM=app-plus UNI_OS_NAME=android jest",
"test:h5": "cross-env UNI_PLATFORM=h5 jest",
"test:ios": "cross-env UNI_PLATFORM=app-plus UNI_OS_NAME=ios jest",
"test:mp-baidu": "cross-env UNI_PLATFORM=mp-baidu jest",
"test:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin jest",
"lint": "eslint --ext .js,.vue src",
"lint:fix": "eslint --ext .js,.vue src --fix",
"build:all": "npm run build:h5 && npm run build:mp-weixin && npm run build:app-plus",
"build:staging": "cross-env NODE_ENV=staging npm run build:all",
"build:production": "cross-env NODE_ENV=production npm run build:all",
"deploy:h5": "npm run build:h5 && node scripts/deploy-h5.js",
"deploy:staging": "npm run build:staging && node scripts/deploy-staging.js",
"deploy:production": "npm run build:production && node scripts/deploy-production.js"
},
"dependencies": {
"@dcloudio/uni-app": "^2.0.0",
"@dcloudio/uni-h5": "^2.0.0",
"@dcloudio/uni-mp-alipay": "^2.0.0",
"@dcloudio/uni-mp-baidu": "^2.0.0",
"@dcloudio/uni-mp-qq": "^2.0.0",
"@dcloudio/uni-mp-toutiao": "^2.0.0",
"@dcloudio/uni-mp-weixin": "^2.0.0",
"@dcloudio/uni-quickapp-native": "^2.0.0",
"@dcloudio/uni-quickapp-webview": "^2.0.0",
"@dcloudio/uni-stat": "^2.0.0",
"core-js": "^3.6.5",
"vue": "^2.6.11",
"vuex": "^3.2.0"
},
"devDependencies": {
"@dcloudio/types": "^2.3.4",
"@dcloudio/uni-automator": "^2.0.0",
"@dcloudio/uni-cli-shared": "^2.0.0",
"@dcloudio/uni-migration": "^2.0.0",
"@dcloudio/uni-template-compiler": "^2.0.0",
"@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.0",
"@dcloudio/vue-cli-plugin-uni": "^2.0.0",
"@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.0",
"@dcloudio/webpack-uni-mp-loader": "^2.0.0",
"@dcloudio/webpack-uni-pages-loader": "^2.0.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-plugin-import": "^1.11.0",
"cross-env": "^7.0.2",
"jest": "^25.4.0",
"mini-types": "*",
"postcss-comment": "^2.0.0",
"vue-template-compiler": "^2.6.11"
},
"browserslist": [
"Android >= 4.4",
"ios >= 9"
],
"uni-app": {
"scripts": {}
}
}
2. 各平台发布
2.1 H5发布
// scripts/deploy-h5.js
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')
const archiver = require('archiver')
const FTP = require('ftp')
const OSS = require('ali-oss')
class H5Deployer {
constructor(config) {
this.config = config
this.distPath = path.resolve(__dirname, '../dist/build/h5')
}
// 部署到静态服务器
async deployToStatic() {
console.log('开始部署到静态服务器...')
try {
// 压缩文件
const zipPath = await this.createZip()
// 上传到服务器
if (this.config.deployType === 'ftp') {
await this.uploadByFTP(zipPath)
} else if (this.config.deployType === 'oss') {
await this.uploadByOSS()
} else if (this.config.deployType === 'ssh') {
await this.uploadBySSH(zipPath)
}
console.log('部署完成!')
} catch (error) {
console.error('部署失败:', error)
process.exit(1)
}
}
// 创建压缩包
createZip() {
return new Promise((resolve, reject) => {
const zipPath = path.resolve(__dirname, '../dist/h5-dist.zip')
const output = fs.createWriteStream(zipPath)
const archive = archiver('zip', { zlib: { level: 9 } })
output.on('close', () => {
console.log(`压缩包创建完成: ${archive.pointer()} bytes`)
resolve(zipPath)
})
archive.on('error', reject)
archive.pipe(output)
archive.directory(this.distPath, false)
archive.finalize()
})
}
// FTP上传
uploadByFTP(zipPath) {
return new Promise((resolve, reject) => {
const ftp = new FTP()
ftp.on('ready', () => {
console.log('FTP连接成功')
// 上传压缩包
ftp.put(zipPath, path.basename(zipPath), (err) => {
if (err) {
reject(err)
return
}
console.log('文件上传完成')
// 解压文件(需要服务器支持)
ftp.raw('SITE', 'UNZIP', path.basename(zipPath), (err, data) => {
ftp.end()
if (err) {
console.warn('自动解压失败,请手动解压:', err.message)
} else {
console.log('文件解压完成')
}
resolve()
})
})
})
ftp.on('error', reject)
ftp.connect({
host: this.config.ftp.host,
port: this.config.ftp.port || 21,
user: this.config.ftp.username,
password: this.config.ftp.password
})
})
}
// OSS上传
async uploadByOSS() {
const client = new OSS({
region: this.config.oss.region,
accessKeyId: this.config.oss.accessKeyId,
accessKeySecret: this.config.oss.accessKeySecret,
bucket: this.config.oss.bucket
})
console.log('开始上传到OSS...')
// 获取所有文件
const files = this.getAllFiles(this.distPath)
// 并发上传
const uploadPromises = files.map(async (file) => {
const relativePath = path.relative(this.distPath, file)
const ossPath = path.posix.join(this.config.oss.prefix || '', relativePath)
try {
await client.put(ossPath, file)
console.log(`上传成功: ${ossPath}`)
} catch (error) {
console.error(`上传失败: ${ossPath}`, error)
throw error
}
})
await Promise.all(uploadPromises)
console.log('OSS上传完成')
}
// SSH上传
async uploadBySSH(zipPath) {
const { NodeSSH } = require('node-ssh')
const ssh = new NodeSSH()
try {
await ssh.connect({
host: this.config.ssh.host,
port: this.config.ssh.port || 22,
username: this.config.ssh.username,
password: this.config.ssh.password,
privateKey: this.config.ssh.privateKey
})
console.log('SSH连接成功')
// 上传压缩包
await ssh.putFile(zipPath, `/tmp/${path.basename(zipPath)}`)
console.log('文件上传完成')
// 解压到目标目录
const commands = [
`cd ${this.config.ssh.targetPath}`,
`rm -rf *`,
`unzip -o /tmp/${path.basename(zipPath)}`,
`rm /tmp/${path.basename(zipPath)}`
]
for (const command of commands) {
const result = await ssh.execCommand(command)
if (result.code !== 0) {
throw new Error(`命令执行失败: ${command}\n${result.stderr}`)
}
}
console.log('文件部署完成')
} finally {
ssh.dispose()
}
}
// 获取所有文件
getAllFiles(dir) {
const files = []
function traverse(currentDir) {
const items = fs.readdirSync(currentDir)
for (const item of items) {
const fullPath = path.join(currentDir, item)
const stat = fs.statSync(fullPath)
if (stat.isDirectory()) {
traverse(fullPath)
} else {
files.push(fullPath)
}
}
}
traverse(dir)
return files
}
}
// 部署配置
const deployConfig = {
deployType: 'oss', // ftp, oss, ssh
ftp: {
host: 'ftp.example.com',
port: 21,
username: 'username',
password: 'password'
},
oss: {
region: 'oss-cn-hangzhou',
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
bucket: 'my-app-bucket',
prefix: 'h5/'
},
ssh: {
host: 'server.example.com',
port: 22,
username: 'root',
password: process.env.SSH_PASSWORD,
targetPath: '/var/www/html'
}
}
// 执行部署
const deployer = new H5Deployer(deployConfig)
deployer.deployToStatic().catch(console.error)
2.2 小程序发布
// scripts/deploy-miniprogram.js
const fs = require('fs')
const path = require('path')
const ci = require('miniprogram-ci')
class MiniprogramDeployer {
constructor(config) {
this.config = config
}
// 微信小程序发布
async deployWeixin() {
console.log('开始发布微信小程序...')
try {
const project = new ci.Project({
appid: this.config.weixin.appid,
type: 'miniProgram',
projectPath: path.resolve(__dirname, '../dist/build/mp-weixin'),
privateKeyPath: this.config.weixin.privateKeyPath,
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,
autoPrefixWXSS: true
},
onProgressUpdate: (progress) => {
console.log(`上传进度: ${progress}%`)
}
})
console.log('微信小程序上传成功:', uploadResult)
// 提交审核(可选)
if (this.config.submitAudit) {
await this.submitWeixinAudit(project)
}
} catch (error) {
console.error('微信小程序发布失败:', error)
throw error
}
}
// 提交微信小程序审核
async submitWeixinAudit(project) {
try {
const submitResult = await ci.submit({
project,
version: this.config.version,
desc: this.config.desc,
setting: {
es6: true,
es7: true,
minifyJS: true,
minifyWXML: true,
minifyWXSS: true
}
})
console.log('提交审核成功:', submitResult)
} catch (error) {
console.error('提交审核失败:', error)
throw error
}
}
// 支付宝小程序发布
async deployAlipay() {
console.log('开始发布支付宝小程序...')
// 支付宝小程序需要使用支付宝开发者工具的命令行工具
const { execSync } = require('child_process')
try {
const distPath = path.resolve(__dirname, '../dist/build/mp-alipay')
// 使用支付宝开发者工具命令行上传
const command = `alipay-dev-tool upload --project ${distPath} --version ${this.config.version} --desc "${this.config.desc}"`
execSync(command, { stdio: 'inherit' })
console.log('支付宝小程序上传成功')
} catch (error) {
console.error('支付宝小程序发布失败:', error)
throw error
}
}
// 百度小程序发布
async deployBaidu() {
console.log('开始发布百度小程序...')
const { execSync } = require('child_process')
try {
const distPath = path.resolve(__dirname, '../dist/build/mp-baidu')
// 使用百度开发者工具命令行上传
const command = `swan upload --project-path ${distPath} --version ${this.config.version} --desc "${this.config.desc}"`
execSync(command, { stdio: 'inherit' })
console.log('百度小程序上传成功')
} catch (error) {
console.error('百度小程序发布失败:', error)
throw error
}
}
// 字节跳动小程序发布
async deployToutiao() {
console.log('开始发布字节跳动小程序...')
const { execSync } = require('child_process')
try {
const distPath = path.resolve(__dirname, '../dist/build/mp-toutiao')
// 使用字节跳动开发者工具命令行上传
const command = `tt-ide-cli upload --project-path ${distPath} --version ${this.config.version} --desc "${this.config.desc}"`
execSync(command, { stdio: 'inherit' })
console.log('字节跳动小程序上传成功')
} catch (error) {
console.error('字节跳动小程序发布失败:', error)
throw error
}
}
// 发布所有小程序
async deployAll() {
const platforms = this.config.platforms || ['weixin']
for (const platform of platforms) {
try {
switch (platform) {
case 'weixin':
await this.deployWeixin()
break
case 'alipay':
await this.deployAlipay()
break
case 'baidu':
await this.deployBaidu()
break
case 'toutiao':
await this.deployToutiao()
break
default:
console.warn(`不支持的平台: ${platform}`)
}
} catch (error) {
console.error(`${platform}平台发布失败:`, error)
if (this.config.stopOnError) {
throw error
}
}
}
}
}
// 发布配置
const deployConfig = {
version: process.env.VERSION || '1.0.0',
desc: process.env.DESC || '版本更新',
platforms: ['weixin', 'alipay'],
submitAudit: false,
stopOnError: true,
weixin: {
appid: 'wxXXXXXXXXXXXXXXXX',
privateKeyPath: path.resolve(__dirname, '../private.key')
}
}
// 执行发布
const deployer = new MiniprogramDeployer(deployConfig)
deployer.deployAll().catch(console.error)
2.3 App发布
// scripts/deploy-app.js
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')
const axios = require('axios')
const FormData = require('form-data')
class AppDeployer {
constructor(config) {
this.config = config
}
// Android打包发布
async deployAndroid() {
console.log('开始Android打包发布...')
try {
// 1. 云端打包
const apkPath = await this.buildAndroidCloud()
// 2. 签名APK
const signedApkPath = await this.signApk(apkPath)
// 3. 上传到分发平台
await this.uploadToDistribution(signedApkPath, 'android')
// 4. 发送通知
await this.sendNotification('Android版本发布成功')
console.log('Android发布完成')
} catch (error) {
console.error('Android发布失败:', error)
throw error
}
}
// iOS打包发布
async deployIOS() {
console.log('开始iOS打包发布...')
try {
// 1. 云端打包
const ipaPath = await this.buildIOSCloud()
// 2. 上传到App Store Connect
await this.uploadToAppStore(ipaPath)
// 3. 上传到分发平台
await this.uploadToDistribution(ipaPath, 'ios')
// 4. 发送通知
await this.sendNotification('iOS版本发布成功')
console.log('iOS发布完成')
} catch (error) {
console.error('iOS发布失败:', error)
throw error
}
}
// 云端打包Android
async buildAndroidCloud() {
console.log('开始云端打包Android...')
// 使用DCloud云端打包API
const buildResult = await this.requestCloudBuild({
platform: 'android',
type: 'release',
certificate: this.config.android.certificate,
profile: this.config.android.profile
})
// 等待打包完成
const apkPath = await this.waitForBuild(buildResult.buildId)
console.log('Android打包完成:', apkPath)
return apkPath
}
// 云端打包iOS
async buildIOSCloud() {
console.log('开始云端打包iOS...')
const buildResult = await this.requestCloudBuild({
platform: 'ios',
type: 'release',
certificate: this.config.ios.certificate,
profile: this.config.ios.profile
})
const ipaPath = await this.waitForBuild(buildResult.buildId)
console.log('iOS打包完成:', ipaPath)
return ipaPath
}
// 请求云端打包
async requestCloudBuild(options) {
const response = await axios.post('https://ide.dcloud.net.cn/build', {
appid: this.config.appid,
platform: options.platform,
type: options.type,
certificate: options.certificate,
profile: options.profile,
version: this.config.version,
versionCode: this.config.versionCode
}, {
headers: {
'Authorization': `Bearer ${this.config.accessToken}`,
'Content-Type': 'application/json'
}
})
if (response.data.code !== 0) {
throw new Error(`云端打包失败: ${response.data.message}`)
}
return response.data.data
}
// 等待打包完成
async waitForBuild(buildId) {
console.log('等待打包完成...')
let attempts = 0
const maxAttempts = 60 // 最多等待30分钟
while (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 30000)) // 等待30秒
try {
const response = await axios.get(`https://ide.dcloud.net.cn/build/${buildId}`, {
headers: {
'Authorization': `Bearer ${this.config.accessToken}`
}
})
const build = response.data.data
if (build.status === 'success') {
// 下载打包文件
return await this.downloadBuildFile(build.downloadUrl)
} else if (build.status === 'failed') {
throw new Error(`打包失败: ${build.error}`)
}
console.log(`打包进度: ${build.progress}%`)
} catch (error) {
console.error('查询打包状态失败:', error)
}
attempts++
}
throw new Error('打包超时')
}
// 下载打包文件
async downloadBuildFile(downloadUrl) {
const response = await axios({
method: 'GET',
url: downloadUrl,
responseType: 'stream'
})
const fileName = path.basename(downloadUrl)
const filePath = path.resolve(__dirname, '../dist', fileName)
const writer = fs.createWriteStream(filePath)
response.data.pipe(writer)
return new Promise((resolve, reject) => {
writer.on('finish', () => resolve(filePath))
writer.on('error', reject)
})
}
// 签名APK
async signApk(apkPath) {
console.log('开始签名APK...')
const signedApkPath = apkPath.replace('.apk', '-signed.apk')
const command = [
'jarsigner',
'-verbose',
'-sigalg', 'SHA1withRSA',
'-digestalg', 'SHA1',
'-keystore', this.config.android.keystorePath,
'-storepass', this.config.android.storePassword,
'-keypass', this.config.android.keyPassword,
'-signedjar', signedApkPath,
apkPath,
this.config.android.keyAlias
].join(' ')
execSync(command, { stdio: 'inherit' })
console.log('APK签名完成:', signedApkPath)
return signedApkPath
}
// 上传到App Store
async uploadToAppStore(ipaPath) {
console.log('上传到App Store...')
const command = [
'xcrun', 'altool',
'--upload-app',
'--type', 'ios',
'--file', ipaPath,
'--username', this.config.ios.appleId,
'--password', this.config.ios.appPassword
].join(' ')
execSync(command, { stdio: 'inherit' })
console.log('App Store上传完成')
}
// 上传到分发平台
async uploadToDistribution(filePath, platform) {
console.log(`上传到分发平台: ${platform}`)
const form = new FormData()
form.append('file', fs.createReadStream(filePath))
form.append('platform', platform)
form.append('version', this.config.version)
form.append('desc', this.config.desc)
const response = await axios.post(this.config.distribution.uploadUrl, form, {
headers: {
...form.getHeaders(),
'Authorization': `Bearer ${this.config.distribution.token}`
},
maxContentLength: Infinity,
maxBodyLength: Infinity
})
if (response.data.code !== 0) {
throw new Error(`分发平台上传失败: ${response.data.message}`)
}
console.log('分发平台上传完成:', response.data.data.downloadUrl)
}
// 发送通知
async sendNotification(message) {
if (this.config.notification.webhook) {
try {
await axios.post(this.config.notification.webhook, {
text: message,
timestamp: Date.now()
})
} catch (error) {
console.error('发送通知失败:', error)
}
}
}
// 发布所有平台
async deployAll() {
const platforms = this.config.platforms || ['android']
for (const platform of platforms) {
try {
if (platform === 'android') {
await this.deployAndroid()
} else if (platform === 'ios') {
await this.deployIOS()
}
} catch (error) {
console.error(`${platform}平台发布失败:`, error)
if (this.config.stopOnError) {
throw error
}
}
}
}
}
// 发布配置
const deployConfig = {
appid: '__UNI__XXXXXXX',
version: '1.0.0',
versionCode: 100,
desc: '版本更新',
platforms: ['android', 'ios'],
stopOnError: true,
accessToken: process.env.DCLOUD_ACCESS_TOKEN,
android: {
certificate: 'android_certificate_id',
profile: 'android_profile_id',
keystorePath: path.resolve(__dirname, '../android.keystore'),
storePassword: process.env.ANDROID_STORE_PASSWORD,
keyPassword: process.env.ANDROID_KEY_PASSWORD,
keyAlias: 'android_key'
},
ios: {
certificate: 'ios_certificate_id',
profile: 'ios_profile_id',
appleId: process.env.APPLE_ID,
appPassword: process.env.APPLE_APP_PASSWORD
},
distribution: {
uploadUrl: 'https://api.distribution.com/upload',
token: process.env.DISTRIBUTION_TOKEN
},
notification: {
webhook: process.env.NOTIFICATION_WEBHOOK
}
}
// 执行发布
const deployer = new AppDeployer(deployConfig)
deployer.deployAll().catch(console.error)
3. CI/CD集成
3.1 GitHub Actions
# .github/workflows/deploy.yml
name: Deploy UniApp
on:
push:
branches: [ main, develop ]
tags: [ 'v*' ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: '16'
DCLOUD_ACCESS_TOKEN: ${{ secrets.DCLOUD_ACCESS_TOKEN }}
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
- name: Run lint
run: npm run lint
build:
needs: test
runs-on: ubuntu-latest
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: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build ${{ matrix.platform }}
run: npm run build:${{ matrix.platform }}
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-${{ matrix.platform }}
path: dist/build/${{ matrix.platform }}
retention-days: 7
deploy-h5:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-h5
path: dist/build/h5
- name: Deploy to OSS
run: npm run deploy:h5
- name: Notify deployment
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'H5部署完成'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
deploy-miniprogram:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-mp-weixin
path: dist/build/mp-weixin
- name: Deploy miniprogram
run: |
export VERSION=${GITHUB_REF#refs/tags/v}
export DESC="Release $VERSION"
node scripts/deploy-miniprogram.js
env:
WEIXIN_PRIVATE_KEY: ${{ secrets.WEIXIN_PRIVATE_KEY }}
deploy-app:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-app-plus
path: dist/build/app-plus
- name: Deploy app
run: |
export VERSION=${GITHUB_REF#refs/tags/v}
export DESC="Release $VERSION"
node scripts/deploy-app.js
env:
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
DISTRIBUTION_TOKEN: ${{ secrets.DISTRIBUTION_TOKEN }}
3.2 GitLab CI
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
NODE_VERSION: "16"
npm_config_cache: "$CI_PROJECT_DIR/.npm"
CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/Cypress"
cache:
paths:
- .npm/
- cache/Cypress/
- node_modules/
before_script:
- node --version
- npm --version
- npm ci --cache .npm --prefer-offline
test:
stage: test
image: node:$NODE_VERSION
script:
- npm run lint
- npm run test
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
build:h5:
stage: build
image: node:$NODE_VERSION
script:
- npm run build:h5
artifacts:
paths:
- dist/build/h5/
expire_in: 1 week
only:
- main
- develop
- tags
build:mp-weixin:
stage: build
image: node:$NODE_VERSION
script:
- npm run build:mp-weixin
artifacts:
paths:
- dist/build/mp-weixin/
expire_in: 1 week
only:
- tags
build:app-plus:
stage: build
image: node:$NODE_VERSION
script:
- npm run build:app-plus
artifacts:
paths:
- dist/build/app-plus/
expire_in: 1 week
only:
- tags
deploy:h5:staging:
stage: deploy
image: node:$NODE_VERSION
script:
- npm run deploy:staging
environment:
name: staging
url: https://staging.example.com
only:
- develop
dependencies:
- build:h5
deploy:h5:production:
stage: deploy
image: node:$NODE_VERSION
script:
- npm run deploy:production
environment:
name: production
url: https://app.example.com
only:
- main
dependencies:
- build:h5
when: manual
deploy:miniprogram:
stage: deploy
image: node:$NODE_VERSION
script:
- export VERSION=${CI_COMMIT_TAG#v}
- export DESC="Release $VERSION"
- node scripts/deploy-miniprogram.js
only:
- tags
dependencies:
- build:mp-weixin
deploy:app:
stage: deploy
image: node:$NODE_VERSION
script:
- export VERSION=${CI_COMMIT_TAG#v}
- export DESC="Release $VERSION"
- node scripts/deploy-app.js
only:
- tags
dependencies:
- build:app-plus
when: manual
4. 版本管理
4.1 语义化版本
// scripts/version-manager.js
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')
class VersionManager {
constructor() {
this.packagePath = path.resolve(__dirname, '../package.json')
this.manifestPath = path.resolve(__dirname, '../src/manifest.json')
}
// 获取当前版本
getCurrentVersion() {
const packageJson = JSON.parse(fs.readFileSync(this.packagePath, 'utf8'))
return packageJson.version
}
// 更新版本
updateVersion(type = 'patch') {
const currentVersion = this.getCurrentVersion()
const newVersion = this.incrementVersion(currentVersion, type)
console.log(`版本更新: ${currentVersion} -> ${newVersion}`)
// 更新package.json
this.updatePackageVersion(newVersion)
// 更新manifest.json
this.updateManifestVersion(newVersion)
// 创建git tag
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版本
updatePackageVersion(version) {
const packageJson = JSON.parse(fs.readFileSync(this.packagePath, 'utf8'))
packageJson.version = version
fs.writeFileSync(this.packagePath, JSON.stringify(packageJson, null, 2))
}
// 更新manifest.json版本
updateManifestVersion(version) {
const manifest = JSON.parse(fs.readFileSync(this.manifestPath, 'utf8'))
manifest.versionName = version
manifest.versionCode = this.getVersionCode(version)
fs.writeFileSync(this.manifestPath, JSON.stringify(manifest, null, 2))
}
// 生成版本代码
getVersionCode(version) {
const parts = version.split('.').map(Number)
return parts[0] * 10000 + parts[1] * 100 + parts[2]
}
// 创建Git标签
createGitTag(version) {
try {
execSync(`git add .`)
execSync(`git commit -m "chore: release v${version}"`)
execSync(`git tag v${version}`)
console.log(`Git标签创建成功: v${version}`)
} catch (error) {
console.error('Git标签创建失败:', error.message)
}
}
// 生成变更日志
generateChangelog(version) {
const changelogPath = path.resolve(__dirname, '../CHANGELOG.md')
const date = new Date().toISOString().split('T')[0]
let changelog = ''
if (fs.existsSync(changelogPath)) {
changelog = fs.readFileSync(changelogPath, 'utf8')
}
const newEntry = `## [${version}] - ${date}\n\n### Added\n- 新功能\n\n### Changed\n- 功能改进\n\n### Fixed\n- 问题修复\n\n`
const updatedChangelog = newEntry + changelog
fs.writeFileSync(changelogPath, updatedChangelog)
console.log('变更日志已更新')
}
}
// 使用示例
const versionManager = new VersionManager()
const type = process.argv[2] || 'patch'
const newVersion = versionManager.updateVersion(type)
versionManager.generateChangelog(newVersion)
module.exports = VersionManager
4.2 发布检查清单
// scripts/pre-release-check.js
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')
class PreReleaseChecker {
constructor() {
this.checks = []
this.results = []
}
// 添加检查项
addCheck(name, checkFn) {
this.checks.push({ name, checkFn })
}
// 运行所有检查
async runChecks() {
console.log('开始发布前检查...')
for (const check of this.checks) {
try {
console.log(`检查: ${check.name}`)
const result = await check.checkFn()
this.results.push({ name: check.name, status: 'pass', result })
console.log(`✓ ${check.name} 通过`)
} catch (error) {
this.results.push({ name: check.name, status: 'fail', error: error.message })
console.log(`✗ ${check.name} 失败: ${error.message}`)
}
}
this.generateReport()
}
// 生成检查报告
generateReport() {
const passCount = this.results.filter(r => r.status === 'pass').length
const failCount = this.results.filter(r => r.status === 'fail').length
console.log('\n=== 发布前检查报告 ===')
console.log(`总检查项: ${this.results.length}`)
console.log(`通过: ${passCount}`)
console.log(`失败: ${failCount}`)
if (failCount > 0) {
console.log('\n失败项目:')
this.results
.filter(r => r.status === 'fail')
.forEach(r => console.log(`- ${r.name}: ${r.error}`))
process.exit(1)
} else {
console.log('\n所有检查通过,可以发布!')
}
}
}
// 预定义检查项
const checker = new PreReleaseChecker()
// 代码质量检查
checker.addCheck('ESLint检查', () => {
execSync('npm run lint', { stdio: 'pipe' })
return '代码格式正确'
})
// 单元测试
checker.addCheck('单元测试', () => {
execSync('npm run test', { stdio: 'pipe' })
return '所有测试通过'
})
// 构建测试
checker.addCheck('构建测试', () => {
execSync('npm run build:h5', { stdio: 'pipe' })
return '构建成功'
})
// 配置文件检查
checker.addCheck('配置文件检查', () => {
const manifestPath = path.resolve(__dirname, '../src/manifest.json')
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
if (!manifest.appid) {
throw new Error('缺少appid配置')
}
if (!manifest.versionName) {
throw new Error('缺少版本号配置')
}
return '配置文件正确'
})
// 依赖检查
checker.addCheck('依赖安全检查', () => {
try {
execSync('npm audit --audit-level=high', { stdio: 'pipe' })
return '依赖安全'
} catch (error) {
throw new Error('发现高危依赖漏洞')
}
})
// 文件大小检查
checker.addCheck('文件大小检查', () => {
const distPath = path.resolve(__dirname, '../dist/build/h5')
if (!fs.existsSync(distPath)) {
throw new Error('构建文件不存在')
}
const stats = fs.statSync(distPath)
const sizeInMB = stats.size / (1024 * 1024)
if (sizeInMB > 50) {
throw new Error(`构建文件过大: ${sizeInMB.toFixed(2)}MB`)
}
return `文件大小正常: ${sizeInMB.toFixed(2)}MB`
})
// 运行检查
if (require.main === module) {
checker.runChecks().catch(console.error)
}
module.exports = PreReleaseChecker
4.3 自动化发布脚本
// scripts/release.js
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')
const VersionManager = require('./version-manager')
const PreReleaseChecker = require('./pre-release-check')
class ReleaseManager {
constructor(options = {}) {
this.options = {
skipTests: false,
skipBuild: false,
platforms: ['h5', 'mp-weixin'],
...options
}
this.versionManager = new VersionManager()
this.checker = new PreReleaseChecker()
}
// 执行发布流程
async release(versionType = 'patch') {
try {
console.log('🚀 开始发布流程...')
// 1. 发布前检查
if (!this.options.skipTests) {
console.log('\n📋 执行发布前检查...')
await this.checker.runChecks()
}
// 2. 更新版本号
console.log('\n📝 更新版本号...')
const newVersion = this.versionManager.updateVersion(versionType)
// 3. 构建项目
if (!this.options.skipBuild) {
console.log('\n🔨 构建项目...')
await this.buildProjects()
}
// 4. 提交代码
console.log('\n📤 提交代码...')
this.commitChanges(newVersion)
// 5. 创建标签
console.log('\n🏷️ 创建Git标签...')
this.createTag(newVersion)
// 6. 推送到远程
console.log('\n⬆️ 推送到远程仓库...')
this.pushToRemote()
// 7. 发布通知
console.log('\n📢 发送发布通知...')
await this.sendNotification(newVersion)
console.log(`\n✅ 发布完成!版本: v${newVersion}`)
} catch (error) {
console.error('\n❌ 发布失败:', error.message)
process.exit(1)
}
}
// 构建项目
async buildProjects() {
for (const platform of this.options.platforms) {
console.log(`构建 ${platform}...`)
execSync(`npm run build:${platform}`, { stdio: 'inherit' })
}
}
// 提交更改
commitChanges(version) {
execSync('git add .')
execSync(`git commit -m "chore: release v${version}"`)
}
// 创建标签
createTag(version) {
execSync(`git tag v${version}`)
}
// 推送到远程
pushToRemote() {
execSync('git push origin main')
execSync('git push origin --tags')
}
// 发送通知
async sendNotification(version) {
const webhookUrl = process.env.RELEASE_WEBHOOK_URL
if (!webhookUrl) {
console.log('未配置通知webhook,跳过通知')
return
}
try {
const axios = require('axios')
await axios.post(webhookUrl, {
text: `🎉 新版本发布: v${version}`,
version,
timestamp: new Date().toISOString()
})
console.log('发布通知发送成功')
} catch (error) {
console.warn('发布通知发送失败:', error.message)
}
}
}
// 命令行接口
if (require.main === module) {
const args = process.argv.slice(2)
const versionType = args[0] || 'patch'
const options = {}
// 解析命令行参数
if (args.includes('--skip-tests')) {
options.skipTests = true
}
if (args.includes('--skip-build')) {
options.skipBuild = true
}
const platformIndex = args.indexOf('--platforms')
if (platformIndex !== -1 && args[platformIndex + 1]) {
options.platforms = args[platformIndex + 1].split(',')
}
const releaseManager = new ReleaseManager(options)
releaseManager.release(versionType).catch(console.error)
}
module.exports = ReleaseManager
5. 监控与维护
5.1 应用监控
// utils/monitor.js
class AppMonitor {
constructor(config) {
this.config = config
this.errorQueue = []
this.performanceQueue = []
this.userActionQueue = []
}
// 错误监控
captureError(error, context = {}) {
const errorInfo = {
message: error.message,
stack: error.stack,
timestamp: Date.now(),
url: this.getCurrentPage(),
userAgent: this.getUserAgent(),
userId: this.getUserId(),
...context
}
this.errorQueue.push(errorInfo)
// 立即上报严重错误
if (this.isCriticalError(error)) {
this.reportError(errorInfo)
} else {
// 批量上报
this.scheduleReport('error')
}
}
// 性能监控
capturePerformance(metric) {
const performanceInfo = {
...metric,
timestamp: Date.now(),
page: this.getCurrentPage(),
userId: this.getUserId()
}
this.performanceQueue.push(performanceInfo)
this.scheduleReport('performance')
}
// 用户行为监控
captureUserAction(action, data = {}) {
const actionInfo = {
action,
data,
timestamp: Date.now(),
page: this.getCurrentPage(),
userId: this.getUserId()
}
this.userActionQueue.push(actionInfo)
this.scheduleReport('userAction')
}
// 判断是否为严重错误
isCriticalError(error) {
const criticalKeywords = ['network', 'timeout', 'crash', 'memory']
return criticalKeywords.some(keyword =>
error.message.toLowerCase().includes(keyword)
)
}
// 获取当前页面
getCurrentPage() {
// #ifdef H5
return window.location.pathname
// #endif
// #ifdef APP-PLUS || MP
const pages = getCurrentPages()
return pages.length > 0 ? pages[pages.length - 1].route : 'unknown'
// #endif
}
// 获取用户代理
getUserAgent() {
// #ifdef H5
return navigator.userAgent
// #endif
// #ifdef APP-PLUS
return plus.navigator.getUserAgent()
// #endif
// #ifdef MP
const systemInfo = uni.getSystemInfoSync()
return `${systemInfo.platform} ${systemInfo.system}`
// #endif
}
// 获取用户ID
getUserId() {
return uni.getStorageSync('userId') || 'anonymous'
}
// 调度上报
scheduleReport(type) {
if (this.reportTimer) {
clearTimeout(this.reportTimer)
}
this.reportTimer = setTimeout(() => {
this.batchReport()
}, this.config.reportInterval || 5000)
}
// 批量上报
async batchReport() {
try {
const data = {
errors: this.errorQueue.splice(0),
performance: this.performanceQueue.splice(0),
userActions: this.userActionQueue.splice(0),
timestamp: Date.now(),
appVersion: this.config.appVersion,
platform: this.getPlatform()
}
if (data.errors.length === 0 &&
data.performance.length === 0 &&
data.userActions.length === 0) {
return
}
await this.sendReport(data)
} catch (error) {
console.error('监控数据上报失败:', error)
}
}
// 发送报告
async sendReport(data) {
return new Promise((resolve, reject) => {
uni.request({
url: this.config.reportUrl,
method: 'POST',
data,
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.apiKey}`
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else {
reject(new Error(`上报失败: ${res.statusCode}`))
}
},
fail: reject
})
})
}
// 获取平台信息
getPlatform() {
// #ifdef H5
return 'h5'
// #endif
// #ifdef APP-PLUS
return 'app'
// #endif
// #ifdef MP-WEIXIN
return 'mp-weixin'
// #endif
// #ifdef MP-ALIPAY
return 'mp-alipay'
// #endif
return 'unknown'
}
}
// 全局监控实例
const monitor = new AppMonitor({
reportUrl: 'https://api.example.com/monitor',
apiKey: 'your-api-key',
appVersion: '1.0.0',
reportInterval: 5000
})
// 全局错误处理
uni.onError((error) => {
monitor.captureError(error)
})
// 全局未处理的Promise拒绝
uni.onUnhandledRejection((event) => {
monitor.captureError(new Error(event.reason), {
type: 'unhandledRejection'
})
})
export default monitor
5.2 性能监控
// utils/performance.js
import monitor from './monitor'
class PerformanceMonitor {
constructor() {
this.pageStartTime = 0
this.navigationStartTime = 0
}
// 页面加载性能监控
measurePageLoad() {
// #ifdef H5
if (window.performance && window.performance.timing) {
const timing = window.performance.timing
const metrics = {
// DNS查询时间
dnsTime: timing.domainLookupEnd - timing.domainLookupStart,
// TCP连接时间
tcpTime: timing.connectEnd - timing.connectStart,
// 请求时间
requestTime: timing.responseEnd - timing.requestStart,
// 解析DOM树时间
domParseTime: timing.domComplete - timing.domLoading,
// 白屏时间
whiteScreenTime: timing.responseStart - timing.navigationStart,
// 首屏时间
firstScreenTime: timing.loadEventEnd - timing.navigationStart
}
monitor.capturePerformance({
type: 'pageLoad',
metrics
})
}
// #endif
// #ifdef APP-PLUS || MP
const endTime = Date.now()
const loadTime = endTime - this.pageStartTime
monitor.capturePerformance({
type: 'pageLoad',
metrics: {
loadTime
}
})
// #endif
}
// API请求性能监控
measureApiRequest(url, startTime, endTime, success) {
const duration = endTime - startTime
monitor.capturePerformance({
type: 'apiRequest',
metrics: {
url,
duration,
success,
timestamp: startTime
}
})
}
// 内存使用监控
measureMemoryUsage() {
// #ifdef H5
if (window.performance && window.performance.memory) {
const memory = window.performance.memory
monitor.capturePerformance({
type: 'memory',
metrics: {
usedJSHeapSize: memory.usedJSHeapSize,
totalJSHeapSize: memory.totalJSHeapSize,
jsHeapSizeLimit: memory.jsHeapSizeLimit
}
})
}
// #endif
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, (info) => {
monitor.capturePerformance({
type: 'memory',
metrics: {
memory: info.memory
}
})
})
// #endif
}
// 帧率监控
measureFPS() {
let lastTime = Date.now()
let frameCount = 0
const measureFrame = () => {
frameCount++
const currentTime = Date.now()
if (currentTime - lastTime >= 1000) {
const fps = Math.round((frameCount * 1000) / (currentTime - lastTime))
monitor.capturePerformance({
type: 'fps',
metrics: {
fps,
timestamp: currentTime
}
})
frameCount = 0
lastTime = currentTime
}
requestAnimationFrame(measureFrame)
}
requestAnimationFrame(measureFrame)
}
// 开始页面计时
startPageTiming() {
this.pageStartTime = Date.now()
}
// 结束页面计时
endPageTiming() {
this.measurePageLoad()
}
}
const performanceMonitor = new PerformanceMonitor()
// 页面生命周期钩子
export const pageLifecycleMixin = {
onLoad() {
performanceMonitor.startPageTiming()
},
onReady() {
performanceMonitor.endPageTiming()
},
onShow() {
// 开始FPS监控
performanceMonitor.measureFPS()
// 定期监控内存使用
this.memoryTimer = setInterval(() => {
performanceMonitor.measureMemoryUsage()
}, 30000)
},
onHide() {
// 停止内存监控
if (this.memoryTimer) {
clearInterval(this.memoryTimer)
}
}
}
export default performanceMonitor
5.3 日志管理
// utils/logger.js
class Logger {
constructor(config = {}) {
this.config = {
level: 'info',
maxLogs: 1000,
enableConsole: true,
enableRemote: true,
remoteUrl: '',
...config
}
this.logs = []
this.levels = {
debug: 0,
info: 1,
warn: 2,
error: 3
}
}
// 记录日志
log(level, message, data = {}) {
if (this.levels[level] < this.levels[this.config.level]) {
return
}
const logEntry = {
level,
message,
data,
timestamp: new Date().toISOString(),
page: this.getCurrentPage(),
userId: this.getUserId()
}
// 添加到本地日志队列
this.logs.push(logEntry)
// 限制日志数量
if (this.logs.length > this.config.maxLogs) {
this.logs.shift()
}
// 控制台输出
if (this.config.enableConsole) {
this.consoleLog(level, message, data)
}
// 远程上报
if (this.config.enableRemote && level === 'error') {
this.reportLog(logEntry)
}
}
// 各级别日志方法
debug(message, data) {
this.log('debug', message, data)
}
info(message, data) {
this.log('info', message, data)
}
warn(message, data) {
this.log('warn', message, data)
}
error(message, data) {
this.log('error', message, data)
}
// 控制台输出
consoleLog(level, message, data) {
const timestamp = new Date().toLocaleTimeString()
const prefix = `[${timestamp}] [${level.toUpperCase()}]`
switch (level) {
case 'debug':
console.debug(prefix, message, data)
break
case 'info':
console.info(prefix, message, data)
break
case 'warn':
console.warn(prefix, message, data)
break
case 'error':
console.error(prefix, message, data)
break
}
}
// 上报日志
async reportLog(logEntry) {
if (!this.config.remoteUrl) {
return
}
try {
await uni.request({
url: this.config.remoteUrl,
method: 'POST',
data: logEntry,
header: {
'Content-Type': 'application/json'
}
})
} catch (error) {
console.error('日志上报失败:', error)
}
}
// 获取所有日志
getLogs(level = null) {
if (level) {
return this.logs.filter(log => log.level === level)
}
return this.logs
}
// 清空日志
clearLogs() {
this.logs = []
}
// 导出日志
exportLogs() {
const logsText = this.logs.map(log => {
return `${log.timestamp} [${log.level.toUpperCase()}] ${log.message} ${JSON.stringify(log.data)}`
}).join('\n')
return logsText
}
// 获取当前页面
getCurrentPage() {
const pages = getCurrentPages()
return pages.length > 0 ? pages[pages.length - 1].route : 'unknown'
}
// 获取用户ID
getUserId() {
return uni.getStorageSync('userId') || 'anonymous'
}
}
// 创建全局日志实例
const logger = new Logger({
level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
remoteUrl: 'https://api.example.com/logs'
})
export default logger
总结
本章详细介绍了 UniApp 应用的打包发布与部署流程,包括:
- 配置管理:manifest.json 配置、环境配置、构建脚本
- 多平台发布:H5、小程序、原生 App 的发布流程
- CI/CD 集成:自动化构建和部署
- 版本管理:语义化版本控制和发布流程
- 监控维护:应用监控、性能监控、日志管理
掌握这些内容,可以建立完整的 UniApp 应用发布流程,确保应用质量和发布效率。通过自动化工具和监控系统,能够实现高效的开发运维一体化流程。