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

          你還在手動(dòng)部署埋點(diǎn)嗎?從0到1開發(fā)Babel埋點(diǎn)自動(dòng)植入插件!

          共 18849字,需瀏覽 38分鐘

           ·

          2022-06-02 13:32

          前言

          本文由網(wǎng)易的?Pluto Lam?大神投稿,授權(quán)本公眾號(hào)發(fā)表。

          在各種大型項(xiàng)目中,流量統(tǒng)計(jì)是一項(xiàng)重要工程,統(tǒng)計(jì)點(diǎn)擊量可以在后端進(jìn)行監(jiān)控,但是這局限于調(diào)用接口時(shí)才能統(tǒng)計(jì)到用戶點(diǎn)擊,而前端埋點(diǎn)監(jiān)控也是一個(gè)統(tǒng)計(jì)流量的手段,下面就基于百度統(tǒng)計(jì)來完成以下需求

          1. html頁面中插入特定的script標(biāo)簽,src為可選值
          2. 在全局window植入可選的函數(shù)
          3. 解析特定格式的excel表,里面包含埋點(diǎn)的id和參數(shù)值(傳遞給上面的函數(shù))
          4. 找到項(xiàng)目中所有帶有表示的行級(jí)注釋,并將其替換成執(zhí)行2中函數(shù)的可執(zhí)行語句,并傳入excel表對應(yīng)的參數(shù)

          可能有些讀者看到這里就有點(diǎn)蒙了,這到底是個(gè)啥,別急,跟著我一步一步做下去就清楚了,接下來我會(huì)以一個(gè)babel初學(xué)者的身份帶你邊學(xué)邊做

          那就讓我們從最難搞定的最后一步開始,這也是比較麻煩的babel操作

          babel前置知識(shí)

          很多讀者估計(jì)知識(shí)配置過babel,但是并不知道它具體是干啥事的,只是依稀記得AST抽象語法樹深度優(yōu)先遍歷babel-loader等詞語

          在我們?nèi)粘5拈_發(fā)中,確實(shí)是使用babel-loader比較多,這是babelwebpack開發(fā)的一個(gè)插件,如果沒有任何配置,它將不會(huì)對代碼做任何修改,只是遍歷AST,在webpack構(gòu)建階段收集依賴并生成module,我們比較常用的是將ES6轉(zhuǎn)為ES5,只需在preset使用babel官方提供的相關(guān)即可,在這就不贅述。

          babel進(jìn)行深度優(yōu)先遍歷時(shí),會(huì)調(diào)用我們配置好的plugin,在不同的plugin里面都可以對遍歷過程進(jìn)行監(jiān)聽和操作。

          {
          ??"presets":?[
          ????"@babel/preset-env"?//?提供ES6轉(zhuǎn)ES5功能
          ??],
          ????"plugins":?[
          ??????"xxx-plugin"
          ????]
          }

          既然是初學(xué)者,那就先來搭建測試環(huán)境吧,下面都是使用yarn裝包

          測試環(huán)境

          創(chuàng)建一個(gè)空文件,執(zhí)行命令npm init -y,如下圖創(chuàng)建文件

          測試環(huán)境在test文件中,index.js中可以放主要的測試代碼

          //?test/index.js
          class?App?{

          }

          安裝webpack,由于我們的webpackwebpack-cli只有開發(fā)時(shí)測試會(huì)用到,所以加上-D,表示開發(fā)依賴

          yarn?add?webpack?webpack-cli?-D

          配置好webpack.config.js

          const?path?=?require("path");

          module.exports?=?{
          ??mode:?"development",?//?開發(fā)模式
          ??entry:?{
          ????main:?path.resolve(__dirname,?"./index.js")?//?入口文件
          ??},
          ??output:?{
          ????filename:?"main.js",
          ????path:?path.resolve(__dirname,?"./dist")?//?出口文件
          ??}
          }

          /package.json里配置打包命令

          //?package.json
          "scripts":?{
          ???"dev":?"webpack?--config?test/webpack.config.js"?//?指向test目錄下的配置文件
          },

          運(yùn)行yarn dev即打包,此時(shí)會(huì)出現(xiàn)/test/dist/main.js文件,里面為打包好的代碼,babel plugin處理過的代碼可以在這里校驗(yàn)是否正確

          確保可以運(yùn)行后,安裝babel-loader@babel/core@babel/preset-env,添加如下配置

          • @babel/core里面包含babel的常用方法,其中babel-loader@babel/preset-env都是依賴于@babel/core
          const?path?=?require("path");

          module.exports?=?{
          ??...
          ??module:?{
          ????rules:?[
          ??????{
          ????????test:?/.js$/,
          ????????exclude:?/node_modules/,
          ????????loader:?"babel-loader",
          ????????options:?{
          ??????????presets:?[
          ????????????"@babel/preset-env"
          ??????????]
          ????????}
          ??????}
          ????]
          ??}
          };

          yarn dev后可以看看main.js,如果class被轉(zhuǎn)為ES5語法表示,則babel-loader是配置成功的

          接下來就可以配置插件了,在options里添加plugins,通過path.resolve()生成絕對路徑, 指向/src/index.js

          options:?{
          ??presets:?[
          ????"@babel/preset-env"
          ??],
          ????plugins:?[
          ??????[
          ????????path.resolve(__dirname,?"../src/index.js"),
          ??????]
          ????]
          }

          /src/index.js里寫plugin相關(guān)代碼

          module.exports?=?function()?{
          ??return?{
          ????visitor:?{
          ??????Program:?{
          ????????enter(path)?{
          ??????????console.log("enter");
          ????????},
          ????????exit(path)?{
          ??????????console.log("exit");
          ????????}
          ??????},
          ??????FunctionDeclaration(path,?state){
          ????????...
          ??????}
          ??????}
          ????};
          };

          babel-plugin在基本結(jié)構(gòu)如上,導(dǎo)出一個(gè)函數(shù),函數(shù)里面返回一個(gè)對象,對象里面就是plugin的相關(guān)配置。

          我們這個(gè)插件主要使用visitor(訪問者),顧名思義,在AST遍歷時(shí),我們能訪問到遍歷時(shí)訪問的每個(gè)節(jié)點(diǎn),在visitor里面,我們可以將節(jié)點(diǎn)類型抽象成方法,簡單點(diǎn)說,只要訪問到這種類型的節(jié)點(diǎn),就會(huì)執(zhí)行對應(yīng)的方法,而且會(huì)傳入兩個(gè)參數(shù),其中path表示改節(jié)點(diǎn)的路徑,可以對該節(jié)點(diǎn)進(jìn)行操作,state是一個(gè)全局的變量,從進(jìn)入visitor開始就存在,里面常用的東西不多,稍后再說。

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

          節(jié)點(diǎn)的類型非常之多,下面只介紹一些需要用到的,如果有需要可以訪問AST node types進(jìn)行學(xué)習(xí)。

          1. Programs

          官方的解釋是這樣的

          A complete program source tree.

          Parsers must specify sourceType as "module" if the source has been parsed as an ES6 module. Otherwise, sourceType must be "script".

          通俗點(diǎn)來說,它就是抽象樹的最頂端的節(jié)點(diǎn),里面包含整棵樹,遍歷開始與結(jié)束時(shí)分別會(huì)進(jìn)入enter方法和exit方法,這樣單純用文字描述讀者可能還對抽象樹的結(jié)構(gòu)云里霧里,下面我們通過一個(gè)網(wǎng)站和控制臺(tái)打印來看一下具體的節(jié)點(diǎn)長什么樣

          我們可以訪問https://astexplorer.net/這個(gè)網(wǎng)址,在左邊輸入想要解析的代碼,右邊就會(huì)對應(yīng)的AST樹,不過這個(gè)樹有點(diǎn)刪減,要詳細(xì)一點(diǎn)的樹可以點(diǎn)擊“JSON”查看JSON格式的AST樹

          我將JSON去除一些目前不需要用到的屬性展示出來

          {
          ??"type":?"File",
          ??"start":?0,
          ??"end":?30,
          ??"loc":?{...},
          ??"errors":?[],
          ??"program":?{
          ????"type":?"Program",
          ????"start":?0,
          ????"end":?30,
          ????"loc":?{...},
          ????"sourceType":?"module",
          ????"interpreter":?null,
          ????"body":?[
          ??????{
          ????????"type":?"FunctionDeclaration",
          ????????"start":?10,
          ????????"end":?30,
          ????????"loc":?{...},
          ????????"id":?{...},
          ????????"generator":?false,
          ????????"async":?false,
          ????????"params":?[],
          ????????"body":?{...},
          ????????"leadingComments":?[
          ??????????{
          ????????????"type":?"CommentLine",
          ????????????"value":?"?@debug",
          ????????????"start":?0,
          ????????????"end":?9,
          ????????????"loc":?{...}
          ??????????}
          ????????]
          ??????}
          ????],
          ????"directives":?[]
          ??},
          ??"comments":?[
          ????{
          ??????"type":?"CommentLine",
          ??????"value":?"?@debug",
          ??????"start":?0,
          ??????"end":?9,
          ??????"loc":?{
          ????????"start":?{
          ??????????"line":?1,
          ??????????"column":?0
          ????????},
          ????????"end":?{
          ??????????"line":?1,
          ??????????"column":?9
          ????????}
          ??????}
          ????}
          ??]
          }

          可以看到有很多很多屬性,但是我們不能拿這個(gè)來開發(fā),只能作為參考,要真正操作節(jié)點(diǎn)還是需要在控制臺(tái)打印或者debug查看AST的結(jié)構(gòu),下面我將控制臺(tái)打印的節(jié)點(diǎn)結(jié)構(gòu)刪減后帖出來,可以和上面對比一下。

          ?NodePath?{
          ??contexts:?[
          ????TraversalContext?{
          ??????queue:?[Array],
          ??????priorityQueue:?[],
          ??????parentPath:?undefined,
          ??????scope:?[Scope],
          ??????state:?undefined,
          ??????opts:?[Object]
          ????}
          ??],
          ??state:?undefined,
          ??opts:?{
          ????Program:?{?enter:?[Array],?exit:?[Array]?},
          ????_exploded:?{},
          ????_verified:?{},
          ????ClassBody:?{?enter:?[Array]?},
          ????...
          ??},
          ??_traverseFlags:?0,
          ??skipKeys:?null,
          ??parentPath:?null,
          ??container:?Node?{
          ????type:?'File',
          ????start:?0,
          ????end:?16,
          ????loc:?SourceLocation?{
          ??????start:?[Position],
          ??????end:?[Position],
          ??????filename:?undefined,
          ??????identifierName:?undefined
          ????},
          ????errors:?[],
          ????program:?Node?{
          ??????type:?'Program',
          ??????start:?0,
          ??????end:?16,
          ??????loc:?[SourceLocation],
          ??????sourceType:?'module',
          ??????interpreter:?null,
          ??????body:?[Array],
          ??????directives:?[],
          ??????leadingComments:?undefined,
          ??????innerComments:?undefined,
          ??????trailingComments:?undefined
          ????},
          ????comments:?[],
          ????leadingComments:?undefined,
          ????innerComments:?undefined,
          ????trailingComments:?undefined
          ??},
          ??listKey:?undefined,
          ??key:?'program',
          ??node:?Node?{
          ????type:?'Program',
          ????start:?0,
          ????end:?16,
          ????loc:?SourceLocation?{
          ??????start:?[Position],
          ??????end:?[Position],
          ??????filename:?undefined,
          ??????identifierName:?undefined
          ????},
          ????sourceType:?'module',
          ????interpreter:?null,
          ????body:?[?[Node]?],
          ????directives:?[],
          ????leadingComments:?undefined,
          ????innerComments:?undefined,
          ????trailingComments:?undefined
          ??},
          ??type:?'Program',
          ??parent:?Node?{
          ????type:?'File',
          ????start:?0,
          ????end:?16,
          ????loc:?SourceLocation?{
          ??????start:?[Position],
          ??????end:?[Position],
          ??????filename:?undefined,
          ??????identifierName:?undefined
          ????},
          ????errors:?[],
          ????program:?Node?{
          ??????type:?'Program',
          ??????start:?0,
          ??????end:?16,
          ??????loc:?[SourceLocation],
          ??????sourceType:?'module',
          ??????interpreter:?null,
          ??????body:?[Array],
          ??????directives:?[],
          ??????leadingComments:?undefined,
          ??????innerComments:?undefined,
          ??????trailingComments:?undefined
          ????},
          ????comments:?[],
          ????leadingComments:?undefined,
          ????innerComments:?undefined,
          ????trailingComments:?undefined
          ??},
          ??hub:??{
          ????file:?File?{
          ??????...
          ??????code:?'class?App?{\r\n\r\n}',
          ??????inputMap:?null,
          ??????hub:?[Circular?*2]
          ????},
          ????...
          ??},
          ??data:?null,
          ??context:?TraversalContext?{
          ???...
          ??},
          ??scope:?Scope?{
          ????...
          ??}
          }

          可以看到總體上結(jié)構(gòu)時(shí)差不多的,在真實(shí)結(jié)構(gòu)中一般就比網(wǎng)頁版多套一層node( path.node ),所以一般參照網(wǎng)頁版后直接path.node就可以取到想要的屬性。而且是沒有最外層的File結(jié)構(gòu)的,只在Programparent中。

          1. FunctionDeclaration,ClassDeclaration,ArrowFunctionExpression

          我們?nèi)粘懙拇a一定是在普通函數(shù)、類、箭頭函數(shù)里面的,所以在這外面的注釋我們一律不管,所以這三個(gè)節(jié)點(diǎn)分別是函數(shù)聲明、類聲明、箭頭函數(shù)聲明。在遍歷過程中凡是遇到這三個(gè)節(jié)點(diǎn)就會(huì)進(jìn)去對應(yīng)的方法。

          注釋

          眼尖的朋友可能發(fā)現(xiàn)了,在上面打印出來的一堆屬性中,有commentsleadingComments等單詞出現(xiàn),沒錯(cuò),這就是我們需要關(guān)注的行級(jí)注釋,我們下面實(shí)驗(yàn)一下。

          function?App(){
          ??//?這是innerComments注釋
          }

          function?fn()?{
          ?//?這是const的前置注釋
          ??const?a?=?1;
          ?function?inFn()?{
          ??//?這是inFn的inner注釋
          ?}
          ??//?這是inFn的后置注釋(trailingComments)
          }

          對應(yīng)的AST樹結(jié)構(gòu)(網(wǎng)頁版)

          {
          ??...
          ??"program":?{
          ????"type":?"Program",
          ????...
          ????"body":?[
          ??????{
          ????????...
          ????????"id":?{
          ??????????...
          ??????????"name":?"fn"
          ????????},
          ????????"generator":?false,
          ????????"async":?false,
          ????????"params":?[],
          ????????"body":?{
          ??????????"type":?"BlockStatement",
          ??????????...
          ??????????"body":?[
          ????????????{
          ??????????????"type":?"VariableDeclaration",
          ??????????????...
          ??????????????"declarations":?[
          ??????????????????{
          ????????????????????...
          ????????????????????"id":?{
          ??????????????????????...
          ??????????????????????"identifierName":?"a"
          ????????????????????},
          ????????????????????"name":?"a"
          ??????????????????},
          ??????????????????"init":?{
          ??????????????????...
          ??????????????????"value":?1
          ??????????????????}
          ????????????????}
          ??????????????],
          ??????????????"kind":?"const",
          ??????????????"leadingComments":?[
          ????????????????{
          ??????????????????"type":?"CommentLine",
          ??????????????????"value":?"?fn",??//?是const的前置注釋
          ??????????????????...
          ????????????????}
          ??????????????]
          ????????????},
          ????????????{
          ??????????????"type":?"FunctionDeclaration",
          ??????????????...
          ??????????????"id":?{
          ????????????????"type":?"Identifier",
          ????????????????...
          ????????????????"name":?"inFn"
          ??????????????},
          ??????????????...
          ??????????????"body":?{
          ????????????????"type":?"BlockStatement",
          ????????????????...
          ????????????????"innerComments":?[??//?inner注釋
          ??????????????????{
          ????????????????????"type":?"CommentLine",
          ????????????????????"value":?"?inFn",
          ????????????????????...
          ????????????????????}
          ??????????????????}
          ????????????????]
          ??????????????},
          ??????????????"trailingComments":?[??//?inFn的后置注釋
          ????????????????{
          ??????????????????"type":?"CommentLine",
          ??????????????????"value":?"?abc",
          ??????????????????...
          ????????????????}
          ??????????????]
          ????????????}
          ??????????],
          ??????????"directives":?[]
          ????????}
          ??????}
          ????],
          ????"directives":?[]
          ??},
          ??"comments":?[
          ????{
          ??????"type":?"CommentLine",
          ??????"value":?"?fn",
          ??????...
          ????},
          ????...其他注釋
          ??]
          }

          可以發(fā)現(xiàn)一共有三種注釋類型

          1. leadingComments前置注釋
          2. innerComments內(nèi)置注釋
          3. trailingComments后置注釋

          這三個(gè)屬性都對應(yīng)一個(gè)數(shù)組,每個(gè)元素都是一個(gè)對象,一個(gè)對象對應(yīng)一個(gè)注釋

          前置和后置注釋很好理解,即存在于某個(gè)節(jié)點(diǎn)的前面或者后面,innerComments呢,只存在于BlockStatement(也就是函數(shù)用的大括號(hào))里面,當(dāng)一個(gè)BlockStatement里面有其他語句的時(shí)候,這個(gè)innerComments又會(huì)變成其他語句的前置或后置注釋

          下面舉個(gè)例子

          function?App(){
          ??//?這是innerComments注釋
          }

          function?App(){
          ??const?a?=?2
          ??//?這是trailingComments注釋
          }

          除了上述三個(gè)屬性外,在最外的File層還有一個(gè)comments屬性,這里存放著里面所有注釋,但是只是可以看,并不能對其進(jìn)行操作,因?yàn)槲覀兪且獎(jiǎng)h除注釋后插入對應(yīng)代碼的,在這里操作后就不知道去哪里注入代碼了

          手寫plugin

          確定結(jié)構(gòu)

          分析完行級(jí)注釋后,接下來就要確定plugin的基本結(jié)構(gòu),既然我們只在函數(shù)里面操作,那肯定要有一個(gè)函數(shù)的入口,可以寫如下代碼

          Program:?{
          ??enter(path)?{
          ????console.log("enter");
          ????console.log("path:?",?path);
          ??},
          ??exit(path)?{
          ????console.log("exit");
          ??}
          },
          FunctionDeclaration(path,?state)?{

          },
          ClassDeclaration(path,?state)?{

          },
          ArrowFunctionExpression(path,?state)?{

          }

          但是我們在這三個(gè)節(jié)點(diǎn)里面進(jìn)行的操作都是一樣的,解決辦法就是把方法名用|分割成xxx|xxx形式的字符串

          "FunctionDeclaration|ClassDeclaration|ArrowFunctionExpression"(path,?state)?{}

          進(jìn)入函數(shù)以后,還需要對函數(shù)里面的節(jié)點(diǎn)進(jìn)行遍歷,我們可以調(diào)用traverse方法

          traverse

          traverse方法是@babel/traverse里默認(rèn)導(dǎo)出的方法,使用traverse可以手動(dòng)遍歷ast樹

          //?示例代碼
          import?*?as?babylon?from?"babylon";
          import?traverse?from?"@babel/traverse";

          const?code?=?`console.log("test")`;

          const?ast?=?babylon.parse(code);?//?babylon是babel的解析器,將代碼轉(zhuǎn)為ast樹

          traverse(ast,?{
          ??enter(path)?{
          ????...
          ??}
          });

          但是我們在visitor里面可以直接使用path.traverse方法,在traverse方法里傳入一個(gè)對象,不同于visitor,對象里面直接可以放enter方法,也可以放其他節(jié)點(diǎn)方法。注釋可能在每一個(gè)節(jié)點(diǎn)旁邊,所以我們需要觀察每一個(gè)節(jié)點(diǎn),所以我們接下來的操作全部都在enter里面。

          "FunctionDeclaration|ClassDeclaration|ArrowFunctionExpression"(path,?state)?{
          ??path.traverse({
          ????enter(path)?{
          ??????//?操作注釋
          ????}
          ??})
          }

          這時(shí)候有同學(xué)就有疑問了,我們所有的操作都在path.traverse里面,那如果是外層函數(shù)的innerComments怎么辦。現(xiàn)實(shí)是innerComments是在BlockStatement里面的,而不是在聲明語句里面的,所以我們進(jìn)來后并沒有錯(cuò)過任何在函數(shù)內(nèi)的comment

          獲取注釋

          下面就是根據(jù)ast結(jié)構(gòu)獲取comment了,我們先拿leadingComments試試水

          const?leadingComments?=?path.node.leadingComments;
          if?(leadingComments?.length)?{
          ??for?(const?comment?of?leadingComments)?{
          ????if?(comment?.type?===?"CommentLine")?{?//?只替換行級(jí)
          ??????console.log("待更換的注釋是",?comment.value);
          ????}
          ??}
          }

          我們給plugin換個(gè)“惡劣”一點(diǎn)的測試代碼

          //?@debug
          class?App?{
          ?//?inner
          ?/*?塊級(jí)?*/
          ?//?123
          ?inClass()?{
          ??//?buried-0
          ??console.log(213);
          ??//?afterConsole
          ??const?b?=?2;
          ?}
          }
          //?外邊的猴嘴

          function?fn()?{?//?fn右邊
          ?//?buried-1
          ?const?a?=?1;
          ?//?猴嘴
          }
          fn();

          const?a?=?()?=>?{
          ?//?buried-7
          };
          a();

          運(yùn)行打包后打印

          $?webpack?--config?test/webpack.config.js
          enter
          待更換的注釋是??inner
          待更換的注釋是??123
          待更換的注釋是??buried-0
          待更換的注釋是??afterConsole
          待更換的注釋是??fn右邊
          待更換的注釋是??buried-1
          exit

          可以看到從上到下打印出了leadingComments,非常成功,這下子就有信心了,趕緊加上另外兩種注釋

          handleComments(innerComments,?"innerComments");
          handleComments(trailingComments,?"trailingComments");
          handleComments(leadingComments,?"leadingComments");
          function?handleComments(comments,?commentName)?{
          ??if?(!comments?.length)?return;
          ??for?(const?comment?of?comments)?{
          ????if?(comment?.type?===?"CommentLine")?{?//?只替換行級(jí)
          ??????console.log("待更換的注釋是",?commentName,?comment.value);
          ????}
          ??}
          }

          另外把處理注釋的代碼抽離出去,沒有人想寫三次不是嗎

          打印出來

          $?webpack?--config?test/webpack.config.js
          enter
          待更換的注釋是?leadingComments??inner
          待更換的注釋是?leadingComments??123
          待更換的注釋是?trailingComments??afterConsole
          待更換的注釋是?leadingComments??buried-0
          待更換的注釋是?leadingComments??afterConsole
          待更換的注釋是?trailingComments??猴嘴
          待更換的注釋是?leadingComments??fn右邊
          待更換的注釋是?leadingComments??buried-1
          待更換的注釋是?innerComments??buried-7
          exit

          感覺非常完美,但是,怎么感覺有一個(gè)注釋重復(fù)打出來了,嚇得我趕緊看看ast樹

          破案了,原來afterConsole既是console.log(123)trailComment,又是const b=2leadingComment,這里我稱這種注釋為“雙重注釋”,這就需要我們進(jìn)行去重了,我們先來看看path有哪些屬性可以用來去重

          去重

          • 使用 path.key獲取路徑所在容器的索引

          path.key可以獲得元素的索引,那用這個(gè)屬性去重好像行得通,下面舉個(gè)例子演示一下

          const?a?=?1;?//??path.key?=?0?
          //?注釋
          const?b?=?2;?//??path.key?=?1?
          const?c?=?3;?//??path.key?=?2

          思路:以上面的a、b、c舉例,給一個(gè)全局的duplicationKey如果注釋在ab中間,當(dāng)要處理bleading時(shí),如果有key-1 === duplicationKey,就是如果b的前面有節(jié)點(diǎn)存在,則只刪除comment,但不注入代碼片段

          下面我們就來在實(shí)際的代碼實(shí)現(xiàn)一下,我們直接打印出path.key即可

          enter(path)?{
          ??console.log("pathKey:?",?path.key);
          ??...
          }

          對應(yīng)代碼片段以及運(yùn)行結(jié)果:

          //?buried-0
          console.log(213);
          //?afterConsole
          const?b?=?2;
          pathKey:??0
          待更換的注釋是?trailingComments??afterConsole
          待更換的注釋是?leadingComments??buried-0?????
          pathKey:??expression
          pathKey:??callee
          pathKey:??object
          pathKey:??property
          pathKey:??0
          pathKey:??1
          待更換的注釋是?leadingComments??afterConsole

          可以看到前一個(gè)節(jié)點(diǎn)的key確實(shí)是后面的key-1,但是這樣又要維護(hù)一個(gè)全局變量又要判斷key是不是數(shù)字,特別麻煩,我們可以使用另一個(gè)屬性

          • 使用path.getSibling(index)來獲得同級(jí)路徑

          使用path.getSibling(path.key - 1)獲取上一個(gè)兄弟節(jié)點(diǎn)即可,如果存在則只刪除但不注入代碼片段(如果只刪除一邊,是沒有效果的,編譯出來的文件還是會(huì)有注釋)

          const?isSiblingTrailExit?=?!path.getSibling(path.key?-?1)?.node?.trailingComments;
          handleComments(path,?leadingComments,?"leadingComments",?path.parent.body,?state.buriedInfo,?isSiblingTrailExit)

          我們再將測試用例寫復(fù)雜點(diǎn)

          function?fn()?{?//?fn右邊
          ?function?abc()?{
          ??//?abcInner
          ?}
          ?//?猴嘴
          }

          打包輸出

          $?webpack?--config?test/webpack.config.js
          enter
          ...
          待更換的注釋是?innerComments??abcInner
          待更換的注釋是?innerComments??abcInner
          exit

          發(fā)現(xiàn)abc函數(shù)里面的注釋被遍歷了兩次

          我們再套一層,發(fā)現(xiàn)被遍歷了三次

          function?fn()?{?//?fn右邊
          ?function?abcd()?{
          ??function?abc()?{
          ???//?abcInner
          ??}
          ?}
          ?//?猴嘴
          }

          $?webpack?--config?test/webpack.config.js
          enter
          ...
          待更換的注釋是?innerComments??abcInner
          待更換的注釋是?innerComments??abcInner
          待更換的注釋是?innerComments??abcInner
          exit

          細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn)問題了,visitor中每次進(jìn)入函數(shù)都會(huì)調(diào)用目前的這個(gè)方法,但是path.traverseenter里面也包含函數(shù)里面的函數(shù),除了最外一層函數(shù),里面的函數(shù)都是重復(fù)遍歷的,這樣時(shí)間復(fù)雜度會(huì)呈指數(shù)級(jí)增加,這當(dāng)然是不能忍的。要解決這個(gè)bug,遍歷時(shí)每個(gè)函數(shù)只需要進(jìn)入一次就夠了,那我們在enter里面碰到函數(shù)節(jié)點(diǎn)進(jìn)來時(shí)直接跳過不就行了嗎。

          跳出遍歷

          path里面每個(gè)節(jié)點(diǎn)都有對應(yīng)的一個(gè)判斷方法,判斷當(dāng)前節(jié)點(diǎn)是否是對應(yīng)類型,一般形式是path.isxxx()xxx為節(jié)點(diǎn)類型名,所以FunctionDeclarationClassDeclarationArrowFunctionExpression對應(yīng)的判斷方法為isFunctionDeclarationisArrowFunctionExpressionisClassDeclaration。判斷以后我們還需調(diào)用path.skip(),該方法能跳過當(dāng)前遍歷到的節(jié)點(diǎn)

          if?(path.isFunctionDeclaration()?||?path.isArrowFunctionExpression()?||?path.isClassDeclaration())?{
          ??path.skip();
          }

          再次打包輸出

          $?webpack?--config?test/webpack.config.js
          enter
          待更換的注釋是?leadingComments??fn右邊
          待更換的注釋是?trailingComments??猴嘴
          待更換的注釋是?innerComments??abcInner
          exit

          插入注釋

          現(xiàn)在我們已經(jīng)可以順利地拿到項(xiàng)目中所有的行級(jí)注釋了,接下來我們先將所有注釋都替換成固定的語句,如果是塊級(jí)注釋,我們可以將節(jié)點(diǎn)使用某些方法替換掉,但是對于行級(jí)注釋,我們需要分成兩步處理

          1. 插入需要的代碼片段
          2. 刪除注釋

          在我們的enter里面,我們是同時(shí)處理三種注釋,但是在插入代碼片段時(shí),leadingCommentstrailComments對應(yīng)的作用域是在上一層節(jié)點(diǎn)的,所以需要使用path.parent找到父級(jí)節(jié)點(diǎn)。

          想要插入代碼片段,必須使用template解析字符串形式的語句,將其轉(zhuǎn)為ast節(jié)點(diǎn),此方法來自@babel/template,在這里因?yàn)榇撕瘮?shù)是作為一個(gè)插件函數(shù)導(dǎo)出,所以babel的一些方法會(huì)傳入這個(gè)函數(shù),我們通過解構(gòu)獲得template,在babel底層還是調(diào)用@babel/core的,所以這個(gè)方法的實(shí)例是在@babel/core上面

          module.exports?=?function({?template?}){
          ??function?handleComments(path,?comments,?commentName,?pathBody,?isAdd)?{
          ??if?(!comments?.length)?return;
          ??for?(let?i?=?comments.length?-?1;?i?>=?0;?i--)?{
          ???const?comment?=?comments[i];
          ???if?(comment?.type?===?"CommentLine")?{?//?只替換行級(jí)
          ????const?commentArr?=?comment.value.split("-");
          ????if?(commentArr?&&?commentArr[0]?.trim()?===?"buried")?{
          ?????if?(isAdd)?{
          ??????console.log("待更換的注釋是",?commentName,?comment.value);
          ?????}

          ?????path.node[commentName].splice(i,?1);
          ????}
          ???}
          ??}
          ?}
          ...
          enter(path){
          ??...
          ??handleComments(path,?innerComments,?"innerComments",?path.node.body,?true);
          ??const?isSiblingTrailExit?=?!path.getSibling(path.key?-?1)?.node?.trailingComments;
          ??handleComments(path,?leadingComments,?"leadingComments",?path.parent.body,?isSiblingTrailExit);
          ??handleComments(path,?trailingComments,?"trailingComments",?path.parent.body,?true);
          }

          如果要對path下面的注釋進(jìn)行操作,一定要用path找到對應(yīng)comment進(jìn)行操作,所以一定要把path傳過去。

          因?yàn)槭褂?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(145, 109, 213);font-weight: bolder;background-image: none;background-position: initial;background-size: initial;background-repeat: initial;background-attachment: initial;background-origin: initial;background-clip: initial;">splice刪除數(shù)組中的元素,所以倒序遍歷

          插入注釋就直接在pathbody里面push即可,如何找到pathBody,就直接在ast樹上尋找即可,這里就省略此過程

          運(yùn)行輸出,查看main.js,可以看到全部注釋已經(jīng)被替換

          處理Excel表

          到此為止最麻煩的部分已經(jīng)解決,接下來就要替換帶有標(biāo)志的注釋了,首先要建立注釋標(biāo)志對應(yīng)的代碼片段的映射,我們的方案是讀取一個(gè)Excel表,這也是為了給不懂代碼配置的人(策劃、運(yùn)營)來填寫。

          首先確定Excel表的格式,id作為標(biāo)識(shí),屬性值是需要傳入全局函數(shù)的,我們將全局函數(shù)命名為AddStatistic,屬性值中帶有#的是變量,不帶#的是字符串

          安裝node-xlsx,運(yùn)行yarn add node-xlsx

          封裝一個(gè)解析函數(shù)

          function?parseXlsx(excelFilePath)?{
          ??//?解析excel,?獲取到所有sheets
          ??const?sheets?=?xlsx.parse(excelFilePath);
          ??const?sheet?=?sheets[0];
          ??return?sheet.data.reduce((v,?t)?=>?{
          ????if?(t[1]?===?"id")?return?v;
          ????const?paramsArr?=?[];
          ????for?(let?i?=?3;?i???????paramsArr.push(t[i]);
          ????}
          ????v[t[1]]?=?paramsArr;
          ????return?v;
          ??},?{});
          }

          返回的數(shù)據(jù)格式如下

          {
          ??'0':?[?'param1',?'#aaa'?],
          ??'1':?[?'param2',?'abc'?],?
          ??'2':?[?'param3',?'#bbb'?],
          ??'3':?[?'param4',?'def'?],?
          ??'4':?[?'param5',?'qqq'?],?
          ??'5':?[?'param6',?'#www'?],
          ??'6':?[?'param7',?'eee'?],?
          ??'7':?[?'param8',?'#rrr'?]?
          }

          處理注釋

          這個(gè)函數(shù)在Program調(diào)用,將得到的buriedInfo存儲(chǔ)在state里面

          state是存儲(chǔ)著一些全局(AST遍歷的時(shí)候所有入口都能拿到)的屬性,作為vistor的全局屬性,它可以用來存儲(chǔ)一些全局變量,我們將buriedInfo放到state里面,然后傳入path.traverse里面的handleComments

          Program:?{
          ??enter(path,?state)?{
          ????state.buriedInfo?=?parseXlsx("./buried.xlsx");
          ??},
          },

          handleComments里加一些東西,這些東西就不贅述了

          function?handleComments(path,?comments,?commentName,?pathBody,?buriedInfo,?isAdd)?{
          ??if?(!comments?.length)?return;
          ??for?(let?i?=?comments.length?-?1;?i?>=?0;?i--)?{
          ????const?comment?=?comments[i];
          ????if?(comment?.type?===?"CommentLine")?{?//?只替換行級(jí)
          ??????const?commentArr?=?comment.value.split("-");
          ??????if?(commentArr?&&?commentArr[0]?.trim()?===?"buried")?{
          ????????if?(isAdd)?{
          ??????????const?id?=?commentArr[1].trim();
          ??????????console.log("buriedInfo[id]",?id,?buriedInfo[id]);
          ??????????const?params?=?buriedInfo[id]?===?undefined???undefined?:?buriedInfo[id].map(v?=>?{
          ????????????return?v?&&?v[0]?===?"#"???v.slice(1,?v.length)?:?`"${v}"`;
          ??????????});
          ??????????const?pointAST?=?template(`window.AddStatistic(${params[0]},${params[1]});`)();
          ??????????pathBody.push(pointAST);
          ????????}

          ????????path.node[commentName].splice(i,?1);
          ??????}
          ????}
          ??}
          }

          我們拿最開始的測試用例進(jìn)行測試

          可以看到成功替換掉標(biāo)志的注釋,而且全部標(biāo)志注釋已經(jīng)刪掉

          現(xiàn)在只能往AddStatistic加入兩個(gè)參數(shù),我曾想根據(jù)Excel表動(dòng)態(tài)加入?yún)?shù),使用Array.prototype.join()形成參數(shù),但是總是報(bào)錯(cuò),如果有大佬知道怎么處理可以評(píng)論一下

          注入全局函數(shù)

          我們已經(jīng)把調(diào)用AddStatistic的語句插入到項(xiàng)目中,接下來將AddStatistic掛載到全局中,直接在Programenter里面插入即可

          Program:?{
          ??enter(path,?state)?{
          ????const?globalNode?=?template(`window.AddStatistic?=?${func}`)();
          ????path.node.body.unshift(globalNode);
          ??}
          },

          注入script

          還有我們要把對應(yīng)的script插入到html中,同樣還是在入口處插入一段代碼

          //?注入添加script代碼
          const?addSctipt?=?`(function()?{
          ???const?script?=?document.createElement("script");
          ???script.type?=?"text/javascript";
          ???script.src?=?"${script}";
          ???document.getElementsByTagName("head")[0].appendChild(script);
          ????})();`;
          const?addSctiptNode?=?template(addSctipt)();
          path.node.body.unshift(addSctiptNode);

          這里的funcscript變量都是可以自定義的,在webpack.config.js里配置

          plugins:?[
          ??[
          ????path.resolve(__dirname,?"../src/index.js"),
          ????{
          ??????xlsxPath:?path.resolve(__dirname,?"../buried.xlsx"),
          ??????func:?`function(category,?action)?{
          ?????????window._hmt?&&?window._hmt.push(["_trackEvent",?category,?action]);
          ????????};
          ????????`,
          ??????script:?"https://test.js"
          ????}
          ??]
          ]

          這里傳進(jìn)去的對象可以在state.opts中獲得

          Program:?{
          ??enter(path,?state)?{
          ????const?{?xlsxPath,?func,?script?}?=?state.opts;
          ??}
          }

          測試一下,在dist目錄下面新建一個(gè)index.html,引入main.js


          發(fā)布

          package.json中寫好配置

          {
          ??"name":?"babel-plugin-tracker",
          ??"version":?"0.0.1",
          ??"description":?"一個(gè)用于統(tǒng)計(jì)埋點(diǎn)的babel插件",
          ??"main":?"./src/index.js",
          ??"scripts":?{
          ????"test":?"echo?"Error:?no?test?specified"?&&?exit?1",
          ????"dev":?"webpack?--config?test/webpack.config.js"
          ??},
          ??"keywords":?[
          ????"webpack",
          ????"plugin",
          ????"babel",
          ????"babel-loader",
          ????"前端",
          ????"工具",
          ????"babel-plugin",
          ????"excel",
          ????"AST",
          ????"埋點(diǎn)"
          ??],
          ??"author":?"plutoLam",
          ??"license":?"MIT",
          ????...
          }

          main指向剛剛的index.js,直接運(yùn)行npm publish即可,沒有配置npm的小伙伴可以看看其他教程

          尾聲

          babel埋點(diǎn)插件的開發(fā)到這里就完成啦,希望大家可以學(xué)到一些東西

          npm地址:https://www.npmjs.com/package/babel-plugin-tracker

          GitHub地址:https://github.com/plutoLam/babel-plugin-tracker

          結(jié)語

          「??關(guān)注+點(diǎn)贊+收藏+評(píng)論+轉(zhuǎn)發(fā)??」,原創(chuàng)不易,鼓勵(lì)筆者創(chuàng)作更多高質(zhì)量文章

          最近JowayYoung上新了一本新小冊從 0 到 1 落地前端工程化,對前端工程化感興趣的可以掃碼了解下!



          瀏覽 178
          點(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>
                  美女激情操逼网站 | 日韩精品在线一二三四五区 | 爱草逼爱草逼爱草逼爱草逼爱草逼爱草逼 | 大香蕉乱伦视频 | 一级h片 |