• 前端利器躬行记(2)——Babel


      Babel是一个JavaScript编译器,不仅能将当前运行环境不支持的JavaScript语法(例如ES6、ES7等)编译成向下兼容的可用语法(例如ES3或ES5),这其中会涉及新语法的转换和缺失特性的修补;还支持语法扩展,从而能随时随地的使用JSX、TypeScript等语法。目前最新版本是7.4,自从6.0以来,Babel被分解的更加模块化,各种转译功能都以插件的形式分离出来,可按自己的需求,灵活配置。

      在7.0版本中,对Babel的包做了一次大调整,统一改成域级包,将原先以“babel-”为前缀的包迁移到@babel的命名空间,例如@babel/core、@babel/cli等。这种模块化的设计,既能区分是否是官方发布的,也能避免命名冲突。

    一、@babel/core

      如果要以编程的方式使用Babel,那么可以通过@babel/core实现,安装命令如下所示。

    npm install --save-dev @babel/core

      在引入该包后,就能在JavaScript文件中直接编译代码、文件或AST。以编译代码为例,可选择的方法有三个,如下所示。

    var babel = require("@babel/core");
    babel.transform(code, options, function(err, result) {
      console.log(result);                   // => { code, map, ast }
    });
    babel.transformSync(code, options)        // => { code, map, ast }
    babel.transformAsync(code, options)       // => Promise<{ code, map, ast }>

      虽然transform()用于异步编译,transformSync()用于同步编译,但是它们得到的结果相同,都是一个由转换后的代码(code)、源码映射信息(map)和抽象语法树(ast)组成的对象。而transformAsync()得到的结果与前面两个方法不同,它返回一个包含code、map和ast的Promise对象。这些方法的第二个options参数,可用于传递配置信息,具体可参考官方文档

      如果要编译文件或AST,那么也有类似的三个方法可供选择。除了能编译之外,利用这个包还能把代码解析成AST(即传入一段代码,得到一个AST)以便其它插件对其进行语法分析,可使用的方法有parse()、parseSync()和parseAsync()。

    二、@babel/cli

      @babel/cli是一个Babel内置的命令行工具,可通过命令来编译文件,其安装如下所示。

    npm install --save-dev @babel/cli

      假设有一个src.js文件,其内容如下代码所示,用到了ES6中的箭头函数。

    let fn = () => true;

      在运行下面的命令后,可以看到输出和输入的代码相同,这是因为此时还未指定任何转译插件。

    ./node_modules/.bin/babel src.js

      如果当前的npm版本是5.2以上,那么可将./node_modules/.bin缩短为npx,如下所示。

    npx babel src.js

      在babel中,有多个可用的参数,下面只列举其中的四个,其余参数的用法可参考官方文档

    npx babel src --out-dir dist
    npx babel src.js --out-file dist.js
    npx babel src.js --out-file dist.js --plugins=@babel/plugin-transform-classes,@babel/transform-modules-amd
    npx babel src.js --out-file dist.js --presets=@babel/preset-env,@babel/flow

      (1)“--out-dir”参数可编译整个src目录下的文件并输出到dist目录中。

      (2)“--out-file”参数可编译src.js文件并输出到dist.js文件中。

      (3)“--plugins”参数可指定编译过程中要使用的插件,多个插件用逗号隔开。

      (4)“--presets”参数可指定编译过程中要使用的预设,多个预设用逗号隔开。

      在package.json文件中,可以通过scripts字段声明脚本命令。如果想要简化babel命令,那么可以将它们迁移到scripts字段中,如下所示。

    {
      "scripts": {
        "compile": "npx babel src.js --out-file dist.js"
      }
    }

      现在要编译目录的话,只要执行下面的这条npm命令即可。

    npm run compile

    三、配置

      在Babel中,可以将各种命令的参数集中到一个配置文件中,而可配置的文件包括babel.config.js、.babelrc和package.json。

    1)babel.config.js

      这是Babel 7最新引入的配置文件,存在于根目录中(即package.json文件所在的目录)。它不仅能以编程的方式声明全局生效的配置参数,还能利用overrides字段对不同的子目录进行针对性的配置,从而就能避免为相关目录创建一个.babelrc文件了。在下面的示例中,为test目录单独配置了预设(presets)。

    module.exports = {
      presets: [...],
      overrides: [{
        test: ["./test"],
        presets: [...]
      }]
    };

      当运行下面两条命令进行编译时,第一条读取的是最外层的预设,第二条读取的是overrides中的预设。

    npx babel src.js
    npx babel ./test/src.js

    2).babelrc

      babel.config.js并不是.babelrc的替代品,.babelrc文件用于局部配置(如下代码所示),可放置于所有目录中。如果当前目录没有.babelrc文件,那么就会往上查找直至找到为止。

    {
      "presets": [...]
    }

      注意,如果.babelrc的后缀是“.js”(即.babelrc.js),那么在文件中可以通过JavaScript配置参数。

    3)package.json

      在package.json文件中,可以声明一个babel字段,其值就是.babelrc文件中的配置参数,如下所示。

    {
      "babel": {
        "presets": [...]
      }
    }

      注意,不能让.babelrc和声明过babel字段的package.json处在相同的目录中。

    4)配置函数或方法

      前面三种都是用单独的文件来配置Babel的参数,其实还可以通过相关的函数或方法来达到相同地目的。在下面的示例中,引入gulp-babel包后就能通过得到的babel()函数来配置Babel的参数。

    let babel = require("gulp-babel");
    babel({
      presets: [...]
    });

    四、插件

      Babel的编译可分为三个阶段:解析、转换和生成,而插件(Plugin)在转换过程中起到了至关重要的作用。借助Babel的插件可将解析完成的AST按照特定的要求进行处理,然后再输出生成的代码。

    1)插件类型

      Babel中的插件分为两种类型:语法和转换。语法插件只允许Babel解析成特定类型的语法,例如JSX、Flow等。转换插件常用来编译ES6、ES7、React和Modules等,由于能自动开启相应的语法插件,因此不用显式的指定两种插件。以之前的箭头函数为例,如果要将其编译成所有浏览器都能识别的形式,那么就得安装相应的插件,如下所示。

    npm install --save-dev @babel/plugin-transform-arrow-functions

      在.babelrc文件中的配置如下所示。

    {
      "plugins": ["@babel/plugin-transform-arrow-functions"]
    }

      在配置文件中,不仅能指定插件的相对或绝对路径,还可省略插件名称中的“babel-plugin-”前缀。假设有个插件名叫@org/babel-plugin-name,那么它的相对路径和简写名称如下所示。

    {
      "plugins": [
        "../@org/babel-plugin-name",
        "@org/name"
      ]
    }

    2)执行顺序

      插件的执行顺序会受配置时所处的位置的影响,具体规则如下所列,其中预设是指官方预先设计的一组插件集,本质上仍然是插件。

    (1)插件执行在预设之前。

    (2)插件会按顺序从前往后执行。

    (3)预设与插件相反,从后往前执行。

      以下面的配置信息为例,在插件中,先执行@babel/plugin-transform-arrow-functions,后执行@babel/plugin-transform-classes。而在预设中,先执行@babel/preset-react,后执行@babel/preset-env。

    {
      "plugins": ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-classes"],
      "presets": ["@babel/preset-env", "@babel/preset-react"]
    }

    3)插件参数

      如果要配置插件的参数,那么得在插件名称后增加一个参数对象,并且写成数组的形式,如下所示。

    {
      "plugins": [
        ["@babel/plugin-transform-arrow-functions", { "spec": true }], 
        ["@babel/plugin-transform-classes", { "loose": true }]
      ]
    }

      预设也能接收参数,其写法与之类似。

    五、预设

      如果在配置文件中一个一个的声明插件,那么不仅会让该文件变得巨大,而且还难免会有所遗漏。官方为了避免此类问题,引入了预设(Preset)的概念。预设类似于生活中的套餐,每个套餐会集合不同的插件,从而能够一次性安装各类插件,并且还可共享配置的参数。

    1)@babel/preset-env

      此预设不仅集合了最新的ES语法(即编译指定的ES标准),还能配置所要支持的平台(例如Node、Chrome等)和版本,以及按需加载插件,其安装命令如下所示。

    npm install --save-dev @babel/preset-env

      下面是一个预设的配置示例,支持Chrome 58和IE 11,以及超过市场份额5%的浏览器。还有许多其它可供选择的配置参数,具体可翻阅官方文档

    {
      presets: [
        [
          "@babel/preset-env",
          {
            targets: {
              chrome: "58",
              ie: "11",
              browsers: "> 5%"
            }
          }
        ]
      ]
    }

      除了@babel/preset-env之外,官方还给出了其它实用的预设,例如@babel/preset-flow、@babel/preset-react和@babel/preset-typescript等。

      与插件类似,预设也能指定相对或绝对路径,只不过它省略的前缀是“babel-preset-”。假设有个预设名叫@org/babel-preset-name,那么它的相对路径和简写名称如下所示。

    {
      "presets": [
        "../@org/babel-preset-name",
        "@org/name"
      ]
    }

    2)stage-x

      这是一组实验性质的预设,囊括了处于提案阶段的标准,TC39委员会将提案分为5个阶段,如表2所示。

    表2  提案的五个阶段

    阶段 描述 预设
    Stage 0 设想(Strawman),由TC39的成员或已经注册成TC39贡献者的会员提出的想法 @babel/preset-stage-0
    Stage 1 建议(Proposal),产生一个正式的提案,确定提案负责人 @babel/preset-stage-1
    Stage 2 草案(Draft),规范的第一个版本,包含新特性的语法和语义,接下来只接受增量修改 @babel/preset-stage-2
    Stage 3 候选(Candidate),完成规范,获得用户反馈,接下来只有出现重大问题才做修改 @babel/preset-stage-3
    Stage 4 完成(Finished),将新规范纳入到标准中,准备发布 不存在,因为它就是已发布的标准预设,例如ES6、ES7等

      以上4个阶段的预设存在着依赖关系,阶段靠前的依赖阶段靠后的(即数字小的包含数字大的),例如@babel/preset-stage-0依赖@babel/preset-stage-1。

      由于这些提案阶段的预设不太稳定,很有可能会被TC39委员会除名或变更,并且混合使用在配置上容易出错,因此从Babel 7开始,它们都将被废弃。

    六、@babel/polyfill

      虽然env预设(@babel/preset-env)能统一JavaScript的新语法(即高版本编译成低版本),但是无法支持内置的新方法或新对象,例如Promise、Array.of()等。为此,Babel引入了的Polyfill技术(全部打包在@babel/polyfill中),将所缺的特性添加到全局对象中或内置对象的原型上,弥补env预设的不足,从而模拟出完整的ES6+语法和特性。

      @babel/polyfill包含两个模块:regenerator-runtime和core-js,前者用于编译生成器与异步函数(async和await),后者用于处理其它兼容性问题。@babel/polyfill的安装命令如下所示,注意,使用的参数是--save而不是--save-dev,因为需要在源码之前先执行Polyfill。

    npm install --save @babel/polyfill

    1)useBuiltIns

      在env预设中,存在一个与@babel/polyfill有紧密联系的useBuiltIns参数,它有三个关键字可供选择,分别是false、entry和usage,具体说明如下所列。

      (1)false:默认值,不开启Polyfill,显式的配置如下所示。

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

      (2)entry:加载运行环境(可在targets参数中声明)所需的Polyfill,下面是一个使用Promise的例子。

    require("@babel/polyfill");
    new Promise();

      当useBuiltIns参数的值为entry时,这段代码在编译后,就会引入许多不相干的JavaScript文件,造成资源的浪费,如下所示。

    require("core-js/modules/es6.promise");
    require("core-js/modules/es6.array.fill");
    require("core-js/modules/es6.math.trunc");
    require("core-js/modules/es6.string.fixed");
    ......
    new Promise();

      (3)usage:自动加载源码所需的Polyfill,仍然以Promise为例,如下代码所示,不用再显式的引入@babel/polyfill。

    new Promise();

      当useBuiltIns参数的值为usage时,这段代码在编译后,就会只引入需要的JavaScript文件,如下所示。

    require("core-js/modules/es6.promise");
    require("core-js/modules/es6.object.to-string");
    new Promise();

    七、@babel/runtime

      @babel/runtime与@babel/polyfill类似,也是用来增强env预设的,只是它以非侵入式的辅助函数来填补平台所没有的特性,其安装命令如下所示。

    npm install --save @babel/runtime

      在Babel 7中还引入了一个@babel/runtime-corejs2,它比@babel/runtime多包含一个core-js模块,即能够编译Promise、Symbol等。接下来以ES6的类为例,如下所示。

    class People {
      name() {}
    }

      在将People类编译后,得到的结果如下所示,省略了三个函数中的逻辑代码。

    "use strict";
    function _classCallCheck(instance, Constructor) { }
    function _defineProperties(target, props) { }
    function _createClass(Constructor, protoProps, staticProps) { }
    
    var People =
    /*#__PURE__*/
    function () {
      function People() {
        _classCallCheck(this, People);
      }
      _createClass(People, [{
        key: "name",
        value: function name() {}
      }]);
      return People;
    }();

    1)@babel/plugin-transform-runtime

      由于编译生成的辅助函数会滞留在所使用的文件中,因此文件越多冗余的函数就越多。如果人工分离,那工作量将巨大,因此Babel提供了能自动将它们分离的@babel/plugin-transform-runtime插件,其安装命令如下所示。

    npm install --save-dev @babel/plugin-transform-runtime

      仍然以ES6的People类为例,先在配置文件中将其声明,如下所示。

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

      然后再将类编译,三个辅助函数就能通过引入的方式得到,如下所示。

    var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
    var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
    var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

    八、动态编译

      在Babel中有两种方式实现动态编译,分别是@babel/register和@babel/node,它们的安装命令如下所示。

    npm install --save-dev @babel/register
    npm install --save-dev @babel/node

      虽然动态编译省去了手动操作,但是会损耗程序的速度和性能,因此不适合生产环境。

    1)@babel/register

      在将其安装后,就会为Node.js的require()函数增加一个钩子。每当通过require()加载后缀为.es6、.es、.jsx、.mjs或.js的文件时,就能自动对其进行Babel编译。接下来用一个例子来演示@babel/register的用法,首先创建一个src.js的文件,其代码如下所示。

    module.exports = () => true;

      然后创建一个default.js文件,引入@babel/register和之前的src.js,如下所示。

    require("@babel/register");
    var code = require("./src.js");
    console.log(code.toString());

      最后在命令行工具中输入“node default.js”,打印出编译后的匿名函数,如下所示。

    function () {
      return true;
    }

      在@babel/register中,如果需要使用Polyfill,那么得逐个引入。还要注意,它默认不会编译node_modules目录中的模块,但是可以通过ignore参数修改忽略规则来覆盖其行为。

      ignore参数是一个由正则表达式和函数组成的数组(如下代码所示),如果要让文件不被编译,那么得将其路径与正则表达式匹配或函数返回true,其中函数的参数就是文件路径。

    require("babel-register")({
      ignore: [
        /regex/,
        function(filepath) {
          return true;
        }
      ]
    });

      另外还有一个only参数,其值也是一个数组,同样也用来设置文件不被编译的条件。只是其规则与ignore参数正好相反,即路径与正则表达式不匹配或函数返回false时,才不编译。

      @babel/register还可以接收其它参数,例如extensions、cache等,并且会合并配置文件中的信息,作为其参数传递进来。

    2)@babel/node

      在Babel 7之前,@babel/cli会自带babel-node命令,但之后,官方将其拆成一个单独的包:@babel/node。与@babel/register不同,@babel/node不需要调整源码,直接一个命令就能实现动态编译,如下代码所示,其中src就是之前的src.js文件。

    npx babel-node src
  • 相关阅读:
    排序算法分析
    图论算法小结
    A*算法
    分支界限法的应用
    图的搜索策略
    最大二分匹配
    C++学习笔记(1)
    vscode简单c语言多文件编译
    c语言变量大小
    十大排序算法总结
  • 原文地址:https://www.cnblogs.com/strick/p/10984632.html
Copyright © 2020-2023  润新知