当我使用ChatGPT写一个IOS的热更新
前些天注册了一个ChatGPT账号,总是问一些没有营养的问题显得太过无聊。想起之前写了一个热更新的文章,仅写了Android端的实现,没有写IOS端的实现方式。
为什么不写IOS端的实现呢,主要就是不会写Objective-C/Swift。既然自己不写,那就让最强人工智能来帮我写一个。
热更新的思路可以查看上一篇《React Native 热更新探索》,再简单梳理一下,大概流程如下:
- 借助Metro生成热更新文件
- 构建热更新服务,用于下发热更新文件
- Native端支持热更新,能够切换热更新文件
- 检查热更新,这一步可以在客户端也可以在RN端,这里用RN端实现比较简单
生成热更新文件
这一步比较简单,直接调用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
热更新服务
热更新服务,可简单也可以复杂,简单的话,只需要判断一下版本号,给出新的热更新文件下载地址即可。复杂来做,需要可能需要考虑多项目、多版本、权限控制等诸多因素,这里不做赘述,依旧起一个简单的PHP Mock Server来验证。
<?php
$versionCode = $_GET['versionCode'] ? (int)$_GET['versionCode'] : 0;
$updateList = [
100000001 => [
"url" => "http://10.12.164.89:8360/bundle/update.zip",
"content" => "1. 增加热更新功能测试。\n2. 增加图片。\n3. 杀了一个设计师祭天。",
"version" => "1.0.1"
]
];
$res = null;
foreach($updateList as $key => $val) {
if ($key > $versionCode) {
$res = $val;
}
}
echo json_encode($res);
?>
Ios 端的热更新改造
原理上与Android差不多,也是在ReactHost实例化时,通过暴露的接口修改ReactHost加载Bundle文件的地址,在Android需要修改getJSBundleFile
这个函数,查找到热更新文件并返回其路径。
在IOS中原理肯定是类似的,IOS储备知识不多,看看工程代码猜一猜看,仔细查看,在AppDelegate.mm
文件中,可以发现如下可疑代码。
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
功能上与Android端类似,命名上也能理解这个函数的含义是获取ReactNative的入口文件。重点来了,虽然知道思路,但是苦于不会写Objective-C的代码啊,是时候让ChatGPT出手了。
ChatGPT帮我写代码
听说ChatGPT很聪明,那我也不兜着了。直接上来就是一句: 帮我写一个react native ios端替换bundle文件位置的代码
也不能说毫无关联,有那么点意思,但是明显我想要的效果。
于是我再问: 我需要的是ios 端代码修改react native bundle文件位置的功能
我希望把这个功能做成一个原生模块,这样代码可以复用: 将这个功能封装成一个react native 原生模块,包名叫@leona-rn/client
看起来更好了一点,但是这样也完不成热更新的流程。原来的文章中知识简单读取一个固定位置Bundle文件来完成热更新。这次我希望更加完善一点:
- 可以根据热更新接口给出的结果来选择最新的Bundle包
- 多次热更新的结果不冲突,永远使用最新的Bundle包
想实现这一点,可以将热更新包都下载到一个固定的位置,并使用版本号作为文件名,查询热更新文件时,遍历目录下的热更新文件版本号,选出版本号最大的那个,但是随着热更新越来越多,文件肯定也越来越多,遍历文件目录可能会成为性能问题。
也可以使用数据库获取其他本地存储方案将最新的版本号存起来,每次只需要读取这个版本号即可,这里选取最简单的文件存储:
// manifest.json
{
"newest": 100
}
在拉取热更新文件的时候,只需要替换这个版本号即可,在梳理一下,获取热更新地址的逻辑变成了:
-
读取
manifest.json
文件,如果存在newest字段且大于0,就返回版本号对应的文件地址。 - 如果不满足条件1,则返回空,使用内置的Bundle包。
第2个逻辑可能存在漏洞,如果某些原因,导致manifest.json
文件出错,可能会导致热更新失效,从而导致应用回退使用到内置包。兼容这个逻辑也比较复杂,这里选择在热更新阶段保证manifest.json
文件的正确性,从而保障整个热更新流程的安全性。
于是我把需求整理了一下,发给了他一个完整的需求:
我需要一个叫 @leona-rn/client 的react native 原生模块,有以下要求:
1. 提供一个对外函数,这个函数可以读取应用程序沙盒文档目录下的 /updates/manifest.json 文件。
manifest.json中有两个字段,分别是current和newest,它们都是数字类型。
如果newest有数值,返回应用程序沙盒文档目录 + /updates/{newest}/index.ios.js 组成的字符串,并且将current置为newest的值,newest置为空,再将新的json数据写回到原来的manifest.json文件中。
如果current有数值,返回应用程序沙盒文档目录 + /updates/{current}/index.ios.js 组成的字符串。
如果都不满足条件,返回空
2. 这个对外函数可以在任何react native项目中引入,并使用这个函数修改react native应用获取js bundle位置的方式
最后ChatGPT宕机了,没有给我答案……终究是免费用户不配了。多试几次之后,ChatGPT给出了答案,但是还是不理想,就不贴了。
ChatGPT其实很聪明,但是对于复杂的需求,而且不是英文,它理解起来可能跟开发者理解的意思不太一样,开发者描述出来的需求可能也并不是那么易于理解。
我可以将需求拆解一下,我其实需要的一个读取manifest.json
并返回里面某个字段与根目录拼接的路径。将这个细分需求抛给ChatGPT看看。直接看最终结果吧,调教过程就不看了,有兴趣的可以自行去调教一下试试。
ChatGPT写的代码确 实挺漂亮。漂亮归漂亮,还是会忽略一些异常处理,比如文件不存在的情况或者是文件操作/json操作失败的异常情况,当然只要你反馈给ChatGPT,它很快就可以修正,确实很厉害。
整体上来说,功能已经实现的七七八八,后面对此对话,给出的代码还是不合我意,于是不再依靠ChatGPT,自己查看一些资料,将这个功能封装成一个ReactNative原生模块,发布成npm包。看看最终代码:
+ (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;
}
完全零基础,依靠ChatGPT写出这么一段代码,也算不错了。
ChatGPT 怎么帮程序员解决难题
在测试过程中Xcode还抛出unrecognized selector sent to class
这么一个错误,谷歌了一个小时也没有找到答案。
最后我将所有代码以及调用方式丢给ChatGPT,很快就给出了答案,原来是实例函数与静态函数的问题。
原先的函数定义为
- (nullable NSURL *)getJSBundlePath:(BOOL)useHermes
改为即可解决
+ (nullable NSURL *)getJSBundlePath:(BOOL)useHermes
对于内行人来说,可能只是一个很小的问题,对于外行来说,很难想到是一个加减号的问题,ChatGPT比搜索引擎强的地方就在于这里了。
检查更新
检查更新就更简单了,请求接口->下载Bundle包->将版本号写入就可以了
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('热更新请求成功,准备下载热更新文件');
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;
}
}
总结
- 对于热更新来说,简单实现没有那么复杂,只需要管理好Bundle包的下载于路径读取即可,复杂应用可以考虑更多。
- 做不出来的需求可以丢给ChatGPT试试,它总能给你实现,但是调教过程可能较长,需要慢慢引导。
- 谷歌不到的难题也可以丢给ChatGPT试试,没准比谷歌更快!
收工,输出文章一篇。顺利造出三个轮子:@leona-rn/cli、@leona-rn/client、@leona-rn/core,分别负责RN打包与上传、客户端替换热更新文件、RN端检查与下载热更新并做版本管理。
关注公众号
欢迎关注作者公众号 前端方程式