【webpack】webpack構建流程筆記(二)

前言

  • 第一篇講了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 }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章