有些时候我们的 App 上线了令人振奋的新功能,需要第一时间让用户知道并推荐用户更新。同样基于 React Native 开发的网易云音乐就提供了这个功能。
基于对传统更新机制的认知,首先需要搭建一个更新服务器,然后提供对应的 API 让 App 能过检测新版本并提示用户更新。由于 App Store 存在着审核机制,这就要求再开发此类功能的时候必须及时关注 App Store 的审核发布结果然后同步更新服务器上边的版本信息。
如果有一种方法既利用现有设施而不用搭建更新服务器,还可以只使用 JavaScript
代码而不接触原生代码就能完成功能就好了。
那么接下来就来解决两个问题:更新检测和跳转到 App Store 页面。
更新检测
对于查询 App 的信息, Apple 提供了 iTunes Search API 可供使用,虽然上边是以专辑示例,但是对于 App 同样适用。这是个公共接口,无需鉴权,只需要知道应用的 Apple ID 就可以查询。
获取 App 的 Apple ID
那么可以知道 Apple ID 呢,最直接的方式就是登陆 App Store Connect 通过后台查看。
另外一种方式就是进入到应用的 App Store 页面,通过地址栏进行获取。
图中,地址栏 id 后的数字即为该应用的 Apple ID。
获取 App 详细信息
有了应用的 Apple ID 之后,就可以构建请求获取应用的信息了。
curl --request GET --url 'https://itunes.apple.com/cn/lookup?id=590338362'
{
"resultCount": 1,
"results": [
{
"screenshotUrls": [
"https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/6d/f3/99/6df3999e-d49d-8e4e-1051-29e86d17c340/d0a79a7d-9322-4045-aa39-dec4225b1be7_iPhone_1242_U002a2208.jpg/392x696bb.jpg",
"https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/65/88/9a/65889a9f-60c1-adb3-8f8e-ef84ee7d55e6/e9fc367d-573b-4320-ac73-3ed9f41bf8cd_iPhone__U5168_U5c4f_U64ad_U653e_U56681242_U002a2208.jpg/392x696bb.jpg",
"https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/0e/68/45/0e68451f-ba09-f713-1fd9-d7c9df09e28d/c2dd0589-d5e0-45c4-80fc-baa54652f3f0_2-_U63a8_U8350-5.jpg/392x696bb.jpg",
"https://is1-ssl.mzstatic.com/image/thumb/PurpleSource221/v4/d4/87/a8/d487a892-003b-05d4-7627-8bc5a5d1be63/ec6f7932-8f54-4ec7-ab0a-4bbe505ede33_3-_U79c1_U4ebaDJ-2.jpg/392x696bb.jpg",
"https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/61/72/06/61720634-a10a-2b58-7605-fd0ef53fa592/4413f9eb-3297-436c-8d41-c3e878317173_5-_U65b0_U7248_U6211_U7684_U9875-3.jpg/392x696bb.jpg",
"https://is1-ssl.mzstatic.com/image/thumb/PurpleSource221/v4/9b/b4/c3/9bb4c3c2-c2db-26df-eaf3-92b1f6cac9ad/c47b2d3d-1c02-42d5-a121-d24d432c0993_6-_U63a8_U8350-6.jpg/392x696bb.jpg",
"https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/aa/dd/6e/aadd6e0a-1026-8c7e-174f-26f1351ba83c/9fab44ec-6db6-4a9e-b135-57c45042c4ad_7-_U4f1a_U5458-_U4e50_U8bc4-2.jpg/392x696bb.jpg"
],
"ipadScreenshotUrls": [],
"appletvScreenshotUrls": [],
"artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/97/58/ba/9758bac8-e275-f295-de5c-48a79b6c6fa7/AppIcon-1x_U007emarketing-0-7-0-0-85-220-0.png/512x512bb.jpg",
"supportedDevices": [
"iPhone5s-iPhone5s",
"iPadAir-iPadAir",
"iPadAirCellular-iPadAirCellular",
"iPadMiniRetina-iPadMiniRetina",
"iPadMiniRetinaCellular-iPadMiniRetinaCellular",
"iPhone6-iPhone6",
"iPhone6Plus-iPhone6Plus",
"iPadAir2-iPadAir2",
"iPadAir2Cellular-iPadAir2Cellular",
"iPadMini3-iPadMini3",
"iPadMini3Cellular-iPadMini3Cellular",
"iPodTouchSixthGen-iPodTouchSixthGen",
"iPhone6s-iPhone6s",
"iPhone6sPlus-iPhone6sPlus",
"iPadMini4-iPadMini4",
"iPadMini4Cellular-iPadMini4Cellular",
"iPadPro-iPadPro",
"iPadProCellular-iPadProCellular",
"iPadPro97-iPadPro97",
"iPadPro97Cellular-iPadPro97Cellular",
"iPhoneSE-iPhoneSE",
"iPhone7-iPhone7",
"iPhone7Plus-iPhone7Plus",
"iPad611-iPad611",
"iPad612-iPad612",
"iPad71-iPad71",
"iPad72-iPad72",
"iPad73-iPad73",
"iPad74-iPad74",
"iPhone8-iPhone8",
"iPhone8Plus-iPhone8Plus",
"iPhoneX-iPhoneX",
"iPad75-iPad75",
"iPad76-iPad76",
"iPhoneXS-iPhoneXS",
"iPhoneXSMax-iPhoneXSMax",
"iPhoneXR-iPhoneXR",
"iPad812-iPad812",
"iPad834-iPad834",
"iPad856-iPad856",
"iPad878-iPad878",
"Watch4-Watch4",
"iPadMini5-iPadMini5",
"iPadMini5Cellular-iPadMini5Cellular",
"iPadAir3-iPadAir3",
"iPadAir3Cellular-iPadAir3Cellular",
"iPodTouchSeventhGen-iPodTouchSeventhGen",
"iPhone11-iPhone11",
"iPhone11Pro-iPhone11Pro",
"iPadSeventhGen-iPadSeventhGen",
"iPadSeventhGenCellular-iPadSeventhGenCellular",
"iPhone11ProMax-iPhone11ProMax",
"iPhoneSESecondGen-iPhoneSESecondGen",
"iPadProSecondGen-iPadProSecondGen",
"iPadProSecondGenCellular-iPadProSecondGenCellular",
"iPadProFourthGen-iPadProFourthGen",
"iPadProFourthGenCellular-iPadProFourthGenCellular",
"iPhone12Mini-iPhone12Mini",
"iPhone12-iPhone12",
"iPhone12Pro-iPhone12Pro",
"iPhone12ProMax-iPhone12ProMax",
"iPadAir4-iPadAir4",
"iPadAir4Cellular-iPadAir4Cellular",
"iPadEighthGen-iPadEighthGen",
"iPadEighthGenCellular-iPadEighthGenCellular",
"iPadProThirdGen-iPadProThirdGen",
"iPadProThirdGenCellular-iPadProThirdGenCellular",
"iPadProFifthGen-iPadProFifthGen",
"iPadProFifthGenCellular-iPadProFifthGenCellular",
"iPhone13Pro-iPhone13Pro",
"iPhone13ProMax-iPhone13ProMax",
"iPhone13Mini-iPhone13Mini",
"iPhone13-iPhone13",
"iPadMiniSixthGen-iPadMiniSixthGen",
"iPadMiniSixthGenCellular-iPadMiniSixthGenCellular",
"iPadNinthGen-iPadNinthGen",
"iPadNinthGenCellular-iPadNinthGenCellular",
"iPhoneSEThirdGen-iPhoneSEThirdGen",
"iPadAirFifthGen-iPadAirFifthGen",
"iPadAirFifthGenCellular-iPadAirFifthGenCellular",
"iPhone14-iPhone14",
"iPhone14Plus-iPhone14Plus",
"iPhone14Pro-iPhone14Pro",
"iPhone14ProMax-iPhone14ProMax",
"iPadTenthGen-iPadTenthGen",
"iPadTenthGenCellular-iPadTenthGenCellular",
"iPadPro11FourthGen-iPadPro11FourthGen",
"iPadPro11FourthGenCellular-iPadPro11FourthGenCellular",
"iPadProSixthGen-iPadProSixthGen",
"iPadProSixthGenCellular-iPadProSixthGenCellular",
"iPhone15-iPhone15",
"iPhone15Plus-iPhone15Plus",
"iPhone15Pro-iPhone15Pro",
"iPhone15ProMax-iPhone15ProMax",
"iPadAir11M2-iPadAir11M2",
"iPadAir11M2Cellular-iPadAir11M2Cellular",
"iPadAir13M2-iPadAir13M2",
"iPadAir13M2Cellular-iPadAir13M2Cellular",
"iPadPro11M4-iPadPro11M4",
"iPadPro11M4Cellular-iPadPro11M4Cellular",
"iPadPro13M4-iPadPro13M4",
"iPadPro13M4Cellular-iPadPro13M4Cellular",
"iPhone16-iPhone16",
"iPhone16Plus-iPhone16Plus",
"iPhone16Pro-iPhone16Pro",
"iPhone16ProMax-iPhone16ProMax",
"iPadMiniA17Pro-iPadMiniA17Pro",
"iPadMiniA17ProCellular-iPadMiniA17ProCellular"
],
"features": [],
"advisories": [
"频繁/强烈的色情内容或裸露"
],
"isGameCenterEnabled": false,
"kind": "software",
"artistViewUrl": "https://apps.apple.com/cn/developer/%E6%9D%AD%E5%B7%9E%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%E7%A7%91%E6%8A%80%E6%9C%89%E9%99%90%E5%85%AC%E5%8F%B8/id1202760281?uo=4",
"artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/97/58/ba/9758bac8-e275-f295-de5c-48a79b6c6fa7/AppIcon-1x_U007emarketing-0-7-0-0-85-220-0.png/60x60bb.jpg",
"artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/97/58/ba/9758bac8-e275-f295-de5c-48a79b6c6fa7/AppIcon-1x_U007emarketing-0-7-0-0-85-220-0.png/100x100bb.jpg",
"languageCodesISO2A": [
"EN",
"ZH"
],
"fileSizeBytes": "522429440",
"formattedPrice": "免费",
"userRatingCountForCurrentVersion": 1507490,
"trackContentRating": "17+",
"averageUserRatingForCurrentVersion": 4.55995,
"artistId": 1202760281,
"artistName": "杭州网易云音乐科技有限公司",
"genres": [
"音乐",
"社交"
],
"price": 0,
"sellerName": "Hangzhou Netease Cloud Music Technology Co., Ltd.",
"trackId": 590338362,
"trackName": "网易云音乐-听有歌2024原创好歌",
"bundleId": "com.netease.cloudmusic",
"releaseDate": "2013-01-24T12:22:25Z",
"primaryGenreName": "Music",
"primaryGenreId": 6011,
"isVppDeviceBasedLicensingEnabled": true,
"currentVersionReleaseDate": "2024-11-13T02:42:03Z",
"releaseNotes": "修复已知问题,优化产品体验\n\n==近期更新==\n【音量均衡优化】\n优化音量均衡体验\n\n【播放器模式上线】\n复古磁带、CD播放器样式带你感受记忆中的滋味\n\n使用中遇到任何问题,请通过左侧边栏-\"我的客服\"进行反馈",
"version": "9.2.0",
"wrapperType": "software",
"currency": "CNY",
"description": "产品介绍\n看了就停不下来,全网超级热闹的歌曲评论区。真实情感故事、连载小说、硬核科普、趣味段子、文学金句、流行玩梗,你想得到想不到的乐评都在这了。\n网易云音乐是当下十分受年轻用户喜爱的音乐平台之一。在网易云音乐,不仅可以听到海量正版音乐,还能遇见和你同样热爱音乐的伙伴。用音乐连接彼此,用音乐传递美好力量。\n\n【海量曲库】\n收录华语/欧美/日韩等众多语种歌曲,涵盖电音/说唱/摇滚/ACG/古风/古典等超全音乐种类。无论是热门歌手,还是小众音乐人,你都可以在这里找到。\n\n【个性推荐】\n超准智能算法比你更懂你,根据你的日常听歌喜好,将自动推荐给你感兴趣的歌曲。\n\n【有声剧场】\n快速充电的人生书库、精彩纷呈的推理悬疑故事、脑洞搞笑的热门小说、甜到停不下来的热门广播剧,超级还原的游戏原著,好听的小说都在这。\n\n【精品播客】\n音乐推荐、热门翻唱、情感树洞、真实故事……你想听的这里都有;解压、充电、助眠……满足你的所有需求;超千万声音库,给你7*24小时的耳边陪伴。\n\n【品质歌单】\n超34亿歌单库,邀你一起定制跑步、学习、工作、聚会等全场景音乐歌单。\n\n【精彩乐评】\n看了就停不下来,全网热闹歌曲评论区。真实情感故事、连载小说、硬核科普、趣味段子、文学金句、流行玩梗,你想得到想不到的乐评都在这了。\n\n【趣味社区】\n在云村,你不仅可以听音乐,还可以看音乐、聊音乐、玩音乐。音乐视频/一起听/动态/歌房等众多创新功能全方位满足你的使用体验。\n\n---------------------------\n【连续包月会员说明】\n1.连续包月音乐包说明\n--订阅周期:1个月\n--订阅价格:每月8元\n\n2.连续包月黑胶VIP说明\n--订阅周期:1个月\n--订阅价格:每月15元\n\n3.连续包月黑胶SVIP说明\n--订阅周期:1个月\n--订阅价格:每月28元\n\n3.其他规则\n--付款:通过用户的iTunes账户扣款,用户确认购买后即付款\n--续订:苹果iTunes账户会在到期前24小时内扣费,扣费成功后订阅周期顺延1个订阅周期\n--取消续订:如需取消续订,请在当前扣费期前至少24小时操作,操作方法:从Home页进入[设置]—>点击[iTunes Store与App Store]—>点击[Apple ID],选择[查看Apple ID]—>[账户设置]—>点击[订阅],选择黑胶VIP连续包月或音乐包连续包月取消订阅即可。\n--自动续费服务协议:https://y.music.163.com/m/at/vipAutopay?fromRN=1\n--付费会员服务协议:https://y.music.163.com/m/at/prime-membership-contract?fromRN=1\n--用户服务条款(含用户隐私协议):https://music.163.com/html/web2/service.html\n\n---------------------------\n\n使用过程中,如搜索不到歌曲,请在APP内私信云音乐曲库;\n如遇其他问题,请私信云音乐客服\n\n也可通过以下方式,与我们联系:\n\n官方网站:https://music.163.com\n官方微博:http://weibo.com/163music\n官方公众号:网易云音乐(yunyinyue163)",
"trackCensoredName": "网易云音乐-听有歌2024原创好歌",
"trackViewUrl": "https://apps.apple.com/cn/app/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90-%E5%90%AC%E6%9C%89%E6%AD%8C2024%E5%8E%9F%E5%88%9B%E5%A5%BD%E6%AD%8C/id590338362?uo=4",
"contentAdvisoryRating": "17+",
"averageUserRating": 4.55995,
"minimumOsVersion": "12.4",
"genreIds": [
"6011",
"6005"
],
"userRatingCount": 1507490
}
]
}
API 返回的内容为 JSON
格式,其中 version
字段是 App Store 中应用最新的版本,正是我们所需要的。
App 版本管理
有了 App Store 的 version
之后,还需要获取应用自身的版本号,才能进行版本的比对。
这里有两种方法获取到版本号,第一个是使用诸如 react-native-device-info 之类的解决方案,另外一种就是讲版本号放入 package.json
中多平台统一维护。
在掌握了前置知识之后就可以进行实现了。
import { Platform } from 'react-native';
import { defer, from, iif, lastValueFrom, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { useQuery } from '@tanstack/react-query';
import type { Observable } from 'rxjs';
import type { UseQueryResult } from '@tanstack/react-query';
export interface StoreAppInfo {
resultCount: number;
results: App[];
}
export interface App {
isGameCenterEnabled: boolean;
screenshotUrls: string[];
ipadScreenshotUrls: string[];
appletvScreenshotUrls: string[];
artworkUrl512: string;
supportedDevices: string[];
advisories: string[];
features: string[];
kind: string;
artistViewUrl: string;
artworkUrl60: string;
artworkUrl100: string;
minimumOsVersion: string;
artistName: string;
genres: string[];
artistId: number;
price: number;
trackId: number;
trackName: string;
sellerName: string;
genreIds: string[];
bundleId: string;
isVppDeviceBasedLicensingEnabled: boolean;
primaryGenreName: string;
primaryGenreId: number;
releaseDate: string;
currentVersionReleaseDate: string;
releaseNotes: string;
averageUserRatingForCurrentVersion: number;
languageCodesISO2A: string[];
fileSizeBytes: string;
formattedPrice: string;
userRatingCountForCurrentVersion: number;
trackContentRating: string;
version: string;
wrapperType: string;
currency: string;
description: string;
trackCensoredName: string;
trackViewUrl: string;
contentAdvisoryRating: string;
averageUserRating: number;
userRatingCount: number;
}
export type AppVersionInfo = {
hasUpdate: boolean;
version: string;
releaseNotes: string;
} | null;
const versionToNumber = (version: string): number =>
version
.split('.')
.map(Number)
.reduce((acc, num, index) => acc + num * Math.pow(100, 2 - index), 0);
export const useAppStoreQuery = (
appleId: string,
appVersion: string
): UseQueryResult<AppVersionInfo> =>
useQuery<AppVersionInfo, Error, AppVersionInfo, string[]>({
queryKey: ['appStore', appleId, appVersion],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
queryFn: async ({ queryKey: [_, id, version] }): Promise<AppVersionInfo> => {
const ob$: Observable<AppVersionInfo> = iif(
() => Platform.OS === 'ios' && id.length > 0 && version.length > 0,
defer(() =>
from(fetch(`https://itunes.apple.com/cn/lookup?id=${id}`)).pipe(
switchMap((res) => from(res.json() as Promise<StoreAppInfo>)),
map((res) => res?.results ?? [])
)
),
of([])
).pipe(
map<App[], AppVersionInfo>((res) => {
if (res.length === 0) {
return null;
}
const app = res[0];
if (versionToNumber(app.version) > versionToNumber(version)) {
return {
hasUpdate: true,
version: app.version,
releaseNotes: app.releaseNotes,
};
}
return null;
})
);
return await lastValueFrom(ob$);
},
});
const { data } = useAppStoreQuery(AppleIdOfYourApp, VersionOfYourApp);
通过判断上述 hook
的返回就可以知道 App 是否有更新。
跳转到 App Store
iOS 是支持 Universal Link 的(Android 称为 Deep Link),通过这两种技术,App 可以通过链接跳转到另外一个 App。例如通过 Safari 访问网易云音乐在 App Store 的页面系统会自动打开 App Store 应用,并跳转到对应的页面。
React Native 提供了 Linking 机制与 Universal Link 和 Deep Link 进行交互。这里我们只需要获取 App 在 App Store 的链接,通过 Linking
打开这个链接就可以实现跳转到应用商店。那么如何知道 App 的链接呢?答案很简单,通过 App Store 的分享功能,复制链接就可以获取。
获取到了链接之后,就可以通过如下方式进行跳转了。
Linking.openURL(YourAppStoreURL);
在 Simulator 或者真机上就可以直观地看到效果。