美文网首页工程化
从 Babel 到组件按需引入原理

从 Babel 到组件按需引入原理

作者: 旭哥_ | 来源:发表于2020-05-16 11:40 被阅读0次

    友情链接

    前言

    谈到 babel 肯定大家都不会感觉陌生。

    • 桌面端组件库 Element ,借助 babel-plugin-component ,我们可以只引入需要的组件,以达到减小项目体积的目的。
    • 使用 babel-polyfill ,开发者可以立即使用 ES 规范中的最新特性。
    • 有了插件: transform-vue-jsxreact ,我们在 vue 和 react 开发中可以直接使用 JSX 编写模板。

    组件能按需引入到底是怎么实现的? Babel 的工作原理是怎样的呢?

    带着疑问,我们尝试对其原理深入探索和理解。

    Babel 编译的三个阶段

    Babel 是一个 JavaScript 编译器。

    和大多数其他语言的编译器相似,Babel 的编译过程可分为三个阶段:

    • 解析 Parse :将代码字符串解析成抽象语法树(AST)。简单来说就是对 JS 代码进行词法分析与语法分析。
    • 转换 Transform :对抽象语法树进行转换操作。这里操作主要是添加、更新及移除。
    • 生成 Generate : 根据变换后的抽象语法树再生成代码字符串。

    解析 Parse

    Babel 会把源代码抽象出来,变成 AST

    可以看看 var answer = 6 * 7; 抽象之后的结果。

    {
        "type": "Program", // 根结点
        "body": [
            {
                "type": "VariableDeclaration", // 变量声明
                "declarations": [
                    {
                        "type": "VariableDeclarator", // 变量声明器
                        "id": {
                            "type": "Identifier",
                            "name": "answer"
                        },
                        "init": {
                            "type": "BinaryExpression", // 表达式
                            "operator": "*", // 操作符是 *
                            "left": {
                                "type": "Literal", // 字面量
                                "value": 6,
                                "raw": "6"
                            },
                            "right": {
                                "type": "Literal",
                                "value": 7,
                                "raw": "7"
                            }
                        }
                    }
                ],
                "kind": "var"
            }
        ],
        "sourceType": "script"
    }
    

    ProgramVariableDeclarationVariableDeclaratorIdentifierBinaryExpressionLiteral 均为节点类型。每个节点都是一个有意义的语法单元。这些节点通过携带的属性描述自己的作用。

    其中的所有节点名词,均来源于 ECMA 规范

    ATS 生成过程分为两个步骤:

    • 分词:将代码字符串分割成语法单元数组 token
    • 语法分析:分析语法单元之间的关联关系。
    分词

    JS 中的语法单元主要包括以下这么几种:

    • 关键字: constletvar 等。
    • 标识符:if/elsereturnfunction 等。
    • 运算符:+-*/ 等。
    • 数字
    • 空格
    • 注释

    比如下面的代码生成的语法单元数组:

    var answer = 6 * 7;
    
    // Tokens
    [
        {
            "type": "Keyword",
            "value": "var"
        },
        {
            "type": "Identifier",
            "value": "answer"
        },
        {
            "type": "Punctuator",
            "value": "="
        },
        {
            "type": "Numeric",
            "value": "6"
        },
        {
            "type": "Punctuator",
            "value": "*"
        },
        {
            "type": "Numeric",
            "value": "7"
        },
        {
            "type": "Punctuator",
            "value": ";"
        }
    ]
    

    分词的大致思路:遍历字符串,通过各种方式(如:正则)匹配当前字符串片段对应的语法单元类型,然后生成数组 token

    语法分析

    先了解语法分析的两个概念:

    • 语句:指一个具备边界的代码区域,相邻的两个语句之间从语法上来讲互不影响,即使调换顺序也不会产生语法错误。
    • 表达式:指最终有个结果的一小段代码,它可以嵌入到另一个表达式,且包含在语句中。

    语法分析就是识别语句和表达式,这是一个递归的过程(理解为深度优先遍历)。Babel 会在解析过程中设置一个暂存器,用来暂存当前读取到的语法单元,如果解析失败,就会返回之前的暂存点,再按照另一种方式进行解析,如果解析成功,则将暂存点销毁,不断重复以上操作,直到最后生成对应的语法树。

    转换 Transform

    Plugins

    插件应用于 Babel 的转译过程。如果不使用任何插件,那么 Babel 会原样输出代码。

    Presets

    Babel 官方已经针对常用环境编写了一些 preset

    Preset 的路径:

    如果 presetnpm 上,你可以输入 preset 的名称,Babel 将检查是否已经将其安装到 node_modules 目录下了

    {
      "presets": ["babel-preset-myPreset"]
    }
    

    你还可以指定指向 preset 的绝对或相对路径。

    {
      "presets": ["./myProject/myPreset"]
    }
    

    Preset 的排列顺序:

    Preset 是逆序排列的(从后往前)。

    {
      "presets": [
        "a",
        "b",
        "c"
      ]
    }
    

    将按如下顺序执行: cb 然后是 a

    这主要是为了确保向后兼容,由于大多数用户将 es2015 放在 stage-0 之前。

    生成 Generate

    babel-generator 通过 AST 树生成 ES5 代码。

    实现一个简单的按需打包功能

    例如 ElementUI 中把 import { Button } from 'element-ui' 转成 import Button from 'element-ui/lib/button'

    可以先对比下 AST

    // import { Button } from 'element-ui'
    {
        "type": "Program",
        "body": [
            {
                "type": "ImportDeclaration",
                "specifiers": [
                    {
                        "type": "ImportSpecifier",
                        "local": {
                            "type": "Identifier",
                            "name": "Button"
                        },
                        "imported": {
                            "type": "Identifier",
                            "name": "Button"
                        }
                    }
                ],
                "source": {
                    "type": "Literal",
                    "value": "element-ui",
                    "raw": "'element-ui'"
                }
            }
        ],
        "sourceType": "module"
    }
    
    // import Button from 'element-ui/lib/button'
    {
        "type": "Program",
        "body": [
            {
                "type": "ImportDeclaration",
                "specifiers": [
                    {
                        "type": "ImportDefaultSpecifier",
                        "local": {
                            "type": "Identifier",
                            "name": "Button"
                        }
                    }
                ],
                "source": {
                    "type": "Literal",
                    "value": "element-ui/lib/button",
                    "raw": "'element-ui/lib/button'"
                }
            }
        ],
        "sourceType": "module"
    }
    

    可以发现, specifierstypesourcevalue、raw 不同。

    然后 ElementUI 官方文档中,babel-plugin-component 的配置如下:

    // 如果 plugins 名称的前缀为 'babel-plugin-',你可以省略 'babel-plugin-' 部分
    {
      "presets": [["es2015", { "modules": false }]],
      "plugins": [
        [
          "component",
          {
            "libraryName": "element-ui",
            "styleLibraryName": "theme-chalk"
          }
        ]
      ]
    }
    

    直接干:

    import * as babel from '@babel/core'
    
    const str = `import { Button } from 'element-ui'`
    const { result } = babel.transform(str, {
        plugins: [
            function({types: t}) {
                return {
                    visitor: {
                        ImportDeclaration(path, { opts }) {
                            const { node: { specifiers, source } } = path
                            // 比较 source 的 value 值 与配置文件中的库名称
                            if (source.value === opts.libraryName) {
                                const arr = specifiers.map(specifier => (
                                    t.importDeclaration(
                                    
                                        [t.ImportDefaultSpecifier(specifier.local)],
                                        // 拼接详细路径
                                        t.stringLiteral(`${source.value}/lib/${specifier.local.name}`)
                                    )
                                ))
                                path.replaceWithMultiple(arr)
                            }
                        }
                    }
                }
            }
        ]
    })
    
    console.log(result) // import Button from "element-ui/lib/Button";
    

    完美!我们的第一个 Babel 插件完成了。

    大家有没有对 Babel 有自己的理解了呢?

    感谢

    如果本文对你有帮助,就点个赞支持下吧!感谢阅读。

    相关文章

      网友评论

        本文标题:从 Babel 到组件按需引入原理

        本文链接:https://www.haomeiwen.com/subject/ahplohtx.html