象語法樹(Abstract Syntax Trees),簡稱AST,如果您正在編寫代碼,那么 AST 很可能已經(jīng)參與了您的開發(fā)流程。它們?yōu)槟拈_發(fā)流程的許多部分提供動力。 有些人可能在編譯器的上下文中聽說過它們,但它們被用于各種工具中。 即使您不編寫通用開發(fā)工具,AST 也可能是您工具帶中的有用工具。 在這篇文章中,我們將討論什么是 AST,它們在哪里使用以及如何利用它們。
什么是AST
抽象語法樹或 AST 是代碼的樹型數(shù)據(jù)結(jié)構(gòu)表示。 它們是編譯器工作方式的基本部分。 當(dāng)編譯器轉(zhuǎn)換某些代碼時,基本上有以下步驟:
詞法分析又名標(biāo)記化
在此步驟中,您編寫的代碼將被轉(zhuǎn)換為一組描述代碼不同部分的標(biāo)記。 這與基本語法突出顯示使用的方法基本相同。 這些標(biāo)記令牌不了解事物如何組合在一起,并且僅關(guān)注文件的組件。
你可以想象這就像你將一個文本分解成單詞。 您可能能夠區(qū)分標(biāo)點符號、動詞、名詞、數(shù)字等,但在這個階段,您對句子的組成部分或句子如何組合沒有任何更深入的了解。
語法分析又名解析
這是我們將標(biāo)記列表轉(zhuǎn)換為抽象語法樹的步驟。 它將我們的標(biāo)記轉(zhuǎn)換為表示代碼實際結(jié)構(gòu)的樹。 以前在標(biāo)記中我們只有一對 (),現(xiàn)在我們知道它是函數(shù)調(diào)用、函數(shù)定義、分組還是其他東西。
這里的等價物是將我們的單詞列表轉(zhuǎn)換為表示諸如句子之類的數(shù)據(jù)結(jié)構(gòu),某個名詞在句子中扮演什么角色,或者我們是否在列表中。
另一個可以與之比較的例子是 DOM。 上一步只是將 HTML 分解為“標(biāo)簽”和“文本”,而這一步將生成表示為 DOM 樹的層次結(jié)構(gòu)。
需要注意的一件事是沒有“單一”的 AST 格式。 它們可能會有所不同,這取決于您要轉(zhuǎn)換為 AST 的語言以及您用于解析的工具。 在 JavaScript 中,一個通用標(biāo)準(zhǔn)是 ESTree,但您會看到不同的工具可能會添加不同的屬性。
一般來說,AST 是一種樹結(jié)構(gòu),其中每個節(jié)點至少有一個類型來指定它所代表的內(nèi)容。
代碼生成
此步驟本身可以是多個步驟。 一旦我們有了抽象語法樹,我們就可以操作它,也可以將它“打印”到不同類型的代碼中。 使用 AST 操作代碼比直接在代碼上作為文本或標(biāo)記列表執(zhí)行這些操作更安全。
操縱文本總是很危險的; 它顯示最少的上下文。 如果您曾經(jīng)嘗試使用字符串替換或正則表達(dá)式來操作文本,您可能會注意到很容易出錯。而且不容易調(diào)試。
甚至操縱令牌也不容易。 雖然我們可能知道變量是什么,但如果我們想重命名它,我們將無法深入了解變量的范圍或可能與之沖突的任何變量。
AST 提供了有關(guān)代碼結(jié)構(gòu)的足夠信息,我們可以更有信心地對其進(jìn)行修改。 例如,我們可以確定變量的聲明位置,并確切地知道由于樹結(jié)構(gòu)而影響程序的哪個部分。
一旦我們操縱了樹,我們就可以打印樹以輸出任何預(yù)期的代碼輸出。 例如,如果我們要構(gòu)建一個像 TypeScript 編譯器這樣的編譯器,我們會輸出 JavaScript,而另一個編譯器可能會輸出機(jī)器代碼。
同樣,使用 AST 更容易實現(xiàn)這一點,因為相同結(jié)構(gòu)的不同輸出可能具有不同的格式。 使用更線性的輸入(如文本或標(biāo)記列表)生成輸出會相當(dāng)困難。
如何處理 AST?
理論涵蓋了哪些實際生活中的 AST 用例? 我們討論了編譯器,但我們并不是整天都在構(gòu)建編譯器。
AST 的用例很廣泛,通常可以分為三個總體操作:讀取、修改和打印。 它們是一種添加劑,這意味著如果您正在打印 AST,那么您以前也閱讀過 AST 并對其進(jìn)行修改的可能性很高。 但我們將介紹每個主要關(guān)注一個用例的示例。
讀取/遍歷 AST
從技術(shù)上講,使用 AST 的第一步是解析文本以創(chuàng)建 AST,但在大多數(shù)情況下,提供解析步驟的庫也提供了一種遍歷 AST 的方法。遍歷 AST 意味著訪問樹的不同節(jié)點以獲取細(xì)節(jié)或執(zhí)行操作。
最常見的用例之一是 linting。 例如,ESLint 使用 espree 生成 AST,如果您想編寫任何自定義規(guī)則,您將根據(jù)不同的 AST 節(jié)點編寫這些規(guī)則。 ESLint 文檔有大量關(guān)于如何構(gòu)建自定義規(guī)則、插件和格式化程序的文檔。
修改/轉(zhuǎn)換 AST
如前所述,與將代碼修改為標(biāo)記或原始字符串相比,擁有 AST 使修改所述樹更容易、更安全。您可能想要使用 AST 修改某些代碼的原因有很多種。
例如,Babel 修改 AST 以向下編譯新功能或?qū)?JSX 轉(zhuǎn)換為函數(shù)調(diào)用。例如,當(dāng)您編譯 React 或 Preact 代碼時會發(fā)生這種情況。
另一個用例是捆綁代碼。在模塊的世界中,捆綁代碼通常比將文件附加在一起要復(fù)雜得多。更好地了解各個文件的結(jié)構(gòu)可以更輕松地合并這些文件并在必要時調(diào)整導(dǎo)入和函數(shù)調(diào)用。如果您查看 webpack、parcel 或 rollup 等工具的代碼庫,您會發(fā)現(xiàn)它們都使用 AST 作為其捆綁工作流程的一部分。
打印 AST
在大多數(shù)情況下,打印和修改 AST 是齊頭并進(jìn)的,因為您必須輸出剛剛修改的 AST。 但是,雖然像 recast 這樣的一些庫明確專注于以與原始代碼樣式相同的代碼樣式打印 AST,但也有各種用例,您希望以不同的方式顯式打印您的 AST。
例如,Prettier 使用 AST 根據(jù)您的配置重新格式化您的代碼,而無需更改代碼的內(nèi)容/含義。 他們這樣做的方式是將您的代碼轉(zhuǎn)換為完全與格式無關(guān)的 AST,然后根據(jù)您的規(guī)則重寫它。
其他常見的用例是用不同的目標(biāo)語言打印代碼或構(gòu)建自己的縮小工具。
您可以使用幾種不同的工具來打印 AST,例如 escodegen 或 astring。 您還可以根據(jù)您的用例構(gòu)建自己的格式化程序,或者為 Prettier 構(gòu)建一個插件。
最后:
雖然 AST 可能是大多數(shù)開發(fā)人員每天都不會使用的東西,但我相信了解它對今后的工作會有幫助。感謝閱讀。
在開始前:如有不準(zhǔn)確的地方希望大家提出,文章可以改知識不能錯。
抽象語法樹(AST),nodejs,UglifyJS,gulp,through2搜索引擎輸入相關(guān)關(guān)鍵字會有很多文章這里就不一一闡述。
UglifyJS http://lisperator.net/uglifyjs/
gulp https://www.gulpjs.com.cn/
through https://cnodejs.org/topic/59f220b928137001719a8270
抽象語法樹(AST) http://www.iteye.com/news/30731
編碼過程中難免會寫一些console.log...輸出語句用于測試,如果未及時刪除發(fā)布后會出現(xiàn)莫名其妙的輸出打印在控制臺,這里叫它們幽靈輸出。
造個輪子?
定義一個統(tǒng)一的日志輸出方法,通過設(shè)置開關(guān)變量來控制輸出打印
...
function printLog(src){
if(dev){
console.log(src);
}
}
...
輪子不圓改一改嘛
為避免統(tǒng)一定義打印方法使用遺漏問題,決定重新定義系統(tǒng)console
let oldInfo=console.info;
console.info=function(){
if(dev){
oldInfo.apply(console,arguments);
}
console.log=...
...
}
這里來了一個新需求,希望在開發(fā)狀態(tài)打印方法參數(shù)值,方法執(zhí)行順序,發(fā)布狀態(tài)不打印并清理無用輸出。
這里需要一個全局?jǐn)r截器,重構(gòu),造輪子已然無望。
源碼-->編譯-->可執(zhí)行文件
在編譯時向源碼中注入一段代碼實現(xiàn)攔截器,或者刪除無用代碼。
以下面這段代碼為例說一下需要做什么
function test(id, name) {
console.log('This is a demo ' + id + ' ' + name);
return;
}
function test111(id, name) {
console.log('This is a demo ' + id + ' ' + name);
return;
}
function doTest() {
test111(4, 5);
test(6, 7);
test111(4, 5);
}
doTest();
在每一個方法體內(nèi)部增加日志打印語句打印方法名稱,方法參數(shù),方法調(diào)用編號(累加,標(biāo)示方法運行順序);
function test(id, name) {
//[TODO 這里需要增加輸出代碼]
console.log('This is a demo ' + id + ' ' + name);
return;
}
function test111(id, name) {
//[TODO 這里需要增加輸出代碼]
console.log('This is a demo ' + id + ' ' + name);
return;
}
function doTest() {
//[TODO 這里需要增加輸出代碼]
test111(4, 5);
test(6, 7);
test111(4, 5);
}
doTest();
找到輸出語句刪除;
function test(id, name) {
console.log('This is a demo ' + id + ' ' + name);// 刪除
return;
}
function test111(id, name) {
console.log('This is a demo ' + id + ' ' + name);// 刪除
return;
}
function doTest() {
test111(4, 5);
test(6, 7);
test111(4, 5);
}
doTest();
創(chuàng)建一個項目來實現(xiàn)這個需求
image.png
libs 資源庫
dev 開發(fā)模式編譯輸出
dist 發(fā)布模式編譯輸出
src 源碼目錄
build.js 編譯主調(diào)度文件
global.js 全局資源定義文件
對于語法遍歷總是需要一定的規(guī)則的,如果真的有人和你說無腦遍歷文件來個正則過濾這樣的的方案請對他好一些。
需要理解一個概念抽象語法樹
使用UglifyJS來對代碼進(jìn)行轉(zhuǎn)換與遍歷。部分如下:
global.js文件
global.U2=require('uglify-js');
global.gulp=require('gulp');
global.through=require('through2');
global.del=require('del');
global.util=require("./libs/util");
util.js
module.exports={
splice_string: function (str, begin, end, replacement) {
if (replacement.length > 0) {
return str.substr(0, begin) + replacement + str.substr(end);
} else {
return str.substr(0, begin) + replacement + str.substr(end + 1);
}
}
}
build_dev.js
//開發(fā)編譯策略
module.exports={
doBuild: function (code) {
console.log("[Dev] Building Code ");
return through.obj(function (file, enc, cb) {
let addLog_nodes=[];
//獲得文件內(nèi)容
let code=file
.contents
.toString();
//將源碼轉(zhuǎn)換成語法樹
let ast=U2.parse(code);
//遍歷樹
ast.walk(new U2.TreeWalker(function (node) {
if (node.body && node.name) {
//找到方法定義并放到等更改數(shù)組中
addLog_nodes.push(node);
}
}));
let runindexstr=`\n\tif (global.runIndex) { global.runIndex++; } else {global.runIndex=1; }\t`;
//遍歷待修改數(shù)組注入代碼
for (var j=addLog_nodes.length; --j >=0;) {
var conStr=`\n\tconsole.log('['+global.runIndex+']\t方法名稱:${addLog_nodes[j].name.print_to_string()} 參數(shù)列表:'+`;
var node=addLog_nodes[j];
var start_pos=node.start.pos;
var end_pos=node.end.endpos;
if (node.argnames.length > 0) {
for (var k=node.argnames.length; --k >=0;) {
conStr +=`'${node.argnames[k].print_to_string()}='+${node.argnames[k].print_to_string()}+','+`;
}
} else {
conStr +=`'無',`;
}
conStr=runindexstr + conStr.substring(0, conStr.length - 1) + ");\n\t";
code=util.splice_string(code, start_pos + (node.print_to_string().indexOf("{")) + 3, start_pos + (node.print_to_string().indexOf("{")) + 3, conStr);
}
file.contents=new Buffer(code);
//寫入轉(zhuǎn)換后文件
this.push(file);
cb();
});
}
}
build_dist.js
//發(fā)布編譯策略
module.exports={
doBuild: function () {
console.log("[Dist] Building Code ");
return through.obj(function (file, enc, cb) {
let console_nodes=[];
//讀取文件內(nèi)容
let code=file
.contents
.toString();
//將代碼轉(zhuǎn)換成語法樹
let ast=U2.parse(code);
//語法樹遍歷
ast.walk(new U2.TreeWalker(function (node) {
//找到console語句放到待處理數(shù)組中
if (node &&node.expression) {
if (node.expression.print_to_string().indexOf('console') > -1) {
console_nodes.push(node);
}
}
}));
//遍歷待處理數(shù)組進(jìn)行代碼刪除
for (var i=console_nodes.length; --i >=0;) {
var node=console_nodes[i];
var start_pos=node.start.pos;
var end_pos=node.end.endpos;
var replacement="";
// var replacement="console.log('add code');\n\t" + node.print_to_string();
code=util.splice_string(code, start_pos, end_pos, replacement);
}
//壓縮代碼
code=U2
.minify(code)
.code;
file.contents=new Buffer(code);
//將文件裝入實體
this.push(file);
cb();
});
}
}
build.js
require("./global");
const build_dev=require("./libs/build_dev");
const build_dist=require("./libs/build_dist");
var buildType="dev";
if (process.argv.length < 3) {
buildType="dev";
} else {
buildType=process.argv[2];
}
/**
* 清理輸出任務(wù)
*/
gulp
.task("clean:dist", function (cb) {
if (buildType=="dev") {
del([process.cwd() + "/dev/*"], cb);
} else {
del([process.cwd() + "/dist/*"], cb);
}
cb();
});
/**
* 代碼更改任務(wù)
*/
gulp.task("modifyCode", function (cb) {
if (buildType=="dev") {
gulp
.src('./src/*.js')
.pipe(build_dev.doBuild())
.pipe(gulp.dest(process.cwd() + "/dev"));
} else {
gulp
.src('./src/*.js')
.pipe(build_dist.doBuild())
.pipe(gulp.dest(process.cwd() + "/dist"));
}
cb();
});
/**
* 入口任務(wù)
*/
gulp.start("modifyCode", ["clean:dist"], function (error, msg) {
console.info("successfull");
if (error) {
console.info(error);
}
});
關(guān)于攔截器部分實現(xiàn),只是實現(xiàn)了一個通用的攔截器,考慮升級實現(xiàn)類似spring 注解形式的定向注入模式,后續(xù)會發(fā)布相關(guān)解決方案。
端用JavaScript實現(xiàn)桑基圖(Sankey圖)
桑基圖(Sankey圖),是流圖的一種,常用來展示事物的數(shù)量、發(fā)展方向、數(shù)據(jù)量大小等,在可視化分析中經(jīng)常使用。
本文,演示如何在前端用JavaScript繪制桑基圖。注:本例使用JShaman數(shù)據(jù)展示JS代碼混淆加密流程。
先看效果:
因為已有成熟的庫可用,比如,可以使用d3引擎,所以sankey的實現(xiàn)較為簡單。
眾所周知,JShaman是國內(nèi)知名的JS代碼混淆加密平臺,我們將用JShaman英文版的混淆返回內(nèi)容做為數(shù)據(jù)源,繪制一張JS代碼混淆加密流程桑基圖。
JShaman數(shù)據(jù)采集,直接復(fù)制即可:
用d3實現(xiàn)桑基圖繪制,核心代碼如下,文末會提供完整代碼。
繪圖成功:
桑基圖效果說明:從圖中,可以看到JShaman對JS代碼的混淆加密流程:初始的JS代碼,先轉(zhuǎn)為AST(抽象語法樹),再進(jìn)行String reverse、Dead Code Insertion、Eval Encryption等數(shù)十種混淆加密操作,生成了新的AST,最后再根據(jù)AST重新生成JS代碼,這便是JS代碼混淆加密的完整流程,由圖可以讓人一目了然的知曉全過程。
最后,附上完整代碼,如果您也需要繪制桑基圖,可以參考此代碼:
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。