[Babel] 1. 使用

174 阅读6分钟

Babel

Babel是js的一个编译器,主要用来将esnext新语法转换为向后兼容的语法。Babel这种“源码到源码”编译, 也被称为转换编译。

主要功能:

  • 转换语法
  • polyfill新特性
  • 源码修改
  • 支持语法扩展,比如jsx等

使用

现在Babel自身已经被拆成了若干个核心模块和官方的插件。简单介绍下几个常用的模块和插件。

@babel/core

@babel/core负责接受和转换代码。

源码

// esnext.js
// es2015
const num = 1

class Person {
  sayHi() {
    console.log('hi')
  }
}

转换

import babel from '@babel/core'

// 加载esnext.js文件
const result = babel.transformFileSync('./esnext.js')

console.log(result.code)

结果

// es2015
const num = 1;
class Person {
  sayHi() {
    console.log('hi');
  }
}

// es2022
const temp = {
  name: 'name'
};
console.log(Object.hasOwn(temp, 'name'));

对比发现,babel只是添加了换行符,其他啥好像都没做。因为还需要告诉babel该怎么处理这些代码,需要配合@babel/preset-env预设。

@babel/preset-env

@babel/preset-env本质为一堆插件的集合,用来生成辅助代码。

转换

import babel from '@babel/core'

const babelOptions = {
  presets: ['@babel/preset-env'],
  // 兼容的最低浏览器版本
  targets: { ie: 11 }
}
const result = babel.transformFileSync('./esnext.js', babelOptions)

console.log(result.code)

结果

"use strict";

function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
// es2015
var num = 1;
var Person = /*#__PURE__*/function () {
  function Person() {
    _classCallCheck(this, Person);
  }
  _createClass(Person, [{
    key: "sayHi",
    value: function sayHi() {
      console.log('hi');
    }
  }]);
  return Person;
}();

// es2022
var temp = {
  name: 'name'
};
console.log(Object.hasOwn(temp, 'name'));

对比发现,babel通过生成一堆辅助代码,使esnext的语法兼容旧浏览器(当前设置的为ie11)。@babel/preset-env会根据targets设置,生成对应的代码,如果目标浏览器支持其中部分新语法,则不会转译。比如将targets设置为{chrome: 111},则不会产生辅助代码。

可是有个问题,如果将第一次测试生成的代码,跑在低版本浏览器会报错,因为Object.hasOwn()为es2022的语法,Babel没法生成一些比如Object.hasOwn, Array.prototype.includes()等方法的辅助代码,还需要一些polyfill代码。

转换

import babel from '@babel/core'

const babelOptions = {
  presets: [
    ['@babel/preset-env',
      {
        "useBuiltIns": "usage",
        "corejs": "3.22"
      }
    ]
  ],
  targets: { chrome: 50 }
}
const result = babel.transformFileSync('./esnext.js', babelOptions)

console.log(result.code)

结果

"use strict";

// 自动导入core-js中相应的模块
require("core-js/modules/es.object.has-own.js");
// es2015
const num = 1;
class Person {
  sayHi() {
    console.log('hi');
  }
}

// es2022
const temp = {
  name: 'name'
};
console.log(Object.hasOwn(temp, 'name'));

通过配置@babel/preset-envcore-jsuseBuiltIns选项,Babel自动引入了目标浏览器原生缺少的方法。

小结

通过以上几个代码实践,可以发现Babel转译代码的大概流程。@babel/core主要负责接受代码、解析、生成。 @babel/preset-env中内置了一些插件,主要用来加工代码,来生成兼容性的代码。

详解

配置

@babel/core中的转换方法可以接受一个配置选项参数,配置支持代码硬编码或者配置文件。

配置文件

  1. 项目范围内的配置(仓库结构为monorepo、支持编译node_modules
    • babel.config.*(后缀:.json.js.cjs.mjs.cts)
  2. 文件相关配置(只编译项目中一部分代码)
    • .babelrc.*(后缀: .json.js.cjs.mjs.cts)
    • .babelrc 
    • package.json 中的"babel"字段

.json无法添加代码,.js可以添加一些代码。

js文件配置的一些额外功能

module.exports = function(api) {
    const plugins = [];
    // 动态添加一些插件
    if (process.env.NODE_ENV === "production") {  
        plugins.push('one-plugin');  
    }
  
    // 缓存配置,避免每次都执行获取,提升性能
    api.cache.using(() => process.env.NODE_ENV)
 
    return {
        plugins
    };  
};

配置选项(部分)

rootMode

值有"root" | "upward" | "upward-optional",默认root。决定Babel怎么决定项目根目录。

envName

值有process.env.BABEL_ENV || process.env.NODE_ENV || "development"

plugins

插件

presets

预设

targets

支持的目标环境

  "targets": "> 0.25%, not dead"  
  "targets": {  
    "chrome": "58",  
    "ie": "11"  
   }

如果没设置targets,则默认目标环境是低版本浏览器,转为兼容ES5的代码。

extends

继承其他配置

overrides

覆盖

test

overrides中常用

include

同test

exclude

overrides中常用

ignore

忽略

ignore: ["./lib"];
only
only: ["./src"];
Name Normalization 名称规范化

默认情况下,Babel希望插件名称有babel-plugin- 或者 babel-preset-的前缀。为了避免重复,Babel有个名称转换的阶段,当加载每一项时,会自动添加这些前缀。

  • 绝对路径原封不动
  • 相对路径原封不动
  • 引用的文件原封不动
  • module:为前缀的,module:会被移除
  • @babel-没有plugin-/preset-前缀,会自动添加plugin-/preset-前缀
  • 一些情况下,babel-plugin-/babel-preset-前缀自动注入

例子:

InputNormalized
"/dir/plugin.js""/dir/plugin.js"
"./dir/plugin.js""./dir/plugin.js"
"mod""babel-plugin-mod"
"mod/plugin""mod/plugin"
"babel-plugin-mod""babel-plugin-mod"
"@babel/mod""@babel/plugin-mod"
"@babel/plugin-mod""@babel/plugin-mod"
"@babel/mod/plugin""@babel/mod/plugin"
"@scope""@scope/babel-plugin"
"@scope/babel-plugin""@scope/babel-plugin"
"@scope/mod""@scope/babel-plugin-mod"
"@scope/babel-plugin-mod""@scope/babel-plugin-mod"
"@scope/prefix-babel-plugin-mod""@scope/prefix-babel-plugin-mod"
"@scope/mod/plugin""@scope/mod/plugin"
"module:foo""foo"

Presets

Babel的预设是一些插件和配置选项的集合

官方预设

  • @babel/preset-env
  • @babel/preset-typescript
  • @babel/preset-react
  • @babel/preset-flow

使用预设

babel.config.json

{  
    "presets": ["babel-preset-myPreset", "@babel/preset-env"]  
}

或者使用项目中自定义的预设

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

创建一个预设

导出一个配置对象。

集成若干个插件的预设

module.exports = function() {  
    return {  
        plugins: ["pluginA", "pluginB", "pluginC"],  
    };  
};

包括其他预设和插件的预设

module.exports = () => ({  
    presets: [require("@babel/preset-env")],  
    plugins: [  
        [require("@babel/plugin-proposal-class-properties"), { loose: true }],  
        require("@babel/plugin-proposal-object-rest-spread"),  
    ],  
});

预设的顺序

预设的顺序是反过来的,从后向前。

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

运行的顺序为c、b、a

配置预设的选项

{  
    "presets": [  
        "presetA", // bare string  
        ["presetA"], // wrapped in array  
        ["presetA", {}] // 第二个对象可配置presetA预设 
    ]  
}

官方@babel/preset-env

主要根据设置的目标环境,转换语法。推荐使用.browserslistrc设置目标环境。

配置选项(部分)
targets

目标浏览器

modules

md" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false, 默认"auto".

debug

默认false

include

包括的插件

有效值

  • @babel/plugin-transform-spread
  • @babel/transform-spread
  • babel-transform-spread
  • transform-spread
  • es.map
  • es.math.sign
  • es.math.*
  • /^transform-.*$/
exclude

排除的插件

useBuiltIns

"usage" | "entry" | false, 默认为false.

用来处理polyfills,当使用usage或者entry,会引用core-js模块

useBuiltIns: 'entry' 项目最开始时,引入core-js

import "core-js";

useBuiltIns: 'usage' 在具体的文件中添加指定的polyfills

corejs

指定corejs的版本

forceAllTransforms

强制都转为es5代码

Plugins

Babel系统已经插件化,大的功能可以通过一个个小的插件组合起来实现。

使用插件

如果插件是维护在npm仓库中,可以把包的名称传递给plugins

{  
    "plugins": ["babel-plugin-myPlugin", "@babel/plugin-transform-runtime"]  
}

也可以指定一个路径

{  
    "plugins": ["./node_modules/asdf/plugin"]  
}

Transform Plugins转换插件

转换插件主要是转换代码

Syntax Plugins语法糖插件

Babel会转换大多数的语法。在极少情况下(转换没实现),可以使用插件,比如像@babel/plugin-syntax-bigint,来允许Babel来解析特殊的语法类型。

插件顺序

  • 插件在预设前运行
  • 插件顺序是从前往后
  • 预设是从后往前

插件选项

{  
    "plugins": ["pluginA", ["pluginA"], ["pluginA", {}]]  
}

开发插件

一个简单的例子

export default function() {  
    return {  
        visitor: {  
            Identifier(path) {  
                const name = path.node.name;  
                // reverse the name: JavaScript -> tpircSavaJ  
                path.node.name = name  
                .split("")  
                .reverse()  
                .join("");  
            },  
        },  
    };  
}

官方插件列表

主要分为提案中的和已经发布成为ES标准。还在提案中的特性,就需要安装对应的插件。而已经成为ES标准的特性,更新@babel/preset-env版本就行,其中已经内置对应插件,不需要单独再安装。

比如要使用装饰器的语法,还在提案中,就需要

  1. 安装插件npm install --save-dev @babel/plugin-proposal-decorators
  2. 使用插件{ "plugins": ["@babel/plugin-proposal-decorators", {version: '2023-01'}]}

Compiler assumptions 编译断定

Babel会尽可能地把代码编译成原生表现一样。然而,为了支持一些极限场景,就会生成很多额外、运行很慢的代码。从Babel 7.13.0开始,可以指定assumptions,来更好地优化编译结果。

比如:

{  
    "targets": ">0.5%",  
    "assumptions": {
        // 配置noDocumentAll
        "noDocumentAll": true
    },  
    "presets": ["@babel/preset-env"]  
}

noDocumentAll When using operators that check for null or undefined, assume that they are never used with the special value document.all 当使用操作符来检查null或者undefined时,断定他们不会用在document.all

源码

let score = points ?? 0;
let name = user?.name;

noDocumentAll开启时,输出代码

var _points, _user;
let score = (_points = points) != null ? _points : 0;
let name = (_user = user) == null ? void 0 : _user.name;

noDocumentAll关闭时,输出代码

var _points, _user;
let score = (_points = points) !== null && _points !== void 0 ? _points : 0;
let name = (_user = user) === null || _user === void 0 ? void 0 : _user.name;

对比发现,开启断定时,会少生成一些判断代码。

其他

@babel/plugin-transform-runtime

通常Babel转换后,会在当前文件头部生成辅助代码,但是这会导致每个文件头部都会产生相同辅助代码的问题,所以需要将Babel辅助代码单独引入。@babel/plugin-transform-runtime插件可以再次利用Babel注入的额外代码,来减少生成的代码大小。

// 开发下的依赖
npm install --save-dev @babel/plugin-transform-runtime
// 生产下的依赖(程序运行时需要)
npm install --save @babel/runtime

当这个插件启用时,@babel/preset-envuseBuiltIns不能设置,否则沙盒环境会失效

转换

import babel from '@babel/core'

const babelOptions = {
  presets: ['@babel/preset-env'],
  targets: { ie: 11 },
  // 启用@babel/plugin-transform-runtime插件
  plugins: ['@babel/plugin-transform-runtime']
}
const result = babel.transformFileSync('./esnext.js', babelOptions)

console.log(result.code)

结果

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
// es2015
var num = 1;
var Person = /*#__PURE__*/function () {
  function Person() {
    (0, _classCallCheck2.default)(this, Person);
  }
  (0, _createClass2.default)(Person, [{
    key: "sayHi",
    value: function sayHi() {
      console.log('hi');
    }
  }]);
  return Person;
}();
// es2022
var temp = {
  name: 'name'
};
console.log(Object.hasOwn(temp, 'name'));

辅助代码通过@babel/runtime依赖导入。

总结

文章主要介绍了Babel的使用和配置,通过代码演示对比,更容易理解每个模块的作用,方便脚手架中的配置。