【閱讀筆記】Taro轉小程序編譯源碼解析

前言

這篇文章的主要是對taro/taro-tarnsformer-wx進行源碼解析,對於想要了解Taro或者瞭解babel的人但願看了能獲得必定的啓發。html

因爲我文筆實在太爛,因此整篇文章都是以閱讀筆記的形式展現,但願能對想了解taro編譯可是不太瞭解babel的人提供一個學習途徑。 若是有已經充分了解babel編譯的的大佬能夠直接去看我fork的taro,在裏面我寫上了所有註釋但願可以幫助到你~~node

準備

在開始講解前你須要準備一下事情:git

  • 從github上clone下來taro的代碼 Taro / Taro-transformer-wx註釋版
  • 最起碼要知道babel是啥
  • 打開 astexplorer.net/ ,這是個ast在線轉換的網站,若是有不理解的地方直接粘貼代碼看結構
  • 打開網易雲播放一首好聽的音樂(文章可能有點枯燥,聽歌能緩解心情)

開始

目錄

首先咱們進入目錄結構能看到下面這堆東西github

  • taro-tarnsformer-wx/src
    • plugins/
    • adapter.ts
    • class.ts
    • constant.ts
    • create-html-element.ts
    • eslint.ts
    • index.ts
    • interface.d.ts
    • jsx.ts
    • lifecycle.ts
    • loop-component.ts
    • options.ts
    • plugins.ts
    • render.ts
    • utils.ts

然而咱們真正主要關注的只有三個文件typescript

  • taro-tarnsformer-wx/src
    • index.ts
    • class.ts
    • render.ts

index.ts

咱們先總體來分析下index.tsexpress

export default function transform (options: Options): TransformResult {
    // ... -> 設置一些參數
    // 若是是 typescript 代碼使用 ts.transpile 轉換爲 esnext 代碼
    const code = options.isTyped
        ? ts.transpile(options.code, {
        jsx: ts.JsxEmit.Preserve, // 保留jsx語法
        target: ts.ScriptTarget.ESNext,
        importHelpers: true,
        noEmitHelpers: true
        })
        : options.code
    // babel的第一步,將 js 代碼轉換成 ast 語法樹
    const ast = parse(code, {
        parserOpts: {
        sourceType: 'module',
        plugins: [ ]
        },
        plugins: []
    }).ast as t.File
    //... -> 定義一些變量
    // babel的第二步,遍歷語法樹,並對語法樹作出修改
    traverse(ast, {
        //... -> **轉換第一步的核心**
    });
    //... -> 一些簡單的處理
    /** * **轉換第二步的核心** * 對 ast 作了更進一步的處理 * 同時生產了模板文件,也就是 wxml */
    result = new Transformer(mainClass, options.sourcePath, componentProperies).result
    // 經過generate將語法樹轉換成js,這就是最終小程序用的js代碼
    result.code = generate(ast).code
    result.ast = ast
    result.compressedTemplate = result.template
    result.template = prettyPrint(result.template, {
        max_char: 0
    })
    result.imageSrcs = Array.from(imageSource)
    return result
}
複製代碼

轉換第一步核心

先簡單瞭解下用到的配置項的意義,有點多,咱一個一個講json

traverse(ast, {
  // 模板字符串
  TemplateLiteral (path) {},
  // 類的宣言
  ClassDeclaration (path) {},
  // 類表達式
  ClassExpression (path) {},
  // 類的函數
  ClassMethod (path) {},
  // if語句
  IfStatement (path) {},
  // 調用表達式
  CallExpression (path) {},
  // JSX元素
  JSXElement (path) {},
  // JSX開合元素
  JSXOpeningElement (path) {},
  // JSX屬性
  JSXAttribute (path) {},
  // 導入宣言
  ImportDeclaration (path) {},
})
複製代碼

咱們從代碼由上往下的方式一個一個來看redux

首先看對導入語句的處理小程序

ImportDeclaration (path) {
  const source = path.node.source.value
    if (importSources.has(source)) {
      throw codeFrameError(path.node, '沒法在同一文件重複 import 相同的包。')
    } else {
      importSources.add(source)
    }
    const names: string[] = []
    // TARO_PACKAGE_NAME = '@tarojs/taro'
    if (source === TARO_PACKAGE_NAME) {
    /** * 若是文件中有import xx from '@tarojs/taro' * 會自動幫你多導入一些輔助函數 * import xx, { * internal_safe_get, * internal_get_orignal, * internal_inline_style, * getElementById * } from '@tarojs/taro' * */
    isImportTaro = true
    path.node.specifiers.push(
      t.importSpecifier(t.identifier(INTERNAL_SAFE_GET), t.identifier(INTERNAL_SAFE_GET)),
      t.importSpecifier(t.identifier(INTERNAL_GET_ORIGNAL), t.identifier(INTERNAL_GET_ORIGNAL)),
      t.importSpecifier(t.identifier(INTERNAL_INLINE_STYLE), t.identifier(INTERNAL_INLINE_STYLE)),
      t.importSpecifier(t.identifier(GEL_ELEMENT_BY_ID), t.identifier(GEL_ELEMENT_BY_ID))
    )
  }


  // REDUX_PACKAGE_NAME = '@tarojs/redux'
  // MOBX_PACKAGE_NAME = '@tarojs/mobx'
  if (
  source === REDUX_PACKAGE_NAME || source === MOBX_PACKAGE_NAME
  ) {
    path.node.specifiers.forEach((s, index, specs) => {
      if (s.local.name === 'Provider') {
        /** * 找到 import { Provider } from 'xxx' * 替換成 * import { setStore } from 'xxx' */
        // 刪除引入參數Provider
        specs.splice(index, 1)
        // 添加引入參數setStore
        specs.push(
            t.importSpecifier(t.identifier('setStore'), t.identifier('setStore'))
        )
      }
    })
  }
  /** * 1.遍歷當前import語句收集全部導入的變量名 * 2.將 import { Component } from '@tarojs/taro' * 替換成 import { __BaseComponent } from '@tarojs/taro' */
  path.traverse({
    ImportDefaultSpecifier (path) {
      const name = path.node.local.name
      DEFAULT_Component_SET.has(name) || names.push(name)
    },
    ImportSpecifier (path) {
      const name = path.node.imported.name
      DEFAULT_Component_SET.has(name) || names.push(name)
      if (source === TARO_PACKAGE_NAME && name === 'Component') {
        path.node.local = t.identifier('__BaseComponent')
      }
    }
  })
  componentSourceMap.set(source, names)
}
複製代碼

接着看對類的定義處理數組

ClassDeclaration (path) {
  // 將找到的類的節點存起來,其實這裏能夠看出,taro默認一個文件只有一個 class
  mainClass = path
  /** * 下面這裏的目的其實就是當你引用了自定義的組件而且繼承了他,這是taro須要把你繼承的這個源碼也進行編譯 */
  const superClass = path.node.superClass
  // 先判斷這個類必須是有繼承的 也就是 class A extends XXX {}
  if (t.isIdentifier(superClass)) {
    const binding = path.scope.getBinding(superClass.name)
    // 再判斷這個被繼承的XXX在以前已經聲明過
    if (binding && binding.kind === 'module') {
      const bindingPath = binding.path.parentPath
      // 第三步判斷這個聲明語句是導入宣言
      if (bindingPath.isImportDeclaration()) {
        /** * 此時匹配到的代碼是這樣 * import XXX from 'xxx'; * class A extends XXX {} */
        const source = bindingPath.node.source
        try {
          // 這裏 p = 'xxx.js' || 'xxx.tsx'
          const p = fs.existsSync(source.value + '.js') ? source.value + '.js' : source.value + '.tsx'
          const code = fs.readFileSync(p, 'utf8')
          // 若是xxx.js存在就對它也再進行一次 transform 轉換
          componentProperies = transform({
            isRoot: false,
            isApp: false,
            code,
            isTyped: true,
            sourcePath: source.value,
            outputPath: source.value
          }).componentProperies
        } catch (error) {
          // 文件 xxx.js || xxx.tsx 不存在
        }
      }
    }
  }
},
ClassExpression (path) {
  mainClass = path as any
},
ClassMethod (path) {
  if (t.isIdentifier(path.node.key) && path.node.key.name === 'render') {
    // 找到render函數節點存起來
    renderMethod = path
  }
},
複製代碼

再來看看對if語句和函數調用的處理

// 調用表達式
// func() this.func() arr.map(()={}) 只要有函數調用都算
CallExpression (path) {
  const callee = path.get('callee')
  // isContainJSXElement 這裏是遍歷的 path 的全部子節點看裏面有沒有JSXElement,若是有啥都不處理
  if (isContainJSXElement(path)) {
    return
  }
  // 被調用者的引用是成員表達式
  // this.func() arr.map()
  if (callee.isReferencedMemberExpression()) {
    /** * 找到被調用者的成員中最靠前的一個標識符 * 如: * this.func() => id 就是 this * arr.map() => id 就是 arr */
    const id = findFirstIdentifierFromMemberExpression(callee.node)
    /** * getIdsFromMemberProps就是找到調用者的全部成員的 name * a.b.c.d() => calleeIds = ['a','b','c','d']; */
    const calleeIds = getIdsFromMemberProps(callee.node)
    if (t.isIdentifier(id) && id.name.startsWith('on') && Adapters.alipay !== Adapter.type) {
      // 到了這一步被調用者的代碼應該是 onXXXX.xxx() || onXXXX.xxx.xxx();
      /** * 解釋下buildFullPathThisPropsRef,大概以下 * 若是: * const onXXX = this.props.xxx; * onXXX.call(this, arg1, arg2); * --- 編譯後,此時 fullPath 有值 * this.props.xxx(); * * const onXXX = other; * onXXX.call(this, arg1, arg2); * --- 編譯後,此時 fullPath 爲空 * onXXX(); */
      const fullPath = buildFullPathThisPropsRef(id, calleeIds, path)
      if (fullPath) {
        path.replaceWith(
          t.callExpression(
            fullPath,
            path.node.arguments
          )
        )
      }
    }
  }
  // 被調用者的引用是標識符
  // func()
  if (callee.isReferencedIdentifier()) {
    const id = callee.node
    const ids = [id.name]
    if (t.isIdentifier(id) && id.name.startsWith('on')) {
      // 到了這一步被調用者的代碼應該是 onXXXX();
      // 以後的處理和上面同樣
      const fullPath = buildFullPathThisPropsRef(id, ids, path)
      if (fullPath) {
        path.replaceWith(
          t.callExpression(
            fullPath,
            path.node.arguments
          )
        )
      }
    }
  }
},
複製代碼

好了,接下來是重頭戲,對JSX的處理

JSXElement (path) {
  /** * 下面這塊代碼是有bug的,不過重要,能夠忽略 * 本意可見 => https://github.com/NervJS/taro/issues/550 * * 實際結果以下: * let a; a = [1,2,3].map(v => <View>{v}</View>); * --- 編譯後 * let a = <View>{v}</View>; * --- 指望結果 * let a = [1,2,3].map(v => <View>{v}</View>); */
  const assignment = path.findParent(p => p.isAssignmentExpression())
  if (assignment && assignment.isAssignmentExpression()) {
    const left = assignment.node.left
    if (t.isIdentifier(left)) {
      const binding = assignment.scope.getBinding(left.name)
      if (binding && binding.scope === assignment.scope) {
        if (binding.path.isVariableDeclarator()) {
          // 錯誤的點其實就是不該該將path.node 直接賦值給 binding.path.node.init
          // 改爲 binding.path.node.init = assignment.node.right 便可
          binding.path.node.init = path.node
          assignment.remove()
        } else {
          throw codeFrameError(path.node, '同一個做用域的JSX 變量延時賦值沒有意義。詳見:https://github.com/NervJS/taro/issues/550')
        }
      }
    }
  }
  /** * 若是是在 switch case 中的JSX會把 switch case切換成 if else * switch (v){ * case 1: { * any = <View1/> * } * case 2: { * <View2/> * break; * } * default: { * return <View3/> * } * } * --- 編譯後 * if(v === 1) { any = <View1/> } * else if(v === 2) { <View2/> } * else { return <View3/> } */
  const switchStatement = path.findParent(p => p.isSwitchStatement())
  if (switchStatement && switchStatement.isSwitchStatement()) {
    const { discriminant, cases } = switchStatement.node
    const ifStatement = cases.map((Case, index) => {
      const [ consequent ] = Case.consequent
      /** * 校驗switch case 必須包含 {} * 因此不支持如下寫法 * case 1: * case 2: * return <View/> */
      if (!t.isBlockStatement(consequent)) {
        throw codeFrameError(switchStatement.node, '含有 JSX 的 switch case 語句必須每種狀況都用花括號 `{}` 包裹結果')
      }
      const block = t.blockStatement(consequent.body.filter(b => !t.isBreakStatement(b)))
      if (index !== cases.length - 1 && t.isNullLiteral(Case.test)) {
        throw codeFrameError(Case, '含有 JSX 的 switch case 語句只有最後一個 case 才能是 default')
      }
      const test = Case.test === null ? t.nullLiteral() : t.binaryExpression('===', discriminant, Case.test)
      return { block, test }
    }).reduceRight((ifStatement, item) => {
      if (t.isNullLiteral(item.test)) {
        ifStatement.alternate = item.block
        return ifStatement
      }
      const newStatement = t.ifStatement(
        item.test,
        item.block,
        t.isBooleanLiteral(ifStatement.test, { value: false })
          ? ifStatement.alternate
          : ifStatement
      )
      return newStatement
    }, t.ifStatement(t.booleanLiteral(false), t.blockStatement([])))

    switchStatement.insertAfter(ifStatement)
    switchStatement.remove()
  }

  // 對for/for in/for of 進行禁用
  const isForStatement = (p) => p && (p.isForStatement() || p.isForInStatement() || p.isForOfStatement())

  const forStatement = path.findParent(isForStatement)
  if (isForStatement(forStatement)) {
    throw codeFrameError(forStatement.node, '不行使用 for 循環操做 JSX 元素,詳情:https://github.com/NervJS/taro/blob/master/packages/eslint-plugin-taro/docs/manipulate-jsx-as-array.md')
  }
  /** * 處理 Array.prototype.map * 將 arr.map((v)=> v) 變成 arr.map((v)=> { return v; }) */
  const loopCallExpr = path.findParent(p => isArrayMapCallExpression(p))
  if (loopCallExpr && loopCallExpr.isCallExpression()) {
    const [ func ] = loopCallExpr.node.arguments
    // 必須是箭頭函數 而且沒有 {}
    if (t.isArrowFunctionExpression(func) && !t.isBlockStatement(func.body)) {
      func.body = t.blockStatement([
        t.returnStatement(func.body)
      ])
    }
  }
},

/** * JSX開合元素 * <View></View> -> JSXOpeningElement = <View>, JSXClosingElement = </View> * <View/> -> JSXOpeningElement = <View>, JSXClosingElement = null */
JSXOpeningElement (path) {
  const { name } = path.node.name as t.JSXIdentifier
  /** * 找到<Provider />組件和store屬性 * 將組件改成View, 移除全部屬性 * * 這裏很尬,taro只修改了 OpeningElement,沒有處理CloseElement * 因此轉換 <Provider store={store} >xxxx</Provider> => <View>xxxx</Provider> * 可是由於最後會轉成wxml因此也沒影響 */
  if (name === 'Provider') {
    const modules = path.scope.getAllBindings('module')
    const providerBinding = Object.values(modules).some((m: Binding) => m.identifier.name === 'Provider')
    if (providerBinding) {
      path.node.name = t.jSXIdentifier('View')
      // 從<Provider store={myStore} >上找屬性store,而且拿到傳給store的值的名字
      const store = path.node.attributes.find(attr => attr.name.name === 'store')
      if (store && t.isJSXExpressionContainer(store.value) && t.isIdentifier(store.value.expression)) {
        // storeName = 'myStore'
        storeName = store.value.expression.name
      }
      path.node.attributes = []
    }
  }
  // IMAGE_COMPONENTS = ['Image', 'CoverImage']
  // 收集全部圖片組件的src值,注意: 只能是字符串
  if (IMAGE_COMPONENTS.has(name)) {
    for (const attr of path.node.attributes) {
      if (
        attr.name.name === 'src'
      ) {
        if (t.isStringLiteral(attr.value)) {
          imageSource.add(attr.value.value)
        } else if (t.isJSXExpressionContainer(attr.value)) {
          if (t.isStringLiteral(attr.value.expression)) {
            imageSource.add(attr.value.expression.value)
          }
        }
      }
    }
  }
},

// 遍歷JSX的屬性 也就是 <View a={1} b={any} /> 上的 a={1} b={any}
JSXAttribute (path) {
  const { name, value } = path.node
  // 過濾 name非 jsx關鍵字 或者 value 是 null、字符串、JSXElement
  // 即 any={null} any='123' any={<View />}
  if (!t.isJSXIdentifier(name) || value === null || t.isStringLiteral(value) || t.isJSXElement(value)) {
    return
  }

  const expr = value.expression as any
  const exprPath = path.get('value.expression')

  // 這裏是向父級找類的名稱 class Index {} -> classDeclName = 'Index';
  // 而後根據classDeclName來判斷是否已經轉換過
  const classDecl = path.findParent(p => p.isClassDeclaration())
  const classDeclName = classDecl && classDecl.isClassDeclaration() && safeGet(classDecl, 'node.id.name', '')
  let isConverted = false
  if (classDeclName) {
    isConverted = classDeclName === '_C' || classDeclName.endsWith('Tmpl')
  }

  /** * 處理內連樣式 * 將style={{ color: 'red' }} => style={internal_inline_style({ color: 'red' })} * 這裏taro在全局上注入了一個函數 internal_inline_style */
  // 判斷是style屬性,且未轉換過,正常來講咱們寫的代碼都是未轉換的,加這個邏輯應該是給taro內部一寫組件使用
  if (!t.isBinaryExpression(expr, { operator: '+' }) && !t.isLiteral(expr) && name.name === 'style' && !isConverted) {
    const jsxID = path.findParent(p => p.isJSXOpeningElement()).get('name')
    if (jsxID && jsxID.isJSXIdentifier() && DEFAULT_Component_SET.has(jsxID.node.name)) {
      exprPath.replaceWith(
        t.callExpression(t.identifier(INTERNAL_INLINE_STYLE), [expr])
      )
    }
  }

  /** * 處理 onXxx 事件屬性 */
  if (name.name.startsWith('on')) {
    /** * 這裏判斷 onClick屬性 他的值 是[引用表達式] * 即 onClick={myAdd} * * 將 const myAdd = this.props.add; <Button onClick={myAdd} /> * 轉換成 <Button onClick={this.props.add} /> */
    if (exprPath.isReferencedIdentifier()) {
      const ids = [expr.name]
      const fullPath = buildFullPathThisPropsRef(expr, ids, path)
      if (fullPath) {
        exprPath.replaceWith(fullPath)
      }
    }

    /** * 這裏判斷 onClick屬性 他的值 是[引用成員表達式] * 即 onClick={a.add} * * 下面這裏的意思應該跟上面差很少 * 將 const a = this.props; <Button onClick={a.add} /> * 轉換成 <Button onClick={this.props.add} /> * * 然而 const a = { add: this.props.add }; <Button onClick={a.add} /> * 這種他就GG了 */
    if (exprPath.isReferencedMemberExpression()) {
      const id = findFirstIdentifierFromMemberExpression(expr)
      const ids = getIdsFromMemberProps(expr)
      if (t.isIdentifier(id)) {
        const fullPath = buildFullPathThisPropsRef(id, ids, path)
        if (fullPath) {
          exprPath.replaceWith(fullPath)
        }
      }
    }

    // @TODO: bind 的處理待定
  }
},
複製代碼

細心的同窗確定發現漏掉了 TemplateLiteral 沒講,其實這裏就是對模板語法作處理,能夠忽略掉

看到這裏Taro編譯的第一步就講解完成了~~

若是你看懂了那你對babel編譯已經有了一個初步的瞭解,接下來的內容能夠加快節奏了~

轉換第二步核心

還記的是第二步是啥麼~幫你回憶一下~~

import { Transformer } from './class'
/** * 分析下參數 * mainClass 第一步收集到的類的節點 * options.sourcePath 代碼文件的根路徑(外面傳進來的) * componentProperies 不重要,具體看 第一步的 ClassDeclaration */
result = new Transformer(mainClass, options.sourcePath, componentProperies).result
複製代碼

而後咱們就來到了要將的第二個文件class.ts

驚不驚險,刺不刺激,已經講完1/3了呢!!!

國際慣例,先看構造函數

很是簡單,一堆賦值咱不關心,而後調用了this.compile(),因此玄機應該就在compile中

constructor ( path: NodePath<t.ClassDeclaration>, sourcePath: string, componentProperies: string[] ) {
  this.classPath = path
  this.sourcePath = sourcePath
  this.moduleNames = Object.keys(path.scope.getAllBindings('module'))
  this.componentProperies = new Set(componentProperies)
  this.compile()
}
複製代碼

compile長成下面這樣,大概描述下各個函數的功能

compile () {
  // 遍歷,各類遍歷,在遍歷的過程當中作了一堆有一堆的修改
  this.traverse()
  // 把遍歷過程當中收集到的自定義組件存到this.result.components,跟編譯沒啥關係可忽略
  this.setComponents()
  // 處理構造函數將constructor改爲_constructor
  this.resetConstructor()
  // 收集到更多使用的props
  this.findMoreProps()
  // 對ref進行處理
  this.handleRefs()
  // 你們最關心的一步,將jsx 編譯成wxml
  this.parseRender()
  this.result.componentProperies = [...this.componentProperies]
}
複製代碼

關於this.traverse,這裏我不是很想講,由於太多了,有興趣的能夠去看我加上註釋的代碼,這裏我會省略掉不少代碼

traverse () {
  const self = this
  self.classPath.traverse({
    JSXOpeningElement: (path) => {
      // ...
      // 是否是在map循環中
      const loopCallExpr = path.findParent(p => isArrayMapCallExpression(p))
      const componentName = jsx.name.name
      // 找到ref屬性
      const refAttr = findJSXAttrByName(attrs, 'ref')
      if (!refAttr) { return }
      // 找到id屬性
      const idAttr = findJSXAttrByName(attrs, 'id')
      // 隨機生成id
      let id: string = createRandomLetters(5)
      let idExpr: t.Expression
      if (!idAttr) {
        /** * 這裏是處理若是tag上沒有 id 屬性時自動添加上 id=randomStr * 若是在map循環中 id = randomStr + index */   
          if (loopCallExpr && loopCallExpr.isCallExpression()) {
            // ...
          } else {
            // ...
          }
      } else {
        // 有id屬性,找到id屬性的值或者表達式
        const idValue = idAttr.value
        if (t.isStringLiteral(idValue)) {
          // ...
        } else if (t.isJSXExpressionContainer(idValue)) {
          // ...
        }
      }

      // 若是ref屬性是字符串且不在循環中,則添加StringRef
      // ref="myRef"
      if (t.isStringLiteral(refAttr.value)) {
        // ...
      }
      // 若是ref屬性是jsx表達式 // ref={any}
      if (t.isJSXExpressionContainer(refAttr.value)) {
        const expr = refAttr.value.expression
        if (t.isStringLiteral(expr)) {
          // ref={"myRef"}
          // 將ref收集起來
          this.createStringRef(componentName, id, expr.value)
        
        } else if (t.isArrowFunctionExpression(expr) || t.isMemberExpression(expr)) {
          // ref={this.xxx} / ref={()=> {}}
          const type = DEFAULT_Component_SET.has(componentName) ? 'dom' : 'component'
          // 根據條件收集函數類型的ref
          if (loopCallExpr) {
            this.loopRefs.set(/*...*/)
          } else {
            this.refs.push({/*...*/})
          }
        } else {
          throw codeFrameError(refAttr, 'ref 僅支持傳入字符串、匿名箭頭函數和 class 中已聲明的函數')
        }
      }
      // 刪除ref屬性
      for (const [index, attr] of attrs.entries()) {
        if (attr === refAttr) {
          attrs.splice(index, 1)
        }
      }
    },
    ClassMethod (path) {
      const node = path.node
      if (t.isIdentifier(node.key)) {
        const name = node.key.name
        self.methods.set(name, path)
        // 處理render函數
        // 處理吧if(xxx) return; 換成 if(xxx) return null;
        if (name === 'render') {
          self.renderMethod = path
          path.traverse({
            ReturnStatement (returnPath) {
              const arg = returnPath.node.argument
              const ifStem = returnPath.findParent(p => p.isIfStatement())
              if (ifStem && ifStem.isIfStatement() && arg === null) {
                const consequent = ifStem.get('consequent')
                if (consequent.isBlockStatement() && consequent.node.body.includes(returnPath.node)) {
                  returnPath.get('argument').replaceWith(t.nullLiteral())
                }
              }
            }
          })
        }
        // 處理constructor函數
        // 收集全部初始化的state
        if (name === 'constructor') {
          path.traverse({
            AssignmentExpression (p) {
              if (
                t.isMemberExpression(p.node.left) &&
                t.isThisExpression(p.node.left.object) &&
                t.isIdentifier(p.node.left.property) &&
                p.node.left.property.name === 'state' &&
                t.isObjectExpression(p.node.right)
              ) {
                const properties = p.node.right.properties
                properties.forEach(p => {
                  if (t.isObjectProperty(p) && t.isIdentifier(p.key)) {
                    self.initState.add(p.key.name)
                  }
                })
              }
            }
          })
        }
      }
    },
    IfStatement (path) {
      // 把if語句中包含jsx語法的複雜判斷邏輯用匿名 state 儲存
      // if(func()) { return <View> }
      const test = path.get('test') as NodePath<t.Expression>
      const consequent = path.get('consequent')
      if (isContainJSXElement(consequent) && hasComplexExpression(test)) {
        const scope = self.renderMethod && self.renderMethod.scope || path.scope
        generateAnonymousState(scope, test, self.jsxReferencedIdentifiers, true)
      }
    },
    ClassProperty (path) {
      const { key: { name }, value } = path.node
      if (t.isArrowFunctionExpression(value) || t.isFunctionExpression(value)) {
        self.methods.set(name, path)
      }
      // 收集全部初始化的state
      if (name === 'state' && t.isObjectExpression(value)) {
        value.properties.forEach(p => {
          if (t.isObjectProperty(p)) {
            if (t.isIdentifier(p.key)) {
              self.initState.add(p.key.name)
            }
          }
        })
      }
    },
    JSXExpressionContainer (path) {
      path.traverse({
        MemberExpression (path) {
          // 遍歷全部的<JSX attr={any} /> 找到使用的state或者 props 添加到 usedState 中
          const sibling = path.getSibling('property')
          if (
            path.get('object').isThisExpression() &&
            (path.get('property').isIdentifier({ name: 'props' }) || path.get('property').isIdentifier({ name: 'state' })) &&
            sibling.isIdentifier()
          ) {
            const attr = path.findParent(p => p.isJSXAttribute()) as NodePath<t.JSXAttribute>
            const isFunctionProp = attr && typeof attr.node.name.name === 'string' && attr.node.name.name.startsWith('on')
            // 判斷是否是方法,默認on開頭就認爲是
            if (!isFunctionProp) {
              self.usedState.add(sibling.node.name)
            }
          }
        }
      })

      const expression = path.get('expression') as NodePath<t.Expression>
      const scope = self.renderMethod && self.renderMethod.scope || path.scope
      const calleeExpr = expression.get('callee')
      const parentPath = path.parentPath
      // 使用了複雜表達式,而且不是bind函數
      if (
        hasComplexExpression(expression) &&
        !(calleeExpr &&
          calleeExpr.isMemberExpression() &&
          calleeExpr.get('object').isMemberExpression() &&
          calleeExpr.get('property').isIdentifier({ name: 'bind' })) // is not bind
      ) {
          generateAnonymousState(scope, expression, self.jsxReferencedIdentifiers)
      } else {
        // 將全部key={any} 生成匿名變量
        if (parentPath.isJSXAttribute()) {
          if (!(expression.isMemberExpression() || expression.isIdentifier()) && parentPath.node.name.name === 'key') {
              generateAnonymousState(scope, expression, self.jsxReferencedIdentifiers)
          }
        }
      }
      const attr = path.findParent(p => p.isJSXAttribute()) as NodePath<t.JSXAttribute>
      if (!attr) return
      const key = attr.node.name
      const value = attr.node.value
      if (!t.isJSXIdentifier(key)) {
        return
      }
      // 處理全部onXxx的事件屬性,生成匿名函數
      if (t.isJSXIdentifier(key) && key.name.startsWith('on') && t.isJSXExpressionContainer(value)) {
          const expr = value.expression
          if (t.isCallExpression(expr) && t.isMemberExpression(expr.callee) && t.isIdentifier(expr.callee.property, { name: 'bind' })) {
              self.buildAnonymousFunc(attr, expr, true)
          } else if (t.isMemberExpression(expr)) {
          self.buildAnonymousFunc(attr, expr as any, false)
        } else {
          throw codeFrameError(path.node, '組件事件傳參只能在類做用域下的確切引用(this.handleXX || this.props.handleXX),或使用 bind。')
        }
      }
      const jsx = path.findParent(p => p.isJSXOpeningElement()) as NodePath<t.JSXOpeningElement>
      // 不在jsx語法中
      if (!jsx) return
      const jsxName = jsx.node.name
      // 不在jsxName不是標識符
      if (!t.isJSXIdentifier(jsxName)) return
      // 是jsx元素
      if (expression.isJSXElement()) return
      // 在收集到的組件中 || 關鍵字 || 成員表達式 || 文本 || 邏輯表達式 || 條件表達式 || on開頭 || 調用表達式
      if (DEFAULT_Component_SET.has(jsxName.name) || expression.isIdentifier() || expression.isMemberExpression() || expression.isLiteral() || expression.isLogicalExpression() || expression.isConditionalExpression() || key.name.startsWith('on') || expression.isCallExpression()) return

      // 上面加了一堆判斷,若是都經過了就抽離生成匿名變量,應該是兜底方案
      generateAnonymousState(scope, expression, self.jsxReferencedIdentifiers)
    },
    JSXElement (path) {
      const id = path.node.openingElement.name
      // 收集全部導入而且使用過的自定義組件
      if (
        t.isJSXIdentifier(id) &&
        !DEFAULT_Component_SET.has(id.name) &&
        self.moduleNames.indexOf(id.name) !== -1
      ) {
        const name = id.name
        const binding = self.classPath.scope.getBinding(name)

        if (binding && t.isImportDeclaration(binding.path.parent)) {
          const sourcePath = binding.path.parent.source.value
          // import Custom from './xxx';
          if (binding.path.isImportDefaultSpecifier()) {
            self.customComponents.set(name, {
              sourcePath,
              type: 'default'
            })
          } else {
            // import { Custom } from './xxx';
            self.customComponents.set(name, {
              sourcePath,
              type: 'pattern'
            })
          }
        }
      }
    },
    MemberExpression: (path) => {
      const object = path.get('object')
      const property = path.get('property')
      if (!(object.isThisExpression() && property.isIdentifier({ name: 'props' }))) {
        return
      }
      const parentPath = path.parentPath
      // 處理全部this.props.xxx
      if (parentPath.isMemberExpression()) {
        const siblingProp = parentPath.get('property')
        if (siblingProp.isIdentifier()) {
          const name = siblingProp.node.name
          if (name === 'children') {
            // 將全部的 <View>{this.props.children}</View> -> <slot />;
            // 注意只能是{this.props.children} 
            // 不能是 const { children } = this.props; <View>{children}</View>
            // 不能是 const p = this.props; <View>{p.children}</View>
            parentPath.replaceWith(t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier('slot'), [], true), t.jSXClosingElement(t.jSXIdentifier('slot')), [], true))
          } else if (/^render[A-Z]/.test(name)) {
            // 將全部的 <View>{this.props.renderAbc}</View> -> <slot name="abc" />;
            // 其餘限制同上
            const slotName = getSlotName(name)
            parentPath.replaceWith(t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier('slot'), [
              t.jSXAttribute(t.jSXIdentifier('name'), t.stringLiteral(slotName))
            ], true), t.jSXClosingElement(t.jSXIdentifier('slot')), []))

            // 給class上添加靜態屬性 static multipleSlots = true
            this.setMultipleSlots()
          } else {
            // 收集其餘使用到的props名稱
            self.componentProperies.add(siblingProp.node.name)
          }
        }
      } else if (parentPath.isVariableDeclarator()) {
        // 處理對this.props的結構語法, 收集全部用到的props
        // const { a, b, c, ...rest } = this.props;
        const siblingId = parentPath.get('id')
        if (siblingId.isObjectPattern()) {
          const properties = siblingId.node.properties
          for (const prop of properties) {
            if (t.isRestProperty(prop)) {
              throw codeFrameError(prop.loc, 'this.props 不支持使用 rest property 語法,請把每個 prop 都單獨列出來')
            } else if (t.isIdentifier(prop.key)) {
              self.componentProperies.add(prop.key.name)
            }
          }
        }
      }
    },

    CallExpression (path) {
      const node = path.node
      const callee = node.callee
      // 處理全部a.b.c(); 形式調用的函數
      /** * processThisPropsFnMemberProperties * * 將this.props.func(a,b,c); -> this.__triggerPropsFn('func', [a,b,c]); * 將this.props.obj.func(a,b,c); -> this.__triggerPropsFn('obj.func', [a,b,c]); */
      if (t.isMemberExpression(callee) && t.isMemberExpression(callee.object)) {
        const property = callee.property
        if (t.isIdentifier(property)) {
          if (property.name.startsWith('on')) {
            self.componentProperies.add(`__fn_${property.name}`)
            processThisPropsFnMemberProperties(callee, path, node.arguments, false)
          } else if (property.name === 'call' || property.name === 'apply') {
            self.componentProperies.add(`__fn_${property.name}`)
            processThisPropsFnMemberProperties(callee.object, path, node.arguments, true)
          }
        }
      }
    }
  })
}
複製代碼
resetConstructor () {
  const body = this.classPath.node.body.body
  // 若是未定義 constructor 則主動建立一個
  if (!this.methods.has('constructor')) {
    const ctor = buildConstructor()
    body.unshift(ctor)
  }
  if (process.env.NODE_ENV === 'test') {
    return
  }
  for (const method of body) {
    if (t.isClassMethod(method) && method.kind === 'constructor') {
      // 找到 constructor 改爲 _constructor
      // 找到 super(xxx) 改爲 super._constructor(xxx);
      method.kind = 'method'
      method.key = t.identifier('_constructor')
      if (t.isBlockStatement(method.body)) {
        for (const statement of method.body.body) {
          if (t.isExpressionStatement(statement)) {
            const expr = statement.expression
            if (t.isCallExpression(expr) && (t.isIdentifier(expr.callee, { name: 'super' }) || t.isSuper(expr.callee))) {
              expr.callee = t.memberExpression(t.identifier('super'), t.identifier('_constructor'))
            }
          }
        }
      }
    }
  }
}
複製代碼
findMoreProps () {
  // 這個方法的目的是收集到更多使用的props
  // 由於前面處理了的只有 constructor 和 this.props.xxx const { xxx } = this.props;
  // 
  // 下面遍歷全部的帶有使用props的聲明週期,找到有使用的props屬性並收集

  /** * 在能生命週期裏收集的props以下: * shouldComponentUpdate(props) { * console.log(props.arg1); * const { arg2, arg3 } = props; * const p = props; * console.log(p.arg4) * const { arg5 } = p; * } * shouldComponentUpdate({ arg6, arg7 }) { * } * * 最終能收集到的 [arg1,arg2,arg3,arg6,arg7]; * [arg4, arg5] 不能收集到 */


  // 第一個參數是 props 的生命週期
  const lifeCycles = new Set([
    // 'constructor',
    'componentDidUpdate',
    'shouldComponentUpdate',
    'getDerivedStateFromProps',
    'getSnapshotBeforeUpdate',
    'componentWillReceiveProps',
    'componentWillUpdate'
  ])
  const properties = new Set<string>()
  // 這裏的methods是遍歷ast的時候收集到的
  this.methods.forEach((method, name) => {
    if (!lifeCycles.has(name)) {
      return
    }
    const node = method.node
    let propsName: null | string = null
    if (t.isClassMethod(node)) {
      propsName = this.handleLifecyclePropParam(node.params[0], properties)
    } else if (t.isArrowFunctionExpression(node.value) || t.isFunctionExpression(node.value)) {
      propsName = this.handleLifecyclePropParam(node.value.params[0], properties)
    }
    if (propsName === null) {
      return
    }
    // 若是找到了propsName說明有相似 shouldComponentUpdate(props) {}
    // 遍歷方法ast
    method.traverse({
      MemberExpression (path) {
        if (!path.isReferencedMemberExpression()) {
          return
        }
        // 進行成員表達式遍歷 a.b.c 找到全部 propsName.xxx並收集
        const { object, property } = path.node
        if (t.isIdentifier(object, { name: propsName }) && t.isIdentifier(property)) {
          properties.add(property.name)
        }
      },
      VariableDeclarator (path) {
        // 進行變量定義遍歷 找到全部 const { name, age } = propsName;
        const { id, init } = path.node
        if (t.isObjectPattern(id) && t.isIdentifier(init, { name: propsName })) {
          for (const prop of id.properties) {
            if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
              properties.add(prop.key.name)
            }
          }
        }
      }
    })
    properties.forEach((value) => {
      this.componentProperies.add(value)
    })
  })
}
複製代碼
handleRefs () {
  /** * this.refs 是在 this.traverse遍歷時收集到的,而後將收集到的refs掛到class的屬性上 * 變成這樣 * class Index { * ..., * $$refs = [{ * type: "dom", * id: "隨機字符串", * refName: "", * fn: this.saveRef * }, { * type: "component", * id: "gMFQv", * refName: "title", * fn: null * }] * } */
  const objExpr = this.refs.map(ref => {
    return t.objectExpression([
      t.objectProperty(
        t.identifier('type'),
        t.stringLiteral(ref.type)
      ),
      t.objectProperty(
        t.identifier('id'),
        t.stringLiteral(ref.id)
      ),
      t.objectProperty(
        t.identifier('refName'),
        t.stringLiteral(ref.refName || '')
      ),
      t.objectProperty(
        t.identifier('fn'),
        ref.fn ? ref.fn : t.nullLiteral()
      )
    ])
  })

  this.classPath.node.body.body.push(t.classProperty(
    t.identifier('$$refs'),
    t.arrayExpression(objExpr)
  ))
}
複製代碼

終於來到了最後一部分,對模板進行生成。這裏引入了一個新模塊RenderParser

import { RenderParser } from './render'

parseRender () {
  if (this.renderMethod) {
    this.result.template = this.result.template
      + new RenderParser(
        this.renderMethod,
        this.methods,
        this.initState,
        this.jsxReferencedIdentifiers,
        this.usedState,
        this.loopStateName,
        this.customComponentNames,
        this.customComponentData,
        this.componentProperies,
        this.loopRefs
      ).outputTemplate
  } else {
    throw codeFrameError(this.classPath.node.loc, '沒有定義 render 方法')
  }
}
複製代碼

老規矩,先看構造函數

constructor ( renderPath: NodePath<t.ClassMethod>, methods: ClassMethodsMap, initState: Set<string>, referencedIdentifiers: Set<t.Identifier>, usedState: Set<string>, loopStateName: Map<NodePath<t.CallExpression>, string>, customComponentNames: Set<string>, customComponentData: Array<t.ObjectProperty>, componentProperies: Set<string>, loopRefs: Map<t.JSXElement, LoopRef> ) {
  this.renderPath = renderPath
  this.methods = methods
  this.initState = initState
  this.referencedIdentifiers = referencedIdentifiers
  this.loopStateName = loopStateName
  this.usedState = usedState
  this.customComponentNames = customComponentNames
  this.customComponentData = customComponentData
  this.componentProperies = componentProperies
  this.loopRefs = loopRefs
  const renderBody = renderPath.get('body')
  this.renderScope = renderBody.scope

  const [, error] = renderPath.node.body.body.filter(s => t.isReturnStatement(s))
  if (error) {
    throw codeFrameError(error.loc, 'render 函數頂級做用域暫時只支持一個 return')
  }
  // 上面定義一堆變量

  // 遍歷整個render函數進行一些處理
  renderBody.traverse(this.loopComponentVisitor)
  // 遍歷整個render函數進行一些處理
  this.handleLoopComponents()
  // 再遍歷整個render函數進行一些處理
  renderBody.traverse(this.visitors)
  // 解析ast生成wxml字符串設置到template上
  this.setOutputTemplate()
  // 清除全部jsx語法
  this.removeJSXStatement()
  // 生成$usedState
  this.setUsedState()
  this.setPendingState()
  // 生成$$events
  this.setCustomEvent()
  // 將 render 函數改爲 _createData
  this.createData()
  // 生成properties
  this.setProperies()
}
複製代碼

從結構上能夠看出,重點在 this.setOutputTemplate() 以前,以後的幾個函數都是在最後階段爲了知足運行時的一些需求給注入一些屬性參數

而前三個函數和咱們以前所講的內容基本都在作一樣的事,遍歷ast、修改ast,由於文章篇幅問題,雖然比較重要但我就不講了,若是你看懂了前面那這裏你直接去看代碼吧~比看我講來會得更快。

有了上面的結果後,咱們就能很輕鬆的處理wxml的生成了

setOutputTemplate () {
  this.outputTemplate = parseJSXElement(this.finalReturnElement)
}

// 根據配置生成 xml字符串 <div attr1="123" >value</div>
export const createHTMLElement = (options: Options) => {
}

// 將jsx數組轉成成wxml字符串
function parseJSXChildren (
  children: (t.JSXElement | t.JSXText | t.JSXExpressionContainer)[]
): string {
  return children
    .filter(child => {
      // 過濾掉全部空字符串節點
      return !(t.isJSXText(child) && child.value.trim() === '')
    })
    .reduce((str, child) => {
      // 若是是字符串,直接拼接
      if (t.isJSXText(child)) {
        return str + child.value.trim()
      }
      // 若是是JSX,經過parseJSXElement轉換成字符串
      if (t.isJSXElement(child)) {
        return str + parseJSXElement(child)
      }
      // 若是是JSX表達式容器 {xxx}
      if (t.isJSXExpressionContainer(child)) {
        // 容器的內容是JSX,經過parseJSXElement轉換成字符串
        if (t.isJSXElement(child.expression)) {
          return str + parseJSXElement(child.expression)
        }
        // 其餘狀況轉換成源代碼拼接上
        return str + `{${
          decodeUnicode(
            generate(child, {
              quotes: 'single',
              jsonCompatibleStrings: true
            })
            .code
          )
          // 去除this. this.state 這些,由於在小程序中wxml中不須要從this開始取值
          .replace(/(this\.props\.)|(this\.state\.)/g, '')
          .replace(/(props\.)|(state\.)/g, '')
          .replace(/this\./g, '')
        }}`
      }
      return str
    }, '')
}

export function parseJSXElement (element: t.JSXElement): string {
  const children = element.children
  const { attributes, name } = element.openingElement
  const TRIGGER_OBSERER = Adapter.type === Adapters.swan ? 'privateTriggerObserer' : '__triggerObserer'
  // <View.A /> 即便 JSX 成員表達式
  if (t.isJSXMemberExpression(name)) {
    throw codeFrameError(name.loc, '暫不支持 JSX 成員表達式')
  }
  const componentName = name.name
  const isDefaultComponent = DEFAULT_Component_SET.has(componentName)
  const componentSpecialProps = SPECIAL_COMPONENT_PROPS.get(componentName)
  let hasElseAttr = false
  attributes.forEach((a, index) => {
    if (a.name.name === Adapter.else && !['block', 'Block'].includes(componentName) && !isDefaultComponent) {
      hasElseAttr = true
      attributes.splice(index, 1)
    }
  })
  if (hasElseAttr) {
    // 若是有 esle 條件且沒有用block包裹起來就包上一層<block></block>
    return createHTMLElement({
      name: 'block',
      attributes: {
        [Adapter.else]: true
      },
      value: parseJSXChildren([element])
    })
  }
  let attributesTrans = {}
  if (attributes.length) {
    // 處理JSX的屬性
    attributesTrans = attributes.reduce((obj, attr) => {
      if (t.isJSXSpreadAttribute(attr)) {
        throw codeFrameError(attr.loc, 'JSX 參數暫不支持 ...spread 表達式')
      }
      let name = attr.name.name
      if (DEFAULT_Component_SET.has(componentName)) {
        // 將className改爲class
        if (name === 'className') {
          name = 'class'
        }
      }
      let value: string | boolean = true
      let attrValue = attr.value
      if (typeof name === 'string') {
        const isAlipayEvent = Adapter.type === Adapters.alipay && /(^on[A-Z_])|(^catch[A-Z_])/.test(name)
        if (t.isStringLiteral(attrValue)) {
          // 若是值是字符串,直接保留
          value = attrValue.value
        } else if (t.isJSXExpressionContainer(attrValue)) {
          // 若是值是jsx表達式容器
          let isBindEvent =
            (name.startsWith('bind') && name !== 'bind') || (name.startsWith('catch') && name !== 'catch')
          // 將表達式轉成代碼,而後一堆正則處理
          let code = decodeUnicode(generate(attrValue.expression, {
              quotes: 'single',
              concise: true
            }).code)
            .replace(/"/g, "'")
            .replace(/(this\.props\.)|(this\.state\.)/g, '')
            .replace(/this\./g, '')
          if (
            Adapters.swan === Adapter.type &&
            code !== 'true' &&
            code !== 'false' &&
            swanSpecialAttrs[componentName] &&
            swanSpecialAttrs[componentName].includes(name)
          ) {
            value = `{= ${code} =}`
          } else {
            if (Adapter.key === name) {
              const splitCode = code.split('.')
              if (splitCode.length > 1) {
                value = splitCode.slice(1).join('.')
              } else {
                value = code
              }
            } else {
              // 若是是事件就直接用 `code` 不然當字符串處理 `{{code}}`
              value = isBindEvent || isAlipayEvent ? code : `{{${code}}}`
            }
          }
          if (Adapter.type === Adapters.swan && name === Adapter.for) {
            value = code
          }
          if (t.isStringLiteral(attrValue.expression)) {
            // 若是自己就是字符串就直接使用
            value = attrValue.expression.value
          }
        } else if (attrValue === null && name !== Adapter.else) {
          // 處理隱式寫法 <View disabled /> => <View disabled="{{true}}">
          value = `{{true}}`
        }
        if (THIRD_PARTY_COMPONENTS.has(componentName) && /^bind/.test(name) && name.includes('-')) {
          name = name.replace(/^bind/, 'bind:')
        }
        if ((componentName === 'Input' || componentName === 'input') && name === 'maxLength') {
          // 單獨處理input maxLength
          obj['maxlength'] = value
        } else if (
          componentSpecialProps && componentSpecialProps.has(name) ||
          name.startsWith('__fn_') ||
          isAlipayEvent
        ) {
          obj[name] = value
        } else {
          // 將屬性名從駝峯改爲`-`
          obj[isDefaultComponent && !name.includes('-') && !name.includes(':') ? kebabCase(name) : name] = value
        }
      }
      if (!isDefaultComponent && !specialComponentName.includes(componentName)) {
        obj[TRIGGER_OBSERER] = '{{ _triggerObserer }}'
      }
      return obj
    }, {})
  } else if (!isDefaultComponent && !specialComponentName.includes(componentName)) {
    attributesTrans[TRIGGER_OBSERER] = '{{ _triggerObserer }}'
  }

  return createHTMLElement({
    // 將駝峯改爲 -
    name: kebabCase(componentName),
    attributes: attributesTrans,
    value: parseJSXChildren(children)
  })
}

複製代碼

因此其實能夠看出來,最終生成wxml沒有多麼高大上的代碼,也是經過遞歸加字符串拼接將代碼一點點拼上,不過之因此最後能這麼輕鬆其實主要是由於在ast語法轉換的過程當中將太多太多的問題都抹平了,將代碼變成了一個比較容易轉換的狀態。

寫在最後的話

第一次寫文章,很爛很是爛,比我平時本身在內心噴的那些爛文章還要爛。

無奈。掙扎了好久仍是發了,畢竟凡事都要有個開始。

2019~ []~( ̄▽ ̄)~*乾杯!

相關文章
相關標籤/搜索