UniApp开发相关
2026/2/2大约 6 分钟
UniApp开发相关
UniApp系统更新
App更新配置设计
配置文件
{
// 热更新包
"wgtUrl": "xxx.wgt",
"grayscaleWgtUrl": "xxx.wgt",
// IOS APPStore 跳转链接
"iosAppstore": "https://itunes.apple.com/cn/lookup?id=xxx",
// 安卓Apk下载地址
"apkUrl": "xxx.apk",
"grayscaleApkUrl": "xxx.apk",
"androidAppstorexiaomi": "market://details?id=xxx",
// 对应渠道更新地址
"channelApkxiaomi": "xxx.apk",
"grayscaleChannelApkxiaomi": "xxx.apk",
"marketUpdateChannelWgt": ",xiaomi,",
"marketUpdateChannelApk": ",xiaomi,",
// 默认不要修改
"androidFullPackage": 0,
// 更新内容提示
"message": "1.修复bug",
// 标准更新版本
"version": "1.0.2",
// 最低版本
"minVersion": "1.0.1",
// 灰度更新版本
"grayscaleVersion": "1.0.3",
// 允许灰度更新版本的账号
"grayscaleAccountAllowList": "",
// 测试版本
"testVersion": ",1.0.4,1.0.5,",
// 提审版本版本
"reviewVersion": ",1.0.3,",
// 正式版配置
"config": {
"apiPath": ""
},
// 版本配置
"testConfigExclude": "",
"testConfig": {
"apiPath": ""
},
// 版本配置
"reviewConfigExclude": ",xiaomi,guanwang,",
"reviewConfig": {
"apiPath": ""
}
}字段说明
灰度发布相关
grayscaleVersion当前灰度版本号grayscaleAccountAllowList支持灰度测试账号grayscaleWgtUrl灰度热更新包地址
首先调用后端接口根据用户IP判断当前用户时候在灰度名单中,其次根据
grayscaleAccountAllowList判断,满足其中之一则进行灰度更新
更新相关字段
当主版本或此版本变更时为应用原生内容更新需要更新整个apk,等小版本变更可直接走wgt热更新
version当前最新版本号minVersion最低可用版本 、grayscaleVersion当前灰度版本号wgtUrl热更新包地址、grayscaleWgtUrl灰度热更新包地址marketUpdateChannelWgt表示支持小版本是否支持应用商店更新,如无紧急情况配置全部渠道已节省流量资源marketUpdateChannelApk表示支持大版本是否支持应用商店更新,如无紧急情况配置全部渠道已节省流量资源apkUrlapk更新包地址grayscaleApkUrl灰度apk更新包地址channelApkxiaomi(channelApk+渠道标签) 为对应渠道的apk更新地址,当该渠道不在marketUpdateChannelApk中时必须配置并更新该地址androidAppstorexiaomi(androidAppstore+渠道标签)为渠道应用商店更新grayscaleChannelApkxiaomi(``grayscaleChannelApk`+渠道标签)为渠道apk灰度发布下载地址
配置环境相关字段
reviewVersion提审版本版本testVersion测试版本config正式版配置testConfig测试版配置reviewConfig提审版配置reviewConfigExclude提审配置需要排除的渠道渠道testConfigExclude测试配置需要排除的渠道渠道
根据当前 reviewVersion 和 testVersion 判断读取的环境变量类型,并根据当前环境读取对应环境变量信息
相关代码
系统启动首屏加载配置信息校验是否更新
<template>
<div>
<!-- 版本更新组件 -->
<VersionUpdate
v-if="updateData"
ref="versionUpdateRef"
:current-version="currentVersion"
:is-grayscale="isGrayscale"
:update-option="updateData"
@cancel-click="onCancelClick"
/>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue'
import { compareVersion, getUpdateVersion, getVersionEnvConfig, isGrayscaleAccount, transformConfig, VersionCompareResult } from './update'
import type { AppUpdateConfig } from './update'
import VersionUpdate from './version-update.vue'
const versionUpdateRef = ref<InstanceType<typeof VersionUpdate>>()
const YxqUpdateFileUrl = 'https://caigouzi1.github.io/blog/app_update.json'
const currentVersion = ref('1.0.0')
/* 是否灰度更新 */
const isGrayscale = ref(false)
const channel = 'xiaomi'
// const channel = getAppChannel()()
const updateData = ref<null | AppUpdateConfig>(null)
const uni = {
request: (option:any) => {
new Promise((resolve, reject) => {
fetch(option.url, {
method: option.method || 'GET'
}).then((response) => response.json())
.then((data) => {
resolve(data)
console.log(data)
}).catch(e => {
reject(e)
})
})
}
}
onMounted(() => {
const systemInfo = uni.getSystemInfoSync()
currentVersion.value = systemInfo.appWgtVersion || systemInfo.version
if (networkStore.isOffline) {
uni.onNetworkStatusChange((res) => {
if (res.isConnected) {
init()
}
})
} else {
init()
}
})
function init() {
Promise.allSettled(
[
uni.request({
url: '',
method: 'POST',
}),
uni.request({
url: YxqUpdateFileUrl,
method: 'GET',
}),
],
).then((results) => {
if (results[0].status === 'fulfilled') {
const apiRes = results[0].value
if (apiRes.statusCode === 200) {
const queryHttpData = apiRes.data as any
// 相关逻辑
}
}
if (results[1].status === 'fulfilled') {
const fileRes = results[1].value
if (fileRes.statusCode === 200) {
updateData.value = fileRes.data as AppUpdateConfig
const config = transformConfig(getVersionEnvConfig(updateData.value, currentVersion.value, channel), channel)
console.log('=======api env===========')
console.log(config.type)
if (config.apiPath) {
uni.$apiUrl = config.apiPath
}
uni.$envConfig = config
updateConfig()
if (!isGrayscale.value && updateData.value) {
const userInfo = userStore.getUserInfo()
isGrayscale.value = isGrayscaleAccount(updateData.value, userInfo.userId)
}
// #ifdef APP-PLUS
const updateVersion = getUpdateVersion(updateData.value, isGrayscale.value)
const res = compareVersion(currentVersion.value, updateVersion)
if (res !== VersionCompareResult.SAME) {
nextTick(() => {
versionUpdateRef.value?.showUpdateModal()
})
return
}
// #endif
}
}
// 跳转首页
}).catch(() => {
// 跳转首页
})
}
function onCancelClick() {
// 跳转首页
}
</script>
<style scoped>
.tip-container {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 400rpx;
background-color: #fff;
padding: 40rpx;
border-radius: 18rpx;
}
</style>更新弹框
<template>
<view v-if="visible" class="update-mask flex-center">
<view class="content botton-radius">
<view class="content-top">
<view class="content-top-text">
<text class="">
发现新版本 v{{ updateVersion }}
</text>
<text class="version">
当前版本:{{ currentVersion }}
</text>
</view>
<image class="content-top-bg" width="100%" height="100%" src="/static/version-update/bg_top.png" />
</view>
<view class="content-header" />
<view class="content-body">
<view class="title">
<text>更新内容</text>
</view>
<view class="body" style="color: #333">
<scroll-view class="box-des-scroll" scroll-y="true">
<rich-text :nodes="updateOption.message.replace(/\n/g, '<br>')" />
</scroll-view>
</view>
<view class="footer flex-center">
<view v-if="!updateBtn" class="progress-box flex-column">
<progress class="progress" border-radius="35" :percent="percent" activeColor="#3DA7FF" show-info stroke-width="10" />
<view>
<text class="fs24" style="color: #333">
正在下载,请稍后 ({{ downloadedSize }}/{{ packageFileSize }}M)
</text>
</view>
</view>
<button v-if="updateBtn" class="content-button" style="border: none;color: #fff;" plain @click="confirm">
立即升级
</button>
</view>
</view>
<image v-if="cancelBtn" class="close-img" src="/static/version-update/app_update_close.png" @click.stop="cancel" />
</view>
</view>
</template>
<script lang="ts" setup>
import { compareVersion, getUpdateResult, getUpdateVersion, UpdateType, VersionCompareResult } from './update.ts'
import type { AppUpdateConfig } from './update.ts'
import { computed, ref, resolveComponent } from 'vue'
const ScrollView = resolveComponent('scroll-view')
const props = withDefaults(defineProps<{
currentVersion: string
updateOption: AppUpdateConfig
isGrayscale?: boolean
}>(), {
isGrayscale: false,
})
const emits = defineEmits<{
cancelClick: []
}>()
const updateVersion = computed(() => getUpdateVersion(props.updateOption, props.isGrayscale))
// 组件状态
const visible = ref(false)
const percent = ref(0)
const updateBtn = ref(true)
const downloadedSize = ref('0')
const packageFileSize = ref('0')
/**
* 当前版本低于最低版本时显示不显示关闭按钮
*/
const cancelBtn = computed(() => compareVersion(props.currentVersion, props.updateOption.minVersion) === VersionCompareResult.SAME)
function cancel() {
visible.value = false
emits('cancelClick')
}
// 确认更新
function confirm() {
const systemInfo = uni.getSystemInfoSync()
const updateRes = getUpdateResult(props.updateOption, {
isGrayscale: props.isGrayscale,
currentVersion: props.currentVersion,
// channel: getAppChannel(),
platform: systemInfo.osName as any,
})
if (updateRes.updateType === UpdateType.WGT) {
download(updateRes.url)
} else if (updateRes.updateType === UpdateType.APK) {
download(updateRes.url)
} else if (updateRes.updateType === UpdateType.STORE) {
plus.runtime.openURL(updateRes.url)
}
}
// 下载更新包
function download(url: string) {
uni.showToast({
title: '开始下载',
icon: 'none',
duration: 2500,
})
updateBtn.value = false
const downloadTask = uni.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200) {
plus.runtime.install(
res.tempFilePath,
{
force: true,
},
() => {
console.log('安装成功')
plus.runtime.restart()
},
(e) => {
console.error('安装失败:', e)
uni.showToast({
title: e.message,
icon: 'none',
duration: 2500,
})
},
)
}
},
})
// 监听下载进度
downloadTask.onProgressUpdate((res) => {
percent.value = res.progress
downloadedSize.value = (res.totalBytesWritten / 1024 ** 2).toFixed(2)
packageFileSize.value = (res.totalBytesExpectedToWrite / 1024 ** 2).toFixed(2)
})
}
function showUpdateModal() {
visible.value = true
}
// 暴露方法给父组件
defineExpose({
showUpdateModal,
})
</script>
<style scoped>
page {
background: transparent;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.update-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.65);
z-index: 9999;
}
.botton-radius {
border-bottom-left-radius: 30rpx;
border-bottom-right-radius: 30rpx;
}
.content {
position: relative;
top: 0;
width: 600rpx;
background-color: #fff;
box-sizing: border-box;
padding: 0 50rpx;
font-family: Source Han Sans CN;
}
.content-top {
position: absolute;
top: -195rpx;
left: 0;
width: 600rpx;
height: 270rpx;
}
.content-top-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.content-top-text {
font-size: 40rpx;
font-weight: bold;
color: #f8f8fa;
position: absolute;
top: 120rpx;
left: 50rpx;
z-index: 1;
display: flex;
flex-direction: column;
}
.content-header {
height: 70rpx;
}
.title {
font-size: 33rpx;
font-weight: bold;
color: #3da7ff;
line-height: 38px;
}
.footer {
height: 150rpx;
display: flex;
align-items: center;
justify-content: space-around;
}
.box-des-scroll {
box-sizing: border-box;
padding: 0 40rpx;
text-align: left;
max-height: 200rpx;
}
.progress-box {
width: 100%;
}
.progress {
width: 83%;
height: 40rpx;
border-radius: 35px;
}
.close-img {
width: 70rpx;
height: 70rpx;
z-index: 1000;
position: absolute;
bottom: -120rpx;
left: calc(50% - 70rpx / 2);
}
.content-button {
text-align: center;
flex: 1;
font-size: 30rpx;
font-weight: 400;
color: #ffffff;
border-radius: 40rpx;
margin: 0 18rpx;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(to right, #1785ff, #3da7ff);
}
.flex-column {
display: flex;
flex-direction: column;
align-items: center;
}
.fs24 {
font-size: 24rpx;
}
.version {
font-size: 24rpx;
margin-top: 10rpx;
color: #eeeeee;
text-decoration: underline;
}
</style>更新相关函数
/**
* API 配置接口
*/
export interface EnvConfig {
apiPath: string
homePageTab?: string []
hiddenTabBer?: string[]
type: 'product' | 'test' | 'review'
ios: EnvConfig
}
// 定义枚举
export enum VersionCompareResult {
/** 全量更新 */
Full = 0,
/** 修订版本小 */
PATCH = 1,
/** 版本不需要更新 */
SAME = 2,
}
/** 更新类型 */
export enum UpdateType {
/** 热更新 */
WGT = 'wgt',
/** 全量更新 */
APK = 'apk',
/** 应用商店更新 */
STORE = 'store',
}
/**
* 应用更新配置接口
*/
export interface AppUpdateConfig {
// 热更新包配置
wgtUrl: string
grayscaleWgtUrl: string
// 应用商店链接
iosAppstore: string
apkUrl: string
grayscaleApkUrl: string
// 渠道配置
marketUpdateChannelWgt: string
marketUpdateChannelApk: string
channelApkxiaomi: string
grayscaleChannelApkxiaomi: string
androidAppstorexiaomi: string
// 更新信息
message: string
version: string
minVersion: string
// 灰度配置
grayscaleVersion: string
grayscaleAccountAllowList: string
// 版本管理
testConfigExclude: string
testVersion: string
reviewConfigExclude: string
reviewVersion: string
// 环境配置
config: EnvConfig
testConfig: EnvConfig
reviewConfig: EnvConfig
}
function isContentIncludes(str: string, content: string, defaultValue = false) {
if (str) {
return str.includes(`,${content},`)
}
return defaultValue
}
/**
* 是否为灰度用户
*/
export function isGrayscaleAccount(config: AppUpdateConfig, userId: string) {
return isContentIncludes(config.grayscaleAccountAllowList, userId)
}
/**
*
* @param appVer 当前版本 如:1.0.0
* @param updateVer 最新本版 如:1.0.1
*/
export function compareVersion(appVer: string, updateVer: string): VersionCompareResult {
const appArr = appVer.split('.').map(Number)
const updateArr = updateVer.split('.').map(Number)
// 主版本号比较
if (appArr[0] < updateArr[0]) {
return VersionCompareResult.Full
}
else if (appArr[0] > updateArr[0]) {
return VersionCompareResult.SAME
}
// 次版本号比较
if (appArr[1] < updateArr[1]) {
return VersionCompareResult.Full
}
else if (appArr[1] > updateArr[1]) {
return VersionCompareResult.SAME
}
// 修订版本号比较
if (appArr[2] < updateArr[2]) {
return VersionCompareResult.PATCH
}
else if (appArr[2] > updateArr[2]) {
return VersionCompareResult.SAME
}
// 版本完全相同
return VersionCompareResult.SAME
}
/**
* 判断可升级版本
* @param config 配型配置
* @param isGrayscale 是否为灰度用户或IP
*/
export function getUpdateVersion(config: AppUpdateConfig, isGrayscale = false) {
if (isGrayscale) {
return config.grayscaleVersion
}
return config.version
}
export function transformConfig(config: EnvConfig, channel = '') {
if (channel && config[channel]) {
return Object.assign({}, config, config[channel])
}
return config
}
/**
* 获取该版本对应的环境变量
* @param config 配型配置
* @param version 当前版本
*/
export function getVersionEnvConfig(config: AppUpdateConfig, version: string, channel = ''): EnvConfig {
const isReview = isContentIncludes(config.reviewVersion, version)
if (isReview) {
if (channel) {
if (!isContentIncludes(config.reviewConfigExclude || '', channel)) {
return config.reviewConfig
}
} else {
return config.reviewConfig
}
}
const isTest = isContentIncludes(config.testVersion, version)
if (isTest) {
if (channel) {
if (!isContentIncludes(config.testConfigExclude || '', channel)) {
return config.testConfig
}
} else {
return config.testConfig
}
}
return config.config
}
export function getChannelMarket(config: AppUpdateConfig, channel: string) {
const target = config?.[`androidAppstore${channel}`]
return target
}
export function getChannelApkUrl(config: AppUpdateConfig, channel: string, isGrayscale: boolean) {
if (isGrayscale) {
if (channel) {
return config?.[`grayscaleChannelApk${channel}`]
}
return config.grayscaleApkUrl
} else {
if (channel) {
return config?.[`channelApk${channel}`]
}
return config.apkUrl
}
}
/**
* 获取更新结果
* @param config
* @param option
*/
export function getUpdateResult(config: AppUpdateConfig, option: {
isGrayscale: boolean
currentVersion: string
channel: string
platform: 'ios' | 'android'
}) {
const isGrayscale = option?.isGrayscale ?? false
const targetVersion = getUpdateVersion(config, isGrayscale)
const compareRes = compareVersion(option.currentVersion, targetVersion)
if (compareRes === VersionCompareResult.PATCH) {
if (option.platform === 'ios') {
if (isContentIncludes(config.marketUpdateChannelWgt, 'ios')) {
return {
updateType: UpdateType.STORE,
url: config.iosAppstore,
}
}
return {
updateType: UpdateType.WGT,
url: isGrayscale ? config.grayscaleWgtUrl : config.wgtUrl,
}
}
const isStore = isContentIncludes(config.marketUpdateChannelWgt, option.channel)
if (!isGrayscale && isStore) {
// 走应用商店APK更新
const channelMarket = getChannelMarket(config, option.channel)
if (channelMarket) {
return {
updateType: UpdateType.STORE,
url: channelMarket,
}
}
}
return {
updateType: UpdateType.WGT,
url: isGrayscale ? config.grayscaleWgtUrl : config.wgtUrl,
}
} else {
if (option.platform === 'ios') {
return {
updateType: UpdateType.STORE,
url: config.iosAppstore,
}
} else if (!isGrayscale) {
const isStore = isContentIncludes(config.marketUpdateChannelApk, option.channel)
if (isStore) {
// 走应用商店APK更新
const channelMarket = getChannelMarket(config, option.channel)
if (channelMarket) {
return {
updateType: UpdateType.STORE,
url: channelMarket,
}
}
}
}
return {
updateType: UpdateType.APK,
url: getChannelApkUrl(config, option.channel, isGrayscale),
}
}
}更新弹框UI
UniApp 常用函数
平台调用方法NJS
/**
* 安卓权限申请 检查相机权限
* @param msgContent
* @returns
*/
export function checkCameraPermission(msgContent) {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
const platform = uni.getSystemInfoSync().platform
if (platform === 'android') {
const main = plus.android.runtimeMainActivity()
const permission = 'android.permission.CAMERA'
// 检查权限
const granted = plus.android.invoke(main, 'checkSelfPermission', permission) === 0
if (granted) {
resolve(true)
return
}
uni.showModal({
content: msgContent || `APP将申请"相机权限"`,
success: (res) => {
if (res.confirm) {
// 请求权限
plus.android.requestPermissions(
[permission],
(res) => {
if (res.granted.length > 0) {
resolve(true)
}
else if (res.deniedAlways && res.deniedAlways.length > 0) {
// 用户拒绝并选择不再询问 → 引导去系统设置
uni.showModal({
title: '提示',
content: '请前往设置打开相机权限',
confirmText: '去设置',
cancelText: '取消',
success: (modalRes) => {
if (modalRes.confirm) {
const Intent = plus.android.importClass('android.content.Intent')
const Settings = plus.android.importClass('android.provider.Settings')
const Uri = plus.android.importClass('android.net.Uri')
const intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
const uri = Uri.parse(`package:${main.getPackageName()}`)
intent.setData(uri)
main.startActivity(intent)
}
resolve(false)
},
})
}
else {
resolve(false)
}
},
(err) => {
console.error('申请摄像头权限失败', err)
resolve(false)
},
)
}
else {
resolve(false)
}
},
})
}
else if (platform === 'ios') {
const AVCaptureDevice = plus.ios.import('AVCaptureDevice')
const status = AVCaptureDevice.authorizationStatusForMediaType('vide')
plus.ios.deleteObject(AVCaptureDevice)
if (status === 3) {
resolve(true)
} else if (status === 0) {
// 系统未授权 → 弹窗
resolve(true)
} else {
// 已拒绝或受限制 → 引导去设置
uni.showModal({
title: '提示',
content: '请前往设置打开相机权限',
confirmText: '去设置',
cancelText: '取消',
success: (modalRes) => {
if (modalRes.confirm) {
plus.runtime.openURL('app-settings://')
}
resolve(false)
},
})
}
}
// #endif
})
}
// 检查相册权限
export function checkAlbumPermission(msgContent) {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
const systemInfo = uni.getSystemInfoSync()
const platform = systemInfo.platform
if (platform === 'android') {
const SDK_INT = systemInfo.osAndroidAPILevel
const main = plus.android.runtimeMainActivity()
let permission = SDK_INT >= 33
? 'android.permission.READ_MEDIA_IMAGES'
: 'android.permission.READ_EXTERNAL_STORAGE'
if (SDK_INT >= 34) {
permission = ['android.permission.READ_MEDIA_VISUAL_USER_SELECTED', 'android.permission.READ_MEDIA_IMAGES']
}
// 检查是否已授权
const granted = plus.android.invoke(main, 'checkSelfPermission', permission) === 0
if (granted) {
resolve(true)
return
}
uni.showModal({
content: msgContent || `APP将申请"存储权限"`,
success: (res) => {
if (res.confirm) {
// 请求权限
plus.android.requestPermissions(
Array.isArray(permission) ? permission : [permission],
(res) => {
if (res.granted.length > 0) {
resolve(true)
}
else if (res.deniedAlways && res.deniedAlways.length > 0) {
// 用户拒绝并“不再询问” → 引导去设置
uni.showModal({
title: '提示',
content: '请前往设置打开相册权限',
confirmText: '去设置',
cancelText: '取消',
success: (modalRes) => {
if (modalRes.confirm) {
const Intent = plus.android.importClass('android.content.Intent')
const Settings = plus.android.importClass('android.provider.Settings')
const Uri = plus.android.importClass('android.net.Uri')
const intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
const uri = Uri.parse(`package:${main.getPackageName()}`)
intent.setData(uri)
main.startActivity(intent)
}
resolve(false)
},
})
}
else {
resolve(false)
}
},
(err) => {
console.error('申请相册权限失败', err)
resolve(false)
},
)
}
else {
resolve(false)
}
},
})
}
else if (platform === 'ios') {
// iOS 自动弹窗请求照片权限
const PHPhotoLibrary = plus.ios.import('PHPhotoLibrary')
const status = PHPhotoLibrary.authorizationStatus()
plus.ios.deleteObject(PHPhotoLibrary)
console.log(status)
if (status === 3) {
// 已授权
resolve(true)
} else if (status === 0) {
// 系统未授权 → 弹窗
resolve(true)
} else {
// 已拒绝或受限制 → 跳转设置
uni.showModal({
title: '提示',
content: '请前往设置打开相册权限',
confirmText: '去设置',
cancelText: '取消',
success: (modalRes) => {
if (modalRes.confirm) {
plus.runtime.openURL('app-settings://')
}
resolve(false)
},
})
}
}
// #endif
})
}
export function readAppMetadataString(field) {
const context = plus.android.runtimeMainActivity()
// 使用 plus.android.invoke 调用 getPackageManager
const packageManager = plus.android.invoke(context, 'getPackageManager')
const packageName = plus.android.invoke(context, 'getPackageName')
// 导入 PackageManager 类以获取常量
const pmPackageManager = plus.android.importClass('android.content.pm.PackageManager')
const GET_META_DATA = pmPackageManager.GET_META_DATA // 值为 128
// 使用 plus.android.invoke 调用 getApplicationInfo
const applicationInfo = plus.android.invoke(packageManager, 'getApplicationInfo', packageName, GET_META_DATA)
const metaData = plus.android.getAttribute(applicationInfo, 'metaData')
const res = plus.android.invoke(metaData, 'getString', field)
return res
}
export const AppChannel = {
IOS: 'ios',
}
export function getAppChannel() {
// #ifdef APP-PLUS
const platform = uni.getSystemInfoSync().platform
if (platform === 'android') {
const channel = readAppMetadataString('channel')
if (channel) {
return channel
}
}
else if (platform === 'ios') {
return 'ios'
}
// #endif
return null
}Android支持多渠道打包
apply plugin: 'com.android.application'
android {
compileSdkVersion 35
buildToolsVersion '35.0.0'
namespace 'xxxx'
defaultConfig {
applicationId "xxxx"
minSdkVersion 21
targetSdkVersion 33
versionCode 3
versionName "1.0.4"
manifestPlaceholders = [
appName: "@string/app_name",
CHANNEL_VALUE: 'guanwang'
]
multiDexEnabled true
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
// abiFilters 'x86', 'armeabi-v7a', 'arm64-v8a'
// 只支持64位架构
// abiFilters 'arm64-v8a'
// abiFilters 'armeabi-v7a'
}
}
signingConfigs {
config {
...
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
signingConfig signingConfigs.config
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
manifestPlaceholders = [
APP_NAME : "@string/app_name_debug",
]
}
release {
signingConfig signingConfigs.config
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
productFlavors {
flavorDimensions = ["channel"]
guanwang {
manifestPlaceholders = [
CHANNEL_VALUE: 'guanwang'
]
}
huawei {
manifestPlaceholders = [
CHANNEL_VALUE: 'huawei'
]
}
oppo {
manifestPlaceholders = [
CHANNEL_VALUE: 'oppo'
]
}
vivo {
manifestPlaceholders = [
CHANNEL_VALUE: 'vivo'
]
}
xiaomi {
manifestPlaceholders = [
CHANNEL_VALUE: 'xiaomi'
]
}
jiuyou {
manifestPlaceholders = [
CHANNEL_VALUE: 'jiuyou'
]
}
tencent {
manifestPlaceholders = [
CHANNEL_VALUE: 'tencent'
]
}
sanliuling {
manifestPlaceholders = [
CHANNEL_VALUE: 'sanliuling'
]
}
rongyao {
manifestPlaceholders = [
CHANNEL_VALUE: 'rongyao'
]
}
baidu {
manifestPlaceholders = [
CHANNEL_VALUE: 'baidu'
]
}
}
// 打包路径配置
applicationVariants.all { variant ->
println("===buildConfig: [${variant.flavorName}_${variant.buildType.name}_${variant.versionName}]")
variant.outputs.all { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
outputFileName = "yxq_${variant.flavorName}_${variant.buildType.name}_${variant.versionName}_${new Date().format("yyyy-MM-dd")}.apk"
println("===newOutputFile: ${outputFileName}")
}
}
// 配置输出目录 - 将APK复制到release目录
variant.assembleProvider.configure { assembleTask ->
assembleTask.doLast {
def releaseDir = new File(projectDir, "release")
if (!releaseDir.exists()) {
releaseDir.mkdirs()
}
variant.outputs.all { output ->
// 获取输出APK文件
def apkFile = output.outputFile
if (apkFile != null && apkFile.exists() && apkFile.name.endsWith('.apk')) {
def targetFile = new File(releaseDir, apkFile.name)
copy {
from apkFile
into releaseDir
rename { fileName ->
fileName
}
}
println("===APK已复制到: ${releaseDir.absolutePath}")
}
}
}
}
}
aaptOptions {
additionalParameters '--auto-add-overlay'
ignoreAssetsPattern "!.svn:!.git:.*:!CVS:!thumbs.db:!picasa.ini:!*.scc:*~"
}
}
// 创建统一的清理任务,在打包前清空输出文件夹
afterEvaluate {
// 创建清理release目录的任务
task cleanReleaseDir {
doLast {
def releaseDir = new File(projectDir, "release")
if (releaseDir.exists()) {
def deletedCount = 0
releaseDir.listFiles().each { file ->
if (file.isFile() && file.name.endsWith('.apk')) {
file.delete()
deletedCount++
println("===已删除旧文件: ${file.name}")
}
}
if (deletedCount > 0) {
println("===已清空release目录,共删除 ${deletedCount} 个APK文件")
} else {
println("===release目录已为空")
}
} else {
releaseDir.mkdirs()
println("===创建release目录")
}
}
}
// 让所有assemble任务依赖清理任务,确保打包前统一清空
tasks.matching { task ->
task.name.startsWith('assemble') && (task.name.contains('Release') || task.name.contains('Debug'))
}.all { task ->
task.dependsOn cleanReleaseDir
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
implementation 'net.lingala.zip4j:zip4j:2.11.5'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.core:core:1.1.0'
implementation "androidx.fragment:fragment:1.1.0"
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.facebook.fresco:fresco:2.5.0'
implementation "com.facebook.fresco:animated-gif:2.5.0"
implementation 'com.github.bumptech.glide:glide:4.9.0'
implementation 'com.alibaba:fastjson:1.2.83'
implementation 'androidx.webkit:webkit:1.5.0'
}