通过一个需求来深入 TypeScript 编译原理

1,668 阅读7分钟

poster.png

本文将会收录到 组件库 专栏中,该专栏主要用来写一些组件库内部实现细节等,并且本文相对来讲有一点深入,对于想了解 typescript 原理的人可以去阅读下,本人也是因为需要实现一个需求,才去看了下相关原理。

背景

当我们一般写组件库文档的时候,都会通过 markdown 去编写,然后 自定义 loader 去做一个动态渲染,而组件库的 api 文档 通常也都是以 markdown 的表格去编写的。

然而,我们可能有时候写着写着组件库的代码,里面的代码稍微改了下参数,或者类型什么的。这时候你其实还要去改组件库的文档,或者你也有可能忘记改了。这种事情也算比较常见。

众所周知,Typescript 具有强大的类型能力,那我们干嘛不借助 typescript 的类型能力,来帮助我们去自动生成 组件库的 api 文档 和对应的类型描述呢?

先上个最终效果图:

image.png

image.png

需求分析

现在我们有了一个需求:如何自动生成 组件库 api 文档 和类型描述?然而需求有了,但是要怎么实现呢。

我们先来思考下,语言是怎么执行的。这里我们可以以 babel 为例,babel实现原理主要分为几步:

  • 解析你的代码,生成 AST(抽象语法树)
  • 然后对你的代码去做进一步的转换,从而可以在低版本进行运行(这一步 babel 主要是自己维护了一个包,里面有各个浏览器支持 api 的最低版本)
  • 然后针对转换后的代码,去做进一步的生成就可以了。

上面就是一个 babel 实现的一个基本原理。然后这和我们的需求有啥关系呢?

对于这个需求也可以套用上面的流程,只不过我们只 需要解析这一个阶段就可以了。拿到代码,生成对应的 ast 语法树就可以了。我们拿到 ast 之后,就和处理普通数组和对象没啥区别了。

当然这里我们还需要涉及一个知识点,我们既然要拿到对应的 ts 类型的话,我们还需要了解下 ts 的编译过程,对于 大部分场景来说,我们不需要知道这个过程,然而对于这个需求来讲,我们有必要了解下这个,从而可以更好的实现这个需求。

typescirpt 运行原理

ts 执行代码,主要有几个关键部分:

  • Scanner 扫描器 (解析 token )
  • Parser 解析器 (生成 ast )
  • Binder 绑定器 ( 在检查器进行类型检查的时候,进行调用)
  • Checker 检查器 (执行类型检查)
  • Emitter 发射器 (生成 js 文件)

typescript在编译过程中,会通过解析器去调用扫描器去生成对应的AST语法树, 在 typescript 中,如果我们想拿到 对应的 AST 语法结构,可以通过 createSourceFile 这个 api,从而让我们拿到对应的 AST 结构。

image.png

其中 statments就是当前文件的子节点。在 ts 中,我们如果需要遍历当前 AST 的子节点的话,我们可以使用 ts.forEachChild 进行遍历。


import * as ts from 'typescript';

function printAllChildren(node: ts.Node, depth = 0) {
  console.log(new Array(depth + 1).join('----'), ts.formatSyntaxKind(node.kind), node.pos, node.end);
  depth++;
  node.getChildren().forEach(c => printAllChildren(c, depth));
}

var sourceCode = `
var foo = 123;
`.trim();


var sourceFile = ts.createSourceFile('foo.ts', sourceCode, ts.ScriptTarget.ES5, true);
printAllChildren(sourceFile);

效果: image.png

其中 node.kind 是当前节点的类型描述,查看类型可以得知该类型为SyntaxKind的枚举类型。 可以在typescript/lib/typesript.d.ts 进行查看。

ts 模块解析规则

我们的目的是生成组件的类型,我们还有必要了解下 TypeScript 模块解析规则,因为我们可能并不在当前文件进行编写类型。

为了简单省事,我这边将解析规则简化下,对于 node_modules 内的将不做解析处理,其余将按照下面规则进行分析

  • 解析当前文件
  • 模块内引入的文件(本地相对路径,本地绝对路径,具有类型别名的文件, node_modules路径)
  • 解析 d.ts 全局类型文件

实现

现在我们知道了如何拿到 ts 的 ast,也知道了如何遍历 AST,以及 ts 的路径解析规则,现在我们就可以动手实现了。

类型解析的 case

// import
import A from 'b'
import { A } from 'b'
import { A, type B } from 'b'
import type { A } from 'b'

// interface
interface A {
    a: number
    b?: string
    c: () => void
}

// 具有继承的情况
interface A extends B, C {}

/**
  type 
  type 算是 类型分析过程最复杂的一个,里面包括联合类型,泛型、交集、等各种情况,
  这里只是放个最简单的例子
*/ 
type A = 'small' | 'middle' | 'large'

// function
/**
 *  因为我们最终导出的是函数组件,所以函数也要一并分析,并且记录当前是否导出,方便后续处理
 */
function A (props: A) {}

实现思路

  • 读取文件,之后通过ts.createSourceFile 生成 AST
  • 借用 ts.forEachChild 对 AST 节点进行遍历处理,并通过一个对象,来保存处理后的结果
  • 返回处理后的结果

现在我们已经有了一些基本知识,也对这个需求,有了一个大概的思路,现在就可以动手实现了。

这里我们建立一个 utils/index.js 的文件,里面用来放一些处理的工具函数吧。

// 获取对应的 AST 节点类型
const getDeclaration = kind => ts.SyntaxKind[kind]

module.exports.getDeclaration = getDeclaration

这里我们建个 generator/core.js

const fs = require('fs')
const ts = require('typescript')
const path = require('path')
const libPath = path.resolve(__dirname, '../../../../otaku-ui')
const entryPath = path.resolve(libPath, './src/index.ts')
const { getDeclaration, parser, readFile, getAbsolutePath } = require('../utils')
const tsconfig = require('../../../../otaku-ui/tsconfig.json')

const program = ts.createProgram([entryPath], tsconfig)

function generator (filePath) {
    const currentFile = {}
    
    program.forEachChild(node => {
        if (getDeclaration(node.kind) === 'ExportDeclaration') {
             // 拿到 export * from xxx 的路径
            const value = node.moduleSpecifier.text
         
            // 这里我们需要拿到绝对路径,从而方便我们后续解析
            const absolutePath = getAbsolutePath(path.resolve(libPath, './src'), value)
            
            currentFile[absolutePath] = null
        }
    })
}

这里我们简单的写了一个 generator 函数,用来解析入口文件的内容,并通过 一个对象去保存解析后的结果,为什么这里我们需要一个对象去保存结果呢?

当然是因为对象简单啦,获取比较方便,这是一方面原因,还有就是方便我们后续处理,如果一个文件内的内容引入了其他文件的内容,我们这时候也要去解析你引入的文件,而且由于文件路径是唯一的,如果我们解析过了当前文件,直接返回即可。

还有这里用到了一个getAbsolutePath的 函数,用来处理 import 或者 export 的路径,毕竟我们一般是不写后缀的,而且路径会有相对路径、具有别名的路径、node_modules 路径,这里我们可以通过 webpackenhanced-resolve 这个库来进行路径解析,这个是 webpack 用来处理 alias 和路径解析的一个库。

utils/index.js 新增一个getAbsolutePath 的函数

const enhancedResolve = require('enhanced-resolve')

// 获取模块的绝对路径
const getAbsolutePath = (basePath, relativePath) => {
  // 相对路径
  const reg = /^\.\S+$/g
  
  // 如果不是相对路径 返回 node_modules
  if (!relativePath.match(reg)) return 'node_modules'
  
  // 调用 enhanced-resolve 解析路径,拿到完整的文件路径
  const parserPath = enhancedResolve.create.sync({
    extensions: ['.ts', '.tsx', '.js'],
  })

  const absolutePath = parserPath({
    resolveToContext: true,
    mainFields: ['main', 'exports']
  }, basePath, relativePath, {})

  return absolutePath
}

module.exports.getAbsolutePath = getAbsolutePath

这里解释下吧,最开始其实并不是这个实现方式,也是通过 enhanced-resolve 进行模块解析,不过解析的时候挂掉了,所以就换成了这种方式。

上面的代码,简单的对入口文件做了下处理,并通过一个对象来保存解析后的路径,之后我们需要对对象里面的所有内容进行解析,并且保存当前文件解析到的内容。这里我们简单的来看下数据结构。

interface Property {
    name: string
    type: string
    required: boolean
    defaultValue: unknown
    typeReference: unknown
    jsDoc: {
        tagName: string
        content: string
    }[]
}

interface Parser {
    [filePath: string]: {
        export: {
          [key: string]: unknown
        }
        import: {
            [name: string]: {
                // 解构
                deconstruct: boolean
                 // 默认导出
                default: boolean
                // 引入的名字
                name: string
                // import 的路径
                importPath: string
            }
        }
        exportFile: Parser
        type: {
            [key: string]: {
            type: 'interface' | 'type'
            name: string
            code: string
            // 继承的 interface
            extendProperty: {
                name: string
                code: string
                property: Propery
            }[],
            property: Property[]
        }
        class: {
            [name: string]: {
                export: boolean
                exportDefault: boolean
                name: string
                property: {
                    name: string
                    type: string
                    defaultValue: string
                    typeReference: unknown
                }
            }
        }
        function: {
            [fn: string]: {
                type: 'function'
                export: boolean
                exportDefault: boolean
                functionName: string,
                args: {
                    name: string
                    type: unknown
                }[]
            }
        }
        
    }
}

数据结构大概就是这个样子了,现在我们已经有了一个基本的数据结构,也通过我们写的generator函数,解析到了文件 import 的内容,现在我们需要把这些 import 的内容去记录下,并且我们解析到发现文件又引用了其他文件的时候,我们也需要进行记录,从而解决掉循环引用的问题。

function generator (filePath, fileMap = {}) {
    // 保存解析的结果
    const currentFile = {
         // 定义的类型
        type: {},
        // import 引入的数据
        import: {},
        // 当前文件的函数声明
        function: {},
        // 当前文件所导出的内容
        export: {},
        // export * from 导出的内容
        exportFile: {}
      }
    
    program.forEachChild(node => {
        const type = getDeclaration(node.kind)
        
        switch (type) {
              case 'ExportDeclaration':
            /**
             * 由于传入的是绝对路径,所以这里需要后退到对应的相对路径,
             * 从而获取到引入的模块绝对路径,主要是因为 enhance-resolve 解析报错
             */ 

            if (node.exportClause) {
              node.exportClause.elements?.reduce((obj, current) => {
                obj[current.name.escapedText] = {
                  type: undefined
                }

                return obj
              }, currentFile.export)
            } else {
             // 拿到 export * from xxx 的路径
              const value = node.moduleSpecifier.text
              // 根据当前的文件路径,来对解析到的文件路径后退
              const relativePath = backPath(filePath, value)
              const exportPath = getAbsolutePath(relativePath, value)

              // 这里我们需要拿到绝对路径,从而方便我们后续解析
              generator(exportPath, filePath)
              currentFile.exportFile[exportPath] = fileMap[exportPath]
            }
        break
    })
}

这里,我们用到了一个backPath 的一个函数,这个函数的作用主要是后退文件,从而拿到引入的绝对路径

const backPath = (absolutePath, relativePath) => {
  const arr = absolutePath.split(path.sep)
  const relative = relativePath.split('/')

  for (let i = 0; i < relative.length - 1; i++) {
    if (relativePath[i] === '.') {
      arr.pop()
    }
    if (relativePath[i] === '..') {
      arr.pop()
    }
  }

  return arr.join(path.sep)
}

现在我们已经处理好了export的内容,现在我们接着来处理import相关的内容,这里先看下需要解析的 case

// import
import A from 'b'
import { A } from 'b'
import { A, type B } from 'b'
import type { A } from 'b'

现在我们就来实现下相关代码:

function generator (filePath, fileMap = {}) {
    // 保存解析的结果
    const currentFile = {
         // 定义的类型
        type: {},
        // import 引入的数据
        import: {},
        // 当前文件的函数声明
        function: {},
        // 当前文件所导出的内容
        export: {},
        // export * from 导出的内容
        exportFile: {}
      }
    
    program.forEachChild(node => {
        const type = getDeclaration(node.kind)
        
        switch (type) {
            // ...
            case 'ImportDeclaration':
                if (!node.importClause) return

                const importClause = node.importClause
                const importPath = path.resolve(filePath, '../')
                const absolutePath = getAbsolutePath(
                  importPath,
                  node.moduleSpecifier.text
                )
                if (absolutePath.includes('.scss')) return
                if (importClause?.namedBindings) {
                  // 解构引入
                  importClause.namedBindings.elements.reduce((obj, current) => {
                    obj[current.name.escapedText] = {
                      // 解构
                      deconstruct: true,
                      // 默认导出的
                      default: importClause.name
                        ? importClause.name.escapedText
                        : undefined,
                      name: current.name.escapedText,
                      importPath: absolutePath
                    }

                    return obj
                  }, currentFile.import)

                  generator(absolutePath, fileMap)
                } else {
                  // 命名导出
                  const name = importClause.name.escapedText
                  currentFile.import[name] = {
                    name,
                    nameExport: true,
                    importPath: node.moduleSpecifier.text
                  }
                }
                break
    })
}

这里先说明下:

  • importClause 是你 import 的内容
  • importClause.namedBindings.elements 是通过解构引入的
  • 对于 import 引入的东西,不管是命名引入,还是解构,或者类型引入,我们都放到 import这个对象下,key 就是引入的名字,value 则记录引入的方式,以及引入的文件路径
  • 对于引入的文件,我们依然会去调用函数进行解析,并通过路径保存

现在我们 import 也已经处理过了,然后我们接着来处理 interfacetypeinterface 会相对比较简单,就来简单实现下:

function generator (filePath, fileMap = {}) {
    // 保存解析的结果
    const currentFile = {
         // 定义的类型
        type: {},
        // import 引入的数据
        import: {},
        // 当前文件的函数声明
        function: {},
        // 当前文件所导出的内容
        export: {},
        // export * from 导出的内容
        exportFile: {}
      }
    
    program.forEachChild(node => {
        const type = getDeclaration(node.kind)
        
        switch (type) {
            // ...
             case 'InterfaceDeclaration':
                // 继承的接口
                const extendsInterface = node.heritageClauses?.[0].types?.map(
                  item => {
                    return {
                      name: item.expression.escapedText
                    }
                  }
                )

                currentFile.type[node.name.escapedText] = {
                  type: 'interface',
                  export: isExport(node),
                  exportDefault: isExportDefault(node),
                  name: node.name.escapedText,
                  code: content.substring(node.pos, node.end).trimStart(),
                  extendProperty: extendsInterface?.reduce((total, current) => {
                    // 当前文件定义的类型
                    if (currentFile.type[current.name]) {
                      return total.concat(currentFile.type[current.name])
                    } else {
                      // 引入的类型
                      const importType = currentFile.import[current.name]

                      if (importType) {
                        const referer = fileMap[importType.importPath]
                        if (referer.type[current.name]) {
                          const type = referer.type[current.name]

                          return total.concat(type)
                        } else {
                          const result = Object.keys(referer.exportFile).find(file => {
                            return Object.keys(fileMap[file].type).some(typeName => typeName === current.name)
                          })

                          if (result) return total.concat(
                            fileMap[result].type[current.name]
                          )
                        }
                      }
                    }

                  }, []),
                  property: node.members.map(item => {
                    const type = content
                      .substring(item.type.pos, item.type.end)
                      .trimStart()

                    return {
                      name: item.name.escapedText,
                      type: type,
                      required: item.questionToken ? false : true,
                      defaultValue: undefined,
                      typeReference: getReferenceType(item, currentFile, fileMap),
                      jsDoc: ts.getJSDocTags(item).map(children => {
                        return {
                          tagName: children.tagName.escapedText,
                          content: children.comment
                        }
                      })
                    }
                  })
                }
                saveExport(node, node.name.escapedText, currentFile, 'interface')
                break
    })
}

interface 其实主要就是根据对应的数据结构进行处理就是了,这里主要说下如何处理引用的类型,针对引用的类型,我们可以封装一个函数来进行获取,名字就叫 getReferenceType,该函数接收三个参数,第一个参数是node,当前处理的节点,第二个是当前解析好的数据对象,也就是 currentFile,第三个是存放所有解析数据的对象,现在来实现下:

const getReferenceType = (node, currentFile, fileMap) => {
  if (!node) return

  let typeName
 // 获取定义的类型名称
  if (node.type?.typeName?.escapedText) {
    typeName = node.type?.typeName?.escapedText
  } else if (node.type?.elementType?.typeName?.escapedText) {
    typeName = node.type?.elementType?.typeName?.escapedText
  }
   
  // 判断当前已经解析好的数据,是否存在,如果存在就返回,如果不存在,则开始读取 import 对象,来找到对应的属性,从而解析该路径,拿到结果
  if (currentFile.type[typeName]) {
    return currentFile.type[typeName]
  } else if (currentFile.import[typeName]) {
    // 说明是引入的
    const absolutePath = currentFile.import[typeName].importPath
    if (absolutePath) {
      if (absolutePath.includes('node_modules')) {
        return 'node_modules'
      }
      // 判断该文件时否已经被解析过
      if (currentFile[absolutePath]) {
        currentFile.type[typeName].typeReference = currentFile[absolutePath].type[typeName]

        return currentFile.type[typeName].typeReference
      } else {
        generator(absolutePath, fileMap)

        currentFile.import[typeName].typeReference = fileMap[absolutePath].type[typeName]

        return currentFile.import[typeName].typeReference
      }
    }
  } else {
    // 都不存在
    return null
  }
}

实现依然比较简单,获取 node 的类型名称,判断当前文件是否已经解析过,如果解析过就返回,没有解析过,就从import去拿,如果 import 也没有就返回 null,这里其实并没有处理 d.ts这种情况,对于 d.ts,这种 会当做全局类型文件进行查找,一般情况下,不建议把组件或者函数类型放到d.ts 这种文件里面。

还有对于多个同名interface 来讲,ts 会去做类型合并,这个也没有去处理。

现在我们接着来处理 type 类型

function generator (filePath, fileMap = {}) {
    // 保存解析的结果
    const currentFile = {
         // 定义的类型
        type: {},
        // import 引入的数据
        import: {},
        // 当前文件的函数声明
        function: {},
        // 当前文件所导出的内容
        export: {},
        // export * from 导出的内容
        exportFile: {}
      }
    
    program.forEachChild(node => {
        const type = getDeclaration(node.kind)
        
        switch (type) {
            // ...
            case 'TypeAliasDeclaration':
                currentFile.type[node.name.escapedText] = {
                  type: 'type',
                  name: node.name.escapedText,
                  export: isExport(node),
                  exportDefault: isExportDefault(node),
                  code: content.substring(node.pos, node.end).trimStart(),
                  typeReference: node.type?.types?.reduce((arr, current) => {
                      // 说明是联合类型
                    if (current.kind === 197) {
                      current.templateSpans.forEach(children => {
                         // 说明是引用的类型
                        if (children.type.kind === 177) {
                          const referenceName = children.type.typeName.escapedText

                          if (currentFile.type[referenceName]) {
                             // 简单的做个去重,因为 type 可能会引入多个类型,所以这里会先去重,
                             // 感觉还是换成对象会好点,方便处理,数组的话,还需要遍历一次才行。
                            const find = arr.find(item => item.name === referenceName)
                            if (!find) arr.push(currentFile.type[referenceName])
                            return arr
                          }
                        }
                      })
                    }
                    if (current.kind === 177) {
                      // 是否已经添加过
                      const find = arr.find(item => item.name === current.typeName.escapedText)
                      if (find) return arr
                    }

                    return arr
                  }, [])
                }
                saveExport(node, node.name.escapedText, currentFile, 'type')
                break
    })
}

目前 type 只处理了联合类型这一种情况,对于一些其他情况,暂时还没去整,后面在去完善。

现在我们还剩 function 这一种还没有进行处理,对于 function, 来讲,我们一般会有这几种情况去写:


function a () {}

const a = () => {}
const a = async () => {}

然而在 ts 中,只有第一种会被当做函数,对于变量生成的函数,并不会认识是函数类型,他是属于一种匿名函数。

function generator (filePath, fileMap = {}) {
    // 保存解析的结果
    const currentFile = {
         // 定义的类型
        type: {},
        // import 引入的数据
        import: {},
        // 当前文件的函数声明
        function: {},
        // 当前文件所导出的内容
        export: {},
        // export * from 导出的内容
        exportFile: {}
      }
    
    program.forEachChild(node => {
        const type = getDeclaration(node.kind)
        
        switch (type) {
            // ...
            case 'FunctionDeclaration':
                // function 声明的函数
                currentFile.function[node.name.escapedText] = {
                  exportDefault: isExportDefault(node),
                  export: isExport(node),
                  functionName: node.name.escapedText,
                  args: transformArgs(node, currentFile, fileMap)
                }
                setDefaultValue(node, currentFile, content)
                saveExport(node, node.name.escapedText, currentFile, 'function')
                break
            default:
                // 匿名函数等
                isFunction(node, currentFile, fileMap, content)
                break
    })
}

定义一个 isFunction 来处理 function

const isFunction = (node, currentFile, fileMap, content) => {
   // 是否变量定义的函数
  const isVariableFunction = node => {
    const initializer = node?.declarationList?.declarations?.[0]?.initializer

    if (initializer) {
      if (ts.isArrowFunction(initializer)) {
        return initializer
      }

      return false
    }
    return false
  }

  const isArrowFunction = ts.isArrowFunction(node)
  const isFunctionExpression = ts.isFunctionExpression(node)
  // const isFunctionLike = ts.isFunctionLike(node)
  const isVariFunctionNode = isVariableFunction(node)

  if (isVariFunctionNode) {
    const functionName = isVariFunctionNode.parent.name.escapedText

    currentFile.function[functionName] = {
      export: isExport(node),
      functionName: functionName,
      arrowFunction: ts.isArrowFunction(isVariFunctionNode),
      asyncFunction: ts.isAsyncFunction(isVariFunctionNode),
      args: transformArgs(isVariFunctionNode, currentFile, fileMap)
    }

    setDefaultValue(isVariFunctionNode, currentFile, content)
    saveExport(node, functionName, currentFile, 'function')

    return isVariFunctionNode
  } else if (ts.isFunctionDeclaration(node)) {
    // 普通函数
    return node
  } else if (isArrowFunction) {
    return isArrowFunction
  } else if (isFunctionExpression) {
    return isFunctionExpression
  }
}

// 处理函数参数
const transformArgs = (node, currentFile, fileMap) => {
  return node.parameters?.map(args => {
     // 获取函数参数的类型
    const typeName = args?.type?.kind === 177
        ? getReferenceType(args, currentFile, fileMap)
        : ''

    return {
      name: args.name.escapedText,
      type: typeName
    }
  })
}

// 处理默认值
const setDefaultValue = (node, currentFile, content) => {
  node.body.forEachChild(current => {
    const declarations = current?.declarationList?.declarations?.[0]

    if (declarations?.initializer) {
      const fnName = node?.name?.escapedText || node.parent.name.escapedText
      // 获取函数名
      const fn = currentFile?.function[fnName]
      const args = fn?.args?.[0]
      // 获取 props 的名字
      const propsName = node.parameters?.[0]?.name.escapedText
        
       // 遍历节点,从而给每个对应的 interface 声明的属性,添加默认值
      const init = (property) => {
        property.map(item => {
          const property = declarations?.name?.elements?.find(children => children.name.escapedText === item.name)

          if (property) {
            item.defaultValue = property.initializer 
              ? property.initializer.text : undefined
          }

          return item
        })
      }

      if  (args.name === propsName) {
        if (args?.type?.extendProperty) {
          init(args.type.extendProperty)
        } 

        if (args?.type?.property) {
          init(args.type.property)
        }
      }
    }
  })
}

生成文档

至此,本文的主要内容就算是写完了,对于生成部分的话,其实也很简单,由于我们已经记录好了哪些内容已经被导出了,所以我们只需要在生成的时候,去拿到这些内容去渲染就可以了。这里以 组件库内部文档实现方式为例:

image.png

这里用了一个from-matter 来定义了一个 api,里面保存了一个 module,用来决定生成哪个组件。可以看到我下面还定义了一个 为 apicontainer。接下来的渲染的话,需要你自定义一个loader 来处理,这里我提前写过了,就不赘述。

   new MarkdownIt()
       .use(container, 'api', {
          render (tokens, index) {
            if (tokens[index].nesting === 1) {
              const apiType = generatorAPI(entryPath)

              const findExport = () => {
                return data.api.module.map(item => {
                  const filePath = Object.keys(apiType).find(children => {
                    if (apiType[children].export[item]) {
                      return true
                    }
                  })

                  return {
                    name: item,
                    type: filePath ? apiType[filePath].export[item].reference : null
                  }
                })
              }
              const result = findExport()
              const interface = []
              const type = []
              const apiData = []
              const property = []

              const appendType = (property) => {
                const find = property.find(property => property.typeReference)

                if (find) {
                  find.typeReference.type === 'interface' ? interface.push(find.typeReference.code) : type.push(find.typeReference.code)
                }
              }

              result.forEach(item => {
                const type = item.type.args[0].type

                if (type.type === 'interface') {
                  if (type.extendProperty) {
                    type.extendProperty.forEach(children => {
                      appendType(children.property)
                      interface.push(children.code)
                    })

                  }
                  appendType(type.property)
                  interface.push(type.code)
                }


                apiData.push({
                  name: item.name,
                  data: type.property
                })
              })
                
        
              const header = ['属性', '是否必填', '类型', '默认值', '描述']
    
              const mdTableData = apiData.reduce((total, current) => {
                const { data } = current

                const table = [
                  header.join('|'),
                  new Array(5).fill('---').join('|'),
                  data.map(children => {
                    return [
                      children.name,
                      children.required ? '是' : '否',
                      children.type,
                      children.defaultValue
                    ].join('|')
                  }).join('\n')
                ]

                total.push(
                  table.join('\n')
                )

                return total
              }, [])


              return `<>
                <Api 
                  code={\`${interface.join('\n\n')}\`}
                  data={\`${JSON.stringify(apiData)}\`}
                  ></Api>`
            } else {
              return `</>`
            }
          }
        })

这个是我自定义的一个 container, container 里面返回了一个 Api 的一个组件,用于渲染 api 文档。之后就和 loader一样,把 处理好的结果丢给 webpack 去执行渲染即可。

效果

再来放一张效果图:

image.png

收获

这个需求是去年 10 月做的了好像,拖到了现在才写了文章总结,目前大概对 ts 的编译流程倒是有了简单的了解,虽然自己也对 ts 不是很熟就是了。

这篇算是专栏的第二篇文章了,本专栏将不定期更新,毕竟我懒,如果各位 dalao 们,对于本文有什么建议的欢迎评论区,顺便来个 star啥的。

顺便丢个地址:OTAKU-UI