babel loader 深度介绍

2024-10-28
webpack
142

简介

babel loader 的主要功能是对 js 代码做特定转换,使其兼容性等能力提升。其核心利用 ast 分析,将节点转换,生成新的代码

Babel 解析 AST、转换节点、生成代码的逻辑这里不再赘述,详情可以查看 AST 实战讲解。

除了其核心的ast解析之外,使用 babel-loader 还需要配置一个 .babelrc 文件,里面配置的preset和plugin众多,让人很是迷惑,本文就主要讲解babel preset 和 babel plugin 的作用。

Babel plugin

实现 Babel 代码转换功能的核心,就是Babel插件(plugin),只使用 Babel 是不能改变代码的,只用使用 plugin 才能做特定的改造。

不同的插件内部设置一套 AST 节点转换规则,通过转后之后就是最终想要的代码

示例

通过transform-es2015-arrow-functions将 es6 的箭头函数转换为普通函数

  1. 安装依赖
npm i babel-cli babel-loader babel-plugin-transform-es2015-arrow-functions
  1. /src/index.js 代码
[1, 2, 3].map(v => v +1)
  1. webpack.config.js
module.exports = {
  entry: './src/index.js',  // 项目的入口文件
  output: {
    filename: 'build.js',  // 输出文件名
  },
  module: {
    rules: [
      {
        test: /\.js$/,  // 匹配所有以 .css 结尾的文件
        use: [
          'babel-loader',   // 将 CSS 转换为 CommonJS 模块
        ],
      },
    ],
  },
};
  1. .babelrc
{
  "plugins": [
    "transform-es2015-arrow-functions"
  ]
}
  1. 执行
npx webpack

生成的代码如下

[1,2,3].map((function(n){return n+1}));

简单实现,这里需要对 babel 的 AST 有一定的了解

// 箭头函数转换为普通函数,可以分 2 步完成
function arrowToFunctionExpression(fn) {
  // 1. 串讲一个新的普通函数
  const functionExpr = t.functionExpression(
    null, // 函数没有名称
    fn.node.params, // 使用箭头函数的参数
    fn.node.body,   // 使用箭头函数的函数体
    false, // 非生成器函数
    false  // 非异步函数
  );
  // 2. 替换原函数并且绑定 this
  fn.replaceWith(
    callExpression(
      // 生成.bind
      memberExpression(functionExpr, identifier("bind")),
      // 获取上下文的 this
      [thisExpression()]
    )
  );
  return fn.get("callee.object");
}

常用 plugin 介绍

plugin-transform-runtime

首先,我们来理清两个概念 polyfilltransform

  • polyfill

Polyfill 是一种在旧环境中实现新功能的代码片段。它们为不支持某些功能的浏览器或环境提供了补充实现。

比如,对于低版本浏览器,没有 Array.join() 方法,polyfill 就给 Array 增加一个 jion 方法

如下是 corejs/stable 实现 Array.join() 的写法

// `Array.prototype.join` method
// https://tc39.es/ecma262/#sec-array.prototype.join
$({ target: 'Array', proto: true, forced: FORCED }, {
  join: function join(separator) {
    return nativeJoin(toIndexedObject(this), separator === undefined ? ',' : separator);
  }
});

在代码开始手动引入 polyfill ,就可以大胆使用最新的 JS API。

常用的 polyfill@babel/polyfillcorejs/stable。其中 @babel/polyfill 已经废弃,不再维护,推荐使用 corejs/stable

-helper

引入 polyfill ,将会污染全局变量,如果是库函数开发,这将是致命的。那如何解决呢,babel 给出另一个解决方案,将高版本代码转换为内置写法,实现已有的能力。这些内置的方法就是 helper,运行时助手。

举个🌰

Object.assign({}, {a:1})

polyfill 的实现方案是,给Object实现一个 assign 属性

// `Object.assign` method
// https://tc39.es/ecma262/#sec-object.assign
// eslint-disable-next-line es/no-object-assign -- required for testing
$({ target: 'Object', stat: true, arity: 2, forced: Object.assign !== assign }, {
  assign: ...
});

使用 transform 的方案

var _extends = Object.assign || function (target) { 
    for (var i = 1; i < arguments.length; i++) { 
        var source = arguments[i];
        for (var key in source){ 
        if (Object.prototype.hasOwnProperty.call(source, key)){ 
            target[key] = source[key]; } 
        } 
    } 
   return target; 
};
var object = _extends({}, { a: 1 });

先声明一个 _extends函数,再将代码中的 Object.assign替换为 _extends

helper方案需要引用特定的 transform-plugin 实现,这里就需要用到 plugin-transform-object-assign

polyfill 虽然能增加 api 能力,但是如 class 箭头函数之类的高级语法,还是不能支持,这个时候要配合 transform 能力,对高级语法作转换。

因此,polyfill 和 helper 是可以同时存在的


再回到 plugin-transform-runtime 上来

plugin-transform-runtime 的功能是将所有的 transform 函数引入到最外层,避免每个文件都自己引入一套,从而减少文件体积。

更多的配置可以参考babel文档:https://babeljs.io/docs/babel-plugin-transform-runtime

值得注意的是,plugin-transform-runtimeuseBuiltIns 属性在 7.× 版本上已经移除,需要去 @babel/preset-env 配置

babel-plugin-import

在使用Antd 或者 arco 时,通常需要使用 babel-plugin-import 来实现动态加载,完成 tree shaking。它的配置比较简单

plugins: [
  [
    'babel-plugin-import',
    {
      libraryName: '@arco-design/web-react',
      libraryDirectory: 'es',
      camel2DashComponentName: false,
      style: true, // 样式按需加载
    },
  ],
];

其原理是将原本全局引入的代码,改造为引入部分代码,例如:

import { Button } from '@arco-design/web-react';

这段代码,引用了 '@arco-design/web-react' 的所有内容,然后从所有内容中解构出了 Button 组件。而使用了 babel-plugin-import,可以将刚才的代码改造为

import Button from '@arco-design/web-react/es/Button';

只引入了 /es/Button 文件,没有全局引入。

Babel preset

Babel插件一般尽可能拆成小的力度,开发者可以按需引进。比如对 ES6 转 ES5 的功能,Babel 官方拆成了20+个插件。

这样的好处显而易见,既提高了性能,也提高了扩展性。比如开发者想要体验ES6的箭头函数特性,那他只需要引入transform-es2015-arrow-functions插件就可以,而不是加载ES6全家桶。

但很多时候,逐个插件引入的效率比较低下。比如在项目开发中,开发者想要将所有ES6的代码转成ES5,插件逐个引入的方式令人抓狂,不单费力,而且容易出错。

这个时候,可以采用 Babel Preset。

可以简单的把 Babel Preset 视为 Babel Plugin 的集合。

常用 preset 介绍

babel-preset-env

babel-preset-env 允许您使用最新的JavaScript,而无需微观管理目标环境需要哪些语法转换(以及可选的浏览器多填充)。这既使您的生活更轻松,又使JavaScript包更小

主要做了两件事

  1. 通过 plugin 做语法转换,例如将箭头函数转换为普通函数、将 class 转换为内置函数(helper)等

  2. 引入 core-js 进行 pollyfill

babel-preset-env 为了按需引入pollyfill,引入了 .browserslistrc 文件,在这里可以规定 pollyfill 的支持范围

babel-preset-env 会引用一些内置函数,因此,可以和plugin-transform-runtime同时使用

babel-preset-env 的 useBuiltIns,用来控制是否需要加载pollyfill(core-js),参数取值如下

  • usage 按需加载,只有使用到的特征函数才会加载 pollyfill

  • entry 统一加载,在项目入口统一添加所有支持的 pollyfill

  • false 不加载pollyfill

默认 false

Plugin 与 Preset 执行顺序

可以同时使用多个Plugin和Preset,此时,它们的执行顺序非常重要。

  1. 先执行完所有Plugin,再执行Preset。

  2. 多个Plugin,按照声明次序顺序执行。

  3. 多个Preset,按照声明次序逆序执行。