當(dāng)我使用ChatGPT寫一個(gè)IOS的熱更新
前些天注冊了一個(gè)ChatGPT賬號(hào),總是問一些沒有營養(yǎng)的問題顯得太過無聊。想起之前寫了一個(gè)熱更新的文章,僅寫了Android端的實(shí)現(xiàn),沒有寫IOS端的實(shí)現(xiàn)方式。
為什么不寫IOS端的實(shí)現(xiàn)呢,主要就是不會(huì)寫Objective-C/Swift。既然自己不寫,那就讓最強(qiáng)人工智能來幫我寫一個(gè)。
熱更新的思路可以查看上一篇《React Native 熱更新探索》,再簡單梳理一下,大概流程如下:
- 借助Metro生成熱更新文件
- 構(gòu)建熱更新服務(wù),用于下發(fā)熱更新文件
- Native端支持熱更新,能夠切換熱更新文件
- 檢查熱更新,這一步可以在客戶端也可以在RN端,這里用RN端實(shí)現(xiàn)比較簡單
生成熱更新文件
這一步比較簡單,直接調(diào)用Metro即可。
npx react-native bundle --entry-file index.js --bundle-output ./update/bundle/index.android.bundle --platform ios --assets-dest ./update/bundle --dev false --reset-cache
熱更新服務(wù)
熱更新服務(wù),可簡單也可以復(fù)雜,簡單的話,只需要判斷一下版本號(hào),給出新的熱更新文件下載地址即可。復(fù)雜來做,需要可能需要考慮多項(xiàng)目、多版本、權(quán)限控制等諸多因素,這里不做贅述,依舊起一個(gè)簡單的PHP Mock Server來驗(yàn)證。
<?php
$versionCode = $_GET['versionCode'] ? (int)$_GET['versionCode'] : 0;
$updateList = [
100000001 => [
"url" => "http://10.12.164.89:8360/bundle/update.zip",
"content" => "1. 增加熱更新功能測試。\n2. 增加圖片。\n3. 殺了一個(gè)設(shè)計(jì)師祭天。",
"version" => "1.0.1"
]
];
$res = null;
foreach($updateList as $key => $val) {
if ($key > $versionCode) {
$res = $val;
}
}
echo json_encode($res);
?>
Ios 端的熱更新改造
原理上與Android差不多,也是在ReactHost實(shí)例化時(shí),通過暴露的接口修改ReactHost加載Bundle文件的地址,在Android需要修改getJSBundleFile這個(gè)函數(shù),查找到熱更新文件并返回其路徑。
在IOS中原理肯定是類似的,IOS儲(chǔ)備知識(shí)不多,看看工程代碼猜一猜看,仔細(xì)查看,在AppDelegate.mm文件中,可以發(fā)現(xiàn)如下可疑代碼。
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
功能上與Android端類似,命名上也能理解這個(gè)函數(shù)的含義是獲取ReactNative的入口文件。重點(diǎn)來了,雖然知道思路,但是苦于不會(huì)寫Objective-C的代碼啊,是時(shí)候讓ChatGPT出手了。
ChatGPT幫我寫代碼
聽說ChatGPT很聰明,那我也不兜著了。直接上來就是一句: 幫我寫一個(gè)react native ios端替換bundle文件位置的代碼

也不能說毫無關(guān)聯(lián),有那么點(diǎn)意思,但是明顯我想要的效果。
于是我再問: 我需要的是ios 端代碼修改react native bundle文件位置的功能

我希望把這個(gè)功能做成一個(gè)原生模塊,這樣代碼可以復(fù)用: 將這個(gè)功能封裝成一個(gè)react native 原生模塊,包名叫@leona-rn/client

看起來更好了一點(diǎn),但是這樣也完不成熱更新的流程。原來的文章中知識(shí)簡單讀取一個(gè)固定位置Bundle文件來完成熱更新。這次我希望更加完善一點(diǎn):
- 可以根據(jù)熱更新接口給出的結(jié)果來選擇最新的Bundle包
- 多次熱更新的結(jié)果不沖突,永遠(yuǎn)使用最新的Bundle包
想實(shí)現(xiàn)這一點(diǎn),可以將熱更新包都下載到一個(gè)固定的位置,并使用版本號(hào)作為文件名,查詢熱更新文件時(shí),遍歷目錄下的熱更新文件版本號(hào),選出版本號(hào)最大的那個(gè),但是隨著熱更新越來越多,文件肯定也越來越多,遍歷文件目錄可能會(huì)成為性能問題。
也可以使用數(shù)據(jù)庫獲取其他本地存儲(chǔ)方案將最新的版本號(hào)存起來,每次只需要讀取這個(gè)版本號(hào)即可,這里選取最簡單的文件存儲(chǔ):
// manifest.json
{
"newest": 100
}
在拉取熱更新文件的時(shí)候,只需要替換這個(gè)版本號(hào)即可,在梳理一下,獲取熱更新地址的邏輯變成了:
-
讀取
manifest.json文件,如果存在newest字段且大于0,就返回版本號(hào)對應(yīng)的文件地址。 - 如果不滿足條件1,則返回空,使用內(nèi)置的Bundle包。
第2個(gè)邏輯可能存在漏洞,如果某些原因,導(dǎo)致manifest.json文件出錯(cuò),可能會(huì)導(dǎo)致熱更新失效,從而導(dǎo)致應(yīng)用回退使用到內(nèi)置包。兼容這個(gè)邏輯也比較復(fù)雜,這里選擇在熱更新階段保證manifest.json文件的正確性,從而保障整個(gè)熱更新流程的安全性。
于是我把需求整理了一下,發(fā)給了他一個(gè)完整的需求:
我需要一個(gè)叫 @leona-rn/client 的react native 原生模塊,有以下要求:
1. 提供一個(gè)對外函數(shù),這個(gè)函數(shù)可以讀取應(yīng)用程序沙盒文檔目錄下的 /updates/manifest.json 文件。
manifest.json中有兩個(gè)字段,分別是current和newest,它們都是數(shù)字類型。
如果newest有數(shù)值,返回應(yīng)用程序沙盒文檔目錄 + /updates/{newest}/index.ios.js 組成的字符串,并且將current置為newest的值,newest置為空,再將新的json數(shù)據(jù)寫回到原來的manifest.json文件中。
如果current有數(shù)值,返回應(yīng)用程序沙盒文檔目錄 + /updates/{current}/index.ios.js 組成的字符串。
如果都不滿足條件,返回空
2. 這個(gè)對外函數(shù)可以在任何react native項(xiàng)目中引入,并使用這個(gè)函數(shù)修改react native應(yīng)用獲取js bundle位置的方式
最后ChatGPT宕機(jī)了,沒有給我答案……終究是免費(fèi)用戶不配了。多試幾次之后,ChatGPT給出了答案,但是還是不理想,就不貼了。
ChatGPT其實(shí)很聰明,但是對于復(fù)雜的需求,而且不是英文,它理解起來可能跟開發(fā)者理解的意思不太一樣,開發(fā)者描述出來的需求可能也并不是那么易于理解。
我可以將需求拆解一下,我其實(shí)需要的一個(gè)讀取manifest.json并返回里面某個(gè)字段與根目錄拼接的路徑。將這個(gè)細(xì)分需求拋給ChatGPT看看。直接看最終結(jié)果吧,調(diào)教過程就不看了,有興趣的可以自行去調(diào)教一下試試。

ChatGPT寫的代碼確 實(shí)挺漂亮。漂亮歸漂亮,還是會(huì)忽略一些異常處理,比如文件不存在的情況或者是文件操作/json操作失敗的異常情況,當(dāng)然只要你反饋給ChatGPT,它很快就可以修正,確實(shí)很厲害。
整體上來說,功能已經(jīng)實(shí)現(xiàn)的七七八八,后面對此對話,給出的代碼還是不合我意,于是不再依靠ChatGPT,自己查看一些資料,將這個(gè)功能封裝成一個(gè)ReactNative原生模塊,發(fā)布成npm包??纯醋罱K代碼:
+ (nullable NSURL *)getJSBundlePath:(BOOL)useHermes
{
NSURL *baseBundleUrl = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
NSString *basePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *fileName = useHermes ? @"index.hermes" : @"index.js";
NSString *manifestPath = [basePath stringByAppendingPathComponent:@"updates/manifest.json"];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSLog(@"manifest.json 文件地址: %@", manifestPath);
if (![fileManager fileExistsAtPath:manifestPath]) {
// 文件不存在,返回 nil
return baseBundleUrl;
}
NSData *data = [NSData dataWithContentsOfFile:manifestPath];
NSError *error;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
if (error) {
NSLog(@"manifest.json 文件解析失敗: %@", error.localizedDescription);
return baseBundleUrl;
}
NSNumber *newest = json[@"newest"];
if (newest && [newest integerValue] > 0) {
NSString *filePath = [basePath stringByAppendingPathComponent:[NSString stringWithFormat:@"updates/%@/%@", newest, fileName]];
// 文件不存在,返回 nil
if ([fileManager fileExistsAtPath:filePath]) {
return [NSURL fileURLWithPath:filePath];
}
}
return baseBundleUrl;
}
完全零基礎(chǔ),依靠ChatGPT寫出這么一段代碼,也算不錯(cuò)了。
ChatGPT 怎么幫程序員解決難題
在測試過程中Xcode還拋出unrecognized selector sent to class這么一個(gè)錯(cuò)誤,谷歌了一個(gè)小時(shí)也沒有找到答案。
最后我將所有代碼以及調(diào)用方式丟給ChatGPT,很快就給出了答案,原來是實(shí)例函數(shù)與靜態(tài)函數(shù)的問題。

原先的函數(shù)定義為
- (nullable NSURL *)getJSBundlePath:(BOOL)useHermes
改為即可解決
+ (nullable NSURL *)getJSBundlePath:(BOOL)useHermes
對于內(nèi)行人來說,可能只是一個(gè)很小的問題,對于外行來說,很難想到是一個(gè)加減號(hào)的問題,ChatGPT比搜索引擎強(qiáng)的地方就在于這里了。
檢查更新
檢查更新就更簡單了,請求接口->下載Bundle包->將版本號(hào)寫入就可以了
export default class Leona {
// xxxx
public async checkUpdate() {
if (!this.enable) {
return;
}
// 包版本無效則不做更新
if (!this.bundleVersion || isNaN(this.bundleVersion)) {
return;
}
const update = await this.getUpdateInfo();
if (!update?.zip_url) {
return;
}
console.info('熱更新請求成功,準(zhǔn)備下載熱更新文件');
const download = await this.downBundle(update);
if (!download) {
return;
}
console.info('熱更新文件下載成功,開始更新manifest.json');
await this.updateManifest(update.version);
console.info('熱更新成功,重啟生效');
}
/**
* 更新manifest.json文件
* @param data UpdateResult
*/
private async updateManifest(newest: number) {
await RNFS.writeFile(this.manifestFile, JSON.stringify({ newest }), 'utf8');
}
/**
* 使用RNFS下載熱更新文件
* @param param UpdateResult
* @returns boolean
*/
private async downBundle({ zip_url, version }: UpdateResult) {
const exist = await RNFS.exists(this.cacheDir);
if (!exist) {
RNFS.mkdir(this.cacheDir);
}
const distFile = `${this.cacheDir}/${version}.zip`;
const down = await RNFS.downloadFile({
fromUrl: zip_url,
toFile: distFile,
}).promise;
if (down.statusCode === 200) {
await unzip(distFile, `${this.distDir}/${version}`);
await RNFS.unlink(distFile);
return true;
}
return false;
}
/**
* 請求熱更新接口
* @returns UpdateResult
*/
private async getUpdateInfo() {
const res = await request.get<UpdateResult>(
this.updateUrl,
{
version_code: this.versionCode,
bundle_version: this.bundleVersion,
platform: this.platform,
},
{
headers: this.requestHeader,
}
);
return res;
}
}
總結(jié)
- 對于熱更新來說,簡單實(shí)現(xiàn)沒有那么復(fù)雜,只需要管理好Bundle包的下載于路徑讀取即可,復(fù)雜應(yīng)用可以考慮更多。
- 做不出來的需求可以丟給ChatGPT試試,它總能給你實(shí)現(xiàn),但是調(diào)教過程可能較長,需要慢慢引導(dǎo)。
- 谷歌不到的難題也可以丟給ChatGPT試試,沒準(zhǔn)比谷歌更快!
收工,輸出文章一篇。順利造出三個(gè)輪子:@leona-rn/cli、@leona-rn/client、@leona-rn/core,分別負(fù)責(zé)RN打包與上傳、客戶端替換熱更新文件、RN端檢查與下載熱更新并做版本管理。
關(guān)注公眾號(hào)
歡迎關(guān)注作者公眾號(hào)?前端方程式
