AST 與前端工程化實(shí)戰(zhàn)

AST : 全稱為 Abstract Syntax Tree,意為抽象語(yǔ)法樹(shù),它是源代碼語(yǔ)法結(jié)構(gòu)的一種抽象表示。

AST 是一個(gè)非?;A(chǔ)但是同時(shí)非常重要的知識(shí)點(diǎn),我們熟知的 TypeScript、babel、webpack、vue-cli 得都是依賴 AST 進(jìn)行開(kāi)發(fā)的。本文將通過(guò) AST 與前端工程化的實(shí)戰(zhàn)向大家展示 AST 的強(qiáng)大以及重要性。

直播分享視頻地址:AST 與前端工程化實(shí)戰(zhàn)

一、初識(shí) AST

1、demo-1

第一次看見(jiàn) AST 這個(gè)概念的時(shí)候還是在《你不知道的 JavaScript》一書(shū)中看到的。我們先看個(gè)例子

const a = 1
復(fù)制代碼

傳統(tǒng)編譯語(yǔ)言中,源代碼執(zhí)行會(huì)先經(jīng)歷三個(gè)階段

  • 詞法分析階段:將字符組成的字符串分解成一個(gè)個(gè)代碼塊(詞法單元),例子中代碼會(huì)被解析成 const、a、=、1 四個(gè)詞法單元。

  • 語(yǔ)法分析階段:將詞法單元流轉(zhuǎn)換成一個(gè)由元素逐級(jí)嵌套組成的語(yǔ)法結(jié)構(gòu)樹(shù),即所謂的抽象語(yǔ)法樹(shù)。例子中被解析出來(lái)的 const、a、=、1 這四個(gè)詞法單元組成的詞法單元流則會(huì)被轉(zhuǎn)換成如下結(jié)構(gòu)樹(shù)

[圖片上傳中...(image-143ec0-1567048431087-7)]

<figcaption></figcaption>

  • 代碼生成階段:將 AST 轉(zhuǎn)換成一系列可執(zhí)行的機(jī)器指令代碼,對(duì)應(yīng)例子的話就是機(jī)器通過(guò)執(zhí)行指令會(huì)在內(nèi)存中創(chuàng)建一個(gè)變量 a,并將值 1 賦值給它。

2、demo-2

我們?cè)賮?lái)拆解一個(gè) recast 官方的例子,相對(duì)來(lái)說(shuō)也會(huì)復(fù)雜一些

function add (a, b) {
  return a + b
}
復(fù)制代碼
  • 首先,進(jìn)入到詞法分析階段,我們會(huì)拿到 function、add、(、a、,、b、)、{、return、a、+、b、} 13 個(gè)代碼塊
  • 然后進(jìn)入語(yǔ)法分析階段,具體如圖所示

[圖片上傳中...(image-560773-1567048431086-6)]

<figcaption></figcaption>

上圖中的 FunctionDeclarationIdentifier、BlockStatement 等這些代碼塊的類型的說(shuō)明請(qǐng)點(diǎn)擊鏈接自行查看:AST 對(duì)象文檔

二、recast

由于文章中用到的 AST 相關(guān)的依賴包是 recast ,加上它本身是木有文檔的,只有一個(gè)非常簡(jiǎn)短的 README.md 文件,所以這里單獨(dú)開(kāi)一篇對(duì)其常見(jiàn)的一些 API 做個(gè)介紹。開(kāi)始之前,先給大家推薦一個(gè)在線查看 AST 結(jié)構(gòu)的平臺(tái),非常好用

相信對(duì) babel 稍有了解的同學(xué)都知道,babel 有一系列包對(duì) AST 進(jìn)行了封裝,專門來(lái)處理編譯這塊的事宜。而 recast 也是基于 @babel/core、@babel/parser 、@babel/types等包進(jìn)行封裝開(kāi)發(fā)的。

引入

引入 recast 有兩種方法,一種是 import 的形式,一種則是 CommonJs 的形式,分別如下

  • import 形式
import { parse, print } from 'recast'
console.log(print(parse(source)).code)

import * as recast from 'recast'
console.log(recast.print(recast.parse(source)).code)
復(fù)制代碼
  • CommonJs 形式
const { parse, print } = require('recast')
console.log(print(parse(source)).code)

const recast = require('recast')
console.log(recast.print(recast.parse(source)).code)
復(fù)制代碼

引入了 recast 之后,我們一起來(lái)看看 recast 都能做些什么吧

1、recast.parse

我們回到我們例子,我們直接對(duì)它進(jìn)行 parse ,看看 parse 后的 AST 結(jié)構(gòu)是如何的

// parse.js
const recast = require('recast')

const code = `function add (a, b) {
  return a + b
}`

const ast = recast.parse(code)
// 獲取代碼塊 ast 的第一個(gè) body,即我們的 add 函數(shù)
const add = ast.program.body[0]
console.log(add)
復(fù)制代碼

執(zhí)行 node parse.js 即可在我們的終端查看到 add 函數(shù)的結(jié)構(gòu)了

FunctionDeclaration {
  type: 'FunctionDeclaration',
  id: Identifier...,
  params: [Identifier...],
  body: BlockStatement...
}
復(fù)制代碼

當(dāng)然你想看更多內(nèi)容直接去 AST Explorer 平臺(tái) 將模式調(diào)成 recast 模式即可看到 ast 的全覽了,和我們上面分析的內(nèi)容基本是一致的。

[圖片上傳中...(image-68f72c-1567048431086-5)]

<figcaption></figcaption>

2、recast.print

目前為止,我們只是對(duì)其進(jìn)行了拆解,如果將 ast 組裝成我們能執(zhí)行的代碼呢?OK,這就需要用到 recast.print 了,我們對(duì)上面拆解好的代碼原封不動(dòng)的組裝起來(lái)

// print.js
const recast = require('recast')

const code = `function add (a, b) {
  return a + b
}`

const ast = recast.parse(code)

console.log(recast.print(ast).code)
復(fù)制代碼

然后執(zhí)行 node print.js ,可以看到,我們打印出了

function add (a, b) {
  return a + b
}
復(fù)制代碼

官方給的解釋就是,這就只是一個(gè)逆向處理而已,即

recast.print(recast.parse(source)).code === source
復(fù)制代碼

3、recast.prettyPrint

除了我們上面提及的 recast.print 外,recast 還提供一個(gè)代碼美化的 API 叫 recast.prettyPrint

// prettyPrint.js
const recast = require('recast')

const code = `function add (a, b) {
  return a +                b
}`

const ast = recast.parse(code)

console.log(recast.prettyPrint(ast, { tabWidth: 2 }).code)
復(fù)制代碼

執(zhí)行 node prettyPrint.js ,會(huì)發(fā)現(xiàn) code 里面的 N 多空格都能被格式化掉,輸出如下

function add(a, b) {
  return a + b;
}
復(fù)制代碼

詳細(xì)的配置請(qǐng)自行查看:prettyPrint

4、recast.types.builders

i. API

關(guān)于 builder 的 API ,別擔(dān)心,我肯定是不會(huì)講的,因?yàn)樘嗔恕?/p>

[圖片上傳中...(image-e34c1f-1567048431086-4)]

<figcaption></figcaption>

想要具體了解每一個(gè) API 能做什么的,可以直接在 Parser API - Builders 中進(jìn)行查看,或者直接查看 recast builders 定義

ii. 實(shí)戰(zhàn)階段

OK,終于進(jìn)入到 recast 操作相關(guān)的核心了。我們要想改造我們的代碼,那么 recast.types.builders 則是我們最重要的工具了。這里我們繼續(xù)通過(guò)改造 recast 官方案例來(lái)了解 recast.types.builders 構(gòu)建工具。

搞個(gè)最簡(jiǎn)單的例子,現(xiàn)在我們要做一件事,那就是將 function add (a, b) {...} 改成 const add = function (a, b) {...}

我們從第一章節(jié)了解到,如果我們需要將其做成 const 聲明式的話,需要先一個(gè) VariableDeclaration 以及一個(gè) VariableDeclarator,然后我們聲明一個(gè) function 則有需要?jiǎng)?chuàng)建一個(gè) FunctionDeclaration,剩下的則是填充表達(dá)式的參數(shù)和內(nèi)容體了。具體操作如下

// builder1.js
const recast = require('recast')
const {
  variableDeclaration,
  variableDeclarator,
  functionExpression
} = recast.types.builders

const code = `function add (a, b) {
  return a + b
}`

const ast = recast.parse(code)
const add = ast.program.body[0]

ast.program.body[0] = variableDeclaration('const', [
  variableDeclarator(add.id, functionExpression(
    null, // 這里弄成匿名函數(shù)即可
    add.params,
    add.body
  ))
])

const output = recast.print(ast).code

console.log(output)
復(fù)制代碼

執(zhí)行 node builder1.js ,輸出如下

const add = function(a, b) {
  return a + b
};
復(fù)制代碼

看到這,是不是覺(jué)得很有趣。真正好玩的才剛開(kāi)始呢,接下來(lái),基于此例子,我們做個(gè)小的延伸。將其直接改成 const add = (a, b) => {...} 的格式。

這里出現(xiàn)了一個(gè)新的概念,那就是箭頭函數(shù),當(dāng)然,recast.type.builders 提供了 arrowFunctionExpression 來(lái)允許我們創(chuàng)建一個(gè)箭頭函數(shù)。所以我們第一步先來(lái)創(chuàng)建一個(gè)箭頭函數(shù)

const arrow = arrowFunctionExpression([], blockStatement([])
復(fù)制代碼

打印下 console.log(recast.print(arrow)),輸出如下

() => {}
復(fù)制代碼

OK,我們已經(jīng)獲取到一個(gè)空的箭頭函數(shù)了。接下來(lái)我們需要基于上面改造的基礎(chǔ)進(jìn)一步進(jìn)行改造,其實(shí)只要將 functionExpression 替換成 arrowFunctionExpression 即可。

ast.program.body[0] = variableDeclaration('const', [
  variableDeclarator(add.id, b.arrowFunctionExpression(
    add.params,
    add.body
  ))
])
復(fù)制代碼

打印結(jié)果如下

const add = (a, b) => {
  return a + b
};
復(fù)制代碼

OK,到這里,我們已經(jīng)知道 recast.types.builders 能為我們提供一系列 API,讓我們可以瘋狂輸出。

5、recast.run

讀取文件命令行。首先,我新建一個(gè) read.js ,內(nèi)容如下

// read.js
recast.run((ast, printSource) => {
  printSource(ast)
})
復(fù)制代碼

然后我再新建一個(gè) demo.js,內(nèi)容如下

// demo.js
function add (a, b) {
  return a + b
}
復(fù)制代碼

然后執(zhí)行 node read demo.js,輸出如下

function add (a, b) {
  return a + b
}
復(fù)制代碼

我們能看出來(lái),我們直接在 read.js 中讀出了 demo.js 里面的代碼內(nèi)容。那么具體是如何實(shí)現(xiàn)的呢?

其實(shí),原理非常簡(jiǎn)單,無(wú)非就是直接通過(guò) fs.readFile 進(jìn)行文件讀取,然后將獲取到的 code 進(jìn)行 parse 操作,至于我們看到的 printSource 則提供一個(gè)默認(rèn)的打印函數(shù) process.stdout.write(output),具體代碼如下

import fs from "fs";

export function run(transformer: Transformer, options?: RunOptions) {
  return runFile(process.argv[2], transformer, options);
}

function runFile(path: any, transformer: Transformer, options?: RunOptions) {
  fs.readFile(path, "utf-8", function(err, code) {
    if (err) {
      console.error(err);
      return;
    }

    runString(code, transformer, options);
  });
}

function defaultWriteback(output: string) {
  process.stdout.write(output);
}

function runString(code: string, transformer: Transformer, options?: RunOptions) {
  const writeback = options && options.writeback || defaultWriteback;
  transformer(parse(code, options), function(node: any) {
    writeback(print(node, options).code);
  });
}
復(fù)制代碼

6、recast.visit

這是一個(gè) AST 節(jié)點(diǎn)遍歷的 API,如果你想要遍歷 AST 中的一些類型,那么你就得靠 recast.visit 了,這里可以遍歷的類型與 recast.types.builders 中的能構(gòu)造出來(lái)的類型一致,builders 做的事是類型構(gòu)建,recast.visit 做事的事則是遍歷 AST 中的類型。不過(guò)使用的時(shí)候需要注意以下幾點(diǎn)

  • 每個(gè) visit,必須加上 return false,或者 this.traverse(path),否則報(bào)錯(cuò)。
if (this.needToCallTraverse !== false) {
  throw new Error(
    "Must either call this.traverse or return false in " + methodName
  );
}
復(fù)制代碼
  • 在需要遍歷的類型前面加上 visit 即可遍歷,如需要遍歷 AST 中的箭頭函數(shù),那么直接這么寫即可
recast.run((ast, printSource) => {
  recast.visit(ast, {
    visitArrowFunctionExpression (path) {
      printSource(path.node)
      return false
    }
  })
})
復(fù)制代碼

7、recast.types.namedTypes

一個(gè)用來(lái)判斷 AST 對(duì)象是否為指定類型的 API。

namedTypes 下有兩個(gè) API,一個(gè)是 namedTypes.Node.assert:當(dāng)類型不配置的時(shí)候,直接報(bào)錯(cuò)退出。另外一個(gè)則是 namedTypes.Node.check:判斷類型是否一致,并輸出 true 或 false。

其中 Node 為任意 AST 對(duì)象,比如我相對(duì)箭頭函數(shù)做一個(gè)函數(shù)類型判定,代碼如下

// namedTypes1.js
const recast = require('recast')
const t = recast.types.namedTypes

const arrowNoop = () => {}

const ast = recast.parse(arrowNoop)

recast.visit(ast, {
  visitArrowFunctionExpression ({ node }) {
    console.log(t.ArrowFunctionExpression.check(node))
    return false
  }
})
復(fù)制代碼

執(zhí)行 node namedTypes1.js,能看出打印臺(tái)輸出結(jié)果為 true。

同理,assert 用法也差不多。

const recast = require('recast')
const t = recast.types.namedTypes

const arrowNoop = () => {}

const ast = recast.parse(arrowNoop)

recast.visit(ast, {
  visitArrowFunctionExpression ({ node }) {
    t.ArrowFunctionExpression.assert(node)
    return false
  }
})
復(fù)制代碼

你想判斷更多的 AST 對(duì)象類型的,直接做替換 Node 為其它 AST 對(duì)象類型即可。

三、前端工程化

現(xiàn)在,咱來(lái)聊聊前端工程化。

前段工程化可以分成四個(gè)塊來(lái)說(shuō),分別為

  • 模塊化:將一個(gè)文件拆分成多個(gè)相互依賴的文件,最后進(jìn)行統(tǒng)一的打包和加載,這樣能夠很好的保證高效的多人協(xié)作。其中包含

    1. JS 模塊化:CommonJS、AMD、CMD 以及 ES6 Module。
    2. CSS 模塊化:Sass、Less、Stylus、BEM、CSS Modules 等。其中預(yù)處理器和 BEM 都會(huì)有的一個(gè)問(wèn)題就是樣式覆蓋。而 CSS Modules 則是通過(guò) JS 來(lái)管理依賴,最大化的結(jié)合了 JS 模塊化和 CSS 生態(tài),比如 Vue 中的 style scoped。
    3. 資源模塊化:任何資源都能以模塊的形式進(jìn)行加載,目前大部分項(xiàng)目中的文件、CSS、圖片等都能直接通過(guò) JS 做統(tǒng)一的依賴關(guān)系處理。
  • 組件化:不同于模塊化,模塊化是對(duì)文件、對(duì)代碼和資源拆分,而組件化則是對(duì) UI 層面的拆分。

    1. 通常,我們會(huì)需要對(duì)頁(yè)面進(jìn)行拆分,將其拆分成一個(gè)一個(gè)的零件,然后分別去實(shí)現(xiàn)這一個(gè)個(gè)零件,最后再進(jìn)行組裝。
    2. 在我們的實(shí)際業(yè)務(wù)開(kāi)發(fā)中,對(duì)于組件的拆分我們需要做不同程度的考量,其中主要包括細(xì)粒度和通用性這兩塊的考慮。
    3. 對(duì)于業(yè)務(wù)組件,你更多需要考量的是針對(duì)你負(fù)責(zé)業(yè)務(wù)線的一個(gè)適用度,即你設(shè)計(jì)的業(yè)務(wù)組件是否成為你當(dāng)前業(yè)務(wù)的 “通用” 組件,比如我之前分析過(guò)的權(quán)限校驗(yàn)組件,它就是一個(gè)典型的業(yè)務(wù)組件。感興趣的小伙伴可以點(diǎn)擊 傳送門 自行閱讀。
  • 規(guī)范化:正所謂無(wú)規(guī)矩不成方圓,一些好的規(guī)范則能很好的幫助我們對(duì)項(xiàng)目進(jìn)行良好的開(kāi)發(fā)管理。規(guī)范化指的是我們?cè)诠こ涕_(kāi)發(fā)初期以及開(kāi)發(fā)期間制定的系列規(guī)范,其中又包含了

    1. 項(xiàng)目目錄結(jié)構(gòu)
    2. 編碼規(guī)范:對(duì)于編碼這塊的約束,一般我們都會(huì)采用一些強(qiáng)制措施,比如 ESLint、StyleLint 等。
    3. 聯(lián)調(diào)規(guī)范:這塊可參考我以前知乎的回答,前后端分離,后臺(tái)返回的數(shù)據(jù)前端沒(méi)法寫,怎么辦?
    4. 文件命名規(guī)范
    5. 樣式管理規(guī)范:目前流行的樣式管理有 BEM、Sass、Less、Stylus、CSS Modules 等方式。
    6. git flow 工作流:其中包含分支命名規(guī)范、代碼合并規(guī)范等。
    7. 定期 code review
    8. … 等等

    以上這些,我之前也寫過(guò)一篇文章做過(guò)一些點(diǎn)的詳細(xì)說(shuō)明,TypeScript + 大型項(xiàng)目實(shí)戰(zhàn)

  • 自動(dòng)化:從最早先的 grunt、gulp 等,再到目前的 webpack、parcel。這些自動(dòng)化工具在自動(dòng)化合并、構(gòu)建、打包都能為我們節(jié)省很多工作。而這些前端自動(dòng)化其中的一部分,前端自動(dòng)化還包含了持續(xù)集成、自動(dòng)化測(cè)試等方方面面。

而,處于其中任何一個(gè)塊都屬于前端工程化。

四、實(shí)戰(zhàn):AST & webpack loader

而本文提及的實(shí)戰(zhàn),則是通過(guò) AST 改造書(shū)寫一個(gè)屬于我們自己的 webpack loader,為我們項(xiàng)目中的 promise 自動(dòng)注入 catch 操作,避免讓我們手動(dòng)書(shū)寫那些通用的 catch 操作。

1、AST 改造

講了這么多,終于進(jìn)入到我們的實(shí)戰(zhàn)環(huán)節(jié)了。那么我們實(shí)戰(zhàn)要做一個(gè)啥玩意呢?

場(chǎng)景:日常的中臺(tái)項(xiàng)目中,經(jīng)常會(huì)有一些表單提交的需求,那么提交的時(shí)候就需要做一些限制,防止有人手抖多點(diǎn)了幾次導(dǎo)致請(qǐng)求重復(fù)發(fā)出去。此類場(chǎng)景有很多解決方案,但是個(gè)人認(rèn)為最佳的交互就是點(diǎn)擊之后為提交按鈕加上 loading 狀態(tài),然后將其 disabled 掉,請(qǐng)求成功之后再解除掉 loading 和 disabled 的狀態(tài)。具體提交的操作如下

this.axiosFetch(this.formData).then(res => {
  this.loading = false
  this.handleClose()
}).catch(() => {
  this.loading = false
})
復(fù)制代碼

這樣看著好像還算 OK,但是如果類似這樣的操作一多,或多或少會(huì)讓你項(xiàng)目整體的代碼看起來(lái)有些重復(fù)冗余,那么如何解決這種情況呢?

很簡(jiǎn)單,咱直接使用 AST 編寫一個(gè) webpack loader,讓其自動(dòng)完成一些代碼的注入,若我們項(xiàng)目中存在下面的代碼的時(shí)候,會(huì)自動(dòng)加上 catch 部分的處理,并將 then 語(yǔ)句第一段處理主動(dòng)作為 catch 的處理邏輯

this.axiosFetch(this.formData).then(res => {
  this.loading = false
  this.handleClose()
})
復(fù)制代碼

我們先看看,沒(méi)有 catch 的這段代碼它的 AST 結(jié)構(gòu)是怎樣的,如圖

[圖片上傳中...(image-afa359-1567048431085-3)]

<figcaption></figcaption>

其 MemberExpression 為

this.axiosFetch(this.formData).then
復(fù)制代碼

arguments 為

res => {
  this.loading = false
  this.handleClose()
}
復(fù)制代碼

OK,我們?cè)賮?lái)看看有 catch 處理的代碼它的 AST 結(jié)構(gòu)又是如何的,如圖

[圖片上傳中...(image-8b6938-1567048431085-2)]

<figcaption></figcaption>

其 MemberExpression 為

this.axiosFetch(this.formData).then(res => {
  this.loading = false
  this.handleClose()
}).catch
復(fù)制代碼

其中有兩個(gè) ArrowFunctionExpression,分別為

// ArrowFunctionExpression 1
res => {
  this.loading = false
  this.handleClose()
}
// ArrowFunctionExpression 2
() => {
  this.loading = false
}
復(fù)制代碼

所以,我們需要做的事情大致分為以下幾步

  1. 對(duì) ArrowFunctionExpression 類型進(jìn)行遍歷,獲得其 BlockStatement 中的第一個(gè) ExpressionStatement,并保存為 firstExp
  2. 使用 builders 新建一個(gè)空的箭頭函數(shù),并將保存好的 firstExp 賦值到該空箭頭函數(shù)的 BlockStatement 中
  3. 對(duì) CallExpression 類型進(jìn)行遍歷,將 AST 的 MemberExpression 修改成為有 catch 片段的格式
  4. 將改造完成的 AST 返回

現(xiàn)在,按照我們的思路,我們一步一步來(lái)做 AST 改造

首先,我們需要獲取到已有箭頭函數(shù)中的第一個(gè) ExpressionStatement,獲取的時(shí)候我們需要保證當(dāng)前 ArrowFunctionExpression 類型的 parent 節(jié)點(diǎn)是一個(gè) CallExpression 類型,并且保證其 property 為 promise 的then 函數(shù),具體操作如下

// promise-catch.js
const recast = require('recast')
const {
  identifier: id,
  memberExpression,
  callExpression,
  blockStatement,
  arrowFunctionExpression
} = recast.types.builders
const t = recast.types.namedTypes

const code = `this.axiosFetch(this.formData).then(res => {
  this.loading = false
  this.handleClose()
})`
const ast = recast.parse(code)
let firstExp

recast.visit(ast, {
  visitArrowFunctionExpression ({ node, parentPath }) {
    const parentNode = parentPath.node
    if (
      t.CallExpression.check(parentNode) &&
      t.Identifier.check(parentNode.callee.property) &&
      parentNode.callee.property.name === 'then'
    ) {
      firstExp = node.body.body[0]
    }
    return false
  }
})
復(fù)制代碼

緊接著,我們需要?jiǎng)?chuàng)建一個(gè)空的箭頭函數(shù),并將 firstExp 賦值給它

const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
復(fù)制代碼

隨后,我們則需要對(duì) CallExpression 類型的 AST 對(duì)象進(jìn)行遍歷,并做最后的 MemberExpression 改造工作

recast.visit(ast, {
  visitCallExpression (path) {
    const { node } = path

    const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
    const originFunc = callExpression(node.callee, node.arguments)
    const catchFunc = callExpression(id('catch'), [arrowFunc])
    const newFunc = memberExpression(originFunc, catchFunc)

    return false
  }
})
復(fù)制代碼

最后我們?cè)?CallExpression 遍歷的時(shí)候?qū)⑵涮鎿Q

path.replace(newFunc)
復(fù)制代碼

初版的全部代碼如下

// promise-catch.js
const recast = require('recast')
const {
  identifier: id,
  memberExpression,
  callExpression,
  blockStatement,
  arrowFunctionExpression
} = recast.types.builders
const t = recast.types.namedTypes

const code = `this.axiosFetch(this.formData).then(res => {
  this.loading = false
  this.handleClose()
})`
const ast = recast.parse(code)
let firstExp

recast.visit(ast, {
  visitArrowFunctionExpression ({ node, parentPath }) {
    const parentNode = parentPath.node
    if (
      t.CallExpression.check(parentNode) &&
      t.Identifier.check(parentNode.callee.property) &&
      parentNode.callee.property.name === 'then'
    ) {
      firstExp = node.body.body[0]
    }
    return false
  }
})

recast.visit(ast, {
  visitCallExpression (path) {
    const { node } = path

    const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
    const originFunc = callExpression(node.callee, node.arguments)
    const catchFunc = callExpression(id('catch'), [arrowFunc])
    const newFunc = memberExpression(originFunc, catchFunc)

    path.replace(newFunc)

    return false
  }
})

const output = recast.print(ast).code
console.log(output)
復(fù)制代碼

執(zhí)行 node promise-catch.js ,打印臺(tái)輸出結(jié)果

this.axiosFetch(this.formData).then(res => {
  this.loading = false
  this.handleClose()
}).catch(() => {
  this.loading = false
})
復(fù)制代碼

所以能看出來(lái),我們已經(jīng)是完成了我們想要完成的樣子了

[圖片上傳中...(image-be2925-1567048431085-1)]

<figcaption></figcaption>

  1. 但是我們還得對(duì)一些情況做處理,第一件就是需要在 CallExpression 遍歷的時(shí)候保證其 arguments 為箭頭函數(shù)。

  2. 緊接著,我們需要判定我們獲取到的 firstExp 是否存在,因?yàn)槲覀兊?then 處理中可以是一個(gè)空的箭頭函數(shù)。

  3. 然后防止 promise 擁有一些自定義的 catch 操作,則需要保證其 property 為 then。

  4. 最后為了防止多個(gè) CallExpression 都需要做自動(dòng)注入的情況,然后其操作又不同,則需要在其內(nèi)部進(jìn)行 ArrowFunctionExpression 遍歷操作

經(jīng)過(guò)這些常見(jiàn)情況的兼容后,具體代碼如下

recast.visit(ast, {
  visitCallExpression (path) {
    const { node } = path
    const arguments = node.arguments

    let firstExp

    arguments.forEach(item => {
      if (t.ArrowFunctionExpression.check(item)) {
        firstExp = item.body.body[0]

        if (
          t.ExpressionStatement.check(firstExp) &&
          t.Identifier.check(node.callee.property) &&
          node.callee.property.name === 'then'
        ) {
          const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
          const originFunc = callExpression(node.callee, node.arguments)
          const catchFunc = callExpression(id('catch'), [arrowFunc])
          const newFunc = memberExpression(originFunc, catchFunc)

          path.replace(newFunc)
        }
      }
    })

    return false
  }
})
復(fù)制代碼

然后由于之后需要做成一個(gè) webpack-loader,用在我們的實(shí)際項(xiàng)目中。所以我們需要對(duì) parse 的解析器做個(gè)替換,其默認(rèn)的解析器為 recast/parsers/esprima,而一般我們項(xiàng)目中都會(huì)用到 babel-loader ,所以我們這也需要將其解析器改為 recast/parsers/babel

const ast = recast.parse(code, {
  parser: require('recast/parsers/babel')
})
復(fù)制代碼

2、webpack loader

到這里,我們對(duì)于代碼的 AST 改造已經(jīng)是完成了,但是如何將其運(yùn)用到我們的實(shí)際項(xiàng)目中呢?

OK,這個(gè)時(shí)候我們就需要自己寫一個(gè) webpack loader 了。

其實(shí),關(guān)于如何開(kāi)發(fā)一個(gè) webpack loader,webpack 官方文檔 已經(jīng)講的很清楚了,下面我為小伙伴們做個(gè)小總結(jié)。

i. 本地進(jìn)行 loader 開(kāi)發(fā)

首先,你需要本地新建你開(kāi)發(fā) loader 的文件,比如,我們這將其丟到 src/index.js 下,webpack.config.js 配置則如下

const path = require('path')

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          // ... 其他你需要的 loader
          { loader: path.resolve(__dirname, 'src/index.js') }
        ]
      }
    ]
  }
}
復(fù)制代碼

src/index.js 內(nèi)容如下

const recast = require('recast')
const {
  identifier: id,
  memberExpression,
  callExpression,
  blockStatement,
  arrowFunctionExpression
} = recast.types.builders
const t = recast.types.namedTypes

module.exports = function (source) {
  const ast = recast.parse(source, {
    parser: require('recast/parsers/babel')
  })

  recast.visit(ast, {
    visitCallExpression (path) {
      const { node } = path
      const arguments = node.arguments

      let firstExp

      arguments.forEach(item => {
        if (t.ArrowFunctionExpression.check(item)) {
          firstExp = item.body.body[0]

          if (
            t.ExpressionStatement.check(firstExp) &&
            t.Identifier.check(node.callee.property) &&
            node.callee.property.name === 'then'
          ) {
            const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
            const originFunc = callExpression(node.callee, node.arguments)
            const catchFunc = callExpression(id('catch'), [arrowFunc])
            const newFunc = memberExpression(originFunc, catchFunc)

            path.replace(newFunc)
          }
        }
      })

      return false
    }
  })

  return recast.print(ast).code
}
復(fù)制代碼

然后,搞定收工。

ii. npm 發(fā)包

這里我在以前的文章中提及過(guò),這里不談了。如果還沒(méi)搞過(guò) npm 發(fā)包的小伙伴,可以點(diǎn)擊下面鏈接自行查看

揭秘組件庫(kù)一二事(發(fā)布 npm 包片段)

OK,到這一步,我的 promise-catch-loader 也是已經(jīng)開(kāi)發(fā)完畢。接下來(lái),只要在項(xiàng)目中使用即可

npm i promise-catch-loader -D
復(fù)制代碼

由于我的項(xiàng)目是基于 vue-cli3.x 構(gòu)建的,所以我需要在我的 vue.config.js 中這樣配置

// js 版本
module.exports = {
  // ...
  chainWebpack: config => {
    config.module
      .rule('js')
      .test(/\.js$/)
      .use('babel-loader').loader('babel-loader').end()
      .use('promise-catch-loader').loader('promise-catch-loader').end()
  }
}
// ts 版本
module.exports = {
  // ...
  chainWebpack: config => {
    config.module
      .rule('ts')
      .test(/\.ts$/)
      .use('cache-loader').loader('cache-loader').end()
      .use('babel-loader').loader('babel-loader').end()
      .use('ts-loader').loader('ts-loader').end()
      .use('promise-catch-loader').loader('promise-catch-loader').end()
  }
}
復(fù)制代碼

然后我項(xiàng)目里面擁有以下 promise 操作

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { Action } from 'vuex-class'

@Component
export default class HelloWorld extends Vue {
  loading: boolean = false
  city: string = '上海'

  @Action('getTodayWeather') getTodayWeather: Function

  getCityWeather (city: string) {
    this.loading = true
    this.getTodayWeather({ city: city }).then((res: Ajax.AjaxResponse) => {
      this.loading = false
      const { low, high, type } = res.data.forecast[0]
      this.$message.success(`${city}今日:${type} ${low} - ${high}`)
    })
  }
}
</script>
復(fù)制代碼

然后在瀏覽器中查看 source 能看到如下結(jié)果

[圖片上傳中...(image-1fb21f-1567048431084-0)]

<figcaption></figcaption>

關(guān)于代碼,我已經(jīng)托管到 GitHub 上了,promise-catch-loader

總結(jié)

到這步,我們的實(shí)戰(zhàn)環(huán)節(jié)也已經(jīng)是結(jié)束了。當(dāng)然,文章只是個(gè)初導(dǎo)篇,更多的類型還得小伙伴自己去探究。

AST 它的用處還非常的多,比如我們熟知的 Vue,它的 SFC(.vue) 文件的解析也是基于 AST 去進(jìn)行自動(dòng)解析的,即 vue-loader,它保證我們能正常的使用 Vue 進(jìn)行業(yè)務(wù)開(kāi)發(fā)。再比如我們常用的 webpack 構(gòu)建工具,也是基于 AST 為我們提供了合并、打包、構(gòu)建優(yōu)化等非常實(shí)用的功能的。

總之,掌握好 AST,你真的可以做很多事情。

最后,希望文章的內(nèi)容能夠幫助小伙伴了解到:什么是 AST?如何借助 AST 讓我們的工作更加效率?AST 又能為前端工程化做些什么?

作者:qiangdada
鏈接:https://juejin.im/post/5d50d1d9f265da03aa25607b
來(lái)源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 在開(kāi)發(fā)中,你是否會(huì)為了系統(tǒng)健壯性,亦或者是為了捕獲異步的錯(cuò)誤,而頻繁的在 async 函數(shù)中寫 try/ca...
    心_c2a2閱讀 3,546評(píng)論 1 19
  • 說(shuō)在前面:這些文章均是本人花費(fèi)大量精力研究整理,如有轉(zhuǎn)載請(qǐng)聯(lián)系作者并注明引用,謝謝本文的受眾人群不是webpack...
    RockSAMA閱讀 7,080評(píng)論 2 7
  • Webpack學(xué)習(xí)總結(jié) 此文只是自己學(xué)習(xí)webpack的一些總結(jié),方便自己查閱,閱讀不變,非常抱歉??! 下載安裝:...
    Lxs_597閱讀 1,085評(píng)論 0 0
  • 中國(guó)的孩子,被作文害得有多慘? 少內(nèi)容、沒(méi)方法,流水賬? 沒(méi)興趣、缺想象,假大空? 而且把原因歸到孩子身上?! 作...
    功成作文閱讀 521評(píng)論 4 8
  • 啟愛(ài)教育第2天育兒分享: 痛苦是孩子學(xué)習(xí)的阻力。痛苦痛苦,心里一痛,學(xué)習(xí)就苦,歸根結(jié)底,孩子們是因?yàn)樵趯W(xué)習(xí)過(guò)程中沒(méi)...
    啟愛(ài)教育工作室閱讀 170評(píng)論 0 0

友情鏈接更多精彩內(nèi)容