你還在手寫TS型別程式碼嗎

TNTWEB發表於2022-02-21

身為一個前端開發,在開發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 ASTTS 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的工具,比如SwaggerYAPI等。

接入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
  • 程式碼生成工具我們也可以用常用的一些模板引擎來生成,比如NunjucksEJS等,它們不僅可以生成HTML,也可以生成任何格式的檔案,並且能夠返回生成的字串。
  • 程式碼美化這步還是推薦使用prettier。
  • 對於前端來說,目前最好的輸出檔案的方式就是Node了。
本文只提供了一種工程化生成TS型別、Mock資料等簡單可複製程式碼的思路,實現後能減少一部分勞動密集型的工作內容,讓我們更專注於業務邏輯開發。

參考文獻

相關文章