What is Babel?

Babel is a toolchain for converting code written in ECMAScript 2015+ syntax to backward-compatible JavaScript syntax so that it can run in current and older browsers or other environments. We use Babel to do a few things:

  • Syntax conversion (es-higher -> es-lower);
  • Handle features that cannot be transformed in the target environment with a Polyfill (viacore-jsImplementation);
  • Source conversion (codemods,jscodeshift);
  • Static analysis (lint, based on commentsAPIDocuments, etc.);

Babel really can do whatever he wants! 😎 😎 😎

Babel7 Minimum configuration

Did you ever feel “hurt by your first love” when you first played Babel? If so, check out this section, it will keep you hooked.

Before we taste the “taste of first love”, let’s make some preparations:

Create a new directory babel-test and create a package.json file:

mkdir babel-test && cdBabel-test // This article uses yarn init -yCopy the code

Install @babel/core, @babel/cli:

yarn add @babel/core @babel/cli -D
Copy the code

Everything is ready, now just buy a plate of 🌰, you can hold her hand:

// Create a new index.js file in the root directory and type 🌰 below
let{ x, y, ... z } = {x: 1.y: 2.a: 3.b: 4 };
console.log(x); / / 1
console.log(y); / / 2
console.log(z); // { a: 3, b: 4 }
Copy the code

First love, you blush when you meet their hair, and you want to see their reaction, right? So execute the following command and see what happens:

NPX Babel./index.js --out-file build.jsCopy the code

After executing the above command, it will output a build.js file in the root directory.

let{ x, y, ... z } = {x: 1.y: 2.a: 3.b: 4
};
console.log(x); / / 1

console.log(y); / / 2

console.log(z); // { a: 3, b: 4 }

Copy the code

What the xxx? This ** isn’t just formatting! Consistency on IE10, another sign of sleepless nights:

Is it surprising or not? Caniuse a check, I nima, which * force to use the extension operator ah, do not know that we should be compatible with IE ah!

But as fierce pursuers, how can we give up just because the other side recoiled! Go to the Babel plugins page and see what plugins you need to handle the extension operator – you can see that this is an ES2018 feature available with the @babel/ plugin-proposal-object-Rest-spread plugin.

Blunt! Hold out your dark hand once more. Create a file named babel.config.json (.babelrc or babel.config.js, depending on your scenario) in the project root directory (where package.json files are located). For the file, see Configuring Babel) and enter the following:

// Go to the terminal and run yarn add@babel /plugin-proposal-object-rest-spread -d to install the dependency first
{
    "plugins": ["@babel/plugin-proposal-object-rest-spread"]}Copy the code

Then execute:

npx babel ./index.js --out-file build.js
Copy the code

Then open the build.js file again, and you can see that the extension operator is no longer visible:

function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; }}return target; }

function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }

let _x$y$a$b = {
  x: 1.y: 2.a: 3.b: 4
},
    {
  x,
  y
} = _x$y$a$b,
    z = _objectWithoutProperties(_x$y$a$b, ["x"."y"]);

console.log(x); / / 1

console.log(y); / / 2

console.log(z); // { a: 3, b: 4 }

Copy the code

Refresh Internet Explorer and open F12 to see the debugger panel:

She withdrew her hand again. Did it hurt? But as a man wearing a red scarf and a small red flower on his head, I will never be discouraged! Look at the location of the error code and recognize that IE does not even support deconstructive assignments. For the same procedure, check caniuse and @babel/plugin-transform-destructuring (tip: click on it to go to the corresponding page!).

This time, go hold her hand again, gogogo:

// Go to the terminal and enter yarn add@babel /plugin-transform-destructuring -d to install dependencies first
{
    "plugins": [
        "@babel/plugin-proposal-object-rest-spread"."@babel/plugin-transform-destructuring"]}Copy the code

After the installation, compile it again, and you can see the generated code as follows:

function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; }}return target; }

function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }

let _x$y$a$b = {
  x: 1.y: 2.a: 3.b: 4
},
    x = _x$y$a$b.x,
    y = _x$y$a$b.y,
    z = _objectWithoutProperties(_x$y$a$b, ["x"."y"]);

console.log(x); / / 1

console.log(y); / / 2

console.log(z); // { a: 3, b: 4 }

Copy the code

Look again at IE’s response:

God waits, IE became, you also hand in hand success!

If you’re careful, both of these Babel plugins have a NOTE:

NOTE: This plugin is included in @babel/preset-env

What does that mean? The girl say hope you next time courage again big point, can pull on and then do not put, why want to try many times!

This leads to @babel/ presets -env, and the document uses the presets field of the configuration file on the wrapper. Then remove the first two plugins.

yarn remove @babel/plugin-transform-destructuring @babel/plugin-proposal-object-rest-spread

yarn add @babel/preset-env -D
Copy the code

Babel.config. json:

{
  "presets": [["@babel/preset-env"]]}Copy the code

Then execute the build command again, and you can see that the output of the build.js file is the same!

And wonder at the same time:

  • Why does a preset satisfy the conversion requirement? How does it do that?
  • BabelHow do you know I want to support itIEBrowser, if I only useChrome, isn’t this conversion redundant? Babel is not only used in browsers, but also in desktop and Node scenarios. How does Babel precisely control the conversion?

Before answering the above question, it occurred to me that while reviewing code at my company, I saw that many children’s shoes were dominated by TypeScript in order to use TypeScript (for example, where AnyScript got its name). Hope to think from the birth background and function of technology, tools and framework itself! Why use it if you don’t have more elaborate type declarations and restrictions?

Babel6 to Babel7:

  • abandonedstage-xes20xxpresetTo change topreset-envplugin-proposal-xx; This gives you more control over the features you need to support;
  • Preset -env relies on Browserslist, Compat-table, and electron-to- Chromium for fine feature control.

compat-table

This library maintains support for each feature in different environments. Take a look at the deconstructed assignment support used above:

{
  name: 'destructuring, declarations'.category: 'syntax'.significance: 'medium'.spec: 'http://www.ecma-international.org/ecma-262/6.0/#sec-destructuring-assignment'.mdn: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment'.subtests: [{name: 'with arrays'.exec: function(){/* var [a, , [b], c] = [5, null, [6]]; return a === 5 && b === 6 && c === void undefined; * /},
      res: {
        tr: true.babel6corejs2: babel.corejs,
        ejs: true.es6tr: true.jsx: true.closure: true.typescript1corejs2: true.firefox2: true.opera10_50: false.safari7_1: true.ie11: false.edge13: edge.experimental,
        edge14: true.xs6: true.chrome49: true.node6: true.node6_5: true.jxa: true.duktape2_0: false.graalvm19: true.graalvm20: true.graalvm20_1: true.jerryscript2_0: false.jerryscript2_2_0: true.hermes0_7_0: true.rhino1_7_13: true}}Copy the code

Ie11 is false, no wonder the first hand failed.

browserslist

This package should be familiar enough that you can query the specific list of browsers. Let’s install this package and run with it:

Yarn add browserslist -d // The query conditions are different. Specific reference https://github.com/browserslist/browserslist#queries NPX browserslist "> 0.25%, Not dead" and_CHR 91 and_FF 89 and_UC 12.12 Android 4.4.3-4.4.4 Chrome 91 Chrome 90 Chrome 89 Chrome 87 Chrome 85 Edge 91 Firefox 89 Firefox 88 IE 11 IOS_SAF 14.5-14.7 iOS_SAF 14.0-14.4 iOS_SAF 13.4-13.7 op_mini all Opera 76 Safari 14.1 Safari 14 Safari 13.1 Samsung 14.0 Samsung 13.0Copy the code

With the above two packages, the implementation of preset-env features fine control is not sprinkling water. Let’s go ahead and change the starting 🌰 to no need to support IE11 and try:

{
  "presets": [["@babel/preset-env",
        {
    		"targets": {
                "chrome": 55}}]]}Copy the code

Preset -env Controls the browser version by configuring the targets field. Build it and see the results:

"use strict";

function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; }}return target; }

function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }

let _x$y$a$b = {
  x: 1.y: 2.a: 3.b: 4
},
    {
  x,
  y
} = _x$y$a$b,
    z = _objectWithoutProperties(_x$y$a$b, ["x"."y"]);

console.log(x); / / 1

console.log(y); / / 2

console.log(z); // { a: 3, b: 4 }

Copy the code

As you can see from the source code above, the extension operator is converted, but the destruct assignment is not. The properties of the converted by module _objectWithoutProperties and _objectWithoutPropertiesLoose defines two methods. What if I have two files that use the extension operator and then output one file? Create a new index2.js file in the root directory:

let{ x, y, ... z } = {x: 1.y: 2.a: 3.b: 4 };
console.log(x); / / 1
console.log(y); / / 2
console.log(z); // { a: 3, b: 4 }
Copy the code

Then execute the following two commands:

npx babel ./index.js ./index2.js --out-file build.js
Copy the code

The result is _objectWithoutProperties and the declaration of _objectWithoutPropertiesLoose incredibly is repeated twice. This is a feature that needs to be converted, and I use it a lot. Doesn’t the converted output explode? At this point you need a plugin to control the amount of code — @babel/ plugin-transform-Runtime. For this conversion function, in the external modularity, the use of the place can be directly introduced. Speaking:

// Install the @babel/plugin-transform-runtime package yarn add @babel/plugin-transform-runtime -d firstCopy the code

Then configure Babel:

{
    "presets": [["@babel/preset-env",
            {
                "targets": {
                    "chrome": "55"}}]],"plugins": [
        "@babel/plugin-transform-runtime"]}Copy the code

Executing the build command above yields the following results:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));

let _x$y$a$b = {
  x: 1.y: 2.a: 3.b: 4
},
    {
  x,
  y
} = _x$y$a$b,
    z = (0, _objectWithoutProperties2.default)(_x$y$a$b, ["x"."y"]);
console.log(x); / / 1

console.log(y); / / 2

console.log(z); // { a: 3, b: 4 }

let _a$b$c = {
  a: 1.b: 2.c: 3
},
    {
  a,
  b
} = _a$b$c,
    c = (0, _objectWithoutProperties2.default)(_a$b$c, ["a"."b"]);

Copy the code

Compared to the above conversion results, this conversion result is much simpler. And function declarations are imported externally.

Take a look at the following code:

const a = [1.2.3.4.6];
console.log(a.includes(7))
Copy the code

@babel/compat-data shows the compatibility of includes:

"es7.array.includes": {
    "chrome": "47"."opera": "34"."edge": "14"."firefox": "43"."safari": "10"."node": "6"."ios": "10"."samsung": "5"."electron": "0.36"
}
Copy the code

Chrome 47+ supports the array includes API. We change targets in babel.config.json to 45 and then execute the conversion command.

"use strict";

var a = [1.2.3.4.6];
console.log(a.includes(7));
Copy the code

It can be concluded that while array.prototype. inlcudes is not supported, Babel does not convert instance methods by default. This is where the @babel/ Polyfill patch comes in. (⚠️ installing polyfill is dependency! In a production environment, the shim is executed before your code.

Import all polyfills in a project entry file or in an entry packaging tool such as WebPack:

// app.js
import '@babel/polyfill';

// webpack.config.js
module.exports = {
  entry: ["@babel/polyfill"."./app/js"]};Copy the code

We don’t need many of these features, so can we also use broswer Targets and the functions used in the code to customize the shims? Choose your own shim and generate a CDN URL. It can be directly introduced in the project, which can be used for micro websites, for large projects, it is impossible to choose a method of their own. This leads to the useBuiltIns configuration, which defines what @babel/preset-env does with washers. The optional values are:

  • usage: Features referenced by each file;
  • entry: All entrances are introduced;
  • false: Do not introduce.

// index.js
const a = [1.2.3.4.6];

console.log(a.includes(7))

new Promise(() = > {})
Copy the code

Then change the Babel configuration to the following:

{
    "presets": [["@babel/preset-env",
            {
                "targets": {
                    "chrome": "45"."ie": 11
                },
                "useBuiltIns": "usage"."corejs": 3}]],"plugins": [
        "@babel/plugin-transform-runtime"]}Copy the code

Babel./index.js –out-file build.js

"use strict";

require("core-js/modules/es.array.includes.js");

require("core-js/modules/es.object.to-string.js");

require("core-js/modules/es.promise.js");

var a = [1.2.3.4.6];
console.log(a.includes(7));
new Promise(function () {});

Copy the code

Niubility!!! Specific core-JS spacers have been introduced for unsupported features. How does this work? Again, this is thanks to the AST, which allows you to make super detailed, on-demand references to the actual situation in your code. If you are interested in children’s shoes, check out the way Core-JS and Babel work together.

summary

Through 🌰 to analyze the Babel7 minimum optimal configuration generation step by step, which also involves some write configuration in the non-aware processing mechanism, such as compat-table, Browserslist. After reading this section, you should have a clear understanding of babel7 configuration methods.

@ Babel series package

Babel is a Monorepo project with 146 packages under packages. Has conjured! Although there are many packages, we can divide them into several categories:

@babel/helper-xx has 28 and @babel/plugin-xx has 98. There are only 20 toolkits and integration packages left. Let’s pick some interesting packages to see what they do.

@babel/standalone

Babel-standalone Provides a standalone build of Babel for browsers and other non-Node environments, such as the online IDE: JSFiddle, JS Bin, and Babel’s official try It Out are all based on this package. Let’s play, too

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>standalone</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.0.0-beta.3/babel.min.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
    <div id="app"></div>
    <script type="text/babel">
        const codeStr = Const getMessage = () => "Babel, do whatever you want "; `;
        const code = Babel.transform(codeStr, { presets: ["env"] }).code;
        document.querySelector('#app').innerHTML = code;
    </script> 
</body>
</html>
Copy the code

Run the above code directly in the browser, and you get the following code:

"use strict"; var getMessage = function getMessage() { return "Babel, do whatever you want."; };
Copy the code

The result is the same as if it were built in the Node environment.

@babel/plugin-xx

Anything that satisfies this tag is a Babel plug-in. It is mainly used to enhance the transform and parser capabilities. An 🌰 :

// index.js
const code = require("@babel/core").transformSync(
    `var b = 0b11; var o = 0o7; const u="Hello\\u{000A}\\u{0009}!" ; `
).code;

console.log(code)
Copy the code

Run node index.js and return:

var b = 0b11;
var o = 0o7;
const u = "Hello\u{000A}\u{0009}!";
Copy the code

Returning it as is, if I want to recognize binary integers, hexadecimal integers, Unicode string literals, newlines, and tabs, I need to add @babel/plugin-transform-literals. The execution result is as follows:

var b = 3;
var o = 7;
const u = "Hello\n\t!";
Copy the code

Through the above Demo, you can understand the function of plugin.

Opening Babel/Packages, we can see that there are three main types of plugins:

  1. babel-plugin-transform-xx: Conversion plug-in, mainly used to enhance the conversion capability, above@babel/plugin-transform-literalsIt belongs to this;
  2. babel-plugin-syntax-xxSyntax plugins that extend compilation capabilities, such as using await outside async function scope, if not introduced@babel/plugin-syntax-top-level-awaitThere is no way to compile toASTOf the tree. And will be submitted to theUnexpected reserved word 'await'This kind of mistake.
  3. Babel-plugin-proposal-xx: Used to compile and transform properties in a proposal. You can see these plug-ins in the Plugins List, such as class-properties and decorators.

summary

By simply understanding the Babel-standalone and Babel-Plugin-XX series packages, you can only sigh that the Babel ecosystem is so strong that you can do whatever you want. There are also some low-level packages that we have been exposed to before, such as @babel/core, @babel/ Parser, @babel/ Generator, @babel/code-frame, etc. I’ll write a plugin to see how these packages work.

Writing a plugin

This section is an advanced part of Babel, so continue with a cup of tea 🍵.

Here’s the need (fun…) , I have the following code:

// Assume that spliceText is a global function that is used extensively
function spliceText (. args) {
  return args[0].replace(/(\{(\d)\})/g.(. args2) = > {
    return args[Number(args2[2]) + 1]
  })   
}

spliceText('I have a little {0}, I never {1}'.'donkey'.'ride')    // There was a little donkey, which I never rode
spliceText('My name is {0}. I am {1} years old. My specialty is {2}.'.'xiao yu'.18.'sleep') // My name is Xiao Yu. I am 18 years old. My special skill is sleeping
spliceText('Funny soul')    // Interesting soul
Copy the code

Because the company code specification says that the maximum number of parameters can not exceed 2. I’ll change the function definition first:

function spliceTextCopy (str, obj) {
    return str.replace(/(\{(\d)\})/g.(. args2) = > {
      return obj[args2[2]]})}Copy the code

Change the usage mode to the second parameter passobject:

spliceTextCopy('I have a little {0}, I never {1}', {0: 'donkey'.1: 'ride'})    // There was a little donkey, which I never rode
spliceTextCopy('My name is {0}. I am {1} years old. My specialty is {2}.', {0: 'xiao yu'.1: 18.2: 'sleep'}) // My name is Xiao Yu. I am 18 years old. My special skill is sleeping
spliceTextCopy('Funny soul')    // Interesting soul
Copy the code

As mentioned above, spliceText is used a lot, so I don’t want to change it one by one. Now let’s try using Babel Transform.

Babel Workflow classic diagram:

Analysis of the

According to the figure above, let’s sort out the logic of requirements:

  1. Use firstastexplorerView the generated abstract syntax tree@babel/parserThe result of treatment;
  2. use@babel/traverseTraverse the syntax tree to find the satisfactionThe name of the function is spliceTextFunction call expressions (CallExpression);
  3. To do the transformation of the node, willCallExpressionargumentsField to do conversion, change the function name;
  4. use@babel/generatorGenerate the final code;

The test case

The Babel-plugin-Tester test suite is based on the jEST package, so you need to install the JEST tool for your project.

yarn add jest -D
Copy the code

Once the test cases are written, we can begin our coding journey. First write the use case according to the requirements above:

import plugin from '.. /plugin/index';
import pluginTester from 'babel-plugin-tester';

pluginTester({
    plugin: plugin,
    tests: {
        'no-params': {
            code: 'spliceText(' interesting soul ')'.snapshot: true
        },
        'has-params': {
            code: 'spliceText(' I have a little {0}, I never {1}',' donkey ', 'ride ')'.snapshot: true}}})Copy the code

⚠️ Explain why snapshot tests are used here? The output from Babel-plugin-Tester changes single quotes to double quotes, but the result after @babel/core transformFromAst does not change this. It didn’t meet my expectations. So take a snapshot of the results. This problem does not affect our results. We will find out the cause later. With the test case in hand, let’s run it with jest:

npx jest --watchAll
Copy the code

coding

Before coding, you can learn how to create a plug-in by using the Babel plug-in manual 😆

First put up the shelves:

const { declare } = require('@babel/helper-plugin-utils');

module.exports = declare((api, options) = > {
    api.assertVersion(7);
    
    return {
        name: 'my-test-plugin',
      	pre (file) {
          
        },
        visitor: {
            
        },
        post (file) {
          
        }
    };
});
Copy the code

Pre and POST are called at the start and end of the traversal, and are generally used as data cache when accessing nodes. Visitor visits each node in turn through the visitor pattern. The Declare function decorator adds the assertVersion method to the internal parameter API as used above.

Using AstExplorer to analyze the fields, start with a simple use case, from spliceText(‘ interesting soul ‘) to spliceTextCopy(‘ interesting soul ‘) :

Astexplorer lower left panel support online modification and view the effect oh! The first step is simple: access the Identifier node and change its name to spliceTextCopy if the name is the old function name spliceText.

SpliceText (‘ I have a small {0}, I never {1}’, ‘donkey ‘,’ ride ‘). In addition to changing the function name, this requires converting the second and subsequent arguments to object form.

How are objects represented in the AST? You can write any object in AstExplorer and then look at the corresponding node type:

Based on the above analysis, to supplement our plug-in code:

const { declare } = require('@babel/helper-plugin-utils');

module.exports = declare((api, options) = > {
    api.assertVersion(7);
    const { types } = api;
    
    return {
        name: 'my-test-plugin',
        pre (file) {
            
        },
        visitor: {
            Identifier (path, state) {
                if (path.node.name === 'spliceText') {
                    path.node.name = 'spliceTextCopy';
                    
                    // Get the parent of the current Identifier, which is the entire expression
                    const parent = path.parent;
                    const args = parent.arguments;

                    // There is only one parameter and no processing is required
                    if (args.length === 1) {
                        return;
                    }

                    // Build an AST for an empty object
                    const params = types.objectExpression([]);

                    // Start at 1, iterate through the parameters, and shove them into the properties of the object expression
                    for (let i = 1, len = args.length; i < len; i++) {
                        // types. ObjectProperty Creates an ast for object properties
                        params.properties.push(
                            types.objectProperty(
                                types.numericLiteral(i-1),
                                args[i]
                            )
                        )
                    }

                    parent.arguments.splice(1);
                    parent.arguments.push(params);
                    path.skip();
                }
            }
        },
        post (file) {

        }
    };
});
Copy the code

Check out the test results:

Nice. The snapshots are as expected. The plugin is done. You can then publish the plug-in to the NPM repository and introduce it into your project babel.config.json.

conclusion

In this article, we share the basics of Babel 7’s minimum and optimal configuration from a working perspective, then take a brief look at Babel Packages, and share the @babel/standalone classification of the interesting package and plug-in family. Finally, I will share the process of writing a Babel Plugin from the business development process of requirements analysis, testing and coding. This demo has been synchronized to Github. More content can be paid attention to xiaoyu public account: