从 Babel 到组件按需引入原理

友情链接

前言

谈到 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 有自己的理解了呢?

感谢

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355