利用AST解決項目webpack alias氾濫問題

文章代碼的源碼倉庫javascript

AST 簡單介紹

AST(Abstract Syntax Tree)既抽象語法樹,或稱語法樹,簡單來講就是代碼語法結構的一種抽象表示。好比 var answer = 6 * 7; 會被解析爲這麼一棵樹 html

那麼代碼怎樣才能解析成這一棵 AST, AST在前端領域通常又能夠幹嗎?前端

編譯器

ast是由編譯器解析生成的,簡單的編譯器能夠由如下幾部分組成:java

  • tokenizer 分詞器 把代碼或文本按類型分開,返回: tokens
  • parser 語法剖析器 對分詞後的文本按照語法分析轉換成 抽象語法樹 返回:ast
  • transformer 語法轉換器 把語法剖析後的抽象語法樹轉換成咱們想要的: newAst
  • codeGenerator 代碼生成器 把AST 轉換爲 目標代碼

咱們前端構建中很經常使用的babel就是這種原理node

babel 初始階段並無作任何事,基本上等於 const babel = code=> code; 先 tokenizer, parser 解析代碼,再 transformer 的時候,徹底不改動原來的 astjquery

對編譯器原理有興趣的,能夠看我之前寫的小demo,500行簡單易懂 min-compiler,看完會有個總體概念。webpack

而生成的AST咱們能夠用來作什麼?git

AST你都拿到了,剩下的事情就是對這棵樹作你想要的操做,好比代碼轉換(babel),代碼壓縮等。github

這裏我用他來處理webpack的alias氾濫問題。web

webpack alias問題

webpack alias 在不少狀況下能夠提供便利,可是若是項目參加的人太多,又沒有什麼約束,你們貪圖方便什麼都加到alias....就會變成這樣子

  • 不少你不知道他究竟是node_modules裏面的包仍是本身封裝過的
  • 不少寫二級路徑就能夠獲取到的,不必多加個alias
  • 如今比較推崇不是很遠的路徑都寫成相對路徑,編輯器能夠直接跳過去方便(雖然這個能夠經過jsconfig來解決,可是太多看着也很煩) 因此我決定把項目裏的alias從23個縮減爲7個。

利用 estools 解決webpack Alias

咱們先來整理一下思路

  • 先用解析器把代碼解析成 AST
  • 再找出咱們須要去除的alias,把他改成其餘值,生成新的AST
  • 把新的AST轉爲代碼,從新寫入文件

咱們這裏的把alias改成其餘值,指的是這種狀況

目錄結構:
- src
  - components
    - btn

alias: {
  btn: path.resolve(basepath, 'src/components/btn'),
  btn: path.resolve(basepath, 'src/components'),
}

原來的引入 import Btn from 'btn';
改成 import Btn from 'components/btn';
複製代碼

這裏咱們用 esprima 來作代碼分析生成ast,用 estraverse 來轉換代碼,用 escodegen 生成代碼。直接上代碼

const aliasConfig = { /* webpack alias 配置*/}

function translateAlias(filePath) {

  // 解析ast
  const codeStr = fs.readFileSync(filePath).toString();
  const ast = esprima.parseModule(codeStr);

  // 轉換ast
  estraverse.traverse(ast, {
    // 對於每一個node節點都會進入這個函數
    enter(node, parent) {
      // 判斷是不是咱們的目標文件
      const isAliasDec = isRequireDeclaration(node, parent);
      if (isAliasDec) {
        // 替換掉alias => newAlias
        const newVal = getModulePath(node.value, filePath);
        node.value = newVal;
      }
    },
  });

  // 從新生成代碼
  const newCodeStr = escodegen.generate(ast);
  fs.writeFileSync(filePath, newCodeStr, {});
}

// 工具函數: 判斷是不是 require
function isRequireDeclaration(node, parent) {
  const { type, value } = node;
  const { callee } = parent || {};
  // 類型一致 && 該key在aliasKey中 && 是 require引入的
  return (
    type === 'Literal' &&
    aliasKey.includes(value) &&
    !allowAliasKey.includes(value) &&
    isRequest(callee)
  );
}

// 工具函數:獲取路徑
function getModulePath(aliasKey, filePath) {
  const firstDir = /\w*/.exec(aliasKey)[0];

  const modulePath = aliasKey.replace(firstDir, aliasConfig[firstDir]);
  const aliasPath = aliasKey.replace(firstDir, aliasMap[firstDir]);

  if (!aliasConfig[firstDir] || !aliasMap[firstDir] || allowAliasKey.includes(firstDir)) return false;

  // 獲取引入的模塊與當前模塊相對路徑,判斷是否太長,是就返回alias,不然就返回相對路徑就完事了
  const relativePath = path.relative(filePath, modulePath);
  const relativeTime = relativePath.split('../').length - 1;
  return (relativeTime < MAX_RELATIVE)? relativePath: aliasPath;
}

translateAlias(filePath);
複製代碼

estool_ast
試跑了一下,發現說雖然代碼引用確實有被替換了,可是代碼裏面的全部空行和註釋都丟了,並且一些規範格式也和原來不同。

這顯然是不行的,先不說格式的問題,一個文件連換行和註釋都沒有,那他就是沒有靈魂的js~

看了下這是由於 esprima 在解析的時候,遇到空行和註釋會直接跳過不解析生成AST,因此會致使後面生成的代碼沒有空行和註釋。

babel解決空行和註釋等問題、prettier保持代碼風格一致

咱們平時項目上用的最多的轉換代碼的工具就是babel,那麼咱們也能夠把 esTool 那一套換成 babel生態,用babel來幫咱們作這些轉換。

原理和思路基本上是同樣的,用 babylon 解析,babel-traverse 轉換,再用babel-generator生成代碼。 生成以後,先不寫進去,而是用 prettier 格式化一遍再重寫到本地,以保持和原來的風格一致。

function translateAlias(filePath) {
  console.log(`開始處理第${i++}個: ${filePath}`)
  const code = fs.readFileSync(filePath).toString();

  // 獲取ast
  const ast = babylon.parse(code, {
    sourceType: 'module',
    plugins: ['jsx', 'objectRestSpread']
  });

  traverse(ast, {
    enter(path) {
      // 轉換 CommonJs 的狀況
      translateRequireModulePath(path, filePath);
      // 轉換 ESM 的狀況
      translateImportModulePath(path, filePath);
    }
  });

  const newCode = generate(ast, {});
  // 從新用項目的prettier配置格式化多一次再寫入
  const prettierCode = prettier.format(newCode.code, prettierConfig);
  fs.writeFileSync(filePath, prettierCode);
  console.log(`處理結束${filePath}`)
}
複製代碼

./babel.png

到此減小webpack-alias的功能處理完成,最後總結一下

  1. glob讀取全部要轉的js文件
  2. babylon 將js文件解析成AST
  3. babel-traverse 處理AST,判斷若是是 require('xxx')或者import xxx from 'xxx' 替換掉這些路徑
  4. babel-generator 將新生成的AST轉化爲代碼
  5. prettier 格式化新生成的代碼,保持與原項目風格一致
  6. 從新寫入本地文件

告辭!感興趣能夠github交個友

./kaiji.jpg

最後寫的時候參考到的連接,大部分是類庫的文檔 迷你編譯器

estools代碼生成escodegen

estools代碼轉換estraverse

代碼解析esprima

babel plugin book

babel-generator

babel-traverse

babylon

在線ast生成

在線ast生成

相關文章
相關標籤/搜索