> 背景:當你接手一些老的前端項目的時候,有時候這個項目同時又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!