阅读视图
Homebrew 5.0:并行加速、MCP 加持,与 Intel 的最后倒计时 -- 肘子的 Swift 周报 #0111
Swift 6 迁移常见 crash: _dispatch_assert_queue_fail
Swift 一个小型游戏对象模型渐进式设计(五)——Swift 并发世界:把 Attackable 搬进 actor
Swift 一个小型游戏对象模型渐进式设计(四)——类型擦除与 Existential:当泛型遇见动态派发
Swift 一个小型游戏对象模型渐进式设计(三)——把能力再抽象一层,写一套“伤害计算器”框架
Swift 一个小型游戏对象模型渐进式设计(二)——协议与默认实现:如何写出不用继承的多态
Swift 一个小型游戏对象模型渐进式设计(一)——继承机制解读:从基础类到防止重写
Swift 中的迭代机制:Sequence、Collection 与 Iterator 完全拆解
告别并发警告:Swift 6 线程安全通知 MainActorMessage & AsyncMessage 实战指南
《Flutter全栈开发实战指南:从零到高级》- 15 -本地数据存储
【SwiftUI 任务身份】task(id:) 如何正确响应依赖变化
精读GitHub - swift-markdown-ui
我开发了一款关于病历管理的app:安康记
Rust RefCell 多线程读为什么也panic了?
微信与苹果就小程序支付达成和解,iOS用户有望在小程序内直接使用苹果支付
《Flutter全栈开发实战指南:从零到高级》- 14 -网络请求与数据解析
网络请求与数据解析
在移动开发中需要与云端服务器进行频繁的数据交互,本节内容讲带你详细了解网络请求与数据解析,让你的应用真正地“活”起来。
一、 为什么网络层如此重要?
举个例子:你正在开发一个新闻App,那些滚动的时事新闻、视频等内容,不可能全部打包在App安装包里,它们需要从服务器实时获取。这个“获取”的过程,就是通过网络请求完成的。
简单来说,流程就是:App 问服务器要数据 -> 服务器返回数据 -> App 把数据展示出来。
在Flutter中,常用的两个网络请求库:官方推荐的 http 库 和 社区维护得 dio 库。我们将从两者入手,带你彻底玩转网络请求。
二、 http库 与 dio库 如何选择?
选择哪个库,就像选择工具,没有绝对的好坏,只有合不合适。
1. http 库
http 库是Flutter团队维护的底层库,它:
- 优点:官方维护,稳定可靠;API简单直接,学习成本低。
- 缺点:功能相对基础,许多高级功能(如拦截器、文件上传/下载进度等)需要自己手动实现。
核心方法:
-
get(): 向指定URL发起GET请求,用于获取数据。 -
post(): 发起POST请求,用于提交数据。 -
put(),delete(),head()等:对应其他HTTP方法。
2. dio 库
dio 是一个强大的第三方HTTP客户端,它:
- 优点:支持拦截器、全局配置、请求取消、FormData、文件上传/下载、超时设置等。
-
缺点:相对于
http库更重一些。
如何选择?
-
新手入门:可以从
http开始,上手快。 -
中大型项目:强烈推荐
dio,它能帮你节省大量造轮子的时间。
本节内容主要以 dio 为例进行讲解,它更符合项目开发的实际情况。
三、 引入依赖
首先,在你的 pubspec.yaml 文件中声明依赖。
dependencies:
flutter:
sdk: flutter
dio: ^5.0.0
# 用于JSON序列化
json_annotation: ^4.8.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.3.0
json_serializable: ^6.5.0
执行 flutter pub get 安装依赖。
四、 http 库
虽然我们推荐使用dio库,但了解http库的基本用法仍是必要的。
以获取一篇博客文章信息为例
import 'package:http/http.dart' as http;
import 'dart:convert';
class HttpExample {
static Future<void> fetchPost() async {
try {
// 1. 发起GET请求
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
);
// 2. 状态码200表示成功
if (response.statusCode == 200) {
// 3. 使用 dart:convert 解析返回的JSON字符串
Map<String, dynamic> jsonData = json.decode(response.body);
// 4. 从解析后的Map中取出数据
String title = jsonData['title'];
String body = jsonData['body'];
print('标题: $title');
print('内容: $body');
} else {
// 请求失败
print('请求失败,状态码: ${response.statusCode}');
print('响应体: ${response.body}');
}
} catch (e) {
// 捕获异常
print('请求发生异常: $e');
}
}
}
代码解读:
-
async/await:网络请求是耗时操作,必须使用异步。await会等待请求完成,而不会阻塞UI线程。 -
Uri.parse:将字符串URL转换为Uri对象。 -
response.statusCode:响应状态码,200系列表示成功。 -
json.decode():反序列化将JSON串转换为Dart中的Map<String, dynamic>或List;
五、 dio 库
下面我们重点讲解下dio库:
1. Dio-发起请求
我们先创建一个Dio实例并进行全局配置。
import 'package:dio/dio.dart';
class DioManager {
// 单例
static final DioManager _instance = DioManager._internal();
factory DioManager() => _instance;
DioManager._internal() {
_dio = Dio(BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: const Duration(seconds: 5), // 连接超时时间
receiveTimeout: const Duration(seconds: 3), // 接收数据超时时间
headers: {
'Content-Type': 'application/json',
},
));
}
late final Dio _dio;
Dio get dio => _dio;
}
// GET请求
void fetchPostWithDio() async {
try {
// baseUrl后面拼接路径
Response response = await DioManager().dio.get('/posts/1');
// dio会自动检查状态码,非200系列会抛异常,所以这里直接处理数据
Map<String, dynamic> data = response.data; // 这里dio帮我们自动解析了JSON
print('获取数据: ${data['title']}');
} on DioException catch (e) {
print('请求异常: $e');
if (e.response != null) {
// 错误状态码
print('错误状态码: ${e.response?.statusCode}');
print('错误信息: ${e.response?.data}');
} else {
// 抛异常
print('异常: ${e.message}');
}
} catch (e) {
// 未知异常
print('未知异常: $e');
}
}
Dio相比Http的优点:
-
自动JSON解析:
response.data直接就是Map或List,无需手动json.decode,太方便了! -
配置清晰:
BaseOptions全局配置一目了然。 -
结构化异常:
DioException包含了丰富的错误信息。
2. Dio-网络请求流程
为了让大家更直观地理解,我们用一个流程图来展示Dio处理请求的完整过程:
sequenceDiagram
participant A as 客户端
participant I as 拦截器
participant D as Dio
participant S as 服务端
A->>D: 发起请求
Note right of A: await dio.get('/users')
D->>I: 请求拦截器
Note right of I: 添加Token、日志等
I->>D: 处理后的请求
D->>S: 发送网络请求
S-->>D: 返回响应
D->>I: 响应拦截器
Note right of I: 解析JSON、错误处理
I->>D: 处理后的响应
D-->>A: 返回最终结果
3. Dio-拦截器
拦截器允许我们在请求发送前和响应返回后,插入自定义逻辑,对所有经过的请求和响应进行检查和加工。
案例:自动添加认证Token
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 请求发送前,为每个请求的Header加上Token
const String token = 'your_auth_token_here';
if (token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// 响应成功处理
print('请求成功: ${response.requestOptions.path}');
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
// 失败处理
// 当Token过期时,自动跳转到登录页
if (err.response?.statusCode == 401) {
print('Token已过期,请重新登录!');
// 这里可以跳转到登录页面
// NavigationService.instance.navigateTo('/login');
}
handler.next(err);
}
}
// 将拦截器添加到Dio实例中
void main() {
final dio = DioManager().dio;
dio.interceptors.add(AuthInterceptor());
// 这里可以添加其他拦截器
dio.interceptors.add(LogInterceptor(responseBody: true));
}
拦截器的添加顺序就是它们的执行顺序。
onRequest正序执行,onResponse和onError倒序执行。
六、 JSON序列化与反序列化
这是新手最容易踩坑的地方。直接从JSON转换成Dart对象(Model),能让我们的代码更安全、更易维护。
1. 为什么要序列化?
-
类型安全:直接操作Map,编译器不知道
data[‘title‘]是String还是int,容易写错; -
代码效率:使用点语法
post.title访问属性,比post[‘title‘]更高效且有代码提示; - 可维护性:当接口字段变更时,你只需要修改一个Model类,而不是分散在各处的字符串key;;
2. 使用 json_serializable自动序列化
通过代码生成的方式,自动创建 fromJson 和 toJson 方法,一劳永逸。
步骤1:创建Model类并使用注解
// post.dart
import 'package:json_annotation/json_annotation.dart';
// 运行 `flutter pub run build_runner build` 后,会生成 post.g.dart 文件
part 'post.g.dart';
// 这个注解告诉生成器这个类需要生成序列化代码
@JsonSerializable()
class Post {
// 使用@JsonKey可以自定义序列化行为
// 例如,如果JSON字段名是`user_id`,而Dart字段是`userId`,可以这样映射:
// @JsonKey(name: 'user_id')
final int userId;
final int id;
final String title;
final String body;
Post({
required this.userId,
required this.id,
required this.title,
required this.body,
});
// 生成的代码会提供这两个方法
factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
Map<String, dynamic> toJson() => _$PostToJson(this);
}
步骤2:运行代码生成命令
在项目根目录下执行:
flutter pub run build_runner build
这个命令会扫描所有带有 @JsonSerializable() 注解的类,并生成对应的 .g.dart 文件(如 post.g.dart)。这个文件里包含了 _$PostFromJson 和 _$PostToJson 的具体实现。
步骤3:自动生成
// 具体的请求方法中使用
void fetchPostModel() async {
try {
Response response = await DioManager().dio.get('/posts/1');
// 将响应数据转换为Post对象
Post post = Post.fromJson(response.data);
print('文章标题: ${post.title}');
} on DioException catch (e) {
// ... 错误处理
}
}
json_serializable 的优势:
- 自动处理类型转换,避免手误;
- 通过
@JsonKey注解可以处理各种复杂场景;
七、 网络层在MVVM模式中的定位
实际项目中,我们不会直接在UI页面里写网络请求代码。让我们看看网络层在MVVM架构中是如何工作的:
graph LR
A[View<br>视图层] -->|调用| B[ViewModel<br>视图模型]
B -->|调用| C[Model<br>模型层]
C -->|使用| D[Dio<br>网络层]
D -->|返回JSON| C
C -->|转换为Model| B
B -->|更新状态| A
各个分层职责:
- View:只关心数据的展示和用户交互;
- ViewModel:持有业务状态,处理UI逻辑,不关心数据从哪里来;
- Model:决定数据是从网络获取还是本地数据库读取,它调用网络层;
- Dio:纯粹的网络请求执行者,负责API调用、错误初步处理等;
这样分层的好处是:最终目的是解耦,各司其职,修改网络层不会影响业务逻辑,代码结构清晰,同事方便单元测试。
八、 错误处理
一个好的应用,必须支持处理各种异常情况。
1. DioException
DioException 的类型 (type) 帮助我们准确判断错误根源。
void handleDioError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
print('超时错误,请检查网络连接是否稳定。');
break;
case DioExceptionType.badCertificate:
print('证书错误。');
break;
case DioExceptionType.badResponse:
// 服务器返回了错误状态码
print('服务器错误: ${e.response?.statusCode}');
// 可以根据不同状态码做不同处理
if (e.response?.statusCode == 404) {
print('请求的资源不存在(404)');
} else if (e.response?.statusCode == 500) {
print('服务器内部错误(500)');
} else if (e.response?.statusCode == 401) {
print('未授权,请重新登录(401)');
}
break;
case DioExceptionType.cancel:
print('请求被取消。');
break;
case DioExceptionType.connectionError:
print('网络连接错误,请检查网络是否开启。');
break;
case DioExceptionType.unknown:
print('未知错误: ${e.message}');
break;
}
}
2. 重试机制
对于因网络波动导致的失败,自动重试能大幅提升用户体验。
class RetryInterceptor extends Interceptor {
final Dio _dio;
RetryInterceptor(this._dio);
@override
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
// 只对超时和网络连接错误进行重试
if (err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.receiveTimeout ||
err.type == DioExceptionType.connectionError) {
final int retryCount = err.requestOptions.extra['retry_count'] ?? 0;
const int maxRetryCount = 3;
if (retryCount < maxRetryCount) {
// 增加重试计数
err.requestOptions.extra['retry_count'] = retryCount + 1;
print('网络不稳定,正在尝试第${retryCount + 1}次重试...');
// 等待一段时间后重试
await Future.delayed(Duration(seconds: 1 * (retryCount + 1)));
try {
// 重新发送请求
final Response response = await _dio.fetch(err.requestOptions);
// 返回成功response
handler.resolve(response);
return;
} catch (retryError) {
// 如果失败继续传递错误
handler.next(err);
return;
}
}
}
// 如果不是指定错误或已达最大重试次数,则继续传递错误
handler.next(err);
}
}
九、 封装一个完整的网络请求库
到这已经把所有的网络请求知识学完了,下面我们用学到的知识封装一个通用的网络请求工具类。
// http_client.dart
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
class HttpClient {
static final HttpClient _instance = HttpClient._internal();
factory HttpClient() => _instance;
late final Dio _dio;
HttpClient._internal() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.yourserver.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {'Content-Type': 'application/json'},
));
// 添加拦截器
_dio.interceptors.add(LogInterceptor(
requestBody: kDebugMode,
responseBody: kDebugMode,
));
_dio.interceptors.add(AuthInterceptor());
_dio.interceptors.add(RetryInterceptor(_dio));
}
// 封装GET请求
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Map<String, dynamic>? headers,
}) async {
try {
final options = Options(headers: headers);
return await _dio.get<T>(
path,
queryParameters: queryParameters,
options: options,
);
} on DioException {
rethrow;
}
}
// 封装POST请求
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Map<String, dynamic>? headers,
}) async {
try {
final options = Options(headers: headers);
return await _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
);
} on DioException {
rethrow;
}
}
// 获取列表数据
Future<List<T>> getList<T>(
String path, {
Map<String, dynamic>? queryParameters,
T Function(Map<String, dynamic>)? fromJson,
}) async {
final response = await get<List<dynamic>>(path, queryParameters: queryParameters);
// 将List<dynamic>转换为List<T>
if (fromJson != null) {
return response.data!.map<T>((item) => fromJson(item as Map<String, dynamic>)).toList();
}
return response.data as List<T>;
}
// 获取单个对象
Future<T> getItem<T>(
String path, {
Map<String, dynamic>? queryParameters,
required T Function(Map<String, dynamic>) fromJson,
}) async {
final response = await get<Map<String, dynamic>>(path, queryParameters: queryParameters);
return fromJson(response.data!);
}
}
//
class PostRepository {
final HttpClient _client = HttpClient();
Future<Post> getPost(int id) async {
final response = await _client.getItem(
'/posts/$id',
fromJson: Post.fromJson,
);
return response;
}
Future<List<Post>> getPosts() async {
final response = await _client.getList(
'/posts',
fromJson: Post.fromJson,
);
return response;
}
Future<Post> createPost(Post post) async {
// Model转JSON
final response = await _client.post(
'/posts',
data: post.toJson(),
);
return Post.fromJson(response.data);
}
}
总结
又到了写总结诶的时候了,让我们用一张表格来回顾所有知识点:
| 知识点 | 核心 | 用途 |
|---|---|---|
| 库选择 |
http 轻量,dio 强大 |
中大型项目首选 dio
|
| 异步编程 | 使用 async/await 处理耗时操作 |
不能阻塞UI线程 |
| JSON序列化 | 自动生成 |
推荐 json_serializable
|
| 错误处理 | 区分网络异常和服务器错误 | 精确捕获 DioException 并分类处理 |
| 拦截器 | 统一处理请求/响应 | 用于添加Token、日志、重试逻辑 |
| 架构分层 | MVVM | 分离解耦 |
| 请求封装 | 统一封装GET/POST等基础方法 | 提供 getItem, getList 等语义化方法 |
网络请求在实际项目中直观重要,没有网络就没有数据,掌握好本章内容,你就能为你Flutter应用注入源源不断的活力。让我们下期见!
iOS 社招 - Runtime 相关知识点
Vibe Coding 实战!花了两天时间,让 AI 写了一个富文本渲染引擎!
一、先上效果图
最近动手实践了下 Vibe Coding,想尝试一行代码不写,纯通过 Prompt 让 AI 写了一个富文本渲染引擎。
整体花了两天时间不到,效果如上图,支持的特性有:
- 类似前端的 Block、InlineBlock、Inline 布局
- 文本样式:加粗、斜体、下划线、删除线,前景色,背景色,同一行不同字体大小混排等
- Attachment:图文混排或插入自定义 View 等
- 异步排版和算高:基于 CoreText API,支持子线程布局算高,主线程渲染
- 单测覆盖
项目用的 Claude AI,差不多耗费了 50$(是真的贵!但也是真的强!),本文将记录整个过程和一些经验总结。
二、过程记录
2.1 Claude 安装和项目初始化
Claude 安装和使用在网上有很多教程,细节这里不再赘述,推荐直接使用 VSCode 的 Claude AI 插件;后文「经验总结」部分也会总结 Claude AI 的常用命令,感兴趣可以直接跳转。
首先,我们需要新建一个空的 iOS 项目和富文本渲染引擎的 pod(这里我们叫 RichView),创建完成之后在 VSCode 中打开,点击右上角 Claude AI 的图标开启会话,输入/init 命令初始化工程。
/init命令的作用是让 Claude 理解整个项目,这是在项目中使用 Claude 的第一步,只需要执行一次就好。
/init会在根目录下自动创建一个CLAUDE.md文件,这个文件可以理解成全局上下文,即每次新开 Claude 会话都会自动加载其中的内容,我们可以在这里记录一些如修改历史、全局说明等内容。
2.2 技术选型、架构
让 AI 写代码,和我们自己写代码基本类似,不过是将我们的思路转换成 Prompt 告诉 AI。
编码之前需要先确定几件事情:这些确定好之后,我们后续的任务拆分才会更顺利。
1)需要支持哪些 Feature
- 支持文本样式:加粗、斜体、下划线、删除线,前景色,背景色,同一行不同字体大小混排等
- 支持 Attachment:图文混排或插入自定义 View 等
- 支持子线程排版算高
- 支持单元测试
2)技术选型
自定义富文本渲染引擎,最难的点在于如何实现精确的文本分词排版(原理可以参考从 0 到 1 自定义文字排版引擎:原理篇),iOS 有内置的 CoreText API(见链接)用于文本分词排版,当然也可以基于开源的跨端排版引擎 HarfBuzz(见链接)进行处理。
我们这里不需要跨端,因此选择 CoreText 作为方案选型。
官方封装的 NSAttributedString 当然也能做这件事情,但是从工程实践看,NSAttributedString 在扩展性(比如支持列表、表格等自定义布局)、使用方便性,以及长文本的性能方面不尽如人意。
3)技术架构
文本分词之后,还需要进行布局排版,为方便后续拓展布局,我们这里参考前端的布局模型,引入 Block、InlineBlock、Inline 的概念。
同时参考浏览器的布局渲染过程,引入三棵树的概念:
- ElementTree:用户输入,整个富文本可以通过一颗 ElementTree 来表示
- LayoutTree:负责布局排版,会在这一层处理好文本的分词、图文混排时各自的位置等
- RenderTree:负责渲染,这一层接收布局完成的结果,进行最终的上屏绘制
敲定技术选型、技术架构之后,我们就可以按思路拆分子任务了。
2.3 子任务:ElementTree
由于我们参考了前端的布局模型,因此我们需要告诉 AI 在 CSS 中 Block、InlineBlock、Inline 的布局规范,这个在 MDN 中可以直接摘录,当然也可以直接让 AI 帮我们生成(如上图)。
接着,我们需要告诉 AI 怎么构建 ElementTree,也就是上图所示 Prompt。
最后,我们就可以让 AI 参照 Prompt,生成 ElementTree 了。
ElementTree 生成完成后,我们发现遗漏了单测环节,继续完善 ElementTree 的 Prompt,然后明确告诉 AI xx 文件新增了 xx 任务,让 AI 继续完成任务,如下图:
ElementTree 的创建还算比较顺利,AI 理解也比较到位,生成的代码基本符合预期。
2.4 子任务:LayoutTree
同样,我们定义好 Prompt,让 AI 生成 LayoutTree。
LayoutTree 的生成不太顺利,而且从最后的测试效果看也有很多 Bug,主要如下:
- AI 将绘制相关逻辑也加到了 LayoutTree 中,但预期绘制是单独的 RenderTree
- 布局问题:InlineBlock 无法整体换行,多个 Inline 在同一行时被换行展示,margin、padding 不生效等
- 对齐问题:同一行包含不同字号的文本时,对齐方式不对
- attachment 无法显示
- …
2.5 子任务:RenderTree
由于 LayoutTree 这个底层基础没扎实,RenderTree 的搭建也不顺利,RenderTree 的 Prompt 如上。
2.6 BugFix
至此,AI 生成了初版的富文本渲染引擎,接下来就是让 AI 写个 Demo 试用一下,在使用过程中,发现了很多上面罗列的 Bug,针对这些 Bug,也可以让 AI 来修复:
在让 AI 修 Bug 过程中,也踩了一些坑,参见下文经验总结。
三、一些经验总结
3.1 Claude AI 常用命令
-
/init:项目初始化,第一次使用 Claude AI 时执行,每个项目只需要执行一次即可;会生成一个CLAUDE.md文件,这是项目的全局上下文,每次新建 Claude 会话时,会自动读取其中的内容;可以在CLAUDE.md文件中补充修改历史、全局说明等 -
@:可以输入@来添加文件到会话窗口,将文件作为上下文给 AI -
/exit:关闭当前会话 -
/clear:清除当前会话上下文,和退出会话然后新开一个会话效果一样 -
/compact:压缩和总结当前会话上下文,和/clear的区别是,/compact会将当前会话上下文总结后作为当前会话的新上下文,/clear会直接清除所有上下文 -
/resume:显示和恢复历史上下文 - 自定义 command:可以将通用的 Prompt 做成自定义 command,文件位置在
.claude/commands/;还可以通过$ARGUMENUTS来接收自定义参数
-
/agents:有的任务比较复杂,或上下文较多,那可以拆分成多个 agents 进行组合,比如写业务逻辑 -> 构建单元测试 -> CI/CD 等,可以拆分多个 agents 组合使用 -
会话模式:在最新版本的 Claude AI 插件中,除了之前命令行风格的 GUI 以外,还提供了会话框风格的 GUI,切换会话模式,查看历史会话等会更方便;如下,会话模式可以在输入框左下角切换
-
- Edit automatically:AI 根据输入 Prompt 进行理解并直接编辑文件,一般使用该模式即可
-
- Plan mode:AI 根据输入 Prompt 列出修改计划,你可以进一步校验和修改 Plan
-
- Ask before edits:AI 修改文件前询问
- MCP:常用的 MCP 是
context7,context7是用于帮助 AI 查找最新文档的,避免使用过时 API
3.2 经验总结
不得不感叹,AI 编程实在太强大了,相信在不久的将来,一个只会写 Prompt 的非专业程序员,也能完整交付一个 App 了。
让 AI 编程,并不是说给一句话就能让 AI 完成代码,各种细节还是需要人来提前想清楚,毕竟最终维护代码和解决问题的还是我们自己,AI 只是帮我们提效和扩展思路的工具;有句话总结的蛮好:你可以将 AI 视为一个非常聪明,甚至资深,但是没有业务经验的程序员。
下面我想总结下最近实战的一些经验,希望对各位有帮助:
1)架构设计需要提前规划好,尽量想清楚细节
谋定而后动,不管是我们自己写代码,还是让 AI 写代码,我觉得提前想好要做什么,怎么做是非常重要的。
架构设计好了,细节想清楚了,那怎么拆分子任务,其实也就明确了。
2)任务拆分越小越好,上下文越明确越好
AI 最适合做有明确输入输出的事情,给的上下文越明确,AI 产生幻觉的概率越低,输出结果也会越准确。
当然,如果是输入输出明确的任务,也可以让 AI 先输出测试用例,测试用例人工检测完备之后,再让 AI 编码也是可以的(测试驱动开发/TDD)。
3)每一项目任务做好之后再进行下一项任务
基础不牢,地动山摇!
推荐打磨好每一项子任务再继续下一项任务,否则千里之堤毁于蚁穴,每个任务都留一点坑,最终可能带来灾难性的结果!
另外,单测是个好东西,对每项任务补齐单测,可以有效防止后续 AI 改出问题。
4)善用 Git,防止代码污染
Claude 在 Edit automatically 模式下会直接修改文件,为了防止污染其他代码,每次让 AI 修改前尽量保证工作区干净,这样也能方便我们 Review 代码。
5)写 Prompt 尽量用明确的词汇,不要表意不清
比如在构建 ElementTree 时,我会明确告诉 AI 要支持哪些 Style,可以有效避免 AI 臆测
与之相反的反例是,在构建 LayoutTree 时,限定不足,导致 AI 自由发挥,最终实现出很多 Bug。
6)善用提示词:think < think hard < think harder < ultrathink
可以在 Prompt 中追加 think hard / think harder 等词汇,来让 AI 进入深度思考,这并不是什么黑魔法,而是 Claude AI 官方认证的,参见:www.anthropic.com/engineering…
实践下来,确实还是有效果的,如下是让 AI 修复文本对齐问题,加了 think hard AI 会更深入理解代码,找到问题原因;当然,这种方式也有弊端,就是会耗费更多的 token(money)👺
7)善用 /compact /clear命令,减少模型幻觉
如果不主动清除,Claude AI 会话中的上下文是会一直保存的,当一个会话中问答轮次过多,可能会导致 AI 理解不准确(幻觉)。
可以通过/compact 或/clear命令,来压缩/清除上下文。
一般我在修复有关联性的 Bug 时,会使用/compact命令,这样 AI 就不需要重新理解工程,理解 Bug 了,可以提高效率。
8)BugFix 尽量构造最小可复现 Demo
BugFix 其实也是一个子任务,最小可复现 Demo 减少 AI 的理解负担。
9)及时人工介入,避免在一个问题上死磕
有时候让 AI 修复 Bug 时,可能反复修改都解决不了,这时候大概率是 AI 没有真正理解问题,或者就是输入的 Prompt 有问题,这种情况下就没必要让 AI 死磕问题了,我们可以及时人工介入,避免浪费时间。
10)善用 Plan 模式
在任务拆分时,我们自己可能也没想明白应该怎么做,那可以切换到 Plan 模式,让 AI 和我们一起拆任务。
3.3 Vibe Coding 的一些弊端
1)付费,而且还挺贵!
这是一个挺现实的问题,一些好的模型都挺贵,而且还是消耗的刀乐,国内厂商的模型质量又不尽如人意。
2)编码风格问题 & 扩展性、易用性、鲁棒性不足
AI 写的代码还是挺容易看出来的,感觉很难带有程序员的个人风格,一个明显的表现是会用一些比较少见的 API,虽然,这可能也是 AI 的厉害之处。
另外,AI 在一些函数复用性、扩展性、使用方便性上有时候差强人意,比如 AI 生成代码如下:如果要配置 Element 的 Style,需要不断的调用text.style.xxx,但其实写成链式调用使用起来会更舒服,如下注释部分
let text = TextElement(text: "一、晨光初照")
text.style.color = .red
text.style.font = UIFont.systemFont(ofSize: 17)
// 更好的写法
// text.style.setColor(xxx).setFont(xxx)
鲁棒性方面,AI 不会主动考虑调用场景,比如我虽然告诉了 AI 我要支持子线程布局,但是 AI 生成的代码并不是线程安全的。
当然,上述这些,可以通过完善 Prompt 来部分弥补。
3)问题定位幻觉
有时候让 AI 排查一些 Bug,它无法找到真正的原因,反复修改后还是有问题。
这种情况下,就需要人工介入了,我们可以自己定位问题,再告诉 AI 怎么修改,而不要让 AI 死磕问题,避免浪费时间。
四、贴下源码 & Prompt
内容首发在公众号「非专业程序员Ping」,觉得有用的话,三连再走吧~ (⁎˃ᴗ˂⁎)
富文本相关,你可能感兴趣: