<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【基礎(chǔ)】1031- 前端也要懂編譯:AST 從入門到上手指南

          共 15796字,需瀏覽 32分鐘

           ·

          2021-07-27 22:10

          大廠技術(shù)  堅(jiān)持周更  精選好文


          閱讀文章之前,不妨打開手頭項(xiàng)目中的 package.json ,我們會發(fā)現(xiàn)眾多工具已經(jīng)占據(jù)了我們開發(fā)日常的各個角落,例如 JavaScript 轉(zhuǎn)譯、CSS 預(yù)處理、代碼壓縮、ESLint、Prettier等等。這些工具模塊大都不會交付到生產(chǎn)環(huán)境中,但它們的存在于我們的開發(fā)而言是不可或缺的。

          有沒有想過這些工具的功能是如何實(shí)現(xiàn)的呢?沒錯,抽象語法樹 (Abstract Syntax Tree) 就是上述工具的基石。

          AST 是什么 & 如何生成

          AST 是一種源代碼的抽象語法結(jié)構(gòu)的樹形表示。樹中的每個節(jié)點(diǎn)都表示源代碼中出現(xiàn)的一個構(gòu)造。

          那么 AST 是如何生成的?為什么需要 AST ?

          了解過編譯原理的同學(xué)知道計(jì)算機(jī)想要理解一串源代碼需要經(jīng)過“漫長”的分析過程:

          1. 詞法分析 (Lexical Analysis)
          2. 語法分析 (Syntax Analysis)
          3. ...
          4. 代碼生成 (Code Generation)

          • 詞法分析 其中詞法分析階段掃描輸入的源代碼字符串,生成一系列的詞法單元 (tokens),這些詞法單元包括數(shù)字,標(biāo)點(diǎn)符號,運(yùn)算符等。詞法單元之間都是獨(dú)立的,也即在該階段我們并不關(guān)心每一行代碼是通過什么方式組合在一起的。

          詞法分析階段——仿佛最初學(xué)英語時,將一個句子拆分成很多獨(dú)立的單詞,我們首先記住每一個單詞的類型和含義,但并不關(guān)心單詞之間的具體聯(lián)系。

          • 語法分析 接著,語法分析階段就會將上一階段生成的 token 列表轉(zhuǎn)換為如下圖右側(cè)所示的 AST,根據(jù)這個數(shù)據(jù)結(jié)構(gòu)大致可以看出轉(zhuǎn)換之前源代碼的基本構(gòu)造。

          語法分析階段——老師教會我們每個單詞在整個句子上下文中的具體角色和含義。

          • 代碼生成 最后是代碼生成階段,該階段是一個非常自由的環(huán)節(jié),可由多個步驟共同組成。在這個階段我們可以遍歷初始的 AST,對其結(jié)構(gòu)進(jìn)行改造,再將改造后的結(jié)構(gòu)生成對應(yīng)的代碼字符串

          代碼生成階段——我們已經(jīng)弄清楚每一條句子的語法結(jié)構(gòu)并知道如何寫出語法正確的英文句子,通過這個基本結(jié)構(gòu)我們可以把英文句子完美地轉(zhuǎn)換成一個中文句子或是文言文(如果你會的話)。

          AST 的基本結(jié)構(gòu)

          拋開具體的編譯器和編程語言,在 “AST 的世界”里所有的一切都是 節(jié)點(diǎn)(Node),不同類型的節(jié)點(diǎn)之間相互嵌套形成一顆完整的樹形結(jié)構(gòu)。


          {
            "program": {
              "type""Program",
              "sourceType""module",
              "body": [
                {
                  "type""FunctionDeclaration",
                  "id": {
                    "type""Identifier",
                    "name""foo"
                  },
                  "params": [
                    {
                      "type""Identifier",
                      "name""x"
                    }
                  ],
                  "body": {
                    "type""BlockStatement",
                    "body": [
                      {
                        "type""IfStatement",
                        "test": {
                          "type""BinaryExpression",
                          "left": {
                            "type""Identifier",
                            "name""x"
                          },
                          "operator"">",
                          "right": {
                            "type""NumericLiteral",
                            "value"10
                          }
                        }
                      }
                    ]
                  }
                  ...
                 }
                 ...
              ]
          }

          AST 的結(jié)構(gòu)在不同的語言編譯器、不同的編譯工具甚至語言的不同版本下是各異的,這里簡單介紹一下目前 JavaScript 編譯器遵循的通用規(guī)范 —— ESTree 中對于 AST 結(jié)構(gòu)的一些基本定義,不同的編譯工具都是基于此結(jié)構(gòu)進(jìn)行了相應(yīng)的拓展。


          AST 的用法 & 實(shí)戰(zhàn)??

          應(yīng)用場景和用法

          了解 AST 的概念和具體結(jié)構(gòu)后,你可能不禁會問:AST 有哪些使用場景,怎么用?

          開篇有提到,其實(shí)我們項(xiàng)目中的依賴和 VSCode 插件已經(jīng)揭曉了答案,AST 的應(yīng)用場景非常廣泛,以前端開發(fā)為例:

          • 代碼高亮、格式化、錯誤提示、自動補(bǔ)全等ESlintPrettierVetur等。
          • 代碼壓縮混淆uglifyJS等
          • 代碼轉(zhuǎn)譯webpackbabelTypeScript等

          至于如何使用 AST ,歸納起來可以把它的使用操作分為以下幾個步驟:


          1. 解析 (Parsing):這個過程由編譯器實(shí)現(xiàn),會經(jīng)過詞法分析過程和語法分析過程,從而生成 AST
          1. 讀取/遍歷 (Traverse):深度優(yōu)先遍歷 AST ,訪問樹上各個節(jié)點(diǎn)的信息(Node)。
          1. 修改/轉(zhuǎn)換 (Transform):在遍歷的過程中可對節(jié)點(diǎn)信息進(jìn)行修改,生成新的 AST
          1. 輸出 (Printing):對初始 AST 進(jìn)行轉(zhuǎn)換后,根據(jù)不同的場景,既可以直接輸出新的 AST,也可以轉(zhuǎn)譯成新的代碼塊。

          通常情況下使用 AST,我們重點(diǎn)關(guān)注步驟2和3,諸如 Babel、ESLint 等工具暴露出來的通用能力都是對初始 AST 進(jìn)行訪問和修改。

          這兩步的實(shí)現(xiàn)基于一種名為訪問者模式的設(shè)計(jì)模式,即定義一個 visitor 對象,在該對象上定義了對各種類型節(jié)點(diǎn)的訪問方法,這樣就可以針對不同的節(jié)點(diǎn)做出不同的處理。例如,編寫 Babel 插件其實(shí)就是在構(gòu)造一個 visitor 實(shí)例來處理各個節(jié)點(diǎn)信息,從而生成想要的結(jié)果。

          const visitor = {

              CallExpression(path) {

                  ...

              }

              FunctionDeclaration(path) {

                  ...

              }   

              ImportDeclaration(path) {

                  ...

              }

              ...

          }

          traverse(AST, visitor)

          實(shí)戰(zhàn)

          《說了一堆,一行代碼沒看見》,最后一部分我們來看如何使用 Bable 在 AST 上做一些“手腳”。

          開發(fā)工具

          • AST Explorer:在線 AST 轉(zhuǎn)換工具,集成了多種語言和解析器
          • @babel/parser :將 JS 代碼解析成對應(yīng)的 AST
          • @babel/traverse:對 AST 節(jié)點(diǎn)進(jìn)行遞歸遍歷
          • @babel/types:集成了一些快速生成、修改、刪除 AST Node的方法
          • @babel/generator :根據(jù)修改過后的 AST 生成新的 js 代碼

          ??

          目標(biāo):將所有函數(shù)中的普通 log 打印轉(zhuǎn)換成 error 打印,并在打印內(nèi)容前方附加函數(shù)名的字符串

          // Before

          function add(a, b{

              console.log(a + b)

              return a + b

          }



          // => After

          function add(a, b{

              console.error('add', a + b)

              return a + b

          }

          思路

          • 遍歷所有的函數(shù)調(diào)用表達(dá)式(CallExpression)節(jié)點(diǎn)
          • 將函數(shù)調(diào)用方法的屬性由 log 改為 error
          • 找到函數(shù)聲明(FunctionDeclaration)父節(jié)點(diǎn),提取函數(shù)名信息
          • 將函數(shù)名信息包裝成字符串字面量(StringLiteral)節(jié)點(diǎn),插入函數(shù)調(diào)用表達(dá)式的參數(shù)節(jié)點(diǎn)數(shù)組中
          const compile = (code) => {

            // 1. tokenizer + parser

            const ast = parser.parse(code)

            // 2. traverse + transform

            const visitor = {

              // 訪問函數(shù)調(diào)用表達(dá)式

              CallExpression(path) {

                const { callee } = path.node

                if (types.isCallExpression(path.node) && types.isMemberExpression(callee)) {

                  const { object, property } = callee

                  // 將成員表達(dá)式的屬性由 log -> error

                  if (object.name === 'console' && property.name === 'log') {

                    property.name = 'error'

                  } else {

                    return

                  }

                  // 向上遍歷,在該函數(shù)調(diào)用節(jié)點(diǎn)的父節(jié)點(diǎn)中找到函數(shù)聲明節(jié)點(diǎn)

                  const FunctionDeclarationNode = path.findParent(parent => {

                    return parent.type === 'FunctionDeclaration'

                  })

                  // 提取函數(shù)名稱信息,包裝成一個字符串字面量節(jié)點(diǎn),插入當(dāng)前節(jié)點(diǎn)的參數(shù)數(shù)組中

                  const funcNameNode = types.stringLiteral(FunctionDeclarationNode.node.id.name)

                  path.node.arguments.unshift(funcNameNode)

                }

              }

            }

            traverse.default(ast, visitor)

            // 3. code generator

            const newCode = generator.default(ast, {}, code).code

          }

          ????

          目標(biāo):為所有的函數(shù)添加錯誤捕獲,并在捕獲階段實(shí)現(xiàn)自定義的處理操作

          // Before

          function add(a, b{

            console.log('23333')

            throw new Error('233 Error')

            return a + b;

          }



          // => After

          function add(a, b{

            // 這里只能捕獲到同步代碼的執(zhí)行錯誤

            try {    

              console.log('23333')

              throw new Error('233 Error')

              return a + b;

            } catch (myError) {

                mySlardar(myError) // 自定義處理(eg:函數(shù)錯誤自動上報(bào))

            }

          }

          思路:

          • 遍歷函數(shù)聲明(FunctionDeclaration)節(jié)點(diǎn)
          • 提取該節(jié)點(diǎn)下整個代碼塊節(jié)點(diǎn),作為 try 語句tryStatement)處理塊中的內(nèi)容
          • 構(gòu)造一個自定義的 catch 子句(catchClause)節(jié)點(diǎn),作為 try 異常處理塊的內(nèi)容
          • 將整個 try 語句節(jié)點(diǎn)作為一個新的函數(shù)聲明節(jié)點(diǎn)的子節(jié)點(diǎn),用新生成的節(jié)點(diǎn)替換原有的函數(shù)聲明節(jié)點(diǎn)
          const compile = (code) => {

            // 1. tokenizer + parser

            const ast = parser.parse(code)

            // utils.writeAst2File(ast) // 查看 ast 結(jié)果

            // 2. traverse

            const visitor = {

              FunctionDeclaration(path) {

                const node = path.node

                const { params, id } = node // 函數(shù)的參數(shù)和函數(shù)體節(jié)點(diǎn)

                const blockStatementNode = node.body

                // 已經(jīng)有 try-catch 塊的停止遍歷,防止 circle loop

                if (blockStatementNode.body && types.isTryStatement(blockStatementNode.body[0])) {

                  return

                }

                // 構(gòu)造 cath 塊節(jié)點(diǎn)

                const catchBlockStatement = types.blockStatement(

                  [types.expressionStatement(

                    types.callExpression(types.identifier('mySlardar'), [types.identifier('myError')])

                  )]

                )

                // catch 子句節(jié)點(diǎn)

                const catchClause = types.catchClause(types.identifier('myError'), catchBlockStatement)

                // try 語句節(jié)點(diǎn)

                const tryStatementNode = types.tryStatement(blockStatementNode, catchClause)

                // try-catch 節(jié)點(diǎn)作為新的函數(shù)聲明節(jié)點(diǎn)

                const tryCatchFunctionDeclare = types.functionDeclaration(id, params, types.blockStatement([tryStatementNode]))

                path.replaceWith(tryCatchFunctionDeclare)

              }

            }

            traverse.default(ast, visitor)

            // 3. code generator

            const newCode = generator.default(ast, {}, code).code

          }

          ??????

          目標(biāo):在 webpack 中實(shí)現(xiàn) import 的按需導(dǎo)入(乞丐版 babel-import-plugin)

          // Before

          import { Button as Btn, Dialog } from '233_UI'

          import { HHH as hhh } from '233_UI'



          設(shè)置自定義參數(shù): 

          (moduleName) => `233_UI/lib/src/${moduleName}/${moduleName} `



          // => After

          import { Button as Btn } from "233_UI/lib/src/Button/Button"

          import { Dialog } from "233_UI/lib/src/Dialog/Dialog"

          import { HHH as hhh } from "233_UI/lib/src/HHH/HHH"

          思路:

          • 在插件運(yùn)行的上下文狀態(tài)中指定自定義的查找文件路徑規(guī)則
          • 遍歷 import 聲明節(jié)點(diǎn)(ImportDeclaration)
          • 提取 import 節(jié)點(diǎn)中所有被導(dǎo)入的變量節(jié)點(diǎn)(ImportSpecifier)
          • 將該節(jié)點(diǎn)的值通過查找文件路徑規(guī)則生成新的導(dǎo)入源路徑,有幾個導(dǎo)入節(jié)點(diǎn)就有幾個新的源路徑
          • 組合被導(dǎo)入的節(jié)點(diǎn)和源頭路徑節(jié)點(diǎn),生成新的 import 聲明節(jié)點(diǎn)并替換
          // 乞丐版按需引入 Babel 插件

          const visitor = ({types}) => {

            return {

              visitor: {

                ImportDeclaration(path, {opts}) {

                  const _getModulePath = opts.moduleName // 獲取模塊指定路徑,通過插件的參數(shù)傳遞進(jìn)來

                  

                  const importSpecifierNodes = path.node.specifiers // 導(dǎo)入的對象節(jié)點(diǎn)

                  const importSourceNode = path.node.source // 導(dǎo)入的來源節(jié)點(diǎn)

                  const sourceNodePath = importSourceNode.value

                  // 已經(jīng)成功替換的節(jié)點(diǎn)不再遍歷

                  if (!opts.libaryName || sourceNodePath !== opts.libaryName) {

                    return

                  }

                  

                  const modulePaths = importSpecifierNodes.map(node => {

                    return _getModulePath(node.imported.name)

                  })

                  const newImportDeclarationNodes = importSpecifierNodes.map((node, index) => {

                    return types.importDeclaration([node], types.stringLiteral(modulePaths[index]))

                  })

                  path.replaceWithMultiple(newImportDeclarationNodes)

                }

              }

            }

          }



          const result = babel.transform(code, {

            plugins: [

              [

                visitor,

                {

                  libaryName'233_UI',

                  moduleNamemoduleName => `233_UI/lib/src/${moduleName}/${moduleName}`

                }

              ]

            ]

          })

          上述三個??的詳細(xì)代碼和運(yùn)行示例的倉庫地址見 https://github.com/xunhui/ast_js_demo[1]

          總結(jié)

          或許我們的日常工作和 AST 打交道的機(jī)會并不多,更不會刻意地去關(guān)注語言底層編譯器的原理,但了解 AST 可以幫助我們更好地理解日常開發(fā)工具的原理,更輕松地上手這些工具暴露的 API。

          工作的每一天,我們的喜怒哀樂通過一行又一行的代碼向眼前的機(jī)器傾訴。它到底是怎么讀懂你的情愫,又怎么給予你相應(yīng)的回應(yīng),這是一件非常值得探索的事情:)

          參考

          ASTs - What are they and how to use them[2]

          AST 實(shí)現(xiàn)函數(shù)錯誤自動上報(bào)[3]

          Babel Handbook[4]

          參考資料

          [1]

          https://github.com/xunhui/ast_js_demo: https://github.com/xunhui/ast_js_demo

          [2]

          ASTs - What are they and how to use them: https%3A%2F%2Fwww.twilio.com%2Fblog%2Fabstract-syntax-trees

          [3]

          AST 實(shí)現(xiàn)函數(shù)錯誤自動上報(bào): https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000037630766

          [4]

          Babel Handbook: https%3A%2F%2Fgithub.com%2Fjamiebuilds%2Fbabel-handbook

          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
          4. 正則 / 框架 / 算法等 重溫系列(16篇全)
          5. Webpack4 入門(上)|| Webpack4 入門(下)
          6. MobX 入門(上) ||  MobX 入門(下)
          7. 120+篇原創(chuàng)系列匯總

          回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~

          點(diǎn)擊“閱讀原文”查看 120+ 篇原創(chuàng)文章

          瀏覽 146
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  色欲av伊人网在线 | 国产精品中文字幕在线观看 | 黄色av网 | 色老板新网址 | 人人做人人摸 |