1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        【工程化】前端也要懂編譯:AST 從入門到上手指南

        共 15509字,需瀏覽 32分鐘

         ·

        2021-04-27 00:51

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

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

        AST 是什么 & 如何生成

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

        • 代碼高亮、格式化、錯(cuò)誤提示、自動(dòng)補(bǔ)全等ESlint、Prettier、Vetur等。
        • 代碼壓縮混淆uglifyJS等。
        • 代碼轉(zhuǎn)譯webpack、babelTypeScript等。

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


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

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

        這兩步的實(shí)現(xiàn)基于一種名為訪問者模式的設(shè)計(jì)模式,即定義一個(gè) visitor 對(duì)象,在該對(duì)象上定義了對(duì)各種類型節(jié)點(diǎn)的訪問方法,這樣就可以針對(duì)不同的節(jié)點(diǎn)做出不同的處理。例如,編寫 Babel 插件其實(shí)就是在構(gòu)造一個(gè) visitor 實(shí)例來處理各個(gè)節(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 代碼解析成對(duì)應(yīng)的 AST
        • @babel/traverse:對(duì) 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ù)名稱信息,包裝成一個(gè)字符串字面量節(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ù)添加錯(cuò)誤捕獲,并在捕獲階段實(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í)行錯(cuò)誤

          try {    

            console.log('23333')

            throw new Error('233 Error')

            return a + b;

          } catch (myError) {

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

          }

        }

        思路:

        • 遍歷函數(shù)聲明(FunctionDeclaration)節(jié)點(diǎn)
        • 提取該節(jié)點(diǎn)下整個(gè)代碼塊節(jié)點(diǎn),作為 try 語句tryStatement)處理塊中的內(nèi)容
        • 構(gòu)造一個(gè)自定義的 catch 子句(catchClause)節(jié)點(diǎn),作為 try 異常處理塊的內(nèi)容
        • 將整個(gè) try 語句節(jié)點(diǎn)作為一個(gè)新的函數(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)入源路徑,有幾個(gè)導(dǎo)入節(jié)點(diǎn)就有幾個(gè)新的源路徑
        • 組合被導(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)入的對(duì)象節(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}`

              }

            ]

          ]

        })

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

        總結(jié)

        或許我們的日常工作和 AST 打交道的機(jī)會(huì)并不多,更不會(huì)刻意地去關(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ù)錯(cuò)誤自動(dòng)上報(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ù)錯(cuò)誤自動(dòng)上報(bào): https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000037630766

        [4]

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



        瀏覽 66
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            处破初破苞一区二区三区最新章节 | 啊啊啊啊啊啊用力 | 国产清纯白嫩高中生在线播放 | 被男人狂揉吃奶胸60分钟 | 初尝人妻滑进去了莹莹视频 | 黄色污污视频网站在线观看 | 欧美男同gay巨大男吊 | 欧美精黑人一级A片蜜桃视频 | 成人做爱毛片在线视频播放器 | A片一区 免费无遮挡 视频网站用片海 |