babel入门&文件转译

Posted webchang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了babel入门&文件转译相关的知识,希望对你有一定的参考价值。

1. babel入门

1.1 babel是什么

Babel 是一个 javascript 编译器

babel 是一个通用的多用途 JavaScript 编译器。通过 Babel 你可以使用(并创建)下一代的 JavaScript,以及下一代的 JavaScript 工具。

JavaScript 在不断发展,新的标准/提案和新的特性层出不穷。 在得到广泛普及之前,Babel 能够让你提前(甚至数年)使用它们。Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译(transpiling,是一个自造合成词,即转换+编译。以下也简称为转译)。

此外它还拥有众多模块可用于不同形式的静态分析。

静态分析是在不需要执行代码的前提下对代码进行分析的处理过程 (执行代码的同时进行代码分析即是动态分析)。 静态分析的目的是多种多样的, 它可用于语法检查,编译,代码高亮,代码转换,优化,压缩等等场景。

下面列出的是 Babel 能为你做的事情:

  • 语法转换,Babel 通过语法转换器来支持新版本的 JavaScript 语法。
  • 通过 Polyfill 方式在目标环境中添加缺失的特性(通过第三方 polyfill 模块,例如 core-js,实现)。比如浏览器还不支持某些高版本语法中的API,此时就使用polyfill来模拟出这个API
  • 源码转换 (codemods)

可以通过https://kangax.github.io/compat-table/es6/来检查各个浏览器对ECMAScript的兼容性

// Babel 输入: ES2015 箭头函数
[1, 2, 3].map((n) => n + 1);

// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) {
  return n + 1;
});

插件化

Babel 构建在插件之上。使用现有的或者自己编写的插件可以组成一个转换管道。通过使用或创建一个 preset 即可轻松使用一组插件。

// 一个插件就是一个函数
export default function ({types: t}) {
  return {
    visitor: {
      Identifier(path) {
        let name = path.node.name; // reverse the name: JavaScript -> tpircSavaJ
        path.node.name = name.split('').reverse().join('');
      }
    }
  };
}

1.2 使用指南

安装方式

  • 在babel6中安装,一般使用的是babel-core、babel-cli,在项目根目录下创建.babelrc文件。
  • 在babel7中安装,使用@babel/core @babel/cli @babel/preset-env ,也就是改成了@xxx,配置文件是在项目根目录下创建babel.config.json文件

概览

下面将展示如何将 ES2015+ 语法的 JavaScript 代码编译为能在当前浏览器上工作的代码。这将涉及到新语法的转换和缺失特性的修补。

  1. 运行以下命令安装所需的包(package)
npm install --save-dev @babel/core @babel/cli @babel/preset-env
  1. 在项目的根目录下创建一个命名为 babel.config.json 的配置文件(需要 v7.8.0 或更高版本),并将以下内容复制到此文件中
{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ]
}
  1. 运行此命令将 src 目录下的所有代码编译到 lib 目录(这将解析 src 目录下的所有 JavaScript 文件,并应用我们所指定的代码转换功能,然后把每个文件输出到 lib 目录下。使用 --out-dir 或者​-d参数)
// 方式1
./node_modules/.bin/babel src --out-dir lib

// 方式2:使用npx命令
npx babel src -d lib

// 方式3:也可以在package.json文件中的scripts中自定义命令,前提是本地已经安装了babel-cli
// 然后执行 npm run build
  {
    "name": "my-project",
    "version": "1.0.0",
+   "scripts": {
+     "build": "babel src -d lib"
+   },
    "devDependencies": {
      "babel-cli": "^6.0.0"
    }
  }

CLI 命令行的基本用法

Babel 模块都是作为独立的 npm 包发布的,并且(从版本 7 开始)都是以 @babel作为冠名的。这种模块化的设计能够让每种工具都针对特定使用情况进行设计。

安装了@babel/cli 就可以在命令行下使用babel
安装了@babel/core 就可以用编程的方式使用babel

@babel/cli

Babel 的 CLI 是一种在命令行下使用 Babel 编译文件的简单方法

核心库

Babel 的核心功能包含在 @babel/core 模块中,能够以编程的方式来使用 Babel

const babel = require("@babel/core");

babel.transformSync("code", optionsObject);

插件和预设(preset)

代码转换功能以插件的形式出现,插件是小型的 JavaScript 程序,用于指导 Babel 如何对代码进行转换。你甚至可以编写自己的插件将你所需要的任何代码转换功能应用到你的代码上。例如将 ES2015+ 语法转换为 ES5 语法,我们可以使用诸如 @babel/plugin-transform-arrow-functions 之类的官方插件

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

npx babel src --out-dir lib --plugins=@babel/plugin-transform-arrow-functions

// 执行上述命令后,我们代码中的所有箭头函数(arrow functions)都将被转换为 ES5 兼容的函数表达式了

Babel 的预设(preset)可以被看作是一组 Babel 插件。我们不需要一个接一个地添加所有需要的插件,我们可以使用一个 “preset” (即一组预先设定的插件)。就像插件一样,你也可以根据自己所需要的插件组合创建一个自己的 preset 并将其分享出去。

// 安装@babel/preset-env预设
npm install --save-dev @babel/preset-env

npx babel src --out-dir lib --presets=@babel/env

如果不进行任何配置,上述 preset 所包含的插件将支持所有最新的 JavaScript (ES2015、ES2016 等)特性。

配置

当我们配置了babel.config.json文件后,执行命令时就可以不用携带–plugins和–presets参数

总结

我们使用 @babel/cli 从终端运行 Babel,利用 @babel/polyfill 来模拟所有新的 JavaScript 功能,而 env preset 只对我们所使用的并且目标浏览器中缺失的功能进行代码转换和加载 polyfill。

babel使用的几种方式:

  1. 命令行中使用babel + 相应的参数。例如执行npx babel src -d lib --plugins @babel/plugin-transform-member-expression-literals
  2. 命令行 + 配置文件(参数放到配置文件中)。例如将该插件配置到babel.config.json文件中,然后就可以使用命令npx babel src -d lib
  3. 在编码中使用babel。需要引入@babel/core
var babel = require("babel-core");

// 注意回调函数的第一个参数是err,第二个参数才是result
babel.transformFile("filename.js", options, function(err, result) {
  result; // => { code, map, ast }
});

1.3 编写babel插件练习

需求:有这样一段代码

// 在dev环境下执行,在prod环境下会移除
// Debug未被定义
if (Debug) {
  const a = 1;
  const b = 2;
  console.log(a + " " + b);
}

编写插件基础知识

可以创建一个plugin.js文件,导出一个函数,该函数接受当前babel对象作为参数

export default function(babel) {
  // plugin contents
}

// 由于经常使用babel.types,可以直接将其取出
export default function({ types: t }) {
  // plugin contents
}

接着返回一个对象,其 visitor 属性是这个插件的主要访问者,Visitor 中的每个函数接收2个参数:path 和 state

export default function({ types: t }) {
  return {
    visitor: {
      // Identifier表示当前是标识符了就会进入到此方法中
      Identifier(path, state) {},
      ASTNodeTypeHere(path, state) {}
    }
  };
};

实现需求

  1. 使用vue-cli创建一个项目
  2. 创建plugin.js文件用来编写插件
  3. 创建testPlugins.js用来测试插件
  4. 控制当前环境是dev还是prod有两种方式:
    1. process.env.NODE_ENV 来判断,我们可以将vue项目打包后,对打包后的文件开启一个服务器来测试
    2. 配置插件时添加{ isRemove: false }参数

插件代码

module.exports = function ({ types: t }) {
    return {
        visitor: {
            Identifier(path) {
                // 如果父级节点是if语句
                let parentIsIf = t.isIfStatement(path.parentPath);
                // 如果当前节点是变量声明,并且名称为Debug
                let isDebug = path.node.name === 'Debug';

                console.log('Identifier',path.node.name);
                if (parentIsIf && isDebug) {
                    // 把Identifier转成StringLiteral
                    let stringNode = t.stringLiteral("Debug");
                    path.replaceWith(stringNode);
                }
            },
            StringLiteral(path, state) {
                console.log('StringLiteral',path.node.value);
                let parentIsIf = t.isIfStatement(path.parentPath);
                let isDebug = path.node.value === "Debug";
                if (parentIsIf && isDebug) {
                    // 方式1
                    // if(process.env.NODE_ENV === "production") {
                    //     path.parentPath.remove();
                    // }

                    // 方式2
                    if (state.opts.isRemove) {
                        path.parentPath.remove();
                    }
                }
            }
        }
    }
}

testPlugin.js文件,用来本地测试代码,执行该文件可以看到输出结果

const babel = require('@babel/core');
const myPlugin = require('./plugin')

// 在dev环境下执行,在prod环境下会被移除
let code = `
if(Debug) {
    const a = 1;
    const b = 2;
    console.log(a + ' ' + b);
}
`

let babelConfig = {
    plugins: [
        // 添加插件时,可以使用数组。第一个元素是插件,第二个元素是对插件的配置
        [myPlugin, { isRemove: false }] 
    ]
};

let result = babel.transformSync(code, babelConfig);
console.log(result.code);

测试

app.vue文件

<template>
  <button @click="btnClick">按钮</button>
</template>

<script>

export default {
  name: "App",
  methods: {
    btnClick() {
      console.log("click");
      // 在dev环境下执行,在prod环境下会移除
      if (Debug) {
        const a = 1;
        const b = 2;
        console.log(a + " " + b);
      }
    },
  },
};
</script>

babel.config.js文件

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  plugins: [
    ["./src/plugin.js", { isRemove: false }]
  ]
}

插件配置参数isRemove是false时,多次点击按钮,会进入到if语句中

插件配置参数isRemove是true时,多次点击按钮,不会进入到if语句中

2. 文件转译

Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。

这个处理过程中的每一步都涉及到创建或是操作抽象语法树,亦称 AST。AST抽象语法树应用场景:使用babel将ES6语法转为ES5语法,webpack中的loader。在解析过程中,会首先将源代码字符串转成AST,然后对AST进行一系列的操作,最后再将AST转成代码。

可以使用https://astexplorer.net/在线查看一段代码的AST

例如let a = 1;的AST如图所示:

2.1 Babel 的处理步骤

Babel 的三个主要处理步骤分别是: 解析(parse)转换(transform),代码生成(generate)

解析步骤接收代码并输出 AST。 这个步骤分为两个阶段词法分析和语法分析。

转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程,同时也是插件将要介入工作的部分,文件编译的主要工作在这里。

代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码。生成过程是深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

2.2 遍历过程

Visitors(访问者)

当我们谈及“进入”一个节点,实际上是说我们在访问它们。访问者就是一个对象,其中可以定义访问具体节点的方法。例如

const MyVisitor = {
  // 每当在AST中遇见一个 Identifier 的时候会调用 Identifier() 方法
  Identifier() {
    console.log("Called!");
  }
};

path

AST 通常会有许多节点,那么节点直接如何相互关联呢? 可以使用path,Path 是表示两个节点之间连接的对象。path是一个节点在树中的位置以及关于该节点各种信息的响应式表示。 当你调用一个修改树的方法后,路径信息也会被更新。

path能够直接作为参数被添加到访问者中,例如:

const MyVisitor = {
  // 每当在AST中遇见Identifier,就会打印出来该节点的名称
  Identifier(path) {
    console.log("Visiting: " + path.node.name);
  }
};

2.3 工具

需要使用的工具:

  • @babel/parser
  • @babel/traverse
  • @babel/types
  • @babel/generator

安装:

npm install @babel/parser @babel/traverse @babel/types @babel/generator -D

@babel/parser

用于将JS代码转成AST抽象语法树

const parser = require('@babel/parser');

let code = `let a = 1;`;

let ast = parser.parse(code);
console.log(ast);

@babel/traverse

对 AST 节点进行递归遍历,在遍历过程中对节点做一些处理,主要编码工作在这里。

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;

let code = `let a = 1;`;

let ast = parser.parse(code);

// 访问者
let visitor = {
    Identifier(path) {
        // 因为代码中只有a一个标识符Identifier,所以会输出一行:Identifier a
        console.log(path.node.type, path.node.name); 
    }
}
traverse(ast, visitor);

@babel/types

它包含了构造验证以及变换 AST 节点的方法。例如:

  • t.identifier(name); 创建一个Identifier(标识符)节点
  • t.isIdentifier(node, opts); 判断一个节点node是否是Identifier类型的
  • t.numericLiteral(value); 创建一个数字文本节点

详细的API可查看文档:https://www.babeljs.cn/docs/babel-types

let code = `
function square(n) {
    return n * n;
}
`;

let ast = parser.parse(code);

// 访问者
let visitor = {
     // 代码中的n*n是BinaryExpression
     BinaryExpression(path) {
        path.replaceWith(t.numericLiteral(1));
    }
}
traverse(ast, visitor);

let result = generate(ast, {}, code);
console.log(result.code);

// 输出
function square(n) {
  return 1;
}

@babel/generator

将AST 抽象语法树转换为新的 js 代码;

2.4 文件转译练习

需求:
对于一个JS文件解析规则如下:

  • this.data -> this.state
  • this.setData -> this.setState
  • function onReady() -> function componentDidMount()
  • 其它不转换

注意点:查找对应节点时要注意判断条件,如果筛选条件过弱,可能会选中操作一些不相干的节点。例如要将this.data转换为this.state,而普通的data变量或者其它地方的data是不转换的。

原文件:


function Person() {

  // 属性赋值
  const name = this.data.name;
  const { age } = this.data;
  const obj = {...this.data};

  this.setData({
    name: 'bill'
  });

  setTimeout(() => {
    this.setData({
      name: 'bill'
    }, () => {
      console.log('this is data callback')
    });
  }, 100);

  
  function onReady() {
    console.log('this is onReady func');
  }

}

转换代码:

const parser = require('@babel/parser')
const t = require('@babel/types')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const fs = require('fs')

/**
 * babel解析
 */
function parseJs() {

  // 读原文件src/index.js的内容
  const code = fs.readFileSync('../src/index.js', 'utf-8');
  const ast = parser.parse(code)

  // 转译逻辑
  traverse(ast, {
    // data -> state
    MemberExpression(path) {
      let node = path.node;
      let isThisExpression = t.isThisExpression(node.object);
      let isData = t.isIdentifier(node.property, { name: 'data' });
      if(isThisExpression && isData) {
        node.property.name = 'state';
      }
    },
    // setData -> setState
    CallExpression(path) {
      let callee = path.node.callee;
      if(!t.isMemberExpression(callee)) return;
      let isThisExpression = t.isThisExpression(callee.object);
      let isSetData = t.isIdentifier(callee.property, { name: 'setData' });
      if(isThisExpression && isSetData) {
        callee.property.name = 'setState';
      }
    }, 
    // 转换onReady
    FunctionDeclaration(path) {
      let funName = path.node.id.name;
      if(funName === 'onReady') {
        path.node.id.name = 'componentDidMount';
      }
    }
  })

  // 输出dest/index.js文件
  let result = generate(ast, {}, code);
  console.log(result.code);
  fs.writeFileSync('../dest/index.js', resultbabel从入门到入门

Babel 2018 为 Node JS 设置自动转译

从 Babel 转译过程浅谈 ES6 实现继承的原理

在新 CRA 中使用 babel 转译包后 CSS 和图像文件未出现

如何使用 babel-loader 转换 node_modules 模块?

Babel / Rollup 错误转译和捆绑 ES2017