必知必会的babel基础

3,271 阅读9分钟

什么是babel

babel 是一个前端的代码转换工具,目的是为了让开发者使用ECMA最新的标准甚至一些在stage阶段的提案功能,而不用过多考虑运行环境的兼容性。

babel的核心原理

对代码进行以下步骤:

  1. 解析 -- 生成tokens
  2. 转化 -- 生成ast
  3. 生成【也叫打印】 -- 生成对应的输出代码

根据开发者的设定,将高版本的ES语法转化为浏览器或者node环境支持的ES语法

当然,babel相关的工具很多,下面我们以babel7为例,边用边分析。

构建项目

如果你想掌握babel的基础配置,请务必跟上一起操作

如果你实在不想写代码,每一步上都会有一个tag,请务必checkout运行一下对应的代码

git clone github.com/jinggk/babe…

初始化项目

运行git checkout step-1 && yarn run build查看当前步骤的结果

因为涉及到打包,我选择了webpack,首先我们不使用任何babel相关的转码,尝试打包一次看下效果:

测试代码如下:

// src/index.js
const fun = async () => {
    const data = await Promise.resolve(123);
    console.log(data);
};

fun(); // 123

打包后的代码如下:

eval("const fun = async () => {\n    const data = await Promise.resolve(123);\n    console.log(data);\n};\n\nfun();\n\n\n//# sourceURL=webpack:///./src/index.js?")

可以看到默认情况下,我们的代码不会经过任何的转化,下一步,我们加入babel

加入babel-loader

运行git checkout step-2 && yarn run build查看当前步骤的结果

使用babel之前,我们需要一个配置文件,以便于告诉babel要按照什么配置来转化代码,有4种配置的方式:

  1. 在根目录下创建babel.config.js
  2. 在根目录下创建.babelrc或者.babelrc.js
  3. 在webpack的loader上用options的方式配置
  4. 在packagejson中加入配置

想了解具体内容的可以看这里

我采用了官方推荐的方式,创建babel.config.js如下

module.exports = {
    presets: ['@babel/env'] //  env 所包含的插件将支持所有最新的 JavaScript (ES2015、ES2016 等)特性
};

添加babel相关的依赖

yarn add babel-loader @babel/core @babel/preset-env -D

@babel/core 包含了转换api的所有核心模块 @babel/preset-env 一组默认的预设插件的集合,所包含的插件将支持所有最新的 JavaScript (ES2015、ES2016 等)特性 babel-loader 通过这个loader来开启babel对js的作用

修改webpack config,增加以下内容

module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            }
        ]
    },

ok,现在我们重新尝试打包,可以看到打包后的结果变为:

eval("function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }\n\nfunction _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"next\", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, \"throw\", err); } _next(undefined); }); }; }\n\nvar fun =\n/*#__PURE__*/\nfunction () {\n  var _ref = _asyncToGenerator(\n  /*#__PURE__*/\n  regeneratorRuntime.mark(function _callee() {\n    var data;\n    return regeneratorRuntime.wrap(function _callee$(_context) {\n      while (1) {\n        switch (_context.prev = _context.next) {\n          case 0:\n            _context.next = 2;\n            return Promise.resolve(123);\n\n          case 2:\n            data = _context.sent;\n            console.log(data);\n\n          case 4:\n          case \"end\":\n            return _context.stop();\n        }\n      }\n    }, _callee);\n  }));\n\n  return function fun() {\n    return _ref.apply(this, arguments);\n  };\n}();\n\nfun();\n\n//# sourceURL=webpack:///./src/index.js?");

可以看到里面加入了很多辅助的函数,比如 asyncGeneratorStep、_asyncToGenerator 等等用于处理async和await的,但是如果你直接运行当前被转化后的代码,你会看到一些报错

ReferenceError: regeneratorRuntime is not defined

这是因为默认的预设组件虽然会帮我们转化代码,但是不会把相关的辅助函数引入,我们需要通过修改配置的方式来开启一些额外的功能,修改babel.config.js如下:

解决的方式不止一种,我们先用最简单的尝试

module.exports = {
    presets: [
        [
            '@babel/env',
            {
                useBuiltIns: 'usage'
            }
        ]
    ]
};

@babel/env的参数说明:

  1. targets:配置转化代码的目标环境,常见配置有:

    1. String, 比如 "targets": "> 0.25%, not dead"或者"targets": { "chrome": "58", "ie": "11" },了解更多选项可以参考browserslist
    2. esmodules: Boolean,启用这个选项会导致targets browser被忽略,同时要注意,启用这个选项,转化后的代码在浏览器中应该和<script type="module"></script>配合使用
    3. node:指定node的信息,比如current || '8.11.0'
  2. spec: 个人理解就是说转化的过程会更严格和规范,但是付出的代价就是转化时间会比较长

  3. loose:Boolean,是否启用“松散”模式来转化,默认是false 举个例子,定义一个class:

class Person{
    say(){
        console.log(123)
    }
}

正常情况下,类的原型方法需要通过Object.defineProperty去定义,以保证不可枚举,所以转化后的代码会是:

var _createClass = (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, descriptor.key, descriptor); // (A)
        }
    }
    return function (Constructor, protoProps, staticProps) {
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
})();

var Person = (function () {
    function Person() {
        _classCallCheck(this, Person);
    }
    _createClass(Person, [{
        key: "say",
        value: function say() {
            console.log(123)
        }
    }]);
    return Person;
})();

但是如果启用了loose模式,代码就会被转为:

var Person = (function () {
    function Person() {
        _classCallCheck(this, Person);
    }
    Person.prototype.say = function toString() { 
        console.log(123)
    };
    return Person;
})();

更像是直接用ES5的形式来模拟类,更详细的内容可以查看loose-mode

  1. modules: String,常见的有 "amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false默认是auto,这个应该都明白转化为不同模块风格的代码
  2. useBuiltIns:选项有"entry" | "usage" | false,用来决定babel/preset如何处理代码中的 polyfills
    1. entry:使用这个选项的时候,我们需要单独安装@babel/polyfill,并在文件中引入,这个选项会启用一个插件,替换我们所有import或者require @babel/polyfill的地方,更改为只引入对应文件需要的polyfill依赖,以此来减少代码体积,去除不必要的引入
    2. useage: 使用这个选项,不需要引入polyfill,babel在遇到需要加polyfill的地方自动帮我们引入依赖,但是要注意他只会进行对应代码的转化,而不一定保证转化过程中引入的polyfill一定存在,后面我们还会说
    3. false,不做任何优化,在顶部加入@babel/polyfill的情况下没有问题

我们这里使用了useage这个选项,尝试打包,依旧报错了..

Can't resolve 'core-js/modules/es6.object.to-string'
Can't resolve 'core-js/modules/es6.promise
Can't resolve 'regenerator-runtime/runtime' 

babel7.x 之后把一些依赖全部拆分开了,core-js 和 regenerator-runtime相关的lib函数被放在了@babel/polyfill 这个依赖中,因此我们需要安装这个依赖,并且为了所有的js都能正常使用,我们需要在js文件的入口去主动require一下这个包以获取一些工具函数的导入~

yarn add @babel/polyfill 

注意这是一个生产环境需要的依赖,不要放到devDependencies中~

然后修改 src/index.js 在最顶部加上require

require('@babel/polyfill');
const fun = async () => {
    const data = await Promise.resolve(123);
    console.log(data);
};

fun();

现在运行重新打包后的代码,发现已经可以正常输出123了~

去掉polyfill

上面我们介绍了使用usage的时候,是可以不需要polyfill的,下面我们就来去掉polyfill

运行git checkout step-3 && yarn run build查看当前步骤的结果

加入polyfill会导致默认打包的内容把所有的polyfill内容都带上这其实会造成一些额外的问题

  • 代码体积过大,因为我们并不一定会需要所有的polyfill
  • 全局污染,polyfill的很多工具函数都是挂在原型上的,如果是开发一个项目还好,如果是开发一个类库,很可能会对宿主环境的一些对象造成影响

因此我们需要一个运行时的插件,可以做到按需加载需要的工具函数,同时还可以支持只在使用到的运行的地方可以获取到并且不污染全局,这个插件就是@babel/plugin-transform-runtime,同时需要安装@babel/runtime 这个依赖,@babel/plugin-transform-runtime只适用于开发环境,打包之后真正使用的是从babel/runtime中引入的一些runtime tools,并且在开发环境下有一些transform也是需要借助babel/runtime来实现,回到项目中,执行下面的命令:

yarn remove @babel/polyfill
yarn add @babel/plugin-transform-runtime -D
yarn add @babel/transform

然后运行build,发现,报错了,报错信息如下:

Can't resolve 'core-js/modules/es6.object.to-string'
Can't resolve 'core-js/modules/es6.promise'

为啥,因为@babel/runtime是不包含一些新的语法和对象的,如果需要对新API和语法的polyfill,则还需要安装@babel/runtime-corejs2这个依赖,并在plugin-transform-runtime中打开对corejs的使用,运行下面命令:

yarn add @babel/runtime-corejs2

修改babel.config.js

module.exports = {
    presets: [
        [
            '@babel/env',
            {
                useBuiltIns: 'usage'
            }
        ]
    ],
    plugins: [
        [
            '@babel/plugin-transform-runtime',
            {
                corejs: 2
            }
        ]
    ]
};

corejs 常用2个选项,false或者2,false会污染全局的属性,而指定为2则不会污染全局的属性

设置为false,打包中对Promise的使用方式:

./node_modules/core-js/modules/_promise-resolve.js

直接去加载依赖会导致全局的属性被影响,如果设置为2,打包后的结果是:

var _Promise = __webpack_require__(/*! ../core-js/promise */ \"./node_modules/@babel/runtime-corejs2/core-js/promise.js\

是会定义一个当前环境下的变量,之后通过_Promise变量来使用promise,这样就不会影响全局的一些属性,这对于类库的开发是很重要的~

当我们使用了 @babel/runtime-corejs2 后其实是可以去掉@babel/runtime了,但是要确保corejs设置为2,由此也可以看出来:

@babel/runtime-corejs2 约等于 @babel/runtime + polyfill

babel-plugin-transform-runtime 的所有参数如下:

  1. corejs,默认false,指定为数字后,代表使用哪个版本的core-js包
  2. helpers,默认true,开启后,对一些工具函数会在代码顶部,从helper包中导入工具而不是在当前代码中去定义工具函数
  3. regenerator,默认true,generator是否被转译成用regenerator runtime包装,这样的话每个包装函数影响的都是当前的 regenerator,而不是全局的 regenerator。
  4. useESModules,默认false,如果设置为true,则不会转化ES6的import和export等语法,对于明确支持的环境可以开启,但是考虑兼容的话还是不要开启比较好~

更多关于 babel-plugin-transform-runtime,请移步到官网查看

总结

babel的插件有很多,但是 @babel/env 和 @babel/plugin-transform-runtime 是每一个前端开发者都应该了解和掌握的插件,对于别的插件,用到的时候去了解和配置一下应该就可以了

到此,我们了解了babel的一些基础配置,留有一个小问题,我并没有说关于babel的最佳实践,因为在每个不同的场景和项目中,我觉得都应该是不一样的,欢迎留言讨论,你认为的最佳实践是什么

几乎每当有新的标准被制定的同时,都会有对应的babel插件的出现,感谢babel社区,让我们能开心的使用新语法。

下一篇,我们一起手撸一个简单的babel,拥有词法分析,语法分析,转化和生成代码的功能,从底层原理来了解babel,一起期待吧~ 如果当前内容对你有所帮助,请点个赞吧,非常感谢~