必知必会的babel基础

什么是babel

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

babel的核心原理

对代码进行如下步骤:html

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

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

固然,babel相关的工具不少,下面咱们以babel7为例,边用边分析。java

构建项目

若是你想掌握babel的基础配置,请务必跟上一块儿操做node

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

git clone github.com/jinggk/babe…git

初始化项目

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

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

测试代码以下:web

// 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 (ES201五、ES2016 等)特性
};
复制代码

添加babel相关的依赖

yarn add babel-loader @babel/core @babel/preset-env -D
复制代码

@babel/core 包含了转换api的全部核心模块 @babel/preset-env 一组默认的预设插件的集合,所包含的插件将支持全部最新的 JavaScript (ES201五、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,一块儿期待吧~ 若是当前内容对你有所帮助,请点个赞吧,很是感谢~

相关文章
相关标签/搜索