坐下来,聊聊babel

什么是babel

官方介绍

Babel 是一个 JavaScript 编译器

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:

  • 语法转换

  • 通过 Polyfill 方式在目标环境中添加缺失的特性(通过第三方 polyfill 模块,例如 core-js,实现)

  • 源码转换 (codemods)

  • 更多资源!(请查看这些 视频 以获得启发)

// Babel 输入: ES2015 箭头函数
[1, 2, 3].map((n) => n + 1);
​
// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) {
  return n + 1;
});

为什么叫编译器?

众所周知,JavaScript是一门解释型语言。他并不像Java一样通过代码编译器和解释器混合模式将代码字节化运行,所以JavaScript本身在浏览器中就是边解释边运行的。那么为什么还需要babel语法编译器?

babel是一门静态语法编译器,他并不是在程序运行时参与工作,而是在程序运行前先执行语法。需要babel的主要原因是ECMA规范的定制和落实的流程繁琐而漫长,在一个规范落地实现前主流浏览器还无法通过内置JavaScript引擎来识别并运行新的代码规范,而babel可以实现将新的代码规范提前在babel中进行编译并将生成的结果输出为指定的ES版本,这样浏览器可以运行通过还未支持的ES规范开发的JavaScript代码。

babel经过了漫长的发展历程并持续针对ECMA的新规范提供对应的实现,而且尽力的实现将新规范的输出范围支持较早的浏览器版本,让开发者可以无需关注JavaScript运行环境的问题。到现在babel已经更新到了第7个大版本,并且从7版本开始彻底与6之前的版本分离。

关于ECMA规范

ECMA组织是一个JavaScript语法的标准化组织,每年都会针对JavaScript语言推出升级的建议和提案。世界各地的主流浏览器厂商和JavaScript引擎提供商都会针对ECMA规范进行落地实现,用以保证无论在任何浏览器中运行JavaScript的运行表现和语法编写总是一致的。

当然以上说明仅仅是理想状态下,由于ECMA组织每一个针对JavaScript的语法提案都需要复杂的流程才能最终敲定,这个过程中需要提交、审核、论证、会议商讨等环节。所以等到一个新的提案敲定之前可能已经经过了很长时间了,这样就意味着等到提案敲定之后进入浏览器厂商手里后,还需要等待浏览器厂商的一个漫长的实现过程。这样的话可能2019年提出的提案实际上在2021年还没有实际的进入浏览器实现。

针对这种情况浏览器厂商也做了他们自己的努力,就是当一个提案已经差不多确定一定会通过的时候,或者这个提案被猜测为未来会通过时,一些浏览器厂商会提前在浏览器中以测试功能的形式进行实现。虽然这是一件好事情,但是综合了以上的描述当任务落到实际工作中的程序员手里时,程序员会变得非常的头疼。

因为不同的提案在落地过程中,有些浏览器可能已经针对其进行实现,有些可能没有。还有一些ECMA标准在提出一段时间后已经被主流的浏览器实现,不过这些新的语法规则仅适用于较新版本的主流浏览器,这样程序员在开发场景中由于需要保证尽量的兼容更早版本以及更全面的浏览器厂商的浏览器,就只能选择继续使用ECMA早期的基础规范进行JavaScript的程序。

针对这种情况,及时ECMA的新规范再好,那么在实际生产中又形同鸡肋了。所以babel就应运而生。babel的核心目的就是对ECMA规范做完整的实现,并针对ECMA的新规范进行早期版本JavaScript的实现,用以保证程序员在项目中集成了babel之后,便无需考虑浏览器版本或规范是否被浏览器支持。只需按照新规范编写代码即可,针对规范的实现交给babel。babel则在程序运行前对代码进行整体的加工,将ECMA最新的规范通过ECMAScript5之前的语法进行输出,或针对程序员指定范围的浏览器版本进行对应代码标准的输出。babel仅仅参与程序的构建和兼容性处理,并不会入侵到程序员编写的代码本身。

不过babel在有些场景中需要通过polyfill技术或core-js技术来对浏览器的一些对象做补偿型的补充。比如在ES6+规范推出之后JavaScript的数组中便包含了forEach、map和fillter等函数形式的遍历方式,浏览器还针对异步任务提供了Promise对象,基于这些新增的API和全局对象,如果想要直接使用新的语法开发对ES6之前版本浏览器的支持,就必须在这些旧版浏览器中对缺失的API和全局对象做补偿。这也是babel的工作流程之一。

ECMAScript的规范经过了多年的演进包含如下:

  1. ES6(2015):新增了class、ES Module、箭头函数、参数默认值、模版字符串、解构赋值、展开运算符、对象属性简写、Promise和let const声明符。

  2. ES7(2016):Array.prototype.includes()和指数操作符。

  3. ES8(2017):async/await、 Object.values()、Object.entries()、String padding、函数参数列表结尾允许逗号、Object.getOwnPropertyDescriptors()、SharedArrayBuffer对象和Atomics对象

  4. ES9(2018) :异步迭代、Promise.finally()、Rest/Spread 属性、正则表达式命名捕获组、正则表达式反向断言和正则表达式dotAll模式

  5. ES10(2019):Array.flat()和Array.flatMap()、String.trimStart()和String.trimEnd()、String.prototype.matchAll、Symbol.prototype.description、Object.fromEntries()和可选 Catch

  6. ES11(2020):Nullish coalescing Operator(空值处理)、Optional chaining(可选链)、Promise.allSettled、import()、新基本数据类型BigInt和globalThis

  7. ES12(2021): replaceAll、Promise.any、WeakRefs、逻辑运算符和赋值表达式和数字分隔符

从ES6规范提出到ES12已经经历了6个年头,所以这六年的过程中迭代的浏览器版本如果都想要运行使用完整ES6到ES12的JavaScript编程的应用程序,那么babel是必不可少的。

babel的编程大法

在浏览器中使用babel

babel在编程使用中有很多种,最简单的方式是通过在浏览器中使用babel提供的browser实现并且配合type为text/babel的script标签中编写新的代码。但是这种方式仅仅适用于学习阶段,如果你在浏览器中直接使用babel运行JavaScript那是完全不适合生产环境的。因为它在执行polyfill时会大量的消耗资源,所以推荐在本地的开发环境中通过Webpack结合babel库来进行代码的处理。

在Webpack中使用babel

babel在Webpack中的使用方式作者在《Webpack从入门到精通01》《Webpack从入门到精通02》以及《Webpack从入门到精通03》三篇文章中具体的介绍并做了详细的说明。可以结合Webpack的文章针对Webpack进行学习。

在NodeJS中使用babel

今天的文章以在Node中使用babel做为针对babel的独立介绍。以上两种方式并不作为今天的主要介绍内容。

1. 准备工作

  1. 在编辑器新创建一个空的文件夹,并且通过npm初始化项目,使用npm init -y命令创建package.json

    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % npm init -y
    Wrote to /Users/zhangyunpeng/Downloads/uni/test/liantong/babel/package.json:
    ​
    {
      "name": "babel",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
    ​
  2. 在根目录创建index.js并在内部编写如下代码:

    console.log('Hello Node!')
  3. 在package.json中创建启动命令

    {
      "name": "babel",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "hello":"node ./index.js"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
  4. 在命令行中运行 npm run hello会输出如下内容

    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % npm run hello
    ​
    > babel@1.0.0 hello
    > node ./index.js
    ​
    Hello Node!
    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % 

2. 测试模块语法

  1. 在根目录下创建model.js并填写如下代码

    // 通过cjs模块导出对象
    module.exports = {
      name:'小明',
      age:18
    }
  2. 在index.js中编写如下代码

    // 导入model模块公开的对象
    const user = require('./model')
    console.log('Hello Node!')
    console.log(user)
  3. 使用npm run hello运行代码,会出现如下结果:

    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % npm run hello
    ​
    > babel@1.0.0 hello
    > node ./index.js
    ​
    Hello Node!
    { name: '小明', age: 18 }
    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % 
  4. 这便是NodeJS的默认CommonJS模块的导出和导入方式

3. 测试ES Module

  1. 在根目录下再次创建model1.js并编写如下代码

    //使用ES Module进行模块导出
    export default {
      name:'小花',
      age:18
    }
  2. 再次更改index.js为如下代码:

    // 导入model1模块公开的对象
    import user1 from './model1.js''
    // 导入model模块公开的对象
    const user = require('./model')
    console.log('Hello Node!')
    console.log(user)
    console.log(user1)
  3. 使用npm run hello运行index.js,此时控制台会出现如下错误

    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % npm run hello
    ​
    > babel@1.0.0 hello
    > node ./index.js
    ​
    (node:34528) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
    (Use `node --trace-warnings ...` to show where the warning was created)
    /Users/zhangyunpeng/Downloads/uni/test/liantong/babel/index.js:2
    import user1 from './model1'
    ^^^^^^
    ​
    SyntaxError: Cannot use import statement outside a module
        at Object.compileFunction (node:vm:352:18)
        at wrapSafe (node:internal/modules/cjs/loader:1025:15)
        at Module._compile (node:internal/modules/cjs/loader:1059:27)
        at Object.Module._extensions..js (node:internal/modules/cjs/loader:1124:10)
        at Module.load (node:internal/modules/cjs/loader:975:32)
        at Function.Module._load (node:internal/modules/cjs/loader:816:12)
        at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
        at node:internal/main/run_main_module:17:47
    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % 
  4. 错误原因是NodeJS会默认以CommonJS的规范来执行JavaScript代码,所以NodeJS的语法与ECMAScript的语法规范不同,Node的模块特色时module.exports和require模式,而ES Module的模块关键字为 export和import。

  5. Node控制台提出了这个问题的解决方案,可以在packpage.json中定义type为module或使用mjs为后缀定义使用ES Module的文件。

  6. 按照这个思路以package.json的修改方案为例子,将其代码修改如下

    {
      "name": "babel",
      "version": "1.0.0",
      "type":"module",//追加模块定义为ES Module模式
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "hello":"node ./index.js"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
    ​
  7. 继续运行npm run hello,新的错误应运而生。

    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % npm run hello
    ​
    > babel@1.0.0 hello
    > node ./index.js
    ​
    file:///Users/zhangyunpeng/Downloads/uni/test/liantong/babel/index.js:4
    const user = require('./model')
                 ^
    ​
    ReferenceError: require is not defined in ES module scope, you can use import instead
    This file is being treated as an ES module because it has a '.js' file extension and '/Users/zhangyunpeng/Downloads/uni/test/liantong/babel/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
        at file:///Users/zhangyunpeng/Downloads/uni/test/liantong/babel/index.js:4:14
        at ModuleJob.run (node:internal/modules/esm/module_job:183:25)
        at async Loader.import (node:internal/modules/esm/loader:178:24)
        at async Object.loadESM (node:internal/process/esm_loader:68:5)
        at async handleMainPromise (node:internal/modules/run_main:63:12)
    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % 
  8. 新的错误出现,这个错误告诉我们当前的js文件中包含了未识别的require关键字,这个关键字时CommonJS规范的模块导入,在ES Module中不支持。想要解决这个问题,可以将文件命名为.cjs结尾,或者将package.json中的type去掉。

5. babel救场

针对这种场景,NodeJS无法处理既包含ES Module模块又包含CommonJS的模块规范,鱼和熊掌的问题来了。为了迎合全新的编程风格,NodeJS中往往也需要大量按照ES规范去进行编程,这样既想保持Node本来的规范,又想使用完整的ECMA规范,就需要将babel追加到Node项目中。

6. 安装必要的依赖

在Node中想要使用babel语法编译器首先需要安装babel的核心库@babel/core,然后为了完美支持全面的JavaScript语法,需要使用@babel/preset-env依赖来配置JavaScript的运行方式和插件管理。最后需要安装babel的执行工具@babel/cli,最后需要安装兼容性处理的补偿包corejs。安装后的package.json如下:

{
  "name": "babel",
  "version": "1.0.0",
  "type": "module",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "hello": "node ./index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.15.7",
    "@babel/core": "^7.15.8",
    "@babel/preset-env": "^7.15.8"
  },
  "dependencies": {
    "core-js": "^3.18.3"
  }
}

7.使用babel大法实现程序的正常运行

  1. 在根目录中创建src文件夹并将所有JavaScript文件移动到文件夹内部坐下来,聊聊babel

  2. 在package.json中修改原始命令,并加入babel构建命令

    {
      "name": "babel",
      "version": "1.0.0",
      "type": "module",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "hello": "node ./src/index.js",//将原有命令路径更改
        "babel-build": "babel src --out-dir lib"//使用babel中的编译整个文件夹命令将当前的src文件夹输出到lib中
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "@babel/cli": "^7.15.7",
        "@babel/core": "^7.15.8",
        "@babel/preset-env": "^7.15.8"
      },
      "dependencies": {
        "core-js": "^3.18.3"
      }
    }
    ​
  3. 在根目录中创建文件名为.babelrc文件并在其中声明如下配置

    {
      "presets": [//将preset-env管理插件安装到babel中
        [
          "@babel/preset-env",
          {
            "useBuiltIns": "usage",
            "corejs":"3"
          }
        ]
      ]
    }
  4. 关于useBuiltIns和corejs的配置方式可以直接参考babel官网,因为这里使用的固定规则原理也很简单所以不多做介绍了。

  5. 接下来在命令行中运行 npm run babel-build,会发现此时并没有报错,这是因为当前的动作只是执行代码的编译并没有执行代码。

    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % npm run babel-build
    ​
    > babel@1.0.0 babel-build
    > babel src --out-dir lib
    ​
    Successfully compiled 3 files with Babel (324ms).
    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % 
  6. 下面查看一下根目录中会出现一个lib文件夹内部会包含如下文件坐下来,聊聊babel

  7. //index.js
    "use strict";
    
    var _model = _interopRequireDefault(require("./model1.js"));
    
    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
    
    // 导入model1模块公开的对象
    // 导入model模块公开的对象
    var user = require('./model');
    
    console.log('Hello Node!');
    console.log(user);
    console.log(_model["default"]);
  8. //model.js
    "use strict";
    ​
    // 通过cjs模块导出对象
    module.exports = {
      name: '小明',
      age: 18
    };
    //model1.js
    "use strict";
    ​
    require("core-js/modules/es.object.define-property.js");
    ​
    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports["default"] = void 0;
    //使用ES Module进行模块导出
    var _default = {
      name: '小花',
      age: 18
    };
    exports["default"] = _default;
  9. 到这里会发现三个文件的代码均有变化,并且输出的内容中已经不存在ES Module的代码了。下面运行一个lib下的index.js。在package.json中增加如下脚本,不要忘记将type:"module"去掉

    {
      "name": "babel",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "hello": "node ./src/index.js",
        "babel-build": "babel src --out-dir lib",
        "lib": "node ./lib/index.js"//追加代码
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "@babel/cli": "^7.15.7",
        "@babel/core": "^7.15.8",
        "@babel/preset-env": "^7.15.8"
      },
      "dependencies": {
        "core-js": "^3.18.3"
      }
    }
  10. 在命令行执行npm run lib,会输出如下信息:

    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % npm run lib
    ​
    > babel@1.0.0 lib
    > node ./lib/index.js
    ​
    Hello Node!
    { name: '小明', age: 18 }
    { name: '小花', age: 18 }
    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % 
  11. 通过babel的编译生成的代码可以正常的运行,并且输出预期的结果。

8. babel针对node的快捷方式

根据第6节的介绍,通过babel解决了代码的编译和运行问题,但是这里出现了一个比较纠结的点。当使用babel进行编译的时候,每次都需要先输出到屋里文件再通过node命令执行屋里文件,这样在程序执行上会比较麻烦,所以babel针对node还有一种解决方案,需要在项目中添加@babel/node依赖:

  1. dev安装@babel/node依赖

  2. 在package.json中增加命令如下

    {
      "name": "babel",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "hello": "node ./src/index.js",
        "babel-build": "babel src --out-dir lib",
        "lib": "node ./lib/index.js",
        "babel-node": "babel-node ./src/index.js"//babel-node运行命令
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "@babel/cli": "^7.15.7",
        "@babel/core": "^7.15.8",
        "@babel/node": "^7.15.8",
        "@babel/preset-env": "^7.15.8"
      },
      "dependencies": {
        "core-js": "^3.18.3"
      }
    }
    ​
  3. 这里使用babel-node命令指定运行src目录下的index.js,代表直接运行混合了ES Module和CommonJS语法的代码。运行npm run babel-node会发现无需生成lib文件内容便可以直接执行index中的代码

    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % npm run babel-node
    ​
    > babel@1.0.0 babel-node
    > babel-node ./src/index.js
    ​
    Hello Node!
    { name: '小明', age: 18 }
    { name: '小花', age: 18 }
    zhangyunpeng@zhangyunpengdeMacBook-Pro babel % 

8. @babel/node说明

@babel/node是babel为node专门提供的命令行工具,它的实际运行原理就是先执行代码的转换和解析,但不生成物理文件,这样就不需要先通过@babel/cli来进行代码的生成后运行node命令。@babel/node将两部操作放在内存中直接处理,这样就相当于直接执行目标文件,并让开发者肉眼上认为NodeJS中可以使用更加多的ECMA语法。

总结

针对babel本文暂时聊到这里,关于使用.browserslist以及其他方面的处理在作者之前的Webpack介绍中已经相对提及了很多所以本文并不会做介绍。关于babel的其他属性和依赖的使用有兴趣的同学可以自行去官方文档学习。babel在现代前端开发中起着举足轻重的地位,所以希望正在前端路上修行的同学将它重视起来并且能深入的学习这个工具,这样可以让大家对工程化的前端开发中更上一层楼。

上一篇:webpack是怎么打包的,babel又是什么?


下一篇:项目实战--在线教育--经验总结(二)