<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 為代碼自動(dòng)引入依賴

          共 7066字,需瀏覽 15分鐘

           ·

          2021-01-11 11:13

          前言

          最近在嘗試玩一玩已經(jīng)被大家玩膩的?Babel,今天給大家分享如何用?Babel?為代碼自動(dòng)引入依賴,通過一個(gè)簡(jiǎn)單的例子入門?Babel?插件開發(fā)。

          需求

          const?a?=?require('a');
          import?b?from?'b';

          console.log(axuebin.say('hello?babel'));

          同學(xué)們都知道,如果運(yùn)行上面的代碼,一定是會(huì)報(bào)錯(cuò)的:

          VM105:2?Uncaught?ReferenceError:?axuebin?is?not?defined

          我們得首先通過?import axuebin from 'axuebin'?引入?axuebin?之后才能使用。。

          為了防止這種情況發(fā)生(一般來說我們都會(huì)手動(dòng)引入),或者為你省去引入這個(gè)包的麻煩(其實(shí)有些編譯器也會(huì)幫我們做了),我們可以在打包階段分析每個(gè)代碼文件,把這個(gè)事情做了。

          在這里,我們就基于最簡(jiǎn)單的場(chǎng)景做最簡(jiǎn)單的處理,在代碼文件頂部加一句引用語句:

          import?axuebin?from?'axuebin';
          console.log(axuebin.say('hello?babel'));

          前置知識(shí)

          什么是 Babel

          簡(jiǎn)單地說,Babel?能夠轉(zhuǎn)譯?ECMAScript 2015+?的代碼,使它在舊的瀏覽器或者環(huán)境中也能夠運(yùn)行。我們?nèi)粘i_發(fā)中,都會(huì)通過?webpack?使用?babel-loader?對(duì)?JavaScript?進(jìn)行編譯。

          Babel 是如何工作的

          首先得要先了解一個(gè)概念:抽象語法樹(Abstract Syntax Tree, AST),Babel?本質(zhì)上就是在操作?AST?來完成代碼的轉(zhuǎn)譯。

          了解了?AST?是什么樣的,就可以開始研究?Babel?的工作過程了。

          Babel?的功能其實(shí)很純粹,它只是一個(gè)編譯器。

          大多數(shù)編譯器的工作過程可以分為三部分,如圖所示:

          • Parse(解析)?將源代碼轉(zhuǎn)換成更加抽象的表示方法(例如抽象語法樹)
          • Transform(轉(zhuǎn)換)?對(duì)(抽象語法樹)做一些特殊處理,讓它符合編譯器的期望
          • Generate(代碼生成)?將第二步經(jīng)過轉(zhuǎn)換過的(抽象語法樹)生成新的代碼

          所以我們?nèi)绻胍薷?Code,就可以在?Transform?階段做一些事情,也就是操作?AST

          AST 節(jié)點(diǎn)

          我們可以看到?AST?中有很多相似的元素,它們都有一個(gè)?type?屬性,這樣的元素被稱作節(jié)點(diǎn)。一個(gè)節(jié)點(diǎn)通常含有若干屬性,可以用于描述?AST?的部分信息。

          比如這是一個(gè)最常見的?Identifier?節(jié)點(diǎn):

          {
          ??type:?'Identifier',
          ??name:?'add'
          }

          所以,操作?AST?也就是操作其中的節(jié)點(diǎn),可以增刪改這些節(jié)點(diǎn),從而轉(zhuǎn)換成實(shí)際需要的?AST

          更多的節(jié)點(diǎn)規(guī)范可以查閱?https://github.com/estree/estree[1]

          AST 遍歷

          AST?是深度優(yōu)先遍歷的,遍歷規(guī)則不用我們自己寫,我們可以通過特定的語法找到的指定的節(jié)點(diǎn)。

          Babel?會(huì)維護(hù)一個(gè)稱作?Visitor?的對(duì)象,這個(gè)對(duì)象定義了用于?AST?中獲取具體節(jié)點(diǎn)的方法。

          一個(gè)?Visitor?一般是這樣:

          const?visitor?=?{
          ??ArrowFunction(path)?{
          ????console.log('我是箭頭函數(shù)');
          ??},
          ??IfStatement(path)?{
          ????console.log('我是一個(gè)if語句');
          ??},
          ??CallExpression(path)?{}
          };

          visitor?上掛載以節(jié)點(diǎn)?type?命名的方法,當(dāng)遍歷?AST?的時(shí)候,如果匹配上?type,就會(huì)執(zhí)行對(duì)應(yīng)的方法。

          操作 AST 的例子

          通過上面簡(jiǎn)單的介紹,我們就可以開始任意造作了,肆意修改?AST?了。先來個(gè)簡(jiǎn)單的例子熱熱身。

          箭頭函數(shù)是?ES5?不支持的語法,所以?Babel?得把它轉(zhuǎn)換成普通函數(shù),一層層遍歷下去,找到了?ArrowFunctionExpression?節(jié)點(diǎn),這時(shí)候就需要把它替換成?FunctionDeclaration?節(jié)點(diǎn)。所以,箭頭函數(shù)可能是這樣處理的:

          import?*?as?t?from?"@babel/types";

          const?visitor?=?{
          ??ArrowFunction(path)?{
          ????path.replaceWith(t.FunctionDeclaration(id,?params,?body));
          ??}
          };

          開發(fā) Babel 插件的前置工作

          在開始寫代碼之前,我們還有一些事情要做一下:

          分析 AST

          原代碼目標(biāo)代碼都解析成?AST,觀察它們的特點(diǎn),找找看如何增刪改?AST?節(jié)點(diǎn),從而達(dá)到自己的目的。

          我們可以在?https://astexplorer.net[2]?上完成這個(gè)工作,比如文章最初提到的代碼:

          const?a?=?require('a');
          import?b?from?'b';
          console.log(axuebin.say('hello?babel'));

          轉(zhuǎn)換成?AST?之后是這樣的:

          可以看出,這個(gè)?body?數(shù)組對(duì)應(yīng)的就是根節(jié)點(diǎn)的三條語句,分別是:

          • VariableDeclaration:?const a = require('a')
          • ImportDeclaration:?import b from 'b'
          • ExpressionStatement:?console.log(axuebin.say('hello babel'))

          我們可以打開?VariableDeclaration?節(jié)點(diǎn)看看:

          它包含了一個(gè)?declarations?數(shù)組,里面有一個(gè)?VariableDeclarator?節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)有?typeidinit?等信息,其中?id?指的是表達(dá)式聲明的變量名,init?指的是聲明內(nèi)容。

          通過這樣查看/對(duì)比?AST?結(jié)構(gòu),就能分析出原代碼目標(biāo)代碼的特點(diǎn),然后可以開始動(dòng)手寫程序了。

          查看節(jié)點(diǎn)規(guī)范

          節(jié)點(diǎn)規(guī)范:https://github.com/estree/estree[3]

          我們要增刪改節(jié)點(diǎn),當(dāng)然要知道節(jié)點(diǎn)的一些規(guī)范,比如新建一個(gè)?ImportDeclaration?需要傳遞哪些參數(shù)。

          寫代碼

          準(zhǔn)備工作都做好了,那就開始吧。

          初始化代碼

          我們的?index.js?代碼為:

          //?index.js
          const?path?=?require('path');
          const?fs?=?require('fs');
          const?babel?=?require('@babel/core');

          const?TARGET_PKG_NAME?=?'axuebin';

          function?transform(file)?{
          ??const?content?=?fs.readFileSync(file,?{
          ????encoding:?'utf8',
          ??});
          ??const?{?code?}?=?babel.transformSync(content,?{
          ????sourceMaps:?false,
          ????plugins:?[
          ??????babel.createConfigItem(({?types:?t?})?=>?({
          ????????visitor:?{
          ????????}
          ??????}))
          ????]
          ??});
          ??return?code;
          }

          然后我們準(zhǔn)備一個(gè)測(cè)試文件?test.js,代碼為:

          //?test.js
          const?a?=?require('a');
          import?b?from?'b';
          require('c');
          import?'d';
          console.log(axuebin.say('hello?babel'));

          分析 AST / 編寫對(duì)應(yīng) type 代碼

          我們這次需要做的事情很簡(jiǎn)單,做兩件事:

          1. 尋找當(dāng)前?AST?中是否含有引用?axuebin?包的節(jié)點(diǎn)
          2. 如果沒引用,則修改?AST,插入一個(gè)?ImportDeclaration?節(jié)點(diǎn)

          我們來分析一下?test.js?的?AST,看一下這幾個(gè)節(jié)點(diǎn)有什么特征:

          ImportDeclaration 節(jié)點(diǎn)

          ImportDeclaration?節(jié)點(diǎn)的?AST?如圖所示,我們需要關(guān)心的特征是?value?是否等于?axuebin, 代碼這樣寫:

          if?(path.isImportDeclaration())?{
          ??return?path.get('source').isStringLiteral()?&&?path.get('source').node.value?===?TARGET_PKG_NAME;
          }

          其中,可以通過?path.get?來獲取對(duì)應(yīng)節(jié)點(diǎn)的?path,嗯,比較規(guī)范。如果想獲取對(duì)應(yīng)的真實(shí)節(jié)點(diǎn),還需要?.node

          滿足上述條件則可以認(rèn)為當(dāng)前代碼已經(jīng)引入了?axuebin?包,不用再做處理了。

          VariableDeclaration 節(jié)點(diǎn)

          對(duì)于?VariableDeclaration?而言,我們需要關(guān)心的特征是,它是否是一個(gè)?require?語句,并且?require?的是?axuebin,代碼如下:

          /**
          ?*?判斷是否?require?了正確的包
          ?*?@param?{*}?node?節(jié)點(diǎn)
          ?*/

          const?isTrueRequire?=?node?=>?{
          ??const?{?callee,?arguments?}?=?node;
          ??return?callee.name?===?'require'?&&?arguments.some(item?=>?item.value?===?TARGET_PKG_NAME);
          };


          if?(path.isVariableDeclaration())?{
          ??const?declaration?=?path.get('declarations')[0];
          ??return?declaration.get('init').isCallExpression?&&?isTrueRequire(declaration.get('init').node);
          }

          ExpressionStatement 節(jié)點(diǎn)

          require('c'),語句我們一般不會(huì)用到,我們也來看一下吧,它對(duì)應(yīng)的是?ExpressionStatement?節(jié)點(diǎn),我們需要關(guān)心的特征和?VariableDeclaration?一致,這也是我把?isTrueRequire?抽出來的原因,所以代碼如下:

          if?(path.isExpressionStatement())?{
          ??return?isTrueRequire(path.get('expression').node);
          }

          插入引用語句

          如果上述分析都沒找到代碼里引用了?axuebin,我們就需要手動(dòng)插入一個(gè)引用:

          import?axuebin?from?'axuebin';

          通過?AST?分析,我們發(fā)現(xiàn)它是一個(gè)?ImportDeclaration

          簡(jiǎn)化一下就是這樣:

          {
          ??"type":?"ImportDeclaration",
          ??"specifiers":?[
          ????"type":?"ImportDefaultSpecifier",
          ????"local":?{
          ??????"type":?"Identifier",
          ??????"name":?"axuebin"
          ????}
          ??],
          ??"source":?{
          ????"type":?"StringLiteral",
          ????"value":?"axuebin"
          ??}
          }

          當(dāng)然,不是直接構(gòu)建這個(gè)對(duì)象放進(jìn)去就好了,需要通過?babel?的語法來構(gòu)建這個(gè)節(jié)點(diǎn)(遵循規(guī)范):

          const?importDefaultSpecifier?=?[t.ImportDefaultSpecifier(t.Identifier(TARGET_PKG_NAME))];
          const?importDeclaration?=?t.ImportDeclaration(importDefaultSpecifier,?t.StringLiteral(TARGET_PKG_NAME));
          path.get('body')[0].insertBefore(importDeclaration);

          這樣就插入了一個(gè)?import?語句。

          Babel Types?模塊是一個(gè)用于?AST?節(jié)點(diǎn)的?Lodash?式工具庫(kù),它包含了構(gòu)造、驗(yàn)證以及變換?AST?節(jié)點(diǎn)的方法。

          結(jié)果

          我們?node index.js?一下,test.js?就變成:

          import?axuebin?from?"axuebin";?//?已經(jīng)自動(dòng)加在代碼最上邊
          const?a?=?require('a');
          import?b?from?'b';
          require('c');
          import?'d';
          console.log(axuebin.say('hello?babel'));

          彩蛋

          如果我們還想幫他再多做一點(diǎn)事,還能做什么呢?

          既然都自動(dòng)引用了,那當(dāng)然也要自動(dòng)安裝一下這個(gè)包呀!

          /**
          ?*?判斷是否安裝了某個(gè)包
          ?*?@param?{string}?pkg?包名
          ?*/

          const?hasPkg?=?pkg?=>?{
          ??const?pkgPath?=?path.join(process.cwd(),?`package.json`);
          ??const?pkgJson?=?fs.existsSync(pkgPath)???fse.readJsonSync(pkgPath)?:?{};
          ??const?{?dependencies?=?{},?devDependencies?=?{}?}?=?pkgJson;
          ??return?dependencies[pkg]?||?devDependencies[pkg];
          }

          /**
          ?*?通過?npm?安裝包
          ?*?@param?{string}?pkg?包名
          ?*/

          const?installPkg?=?pkg?=>?{
          ??console.log(`開始安裝?${pkg}`);
          ??const?npm?=?shell.which('npm');
          ??if?(!npm)?{
          ????console.log('請(qǐng)先安裝?npm');
          ????return;
          ??}
          ??const?{?code?}?=?shell.exec(`${npm.stdout}?install?${pkg}?-S`);
          ??if?(code)?{
          ????console.log(`安裝?${pkg}?失敗,請(qǐng)手動(dòng)安裝`);
          ??}
          };

          //?biu~
          if?(!hasPkg(TARGET_PKG_NAME))?{
          ??installPkg(TARGET_PKG_NAME);
          }

          判斷一個(gè)應(yīng)用是否安裝了某個(gè)依賴,有沒有更好的辦法呢?

          總結(jié)

          我也是剛開始學(xué)?Babel,希望通過這個(gè)?Babel?插件的入門例子,可以讓大家了解?Babel?其實(shí)并沒有那么陌生,大家都可以玩起來 ~

          完整代碼見:https://github.com/axuebin/babel-inject-dep-demo[4]

          • Babel 用戶手冊(cè)[5]
          • Babel 插件手冊(cè)[6]
          • ast 分析[7]
          • 節(jié)點(diǎn)規(guī)范[8]

          參考資料

          [1]?

          https://github.com/estree/estree:?https://github.com/estree/estree

          [2]?

          https://astexplorer.net:?https://astexplorer.net

          [3]?

          https://github.com/estree/estree:?https://github.com/estree/estree

          [4]?

          https://github.com/axuebin/babel-inject-dep-demo:?https://github.com/axuebin/babel-inject-dep-demo

          [5]?

          Babel 用戶手冊(cè):?https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/user-handbook.md

          [6]?

          Babel 插件手冊(cè):?https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md

          [7]?

          ast 分析:?https://astexplorer.net/

          [8]?

          節(jié)點(diǎn)規(guī)范:?https://github.com/estree/estree


          最后



          如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:

          1. 點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)

          2. 歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

          3. 關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。


          點(diǎn)個(gè)在看支持我吧,轉(zhuǎn)發(fā)就更好了

          瀏覽 40
          點(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>
                  国产免费性爱视频 | 亚洲高清无码在线观看视频 | 淫色视频网站 | 东京热久久 | 超碰成人在播放 |