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

          代碼自動化重構利器——jscodeshift 初探

          共 8438字,需瀏覽 17分鐘

           ·

          2021-03-10 16:22

          背景

          開發(fā)維護規(guī)模較大的前端項目,難免時不時需要進行一些代碼重構工作。舉一個簡單的 ??:

          • 有一個 npm 包 an-npm-package-containing-constants,用于維護項目埋點時使用的字符串常量,其代碼主要內容如下:
          export const ConstantsForTrack = {
            Track1'track_in_scene_1',
            Track2'track_in_scene_2',
            ...
          };
          • 調用方以如下方式使用此包:
          import { ConstantsForTrack } from 'an-npm-package-containing-constants';
          track(ConstantsForTrack.Track1, ...otherParams);
          • 隨著業(yè)務迭代,此包體積不斷增長,已經開始影響到項目的整體性能。所有業(yè)務的埋點標識字符串都集中于此包中,因此我們改變代碼的導出方式:
          export const Track1 = 'track_in_scene_1';
          export const Track2 = 'track_in_scene_2';
          ...
          • 各業(yè)務通過按需引入,控制代碼體積:
          import { Track1 } from 'an-npm-package-containing-constants/es/constants';

          track(Track1, ...otherParams);

          包內代碼的重構不算特別復雜,但是要對項目內各處引入該包以及埋點調用的方式進行修改,還是一件比較頭疼的事情。IDE 的全局替換功能顯然無法勝任這一工作;人工替換違背了程序員的 DRY[1] 原則,過程枯燥且有可能出錯;寫一個基于正則的替換腳本倒不是不可行,只是這樣的腳本一般可維護性和可復用性都不太好,并且可能存在一些 bad case(如可能會匹配到注釋或字符串字面量中的內容、需要考慮不同代碼風格的情況)。

          此時筆者想起了之前聽說到的一個叫作 codemod 的概念,據說是一種對代碼進行批量修改操作的方法;并且 react[2]vue[3] 還有 Ant Design[4] 都提供了自己的官方 codemod,以幫助用戶遷移到更新版本,避免其面對升級新版本后手動處理接口變更的痛苦。這次不妨嘗嘗鮮,也算是為今后這種類似的重構工作先踩踩坑。

          相關概念簡介

          Codemod[5]

          Codemod is a tool/library to assist you with large-scale codebase refactors that can be partially automated but still require human oversight and occasional intervention.

          Codemod 是一個誕生于 Facebook 內部的概念,可以理解為 "code modification" 的縮寫。如官方介紹所述,codemod 針對的場景是規(guī)模較大的代碼庫中的重構工作。當某個在代碼中被頻繁使用的接口發(fā)生了無法向前兼容的重大變化,codemod 提供了快速且可靠的、半自動的工具來對代碼庫中所有相關代碼進行重構,以幫助開發(fā)者對代碼進行快速迭代。

          jscodeshift[6]

          jscodeshift 是一個基于 codemod 理念的 JavaScript/TypeScript 重構工具,其原理是將 JS/TS 代碼解析為抽象語法樹(Abstract Syntax Tree,AST),并提供一系列用于訪問和修改 AST 的 API 以實現自動化的代碼重構。jscodeshift 將 babel parser、ast-types[7](用于快速創(chuàng)建新的 AST 節(jié)點)和 recast[8](維護生成代碼的代碼風格信息)三大工具整合在一起,提供了簡便快捷的操作接口;同時它還提供了多任務并行執(zhí)行的功能,使其對于海量代碼文件的重構操作可以并行運行,充分利用多核 CPU 算力,縮短重構任務執(zhí)行時間。

          抽象語法樹

          相信大家都在編譯原理的課程中了解過抽象語法樹的概念,這里先引用一段維基百科上的描述:

          在計算機科學中,抽象語法樹Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每個節(jié)點都表示源代碼中的一種結構。之所以說語法是“抽象”的,是因為這里的語法并不會表示出真實語法中出現的每個細節(jié)。

          我們再以一段簡單代碼為例:

          if (1 + 1 == 3) {
            alert('time to wake up!');
          }

          來更加具體地了解一下抽象語法樹是什么樣子:(由 http://nhiro.org/learn_language/AST-Visualization-on-browser.html 生成)

          在 JavaScript 工程領域中,不僅僅只有 JavaScript 引擎解析代碼會涉及到 AST,在代碼轉譯(babel)、靜態(tài)分析(eslint)、打包構建(webpack、rollup...)中,都會將代碼解析為 AST 以作進一步的操作。

          動手開干,寫一個 codemod

          jscodeshift 雖然很好很強大,但是官方文檔提供的信息對于幫助我們快速寫出一個 codemod 來說卻相對有限。因此了解掌握 jscodeshift 最好的辦法就是 get hands dirty,參考一些已有的 codemod 腳本和網上的文章,自己寫一個 codemod。我們以解決背景一節(jié)提及的這個問題為例,寫一個 codemod 來完成 an-npm-package-containing-constants 引入和使用方式的修改。

          在動手之前,先介紹一個超強的 AST 可視化工具—— AST Explorer[9]。如下圖所示,我們把想要修改的代碼粘貼在左側,即可即時在右側看到解析代碼獲得的語法樹并查看其中各個節(jié)點的屬性。通過這個工具,我們可以對 AST 有一個更加直觀的認識;在編寫 codemod 的過程中,我們也可以通過這個工具快速定位需要修改的節(jié)點。打開 "Transform" 開關,你還可以直接在瀏覽器里書寫 codemod 腳本,并即時看到 codemod 轉換后的效果。

          通過對代碼及其 AST 的大致分析,我們可以把替換工作拆解為以下幾步:

          1. 遍歷文件,篩選出引入了 "an-npm-package-containing-constants" 的代碼文件;
          2. 查找篩選出的文件中所有對 ConstantsForTrack 對象成員的訪問,并將 ConstantsForTrack 成員訪問表達式直接替換為常量名的表達式;
          3. 收集代碼中使用的常量名,生成新的 import 語句并替換舊語句。

          現在我們可以開始編寫 codemod 了!根據官方文檔[10],codemod 代碼需導出一個函數,jscodeshift 在運行此函數時會通過參數注入 API 和一些相關信息。由此我們可以寫出一個 codemod 的初步的框架:

          module.exports = function (
            fileInfo, // 當前處理文件的相關信息,包括文件路徑與內容
            api, // jscodeshift 提供的接口
            options // 通過 jscodeshift CLI 傳入的參數
          {
            const { source, path } = fileInfo;
            const { jscodeshift: j } = api;
            const root = j(source);  // 解析代碼獲得 AST

            // 在這里編寫操作 AST 的代碼

            return root.toSource({ quote'single' }); // 將 AST 轉換為代碼字符串后返回
          }

          需要注意的是,代碼最后的 toSource() 函數可以傳入一些代碼風格相關的配置。recast 在解析代碼時,會將代碼風格相關信息維護在語法樹中,在 toSource() 過程中再將代碼還原成原本的樣子。而在代碼轉換為語法樹后新插入的節(jié)點并沒有這些具體的代碼風格信息。具體如何配置這些代碼的風格可以參考這個文件[11]里的接口定義。

          第一步:篩選需要修改的文件

          由于我們只需關注引入了 an-npm-package-containing-constants 的代碼文件,所以我們先在 AST Explorer 里觀察解析 import { ConstantsForTrack } from 'an-npm-package-containing-constants'; 語句獲得的 AST 子樹,可以發(fā)現這棵子樹的根結點類型是 "ImportDeclaration",節(jié)點中 source 屬性的 value 字段就是我們所尋找的節(jié)點特征 "an-npm-package-containing-constants"。

          接下來就是編寫代碼將這類節(jié)點篩選出來。鑒于 AST 的數據結構特征,一般的語法解析器 (parser) 以及 recast 提供的 API 都基于訪問者模式來對 AST 進行遍歷 (traversal)。jscodeshift 基于 Collection 的概念對 API 進行了進一步封裝[12],令使用者對 AST 節(jié)點的篩選及修改操作變得更加簡單。一些常用的 Collection API 我們會在接下來的實踐過程中用到,完整的 API 列表可以參閱以下源碼:

          集合基本操作:https://github.com/facebook/jscodeshift/blob/master/src/Collection.js AST 節(jié)點訪問與修改:https://github.com/facebook/jscodeshift/blob/master/src/collections/Node.js

          這里我們使用 find() 從語法樹中獲得目標節(jié)點的集合,若集合為空,則跳過此文件。代碼如下:

          const trackConstantsImportDeclarations = root.find(j.ImportDeclaration, {
            source: { value'an-npm-package-containing-constants' }
          });

          if (!trackConstantsImportDeclarations.length) {
            // 返回 undefined 表示此文件無需修改
            return;
          }

          第二步:收集代碼中所有相關常量的訪問并進行替換

          我們再來分析一下 ConstantsForTrack.Track2

          可以看到這個表達式被解析成了一個 MemberExpression 類型的節(jié)點,該節(jié)點的 object 屬性是一個 name 為 "ConstantsForTrack" 的 Identifier 節(jié)點,property 是一個 name 為 "Track2" 的 Identifier 節(jié)點。

          我們要把所有 ConstantForTrack.[constant name] 替換為 [constant name],只需使用 jscodeshift 提供的 replaceWith() 接口,把相應的 MemberExpression 節(jié)點替換為其 property 屬性中的 Identifier 節(jié)點即可。另外為了下一步將這些常量引入進來,我們需要把這些 Identifier 的 name 屬性收集起來。代碼如下:

          let usedKeys = [];
          const trackConstantsMemberExpressions = root.find(j.MemberExpression, {
            object: { name'ConstantsForTrack' }
          });
          trackConstantsMemberExpressions.replaceWith((nodePath) => {
            // replaceWith 在遍歷集合的回調函數中傳入的參數類型是 NodePath
            // NodePath 除了節(jié)點自身的信息外還包含節(jié)點的上下文信息,因此需要先把節(jié)點從中取出來
            const { node } = nodePath;
            const keyId = node.property;
            if (keyId.name) {
              usedKeys.push(keyId.name);
              return keyId;
            }
          });
          if (!usedKeys.length) {
            return;
          }

          第三步:替換 import 語句

          這一步的目標是將 import { ConstantsForTrack } from 'an-npm-package-containing-constants'; 替換為 import { Track2 } from 'an-npm-package-containing-constants/es/constants';。我們先分析一下后者的語法樹:

          可見 ImportDeclaration 的 specifiers 屬性記錄了 import 語句所引入的內容,以 ImportSpecifier 數組的形式表示。現在我們可以重新構造一個新的 ImportDeclaration 節(jié)點,代碼如下:

          usedKeys = [...new Set(usedKeys)];

          const keyIds = usedKeys.map((key) => j.importSpecifier(j.identifier(key)));
          const trackConstantsEsImportDeclaration = j.importDeclaration(
            keyIds,
            j.literal("an-npm-package-containing-constants/es/constants")
          );
          // 替換原來的 import 語句
          trackConstantsImportDeclarations.at(0).replaceWith(
            () => trackConstantsEsImportDeclaration
          );

          jscodeshift 基于 ast-types 提供了各種節(jié)點的構建接口[13],接口形式如以上代碼所示,以駝峰命名法(小寫字母開頭)形式表示,與第一步中用于篩選的節(jié)點類型(帕斯卡命名法表示,大寫字母開頭)區(qū)分開來。不同節(jié)點構建方法的具體參數可以參閱源代碼[14],AST Explorer 中也提供了相關代碼提示。

          至此我們的 codemod 腳本已經完成,可以嘗試執(zhí)行一下:(在運行之前,你需要保證已經通過 npm install -g jscodeshift 全局安裝上了 jscodeshift)

          自動重構順利完成!這里介紹幾個常用的 CLI 參數,更加具體的信息可以參閱這里[15]

          -c, --cpus=N            最多開啟 N 個子進程并行運行 codemod 腳本
          (默認為 max(CPU 總核心數 - 1, 1))
          -d, --(no-)dry 測試運行,不對文件作實際修改(默認關閉)
          --extensions=EXT 需要處理的文件擴展名(多個用“,”隔開,默認為 js)
          --parser=babel|babylon|flow|ts|tsx
          解析文件使用的 parser,默認為 babel
          -t, --transform=FILE codemod 腳本的路徑或 URL,默認為 "./transform.js"
          -v, --verbose=0|1|2 展示 codemod 執(zhí)行過程中的相關信息

          總結

          文章的最后,總結一下使用 jscodeshift 常用的 API 以及相關的參考文檔和源碼:

          • AST 的查找與篩選:find()、filter()
          • Collection 訪問:get()at()(兩者區(qū)別在于前者返回 NodePath,后者返回 Collection
          • 節(jié)點的插入與修改:replaceWith()、insertBefore()、insertAfter()

          Collection 常用 API: https://github.com/facebook/jscodeshift/blob/master/src/Collection.js https://github.com/facebook/jscodeshift/blob/master/src/collections/Node.js

          AST 節(jié)點構建參數:https://github.com/benjamn/ast-types/tree/master/def

          jscodeshift CLI 參數:https://github.com/facebook/jscodeshift#usage-cli

          希望本文可以為大家的代碼重構工作提供一些啟發(fā),幫助大家從一些重復的代碼修改工作中解放出來。

          來源:字節(jié)前端 Byte FE

          最后

          歡迎關注【前端瓶子君】??ヽ(°▽°)ノ?
          回復「算法」,加入前端算法源碼編程群,每日一刷(工作日),每題瓶子君都會很認真的解答喲
          回復「交流」,吹吹水、聊聊技術、吐吐槽!
          回復「閱讀」,每日刷刷高質量好文!
          如果這篇文章對你有幫助,在看」是最大的支持
          》》面試官也在看的算法資料《《
          “在看和轉發(fā)”就是最大的支持
          瀏覽 59
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  操逼网站欧美 | 亚洲欧洲欧美日韩中文字幕 | 91美女被操逼 | 手机看黑人操逼片 | 秘 蜜桃视频在线播放 |