前端开发者的登天梯 - babel

3,329 阅读12分钟

《圣经》创世纪中记载,在上帝耶稣见人罪孽深重,心中整日思想恶事,世界因人行为败坏而败坏,于是使洪水泛滥,只有与上帝同行的诺亚蒙恩,通过诺亚方舟躲过了大洪水。而后世界分化前,人类之间只存在一种文字和语言。人类为了防止洪水再次来临时无处可躲,人们开始着手建造一座能通天的巨塔。为了阻止人类这一计划,上帝逼迫人类将不同的语言,使得他们无法沟通。为了完成躲避大洪水的通天,人类必须克服语言不同的问题,进而才能集中力量工作协作完成这个浩大的工程。

前端开发者,早起因为各种因素奔忙与各种浏览器之间,疲于兼容性;而后随着语言的快速发展,整天困扰在各种新语法的不同支持实现;而今又要不断被新的框架、标准处处受制,很难集中于前端技术本身、或者前端项目工程主体,精力透支,却收效甚微。

如同通天塔的建造者,如果有一个统一的前端语言版本,统一不同实现、不同标准的工具,那么将极大推进前端的日常开发与整体的发展。

幸运的是,通天塔Babel已经建好了。

1、babel简介

babel是什么?

Dan.abramov 的评价

又一个JQuery?

博客:overreacted.io/, twitter: twitter.com/dan_abramov

github主页:github.com/gaearon,邮箱:dan.abramov@gmail.com

babel是FaceBook的工程师Sebastian McKenzie。早在2014年,ES2015的名字还没正式确定为ES6,它 就发布了一款名为6to5的javascript编译器,名如其意,他的作用就是将ES6转换为ES5,Sebastian McKenzie的初衷是6to5不光是按照标准逐步完善,而且具备反过来影响并推进标准制定的潜在作用。后来ES6正式命名为ES2015,其名字更加偏离其意图,于是在2015年2月15号,6to5正式更名为babel。

Babel官网对其的描述是:babel是一个javascript编译器;再说详细一点也就是一个主要用于将ES2015以及更高版本的代码转换成支持当前或更老版本浏览器环境的工具链。

支持更老版本的浏览器环境需要代码转换很容易理解,为什么即便我使用的当前的浏览器为最新版本也需要转换呢,这里就涉及到javascript语言的一个规范化组织TC39

babel主要功能如下:

babel原理

1、语法转换
2、Polyfill实现目标环境中缺少的功能(通过core-js库)
3、代码转换(比如将高级语法解析为当前可用的实现)
4、DSL转换(如解析JSX)、代码静态分析,如code linter、type check、jsdoc生成

2、babel的版本

Babel和其他工具库一样在不断更新迭代,从2016年开始到现在已经从V5版本更新到V7版本,每一次版本的变化都会有大量内容的变动,而且很多资料也将新旧版本的内容混杂在一起,给初学者带来了很大的学习难度。所以需要首先了解一下babel的版本。

babel这个工具集是围绕@babel/core这个核心包构成的,每次发布新版本的时候,整个工具集的其他npm包也都会跟着升级到@babel/core相同的版本,即使一行代码也没有改变。

当我们提到babel版本的时候,通常指@babel/core这个核心板的版本。

目前前端开发使用的Babel版本主要是babel V6 和 V7两个版本。

作为一个有文化的前端,自然需要了解一下最近两个版本主要区别:文件命名及文件组织方式:

  • Babel6: 子包命名为 babel-xxx
  • Babel7:子包命名为@babel/xxx, 即npm包都放在babel域下

3、Babel配置

作为一个追求时髦的软件开发人员,一定注意到项目中的.gitignore、.editorconfig、eslintrc.json、.eslintrc.js、tslint.json、webpack.config.js类似的各种文件,这些文件都是项目全局性的配置,对项目的所有(或指定)代码其作用,那么像其他所有优秀的库一样,babel需要怎么配置呢?

在此之前,我们先考虑一下下面这个问题:一个优秀的工具库都有那些可配置的方法?

回想一下我们是如何使用其他工具库:

3.1 一个工具库的正确使姿势

3.1.1 命令行内直接输入配置参数

一个工具最简单直接的使用方式便是开箱即用。 在命令行中直接输入需要运行的库指令,并带上相关的配置参数,这种方式简单快捷,适用于演示或自动脚本,但对于需要复杂配置参数的大型项这种方式就显得捉襟见肘,如演示一个简单的webpack构建示例:

webpack index.js -o dist/bundle.js --hot --inline --quiet

3.1.2 配置文件 + 简单命令

对于方式3.2的一种改进就是将配置信息提取统一放在一个文件中,然后用基本命令调用配置文件。但由于是仅可读的配置文件,且对配置注释支持有限,如果需要获取动态配置参数进行配置,仍无能为力。如 添加eslint.*文件,然后执行eslint src/*

// eslint.json
{
    "parser": "esprima",
    "rules": {
        "semi": "error"
    }
}
//  eslint.yaml
---
  env:
    browser: true
    node: true

// package.json
{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
  },
  "eslintConfig": {
    'extends': ['@sunl-fe/eslint-config-sunlands'],
    "rules":{
        "function-paren-newline": 0,
    }
}
3.1.3 文件配置 + 代码逻辑

这种方式不仅将配置信息进行提取统一,同时可以根据外部传入参数获取动态化参数配置,通常现在的前端项目工程都采用此种方式如 webpack compile逻辑:

const fs = require('fs')
const path = require('path')
const wepack = require('webapck');
const filePath = process.NODE_ENV=== 'dev' ? devConfigPath : prodConfigPath;
const config = fs.readFileSync(path.resolve(filePath))

webpack(config)

讲到这里,相信大家对工具库的最佳使用特性已经了然于胸; 方便快捷、统一配置、动态参数、共享、扩展支持、便捷注释等等,这些属于你应该有了一个初步的概念。

鉴于以上,也是我们为什么在进行项目配置文件编写时,首先推荐使用js文件的方式。

3.2 babel的配置方式

同样,作为一个优秀的工具库,babel也支持以上提到的所有配置使用方式

  • 方式① 命令行直接使用

babel --plugins @babel/plugin-transform-arrow-functions src/index.js -d dist

  • 方式② 配置文件
// babel.config.json (编译node_modules)
// .babelrc.json      (编译项目单个部分)
// .babelrc
{
  "presets": [
    [
      "@babel/env",
      {
        "useBuiltIns":"usage"
      }
    ]
  ],
  "plugins": [...],
  "extends": [...]
}
// package.json
{
  "name": "my-package",
  "version": "1.0.0",
  "babel": {
    "presets": [ ... ],
    "plugins": [ ... ],
  }
}
  • 方式③ javascript配置文件
// js 文件配置导出
module.exports = {
    "presets": [ ... ],
    "plugins": [ ... ],
}
// js代码逻辑 使用API(@babel/core)
// compile.js
require("@babel/core").transfromSync("code", options)
3.3 babel配置参数

babel最主要的两个参数辨识presets、plugins

3.3.1 @babel/preset-env

@babel/preset-env是babel中最重要的一个preset,其本质是一个插件集合,包含了所有稳定的转码插件,还支持根据目标环境进行自定义参数设置。 babel的预设可以分为 类:

  • ① 官方预设: babel官方提供了几个常见环境的预设:
    • @babel/preset-env 用于编译ES2015+语法
    • @babel/preset-flow用于编译Flow类型声名语法
    • @babel/preset-react用于编译React项目语法
    • @babel/preset-typescript用于编译Typescript语法
  • ② 实验性预设Stage-X: state-x预设中的任何转换都是对一部分未经批准作为javascriipt发行版的更改,TC39将提案分为以下几个阶段:
    • Stage-0 稻草人:仅仅是一个想法
    • Stage-1 提案 :值得进行深入讨论的提案
    • Stage-2 草案 :初步的规范
    • Stage-3 候选方案: 完整的规范和初步的浏览器实现
    • Stage-4 完成: 已经确定并将被添加到下一年度发行版中的规范
  • ③ 其他npm插件预设 如: {presets:["babel-preset-npm"]}
  • ④ 用户自定义预设:{presets:["./localPath/baabel-preset-myPreset"]}

对于同时进行多种方式进行配置的项目,如何确定配置参数的解析?

针对不同配置方式的优先级:

babel.config.json < .babelrc < @babel/cli 可编程选项

针对同一个字段的预设,也有一定的优先级或解析规则。

即babel.config.json会被.babelrc覆盖,而.babelrc会被可编程选项覆盖; 此外对于同一个文件的多环境配置,覆盖逻辑也分为plugins 、 presets和其他字段如parserOpts、generatorOpts, 具体参考babel官网

4、babel的简单例子

  • ① 创建项目目录 mkdir demo && cd demo
  • ② 初始化仓库: ·npm init -y
  • ③ 安装babel核心依赖:npm install @babel/core @babel/cli @babel/preset-env -D
  • ④ 添加babel配置文件 babel.config.js
// babel.config.js
module.exports = {
    presets: [
        [
            "@babel/preset-env",
            { debug: true },  // 此处添加debug配置是为了打印babel工作详细日志,以便于查看babel转换了哪些语句
        ]
    ],
    plugins: []
}
  • ⑤ 添加代码文件
// src/index.js
const plus = (a, b) => a + b
  • ⑥ 添加打包命令
// package.json
<!--注意如果这里直接执行命令babel src/index.js --out-dir dist需要全局安装@babel/cli-->
{
  "name": "demo-1",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "compile": "babel src/index.js --out-dir dist",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.15.4",
    "@babel/core": "^7.15.5",
    "@babel/preset-env": "^7.15.4"
  }
}

  • ⑦ 执行编译命令: npm run compile 此时输出打包后文件,可以看到ES6的const被转化为var,箭头函数被转化为普通函数,同时添加了 use strict严格模式
// dist/index.js
"use strict";

var add = function add(a, b) {
  return a + b;
};

控制台输出打包日志:

> demo-1@1.0.0 compile /Users/admin/project/demo/babel/demo-1
> babel src/index.js --out-dir dist

@babel/preset-env: `DEBUG` option

Using targets:
{}

Using modules transform: auto

Using plugins:
  proposal-private-property-in-object { }
  proposal-class-properties { }
  proposal-private-methods { }
  proposal-numeric-separator { }
  proposal-logical-assignment-operators { }
  proposal-nullish-coalescing-operator { }
  proposal-optional-chaining { }
  proposal-json-strings { }
  proposal-optional-catch-binding { }
  transform-parameters { }
  proposal-async-generator-functions { }
  proposal-object-rest-spread { }
  transform-dotall-regex { }
  proposal-unicode-property-regex { }
  transform-named-capturing-groups-regex { }
  transform-async-to-generator { }
  transform-exponentiation-operator { }
  transform-template-literals { }
  transform-literals { }
  transform-function-name { }
  transform-arrow-functions { }
  transform-block-scoped-functions { }
  transform-classes { }
  transform-object-super { }
  transform-shorthand-properties { }
  transform-duplicate-keys { }
  transform-computed-properties { }
  transform-for-of { }
  transform-sticky-regex { }
  transform-unicode-escapes { }
  transform-unicode-regex { }
  transform-spread { }
  transform-destructuring { }
  transform-block-scoping { }
  transform-typeof-symbol { }
  transform-new-target { }
  transform-regenerator { }
  transform-member-expression-literals { }
  transform-property-literals { }
  transform-reserved-words { }
  proposal-export-namespace-from { }
  transform-modules-commonjs
  proposal-dynamic-import

Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set.
Successfully compiled 1 file with Babel (634ms).

5、工作原理

上面是一个简单的打包,了解了使用后,我们看一下babel编译背后所做的事情,也就是babel内部底层所做的工作;

5.1 babel工作核心流程

babel编译的核心流程非常简单,即把代码解析为AST语法树,然后再对AST语法树执行操作,最后根据规则生成代码。

这里提到一个概念:编译。编译

想一下为什么解析器没有用正则去处理字符串代码?

5.1.1 当前流行的前端工具,如eslinttypescriptwebpackprettier底层实现也都是这种方式。

5.1.2 babel工作大致可分为如下三个步骤:

  • parse: 解析代码:通过@babel/parser、@babel/traverse将输入代码解析为AST抽象语法树
  • transform: 编译代码:通过@babel/traverse深度优先遍历AST,配合@babel/types判断AST节点类型,实现AST抽象语法树CURD
  • generator:生成新代码:通过@babel/generator将编辑过的AST输出为转译后代码,并生成sourcemap
5.2 babel主要库和工具类
  • @babel/core 整个babel的入口,包含babel的整个工作流,如解析配置、应用plugins、presets,整体编译流程插件和插件的一些公共函数,如:
  • @babel/helpers 用于转换es next代码需要的通过模板创建AST
  • @babel/helpers-xxx 其他插件之间共享用于操作AST的公共函数

此外,除编译阶段转换需要的公共函数以外,还有一些是运行时的公共函数,如:

  • @babel/runtime 主要用于将转换后的文件中注入辅助函数,主要包含corejs、helpers、regenerator这3部分;如:function _classCallCheck等
    • helpers : helper函数的运行时版本
    • corejs : es next的api实现;corejs2只支持静态方法;corejs3还支持实例方法
    • regenerator:async await的实现;
  • @babel/plugin-transform-runtime 工具类:
    • ①自动移除语法转换后的内联辅助函数,使用@babel/runtime/helpers里面的辅助函数来替;
    • ②代码里面使用core-js的API时,自动引入@babel/runtime-corejs3/core-js-stable,替换全局引入的core-js/stable;
    • ③当代码里面使用Generator/async函数时,自动引入@bable/runtime/regenerator,替换全局引入的regenerator/runtime/runtime;

babel生成AST并进行加工需要用到:

  • @babel/parser 将源代码解析成AST,对应parse阶段
  • @babel/traverse 遍历AST并调用visitor函数,对应tranform阶段
  • @babel/generator 将AST代码生成目标js代代码

遍历AST的过程需要创建AST,会用到:

  • @babel/types 工具类:主要用途是创建AST过程判断各种语法的类型
  • @babel/template 工具类:根据模块批量创建AST

其他

  • @babel/code-frame 工具类:主要用于生成错误信息并输出错误原因、函数,可类比console

babel 解析过程

Babel将ES6的标准分为syntax、build-in两种类型。默认只转移syntax部分。build-in部分依赖polyfill/core-js完成转换。

  • syntax: 可以理解为语法,通过presets设置解析规则,比如箭头函数=>、保留字const等;
  • built-in: 可以理解为代码片段解析(code anylysise、codemods);通过plugins设置解析规则,比如Promise等

@babel/parser的前身babylon是基于acorn、acorn-jsx解析器开发的。 js解析器是前端工程化的基石,我们日常使用的webpack、babel、eslint、TypeScript背后都对应一套解析器。 他们基本上都大同小异。

Esprima是第一个用JavaScript实现的符合EsTree规范的Javascript解析器

js解析器

header 1header 2
row 1 col 1row 1 col 2
row 2 col 1row 2 col 2