【閱讀筆記】Taro轉小程式編譯原始碼解析

blankPen發表於2019-02-20

前言

這篇文章的主要是對taro/taro-tarnsformer-wx進行原始碼解析,對於想要了解Taro或者瞭解babel的人希望看了能得到一定的啟發。

由於我文筆實在太爛,所以整篇文章都是以閱讀筆記的形式展示,希望能對想了解taro編譯但是不太瞭解babel的人提供一個學習途徑。 如果有已經充分了解babel編譯的的大佬可以直接去看我fork的taro,在裡面我寫上了全部註釋希望能夠幫助到你~~

準備

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

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

開始

目錄

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

  • 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

然而我們真正主要關注的只有三個檔案

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

index.ts

我們先整體來分析下index.ts

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
}
複製程式碼

轉換第一步核心

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

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) {},
})
複製程式碼

我們從程式碼由上往下的方式一個一個來看

首先看對匯入語句的處理

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~ []~( ̄▽ ̄)~*乾杯!

相關文章