Taro3無埋點的探索與實踐

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"引言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於Taro框架,相信大多數小程序開發者都是有一定了解的。藉助Taro框架,開發者們可以使用React進行小程序的開發,並實現一套代碼就能夠適配到各端小程序。這種促使開發成本降低的能力使得Taro被各大小程序開發者所使用。使用Taro打包出來的小程序和原生相比是有一定區別的,GrowingIO小程序的原生SDK還不足以直接在Taro中使用,需要針對其框架的特別進行適配。這點在Taro2時期已經是實現完美適配的,但在Taro3之後,由於Taro團隊對其整體架構的調整,使得之前的方式已經無法實現準確的無埋點,促使了本次探索。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"背景","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GrowingIO小程序SDK無埋點功能的實現有兩個核心問題:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"如何攔截到用戶事件的觸發方法","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如何爲節點生成一個唯一且穩定的標識符","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只要能處理好這兩個問題,那就能實現一個穩定小程序無埋點SDK。在Taro2中,框架在編譯期和運行期有不同的工作內容。其中編譯時主要是將 Taro 代碼通過 Babel 轉換成小程序的代碼,如:JS、WXML、WXSS、JSON。在運行時Taro2提供了兩個核心ApicreateApp,createComponent,分別用來創建小程序App和實現小程序頁面的構建。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GrowingIO 小程序SDK通過重寫createComponent方法實現了對頁面中用戶事件的攔截,攔截到方法後便能在事件觸發的時候獲取到觸發節點信息和方法名,若節點存在id,則用id+方法名作爲標識符,否則就直接使用方法名作爲標識符。這裏方法名獲取上sdk並沒有任何處理,因爲在Taro2的編譯期已經做好了這一系列的工作,它會將用戶方法名完整的保留下來,並且對於匿名方法,箭頭函數也會進行編號賦予合適的方法名。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是在Taro3之後,Taro的整個核心發生了巨大的變化,不論是編譯期還是運行期和之前都是不一樣的。createApp和createComponent接口也不再提供,編譯期也會對用戶方法進行壓縮,不在保留用戶方法名也不會對匿名方法進行編號。這樣就導致現有GrowingIO 小程序SDK無法在Taro3上實現無埋點能力。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"問題分析","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在面對Taro3的這種變化,GrowingIO之前也做過適配。在分析Taro3運行期的代碼中發現,Taro3會爲頁面內所有節點分配一個相對穩定的id,並且節點上的所有事件監聽方法都是頁面實例中的eh方法。在此條件下之前的GrowingIO便是按照原生小程序SDK的處理方式攔截該eh方法,在用戶事件觸發的時候獲取到節點上的id以生成唯一標識符。這種處理方式在一定程度上也是解決了無埋點SDK的兩個核心問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不難想到,GrowingIO之前的處理方式上,是沒辦法做到獲取一個穩定的節點標識符的。當頁面中節點的順序發生變化,或者動態的增刪了部分節點,這時Taro3都會給節點分配一個新的id,這樣的話那就無法提供一個穩定的標識符了,導致之前圈選定義的無埋點事件失效。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果想處理掉已定義無埋點事件失效問題,那就必須能提供一個穩定的標識符。類比與在Taro2上的實現,如果也能在攔截到事件觸發的時候獲取到用戶方法名,那就可以了。也就是說只要能把以下兩個問題處理掉,便能實現這個目標了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"運行時SDK能攔截用戶方法","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"能在生產環境將用戶方法名保留下來","attrs":{}}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"逐一攻破","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"獲取用戶方法","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先看第一個問題,SDK如何獲取到用戶綁定的方法,並攔截它。分析下Taro3的源碼,不難就能解決掉。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有的頁面配置都是通過","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/NervJS/taro/blob/c064f62ca1fab16d663fb3bae04431f786118f7f/packages/taro-runtime/src/dsl/common.ts#L91","title":"https://github.com/NervJS/taro/blob/c064f62ca1fab16d663fb3bae04431f786118f7f/packages/taro-runtime/src/dsl/common.ts#L91","type":null},"content":[{"type":"text","text":"createPageConfig","attrs":{}}]},{"type":"text","text":"方法返回的,每個page配置都會有一個eh,從這裏下手便能獲取到綁定的方法。可見taro-runtime源碼中的 ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/NervJS/taro/blob/f161ced66af171c74ba596c0d66baefb7a52609e/packages/taro-runtime/src/dom/event.ts#L93","title":"https://github.com/NervJS/taro/blob/f161ced66af171c74ba596c0d66baefb7a52609e/packages/taro-runtime/src/dom/event.ts#L93","type":null},"content":[{"type":"text","text":"eventHandler","attrs":{}}]},{"type":"text","text":",","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/NervJS/taro/blob/e7ec85f2465deb9fcf3d9e281d48876e50a788b3/packages/taro-runtime/src/dom/element.ts#L173","title":"https://github.com/NervJS/taro/blob/e7ec85f2465deb9fcf3d9e281d48876e50a788b3/packages/taro-runtime/src/dom/element.ts#L173","type":null},"content":[{"type":"text","text":"dispatchEvent","attrs":{}}]},{"type":"text","text":"方法。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// page配置中的eh即爲該方法\nexport function eventHandler (event: MpEvent) {\n if (event.currentTarget == null) {\n event.currentTarget = event.target\n }\n // 運行時的document是Taro3.0定義的,可以獲取虛擬dom中的節點\n const node = document.getElementById(event.currentTarget.id)\n if (node != null) {\n // 觸發事件\n node.dispatchEvent(createEvent(event, node))\n }\n}\n\n// 在看看dispatchEvent方法,簡化後\nclass TaroElement extends TaroNode {\n ...\n public dispatchEvent (event: TaroEvent) {\n const cancelable = event.cancelable\n // 這個__handlers屬性是關鍵,這裏保存着該節點上所有監聽方法\n const listeners = this.__handlers[event.type]\n \n // ...省略很多\n return listeners != null\n }\n ...\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"__handlers具體結構如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2e/2e332e1d62f3d2d2ec12ea72b0667085.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模仿這個過程,就能拿到用戶綁定的方法了。那應該怎麼模仿呢?如何才能切入這個過程中?再觀察可以發現運行時的document不在是小程序內置的了,而是Taro3通過ProvidePlugin提供的(可見Taro3的taro-runtime包中README),這裏基本都是將dom中各類的實現了一遍。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在看dispatchEvent這個方法,想一下如果我們切入這個方法,那豈不是就能複製以上的過程來獲取到__handlers了,同時也實現了事件的攔截。根據document的繼承關係,通過原型鏈就能實現,如下:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"function hookDispatchEvent(dispatch) {\n return function() {\n const event = arguments[0]\n let node = document.getElementById(event.currentTarget.id)\n // 這就把觸發元素上的綁定的方法拿到了\n let handlers = node.__handlers\n ...\n return dispatch.apply(this, arguments)\n }\n}\n\n// 判斷是不是在Taro3環境中\nif (document?.tagName === '#DOCUMENT' && !!document.getElementById) {\n const TaroNode = document.__proto__.__proto__\n const dispatchEvent = TaroNode.dispatchEvent\n Object.defineProperty(TaroNode, 'dispatchEvent', {\n value: hookDispatchEvent(dispatchEvent),\n enumerable: false,\n configurable: false\n })\n}","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"保留方法名","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先來看看現狀吧,在上面的步驟中已經可以拿到用戶方法了,用戶方法主要分爲以下幾類:","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"方法分類","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具名方法","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"function signName() {}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"匿名方法","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const anonymousFunction = function () {}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"箭頭函數","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const arrowsFunction = () => {}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"內聯箭頭函數","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":" {}}>","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類方法","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"class Index extends Component {\n hasName() {}\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"class fields語法方法","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"class Index extends Component {\n arrowFunction = () => {}\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於具名方法和類方法都是可以通過Function.name來獲取到方法名的,但是其他幾種就沒法直接獲取到了。那如何才能獲取這些方法的名字呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照當前可操作的內容,想要在運行期拿到這些方法的方法名那已經是不可能實現的事情了。因爲Taro3在生成環境中會進行壓縮,而且對於匿名方法也不會像Taro2那樣爲其進行編號。那既然運行期做不到,就只能把目光聚焦到編譯期來處理了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"留下方法名","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Taro3在編譯期還是要藉助Babel來處理的,那如果實現一個Babel插件來把這些匿名方法賦予一個合適的方法名那不就能把這個問題處理掉了嗎。插件開發指南可以參考","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md","title":"","type":null},"content":[{"type":"text","text":"handbook","attrs":{}}]},{"type":"text","text":",可以通過AST explorer直觀的看到這棵樹的結構。瞭解了babel插件的基本開發,下面就是要選擇一個合適的時機去訪問這棵樹。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在最初考慮是把訪問點設置爲Function,這樣不論什麼類型的方法,都是可以攔截到,然後再根據一定規則將方法名保留下來。這個思路是沒有問題的,並且嘗試實現後也是可以使用的,但它會有以下兩點問題:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"範圍太大,把非事件監聽的方法也給轉化了,這是不必要的","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"面對代碼壓縮依舊是無能爲力,只能通過配置保留函數名的壓縮方式來處理,對最終包體積造成一定影響","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讓我們在分析下JSX語法吧,想一下所有的用戶方法都是要通過onXXX的形式爲元素綁定監聽,如下","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下圖爲其AST結構,由此可以想到把訪問點設置爲JSXAttribute,並只需對其value值的方法賦予合適的名字就行了。JSX相關的類型可見","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/facebook/jsx/blob/master/AST.md","title":"","type":null},"content":[{"type":"text","text":"jsx/AST.md · GitHub","attrs":{}}]}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f3/f37f89b13021db5d59624ce99cee1c9a.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"插件的整體框架可以如下","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"function visitorComponent(path, state) {\n path.traverse({\n // 訪問元素的屬性\n JSXAttribute(path) {\n let attrName = path.get('name').node.name\n let valueExpression = path.get('value.expression')\n if (!/^on[A-Z][a-zA-Z]+/.test(attrName)) return\n \n // 在這裏爲用戶方法設置名字即可\n replaceWithCallStatement(valueExpression)\n }\n })\n}\n\nmodule.exports = function ({ template }) {\n return {\n name: 'babel-plugin-setname',\n // React的組件可以Class和Function\n // 在組件內部在進行JSXAttribute的訪問\n visitor: {\n Function: visitorComponent,\n Class: visitorComponent\n }\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只要插件處理好JSXAttribute中value表達式,能爲各種類型的用戶方法設置合適的方法名,就能完成保留方法名的這一任務了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Babel插件功能實現","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"插件主要實現以下幾部分功能","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"訪問JSXAttribute中用戶方法","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獲取合適的方法名","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注入設置方法名的代碼","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最終效果如下","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/17/17176bb04b62f687f5ef8fc3c37131eb.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"_GIO_DI_NAME_通過Object.defineProperty爲函數設置了方法名。插件提供了默認實現,也可以自定義。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"Object.defineProperty(func, 'name', {\n value: name,\n writable: false,\n configurable: false\n})","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能會發現轉化後的代碼中handleClick已經是具名的了,再set下不就多此一舉嗎。但是可別忘了生產環境的代碼還是要壓縮的,這樣函數名可就不知道會是啥了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面分別介紹針對不同的事件綁定方式的處理,基本涵蓋的React中的各種寫法。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"標識符","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"標識符是指在jsx屬性上使用的標識符,函數具體如何聲明不限。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"AST結構如下","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c1/c1760569a105eeb204d0862551b42052.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這時方法名直接取標識符的name值即可。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"成員表達式","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"普通成員表達式","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如以下成員表達式內的方法","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"會被轉化爲如下形式","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"_reactJsxRuntime.jsx(Button, {onClick: GIO_DI_NAME(\"parent_props_arrowsFunction\", parent.props.arrowsFunction)})","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"成員表達式的AST結構大致是這樣的,插件會取所有成員標識符,並以","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"_","attrs":{}}],"attrs":{}},{"type":"text","text":"連接作爲方法名。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/72/724167a49ff0fdd9cb658d6acbf1a0ed.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"this成員表達式","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"this表達式會進行特殊處理,將不會保留this取其餘部分,如下","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"會被轉換爲","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"_reactJsxRuntime.jsx(Button, {\n onClick: _GIO_DI_NAME_(\"arrowsFunction\", this.arrowsFunction)\n})","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"函數執行表達式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行表達式就是函數的調用,形如","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的bind()就是一個CallExpression,插件處理後會有以下結果","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"_reactJsxRuntime.jsx(\"button\", {\n onClick: _GIO_DI_NAME_(\"handlerClick\", this.handlerClick.bind(this))\n})","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行表達式可能是比較複雜的,比如一個頁面中幾個監聽函數是同一個高階函數使用不同參數生成的,這時是需要保留參數信息的。如下","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"需要被轉化爲以下形式","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// getHandler('tab1')\n_reactJsxRuntime.jsx(Button, {\n onClick: _GIO_DI_NAME_(\"getHandler$tab1\", getHandler('tab1')),\n children: \"\"\n})\n// getHandler(h1)\n_reactJsxRuntime.jsx(Button, {\n onClick: _GIO_DI_NAME_(\"getHandler$h1\", getHandler(h1)),\n children: \"\"\n})\n// getHandler(['test'])\n_reactJsxRuntime.jsx(Button, {\n onClick: _GIO_DI_NAME_(\"getHandler$$$1\", getHandler(['test'])),\n children: \"\"\n})","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對不同的參數類型會有不同的處理方式,整體思路就是把高階函數名和參數進行拼接組成方法名。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個CallExpression的AST結構如下","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c3/c3d06b53596b47aed05d5ff00f3ea85f.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據AST結構,對不同參數處理邏輯代碼可見插件源碼:transform.js [60-73]","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面說的都只是直接的函數執行表達式,再考慮以下情況","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"觀察下這裏的AST結構,callee部分將是一個成員表達式,這裏的取值將按照上面的成員表達式來","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d1/d1b81d1ea066cde471b7e28198f7f315.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉換後結果如下","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"_reactJsxRuntime.jsx(Button, {\n onClick: _GIO_DI_NAME_(\"factory_buildHandler$tab2\", factory.buildHandler('tab2')),\n children: \"\"\n})","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"函數表達式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"函數處理起來就有點小麻煩了,先看下有幾種形式","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n// 上面兩種估計沒人會寫,下面將是最常見的\n
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章