這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 我們知道,在打包Android App之前,我們需要先通過HX生成打包資源。如果是通過cli創建的項目,則通過以下命令生成打包資源: yarn build:app-plus 生成打包資源後的目錄長這樣: 然後將整個目錄中的所有文件拷貝到A ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
我們知道,在打包Android App之前,我們需要先通過HX生成打包資源。如果是通過cli創建的項目,則通過以下命令生成打包資源:
yarn build:app-plus
生成打包資源後的目錄長這樣:
然後將整個目錄中的所有文件拷貝到Android項目的 assets/apps/<appid>/www
中:
可以看出,所有生成的文件,其實只是一個資源目錄。
熱更新的原理就是:替換資源目錄中的所有打包資源
熱更新包分析
我們通過HX生成的熱更新包:
生成的熱更新包長這樣:
可以看出,wgt其實就是一個壓縮文件,將生成的資源文件全部打包。
知道原理後,我們就不一定需要通過HX創建wgt了,我們可以使用yarn build:app-plus
命令先生成打包資源目錄,再將其壓縮為zip包,修改擴展名為wgt即可
註意:wgt包中,必須將manifest,json所在路徑當做根節點進行打包。
打完包後,我們可以將其上傳到OSS。
熱更新方案
熱更新方案:通過增加當前APP資源的版本號(versionCode),跟上一次打包時的APP資源版本號進行對比,如果比之前的資源版本號高,即進行熱更新。
熱更新原理:uniapp的熱更新,其實是將build後的APP資源,打包為一個zip壓縮包(擴展名改為wgt)。
涉及到的版本信息文件:
- src/manifest.json
- app.json (自己創建,用於版本對比)
- platforms/android/app/build.gradle
註意事項:
保證以上文件的versionName
和versionCode
均保持一致。
熱更新核心代碼
以下為熱更新的核心代碼:
// #ifdef APP-PLUS let downloadPath = "https://xxx.cn/apk/app.wgt" uni.downloadFile({ url: downloadPath, success: (downloadResult) => { if (downloadResult.statusCode === 200) { plus.runtime.install(downloadResult.tempFilePath, { force: true // 強制更新 }, function() { console.log('install success...'); plus.runtime.restart(); }, function(e) { console.error(e); console.error('install fail...'); }); } } }) // #endif
這裡是下載wgt包,併進行安裝的代碼。以上代碼無論如何都會下載wgt進行安裝。
更新介面
實際上,在這之前,我們還需要判斷是否需要更新,這就涉及到介面的部分。在此,只講講思路:
- 獲取安裝的版本名、版本號等信息,將其當做參數調用對應的更新介面;
- 介面取到這些信息,與最新版本進行對比,如果版本已經更新,返回需要更新的信息;
- 介面可以自行約定,怎麼方便這麼來。
我自己做的話,根本沒寫什麼介面,只是創建了一個app.json
文件,用於存放最新版本信息:
{ "versionCode": "100", "versionName": "1.0.0" }
將其上傳到OSS,然後在下載wgt包之前進行版本檢查即可:
// #ifdef APP-PLUS plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) { console.log(widgetInfo); uni.request({ url: 'https://xxx.cn/apk/app.json', success: (result) => { let { versionCode, versionName } = result.data console.log({ versionCode, versionName }); // 判斷版本名是否一致 if (versionName === widgetInfo.version) { // 如果安裝的版本號小於最新發佈的版本號,則進行更新 if (parseInt(widgetInfo.versionCode) < parseInt(versionCode)) { // 下載wgt更新包 let downloadPath = "https://xxx.cn/apk/app.wgt" uni.downloadFile({ url: downloadPath, success: (downloadResult) => { if (downloadResult.statusCode === 200) { plus.runtime.install(downloadResult.tempFilePath, { force: true // 強制更新 }, function() { console.log('熱更新成功'); plus.runtime.restart(); }, function(e) { console.error('熱更新失敗,錯誤原因:' + e); }); } } }) } else { console.log('你的版本為最新,不需要熱更新'); } } else { console.log('版本名不一致,請使用整包更新'); } } }); }); // #endif
OK,至此,熱更新就完成了。
Android整包更新
看到上面更新邏輯,如果版本名不一致,則需要下載最新的apk進行安裝,在下載之前,建議給用戶一個更新提示:
console.log('版本名不一致,請使用整包更新'); let url = "https://xxx.cn/apk/app.apk" uni.showModal({ //提醒用戶更新 title: "更新提示", content: "有新的更新可用,請升級", success: (res) => { if (res.confirm) { plus.runtime.openURL(url); } } })
以上代碼是官方提供的,其實也可以下載apk成功後,直接調用install
進行安裝:
console.log('版本名不一致,請使用整包更新'); let downloadPath = "https://zys201811.boringkiller.cn/shianonline/apk/app.apk" uni.showModal({ //提醒用戶更新 title: "更新提示", content: "有新的更新可用,請升級", success: (res) => { if (res.confirm) { // plus.runtime.openURL(downloadPath); uni.downloadFile({ url: downloadPath, success: (downloadResult) => { if (downloadResult.statusCode === 200) { console.log('正在更新...'); plus.runtime.install(downloadResult.tempFilePath, { force: true // 強制更新 }, function() { console.log('整包更新成功'); plus.runtime.restart(); }, function(e) { console.error('整包更新失敗,錯誤原因:' + e); }); } } }) } } })
熱更新的自動化處理
知道原理後,就好辦了,我們可以將其繁雜的工作自動化,以減少重覆勞動。
修改package.json
的相關打包腳本:
{ "name": "shianaonline", "version": "0.1.224", "private": true, "scripts": { "apk": "node deploy/scripts/build-apk.js", "wgt": "node deploy/scripts/build-wgt.js", "build:app-plus-android": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus UNI_OUTPUT_DIR=./platforms/android/app/src/main/assets/apps/your appid/www vue-cli-service uni-build", "build:app-plus-ios": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus UNI_OUTPUT_DIR=./platforms/iOS/apps/your appid/www vue-cli-service uni-build", } }
其中,需要替換的地方是your appid
,換為自己的uniapp appid
創建app.json
,用於存儲當前app的版本信息:
{ "versionName": "1.0.27", "versionCode": 336, "appPath": "https://xxx.oss.com/apk/app-release.apk", "wgtPath": "https://xxx.oss.com/apk/www.wgt" }
創建自動化打包腳本build-wgt.js
:
const fs = require('fs') const { execSync } = require('child_process') const join = require('path').join // 修改版本號 let app = require('../../app.json') let manifest = require('../../src/manifest.json') if (app.versionName !== manifest.versionName) { console.info('manifest.json和app.json的versionName不一致,請檢查') return } if (app.versionCode !== manifest.versionCode) { console.info('manifest.json和app.json的versionCode不一致,請檢查') return } // 獲取build.gradle的版本名 let gradleFilePath = '../../platforms/android/app/build.gradle' let data = fs.readFileSync(__dirname + '/' + gradleFilePath, { encoding: 'utf-8' }) let reg = new RegExp(`versionCode ${app.versionCode}`, "gm") if (!reg.test(data)) { console.log('platforms/android/app/build.gradle的versionCode不一致,請檢查') return } app.versionCode += 1 manifest.versionCode += 1 console.log('===================='); console.log('newVersion:' + app.versionName + "." + app.versionCode); console.log('===================='); let appJSON = JSON.stringify(app, null, 2) let manifestJSON = JSON.stringify(manifest, null, 2) let replaceFiles = [{ path: '../../app.json', name: 'app.json', content: appJSON }, { path: '../../src/manifest.json', name: 'manifest.json', content: manifestJSON }] replaceFiles.forEach(file => { fs.writeFileSync(__dirname + '/' + file.path, file.content, { encoding: 'utf-8' }) console.log(file.name + ': 替換成功'); }) // 替換build.gradle的版本名 let result = data.replace(reg, `versionCode ${app.versionCode}`) fs.writeFileSync(__dirname + '/' + gradleFilePath, result, { encoding: 'utf-8' }) console.log('platforms/android/build.gradle: 替換成功') console.log('===================='); // 編譯 console.log(execSync('yarn build:app-plus-android', { encoding: 'utf-8'})) // 打包 const compressing = require('compressing'); const tarStream = new compressing.zip.Stream(); const targetPath = './platforms/android/app/src/main/assets/apps/your appid/www' const targetFile = './www.wgt' let paths = fs.readdirSync(targetPath); paths.forEach(function (item) { let fPath = join(targetPath, item); tarStream.addEntry(fPath); }); tarStream .pipe(fs.createWriteStream(targetFile)) .on('finish', upToOss) // 上傳至OSS let OSS = require('ali-oss'); function upToOss() { let client = new OSS({ region: 'oss-cn-shenzhen', accessKeyId: 'your accessKeyId', accessKeySecret: 'your accessKeySecret' }); client.useBucket('your bucketName'); let ossBasePath = `apk` put(`${ossBasePath}/www.wgt`, 'www.wgt') put(`${ossBasePath}/wgts/${app.versionCode}/www.wgt`, 'www.wgt') put(`webview/vod.html`, 'src/hybrid/html/vod.html') put(`${ossBasePath}/app.json`, 'app.json') async function put (ossPath, localFile) { try { await client.put(ossPath, localFile); console.log(`${localFile}上傳成功:${ossPath}`); } catch (err) { console.log(err); } } } console.log('===================='); console.log('更新完畢,newVersion:' + app.versionName + "." + app.versionCode); console.log('====================');
以上打包腳本,做了以下工作:
- 驗證版本號和版本名是否正確,如果不正確,終止腳本
- 修改當前APP版本號
- 生成APP打包資源
- 將打包資源做成zip包(擴展名改為wgt)
- 上傳wgt資源包到OSS
一鍵式操作,打包為wgt只需要執行:
yarn wgt
Android整包更新的自動化處理
Android整包更新需要在AndroidManifest.xml
中配置:
<uses-permission android:name="android.permission.INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
Android整包更新的業務代碼跟熱更新一樣,都可以調用plus.runtime.install
來實現。
主要還是說一下打包apk的自動化腳本build-apk.js
const fs = require('fs') const { execSync } = require('child_process') let app = require('../../app.json') let manifest = require('../../src/manifest.json') if (app.versionName !== manifest.versionName) { console.log('manifest.json和app.json的versionName不一致,請檢查') return } if (app.versionCode !== manifest.versionCode) { console.log('manifest.json和app.json的versionCode不一致,請檢查') return } // 獲取build.gradle的版本名 let gradleFilePath = '../../platforms/android/app/build.gradle' let data = fs.readFileSync(__dirname + '/' + gradleFilePath, { encoding: 'utf-8' }) let reg = new RegExp(`versionName "${app.versionName}"`, "gm") if (!reg.test(data)) { console.info('platforms/android/app/build.gradle的versionName不一致,請檢查') return } let regCode = new RegExp(`versionCode ${app.versionCode}`, "gm") if (!regCode.test(data)) { console.info('platforms/android/app/build.gradle的versionCode不一致,請檢查') return } // 修改版本名 let appVersionName = app.versionName.split('.') let manifestVersionName = manifest.versionName.split('.') let appVersionLast = Number(appVersionName[2]) let manifestVersionLast = Number(manifestVersionName[2]) appVersionLast += 1 manifestVersionLast += 1 app.versionName = appVersionName[0] + '.' + appVersionName[1] + '.' + appVersionLast manifest.versionName = manifestVersionName[0] + '.' + manifestVersionName[1] + '.' + manifestVersionLast console.log('===================='); console.log('newVersion:' + app.versionName + "." + app.versionCode); console.log('===================='); let appJSON = JSON.stringify(app, null, 2) let manifestJSON = JSON.stringify(manifest, null, 2) // 替換項目版本名 let replaceFiles = [{ path: '../../app.json', name: 'app.json', content: appJSON }, { path: '../../src/manifest.json', name: 'manifest.json', content: manifestJSON }] replaceFiles.forEach(file => { fs.writeFileSync(__dirname + '/' + file.path, file.content, { encoding: 'utf-8' }) console.log(file.name + ': 替換成功'); }) // 替換build.gradle的版本名 let result = data.replace(reg, `versionName "${app.versionName}"`) fs.writeFileSync(__dirname + '/' + gradleFilePath, result, { encoding: 'utf-8' }) console.log('platforms/android/build.gradle: 替換成功') console.log('===================='); // 打包資源 console.log(execSync(`yarn build:app-plus-android`, { encoding: 'utf-8'})) // 打包apk console.log(execSync(`cd platforms/android && gradle assembleRelease`, { encoding: 'utf-8'})) // 上傳至OSS let OSS = require('ali-oss'); function upToOss() { let client = new OSS({ region: 'oss-cn-shenzhen', accessKeyId: 'your accessKeyId', accessKeySecret: 'your accessKeySecret' }); client.useBucket('your bucketName'); let ossBasePath = `apk` put(`${ossBasePath}/app-release.apk`, 'platforms/android/app/build/outputs/apk/release/app-release.apk') put(`${ossBasePath}/apks/${app.versionName}/app-release.apk`, 'platforms/android/app/build/outputs/apk/release/app-release.apk') put(`${ossBasePath}/apks/${app.versionName}/output.json`, 'platforms/android/app/build/outputs/apk/release/output.json') put(`webview/vod.html`, 'src/hybrid/html/vod.html') put(`${ossBasePath}/app.json`, 'app.json') async function put (ossPath, localFile) { try { await client.put(ossPath, localFile); console.log(`${localFile}上傳成功:${ossPath}`); } catch (err) { console.log(err); } } } upToOss() console.log('===================='); console.log('更新完畢,newVersion:' + app.versionName + "." + app.versionCode); console.log('====================');
以上打包腳本,做了以下工作:
- 驗證版本號和版本名是否正確,如果不正確,終止腳本
- 修改當前APP版本名
- 生成APP打包資源
- 打包Android APP(擴展名apk)
- 上傳apk到OSS
一鍵式操作,打包為apk只需要執行:
yarn apk
安裝更新
我們看看plus.runtime.install
的官方文檔:
void plus.runtime.install(filePath, options, installSuccessCB, installErrorCB);
支持以下類型安裝包:
- 應用資源安裝包(wgt),擴展名為'.wgt';
- 應用資源差量升級包(wgtu),擴展名為'.wgtu';
- 系統程式安裝包(apk),要求使用當前平臺支持的安裝包格式。 註意:僅支持本地地址,調用此方法前需把安裝包從網路地址或其他位置放置到運行時環境可以訪問的本地目錄。
知道了調用方式就好辦了,我們封裝一個檢測更新的方法:
class Utils { ... // 獲取APP版本信息 getVersion() { let {versionName, versionCode} = manifest return { versionName, versionCode, version: `${versionName}.${versionCode}` } } // 檢測更新 detectionUpdate(needRestartHotTip = false, needRestartFullTip = false) { return new Promise(async (resolve, reject) => { let appInfo = this.getVersion() uni.request({ url: 'https://xxx.oss.com/apk/app.json', success: async (result) => { let { versionCode, versionName, appPath, wgtPath } = result.data let versionInfo = { appPath, wgtPath, newestVersion: `${versionName}.${versionCode}`, newestVersionCode: versionCode, newestVersionName: versionName, currentVersion: appInfo.version, currentVersionCode: appInfo.versionCode, currentVersionName: appInfo.versionName } // 判斷版本名是否一致 try { if (versionName === appInfo.versionName) { // 如果安裝的版本號小於最新發佈的版本號,則進行更新 if (appInfo.versionCode < versionCode) { // 下載wgt更新包 if (needRestartHotTip) { uni.showModal({ title: '提示', content: `檢測到新版本 ${versionInfo.newestVersion} (當前版本:${versionInfo.currentVersion}),是否立即更新並重啟應用,以使更新生效?`, success: async (res) => { if (res.confirm) { await this.downloadAndInstallPackage(wgtPath) plus.runtime.restart(); resolve({code: 1, data: versionInfo}) } else if (res.cancel) { await this.downloadAndInstallPackage(wgtPath) resolve({code: 1, data: versionInfo}) } } }) } else { await this.downloadAndInstallPackage(wgtPath) resolve({code: 1, data: versionInfo}) } } else { resolve({code: 0, data: versionInfo}) console.log('你的版本為最新,不需要熱更新'); } } else { // 整包更新 console.log('版本名不一致,請使用整包更新'); if (needRestartFullTip) { uni.showModal({ title: '提示', content: `檢測到新版本 ${versionInfo.newestVersion} (當前版本:${versionInfo.currentVersion}),是否立即更新應用?`, success: async (res) => { if (res.confirm) { // await this.downloadAndInstallPackage(appPath) plus.runtime.openURL(appPath) resolve({code: 2, data: versionInfo}) } else if (res.cancel) {} } }) } else { // await this.downloadAndInstallPackage(appPath) plus.runtime.openURL(appPath) resolve({code: 2, data: versionInfo}) } } } catch (e) { reject(e) } } }); }) } // 下載並安裝更新包 downloadAndInstallPackage(url) { console.log('開始下載更新包:' + url) return new Promise((resolve, reject) => { uni.downloadFile({ url: url, success: (downloadResult) => { if (downloadResult.statusCode === 200) { console.log('正在更新...'); plus.runtime.install(downloadResult.tempFilePath, { force: true // 強制更新 }, function() { console.log('更新成功'); resolve() }, function(e) { console.error('更新失敗,錯誤原因:' + JSON.stringify(e)); reject(e) }); } } }) }) } } ...
創建Utils的實例,並掛載到Vue的原型中,調用起來非常方便:
... let res = await this.$utils.detectionUpdate(false, true) if (res.code === 1) { uni.showModal({ title: '提示', content: `發現新的熱更新包,是否立即重啟APP以使更新生效?`, success: async (res) => { if (res.confirm) { plus.runtime.restart() } else if (res.cancel) {} } }) }
... let res = await this.$utils.detectionUpdate(true, true) if (res.code === 0) { let {currentVersion} = res.data uni.showModal({ title: '提示', content: `你的APP為最新版本 ${currentVersion},不需要更新!`, showCancel: false, success: async (res) => { if (res.confirm) { } else if (res.cancel) {} } }) }
實戰案例代碼及過程
思路
伺服器中存儲著最新版本號,前端進行查詢 可以在首次進入應用時進行請求版本號進行一個匹對 如果版本號一致則不提示,反之則提示進行更新執行更新操作
1.封裝一個對比版本號的函數
/** * 對比版本號,如需要,請自行修改判斷規則 * 支持比對 ("3.0.0.0.0.1.0.1", "3.0.0.0.0.1") ("3.0.0.1", "3.0") ("3.1.1", "3.1.1.1") 之類的 * @param {Object} v1 * @param {Object} v2 * v1 > v2 return 1 * v1 < v2 return -1 * v1 == v2 return 0 */ function compare(v1 = '0', v2 = '0') { v1 = String(v1).split('.') v2 = String(v2).split('.') const minVersionLens = Math.min(v1.length, v2.length); let result = 0; for (let i = 0; i < minVersionLens; i++) { const curV1 = Number(v1[i]) const curV2 = Number(v2[i]) if (curV1 > curV2) { result = 1 break; } else if (curV1 < curV2) { result = -1 break; } } if (result === 0 && (v1.length !== v2.length)) { const v1BiggerThenv2 = v1.length > v2.length; const maxLensVersion = v1BiggerThenv2 ? v1 : v2; for (let i = minVersionLens; i < maxLensVersion.length; i++) { const curVersion = Number(maxLensVersion[i]) if (curVersion > 0) { v1BiggerThenv2 ? result = 1 : result = -1 break; } } } return result; }
2.封裝更新函數
var updateUseModal = (packageInfo) => { const { title, // 標題 contents, // 升級內容 is_mandatory, // 是否強制更新 url, // 安裝包下載地址 platform, // 安裝包平臺 type // 安裝包類型 } = packageInfo; let isWGT = type === 'wgt' let isiOS = !isWGT ? platform.includes('iOS') : false; let confirmText = isiOS ? '立即跳轉更新' : '立即下載更新' return uni.showModal({ title, content: contents, showCancel: !is_mandatory, confirmText, success: res => { if (res.cancel) return; // 安裝包下載 if (isiOS) { plus.runtime.openURL(url); return; } let waiting = plus.nativeUI.showWaiting("正在下載 - 0%"); // uni.showLoading({ // title: '安裝包下載中' // }); // wgt 和 安卓下載更新 const downloadTask = uni.downloadFile({ url, success: res => { if (res.statusCode !== 200) { console.error('下載安裝包失敗', err); return; } // 下載好直接安裝,下次啟動生效 plus.runtime.install(res.tempFilePath, { force: false }, () => { uni.hideLoading() if (is_mandatory) { //更新完重啟app plus.runtime.restart(); return; } uni.showModal({ title: '安裝成功是否重啟?', success: res => { if (res.confirm) { //更新完重啟app plus.runtime.restart(); } } }); }, err => { uni.hideLoading() uni.showModal({ title: '更新失敗', content: err.message, showCancel: false }); }); }, //介面調用結束 complete: ()=>{ uni.hideLoading(); downloadTask.offProgressUpdate();//取消監聽載入進度 } }); //監聽下載進度 downloadTask.onProgressUpdate(res => { // state.percent = res.progress; waiting.setTitle("正在下載 - "+res.progress+"%"); // console.log('下載進度百分比:' + res.progress); // 下載進度百分比 // console.log('已經下載的數據長度:' + res.totalBytesWritten); // 已經下載的數據長度,單位 Bytes // console.log('預期需要下載的數據總長度:' + res.totalBytesExpectedToWrite); // 預期需要下載的數據總長度,單位 Bytes }); } }); }
3.用變數接收實現函數(在函數中使用上方封裝的函數)並導出
fRequestWithToken為我封裝的請求方法,可自行進行使用axios進行請求也行!!!
var fCheckVersion = (cb) => { // #ifdef APP-PLUS plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) { // console.log(widgetInfo.version) // console.log(plus.runtime.version) // console.log(widgetInfo.version) var nVerSta = compare(plus.runtime.version, widgetInfo.version), sLaststVer = plus.runtime.version; if (widgetInfo.version) { if (nVerSta == 1) { console.log(plus.runtime.version) sLaststVer = plus.runtime.version } else if (nVerSta == -1) { console.log(widgetInfo.version) sLaststVer = widgetInfo.version } } console.log(sLaststVer) //發送請求進行匹對,我這裡資料庫設定的是如果返回null則版本號一致,反之需要更新!!! fRequestWithToken({ ajaxOpts: { url: URLS_COM.d_lastVer, data: { versionCode: sLaststVer } }, showloading: false, silence:true }).then(data => { console.log(data) // console.log('################') if (data) { var sUrl = '', type = ''; if (data.wgtName) { sUrl = data.wgtName; type = "wgt" } else { sUrl = data.pkgName; type = "pkg"; } updateUseModal({ title: data.title||"", contents: data.note||'', is_mandatory: true, url: sUrl, platform: 'android', type: type // 安裝包類型 }) } }).catch((res)=>{ cb&&cb() console.log(res) }) }) // #endif } export { fCheckVersion }
使用
可在App.vue中進行使用,根據項目需求而定
1.引入封裝好的函數
路徑自己記得填寫自己封裝的位置
import{fCheckVersion} from '@/common/project/checkversion.js'
2.然後可以在onLoad函數中進行觸發
onLoad() { fCheckVersion();//檢查更新 }
這樣就實現了熱更新
然後的話只需要進行打包個熱更新的包
後端進行上傳至伺服器進行更新數據
本地再進行一個雲打包,記得在mainifest.json文件中進行版本號的修改,修改成低於熱更新包的版本號即可