> 背景:當你接手一些老的前端項目的時候,有時候這個項目同時又js和ts,如何快速將一個老的基於React的項目快速轉換為ts風格。

## [ts compiler](microsoft/TypeScript)

### 鏈接是typescript官方的介紹,這裡我簡單說一說一些ts compiler的基本概念。

- Parser 根據源代碼生產ast

- Type Checker 可以分析生成一些推斷類型

- Emitter 生成器

- Pre-processor 分析源代碼依賴,生成一系列SourceFile

## 目標

### 基本上我們需要將一個jsx風格的React Component 轉換為一個tsx風格的Component

import PropTypes from prop-types
import React, { Component } from react
import { connect } from react-redux
import { bindActionCreators } from redux

@connect(
(state, props) => {
const { app, global, info, list, curItem } = state

return {
curApp: list[ProPS - Uplifting Publisher],
app,
curItem,
total: info.total,
name: global.name,
}
},
dispatch =>
bindActionCreators(
{
save,
update,
remove,
},
dispatch
)
)
export default class Test extends Component {
static propTypes = {
obj: PropTypes.object,
isBool: PropTypes.bool,
str: PropTypes.string,
arr: PropTypes.array,
oneOfType: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
node: PropTypes.node,
oneOf: PropTypes.oneOf([a, b, c, d]),
func: PropTypes.func,
required: PropTypes.func.isRequired,
}

constructor(props) {
super(props)
this.state = {
isShowModal: false,
modalName: props.report_name || ,
modalType: save,
confirmLoading: false,
monitorModalVisible: false,
}
this.aaa = 111
}

render() {
return <div>hello tscer</div>
}
}

### 我們需要做什麼

- 將 static propTypes 轉換為 interface IProps

- 根據 this.state 生成 interface IState

- 生成 Component 的 generic types

- 去掉 PropTypes

### 在開始之前我們需要個 ast viewer 幫我們快速分析ast樹結構, [這裡](TypeScript AST Viewer),讓我們把上述代碼複製進去, 看看ast樹結構。

## 開始分析

### 入口

import { CompilerOptions, createPrinter, createProgram, EmitHint, transform } from typescript

const program = createProgram([realPath], compileOptions)

const sourceFiles = program
.getSourceFiles()
.filter(s => s.fileName === realPath)

const typeChecker = program.getTypeChecker()

const result = transform(sourceFiles, [
generateGenericPropAndState(typeChecker),
removeImportPropTypes(typeChecker),
removeStaticPropTypes(typeChecker),
])

const printer = createPrinter()
const printed = printer.printNode(
EmitHint.SourceFile,
result.transformed[0],
sourceFiles[0]
)

const res = prettier.format(printed, {
semi: true,
singleQuote: true,
trailingComma: es5,
bracketSpacing: true,
parser: typescript,
})

- createProgram

- 第一個參數是我們需要編譯文件的地址數組

- 第二個參數是ts項目下面的tsconfig.json的一些編譯選項,這裡可以讀取目標文件的compileOptions,或者自選一些默認的選項生成。

- 簡單來說一個program就是編譯的入口(pre-processor),根據的參數ts compiler會生成一系列目標文件編譯所依賴的庫,生成一系列SourceFiles

- program.getSourceFiles()

- 獲取編譯後的SourceFile對象,這裡需要filter一下我們需要的目標文件,因為默認ts compiler會添加一系列目標文件所依賴的一些文件,例如一個es dom lib庫等, 我們需要的只是目標文件。

- transform

- 類似於babel分析裡面的 traverse,本質就是便利ast,分析每個node, 然後做一個我們需要的操作

- generateGenericPropAndState 做的就是生成IProps 和 IState

- removeImportPropTypes 刪除掉 import PropTypes

- removeStaticPropTypes 刪除掉 static PropTypes

- createPrinter

- Printer 就是最後的generator, 生成最終我們的代碼

- 最後可以利用 prettier, tslint等格式化工具生成你項目中需要的代碼

### removeImportPropTypes

import { isImportDeclaration, isStringLiteral, SourceFile, updateSourceFileNode } from typescript

export const removeImportPropTypes = (typeChecker: TypeChecker) => (
context: TransformationContext
) => (sourceFile: SourceFile) => {
const statements = sourceFile.statements.filter(
s =>
!(
isImportDeclaration(s) &&
isStringLiteral(s.moduleSpecifier) &&
s.moduleSpecifier.text === prop-types
)
)
return updateSourceFileNode(sourceFile, statements)
}

- 這裡更多的會將思考過程,代碼細節大家可以自己去試試就知道了

- 一個transform的高階方程

- 第一個參數 typeChecker,使我們在transform中自己傳遞的

- 第二個參數 context, 使整個編譯過程中保存的上下文信息

- 第三個參數 sourceFile,就是我們需要編譯的源文件了

- sourceFile的結構, 這裡就用到我之前說的[ast viewwe](TypeScript AST Viewer)

- 中間的SourceFile即是sourceFile的結構了,選擇代碼也可以看到代碼對應的ast結構

- 這裡我們需要把 import PropTypes from prop-types 刪除掉,明細這裡對應的是一個叫做 ImportDeclaration 的結構

- 我們看看圖中最右側Node節點

- 我們需要的是一個叫做 prop-types的import 聲明,很明顯在右側它在 moduleSpecifier -> text 裡面

- 到這裡我們就得到我們需要的了 找到一個sourceFile裡面 ImportDeclaration的moduleSpecifier的text是prop-types的節點,去除掉即可。

- sourceFile.statements 代表的是每一個代碼塊

- filter就按照我以上說的邏輯 去除掉 prop-types

- 最後返回 updateSourceFileNode, 生成了一個更新後我們需要新的sourceFile返回

- 之後的transform功能類似於此的思考過程,由於ts結構vscode有很好的代碼提示,以及類型注釋,一些ts compiler的api大家根據對應Node的定義應該可以很快的適應

### removeStaticPropTypes

export const removeStaticPropTypes = (typeChecker: TypeChecker) => (
context: TransformationContext
) => (sourceFile: SourceFile) => {
const visitor = (node: Node) => {
if (isClassDeclaration(node) && isReactClassComponent(node, typeChecker)) {
return updateClassDeclaration(
node,
node.decorators,
node.modifiers,
node.name,
node.typeParameters,
createNodeArray(node.heritageClauses),
node.members.filter(m => {
if (
isPropertyDeclaration(m) &&
isStaticMember(m) &&
isPropTypesMember(m)
) {
// static and propTypes
return false
}

if (
isGetAccessorDeclaration(m) &&
isStaticMember(m) &&
isPropTypesMember(m)
) {
// static and propTypes
return false
}

return true
})
)
}
return node
}

return visitEachChild(sourceFile, visitor, context)
}

- visitEachChild 遍歷ast tree,在每一個node給一個回調

- 我們需要去除掉 static propTypes, 前提如圖, 首先是一個ClassDeclaration,其次是一個 React Component, 最後class裡面有一個 PropertyDeclaration, static修飾,名字是propTypes

- ClassDeclaration很好判斷, isClassDeclaration

- react Component, 這裡我們需要分析 ClassDeclaration 的 HeritageClause,也就是繼承條件。如圖展示,我們需要獲得 HeritageClause.types[0].expression.getText(), 這裡可以利用正則去判斷一下,/React.Component|Component|PureComponent|React.PureComponent/, 基本情況下這是react class component

- isPropertyDeclaration可以判斷是否是 PropertyDeclaration,如圖,PropertyDeclaration.modifiers[0] === StaticKeyword, 這裡判斷其修飾是否是一個 static, PropertyDeclaration.name === propTypes 去判斷。

### generateGenericPropAndState

- 思考過程還是如上面所述,大家可以先自己嘗試一下。 [源碼](sonacy/tscer)

- 關於生成的新的代碼的ast結構, 大家可以在 [ast viewer](TypeScript AST Viewer)輸入需要的代碼,在觀察一下生成的ast結構,從而去構建例如 interface 等類型結構。

## 總結

### 還可以做什麼

- 例如將 redux connect轉換為hoc,利用 returnType<typeof mapStateToProps> 獲取redux的一些類型

- 可以根據 class 裡面 this.props 去分析, 添加一個沒有定義的屬性添加到 IProps 中間

- 給一些聲明週期添加一些IProps, IState, params的參數

- 可以將一些 簡單function, 利用checker生成一些類型,並在複雜類型添加TODO,給後期方便添加。

- 可以根據一些project的目錄,批量去處理js, jsx去轉換成需要的ts文件

- 做一個回退操作, 如果不理想, 用戶可以回退為原來的js文件

- 。。。

### ts compile 可以根據大家的需要做一些大家預期的操作,類似於babel的parser, traverse, generator等。這裡只是大概的思路提供給大家,畢竟現在很多小程序框架也是類似的方式。happy coding!


推薦閱讀:
相關文章