身為一個前端開發,在開發ts專案時,最繁瑣的工作應該就是手寫介面的資料型別和mock資料,因為這部分工作如果不做,後面寫業務邏輯難受,做的話全是複製貼上類似的重複工作,還挺費時間。下文將給大家介紹一個自動生成ts型別和mock資料的方法,幫助同學們從繁瑣得工作中解脫出來。
下面我們將通過一個示例,讓大家一起了解一下程式碼生成的基本過程。
TS程式碼生成基本流程
我們以下面這段ts程式碼為例,一起過一下生成它的基本流程。
export interface TestA {
age: number;
name: string;
other?: boolean;
friends: {
sex?: 1 | 2;
},
cats: number[];
}
第一步:選定資料來源
我們先思考一個問題,把上述程式碼寫的interface生成需要哪些資訊?
通過分析,我們首先需要知道它一共有幾個屬性,然後要知道哪些屬性是必須的,除此以外還需要知道每個屬性的型別、列舉等資訊。有一種資料格式可以完美的給我們提供我們所需要的資料,它就是JSON Schema。
接觸過後端的同學應該都瞭解過JSON Schema,它是對JSON資料的描述,舉個例子,我們定義了下面這個JSON結構:
{
"age": 1,
"name": "測試",
"friends": {
"sex": 1
},
"cats": [
1,
2,
3
],
"other": true
}
我們口頭描述下這個json:它有age、name、friends、cats、other5個屬性,age屬性的型別是number,name屬性的型別是string,cats屬性的型別是number組成的arry,friends屬性是一個object,它有一個sex屬性,型別是數字,other屬性的型別是boolean。
用JSON Schema的描述如下:
{
"type": "object",
"properties": {
"age": {
"type": "number"
},
"name": {
"type": "string"
},
"cats": {
"type": "array",
"items": {
"type": "number"
}
},
"friends": {
"type": "object",
"properties": {
"sex": {
"type": "number"
},
"required": [
"e"
]
}
},
"other": {
"type": "boolean",
},
"required": [
"a",
"b"
]
}
}
可以看出JSON Schema可以完美的程式化實現我們的口頭描述,這個例子比較簡單,JSON Schema的描述能力遠不止於此,比如列舉,陣列的最大長度,數字的最大最小值,是否是必須的等我們常用的屬性都能精確描述,所以它也常用於使用者輸入校驗的場景。
第二步:選定程式碼生成工具
看到這個標題,相信大多數同學都已經知道了答案,沒錯,就是TS AST和TS Compiler API,後者可以生成或者修改TS AST,也可以輸出編譯後的檔案。我們來看一下如何使用TS Compiler API生成抽象語法樹並且編譯成上文中提的程式碼。
對應的TS Compiler程式碼如下:
factory.createInterfaceDeclaration(
undefined,
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
factory.createIdentifier("TestA"),
undefined,
undefined,
[
factory.createPropertySignature(
undefined,
factory.createIdentifier("age"),
undefined,
factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)
),
factory.createPropertySignature(
undefined,
factory.createIdentifier("name"),
undefined,
factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
),
factory.createPropertySignature(
undefined,
factory.createIdentifier("other"),
factory.createToken(ts.SyntaxKind.QuestionToken),
factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword)
),
factory.createPropertySignature(
undefined,
factory.createIdentifier("friends"),
undefined,
factory.createTypeLiteralNode([factory.createPropertySignature(
undefined,
factory.createIdentifier("sex"),
factory.createToken(ts.SyntaxKind.QuestionToken),
factory.createUnionTypeNode([
factory.createLiteralTypeNode(factory.createNumericLiteral("1")),
factory.createLiteralTypeNode(factory.createNumericLiteral("2"))
])
)])
),
factory.createPropertySignature(
undefined,
factory.createIdentifier("cats"),
undefined,
factory.createArrayTypeNode(factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword))
)
]
)
乍一看生成這段簡單型別的程式碼非常複雜,但是仔細一看如果這些方法經過封裝,程式碼會簡潔不少,而且目前已經有一些比較成熟的第三方庫庫,比如ts-morph等。
Ts Compiler Api只有英文文件,而且使用複雜,而且生成不同型別的程式碼需要呼叫哪個函式我們不好確定,但我們可以去TS AST View查詢,它能根據你輸入的TS程式碼生成對應的抽象語法樹和Compiler程式碼,上述程式碼就是TS AST View提供的。
factory.createInterfaceDeclaration
方法會生成一個interface節點,生成之後,我們還需要呼叫一個方法將生成的interface列印出來,輸出成字串檔案,參考程式碼如下:
// ast轉程式碼
// 需要將上文factory.createInterfaceDeclaration生成的節點傳入
export const genCode = (node: ts.Node, fileName: string) => {
const printer = ts.createPrinter();
const resultFile = ts.createSourceFile(fileName, '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
const result = printer.printNode(
ts.EmitHint.Unspecified,
node,
resultFile
);
return result;
};
第三步:美化輸出的程式碼
美化程式碼這一步我們應該很熟悉了,相信我們編譯器中都裝有Prettier,每個前端專案必備的工具,它不僅可以直接格式化我們正在編寫的檔案,也可以格式化我們手動傳入的字串程式碼,話不多說,上程式碼:
import * as prettier from 'prettier';
// 預設的prettier配置
const defaultPrettierOptions = {
singleQuote: true,
trailingComma: 'all',
printWidth: 120,
tabWidth: 2,
proseWrap: 'always',
endOfLine: 'lf',
bracketSpacing: false,
arrowFunctionParentheses: 'avoid',
overrides: [
{
files: '.prettierrc',
options: {
parser: 'json',
},
},
{
files: 'document.ejs',
options: {
parser: 'html',
},
},
],
};
// 格式化美化檔案
type prettierFileType = (content:string) => [string, boolean];
export const prettierFile: prettierFileType = (content:string) => {
let result = content;
let hasError = false;
try {
result = prettier.format(content, {
parser: 'typescript',
...defaultPrettierOptions
});
}
catch (error) {
hasError = true;
}
return [result, hasError];
};
第四步:將生成的程式碼寫入我們的檔案
這一步比較簡單,直接是有node提供的fs Api生成檔案即可,程式碼如下:
// 建立目錄
export const mkdir = (dir:string) => {
if (!fs.existsSync(dir)) {
mkdir(path.dirname(dir));
fs.mkdirSync(dir);
}
};
// 寫檔案
export const writeFile = (folderPath:string, fileName:string, content:string) => {
const filePath = path.join(folderPath, fileName);
mkdir(path.dirname(filePath));
const [prettierContent, hasError] = prettierFile(content);
fs.writeFileSync(filePath, prettierContent, {
encoding: 'utf8',
});
return hasError;
};
前後端的協同
上面的流程還缺少重要的一步:資料來源JSON Schema誰提供?
這就需要前後端的協同,目前後端已經有了很成熟的生成JSON Schema的工具,比如Swagger,YAPI等。
接入Swagger的後端系專案都能給前端提供swagger.json檔案,檔案的內容就包括所有介面的詳細資料,包括JSON Schema資料。
YAPI和Swagger不同,它是API的集中管理平臺,在它上面管理的api我們都可以通過它提供的介面獲取的所有api的詳細資料,和swagger.json提供的內容大同小異,而且YAPI平臺支援匯入或者生成swagger.json。
如果有了介面管理平臺和制定了相關規範,前後端的協作效率會提升很多,減少溝通成本,而且前端也可以基於管理平臺做一些工程效能相關的工作。
難點攻克
上述步驟只是簡單的介紹了一下生成ts型別程式碼的一個思路,這思路下還有有一些難點需要解決的,比如:
- 實際開發中我們需要註釋,但TS Compiler API不能生成註釋:這個問題我們可以通過再程式碼的string生成之後然後在對應的地方手動插入註釋的方式解決。
- 實際業務的型別可能非常複雜,巢狀層次很深:這個問題我們可以通過遞迴函式來解決。
- 已經生成的型別程式碼,如果API有改動,應該怎麼辦,或者新增的API要和原來生成的放的一個檔案下,這種情況怎麼處理?TS ComPiler API是可以讀取原始檔的,就是已經存在的檔案也是可以讀取的,我們可以讀取原始檔然後再利用Compiler API修改它的抽象語法樹實現修改或者追加型別的功能。
- 前後端的協同問題:這個就需要找leader解決了。
總結
經過上面提到的四個步驟,我們瞭解了生成程式碼的基本流程,而且每一步的實現方案不是固定的,可以自行選擇:
- 在資料來源選擇的問題上,我們除了JSON Schema還可以選擇原始的json資料當作資料來源,只是生成的型別不是那麼精準,在這推薦一個很好用的網站:JSON2TS。
- 程式碼生成工具我們也可以用常用的一些模板引擎來生成,比如Nunjucks,EJS等,它們不僅可以生成HTML,也可以生成任何格式的檔案,並且能夠返回生成的字串。
- 程式碼美化這步還是推薦使用prettier。
- 對於前端來說,目前最好的輸出檔案的方式就是Node了。
本文只提供了一種工程化生成TS型別、Mock資料等簡單可複製程式碼的思路,實現後能減少一部分勞動密集型的工作內容,讓我們更專注於業務邏輯開發。