<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>

          babel原理&plugin實(shí)戰(zhàn)

          共 7139字,需瀏覽 15分鐘

           ·

          2020-12-21 15:58

          點(diǎn)擊上方藍(lán)字關(guān)注我們




          本文將講解babel是如何運(yùn)行的,AST的結(jié)構(gòu),以及怎么創(chuàng)建一個(gè)babel的插件。


          再講babel之前,先不講babel,AST的這些概念,先帶你實(shí)現(xiàn)一個(gè)簡(jiǎn)易的babel解析器,這樣再回過(guò)頭來(lái)講這些概念就容易理解多了。


          tiny-compiler 編譯器


          想象一下我們有一些新特性的語(yǔ)法,其中add subtract是普通的函數(shù)名,需要轉(zhuǎn)義到正常的javascript語(yǔ)法,以便讓瀏覽器能夠兼容的運(yùn)行。

          (add 2 2)(subtract 4 2)(add 2 (subtract 4 2))


          要轉(zhuǎn)義成如下


          add(2,?2)subtract(4, 2)add(2,?subtract(4,?2))

          編譯器都分為三個(gè)步驟:



          • Parsing 解析

          • Transformation 轉(zhuǎn)義

          • Code Generation 代碼生成



          Parsing 解析


          Parsing階段分成兩個(gè)子階段,


          • Lexical Analysis 詞法分析

          • Syntactic Analysis語(yǔ)法分析,先寫(xiě)好我們要轉(zhuǎn)化的代碼

          // 這是我們要轉(zhuǎn)化的code(add 2 (subtract 4 2))

          Lexical Analysis 詞法分析


          Lexical Analysis 詞法分析可以理解為把代碼拆分成最小的獨(dú)立的語(yǔ)法單元,去描述每一個(gè)語(yǔ)法,可以是操作符,數(shù)字,標(biāo)點(diǎn)符號(hào)等,最后生成token數(shù)組。

          // 第一步,Lexical Analysis,轉(zhuǎn)化成tokens類似如下[    { type: 'paren', value: '(' },    { type: 'name', value: 'add' },    { type: 'number', value: '2' },    { type: 'paren', value: '(' },    { type: 'name', value: 'subtract' },    { type: 'number', value: '4' },    { type: 'number', value: '2' },    { type: 'paren', value: ')' },    { type: 'paren', value: ')' },]


          那我們開(kāi)始實(shí)現(xiàn)它吧,干!

          function tokenizer(input) {    let current = 0;    let tokens = [];    while (current < input.length) {        let char = input[current];        // 處理(        if (char === '(') {            tokens.push({                type: 'paren',                value: '(',            });            current++;            continue;        }        // 處理)        if (char === ')') {            tokens.push({                type: 'paren',                value: ')',            });            current++;            continue;        }        // 處理空白字符        let WHITESPACE = /\s/;        if (WHITESPACE.test(char)) {            current++;            continue;        }        // 處理數(shù)字        let NUMBERS = /[0-9]/;        if (NUMBERS.test(char)) {            let value = '';            while (NUMBERS.test(char)) {                value += char;                char = input[++current];            }            tokens.push({ type: 'number', value });            continue;        }        // 處理字符串        if (char === '"') {            let value = '';            char = input[++current];            while (char !== '"') {                value += char;                char = input[++current];            }            char = input[++current];            tokens.push({ type: 'string', value });            continue;        }        // 處理函數(shù)名        let LETTERS = /[a-z]/i;        if (LETTERS.test(char)) {            let value = '';            while (LETTERS.test(char)) {                value += char;                char = input[++current];            }            tokens.push({ type: 'name', value });            continue;        }        // 報(bào)錯(cuò)提示        throw new TypeError('I dont know what this character is: ' + char);    }    return tokens;}

          Syntactic Analysis 語(yǔ)法分析


          Syntactic Analysis 語(yǔ)法分析就是根據(jù)上一步的tokens數(shù)組轉(zhuǎn)化成語(yǔ)法之前的關(guān)系,這就是Abstract Syntax Tree,也就是我們常說(shuō)的AST

          // 第二步,Syntactic Analysis,轉(zhuǎn)化成AST類似如下{    type: 'Program',    body: [{        type: 'CallExpression',        name: 'add',        params: [{            type: 'NumberLiteral',            value: '2',        }, {            type: 'CallExpression',            name: 'subtract',            params: [{                type: 'NumberLiteral',                value: '4',            }, {                type: 'NumberLiteral',                value: '2',            }]        }]    }]}


          我們?cè)賮?lái)實(shí)現(xiàn)一個(gè)parser,轉(zhuǎn)化成AST。

          function parser(tokens) {    let current = 0;    function walk() {        let token = tokens[current];        // 處理數(shù)字        if (token.type === 'number') {            current++;            return {                type: 'NumberLiteral',                value: token.value,            }        }        // 處理字符串        if (token.type === 'string') {            current++;            return {                type: 'StringLiteral',                value: token.value,            };        }        // 處理括號(hào)表達(dá)式        if (            token.type === 'paren' &&            token.value === '('        ) {            token = tokens[++current];            let node = {                type: 'CallExpression',                name: token.value,                params: [],            };            token = tokens[++current];            while (                (token.type !== 'paren') ||                (token.type === 'paren' && token.value !== ')')            ) {                node.params.push(walk());                token = tokens[current];            }            current++;            return node;        }        throw new TypeError(token.type);    }    let ast = {        type: 'Program',        body: [],    };    while (current < tokens.length) {        ast.body.push(walk());    }    return ast;}

          從上述代碼來(lái)看,跟階段AST是根節(jié)點(diǎn)是type=Program,body是一個(gè)嵌套的AST數(shù)組結(jié)構(gòu)。再單獨(dú)處理了number和string類型之后,再遞歸的調(diào)用walk函數(shù),以解決嵌套的括號(hào)表達(dá)式。


          Transformation 轉(zhuǎn)義


          traverser 遍歷器


          我們最終的目的肯定是想轉(zhuǎn)化成我們想要的代碼,那怎么轉(zhuǎn)化呢?答案就是更改我們剛剛得到的AST結(jié)構(gòu)。

          那怎么去改AST呢?直接去操作這個(gè)樹(shù)結(jié)構(gòu)肯定是不現(xiàn)實(shí)的,所以我們需要遍歷這個(gè)AST,利用深度優(yōu)先遍歷的方法遍歷這些節(jié)點(diǎn),當(dāng)遍歷到某個(gè)節(jié)點(diǎn)時(shí),再去調(diào)用這個(gè)節(jié)點(diǎn)對(duì)應(yīng)的方法,再方法里面改變這些節(jié)點(diǎn)的值就輕而易舉了。

          想象一下我們有這樣的一個(gè)visitor,就是上文說(shuō)道的遍歷時(shí)調(diào)用的方法

          var visitor = {    NumberLiteral: {        enter(node, parent) { },        exit(node, parent) { },    }};


          由于深度優(yōu)先遍歷的特性,我們遍歷到一個(gè)節(jié)點(diǎn)時(shí)有enterexit的概念,代表著遍歷一些類似于CallExpression這樣的節(jié)點(diǎn)時(shí),這個(gè)語(yǔ)句,enter表示開(kāi)始解析,exit表示解析完畢。比如說(shuō)上文中:

           *   -> Program (enter) *     -> CallExpression (enter) *       -> Number Literal (enter) *       <- Number Literal (exit) *       -> Call Expression (enter) *          -> Number Literal (enter) *          <- Number Literal (exit) *          -> Number Literal (enter) *          <- Number Literal (exit) *       <- CallExpression (exit) *     <- CallExpression (exit) *   <- Program (exit)

          然后有一個(gè)函數(shù),接受astvistor作為參數(shù),實(shí)現(xiàn)遍歷,類似于:

          traverse(ast, {    CallExpression: {        enter(node, parent) {            // ...        },        exit(node, parent) {            // ...        },    }})

          先實(shí)現(xiàn)traverser吧。

          function traverser(ast, visitor) {    // 遍歷一個(gè)數(shù)組節(jié)點(diǎn)    function traverseArray(array, parent) {        array.forEach(child => {            traverseNode(child, parent);        });    }    // 遍歷節(jié)點(diǎn)    function traverseNode(node, parent) {        let methods = visitor[node.type];        // 先執(zhí)行enter方法        if (methods && methods.enter) {            methods.enter(node, parent);        }        switch (node.type) {            // 一開(kāi)始節(jié)點(diǎn)的類型是Program,去接著解析body字段            case 'Program':                traverseArray(node.body, node);                break;            // 當(dāng)節(jié)點(diǎn)類型是CallExpression,去解析params字段            case 'CallExpression':                traverseArray(node.params, node);                break;            // 數(shù)字和字符串沒(méi)有子節(jié)點(diǎn),直接執(zhí)行enter和exit就好            case 'NumberLiteral':            case 'StringLiteral':                break;            // 容錯(cuò)處理            default:                throw new TypeError(node.type);        }        // 后執(zhí)行exit方法        if (methods && methods.exit) {            methods.exit(node, parent);        }    }    // 開(kāi)始從根部遍歷    traverseNode(ast, null);}


          transformer 轉(zhuǎn)換器


          有了traverser遍歷器后,就開(kāi)始遍歷吧,先看看前后兩個(gè)AST的對(duì)比。



          * ---------------------------------------------------------------------------- * Original AST | Transformed AST * ---------------------------------------------------------------------------- * { | { * type: 'Program', | type: 'Program', * body: [{ | body: [{ * type: 'CallExpression', | type: 'ExpressionStatement', * name: 'add', | expression: { * params: [{ | type: 'CallExpression', * type: 'NumberLiteral', | callee: { * value: '2' | type: 'Identifier', * }, { | name: 'add' * type: 'CallExpression', | }, * name: 'subtract', | arguments: [{ * params: [{ | type: 'NumberLiteral', * type: 'NumberLiteral', | value: '2' * value: '4' | }, { * }, { | type: 'CallExpression', * type: 'NumberLiteral', | callee: { * value: '2' | type: 'Identifier', * }] | name: 'subtract' * }] | }, * }] | arguments: [{ * } | type: 'NumberLiteral', * | value: '4' * ---------------------------------- | }, { * | type: 'NumberLiteral', * | value: '2' * | }] * (sorry the other one is longer.) | } * | } * | }] * | } * ----------------------------------------------------------------------------?*/


          這里注意多了一中ExpressionStatement的type,以表示subtract(4, 2)這樣的結(jié)構(gòu)。

          遍歷的過(guò)程就是把左側(cè)AST轉(zhuǎn)化成右側(cè)AST


          function?transformer(ast)?{    let newAst = {        type: 'Program',        body: [],    };    // 給節(jié)點(diǎn)一個(gè)_context,讓遍歷到子節(jié)點(diǎn)時(shí)可以push內(nèi)容到parent._context中    ast._context = newAst.body;    traverser(ast, {        CallExpression: {            enter(node, parent) {                let expression = {                    type: 'CallExpression',                    callee: {                        type: 'Identifier',                        name: node.name,                    },                    arguments: [],                };                // 讓子節(jié)點(diǎn)可以push自己到expression.arguments中                node._context = expression.arguments;                // 如果父節(jié)點(diǎn)不是CallExpression,則外層包裹一層ExpressionStatement                if (parent.type !== 'CallExpression') {                    expression = {                        type: 'ExpressionStatement',                        expression: expression,                    };                }                parent._context.push(expression);            }        },        NumberLiteral: {            enter(node, parent) {                parent._context.push({                    type: 'NumberLiteral',                    value: node.value,                });            }        },        StringLiteral: {            enter(node, parent) {                parent._context.push({                    type: 'StringLiteral',                    value: node.value,                });            },        },    });    return newAst;}


          CodeGeneration 代碼生成


          那最后一個(gè)階段就是用心生成的AST生成我們最后的代碼了,也是生成AST的一個(gè)反過(guò)程。


          function codeGenerator(node) {    switch (node.type) {        // 針對(duì)于Program,處理其中的body屬性,依次再遞歸調(diào)用codeGenerator        case 'Program':            return node.body.map(codeGenerator)                .join('\n');        // 針對(duì)于ExpressionStatement,處理其中的expression屬性,再后面添加一個(gè)分號(hào)        case 'ExpressionStatement':            return (                codeGenerator(node.expression) +                ';'            );        // 針對(duì)于CallExpression,左側(cè)處理callee,括號(hào)中處理arguments數(shù)組        case 'CallExpression':            return (                codeGenerator(node.callee) +                '(' +                node.arguments.map(codeGenerator)                    .join(', ') +                ')'            );        // 直接返回name        case 'Identifier':            return node.name;        // 返回?cái)?shù)字的value        case 'NumberLiteral':            return node.value;        // 字符串類型添加雙引號(hào)        case 'StringLiteral':            return '"' + node.value + '"';        // 容錯(cuò)處理        default:            throw new TypeError(node.type);    }}




          總結(jié)


          這樣我們一個(gè)tiny-compiler就寫(xiě)好了,最后可以執(zhí)行下面的代碼去試試?yán)病?/p>



          function compiler(input) { let tokens = tokenizer(input); let ast = parser(tokens); let newAst = transformer(ast); let output = codeGenerator(newAst); return output;}


          從上述代碼中就可以看出來(lái),一個(gè)代碼轉(zhuǎn)化的過(guò)程就把包括了tokenizer詞法分析階段,parser預(yù)發(fā)分析階段(AST生成),transformer轉(zhuǎn)義階段,codeGenerator代碼生成階段。那么在寫(xiě)babel-plugin的時(shí)候,其實(shí)就是在寫(xiě)其中的transformer,其他的部分已經(jīng)被babel完美的實(shí)現(xiàn)了。


          babel plugin 概念


          先上手看一個(gè)簡(jiǎn)單的babel plugin示例



          module.exports = function ({ types: t }) { const TRUE = t.unaryExpression("!", t.numericLiteral(0), true); const FALSE = t.unaryExpression("!", t.numericLiteral(1), true); return { visitor: { BooleanLiteral(path) { path.replaceWith(path.node.value ? TRUE : FALSE) } }, };}

          這個(gè)plugin造成的效果:


          //?源代碼const x = true;// 轉(zhuǎn)義后的的代碼const x = !0;


          就是把所有的bool類型的值轉(zhuǎn)化成 !0 或者 !1,這是代碼壓縮的時(shí)候使用的一個(gè)技巧。

          那么逐行來(lái)分析這個(gè)簡(jiǎn)單的plugin。一個(gè)plugin就是一個(gè)function,入?yún)⒕褪莃abel對(duì)象,這里利用到了babel中types對(duì)象,來(lái)自于@babel/types這個(gè)庫(kù),然后操作path對(duì)象進(jìn)行節(jié)點(diǎn)替換操作。


          path


          path是肯定會(huì)用到的一個(gè)對(duì)象。我們可以用過(guò)path訪問(wèn)到當(dāng)前節(jié)點(diǎn),父節(jié)點(diǎn),也可以去調(diào)用添加、更新、移動(dòng)和刪除節(jié)點(diǎn)有關(guān)的其他很多方法。舉幾個(gè)示例

          // 訪問(wèn)當(dāng)前節(jié)點(diǎn)的屬性,用path.node.property訪問(wèn)node的屬性path.node.nodepath.node.left// 直接改變當(dāng)前節(jié)點(diǎn)的屬性path.node.name = "x";// 當(dāng)前節(jié)點(diǎn)父節(jié)點(diǎn)path.parent// 當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)的pathpath.parentPath// 訪問(wèn)節(jié)點(diǎn)內(nèi)部屬性path.get('left')// 刪除一個(gè)節(jié)點(diǎn) path.remove();// 替換一個(gè)節(jié)點(diǎn)path.replaceWith();// 替換成多個(gè)節(jié)點(diǎn)path.replaceWithMultiple();// 插入兄弟節(jié)點(diǎn)path.insertBefore();path.insertAfter();// 跳過(guò)子節(jié)點(diǎn)的遍歷path.skip();// 完全跳過(guò)遍歷path.stop();


          @babel/types


          可以理解它為一個(gè)工具庫(kù),類似于Lodash,里面封裝了非常多的幫做方法,一般用處如下

          • 檢查節(jié)點(diǎn) 一般在類型前面加is就是判斷是否該類型


          //?判斷當(dāng)前節(jié)點(diǎn)的left節(jié)點(diǎn)是否是identifier類型if (t.isIdentifier(path.node.left)) {  // ...?}// 判斷當(dāng)前節(jié)點(diǎn)的left節(jié)點(diǎn)是否是identifer類型,并且name='n'if (t.isIdentifier(path.node.left, { name: "n" })) {    // ...}// 上述判斷等價(jià)于if (    path.node.left != null &&    path.node.left.type === "Identifier" &&    path.node.left.name === "n"  ) {    // ...??}


          • 構(gòu)建節(jié)點(diǎn)
            直接手寫(xiě)復(fù)雜的AST結(jié)構(gòu)是不現(xiàn)實(shí)的,所以有了一些幫助方法去構(gòu)建這些節(jié)點(diǎn),示例:


          //?調(diào)用binaryExpression和identifier的構(gòu)建方法,生成astt.binaryExpression("*", t.identifier("a"), t.identifier("b"));// 生成如下{  type: "BinaryExpression",  operator: "*",  left: {    type: "Identifier",    name: "a"  },  right: {    type: "Identifier",    name: "b"  }}// 最后經(jīng)過(guò)AST反轉(zhuǎn)回來(lái)如下a * b


          其中每一種節(jié)點(diǎn)都有自己的構(gòu)造方法,都有自己特定的入?yún)ⅲ敿?xì)請(qǐng)參考官方文檔


          scope


          最后講一下作用域的概念,每一個(gè)函數(shù),每一個(gè)變量都有自己的作用域,在編寫(xiě)babel plugin的時(shí)候要特別小心,再改變或者添加代碼的時(shí)候要注意不要破壞了原有的代碼結(jié)構(gòu)。

          path.scope中的一些方法可以操作作用域,示例:


          //?檢查變量n是否被綁定(是否在上下文已經(jīng)有引用)path.scope.hasBinding("n")// 檢查自己內(nèi)部是否有引用npath.scope.hasOwnBinding("n")// 創(chuàng)建一個(gè)上下文中新的引用 生成類似于{ type: 'Identifier', name: '_n2' }path.scope.generateUidIdentifier("n"); // 重命名當(dāng)前的引用path.scope.rename("n", "x");


          plugin實(shí)戰(zhàn)


          寫(xiě)一個(gè)自定義plugin是什么步驟呢?



          • 這個(gè)plugin用來(lái)干嘛

          • 源代碼的AST

          • 轉(zhuǎn)換后代碼的AST
            tip: 可以去這個(gè)網(wǎng)站查看代碼的AST。



          plugin的目的


          現(xiàn)在就做一個(gè)自定義的plugin,大家在應(yīng)用寫(xiě)代碼的時(shí)候可以通過(guò)webpack配置alias,比如說(shuō)配置@ -> ./src,這樣import的時(shí)候就直接從src目錄下找所需要的代碼了,那么大家有在寫(xiě)組件的時(shí)候用過(guò)這個(gè)功能嗎?這就是我們這個(gè)plugin的目的。


          代碼


          我們有如下配置


          "alias":?{   "@": "./src"}


          源代碼以及要轉(zhuǎn)化的代碼如下:


          //?./src/index.jsimport?add?from?'@/common';?//?->?import?add?from?"./common";
          // ./src/foo/test/index.jsimport?add?from?'@/common';?//?->??import?add?from?"../../common";

          AST


          源碼的AST展示如下

          那我們看見(jiàn)是不是只需要找到ImportDeclaration節(jié)點(diǎn)中將source改成轉(zhuǎn)換之后的代碼是不是就可以了。


          開(kāi)始寫(xiě)plugin


          const?localPath?=?require('path');module.exports = function ({ types: t }) {    return {        visitor: {            ImportDeclaration(path, state) {                // 從state中拿到外界傳進(jìn)的參數(shù),這里我們外界設(shè)置了alias                const { alias } = state.opts;                if (!alias) {                    return;                }                // 拿到當(dāng)前文件的信息                const { filename, root } = state.file.opts;                // 找到相對(duì)地址                const relativePath = localPath.relative(root, localPath.dirname(filename));                // 利用path獲取當(dāng)前節(jié)點(diǎn)中source的value,這里對(duì)應(yīng)的就是 '@/common'了                let importSource = path.node.source.value;                // 遍歷我們的配置,進(jìn)行關(guān)鍵字替換                Object.keys(alias).forEach(key => {                    const reg = new RegExp(`^${key}`);                    if (reg.test(importSource)) {                        importSource = importSource.replace(reg, alias[key]);                        importSource = localPath.relative(relativePath, importSource)                    }                })                // 利用t.StringLiteral構(gòu)建器構(gòu)建一個(gè)StringLiteral類型的節(jié)點(diǎn),賦值給source                path.node.source = t.StringLiteral(importSource)            }        },    };}

          用plugin


          回到我們的babel配置文件中來(lái),這里我們用的是babel.config.json

          {    "plugins": [        [        // 這里使用本地的文件當(dāng)做plugin,實(shí)際上可以把自己制作的plugin發(fā)布成npm包供大家使用            "./plugin.js",         // 傳配置到plugin的第二個(gè)參數(shù)state.opts中            {                "alias": {                    "@": "./src"                }            }        ]    ]}


          這樣一個(gè)plugin的流程就走完了,歡迎大家多多交流。




          推薦閱讀




          我的公眾號(hào)能帶來(lái)什么價(jià)值?(文末有送書(shū)規(guī)則,一定要看)

          每個(gè)前端工程師都應(yīng)該了解的圖片知識(shí)(長(zhǎng)文建議收藏)

          為什么現(xiàn)在面試總是面試造火箭?

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

          手機(jī)掃一掃分享

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

          手機(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>
                  亚洲AⅤ网站 | 青青草,十月丁香在线 | 丁香五月激情综合部 | 就操在线| 自拍三级片青青草视频 |