天天看點

[第5期] AST 與前端工程化實戰AST 與前端工程化實戰

AST : 全稱為 Abstract Syntax Tree,意為抽象文法樹,它是源代碼文法結構的一種抽象表示。

AST 是一個非常基礎但是同時非常重要的知識點,我們熟知的 TypeScript、babel、webpack、vue-cli 都是依賴 AST 進行開發的。本文将通過 AST 與前端工程化的實戰向大家展示 AST 的強大以及重要性。

直播分享視訊位址:AST 與前端工程化實戰

一、初識 AST

1、demo-1

第一次看見 AST 這個概念的時候還是在《你不知道的 JavaScript》一書中看到的。我們先看個例子

const a = 1
           

複制

傳統編譯語言中,源代碼執行會先經曆三個階段

  • 詞法分析階段:将字元組成的字元串分解成一個個代碼塊(詞法單元),例子中代碼會被解析成 const、a、=、1 四個詞法單元。
  • 文法分析階段:将詞法單元流轉換成一個由元素逐級嵌套組成的文法結構樹,即所謂的抽象文法樹。例子中被解析出來的 const、a、=、1 這四個詞法單元組成的詞法單元流則會被轉換成如下結構樹
[第5期] AST 與前端工程化實戰AST 與前端工程化實戰
  • 代碼生成階段:将 AST 轉換成一系列可執行的機器指令代碼,對應例子的話就是機器通過執行指令會在記憶體中建立一個變量 a,并将值 1 指派給它。

2、demo-2

我們再來拆解一個

recast

官方的例子,相對來說也會複雜一些

function add (a, b) {
  return a + b
}
           

複制

  • 首先,進入到詞法分析階段,我們會拿到

    function、add、(、a、,、b、)、{、return、a、+、b、}

    13 個代碼塊
  • 然後進入文法分析階段,具體如圖所示
[第5期] AST 與前端工程化實戰AST 與前端工程化實戰

上圖中的

FunctionDeclaration

Identifier

BlockStatement

等這些代碼塊的類型的說明請點選連結自行檢視:AST 對象文檔

二、recast

由于文章中用到的 AST 相關的依賴包是

recast

,加上它本身是木有文檔的,隻有一個非常簡短的

README.md

檔案,是以這裡單獨開一篇對其常見的一些 API 做個介紹。開始之前,先給大家推薦一個線上檢視 AST 結構的平台,非常好用

  • AST Explorer

相信對

babel

稍有了解的同學都知道,

babel

有一系列包對 AST 進行了封裝,專門來處理編譯這塊的事宜。而

recast

也是基于

@babel/core

@babel/parser

@babel/types

等包進行封裝開發的。

引入

引入

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)
           

複制

  • CommonJs

    形式
const { parse, print } = require('recast')
console.log(print(parse(source)).code)

const recast = require('recast')
console.log(recast.print(recast.parse(source)).code)
           

複制

引入了

recast

之後,我們一起來看看

recast

都能做些什麼吧

1、recast.parse

我們回到我們例子,我們直接對它進行 parse ,看看 parse 後的 AST 結構是如何的

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

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

const ast = recast.parse(code)
// 擷取代碼塊 ast 的第一個 body,即我們的 add 函數
const add = ast.program.body[0]
console.log(add)
           

複制

執行

node parse.js

即可在我們的終端檢視到 add 函數的結構了

FunctionDeclaration {
  type: 'FunctionDeclaration',
  id: Identifier...,
  params: [Identifier...],
  body: BlockStatement...
}
           

複制

當然你想看更多内容直接去 AST Explorer 平台 将模式調成

recast

模式即可看到 ast 的全覽了,和我們上面分析的内容基本是一緻的。

[第5期] AST 與前端工程化實戰AST 與前端工程化實戰

2、recast.print

目前為止,我們隻是對其進行了拆解,如果将 ast 組裝成我們能執行的代碼呢?OK,這就需要用到

recast.print

了,我們對上面拆解好的代碼原封不動的組裝起來

// 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)
           

複制

然後執行

node print.js

,可以看到,我們列印出了

function add (a, b) {
  return a + b
}
           

複制

官方給的解釋就是,這就隻是一個逆向處理而已,即

recast.print(recast.parse(source)).code === source
           

複制

3、recast.prettyPrint

除了我們上面提及的

recast.print

外,

recast

還提供一個代碼美化的 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)
           

複制

執行

node prettyPrint.js

,會發現 code 裡面的 N 多空格都能被格式化掉,輸出如下

function add(a, b) {
  return a + b;
}
           

複制

詳細的配置請自行檢視:prettyPrint

4、recast.types.builders

i. API

關于

builder

的 API ,别擔心,我肯定是不會講的,因為太多了。

[第5期] AST 與前端工程化實戰AST 與前端工程化實戰

想要具體了解每一個 API 能做什麼的,可以直接在 Parser API - Builders 中進行檢視,或者直接檢視 recast builders 定義

ii. 實戰階段

OK,終于進入到

recast

操作相關的核心了。我們要想改造我們的代碼,那麼

recast.types.builders

則是我們最重要的工具了。這裡我們繼續通過改造

recast

官方案例來了解

recast.types.builders

建構工具。

搞個最簡單的例子,現在我們要做一件事,那就是将

function add (a, b) {...}

改成

const add = function (a, b) {...}

我們從第一章節了解到,如果我們需要将其做成

const

聲明式的話,需要先一個

VariableDeclaration

以及一個

VariableDeclarator

,然後我們聲明一個

function

則有需要建立一個

FunctionDeclaration

,剩下的則是填充表達式的參數和内容體了。具體操作如下

// 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, // 這裡弄成匿名函數即可
    add.params,
    add.body
  ))
])

const output = recast.print(ast).code

console.log(output)
           

複制

執行

node builder1.js

,輸出如下

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

複制

看到這,是不是覺得很有趣。真正好玩的才剛開始呢,接下來,基于此例子,我們做個小的延伸。将其直接改成

const add = (a, b) => {...}

的格式。

這裡出現了一個新的概念,那就是箭頭函數,當然,

recast.type.builders

提供了

arrowFunctionExpression

來允許我們建立一個箭頭函數。是以我們第一步先來建立一個箭頭函數

const arrow = arrowFunctionExpression([], blockStatement([])
           

複制

列印下

console.log(recast.print(arrow))

,輸出如下

() => {}
           

複制

OK,我們已經擷取到一個空的箭頭函數了。接下來我們需要基于上面改造的基礎進一步進行改造,其實隻要将

functionExpression

替換成

arrowFunctionExpression

即可。

ast.program.body[0] = variableDeclaration('const', [
  variableDeclarator(add.id, b.arrowFunctionExpression(
    add.params,
    add.body
  ))
])
           

複制

列印結果如下

const add = (a, b) => {
  return a + b
};
           

複制

OK,到這裡,我們已經知道

recast.types.builders

能為我們提供一系列 API,讓我們可以瘋狂輸出。

5、recast.run

讀取檔案指令行。首先,我建立一個

read.js

,内容如下

// read.js
recast.run((ast, printSource) => {
  printSource(ast)
})
           

複制

然後我再建立一個

demo.js

,内容如下

// demo.js
function add (a, b) {
  return a + b
}
           

複制

然後執行

node read demo.js

,輸出如下

function add (a, b) {
  return a + b
}
           

複制

我們能看出來,我們直接在

read.js

中讀出了

demo.js

裡面的代碼内容。那麼具體是如何實作的呢?

其實,原理非常簡單,無非就是直接通過

fs.readFile

進行檔案讀取,然後将擷取到的

code

進行

parse

操作,至于我們看到的

printSource

則提供一個預設的列印函數

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);
  });
}
           

複制

6、recast.visit

這是一個 AST 節點周遊的 API,如果你想要周遊 AST 中的一些類型,那麼你就得靠

recast.visit

了,這裡可以周遊的類型與

recast.types.builders

中的能構造出來的類型一緻,

builders

做的事是類型建構,

recast.visit

做事的事則是周遊 AST 中的類型。不過使用的時候需要注意以下幾點

  • 每個 visit,必須加上

    return false

    ,或者

    this.traverse(path)

    ,否則報錯。
if (this.needToCallTraverse !== false) {
  throw new Error(
    "Must either call this.traverse or return false in " + methodName
  );
}
           

複制

  • 在需要周遊的類型前面加上 visit 即可周遊,如需要周遊 AST 中的箭頭函數,那麼直接這麼寫即可
recast.run((ast, printSource) => {
  recast.visit(ast, {
    visitArrowFunctionExpression (path) {
      printSource(path.node)
      return false
    }
  })
})
           

複制

7、recast.types.namedTypes

一個用來判斷 AST 對象是否為指定類型的 API。

namedTypes 下有兩個 API,一個是

namedTypes.Node.assert

:當類型不配置的時候,直接報錯退出。另外一個則是

namedTypes.Node.check

:判斷類型是否一緻,并輸出 true 或 false。

其中 Node 為任意 AST 對象,比如我想對箭頭函數做一個函數類型判定,代碼如下

// 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
  }
})
           

複制

執行

node namedTypes1.js

,能看出列印台輸出結果為 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
  }
})
           

複制

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

三、前端工程化

現在,咱來聊聊前端工程化。

前段工程化可以分成四個塊來說,分别為

  • 子產品化:将一個檔案拆分成多個互相依賴的檔案,最後進行統一的打包和加載,這樣能夠很好的保證高效的多人協作。其中包含
  1. JS 子產品化:CommonJS、AMD、CMD 以及 ES6 Module。
  2. CSS 子產品化:Sass、Less、Stylus、BEM、CSS Modules 等。其中預處理器和 BEM 都會有的一個問題就是樣式覆寫。而 CSS Modules 則是通過 JS 來管理依賴,最大化的結合了 JS 子產品化和 CSS 生态,比如 Vue 中的 style scoped。
  3. 資源子產品化:任何資源都能以子產品的形式進行加載,目前大部分項目中的檔案、CSS、圖檔等都能直接通過 JS 做統一的依賴關系處理。
  • 元件化:不同于子產品化,子產品化是對檔案、對代碼和資源拆分,而元件化則是對 UI 層面的拆分。
  1. 通常,我們會需要對頁面進行拆分,将其拆分成一個一個的零件,然後分别去實作這一個個零件,最後再進行組裝。
  2. 在我們的實際業務開發中,對于元件的拆分我們需要做不同程度的考量,其中主要包括細粒度和通用性這兩塊的考慮。
  3. 對于業務元件,你更多需要考量的是針對你負責業務線的一個适用度,即你設計的業務元件是否成為你目前業務的 “通用” 元件,比如我之前分析過的權限校驗元件,它就是一個典型的業務元件。感興趣的小夥伴可以點選 傳送門 自行閱讀。
  • 規範化:正所謂無規矩不成方圓,一些好的規範則能很好的幫助我們對項目進行良好的開發管理。規範化指的是我們在工程開發初期以及開發期間制定的系列規範,其中又包含了
  1. 項目目錄結構
  2. 編碼規範:對于編碼這塊的限制,一般我們都會采用一些強制措施,比如 ESLint、StyleLint 等。
  3. 聯調規範:這塊可參考我以前知乎的回答,前後端分離,背景傳回的資料前端沒法寫,怎麼辦?
  4. 檔案命名規範
  5. 樣式管理規範:目前流行的樣式管理有 BEM、Sass、Less、Stylus、CSS Modules 等方式。
  6. git flow 工作流:其中包含分支命名規範、代碼合并規範等。
  7. 定期 code review
  8. … 等等

    以上這些,我之前也寫過一篇文章做過一些點的詳細說明,TypeScript + 大型項目實戰

  • 自動化:從最早先的 grunt、gulp 等,再到目前的 webpack、parcel。這些自動化工具在自動化合并、建構、打包都能為我們節省很多工作。而這些前端自動化其中的一部分,前端自動化還包含了持續內建、自動化測試等方方面面。

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

四、實戰:AST & webpack loader

而本文提及的實戰,則是通過 AST 改造書寫一個屬于我們自己的 webpack loader,為我們項目中的 promise 自動注入 catch 操作,避免讓我們手動書寫那些通用的 catch 操作。

1、AST 改造

講了這麼多,終于進入到我們的實戰環節了。那麼我們實戰要做一個啥玩意呢?

場景:日常的中台項目中,經常會有一些表單送出的需求,那麼送出的時候就需要做一些限制,防止有人手抖多點了幾次導緻請求重複發出去。此類場景有很多解決方案,但是個人認為最佳的互動就是點選之後為送出按鈕加上 loading 狀态,然後将其 disabled 掉,請求成功之後再解除掉 loading 和 disabled 的狀态。具體送出的操作如下

this.axiosFetch(this.formData).then(res => {
  this.loading = false
  this.handleClose()
}).catch(() => {
  this.loading = false
})
           

複制

這樣看着好像還算 OK,但是如果類似這樣的操作一多,或多或少會讓你項目整體的代碼看起來有些重複備援,那麼如何解決這種情況呢?

很簡單,咱直接使用 AST 編寫一個 webpack loader,讓其自動完成一些代碼的注入,若我們項目中存在下面的代碼的時候,會自動加上 catch 部分的處理,并将 then 語句第一段處理主動作為 catch 的處理邏輯

this.axiosFetch(this.formData).then(res => {
  this.loading = false
  this.handleClose()
})
           

複制

我們先看看,沒有 catch 的這段代碼它的 AST 結構是怎樣的,如圖

[第5期] AST 與前端工程化實戰AST 與前端工程化實戰

其 MemberExpression 為

this.axiosFetch(this.formData).then
           

複制

arguments 為

res => {
  this.loading = false
  this.handleClose()
}
           

複制

OK,我們再來看看有 catch 處理的代碼它的 AST 結構又是如何的,如圖

[第5期] AST 與前端工程化實戰AST 與前端工程化實戰

其 MemberExpression 為

this.axiosFetch(this.formData).then(res => {
  this.loading = false
  this.handleClose()
}).catch
           

複制

其中有兩個 ArrowFunctionExpression,分别為

// ArrowFunctionExpression 1
res => {
  this.loading = false
  this.handleClose()
}
// ArrowFunctionExpression 2
() => {
  this.loading = false
}
           

複制

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

  1. 對 ArrowFunctionExpression 類型進行周遊,獲得其 BlockStatement 中的第一個 ExpressionStatement,并儲存為 firstExp
  2. 使用 builders 建立一個空的箭頭函數,并将儲存好的 firstExp 指派到該空箭頭函數的 BlockStatement 中
  3. 對 CallExpression 類型進行周遊,将 AST 的 MemberExpression 修改成為有 catch 片段的格式
  4. 将改造完成的 AST 傳回

現在,按照我們的思路,我們一步一步來做 AST 改造

首先,我們需要擷取到已有箭頭函數中的第一個 ExpressionStatement,擷取的時候我們需要保證目前 ArrowFunctionExpression 類型的 parent 節點是一個 CallExpression 類型,并且保證其 property 為 promise 的then 函數,具體操作如下

// 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
  }
})
           

複制

緊接着,我們需要建立一個空的箭頭函數,并将 firstExp 指派給它

const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
           

複制

随後,我們則需要對 CallExpression 類型的 AST 對象進行周遊,并做最後的 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
  }
})
           

複制

最後我們在 CallExpression 周遊的時候将其替換

path.replace(newFunc)
           

複制

初版的全部代碼如下

// 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)
           

複制

執行

node promise-catch.js

,列印台輸出結果

this.axiosFetch(this.formData).then(res => {
  this.loading = false
  this.handleClose()
}).catch(() => {
  this.loading = false
})
           

複制

是以能看出來,我們已經是完成了我們想要完成的樣子了

[第5期] AST 與前端工程化實戰AST 與前端工程化實戰
  1. 但是我們還得對一些情況做處理,第一件就是需要在 CallExpression 周遊的時候保證其 arguments 為箭頭函數。
  2. 緊接着,我們需要判定我們擷取到的 firstExp 是否存在,因為我們的 then 進行中可以是一個空的箭頭函數。
  3. 然後防止 promise 擁有一些自定義的 catch 操作,則需要保證其 property 為 then。
  4. 最後為了防止多個 CallExpression 都需要做自動注入的情況,然後其操作又不同,則需要在其内部進行 ArrowFunctionExpression 周遊操作

經過這些常見情況的相容後,具體代碼如下

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
  }
})
           

複制

然後由于之後需要做成一個 webpack-loader,用在我們的實際項目中。是以我們需要對 parse 的解析器做個替換,其預設的解析器為

recast/parsers/esprima

,而一般我們項目中都會用到

babel-loader

,是以我們這也需要将其解析器改為

recast/parsers/babel

const ast = recast.parse(code, {
  parser: require('recast/parsers/babel')
})
           

複制

2、webpack loader

到這裡,我們對于代碼的 AST 改造已經是完成了,但是如何将其運用到我們的實際項目中呢?

OK,這個時候我們就需要自己寫一個 webpack loader 了。

其實,關于如何開發一個 webpack loader,webpack 官方文檔 已經講的很清楚了,下面我為小夥伴們做個小總結。

i. 本地進行 loader 開發

首先,你需要本地建立你開發 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') }
        ]
      }
    ]
  }
}
           

複制

src/index.js

内容如下

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
}
           

複制

然後,搞定收工。

ii. npm 發包

這裡我在以前的文章中提及過,這裡不談了。如果還沒搞過 npm 發包的小夥伴,可以點選下面連結自行檢視

揭秘元件庫一二事(釋出 npm 包片段)

OK,到這一步,我的

promise-catch-loader

也是已經開發完畢。接下來,隻要在項目中使用即可

npm i promise-catch-loader -D
           

複制

由于我的項目是基于 vue-cli3.x 建構的,是以我需要在我的

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()
  }
}
           

複制

然後我項目裡面擁有以下 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>
           

複制

然後在浏覽器中檢視 source 能看到如下結果

[第5期] AST 與前端工程化實戰AST 與前端工程化實戰

關于代碼,我已經托管到 GitHub 上了,promise-catch-loader

總結

到這步,我們的實戰環節也已經是結束了。當然,文章隻是個初導篇,更多的類型還得小夥伴自己去探究。

AST 它的用處還非常的多,比如我們熟知的 Vue,它的 SFC(.vue) 檔案的解析也是基于 AST 去進行自動解析的,即 vue-loader,它保證我們能正常的使用 Vue 進行業務開發。再比如我們常用的 webpack 建構工具,也是基于 AST 為我們提供了合并、打包、建構優化等非常實用的功能的。

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

最後,希望文章的内容能夠幫助小夥伴了解到:什麼是 AST?如何借助 AST 讓我們的工作更加效率?AST 又能為前端工程化做些什麼?

如果覺得文章不錯,那麼希望你能動動你的小手,幫忙點個贊,謝謝了 ~

前端交流群:731175396