【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 }
}
发布了178 篇原创文章 · 获赞 11 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/yehuozhili/article/details/105308154