feat: 添加 CancelToken 请求取消支持 (v3.3.0)
新增功能: - 可通过 cancel_token_support 配置启用 - 自动为所有 API 方法添加 @CancelRequest() CancelToken 参数 - 完全向后兼容,默认禁用 修改文件: - lib/core/config_repository.dart: 添加 cancelTokenSupport getter - lib/pipeline/generate/impl/retrofit_api/api_parameters.dart: 参数生成逻辑 - generator_config.template.yaml: 配置项说明 新增文档: - CANCELTOKEN_USAGE_GUIDE.md: 使用指南 - CHANGELOG_CANCELTOKEN.md: 功能变更详情 测试: - test/cancel_token_support_test.dart: 10 个单元测试
This commit is contained in:
parent
264ec39ca9
commit
52a242f392
|
|
@ -0,0 +1,132 @@
|
|||
# CancelToken 使用指南 / CancelToken Usage Guide
|
||||
|
||||
## 概述 / Overview
|
||||
|
||||
CancelToken 是 Dio 提供的请求取消机制。启用此功能后,生成的 API 方法将自动包含可选的 `CancelToken?` 参数,允许您在请求进行中取消它。
|
||||
|
||||
CancelToken is a request cancellation mechanism provided by Dio. When enabled, generated API methods will automatically include an optional `CancelToken?` parameter, allowing you to cancel requests while they are in progress.
|
||||
|
||||
---
|
||||
|
||||
## 配置 / Configuration
|
||||
|
||||
在 `generator_config.yaml` 中启用:
|
||||
|
||||
```yaml
|
||||
generation:
|
||||
api:
|
||||
cancel_token_support: true # 启用 CancelToken 支持
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 生成效果 / Generated Code
|
||||
|
||||
**启用前 / Before:**
|
||||
|
||||
```dart
|
||||
Future<BaseResult<User>> getUser({
|
||||
@Query('id') int? id,
|
||||
});
|
||||
```
|
||||
|
||||
**启用后 / After:**
|
||||
|
||||
```dart
|
||||
Future<BaseResult<User>> getUser({
|
||||
@Query('id') int? id,
|
||||
@CancelRequest() CancelToken? cancelToken,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用示例 / Usage Examples
|
||||
|
||||
### 基本用法 / Basic Usage
|
||||
|
||||
```dart
|
||||
final cancelToken = CancelToken();
|
||||
|
||||
// 发起请求 / Make request
|
||||
final future = api.getUser(id: 1, cancelToken: cancelToken);
|
||||
|
||||
// 取消请求 / Cancel the request
|
||||
cancelToken.cancel('用户取消操作');
|
||||
|
||||
try {
|
||||
final result = await future;
|
||||
} on DioException catch (e) {
|
||||
if (CancelToken.isCancel(e)) {
|
||||
print('请求已取消: ${e.message}');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 页面离开时取消 / Cancel on Page Dispose
|
||||
|
||||
```dart
|
||||
class MyPageState extends State<MyPage> {
|
||||
final _cancelToken = CancelToken();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancelToken.cancel('页面已关闭');
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> loadData() async {
|
||||
try {
|
||||
final result = await api.getData(cancelToken: _cancelToken);
|
||||
// 处理结果
|
||||
} on DioException catch (e) {
|
||||
if (!CancelToken.isCancel(e)) {
|
||||
// 处理真正的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GetX Controller 使用 / With GetX Controller
|
||||
|
||||
```dart
|
||||
class MyController extends GetxController {
|
||||
final _cancelToken = CancelToken();
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_cancelToken.cancel('Controller 已关闭');
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future<void> fetchData() async {
|
||||
try {
|
||||
final result = await api.getData(cancelToken: _cancelToken);
|
||||
// 更新状态
|
||||
} on DioException catch (e) {
|
||||
if (!CancelToken.isCancel(e)) {
|
||||
// 显示错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践 / Best Practices
|
||||
|
||||
1. **每个请求使用独立 Token** - 如果需要单独取消某个请求
|
||||
2. **页面级别共享 Token** - 页面离开时取消所有进行中的请求
|
||||
3. **检查取消状态** - 使用 `CancelToken.isCancel(e)` 区分取消和真正的错误
|
||||
4. **及时取消** - 避免不必要的网络请求和资源浪费
|
||||
|
||||
---
|
||||
|
||||
## 注意事项 / Notes
|
||||
|
||||
- CancelToken 来自 `package:dio/dio.dart`,无需额外导入
|
||||
- `@CancelRequest()` 注解来自 `package:retrofit/retrofit.dart`
|
||||
- 取消已完成的请求不会有任何效果
|
||||
- 一个 CancelToken 可以用于多个请求,调用 `cancel()` 会取消所有使用它的请求
|
||||
38
CHANGELOG.md
38
CHANGELOG.md
|
|
@ -2,6 +2,44 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [3.3.0] - 2026-01-31
|
||||
|
||||
### 🎉 新特性
|
||||
|
||||
#### CancelToken 请求取消支持
|
||||
- ✅ **可选配置**:在 `generator_config.yaml` 中通过 `generation.api.cancel_token_support: true` 启用
|
||||
- ✅ **自动参数注入**:为每个生成的 API 方法自动添加 `@CancelRequest() CancelToken? cancelToken` 参数
|
||||
- ✅ **完全可选**:CancelToken 参数可选,不影响现有代码
|
||||
- ✅ **Retrofit 兼容**:生成的代码与 retrofit_generator 完全兼容
|
||||
|
||||
### 📝 文档更新
|
||||
- 新增 [**CancelToken 使用指南**](./CANCELTOKEN_USAGE_GUIDE.md) - 详细配置说明和使用示例
|
||||
- 新增 [**CancelToken 变更日志**](./CHANGELOG_CANCELTOKEN.md) - 功能实现细节
|
||||
|
||||
### 🔧 技术细节
|
||||
- 修改 `lib/core/config_repository.dart`:添加 `cancelTokenSupport` getter
|
||||
- 修改 `lib/pipeline/generate/impl/retrofit_api/api_parameters.dart`:在 `_generateParameters()` 中添加 CancelToken 参数
|
||||
- 更新 `generator_config.template.yaml`:添加 `cancel_token_support` 配置项说明
|
||||
- 新增 `test/cancel_token_support_test.dart`:10 个单元测试验证功能正确性
|
||||
|
||||
### 💡 使用示例
|
||||
```yaml
|
||||
# generator_config.yaml
|
||||
generation:
|
||||
api:
|
||||
cancel_token_support: true
|
||||
```
|
||||
|
||||
生成的代码:
|
||||
```dart
|
||||
Future<Result> getUsers({
|
||||
@Query('page') int? page,
|
||||
@CancelRequest() CancelToken? cancelToken,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [3.2.1] - 2026-01-12
|
||||
|
||||
### 🐛 修复
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# CancelToken 功能变更日志 / CancelToken Feature Changelog
|
||||
|
||||
## [3.3.0] - 2026-01-31
|
||||
|
||||
### 🎉 新特性 / New Features
|
||||
|
||||
#### CancelToken 支持
|
||||
- ✅ **配置开关**:在 `generator_config.yaml` 中配置 `generation.api.cancel_token_support: true` 启用
|
||||
- ✅ **自动添加参数**:启用后,所有 API 方法自动添加 `@CancelRequest() CancelToken? cancelToken` 参数
|
||||
- ✅ **向后兼容**:默认关闭,不影响现有项目
|
||||
|
||||
### 📝 文档 / Documentation
|
||||
- ✅ 新增 `CANCELTOKEN_USAGE_GUIDE.md` - 使用指南和最佳实践
|
||||
- ✅ 更新 `generator_config.template.yaml` - 添加配置示例
|
||||
|
||||
### 🔧 技术细节 / Technical Details
|
||||
|
||||
**修改的文件:**
|
||||
- `lib/core/config_repository.dart` - 添加 `cancelTokenSupport` 配置解析
|
||||
- `lib/pipeline/generate/impl/retrofit_api/api_parameters.dart` - 在 `_generateParameters()` 中添加 CancelToken 参数生成逻辑
|
||||
- `generator_config.template.yaml` - 添加配置项说明
|
||||
|
||||
**生成的参数格式:**
|
||||
```dart
|
||||
@CancelRequest() CancelToken? cancelToken
|
||||
```
|
||||
|
||||
### 💡 使用场景 / Use Cases
|
||||
|
||||
1. **页面离开取消请求** - 避免内存泄漏和不必要的状态更新
|
||||
2. **用户主动取消** - 如搜索框输入时取消之前的搜索请求
|
||||
3. **超时控制** - 配合 Timer 实现自定义超时逻辑
|
||||
4. **批量取消** - 一个 CancelToken 可用于多个请求
|
||||
|
||||
---
|
||||
|
||||
## 相关资源 / Related Resources
|
||||
|
||||
- [Dio CancelToken 文档](https://pub.dev/packages/dio#cancellation)
|
||||
- [Retrofit CancelRequest 注解](https://pub.dev/packages/retrofit)
|
||||
- [使用指南](./CANCELTOKEN_USAGE_GUIDE.md)
|
||||
|
|
@ -115,6 +115,15 @@ generation:
|
|||
# 方法命名
|
||||
method_naming: "camelCase" # camelCase, snake_case
|
||||
|
||||
# CancelToken 支持(可选)
|
||||
# 启用后,为每个 API 方法添加可选的 CancelToken 参数,用于取消请求
|
||||
# 生成的方法签名示例:
|
||||
# Future<Result> getUser({
|
||||
# @Query('id') int? id,
|
||||
# @CancelRequest() CancelToken? cancelToken, // <- 自动添加
|
||||
# });
|
||||
cancel_token_support: false # 默认: false
|
||||
|
||||
# 数据模型配置
|
||||
models:
|
||||
enabled: true
|
||||
|
|
|
|||
|
|
@ -432,6 +432,14 @@ class ConfigRepository {
|
|||
return output?['split_by_tags'] as bool? ?? true;
|
||||
}
|
||||
|
||||
/// 获取是否启用 CancelToken 支持
|
||||
/// 启用后,每个 API 方法都会添加可选的 CancelToken 参数
|
||||
bool get cancelTokenSupport {
|
||||
final generation = _config['generation'] as Map<String, dynamic>?;
|
||||
final api = generation?['api'] as Map<String, dynamic>?;
|
||||
return api?['cancel_token_support'] as bool? ?? false;
|
||||
}
|
||||
|
||||
/// 获取额外的包导入列表
|
||||
List<String> get packageImports {
|
||||
final imports = _config['imports'] as Map<String, dynamic>?;
|
||||
|
|
|
|||
|
|
@ -96,6 +96,20 @@ mixin RetrofitApiParameters {
|
|||
);
|
||||
}
|
||||
|
||||
// 添加 CancelToken 参数(如果配置启用)
|
||||
final config = ConfigRepository.loadSync();
|
||||
if (config.cancelTokenSupport) {
|
||||
parameters.add(
|
||||
ApiMethodParameter(
|
||||
name: 'cancelToken',
|
||||
type: 'CancelToken?',
|
||||
annotation: _g.useRetrofit ? '@CancelRequest()' : '',
|
||||
required: false,
|
||||
description: 'Cancel token for request cancellation',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
name: swagger_generator_flutter
|
||||
description: A powerful Swagger/OpenAPI code generator for Flutter projects with Dio + Retrofit support
|
||||
|
||||
version: 3.2.1
|
||||
version: 3.3.0
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,272 @@
|
|||
import 'package:swagger_generator_flutter/core/config_repository.dart';
|
||||
import 'package:swagger_generator_flutter/core/models.dart';
|
||||
import 'package:swagger_generator_flutter/pipeline/generate/apis.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('CancelToken Support Tests', () {
|
||||
late SwaggerDocument simpleDocument;
|
||||
|
||||
setUp(() {
|
||||
// 清除缓存以确保每个测试使用新配置
|
||||
ConfigRepository.setCachedConfig(ConfigRepository({}));
|
||||
|
||||
simpleDocument = SwaggerDocument(
|
||||
title: 'Test API',
|
||||
version: '1.0.0',
|
||||
description: 'A test API for request cancellation',
|
||||
servers: [
|
||||
const ApiServer(
|
||||
url: 'https://api.example.com',
|
||||
description: 'Test server',
|
||||
),
|
||||
],
|
||||
paths: {
|
||||
const ApiPathKey('/users', HttpMethod.get): const ApiPath(
|
||||
path: '/users',
|
||||
method: HttpMethod.get,
|
||||
summary: 'Get users',
|
||||
description: 'Get all users',
|
||||
operationId: 'getUsers',
|
||||
tags: ['users'],
|
||||
parameters: [
|
||||
ApiParameter(
|
||||
name: 'page',
|
||||
location: ParameterLocation.query,
|
||||
required: false,
|
||||
type: PropertyType.integer,
|
||||
description: 'Page number',
|
||||
),
|
||||
],
|
||||
responses: {
|
||||
'200': ApiResponse(
|
||||
code: '200',
|
||||
description: 'Success',
|
||||
),
|
||||
},
|
||||
),
|
||||
const ApiPathKey('/users/{id}', HttpMethod.get): const ApiPath(
|
||||
path: '/users/{id}',
|
||||
method: HttpMethod.get,
|
||||
summary: 'Get user by ID',
|
||||
description: 'Get a specific user',
|
||||
operationId: 'getUserById',
|
||||
tags: ['users'],
|
||||
parameters: [
|
||||
ApiParameter(
|
||||
name: 'id',
|
||||
location: ParameterLocation.path,
|
||||
required: true,
|
||||
type: PropertyType.integer,
|
||||
description: 'User ID',
|
||||
),
|
||||
],
|
||||
responses: {
|
||||
'200': ApiResponse(
|
||||
code: '200',
|
||||
description: 'User found',
|
||||
),
|
||||
},
|
||||
),
|
||||
const ApiPathKey('/users', HttpMethod.post): const ApiPath(
|
||||
path: '/users',
|
||||
method: HttpMethod.post,
|
||||
summary: 'Create user',
|
||||
description: 'Create a new user',
|
||||
operationId: 'createUser',
|
||||
tags: ['users'],
|
||||
parameters: [],
|
||||
requestBody: ApiRequestBody(
|
||||
description: 'User data',
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': ApiMediaType(
|
||||
schema: {'type': 'object'},
|
||||
),
|
||||
},
|
||||
),
|
||||
responses: {
|
||||
'201': ApiResponse(
|
||||
code: '201',
|
||||
description: 'User created',
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
models: {},
|
||||
controllers: {},
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
// 清除缓存
|
||||
ConfigRepository.setCachedConfig(ConfigRepository({}));
|
||||
});
|
||||
|
||||
group('CancelToken 参数生成测试', () {
|
||||
test('当 cancelTokenSupport 启用时,生成 CancelToken 参数', () {
|
||||
// 设置配置启用 CancelToken
|
||||
ConfigRepository.setCachedConfig(
|
||||
ConfigRepository({
|
||||
'generation': {
|
||||
'api': {
|
||||
'cancel_token_support': true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
final generator = RetrofitApiGenerator(
|
||||
className: 'TestApi',
|
||||
splitByTags: false,
|
||||
);
|
||||
|
||||
final result = generator.generateFromDocument(simpleDocument);
|
||||
|
||||
// 验证生成的代码包含 CancelToken 参数(使用更精确的匹配)
|
||||
expect(result, contains('CancelToken? cancelToken'));
|
||||
expect(result, contains('@CancelRequest()'));
|
||||
});
|
||||
|
||||
test('当 cancelTokenSupport 禁用时,不生成 CancelToken 参数', () {
|
||||
// 设置配置禁用 CancelToken
|
||||
ConfigRepository.setCachedConfig(
|
||||
ConfigRepository({
|
||||
'generation': {
|
||||
'api': {
|
||||
'cancel_token_support': false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
final generator = RetrofitApiGenerator(
|
||||
className: 'TestApi',
|
||||
splitByTags: false,
|
||||
);
|
||||
|
||||
final result = generator.generateFromDocument(simpleDocument);
|
||||
|
||||
// 验证生成的代码不包含 CancelToken 参数(使用更精确的匹配)
|
||||
expect(result, isNot(contains('CancelToken? cancelToken')));
|
||||
expect(result, isNot(contains('@CancelRequest()')));
|
||||
});
|
||||
|
||||
test('默认情况下不生成 CancelToken 参数', () {
|
||||
// 使用默认配置(空配置)
|
||||
ConfigRepository.setCachedConfig(ConfigRepository({}));
|
||||
|
||||
final generator = RetrofitApiGenerator(
|
||||
className: 'TestApi',
|
||||
splitByTags: false,
|
||||
);
|
||||
|
||||
final result = generator.generateFromDocument(simpleDocument);
|
||||
|
||||
// 验证默认情况下不生成 CancelToken 参数
|
||||
expect(result, isNot(contains('CancelToken? cancelToken')));
|
||||
expect(result, isNot(contains('@CancelRequest()')));
|
||||
});
|
||||
|
||||
test('CancelToken 参数应该是可选的', () {
|
||||
ConfigRepository.setCachedConfig(
|
||||
ConfigRepository({
|
||||
'generation': {
|
||||
'api': {
|
||||
'cancel_token_support': true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
final generator = RetrofitApiGenerator(
|
||||
className: 'TestApi',
|
||||
splitByTags: false,
|
||||
);
|
||||
|
||||
final result = generator.generateFromDocument(simpleDocument);
|
||||
|
||||
// 验证 CancelToken 参数是可选的(使用 ? 表示)
|
||||
expect(result, contains('CancelToken?'));
|
||||
// 验证不是必选参数
|
||||
expect(result, isNot(contains('required CancelToken')));
|
||||
});
|
||||
|
||||
test('所有 API 方法都应包含 CancelToken 参数', () {
|
||||
ConfigRepository.setCachedConfig(
|
||||
ConfigRepository({
|
||||
'generation': {
|
||||
'api': {
|
||||
'cancel_token_support': true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
final generator = RetrofitApiGenerator(
|
||||
className: 'TestApi',
|
||||
splitByTags: false,
|
||||
);
|
||||
|
||||
final result = generator.generateFromDocument(simpleDocument);
|
||||
|
||||
// 统计 CancelToken 参数出现的次数(应该等于 API 方法数量)
|
||||
final cancelTokenCount =
|
||||
'CancelToken? cancelToken'.allMatches(result).length;
|
||||
|
||||
// 我们有 3 个 API 方法(getUsers, getUserById, createUser)
|
||||
expect(cancelTokenCount, equals(3));
|
||||
});
|
||||
});
|
||||
|
||||
group('ConfigRepository CancelToken 配置测试', () {
|
||||
test('cancelTokenSupport getter 正确解析 true', () {
|
||||
final config = ConfigRepository({
|
||||
'generation': {
|
||||
'api': {
|
||||
'cancel_token_support': true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(config.cancelTokenSupport, isTrue);
|
||||
});
|
||||
|
||||
test('cancelTokenSupport getter 正确解析 false', () {
|
||||
final config = ConfigRepository({
|
||||
'generation': {
|
||||
'api': {
|
||||
'cancel_token_support': false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(config.cancelTokenSupport, isFalse);
|
||||
});
|
||||
|
||||
test('cancelTokenSupport getter 默认为 false', () {
|
||||
final config = ConfigRepository({});
|
||||
|
||||
expect(config.cancelTokenSupport, isFalse);
|
||||
});
|
||||
|
||||
test('cancelTokenSupport getter 处理部分配置', () {
|
||||
final config = ConfigRepository(<String, dynamic>{
|
||||
'generation': <String, dynamic>{
|
||||
'api': <String, dynamic>{},
|
||||
},
|
||||
});
|
||||
|
||||
expect(config.cancelTokenSupport, isFalse);
|
||||
});
|
||||
|
||||
test('cancelTokenSupport getter 处理缺少 api 配置', () {
|
||||
final config = ConfigRepository(<String, dynamic>{
|
||||
'generation': <String, dynamic>{},
|
||||
});
|
||||
|
||||
expect(config.cancelTokenSupport, isFalse);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue