@gogenger/go-gen 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.en.md +151 -0
- package/README.md +151 -0
- package/bin/index.js +86 -0
- package/core/config.js +176 -0
- package/core/fetch-mode.js +400 -0
- package/core/openapi-mode.js +328 -0
- package/core/quicktype.js +31 -0
- package/core/writer.js +413 -0
- package/docs/BEST_PRACTICES.md +327 -0
- package/docs/CONFIGURATION.md +161 -0
- package/docs/FEATURES.md +136 -0
- package/docs/TROUBLESHOOTING.md +386 -0
- package/docs/USE_CASES.md +247 -0
- package/package.json +68 -0
- package/utils/load-openapi.js +23 -0
- package/utils/name.js +17 -0
- package/utils/sampler.js +28 -0
package/core/writer.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const shell = require('shelljs');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const { loadConfig } = require('./config');
|
|
9
|
+
|
|
10
|
+
const desktopPath = path.join(os.homedir(), 'Desktop');
|
|
11
|
+
const currentPath = process.cwd();
|
|
12
|
+
|
|
13
|
+
function generateApiFile({
|
|
14
|
+
apiName,
|
|
15
|
+
typeName,
|
|
16
|
+
url,
|
|
17
|
+
method = 'GET',
|
|
18
|
+
hasRequestBody = false,
|
|
19
|
+
}) {
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
const requestModule = config.requestModule;
|
|
22
|
+
const finalTypeName = typeName.charAt(0).toUpperCase() + typeName.slice(1);
|
|
23
|
+
const methodLower = method.toLowerCase();
|
|
24
|
+
|
|
25
|
+
let imports = `import type { ${finalTypeName}`;
|
|
26
|
+
let params = '';
|
|
27
|
+
let requestCall = '';
|
|
28
|
+
|
|
29
|
+
if (hasRequestBody) {
|
|
30
|
+
imports += `, ${finalTypeName}Request`;
|
|
31
|
+
params = `data: ${finalTypeName}Request`;
|
|
32
|
+
requestCall = `request.${methodLower}<${finalTypeName}>("${url}", data)`;
|
|
33
|
+
} else {
|
|
34
|
+
requestCall = `request.${methodLower}<${finalTypeName}>("${url}")`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
imports += ` } from "./types";`;
|
|
38
|
+
|
|
39
|
+
return `
|
|
40
|
+
import request from "${requestModule}";
|
|
41
|
+
${imports}
|
|
42
|
+
|
|
43
|
+
export function ${apiName}(${params}) {
|
|
44
|
+
return ${requestCall};
|
|
45
|
+
}
|
|
46
|
+
`.trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseExistingTypes(typesFilePath) {
|
|
50
|
+
if (!fs.existsSync(typesFilePath)) {
|
|
51
|
+
return { types: [], content: '' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const content = fs.readFileSync(typesFilePath, 'utf-8');
|
|
55
|
+
const typeRegex = /export\s+(?:interface|type)\s+(\w+)/g;
|
|
56
|
+
const types = [];
|
|
57
|
+
let match;
|
|
58
|
+
|
|
59
|
+
while ((match = typeRegex.exec(content)) !== null) {
|
|
60
|
+
types.push(match[1]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { types, content };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseExistingApis(apiFilePath) {
|
|
67
|
+
if (!fs.existsSync(apiFilePath)) {
|
|
68
|
+
return { functions: [], content: '' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const content = fs.readFileSync(apiFilePath, 'utf-8');
|
|
72
|
+
const funcRegex = /export\s+function\s+(\w+)\s*\(/g;
|
|
73
|
+
const functions = [];
|
|
74
|
+
let match;
|
|
75
|
+
|
|
76
|
+
while ((match = funcRegex.exec(content)) !== null) {
|
|
77
|
+
functions.push(match[1]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { functions, content };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractTypeDefinitions(newTypesContent) {
|
|
84
|
+
const lines = newTypesContent.split('\n');
|
|
85
|
+
const definitions = [];
|
|
86
|
+
let currentDef = [];
|
|
87
|
+
let inDefinition = false;
|
|
88
|
+
let braceCount = 0;
|
|
89
|
+
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
if (/export\s+(?:interface|type)\s+\w+/.test(line)) {
|
|
92
|
+
inDefinition = true;
|
|
93
|
+
currentDef = [line];
|
|
94
|
+
braceCount =
|
|
95
|
+
(line.match(/{/g) || []).length - (line.match(/}/g) || []).length;
|
|
96
|
+
|
|
97
|
+
if (braceCount === 0 && line.includes('=')) {
|
|
98
|
+
definitions.push(currentDef.join('\n'));
|
|
99
|
+
inDefinition = false;
|
|
100
|
+
currentDef = [];
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (inDefinition) {
|
|
106
|
+
currentDef.push(line);
|
|
107
|
+
braceCount += (line.match(/{/g) || []).length;
|
|
108
|
+
braceCount -= (line.match(/}/g) || []).length;
|
|
109
|
+
|
|
110
|
+
if (braceCount === 0) {
|
|
111
|
+
definitions.push(currentDef.join('\n'));
|
|
112
|
+
inDefinition = false;
|
|
113
|
+
currentDef = [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return definitions;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveTypeNameConflict(existingTypes, typeName) {
|
|
122
|
+
let finalTypeName = typeName;
|
|
123
|
+
let suffix = 1;
|
|
124
|
+
|
|
125
|
+
while (existingTypes.includes(finalTypeName)) {
|
|
126
|
+
finalTypeName = `${typeName}${suffix}`;
|
|
127
|
+
suffix++;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { finalTypeName, hasConflict: finalTypeName !== typeName };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function mergeTypesContent(existingContent, newTypesContent, typeName) {
|
|
134
|
+
const typeRegex = /export\s+(?:interface|type)\s+(\w+)/g;
|
|
135
|
+
const existingTypes = [];
|
|
136
|
+
let match;
|
|
137
|
+
|
|
138
|
+
while ((match = typeRegex.exec(existingContent)) !== null) {
|
|
139
|
+
existingTypes.push(match[1]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { finalTypeName, hasConflict } = resolveTypeNameConflict(
|
|
143
|
+
existingTypes,
|
|
144
|
+
typeName,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (hasConflict) {
|
|
148
|
+
newTypesContent = newTypesContent.replace(
|
|
149
|
+
new RegExp(`\\b${typeName}\\b`, 'g'),
|
|
150
|
+
finalTypeName,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const newDefinitions = extractTypeDefinitions(newTypesContent);
|
|
155
|
+
|
|
156
|
+
const uniqueDefinitions = newDefinitions.filter(def => {
|
|
157
|
+
const typeMatch = def.match(/export\s+(?:interface|type)\s+(\w+)/);
|
|
158
|
+
if (!typeMatch) return false;
|
|
159
|
+
return !existingTypes.includes(typeMatch[1]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (uniqueDefinitions.length === 0) {
|
|
163
|
+
return { merged: existingContent, isDuplicate: true, finalTypeName };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 确保有换行分隔
|
|
167
|
+
const merged =
|
|
168
|
+
existingContent.trim() + '\n\n' + uniqueDefinitions.join('\n\n');
|
|
169
|
+
|
|
170
|
+
return { merged, isDuplicate: false, finalTypeName, hasConflict };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function extractImportedTypes(apiContent) {
|
|
174
|
+
const importMatch = apiContent.match(/import\s+type\s+{\s*([^}]+)\s*}/);
|
|
175
|
+
if (!importMatch) return [];
|
|
176
|
+
|
|
177
|
+
return importMatch[1]
|
|
178
|
+
.split(',')
|
|
179
|
+
.map(t => t.trim())
|
|
180
|
+
.filter(Boolean);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function mergeApiContent(existingContent, newApiContent, newApiName) {
|
|
184
|
+
const funcRegex = /export\s+function\s+(\w+)\s*\(/g;
|
|
185
|
+
const existingFunctions = [];
|
|
186
|
+
let match;
|
|
187
|
+
|
|
188
|
+
while ((match = funcRegex.exec(existingContent)) !== null) {
|
|
189
|
+
existingFunctions.push(match[1]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (existingFunctions.includes(newApiName)) {
|
|
193
|
+
return { merged: existingContent, isDuplicate: true };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const newTypes = extractImportedTypes(newApiContent);
|
|
197
|
+
|
|
198
|
+
const existingImportMatch = existingContent.match(
|
|
199
|
+
/import\s+type\s+{\s*([^}]+)\s*}/,
|
|
200
|
+
);
|
|
201
|
+
const existingTypes = existingImportMatch
|
|
202
|
+
? existingImportMatch[1]
|
|
203
|
+
.split(',')
|
|
204
|
+
.map(t => t.trim())
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
: [];
|
|
207
|
+
|
|
208
|
+
const allTypes = [...new Set([...existingTypes, ...newTypes])];
|
|
209
|
+
|
|
210
|
+
const newFunctionMatch = newApiContent.match(/(export\s+function[\s\S]+)/);
|
|
211
|
+
const newFunction = newFunctionMatch ? newFunctionMatch[1] : '';
|
|
212
|
+
|
|
213
|
+
let merged = existingContent;
|
|
214
|
+
|
|
215
|
+
if (allTypes.length > existingTypes.length) {
|
|
216
|
+
const importStatement = `import type { ${allTypes.join(', ')} } from "./types";`;
|
|
217
|
+
merged = merged.replace(
|
|
218
|
+
/import\s+type\s+{[^}]+}\s+from\s+"\.\/types";/,
|
|
219
|
+
importStatement,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (newFunction) {
|
|
224
|
+
merged = merged.trim() + '\n\n' + newFunction;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { merged, isDuplicate: false };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function validatePath(inputPath) {
|
|
231
|
+
const resolved = path.resolve(inputPath);
|
|
232
|
+
|
|
233
|
+
// Windows 系统路径(C:\Windows, C:\Program Files 等)
|
|
234
|
+
const windowsDangerousPaths = [
|
|
235
|
+
'C:\\Windows',
|
|
236
|
+
'C:\\Program Files',
|
|
237
|
+
'C:\\System',
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
// Unix/Linux 系统路径
|
|
241
|
+
const unixDangerousPaths = ['/System', '/usr', '/bin', '/sbin', '/etc'];
|
|
242
|
+
|
|
243
|
+
// 检查 Windows 路径
|
|
244
|
+
for (const dangerousPath of windowsDangerousPaths) {
|
|
245
|
+
if (resolved.toUpperCase().startsWith(dangerousPath.toUpperCase())) {
|
|
246
|
+
throw new Error('⛔ 不允许写入系统目录');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 检查 Unix 路径
|
|
251
|
+
for (const dangerousPath of unixDangerousPaths) {
|
|
252
|
+
if (resolved.startsWith(dangerousPath)) {
|
|
253
|
+
throw new Error('⛔ 不允许写入系统目录');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return resolved;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function writeFiles({
|
|
261
|
+
apiName,
|
|
262
|
+
typeName,
|
|
263
|
+
url,
|
|
264
|
+
typesContent,
|
|
265
|
+
method = 'GET',
|
|
266
|
+
hasRequestBody = false,
|
|
267
|
+
interactive = true,
|
|
268
|
+
}) {
|
|
269
|
+
const config = loadConfig();
|
|
270
|
+
|
|
271
|
+
let baseDir;
|
|
272
|
+
|
|
273
|
+
if (interactive) {
|
|
274
|
+
const { outputPath } = await inquirer.prompt([
|
|
275
|
+
{
|
|
276
|
+
type: 'list',
|
|
277
|
+
name: 'outputPath',
|
|
278
|
+
message: '📂 输出目录:',
|
|
279
|
+
default: config.defaultOutputPath,
|
|
280
|
+
choices: [
|
|
281
|
+
{ name: '💻 桌面', value: desktopPath },
|
|
282
|
+
{ name: '📁 当前目录', value: currentPath },
|
|
283
|
+
{ name: '🔍 自定义路径', value: 'custom' },
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
baseDir = outputPath;
|
|
289
|
+
if (outputPath === 'custom') {
|
|
290
|
+
const { customPath } = await inquirer.prompt([
|
|
291
|
+
{
|
|
292
|
+
type: 'input',
|
|
293
|
+
name: 'customPath',
|
|
294
|
+
message: '📁 请输入保存路径:',
|
|
295
|
+
default: config.customPath || currentPath,
|
|
296
|
+
validate: input => {
|
|
297
|
+
try {
|
|
298
|
+
validatePath(input);
|
|
299
|
+
return shell.test('-d', input) || '路径不存在';
|
|
300
|
+
} catch (error) {
|
|
301
|
+
return error.message;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
]);
|
|
306
|
+
baseDir = customPath;
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
baseDir = currentPath;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const outputDir = path.join(baseDir, apiName);
|
|
313
|
+
const dirExists = fs.existsSync(outputDir);
|
|
314
|
+
|
|
315
|
+
if (dirExists && interactive) {
|
|
316
|
+
console.log(chalk.yellow(`\n📁 目录已存在,将进行增量写入: ${outputDir}`));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
shell.mkdir('-p', outputDir);
|
|
320
|
+
|
|
321
|
+
const typesFilePath = path.join(outputDir, 'types.ts');
|
|
322
|
+
const apiFilePath = path.join(outputDir, 'api.ts');
|
|
323
|
+
|
|
324
|
+
let finalTypesContent = typesContent;
|
|
325
|
+
let finalTypeName = typeName;
|
|
326
|
+
let typeSkipped = false;
|
|
327
|
+
let typeConflict = false;
|
|
328
|
+
|
|
329
|
+
if (fs.existsSync(typesFilePath)) {
|
|
330
|
+
const existingTypes = fs.readFileSync(typesFilePath, 'utf-8');
|
|
331
|
+
const {
|
|
332
|
+
merged,
|
|
333
|
+
isDuplicate,
|
|
334
|
+
finalTypeName: resolvedName,
|
|
335
|
+
hasConflict,
|
|
336
|
+
} = mergeTypesContent(existingTypes, typesContent, typeName);
|
|
337
|
+
|
|
338
|
+
if (hasConflict && interactive) {
|
|
339
|
+
console.log(
|
|
340
|
+
chalk.yellow(
|
|
341
|
+
`⚠️ 类型名冲突,已自动重命名: ${typeName} → ${resolvedName}`,
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
typeConflict = true;
|
|
345
|
+
finalTypeName = resolvedName;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (isDuplicate && !hasConflict && interactive) {
|
|
349
|
+
console.log(chalk.yellow(`⚠️ 类型 ${typeName} 已存在,跳过写入`));
|
|
350
|
+
typeSkipped = true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
finalTypesContent = merged;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fs.writeFileSync(typesFilePath, finalTypesContent);
|
|
357
|
+
|
|
358
|
+
const newApiContent = generateApiFile({
|
|
359
|
+
apiName,
|
|
360
|
+
typeName: finalTypeName,
|
|
361
|
+
url,
|
|
362
|
+
method,
|
|
363
|
+
hasRequestBody,
|
|
364
|
+
});
|
|
365
|
+
let finalApiContent = newApiContent;
|
|
366
|
+
let apiSkipped = false;
|
|
367
|
+
|
|
368
|
+
if (fs.existsSync(apiFilePath)) {
|
|
369
|
+
const existingApi = fs.readFileSync(apiFilePath, 'utf-8');
|
|
370
|
+
const { merged, isDuplicate } = mergeApiContent(
|
|
371
|
+
existingApi,
|
|
372
|
+
newApiContent,
|
|
373
|
+
apiName,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (isDuplicate && interactive) {
|
|
377
|
+
console.log(chalk.yellow(`⚠️ API 函数 ${apiName} 已存在,跳过写入`));
|
|
378
|
+
apiSkipped = true;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
finalApiContent = merged;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fs.writeFileSync(apiFilePath, finalApiContent);
|
|
385
|
+
|
|
386
|
+
if (interactive) {
|
|
387
|
+
const spinner = ora();
|
|
388
|
+
spinner.text = chalk.cyan('📂 输出目录:') + outputDir;
|
|
389
|
+
|
|
390
|
+
if (typeSkipped && apiSkipped) {
|
|
391
|
+
spinner.warn('⚠️ 内容已存在,无新增内容');
|
|
392
|
+
} else if (typeConflict) {
|
|
393
|
+
spinner.succeed(`✨ 生成成功!(类型已重命名为 ${finalTypeName})`);
|
|
394
|
+
} else if (dirExists) {
|
|
395
|
+
spinner.succeed('✨ 增量写入成功!');
|
|
396
|
+
} else {
|
|
397
|
+
spinner.succeed('🎉 文件生成成功!');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return { success: true, outputDir, finalTypeName };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = {
|
|
405
|
+
writeFiles,
|
|
406
|
+
generateApiFile,
|
|
407
|
+
parseExistingTypes,
|
|
408
|
+
parseExistingApis,
|
|
409
|
+
mergeTypesContent,
|
|
410
|
+
mergeApiContent,
|
|
411
|
+
resolveTypeNameConflict,
|
|
412
|
+
validatePath,
|
|
413
|
+
};
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# 🎓 最佳实践
|
|
2
|
+
|
|
3
|
+
## 1. 团队规范化
|
|
4
|
+
|
|
5
|
+
### 推荐做法
|
|
6
|
+
|
|
7
|
+
在项目根目录创建 `.apirc.json`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"requestModule": "@/api/request",
|
|
12
|
+
"typePrefix": "I",
|
|
13
|
+
"apiPrefix": "api"
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 优势
|
|
18
|
+
|
|
19
|
+
- ✅ 团队成员自动使用统一配置
|
|
20
|
+
- ✅ 新成员无需额外培训
|
|
21
|
+
- ✅ 代码风格高度一致
|
|
22
|
+
- ✅ 减少 Code Review 时间
|
|
23
|
+
|
|
24
|
+
### 提交规范
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git add .apirc.json
|
|
28
|
+
git commit -m "chore: add api generator config"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. 命名规范
|
|
34
|
+
|
|
35
|
+
### 类型名:PascalCase + Response 后缀
|
|
36
|
+
|
|
37
|
+
✅ **推荐:**
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
UserResponse;
|
|
41
|
+
CreateOrderResponse;
|
|
42
|
+
UpdateProfileResponse;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
❌ **不推荐:**
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
userResponse; // 首字母小写
|
|
49
|
+
User; // 缺少后缀
|
|
50
|
+
user_response; // 下划线命名
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### API 方法名:camelCase + 动词前缀
|
|
54
|
+
|
|
55
|
+
✅ **推荐:**
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
getUsers();
|
|
59
|
+
createOrder();
|
|
60
|
+
updateUserProfile();
|
|
61
|
+
deletePost();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
❌ **不推荐:**
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
Users(); // 缺少动词
|
|
68
|
+
GetUsers(); // 首字母大写
|
|
69
|
+
user_list(); // 下划线命名
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 动词建议
|
|
73
|
+
|
|
74
|
+
| HTTP 方法 | 推荐动词前缀 |
|
|
75
|
+
| --------- | ---------------------- |
|
|
76
|
+
| GET | get, fetch, query |
|
|
77
|
+
| POST | create, add |
|
|
78
|
+
| PUT | update, replace |
|
|
79
|
+
| PATCH | update, modify |
|
|
80
|
+
| DELETE | delete, remove, revoke |
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 3. 目录组织
|
|
85
|
+
|
|
86
|
+
### 推荐结构
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
src/api/
|
|
90
|
+
├── user/
|
|
91
|
+
│ ├── api.ts # 用户相关 API
|
|
92
|
+
│ └── types.ts # 用户相关类型
|
|
93
|
+
├── order/
|
|
94
|
+
│ ├── api.ts # 订单相关 API
|
|
95
|
+
│ └── types.ts # 订单相关类型
|
|
96
|
+
├── product/
|
|
97
|
+
│ ├── api.ts # 产品相关 API
|
|
98
|
+
│ └── types.ts # 产品相关类型
|
|
99
|
+
└── common/
|
|
100
|
+
├── api.ts # 通用 API
|
|
101
|
+
└── types.ts # 通用类型
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 优势
|
|
105
|
+
|
|
106
|
+
- ✅ 模块化清晰
|
|
107
|
+
- ✅ 便于维护和查找
|
|
108
|
+
- ✅ 减少命名冲突
|
|
109
|
+
- ✅ 支持按需导入
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 4. 版本控制
|
|
114
|
+
|
|
115
|
+
### 应该提交的文件
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# ✅ 生成的代码(推荐提交)
|
|
119
|
+
git add src/api/
|
|
120
|
+
|
|
121
|
+
# ✅ 项目配置(强烈推荐)
|
|
122
|
+
git add .apirc.json
|
|
123
|
+
|
|
124
|
+
# ✅ 文档更新
|
|
125
|
+
git add docs/
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 不应该提交的文件
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# ❌ 全局配置(个人配置)
|
|
132
|
+
# ~/.apirc.json 不要提交
|
|
133
|
+
|
|
134
|
+
# ❌ 临时文件
|
|
135
|
+
# *.tmp, *.bak 等
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Commit 规范
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# 新增接口
|
|
142
|
+
git commit -m "feat: add user api"
|
|
143
|
+
|
|
144
|
+
# 更新接口
|
|
145
|
+
git commit -m "feat: update order api"
|
|
146
|
+
|
|
147
|
+
# 添加配置
|
|
148
|
+
git commit -m "chore: add api-gen config"
|
|
149
|
+
|
|
150
|
+
# 修复问题
|
|
151
|
+
git commit -m "fix: correct api type definition"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 5. 类型安全
|
|
157
|
+
|
|
158
|
+
### 使用泛型
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// ✅ 推荐:使用泛型
|
|
162
|
+
export function getUsers() {
|
|
163
|
+
return request.get<UserResponse>('/api/users');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ❌ 不推荐:不指定类型
|
|
167
|
+
export function getUsers() {
|
|
168
|
+
return request.get('/api/users');
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 避免 any
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// ✅ 推荐:明确类型
|
|
176
|
+
export interface CreateUserRequest {
|
|
177
|
+
name: string;
|
|
178
|
+
email: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ❌ 不推荐:使用 any
|
|
182
|
+
export interface CreateUserRequest {
|
|
183
|
+
[key: string]: any;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 6. 错误处理
|
|
190
|
+
|
|
191
|
+
### 统一错误处理
|
|
192
|
+
|
|
193
|
+
在 `request` 模块中统一处理:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// @/utils/request.ts
|
|
197
|
+
import axios from 'axios';
|
|
198
|
+
|
|
199
|
+
const request = axios.create({
|
|
200
|
+
baseURL: '/api',
|
|
201
|
+
timeout: 10000,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// 统一错误处理
|
|
205
|
+
request.interceptors.response.use(
|
|
206
|
+
response => response.data,
|
|
207
|
+
error => {
|
|
208
|
+
console.error('API Error:', error);
|
|
209
|
+
return Promise.reject(error);
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
export default request;
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 7. 增量开发
|
|
219
|
+
|
|
220
|
+
### 新增接口时
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# 输出到已存在的目录
|
|
224
|
+
go-gen fetch
|
|
225
|
+
# 选择相同的输出目录
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 注意事项
|
|
229
|
+
|
|
230
|
+
- ✅ 新接口会自动追加
|
|
231
|
+
- ✅ 已有代码不会被覆盖
|
|
232
|
+
- ⚠️ 类型名冲突会自动重命名
|
|
233
|
+
- ⚠️ 检查是否有重复的 API 方法
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 8. 性能优化
|
|
238
|
+
|
|
239
|
+
### 批量生成时
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# OpenAPI 批量模式
|
|
243
|
+
go-gen openapi https://api.example.com/swagger.json
|
|
244
|
+
# 选择 "批量生成"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 单个生成时
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
# 只生成需要的接口
|
|
251
|
+
go-gen fetch
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### 建议
|
|
255
|
+
|
|
256
|
+
- ✅ 大型项目用批量模式
|
|
257
|
+
- ✅ 小型项目或单个接口用 fetch 模式
|
|
258
|
+
- ✅ 定期清理不用的接口
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 9. 团队协作流程
|
|
263
|
+
|
|
264
|
+
### 步骤 1:项目负责人
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# 初始化配置
|
|
268
|
+
go-gen init
|
|
269
|
+
|
|
270
|
+
# 编辑 .apirc.json
|
|
271
|
+
vim .apirc.json
|
|
272
|
+
|
|
273
|
+
# 提交配置
|
|
274
|
+
git add .apirc.json
|
|
275
|
+
git commit -m "chore: setup api generator config"
|
|
276
|
+
git push
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 步骤 2:团队成员
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
# 拉取配置
|
|
283
|
+
git pull
|
|
284
|
+
|
|
285
|
+
# 直接使用
|
|
286
|
+
go-gen fetch
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 步骤 3:代码审查
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# 检查生成的代码
|
|
293
|
+
git diff src/api/
|
|
294
|
+
|
|
295
|
+
# 确认无误后提交
|
|
296
|
+
git add src/api/
|
|
297
|
+
git commit -m "feat: add new api endpoints"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## 10. 文档维护
|
|
303
|
+
|
|
304
|
+
### 在 README 中说明
|
|
305
|
+
|
|
306
|
+
```markdown
|
|
307
|
+
## API 生成
|
|
308
|
+
|
|
309
|
+
本项目使用 `go-gen` 生成 API 代码。
|
|
310
|
+
|
|
311
|
+
### 生成新接口
|
|
312
|
+
|
|
313
|
+
\`\`\`bash
|
|
314
|
+
go-gen fetch
|
|
315
|
+
\`\`\`
|
|
316
|
+
|
|
317
|
+
### 配置
|
|
318
|
+
|
|
319
|
+
查看 `.apirc.json` 了解项目配置。
|
|
320
|
+
\`\`\`
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### 保持文档同步
|
|
324
|
+
|
|
325
|
+
- 更新 API 时更新文档
|
|
326
|
+
- 记录重要的配置变更
|
|
327
|
+
- 说明特殊的使用方式
|