babel loader 深度介绍
简介
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 的箭头函数转换为普通函数
- 安装依赖
npm i babel-cli babel-loader babel-plugin-transform-es2015-arrow-functions
/src/index.js
代码
[1, 2, 3].map(v => v +1)
- webpack.config.js
module.exports = {
entry: './src/index.js', // 项目的入口文件
output: {
filename: 'build.js', // 输出文件名
},
module: {
rules: [
{
test: /\.js$/, // 匹配所有以 .css 结尾的文件
use: [
'babel-loader', // 将 CSS 转换为 CommonJS 模块
],
},
],
},
};
- .babelrc
{
"plugins": [
"transform-es2015-arrow-functions"
]
}
- 执行
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
首先,我们来理清两个概念 polyfill
和 transform
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/polyfill
和 corejs/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-runtime
的 useBuiltIns
属性在 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包更小
主要做了两件事
通过 plugin 做语法转换,例如将箭头函数转换为普通函数、将 class 转换为内置函数(helper)等
引入 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,此时,它们的执行顺序非常重要。
先执行完所有Plugin,再执行Preset。
多个Plugin,按照声明次序顺序执行。
多个Preset,按照声明次序逆序执行。