做者:陳曉強html
上篇已經對AST基礎作了介紹,本篇介紹AST的運用vue
經常使用的知足上述3個要點的工具包有兩個,一個是esprima
,一個是babel
node
esprima相關包及使用以下webpack
const esprima = require('esprima'); // code => ast
const estraverse = require('estraverse'); //ast遍歷
const escodegen = require('escodegen'); // ast => code
let code = 'const a = 1';
const ast = esprima.parseScript(code);
estraverse.traverse(ast, {
enter: function (node) {
//節點操做
}
});
const transformCode = escodegen.generate(ast);
複製代碼
babel相關包及使用以下git
const parser = require('@babel/parser'); //code => ast
const traverse = require('@babel/traverse').default; // ast遍歷,節點增刪改查,做用域處理等
const generate = require('@babel/generator').default; // ast => code
const t = require('@babel/types'); // 用於AST節點的Lodash式工具庫,各節點構造、驗證等
let code = 'const a = 1';
let ast = parser.parse(sourceCode);
traverse(ast, {
enter (path) {
//節點操做
}
})
const transformCode = escodegen.generate(ast);
複製代碼
目前babel無論是從生態上仍是文檔上比esprima要好不少,所以推薦你們使用babel工具,本文示例也使用babel來作演示。github
如上一章節所示web
@babel/parser
用於將代碼轉換爲AST@babel/traverse
用於對AST的遍歷,包括節點增刪改查、做用域等處理@babel/generator
用於將AST轉換成代碼@babel/types
用於AST節點操做的Lodash式工具庫,各節點構造、驗證等更多api詳見babel手冊[1]小程序
下面經過簡單案例來介紹如何操做AST,注意案例只是示例,因爲篇幅對部分邊界問題只會註釋說明,實際開發過程當中須要考慮周全。api
實現代碼微信
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` function square(n) { console.log(n); console.warn(n); return n * n; } `
let ast = parser.parse(sourceCode);
traverse(ast, {
CallExpression(path) {
let { callee } = path.node;
if (callee.type === ‘MemberExpression’ && callee.object.name === ‘console’ && callee.property.name === ‘log’ ) {
path.remove(); // 注意考慮對象掛載的識別,如global.console.log(),此時remove後剩下global.,會致使語法錯誤,此時能夠判斷父節點類型來排除
}
}
})
console.log(generate(ast).code);
複製代碼
處理結果
function square(n) {
- console.log(n);
console.warn(n);
return n * n;
}
複製代碼
此案例涉及知識點
CallExpression
。callee
是在對象console上的一個方法,所以console.log
是一個成員表達式,類型爲MemberExpression
。MemberExpression
根據規範有一個object
屬性表明被訪問的對象,有一個property
表明訪問的成員。path.remove()
api能夠對節點進行刪除。babylon7
,即babe7,對應@babel/parser
實現代碼
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` function square(number) { console.warn(number); return number * number; } `
let ast = parser.parse(sourceCode);
traverse(ast, {
FunctionDeclaration(path) {
let unia = path.scope.generateUidIdentifier("a");
path.scope.rename("number",unia.name);
}
})
console.log(generate(ast).code);
複製代碼
處理結果
-function square(number) {
+ function square(_a) {
- console.warn(number);
+ console.warn(_a);
- return number * number;
+ return _a * _a;
}
複製代碼
此案例涉及知識點
path.scope
保存了當前做用域的相關信息path.scope
能夠得到當前做用域惟一標識符,避免變量名衝突實現代碼
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` new Promise((resolve,reject)=>{ setTimeout(()=>{ resolve(1); },200) }); `
let ast = parser.parse(sourceCode);
traverse(ast, {
ArrowFunctionExpression (path) {
let { id, params, body } = path.node;
for(let key in path.scope.bindings){ //注意考慮箭頭函數的this特性,若發現函數體中有this調用,則須要在當前做用域綁定其父做用域的this
if(!path.scope.bindings[key].referenced){
params = params.filter(param=>{
return param.name!==key;
})
}
}
path.replaceWith(t.functionExpression(id, params, body));
}
})
console.log(generate(ast).code);
複製代碼
處理結果
-new Promise((resolve,reject)=>{
+new Promise(function(resolve){
- setTimeout(()=>{
+ setTimeout(function(){
resolve(1);
},200)
});
複製代碼
此案例涉及知識點
ArrowFunctionExpression
path.replaceWith()
能夠進行節點替換刪掉小程序中的冗餘代碼,部分
實現代碼示例以下
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
let sourceCode = ` export function square (x) { return x * x; } export function cube (x) { return x * x * x; } `
let ast = parser.parse(sourceCode);
traverse(ast, {
ExportNamedDeclaration (path) {
let unused = ['cube'] // 藉助webpack,咱們能得到導出的方法中,哪些是沒有被使用過的
let { declaration = {} } = path.node;
if (declaration.type === 'FunctionDeclaration') {
unused.forEach(exportItem => {
// references=1表示僅有一次引用,即export的引用,沒有在別處調用
if (declaration.id.name === exportItem && path.scope.bindings[exportItem].references === 1) {
path.remove();
}
});
}
}
})
console.log(generate(ast).code);
複製代碼
處理結果
export function square (x) {
return x * x;
}
-export function cube (x) {
- return x * x * x;
-}
複製代碼
此案例涉及知識點
ExportNamedDeclaration
此案例是git上一個比較有意思的開源項目,經過AST將代碼轉換爲svg流程圖,詳見js-code-to-svg-flowchart[2]
能夠體驗一下:demo[3]
經過以上示例,能夠看到經過AST咱們能夠對代碼任意蹂躪,作出不少有意思的事情
除了Javascript,其餘語言如HTML、CSS、SQL等也有普遍的AST應用。以下圖,能夠在這裏找到對應語言的解析器,開啓AST之門。
在上述AST網站中,能夠看到HTML的解析器有個vue選項,讀過vue源碼的同窗應該知道vue模板在轉換成HTML以前會先將模板轉換成AST而後生成render function進而生成VirtualDOM。咱們平時開發對AST使用比較少,但其實處處都能見到AST的影子:babel、webpack、eslint、taro等等。但願能拋磚引玉,使同窗們在各自團隊產出更多基於AST的優秀工具、項目。
References
[1] babel手冊
[2] js-code-to-svg-flowchart
[3] demo
若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam):