likes
comments
collection
share

babel怎么用?(基础版)

作者站长头像
站长
· 阅读数 21

序言 前段时间在做vue UI组件库打包,打包UMD格式的时候rolup的babel插件报错: [!] (plugin commonjs--resolver) Error: You have declared using "bundled" babelHelpers, but transforming xx/components/index.js resulted in "runtime". Please check your configuration. 事倍功半,这让我比较迫切的想要详细了解下babel的使用方式。

从业了几年,就接触过babel几年。做过不少项目,babel配置差不多,对babel的了解停留在最底层。

2020年12月份时看过文档,学习过一些,但因为这中间的日常工作挺大部分时间在Node.js上,babel用的少,就又还回去了。而且现在来看,当时了解过于浅显且结构不太清晰。

说明

  1. 本文涉及的使用方法的babel版本为7.18.x,babel不同版本 6.x, 7.4以下的存在差异
  2. 本文只是单纯babel的使用说明,不含不同使用场景的配置推荐,不含其他构建工具(比如rollup、webpack)的babel使用说明
  3. 文章写于2022-08-24,时间不止,变化不停

babel的作用

官网说明

Babel 是一个 JavaScript 编译器

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

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性
  • 源码转换 (要完成上面两个功能,代码肯定会被转换,不太清楚这里具体指的是什么。可能是指可以通过babel插件来方便的改变源码?暂时忽略)

怎么理解?

我们对ES2015+的JS新特性进行分类:

  1. 新语法 例:箭头函数、array spread、for of、async、class
  2. 新增对象和对象静态/实例方法
    • 新对象 例:Promise、Generator、Map
    • 新增对象实例/静态方法 例:array.includes、Object.assign

现在来翻译一下babel官网说明—babel能为你做的事情:

  • 将新的语法转换成旧的语法
  • 通过polyfill方式在目标环境中添加新的对象和对象方法

准备工作

1. 确认babel使用方式,babel的使用方式有三种:

  • 引入@babel/core,自行写代码调用babel的API
  • 安装@babel/cli,通过命令行的形式执行调用
  • 使用打包工具(比如webpack、rollup)并安装相应插件来进行调用

为了更详细的了解、排除其他因素的干扰,我们采用第一种最原始的方式。

2.准备源码,包含以下ES2015+的新特性:

  • 箭头函数
  • array spread操作符
  • async/await
  • Array.prototype.includes
  • Promise
// 项目index.js
const sleep = (seconds) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('ok');
        }, seconds * 1000);
    });
};

function getHighlightNodeIds(historyNodeIds, currentNodeIds) {
    const nodeIds = [
        ...historyNodeIds,
        ...currentNodeIds,
    ];
    // 去重
    const result = [];
    nodeIds.forEach((item) => {
        if(!result.includes(item)){
            result.push(item);
        }
    });
    return result;
}

async function run() {
    console.time('run');
    await sleep(5);

    const historyNodeIds = ['1', '2', '3', 'a'];
    const currentNodeIds = ['a', 'b', 'c'];
    const highlightNodeIds = getHighlightNodeIds(historyNodeIds, currentNodeIds);
    console.log('highlightNodeIds: ', highlightNodeIds);

    console.timeEnd('run');
}

run().then(() => {
    console.log('执行完毕');
});

作用一:语法转换

// tranform/1.js
const path = require('path');
const fs = require('fs');
const babel = require('@babel/core');

// 1.获取源码
const filepath = path.resolve(__dirname, '../index.js');
const sourceCode = fs.readFileSync(filepath, 'utf8');

// 2. 执行编译(babel的转换函数有好几个,感兴趣的自行查看)
const result = babel.transformSync(sourceCode);

// 3. 将编译后的结果输出到文件
fs.writeFileSync('./dist/1.js', result.code);
babel怎么用?(基础版)

编译前后除了缩进、换行这种无关紧要的变化外,其他就没了。 那我们应该如果转换语法?

方式一:手动添加语法转换插件

语法插件列表见Plugins List

// transform/2.js
// 引入依赖省略 同上
// 1.获取源码  同上

// 2. 执行编译
const result = babel.transformSync(sourceCode, {
    sourceMaps: 'both', // 增加下sourceMap
    plugins: [
        '@babel/plugin-transform-arrow-functions',
        '@babel/plugin-transform-spread',
        '@babel/plugin-transform-async-to-generator'
    ]
});

// 3. 将编译后的结果输出到文件
fs.writeFileSync('./dist/2.js', result.code);
fs.writeFileSync('./dist/2.js.map', JSON.stringify(result.map, null, 4));

babel怎么用?(基础版) 可以看到array spread、箭头函数、async函数都已经转换了,Promise、array.includes保持原样,符合预期。

这种方式的缺点很明显,开发者必须知道项目用了哪些语法,然后添加对应的插件。这样太琐碎且重复,不利于长期维护。所以babel还提供另外一种方式—preset-env(预设)

方式二:preset-env

@babel/preset-env让你可以使用最新的JS 提案(不低于stage 3)且不需要自己声明目标环境需要哪些插件。它的依赖中含有官方所有的plugins list babel怎么用?(基础版) 怎么告诉preset-env希望转换的语法? 使用targets配置项,声明编译后的代码需要运行的目标环境

// 其他同上
// 2. 执行编译
const result = babel.transformSync(sourceCode, {
     presets: [
        [
        '@babel/preset-env',
            {
                targets: {'chrome': '53'}
            }
        ]
    ]
});

// 3. 将编译后的结果输出到文件
fs.writeFileSync('./dist/3.js', result.code);

babel怎么用?(基础版) 可以看到,此时只有async函数被编译了(使用到了generator函数),其他两个语法保持原样,因为目标环境已经可以支持,不需要转换了。

targets的更多值请参考官网

作用二:对象及对象方法支持(polyfill)

新语法可以编译成旧语法,但是新增的对象和对象方法不同,这些需要环境支持。如果环境不支持,就需要自己来新增实现,当然实际开发我们基本会用工具来做,babel就可以。

方式一:全局修改

特点

  • 已有对象的新方法 -> 通过修改原型链新增方法
  • 新对象 -> 增加新的全局对象

验证

  • 当前运行环境并不支持at方法 babel怎么用?(基础版)
  • 引入core-js
    const customPromise = require("core-js/stable/promise");
    require("core-js/stable/array/at");
    
    /**
     * customPromise 和 Promise 全等
     * => core-js/stable/promise 运行后增加了全局对象Promise
    */
    console.log('customPromise === Promise:' , customPromise === Promise);
    
    /**
     * 引入at方法后,即可直接在数组实例上调用了
     */
    const array1 = [5, 12, 8, 130, 44];
    const index = 2;
    console.log('at运行结果', array1.at(index));
    
    babel怎么用?(基础版)

依赖

注意:依赖中前三个是代码运行的依赖,最后一个是babel打包的依赖

  • core-js(3.x):提供了全局新对象和方法和修改原型链
  • regenerator-runtime:实现了generator对象及相关方法
  • @babel/runtime:提供辅助函数
  • @babel/plugin-transform-runtime:提取编译用到的辅助函数

babel怎么用?(基础版) 可以看出已有对象的新方法core-js是通过修改原型来实现的。

使用

  1. 方式一:手动引入依赖 + useBuiltIns: 'entry' 我们先引入依赖文件 babel怎么用?(基础版) 从 can i use中查到这些特性开始支持的chrome版本: promise 32,arrow 45,spread 46,includes 47,async 62(generator 39)

    我们使用46,此时目标环境不支持includes和async。编译后的代码如下: babel怎么用?(基础版)

    • includes方法使用保持原样
    • generator和arrow function也被转换了,有点不符合预期,但这个疑问先放一下

    源码中引入core-js/stage(内部引入了提案阶段的所有特性),编译后的代码这里保持原样。如此,编译代码就引入了目标环境已经支持的、源码中没有使用到的特性。这显然不适合,编译过程需要更多的处理。所幸babel已经提供了配置,我们使用就好。

    preset-env增加配置useBuiltIns: 'entry' (默认为false) babel怎么用?(基础版) 增加此配置后,babel会根据targets值,全量引入那些目前环境不支持的特性。 但是这种方式还是会引入项目中不需要的特性,如果场景不需要,可以使用下一种方式。

  2. 方式二:useBuiltIns: 'usage' 注意,此时不需要在源码中引入上面的core-js和regenerator-runtime了

babel怎么用?(基础版)

  1. 其他:提取辅助函数 上面两种方式都可以看到babel(@babel/core)有引入辅助函数_regneratorRuntime, asyncGeneratorStep等。如果我们使用其他特性,比如class也会有别的辅助函数。

    如果一个项目要打包多个文件(结果是多文件,比如开发库的时候),不希望每个文件都包含辅助函数,因为这样会有重复代码,会增加文件体积。此时,我们就可以使用@babel/plugin-transform-runtime & @babel/runtime这两个依赖。 babel怎么用?(基础版) 如果helpers参数设置为false,帮助函数会和之前一样以源码的形式包含在文件中。

方式二:局部修改

特点

  • 已有对象的新方法 -> 增加方法
  • 新对象 -> 增加新的局部对象

注:babel会,对象、方法的调用方式会发生改变

验证 运行环境和之前的验证相同

const customPromise = require("@babel/runtime-corejs3/core-js-stable/promise");
const at = require("@babel/runtime-corejs3/core-js/instance/at");

/**
 * 1. customPromise的API和Promise相同
 * 2. customPromise 和 Promise 不相等
*/
new customPromise((resolve) => {
    setTimeout(() => {
        resolve('ok');
        console.log('promise resolved');
    }, 100);
});
console.log('customPromise === Promise:' , customPromise === Promise);

/**
 * array实例的at方法
 * 1. at方法的调用方法和原生支持时的不一样
 * 2. 引入at后,直接实例调用还是抛错,说明未做全局修改
 */
const array1 = [5, 12, 8, 130, 44];
const index = 2;
console.log('at返回值:' , at(array1).call(array1, index));

const array2 = [5, 12, 8, 130, 44];
array2.at(index);

babel怎么用?(基础版)

依赖

  • @babel/runtime-corejs3:提供非全局修改的特性(代码运行需要)
  • @babel/plugin-transform-runtime:提取辅助函数(babel编译需要)

使用

方式:@babel/plugin-transform-runtime增加配置corejs babel怎么用?(基础版)

如果此时把helpers改成false,辅助函数一样会变成源码 babel怎么用?(基础版)

参考文档

转载自:https://juejin.cn/post/7136169010542379039
评论
请登录