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

          【實戰(zhàn)】基于 babel 和 postcss 查找項目中的無用模塊

          共 17390字,需瀏覽 35分鐘

           ·

          2021-05-22 23:04

          背景

          昊昊是業(yè)務(wù)線前端工程師(專業(yè)頁面仔),我是架構(gòu)組工具鏈工程師(專業(yè)工具人),有一天昊昊和說我他維護(hù)的項目中沒用到的模塊太多了,其實可以刪掉的,但是現(xiàn)在不知道哪些沒用,就不敢刪,問我是不是可以做一個工具來找出所有沒有被引用的模塊。畢竟是專業(yè)的工具人,這種需求難不倒我,于是花了半天多實現(xiàn)了這個工具。

          這個工具是一個通用的工具,node 項目、前端項目都可以用它來查找沒有用到的模塊,而且其中模塊遍歷器的思路可以應(yīng)用到很多別的地方。所以我整理了實現(xiàn)思路,寫了這篇文章。

          思路分析

          目標(biāo)是找到項目中所有沒用到的模塊。項目中總有幾個入口模塊,代碼會從這些模塊開始打包或者運(yùn)行。我們首先要知道所有的入口模塊。

          有了入口模塊之后,分析入口模塊的用到(依賴)了哪些模塊,然后再從用到的模塊分析依賴,這樣遞歸的進(jìn)行分析,直到?jīng)]有新的依賴。這個過程中,所有遍歷到的模塊就是用到的,而沒有被遍歷到的就是沒有用到的,就是我們要找的可以刪除的模塊。

          我們可以在遍歷的過程中把模塊信息和模塊之間的關(guān)系以對象和對象的關(guān)系保存,構(gòu)造成一個依賴圖(因為可能有一個模塊被兩個模塊依賴,甚至循環(huán)依賴,所以是圖)。之后對這個依賴圖的數(shù)據(jù)結(jié)構(gòu)的分析就是對模塊之間依賴關(guān)系的分析。我們這個需求只需要保存遍歷到的模塊路徑就可以,可以不生成依賴圖。

          遍歷到不同的模塊要找到它依賴的哪些模塊,對于不同的模塊有不同的分析依賴的方式:

          • js、ts、jsx、tsx 模塊根據(jù) es module 的 import 或者 commonjs 的 require 來確定依賴
          • css、less、scss 模塊根據(jù) @import 和 url() 的語法來確定依賴

          而且拿到了依賴的路徑也可能還要做一層處理,因為比如 webpack 可以配置 alias,typescript 可以配置 paths,還有 monorepo 的路徑也有自己的特點,這些路徑解析規(guī)則是我們要處理的,處理之后才能找到模塊真實路徑是啥。

          經(jīng)過從入口模塊開始的依賴分析,對模塊圖完成遍歷,把用到的模塊路徑保存下來,然后用所有模塊路徑過濾掉用到的,剩下的就是沒有使用的模塊。

          思路大概這樣,我們來實現(xiàn)一下:

          代碼實現(xiàn)

          模塊遍歷

          我們要寫一個模塊遍歷器,傳入當(dāng)前模塊的路徑和處理模塊內(nèi)容的回調(diào)函數(shù),處理過程如下:

          • 嘗試補(bǔ)全路徑,因為 .js、.json、.tsx 等可以省略后綴名
          • 根據(jù)路徑獲得模塊的類型
          • 如果是 js 模塊,用遍歷 js 的方式進(jìn)行處理
          • 如果是 css 模塊,用遍歷 css 的方式進(jìn)行處理
          const MODULE_TYPES = {
              JS1 << 0,
              CSS1 << 1,
              JSON1 << 2
          };

          function getModuleType(modulePath{
              const moduleExt = extname(modulePath);
               if (JS_EXTS.some(ext => ext === moduleExt)) {
                   return MODULE_TYPES.JS;
               } else if (CSS_EXTS.some(ext => ext === moduleExt)) {
                   return MODULE_TYPES.CSS;
               } else if (JSON_EXTS.some(ext => ext === moduleExt)) {
                   return MODULE_TYPES.JSON;
               }
          }

          function traverseModule (curModulePath, callback{
              curModulePath = completeModulePath(curModulePath);

              const moduleType = getModuleType(curModulePath);

              if (moduleType & MODULE_TYPES.JS) {
                  traverseJsModule(curModulePath, callback);
              } else if (moduleType & MODULE_TYPES.CSS) {
                  traverseCssModule(curModulePath, callback);
              }
          }

          js 模塊遍歷

          遍歷 js 模塊需要分析其中的 import 和 require 依賴。我們使用 babel 來做:

          • 讀取文件內(nèi)容
          • 根據(jù)后綴名是 .jsx、.tsx 等來決定是否啟用 typescript、jsx 的 parse 插件
          • 使用 babel parser 把代碼轉(zhuǎn)成 AST
          • 使用 babel traverse 對 AST 進(jìn)行遍歷
          • 處理 ImportDeclaration 和 CallExpression 的 AST,從中提取依賴路徑
          • 對依賴路徑進(jìn)行處理,變成真實路徑之后,繼續(xù)遍歷該路徑的模塊

          代碼如下:

          function traverseJsModule(curModulePath, callback{
              const moduleFileContent = fs.readFileSync(curModulePath, {
                  encoding'utf-8'
              });

              const ast = parser.parse(moduleFileContent, {
                  sourceType'unambiguous',
                  plugins: resolveBabelSyntaxtPlugins(curModulePath)
              });

              traverse(ast, {
                  ImportDeclaration(path) {
                      const subModulePath = moduleResolver(curModulePath, path.get('source.value').node);
                      if (!subModulePath) {
                          return;
                      }
                      callback && callback(subModulePath);
                      traverseModule(subModulePath, callback);
                  },
                  CallExpression(path) {
                      if (path.get('callee').toString() === 'require') {
                          const subModulePath = moduleResolver(curModulePath, path.get('arguments.0').toString().replace(/['"]/g''));
                          if (!subModulePath) {
                              return;
                          }
                          callback && callback(subModulePath);
                          traverseModule(subModulePath, callback);
                      }
                  }
              })
          }

          css 模塊遍歷

          遍歷 css 模塊需要分析 @import 和 url()。我們使用 postcss 來做:

          • 讀取文件內(nèi)容
          • 根據(jù)文件路徑是 .less、.scss 來決定是否啟用 less、scss 的語法插件
          • 使用 postcss.parse 把文件內(nèi)容轉(zhuǎn)成 AST
          • 遍歷 @import 節(jié)點,提取依賴路徑
          • 遍歷樣式聲明(declaration),過濾出 url() 的值,提取依賴路徑
          • 對依賴路徑進(jìn)行處理,變成真實路徑之后,繼續(xù)遍歷該路徑的模塊

          代碼如下:

          function traverseCssModule(curModulePath, callback{
              const moduleFileConent = fs.readFileSync(curModulePath, {
                  encoding'utf-8'
              });

              const ast = postcss.parse(moduleFileConent, {
                  syntaxt: resolvePostcssSyntaxtPlugin(curModulePath)
              });
              ast.walkAtRules('import', rule => {
                  const subModulePath = moduleResolver(curModulePath, rule.params.replace(/['"]/g''));
                  if (!subModulePath) {
                      return;
                  }
                  callback && callback(subModulePath);
                  traverseModule(subModulePath, callback);
              });
              ast.walkDecls(decl => {
                  if (decl.value.includes('url(')) {
                      const url = /.*url\((.+)\).*/.exec(decl.value)[1].replace(/['"]/g'');
                      const subModulePath = moduleResolver(curModulePath, url);
                      if (!subModulePath) {
                          return;
                      }
                      callback && callback(subModulePath);
                  }
              } )
          }

          模塊路徑處理

          不管是 css 還是 js 模塊都要在提取了路徑之后進(jìn)行處理:

          • 支持自定義路徑解析邏輯,讓用戶可以根據(jù)需要定制路徑解析的規(guī)則
          • 過濾掉 node_modules 下的模塊,不需要分析
          • 補(bǔ)全路徑的后綴名
          • 如果遍歷過的模塊則跳過遍歷,避免循環(huán)依賴

          代碼如下:

          const visitedModules = new Set();

          function moduleResolver (curModulePath, requirePath{
              if (typeof requirePathResolver === 'function') {// requirePathResolver 是用戶自定義的路徑解析邏輯
                  const res = requirePathResolver(dirname(curModulePath), requirePath);
                  if (typeof res === 'string') {
                      requirePath = res;
                  }
              }

              requirePath = resolve(dirname(curModulePath), requirePath);

              // 過濾掉第三方模塊
              if (requirePath.includes('node_modules')) {
                  return '';
              }

              requirePath =  completeModulePath(requirePath);

              if (visitedModules.has(requirePath)) {
                  return '';
              } else {
                  visitedModules.add(requirePath);
              }
              return requirePath;
          }

          這樣我們就完成了分析出的依賴路徑到它真實的路徑的轉(zhuǎn)換。

          路徑補(bǔ)全

          寫代碼的時候是可以省略掉一些文件的后綴(.js、.tsx、.json 等)的,我們要實現(xiàn)補(bǔ)全的邏輯:

          • 如果已經(jīng)有后綴名了,則跳過
          • 如果是目錄,則嘗試查找 index.xxx 的文件,找到了則返回該路徑
          • 如果是文件,則嘗試補(bǔ)全 .xxx 的后綴,找到了則返回該路徑
          • 沒有找到則報錯:module not found
          const JS_EXTS = ['.js''.jsx''.ts''.tsx'];
          const JSON_EXTS = ['.json'];

          function completeModulePath (modulePath{
              const EXTS = [...JSON_EXTS, ...JS_EXTS];
              if (modulePath.match(/\.[a-zA-Z]+$/)) {
                  return modulePath;
              }

              function tryCompletePath (resolvePath{
                  for (let i = 0; i < EXTS.length; i ++) {
                      let tryPath = resolvePath(EXTS[i]);
                      if (fs.existsSync(tryPath)) {
                          return tryPath;
                      }
                  }
              }

              function reportModuleNotFoundError (modulePath{
                  throw chalk.red('module not found: ' + modulePath);
              }

              if (isDirectory(modulePath)) {
                  const tryModulePath = tryCompletePath((ext) => join(modulePath, 'index' + ext));
                  if (!tryModulePath) {
                      reportModuleNotFoundError(modulePath);
                  } else {
                      return tryModulePath;
                  }
              } else if (!EXTS.some(ext => modulePath.endsWith(ext))) {
                  const tryModulePath = tryCompletePath((ext) => modulePath + ext);
                  if (!tryModulePath) {
                      reportModuleNotFoundError(modulePath);
                  } else {
                      return tryModulePath;
                  }
              }
              return modulePath;
          }

          按照上面的思路,我們實現(xiàn)了模塊的遍歷,找到了所有的用到的模塊。

          過濾出無用模塊

          上面我們找到了所有用到的模塊,接下來只要用所有的模塊過濾掉用到的模塊,就是沒有用到的模塊。

          我們封裝一個 findUnusedModule 的方法。

          傳入?yún)?shù):

          • entries(入口模塊數(shù)組)
          • includes(所有模塊的 glob 表達(dá)式)
          • resolveRequirePath(自定義路徑解析邏輯)
          • cwd(解析模塊的根路徑)

          返回一個對象,包含:

          • all (所有模塊)
          • used(用到的模塊)
          • unused(沒用到的模塊)

          處理過程:

          • 合并參數(shù)和默認(rèn)參數(shù)
          • 基于 cwd 處理 includes 的模塊路徑
          • 根據(jù) includes 的 glob 表達(dá)式找出所有的模塊
          • 以所有 entires 為入口進(jìn)行遍歷,記錄用到的模塊
          • 過濾掉用到的模塊,求出沒有用到的模塊
          const defaultOptions = {
              cwd'',
              entries: [],
              includes: ['**/*''!node_modules'],
              resolveRequirePath() => {}
          }

          function findUnusedModule (options{
              let {
                  cwd,
                  entries,
                  includes,
                  resolveRequirePath
              } = Object.assign(defaultOptions, options);

              includes = includes.map(includePath => (cwd ? `${cwd}/${includePath}` : includePath));

              const allFiles = fastGlob.sync(includes).map(item => normalize(item));
              const entryModules = [];
              const usedModules = [];

              setRequirePathResolver(resolveRequirePath);
              entries.forEach(entry => {
                  const entryPath = resolve(cwd, entry);
                  entryModules.push(entryPath);
                  traverseModule(entryPath, (modulePath) => {
                      usedModules.push(modulePath);
                  });
              });

              const unusedModules = allFiles.filter(filePath => {
                  const resolvedFilePath = resolve(filePath);
                  return !entryModules.includes(resolvedFilePath) && !usedModules.includes(resolvedFilePath);
              });
              return {
                  all: allFiles,
                  used: usedModules,
                  unused: unusedModules
              }
          }

          這樣,我們封裝的 findUnusedModule 能夠完成最初的需求:查找項目下沒有用到的模塊。

          測試功能

          我們來測試一下效果,用這個目錄作為測試項目:

          const { all, used, unused } = findUnusedModule({
              cwd: process.cwd(),
              entries: ['./demo-project/fre.js''./demo-project/suzhe2.js'],
              includes: ['./demo-project/**/*'],
              resolveRequirePath (curDir, requirePath) {
                  if (requirePath === 'b') {
                      return path.resolve(curDir, './lib/ssh.js');
                  }
                  return requirePath;
              }
          });

          結(jié)果如下:

          成功的找出了沒有用到的模塊!(可以把代碼拉下來跑一下試試)

          思考

          我們實現(xiàn)了一個模塊遍歷器,它可以對從某一個模塊開始遍歷?;谶@個遍歷器我們實現(xiàn)了查找無用模塊的需求,其實也可以用它來做別的分析需求,這個遍歷的方式是通用的。

          我們知道 babel 可以用來做兩件事情:

          • 代碼的轉(zhuǎn)譯:從 es next、typescript 等代碼轉(zhuǎn)譯成目標(biāo)環(huán)境支持的 js
          • 靜態(tài)分析:對代碼內(nèi)容做分析,比如類型檢查、lint 等,不生成代碼

          這個模塊遍歷器也可以做同樣的事情:

          • 靜態(tài)分析:分析模塊間的依賴關(guān)系,構(gòu)造依賴圖,完成一些分析功能
          • 打包:把依賴圖中每一個模塊用相應(yīng)的代碼模版打印成目標(biāo)代碼

          總結(jié)

          我們先分析了需求:找出項目中沒用到的模塊。這需要實現(xiàn)一個模塊遍歷器。

          模塊遍歷要對 js 模塊和 css 模塊做不同的處理:js 模塊分析 import 和 require,css 分析 url() 和 @import。

          之后要對分析出的路徑做處理,變成真實路徑。要處理 node_modules、webpack alias、typescript 的 types 等情況,我們暴露了一個回調(diào)函數(shù)給開發(fā)者自己去擴(kuò)展。

          實現(xiàn)了模塊遍歷之后,只要指定所有的模塊、入口模塊,那么我們就可以找出用到了哪些模塊,沒用到哪些模塊。

          經(jīng)過測試,符合我們的需求。

          這個模塊遍歷器是通用的,可以用來做各種靜態(tài)分析,也可以做后續(xù)的代碼打印做成一個打包器。

          代碼的 github 地址在這,感興趣可以拉下來跑跑,學(xué)會寫模塊遍歷器還是挺有幫助的。

          彩蛋

          當(dāng)時給昊昊介紹這個功能的時候,寫了一份實現(xiàn)思路的文檔,也貼在這里吧:

          昊昊: 光哥,整體的思路是什么樣的啊,一上來就看代碼比較亂

          :模塊是一個圖的結(jié)構(gòu),指定從某個入口開始遍歷,其實這是一個 dfs 的過程,但是有循環(huán)引用,要通過記錄處理過的模塊來解決。遞歸遍歷這個圖,處理到的模塊就是用到的。

          昊昊:dfs 一個模塊,怎么確定子模塊呢?

          :不同的模塊有不同的處理方式,比如 js 模塊,就要通過 import 或者 require 來確定子模塊,而 css 則要通過 @import 和 url() 來確定。但是這些只是提取路徑,這個路徑還是不可用的,還需要轉(zhuǎn)換成真實路徑,要有一個 resolve path 的過程。

          昊昊:resolve path 都做啥?。?/p>

          :就是處理 alias、過濾 node_modules 下的模塊,因為我們這里用不到,然后根據(jù)當(dāng)前模塊的路徑確定子模塊的絕對路徑。還要暴露出一個鉤子函數(shù)去讓用戶能夠自定義 require path 的 resolve 邏輯。

          昊昊:就是那個 requireRequirePath 么?

          :對的,那個就是暴露出去讓用戶自定義 path resolve 邏輯的鉤子。

          昊昊:我大體明白流程了?

          :說說看

          昊昊:項目的模塊構(gòu)成依賴圖,我們要確定沒有用到的模塊,那就要先找出用到的模塊,之后把它們過濾掉。用到的模塊要用幾個入口模塊開始做 dfs,遍歷不同的模塊有不同的提取 require path 的方式,提取出來以后還要對 path 進(jìn)行 resolve,得到真實路徑,然后遞歸進(jìn)行子模塊的處理。這樣遍歷完一遍就能確定用到了哪些。同時還要處理循環(huán)引用問題,因為畢竟模塊是一個圖,進(jìn)行 dfs 會有環(huán)在。

          :對的,棒棒的。


          瀏覽 92
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  日韩无码成人影片 | 国产丨在线| 大鸡巴久久久久久久 | 日本黄色视频网站大全 | 婷婷三级 |