文章代碼的源碼倉庫javascript
AST(Abstract Syntax Tree)既抽象語法樹,或稱語法樹,簡單來講就是代碼語法結構的一種抽象表示。好比 var answer = 6 * 7;
會被解析爲這麼一棵樹 html
那麼代碼怎樣才能解析成這一棵 AST, AST在前端領域通常又能夠幹嗎?前端
ast是由編譯器解析生成的,簡單的編譯器能夠由如下幾部分組成:java
tokens
咱們前端構建中很經常使用的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 在不少狀況下能夠提供便利,可是若是項目參加的人太多,又沒有什麼約束,你們貪圖方便什麼都加到alias....就會變成這樣子
咱們先來整理一下思路
咱們這裏的把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);
複製代碼
這顯然是不行的,先不說格式的問題,一個文件連換行和註釋都沒有,那他就是沒有靈魂的js~
看了下這是由於 esprima
在解析的時候,遇到空行和註釋會直接跳過不解析生成AST,因此會致使後面生成的代碼沒有空行和註釋。
咱們平時項目上用的最多的轉換代碼的工具就是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}`)
}
複製代碼
到此減小webpack-alias的功能處理完成,最後總結一下
glob
讀取全部要轉的js文件babylon
將js文件解析成ASTbabel-traverse
處理AST,判斷若是是 require('xxx')
或者import xxx from 'xxx'
替換掉這些路徑babel-generator
將新生成的AST轉化爲代碼prettier
格式化新生成的代碼,保持與原項目風格一致最後寫的時候參考到的連接,大部分是類庫的文檔 迷你編譯器