前言
- 第一篇講了tapable,這篇記錄ast。
- ast是成爲大神必會的,曾經我也是非常非常討厭學這個,畢竟感覺寫的沒啥成就感。不過這個熟練掌握之後可以寫很多插件之類,比如給你的組件寫個按需加載插件,所以還是得學!!!
AST
- ast是抽象語法樹,webpack和很多工具核心就是通過ast對代碼檢查和分析。
- 現在把js轉換成語法樹有很多解析器,每個js引擎有自己的抽象語法樹格式。
- babel步驟分爲解析、轉換、生成。解析步驟分爲詞法分析和語法分析,詞法分析是轉成token流,類似扁平語法片段數組。語法分析是轉成ast樹形式。
- 轉換步驟接收 AST 並對其進行遍歷,在此過程中對節點進行添加、更新及移除等操作。 這是 Babel 或是其他編譯器中最複雜的過程 同時也是插件將要介入工作的部分。
- 生成步驟深度優先遍歷整個 AST,然後構建可以表示轉換後代碼的字符串,同時創建source map。
箭頭函數轉換插件
- 寫插件前,先打開2個網址備用
- 一個是可視化ast網站,能看清結構。
- 一個是babelAPI官網,可以找到想生成的語法樹api。
- 安裝:
@babel/core babel-types babel-traverse
- 其中@babel/core就是轉語法樹的,babel-types用來生成語法樹,babel-traverse用於對 AST 的遍歷,維護了整棵樹的狀態,並且負責替換、移除和添加節點。
- 下面例子的目的就是把
const sum = (a,b)=>a+b
變爲
const sum = function sum(a, b) {
return a + b;
};
代碼
let babel = require('@babel/core')
let t = require('babel-types')
const code = `const sum = (a,b)=>a+b`
//找出複用節點
let ArrayFunctionPlugin = {
visitor: {//訪問所有節點
ArrowFunctionExpression: (path) => {//函數名是type,如果一段代碼沒匹配到type,代碼就不會傳來
let node = path.node //獲得當前對應節點
let id = path.parent.id //拿到sum的節點
let params = node.params// 拿到參數節點
let body = t.blockStatement([//語句塊
t.returnStatement(node.body)//返回語句 原來式子中的body
])
let functionExpression = t.functionExpression(id, params, body, false, false)//創建function函數 名字就是sum
path.replaceWith(functionExpression)//替換
}
}
}
let result = babel.transform(code, {
plugins: [ArrayFunctionPlugin]
})
console.log(result.code)
-
邏輯不難,需要自己打印下看下就明白,所有代碼已註釋,其實這個path是相對於你這個type做的樹。所以path.parent就會有東西。
-
主要還是能複用就複用,不能複用就造,然後組裝,組裝完替換。
-
插件選項可以在第二個參數收到。
{
plugins: [
["my-plugin", {
"option1": true,
"option2": false
}]
]
}
轉換爲插件
- 剛纔做了個箭頭函數的插件,現在得用起來,這個找了我很長時間,很多地方都說怎麼做這個,但是沒說咋變成插件。
- 首先還是上面那段,改成:
module.exports = function ({ types: t }) {
return {
visitor: {
ArrowFunctionExpression: (path) => {//函數名是type,如果一段代碼沒匹配到type,代碼就不會傳來
let node = path.node //獲得箭頭表達式下的節點
let id = path.parent.id //拿到sum的節點
let params = node.params// 拿到參數節點
let body = t.blockStatement([//語句塊
t.returnStatement(node.body)//返回語句原來式子中的body
])
let functionExpression = t.functionExpression(id, params, body, false, false)//創建function函數 名字就是sum
path.replaceWith(functionExpression)//替換
}
}
};
}
- 然後,我們需要在node_modules裏面建個文件夾,叫babel-plugin-myplugin。
- 裏面index.js就寫成上面那樣。
- 這樣在.babelrc中就可以配置它了!
- 比如我們就只配我們自己的插件:
{
// "presets": [
// ["@babel/preset-env",{
// "corejs":{ "version": 3,"proposals": true },
// "useBuiltIns":"usage"
// }]
// ],
"plugins": [
[
"myplugin"
]
]
}
- 這樣完全沒有干擾,使用webpack編譯下,然後看編譯後的結果。發現已經完美轉換了:
/***/ (function(module, exports) {
eval("const sum = function sum(a, b) {\n return a + b;\n};\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
- 把babelrc清空再編譯下:
/***/ (function(module, exports) {
eval("const sum = (a, b) => a + b;\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
- 完美說明是我們插件的功勞。
- 注意:plugins 的插件使用順序是順序的,而 preset 則是逆序的。
- 除了直接放node_module,還可以用npm link 或者webpack配置resolve目錄讓webpack找到插件。
按需加載插件
- 以做lodash按需加載爲例
- 首先試着
import { flatten, concat } from 'lodash'
這種方式引入,發現打包出的js有552k。 - 然後換成
import flatten from 'lodash/flatten'
import concat from 'lodash/concat'
- 引入發現打包後js有24k。這體積直接小了20倍。
- 所以插件目的就是把一行引入轉成2行引入。
- 首先分析一行的寫法:
- 一行寫法裏是再ImportDeclaration節點裏,其中的specifiers是數組,裏面的2個ImportSpecifier就是flatten與concat。
- 這個ImportSpecifier裏面有個imported和local,imported代表它本來導入的名字,local代表它在這個文件裏被改名後的名字。所以,我們要變成2行的時候,需要使用Imported而不是Local的名字。
- 再分析下二行的寫法:
- 這個body裏面就直接寫成了2個ImportDeclaration。裏面是ImportDefaultSpecifier,是默認導入不是普通導入了。
- 所以,我們需要把1行裏普通導入拿出來,拼成2行默認導入,有別名默認導入用別名。模塊名加上普通導入的名字。
代碼
const t = require('babel-types')
const visitor = {
ImportDeclaration: {
enter(path, state = { opts }) {
let specifiers = path.node.specifiers//拿到數組
let source = path.node.source //拿到最後的lodash
if (!t.isImportDefaultSpecifier(specifiers[0]) && state.opts.libraryName === source.value) {//默認導入不替換,來源名要是配的名字
let importDeclaration = specifiers.map(specifier => {//遍歷數組,改成導入語句
return t.importDeclaration([t.importDefaultSpecifier(specifier.local)],
t.stringLiteral(`${source.value}/${specifier.imported.name}`)
)
})
path.replaceWithMultiple(importDeclaration)
}
}
}
}
module.exports = function (babel) {
return { visitor }
}
- 註釋已經加上,並不難,講就是api名字長,還需要自己分析下語法樹。
- 測試過無問題,babelrc這麼配置:
"plugins": [
[
"myimport",
{
"libraryName": "lodash"
}
]
]
- 可以自己改一下代碼試試輸出的是啥:驗證下別名是否正確。
const t = require('babel-types')
const babel = require('@babel/core')
const visitor = {
ImportDeclaration: {
enter(path, state = { opts }) {
let specifiers = path.node.specifiers//拿到數組
let source = path.node.source //拿到最後的lodash
if (!t.isImportDefaultSpecifier(specifiers[0]) && state.opts.libraryName === source.value) {//默認導入不替換,來源名要是配的名字
let importDeclaration = specifiers.map(specifier => {//遍歷數組,改成導入語句
return t.importDeclaration([t.importDefaultSpecifier(specifier.local)],
t.stringLiteral(`${source.value}/${specifier.imported.name}`)
)
})
path.replaceWithMultiple(importDeclaration)
}
}
}
}
const fll = {
visitor
}
const code = `import { flatten as tt , concat } from 'lodash'`
let result = babel.transform(code, {
plugins: [[fll, {
"libraryName": "lodash"
}]]
})
console.log(result.code)
module.exports = function (babel) {
return { visitor }
}