brunoyang / blog

134 stars 13 forks source link

Babel 全家桶 #20

Open brunoyang opened 8 years ago

brunoyang commented 8 years ago

15 年 11 月,Babel 发布了 6.0 版本。相较于前一代 Babel 5,新一代 Babel 更加模块化, 将所有的转码功能以插件的形式分离出去,默认只提供 babel-core。原本只需要装一个 babel ,现在必须按照自己的需求配置,灵活性提高的同时也提高了使用者的学习成本。下面就来讲讲 Babel 全家桶中的各个部分。

npm i babel

已经弃用,你能下载到的仅仅是一段 console.warn,告诉你 babel 6 不再以大杂烩的形式提供转码功能了。

npm i babel-core

babel-core 的作用是把 js 代码分析成 ast ,方便各个插件分析语法进行相应的处理。有些新语法在低版本 js 中是不存在的,如箭头函数,rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 ast,分析其语法后再转为低版本 js。

以下代码转自阮一峰老师博客

const babel = require('babel-core');

// 字符串转码
babel.transform('console.log', options);
// => { code, map, ast }

// 文件转码(异步)
babel.transformFile('filename.js', options, (err, result) => {
  result; // => { code, map, ast }
});

// 文件转码(同步)
babel.transformFileSync('filename.js', options);
// => { code, map, ast }

// Babel AST转码
babel.transformFromAst(ast, code, options);
// => { code, map, ast }

npm i babel-register

该模块给 require 加了个钩子,.jsjsx.eses6 后缀的模块都会先转码。此外有几点需要注意:

  1. 当前文件不会被转码;
  2. 需首先加载 babel-register
  3. 由于是实时转码,只适合开发环境。
require('babel-register')
const index = require('./index.jsx')

npm i babel-cli -g

babel 命令行工具,可以转码文件或目录并输出至指定文件,直接看用法:

#1. 直接在终端输出
$ babel script.js
# output...

#2. 输出到文件
$ babel script.js --out-file script-compiled.js

#3. 支持 source maps
$ babel script.js --out-file script-compiled.js --source-maps

#4. watch 变动并输出到文件
$ babel script.js --watch --out-file script-compiled.js

#5. 可以编译文件夹
$ babel src --out-dir lib

#6. 也可以编译成一个文件
$ babel src --out-file script-compiled.js

npm i babel-plugin-*

babel-plugin-* 代表了一系列的转码插件,如babel-plugin-transform-es2015-arrow-functions 用于转码 es6 中的箭头函数,babel-plugin-transform-async-to-generator 用于将 es7 中的 async 转成 generator。

.babelrc中的配置:

{
  plugins: [
    'transform-es2015-arrow-functions',
    'transform-async-to-generator',
  ]
}

npm i babel-preset-*

我们现在有了 babel-plugin 系列,可以按需配置自己想要的特性。但若是想搭个 es6 环境,一个个地配置各个插件,我猜你会疯掉。babel-preset 系列就可以满足我们的需求,babel-preset 系列打包了一组插件,类似于餐厅的套餐。如 babel-preset-es2015 打包了 es6 的特性,babel-preset-stage-0 打包处于 strawman 阶段的语法(关于 ECMAScript 制定流程可以看这里),

.babelrc中可以这样配置:

{
  presets: [
    'es2015',
    'stage-3',
    'react'
  ]
}

npm i babel-runtime / babel-polyfill

上面提到的插件可以将语法从 es6 转成 es5,但没有提供 api 的转码功能,如 Promise、Set、Map 等新增对象,Object.assign、Object.entries 等全局对象上的新增方法都不会转码。而 babel-runtimebabel-polyfill 就是为此而生。

这两个模块功能几乎相同,就是转码新增 api,模拟 es6 环境,但实现方法完全不同。babel-polyfill 的做法是将全局对象通通污染一遍,比如想在 node 0.10 上用 Promise,调用 babel-polyfill 就会往 global 对象挂上 Promise 对象。对于普通的业务代码没有关系,但如果用在模块上就有问题了,会把模块使用者的环境污染掉。

babel-runtime 的做法是自己手动引入 helper 函数,还是上面的例子,const Promise = require('babel-runtime/core-js/promise') 就可以引入 Promise。

但 babel-runtime 也有问题,第一,很不方便,第二,在代码中中直接引入 helper 函数,意味着不能共享,造成最终打包出来的文件里有很多重复的 helper 代码。所以,babel 又开发了 babel-plugin-transform-runtime,这个模块会将我们的代码重写,如将 Promise 重写成 _Promise(只是打比方),然后引入_Promise helper 函数。这样就避免了重复打包代码和手动引入模块的痛苦。此外,babel-runtime 不能转码实例方法,比如这样的代码:

'!!!'.repeat(3);
'hello'.includes('h');

这只能通过 babel-polyfill 来转码,因为 babel-polyfill 是直接在原型链上增加方法。

babel-polyfill vs babel-runtime

那什么时候用 babel-polyfill 什么时候用 babel-runtime 呢?如果你不介意污染全局变量(如上面提到的业务代码),放心大胆地用 babel-polyfill ;而如果你在写模块,为了避免污染使用者的环境,没的选,只能用 babel-runtime + babel-plugin-transform-runtime

和 webpack 配合

很少有大型项目仅仅需要 babel,一般都是 babel 配合着 webpack 或 glup 等编译工具一起上的。下面就来介绍下 babel 和 webpack 如何 1 + 1 > 2。

为了显出 babel 的能耐,我们分别配个用 babel-polyfillbabel-runtime 、支持 react 的webpack.config.js

先来配使用 babel-runtime 的:

module: {
  loaders: [{
    loader: 'babel',
    test: /\.jsx?$/,
    include: path.join(__dirname, 'src'),
    query: {
      plugins: ['transform-runtime'],
      presets: [
        'es2015', 
        'stage-0',
        'stage-1',
        'stage-2',
        'stage-3',
        'react',
      ],
    }
  }]
}

需要注意的是,babel-runtime 虽然没有出现在配置里,但仍然需要安装,因为 transform-runtime 依赖它。

再来个 babel-polyfill 的:

entry: [
  'babel-polyfill',
  'src/index.jsx',
],

module: {
  loaders: [{
    loader: 'babel',
    test: /\.jsx?$/,
    include: path.join(__dirname, 'src'),
    query: {
      presets: [
        'es2015', 
        'stage-0',
        'stage-1',
        'stage-2',
        'stage-3',
        'react',
      ],
    }
  }]
}

行文仓促,多有疏漏,理解上可能有偏差,欢迎各位搞个大新闻。

AyumiKai commented 8 years ago

Up!

chunghaochow commented 7 years ago

cool

wonyun commented 7 years ago

请问一下作者,babel-runtime不污染全局,用到什么需要polyfill的api需要手动引入,比如promise;而babel-plugin-transform-runtime引入后,还是会用到什么api需要单独手动引入么???

若是这样,这样开发时岂不是很繁琐?

brunoyang commented 7 years ago

@wonyun 首先感谢回复。行文上可能有疏漏引起误解。因为 babel-runtime 都是自动帮你引入所需 polyfill,但每个文件引入的都是全量的 polyfill,很容易引起体积暴涨。所以我们可以加上 babel-plugin-transform-runtime,这样 runtime 所用的 polyfill 就会从 transform-runtime 里引入,就避免了体积暴涨的问题。

wonyun commented 7 years ago

@brunoyang ,不太明白这句因为 babel-runtime 都是自动帮你引入所需 polyfill,比方说我在代码中用到了Promise,在IE8下,babel-runtime难道会自动的帮我require这个promise的polyfill,还是需要开发者在代码开头手动引入这个promise polyfill文件?

brunoyang commented 7 years ago

@wonyun 应该这么说,Babel 中 runtime 只是个 helper 函数库,runtime transform 根据 ast 结果帮你引入所需 helper 函数。所以是只用 runtime 是需要手动,但不要这样用,而是用 runtime transform。

wonyun commented 7 years ago

@brunoyang ,非常感谢,理解其中情况,babel-runtime是会自动引入polyfill的

lcoder commented 7 years ago

博主,问下transform-runtimetransform-decorators-legacytransform-class-properties这些plugins引入的数组顺序会有关系吗

vcxiaohan commented 7 years ago

好文,顶

lcoder commented 7 years ago

@wonyun 没明白意思

非常感谢,理解其中情况,babel-runtime是会自动引入polyfill的

这句话中babel-plugin-runtime-transform 是自动的吧?所有的helper,一个bundle里面就一个;而babel-runtime,一个模块(文件)里一个,打包成bundle就有好多冗余的代码了。

wonyun commented 7 years ago

@lcoder 对,一个bundle里面一个但不用太在意,这些helper的体积很小,用这些helper换来的是自动引入polyfill的开发体验

xiaofan9 commented 7 years ago

那问问作者,babel-plugin-runtime-transform,引入的模块,需要手动安装并在.babelrc添加plugins或presets么?

RachelRen commented 7 years ago

现在引入了runtime-transform,但是发现array中的fill不起作用,然后又去引用了polyfill,是在webpack中引入的,但是发现都不起作用

unclemeric commented 7 years ago

好文章!顶顶

cunjieliu commented 7 years ago

写得不错

bigeyefish commented 7 years ago

写的不错,顶

450611 commented 7 years ago

请问作者, es2015 跟 stage-* 是不是得同时存在, 如果自己写的代码存在es6、7语法的话

JoeeeeeWu commented 7 years ago

是不是说transform-runtime有两个作用:

这样的理解对吗?

@wonyun @brunoyang

JoeeeeeWu commented 7 years ago

还有一个疑问就是,babel-polyfill和babel-runtime + transform-runtime这两种方案是不是有以下区别:

那么这样的话babel-runtime + transform-runtime会比babel-polyfill的方案更省空间。

这样的理解对吗?

@wonyun @brunoyang

sohucw commented 7 years ago

@xiaozhouwu babel-runtime 是会自动引入 polyfill 每一个模块 都有 ransform-runtime 的方案只会把那些用到的重复helper过滤掉 打包。 babel-polyfill 应该是全局的

rylan0119 commented 7 years ago

有点疑惑想请教,transform-runtime中的"helpers"和"moduleName"到底设置了什么,我是用webpack打包的,感觉没什么影响,官网上的解释也不是很理解

CommanderXL commented 6 years ago

@xiaozhouwu

babel-polyfill 内部实现包含了所有的polyfill,而且部分polyfill是通过改变全局对象的方式。使用的方式也是直接在你的入口文件中去引入babel-polyfillbabel-runtime及其插件babel-plugin-transform-runtime,会在babel对你的代码编译的过程中,分析你的代码,如果用到了一些新的API,需要引入polyfill的代码,会自动帮你引入在core-js或者regenerator.js中的shim代码。babel-plugin-transform-runtime这个插件主要是做了一层映射,映射到babel-runtime内引用的core-jsregenerator.js中具体对应的helper:

如果你使用了一些新的实例上的方法,例如:

const str = [1, 2, 3]
str.includes(2)

像这种实例上添加的新的方法,仅仅用core-js中的shim是没法解决的。这个时候必须要引入babel-polyfill

babel-runtime及其插件babel-plugin-transform-runtime事实上就是按需去引入你需要的helper,而babel-polyfill是全部引入进去,从某种程度上说,确实比全部引入babel-polyfill最终打出的包更小。

zhangenming commented 6 years ago

"上面提到的插件可以将语法从 es6 转成 es5,但没有提供 api 的转码功能,如 Promise、..." 不太对吧, 比如babel-plugin-transform-object-assign属于 babel-plugin-*可以转换Object.assign