背景
提及ES6,webpack,打包,模块化老是离不开babel,babel做为一个js的编译器已经被普遍使用。在babel的官网是这样介绍它的:javascript
Babel is a JavaScript compiler.java
Use next generation JavaScript, today.node
你们都知道js做为宿主语言,很依赖执行的环境(浏览器、node等),不一样环境对js语法的支持不尽相同,特别是ES6以后,ECMAScrip对版本的更新已经到了一年一次的节奏,虽然每一年更新的幅度不大,可是每一年的提案可很多。babel的出现就是为了解决这个问题,把那些使用新标准编写的代码转译为当前环境可运行的代码,简单点说就是把ES6代码转译(转码+编译)到ES5。react
常常有人在使用babel的时候并无弄懂babel是干吗的,只知道要写ES6就要在webpack中引入一个babel-loader,而后胡乱在网上copy一个.babelrc到项目目录就开始了(ps: 其实我说的是我本身)。理解babel的配置很重要,能够避免一些没必要要的坑,好比:代码中使用Object.assign在一些低版本浏览器会报错,觉得是webpack打包时出现了什么问题,实际上是babel的配置问题。linux
ES6
正文以前先谈谈ES6,ES即ECMAScript,6表示第六个版本(也被称为是ES2015,由于是2015年发布的),它是javascript的实现标准。webpack
被归入到ES标准的语法必需要通过以下五个阶段:git
- Stage 0: strawman
- Stage 1: proposal
- Stage 2: draft - 必须包含2个实验性的具体实现,其中一个能够是用转译器实现的,例如Babel。
- Stage 3: candidate - 至少要有2个符合规范的具体实现。
- Stage 4: finished
能够看到提案在进入stage3阶段时就已经在一些环境被实现,在stage2阶段有babel的实现。因此被归入到ES标准的语法其实在大部分环境都已是有了实现的,那么为何还要用babel来进行转译,由于不能确保每一个运行代码的环境都是最新版本并已经实现了规范。程序员
更多关于ES6的内容能够参考hax的live:Hax:如何学习和实践ES201X?es6
Babel的版本变动
写这篇文章时babel版本已经到了v7.0.0-beta.3,也就是说7.0的正式版就要发布了,可喜可贺。可是今天不谈7.0,只谈babel6,在我知道并开始使用的babel的时候babel已经到了版本6,没有经历过5的时代。github
在babel5的时代,babel属于全家桶型,只要安装babel就会安装babel相关的全部工具,
即装即用。
可是到了babel6,具体有如下几点变动:
- 移除babel全家桶安装,拆分为单独模块,例如:babel-core、babel-cli、babel-node、babel-polyfill等;
能够在babel的github仓库看到babel如今有哪些模块。 - 新增 .babelrc 配置文件,基本上全部的babel转译都会来读取这个配置;
- 新增 plugin 配置,全部的东西都插件化,什么代码要转译都能在插件中自由配置;
- 新增 preset 配置,babel5会默认转译ES6和jsx语法,babel6转译的语法都要在perset中配置,preset简单说就是一系列plugin包的使用。
babel各个模块介绍
babel6将babel全家桶拆分红了许多不一样的模块,只有知道这些模块怎么用才能更好的理解babel。
下面的一些示例代码已经上传到了github,欢迎访问,欢迎star。
安装方式:
#经过npm安装 npm install babel-core babel-cli babel-node #经过yarn安装 yarn add babel-core babel-cli babel-node
一、babel-core
看名字就知道,babel-core是做为babel的核心存在,babel的核心api都在这个模块里面,好比:transform。
下面介绍几个babel-core中的api
- babel.transform:用于字符串转码获得AST
-
/*
-
* @param {string} code 要转译的代码字符串
-
* @param {object} options 可选,配置项
-
* @return {object}
-
*/
-
babel.transform(code: string, options?: Object)
-
-
//返回一个对象(主要包括三个部分):
-
{
-
generated code, //生成码
-
sources map, //源映射
-
AST //即abstract syntax tree,抽象语法树
-
}
-
更多关于AST知识点请看这里。
一些使用babel插件的打包或构建工具都有使用到这个方法,下面是一些引入babel插件中的源码:
-
//gulp-babel
-
const babel = require('babel-core');
-
/*
-
some codes...
-
*/
-
module.exports = function(opts){
-
opts = opts || {};
-
return through.obj(function(file, enc, cb){
-
try {
-
const fileOpts = Object.assign({}, opts, {
-
filename: file.path,
-
filenameRelative: file.relative,
-
sourceMap: Boolean(file.sourceMap),
-
sourceFileName: file.relative,
-
sourceMapTarget: file.relative
-
});
-
const res = babel.transform(file.contents.toString(), fileOpts);
-
if (res !== null) {
-
//some codes
-
}
-
} catch (err) {
-
//some codes
-
}
-
}
-
}
-
-
//babel-loader
-
var babel = require("babel-core");
-
/*
-
some codes...
-
*/
-
var transpile = functiontranspile(source, options){
-
//some code
-
try {
-
result = babel.transform(source, options);
-
} catch (error) {
-
//some codes
-
}
-
//some codes
-
}
-
-
//rollup-pugin-babel
-
import { buildExternalHelpers, transform } from 'babel-core';
-
/*
-
some codes...
-
*/
-
export default function babel ( options ){
-
//some codes
-
return {
-
// some methods
-
transform ( code, id ) {
-
const transformed = transform( code, localOpts );
-
//some codes
-
return {
-
code: transformed.code,
-
map: transformed.map
-
};
-
}
-
}
-
}
-
上面是一些打包工具引入babel插件时的一些源码,能够看到基本都是先经过调用transform方法进行代码转码。
- babel.transformFile
-
//异步的文件转码方式,回调函数中的result与transform返回的对象一至。
-
babel.transformFile("filename.js", options, function(err, result){
-
result; // => { code, map, ast }
-
});
-
- babel.transformFileSync
-
//同步的文件转码方式,返回结果与transform返回的对象一至。
-
babel.transformFileSync(filename, options) // => { code, map, ast }
-
- babel.transformFromAst
-
//将ast进行转译
-
const { code, map, ast } = babel.transformFromAst(ast, code, options);
-
二、babel-cli
babel-cli是一个经过命令行对js文件进行换码的工具。
使用方法:
- 直接在命令行输出转译后的代码
babel script.js
- 指定输出文件
babel script.js --out-file build.js 或者是 babel script.js -o build.js
让咱们来编写了一个具备箭头函数的代码:
-
//script.js
-
const array = [1,2,3].map((item, index)=> item * 2);
-
而后在命令行执行 babel script.js,发现输出的代码好像没有转译。
由于咱们没有告诉babel要转译哪些类型,如今看看怎么指定转译代码中的箭头函数。
babel --plugins transform-es2015-arrow-functions script.js
或者在目录里添加一个.babelrc文件,内容以下:
{ "plugins": [ "transform-es2015-arrow-functions" ] }
.babelrc是babel的全局配置文件,全部的babel操做(包括babel-core、babel-node)基本都会来读取这个配置,后面会详细介绍。
三、babel-node
babel-node是随babel-cli一块儿安装的,只要安装了babel-cli就会自带babel-node。
在命令行输入babel-node会启动一个REPL(Read-Eval-Print-Loop),这是一个支持ES6的js执行环境。
其实不用babel-node,直接在node下,只要node版本大于6大部分ES6语法已经支持,何况如今node的版本已经到了8.7.0。
babel-node还能直接用来执行js脚本,与直接使用node命令相似,只是会在执行过程当中进行babel的转译,而且babel官方不建议在生产环境直接这样使用,由于babel实时编译产生的代码会缓存在内存中,致使内存占用太高,因此咱们了解了解就好。
babel-node script.js
四、babel-register
babel-register字面意思能看出来,这是babel的一个注册器,它在底层改写了node的require方法,引入babel-register以后全部require并以.es6, .es, .jsx 和 .js为后缀的模块都会通过babel的转译。
一样经过箭头函数作个实验:
-
//test.js
-
const name = 'shenfq';
-
module.exports = ()=> {
-
const json = {name};
-
return json;
-
};
-
//main.js
-
require('babel-register');
-
var test = require('./test.js'); //test.js中的es6语法将被转译成es5
-
-
console.log(test.toString()); //经过toString方法,看看控制台输出的函数是否被转译
-
默认babel-register会忽略对node_modules目录下模块的转译,若是要开启能够进行以下配置。
-
require("babel-register")({
-
ignore: false
-
});
-
babel-register与babel-core会同时安装,在babel-core中会有一个register.js文件,因此引入babel-register有两种方法:
-
require('babel-core/register');
-
require('babel-register');
-
可是官方不推荐第一种方法,由于babel-register已经独立成了一个模块,在babel-core的register.js文件中有以下注释。
TODO: eventually deprecate this console.trace(“use the
babel-register
package instead ofbabel-core/register
“);
五、babel-polyfill
polyfill这个单词翻译成中文是垫片
的意思,详细点解释就是桌子的桌脚有一边矮一点,拿一个东西把桌子垫平。polyfill在代码中的做用主要是用已经存在的语法和api实现一些浏览器尚未实现的api,对浏览器的一些缺陷作一些修补。例如Array新增了includes方法,我想使用,可是低版本的浏览器上没有,我就得作兼容处理:
-
if (!Array.prototype.includes) {
-
Object.defineProperty(Array.prototype, 'includes', {
-
value: function(searchElement, fromIndex){
-
if (this == null) {
-
throw new TypeError('"this" is null or not defined');
-
}
-
var o = Object(this);
-
var len = o.length >>> 0;
-
if (len === 0) {
-
return false;
-
}
-
var n = fromIndex | 0;
-
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
-
while (k < len) {
-
if (o[k] === searchElement) {
-
return true;
-
}
-
k++;
-
}
-
return false;
-
}
-
});
-
}
-
上面简单的提供了一个includes方法的polyfill,代码来自MDN。
理解polyfill的意思以后,再来讲说babel为何存在polyfill。由于babel的转译只是语法层次的转译,例如箭头函数、解构赋值、class,对一些新增api以及全局函数(例如:Promise)没法进行转译,这个时候就须要在代码中引入babel-polyfill,让代码完美支持ES6+环境。前面介绍的babel-node就会自动在代码中引入babel-polyfill包。
引入方法:
-
//在代码的最顶部进行require或者import
-
-
require("babel-polyfill");
-
-
import "babel-polyfill";
-
-
//若是使用webpack,也能够在文件入口数组引入
-
module.exports = {
-
entry: ["babel-polyfill", "./app/js"]
-
};
-
但不少时候咱们并不会使用全部ES6+语法,全局添加全部垫片确定会让咱们的代码量上升,以后会介绍其余添加垫片的方式。
.babelrc
前面已经介绍了babel经常使用的一些模块,接下来看看babel的配置文件 .babelrc
。
后面的后缀rc来自linux中,使用过linux就知道linux中不少rc结尾的文件,好比.bashrc
,rc是run command
的缩写,翻译成中文就是运行时的命令,表示程序执行时就会来调用这个文件。
babel全部的操做基本都会来读取这个配置文件,除了一些在回调函数中设置options参数的,若是没有这个配置文件,会从package.json
文件的babel属性中读取配置。
plugins
先简单介绍下 plugins ,babel中的插件,经过配置不一样的插件才能告诉babel,咱们的代码中有哪些是须要转译的。
这里有一个babel官网的插件列表,里面有目前babel支持的所有插件。
举个例子:
-
{
-
"plugins": [
-
"transform-es2015-arrow-functions", //转译箭头函数
-
"transform-es2015-classes", //转译class语法
-
"transform-es2015-spread", //转译数组解构
-
"transform-es2015-for-of" //转译for-of
-
]
-
}
-
//若是要为某个插件添加配置项,按以下写法:
-
{
-
"plugins":[
-
//改成数组,第二个元素为配置项
-
["transform-es2015-arrow-functions", { "spec": true }]
-
]
-
}
-
上面这些都只是语法层次的转译,前面说过有些api层次的东西须要引入polyfill,一样babel也有一系列插件来支持这些。
-
{
-
"plugins":[
-
//若是咱们在代码中使用Object.assign方法,就用以下插件
-
"transform-object-assign"
-
]
-
}
-
-
//写了一个使用Object.assign的代码以下:
-
const people = Object.assign({}, {
-
name: 'shenfq'
-
});
-
//通过babel转译后以下:
-
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; };
-
-
const people = _extends({}, {
-
name: 'shenfq'
-
});
-
这种经过transform添加的polyfill只会引入到当前模块中,试想实际开发中存在多个模块使用同一个api,每一个模块都引入相同的polyfill,大量重复的代码出如今项目中,这确定是一种灾难。另一个个的引入须要polyfill的transform挺麻烦的,并且不能保证手动引入的transform必定正确,等会会提供一个解决方案:transform-runtime
。
除了添加polyfill,babel还有一个工具包helpers,若是你有安装babel-cli,你能够直接经过下面的命令把这个工具包输出:
./node_modules/.bin/babel-external-helpers > helpers.js
这个工具包相似于babel的utils模块,就像咱们项目中的utils同样,不少地方都会用到,例如babel实现Object.assign就是使用的helpers中的_extend方法。为了不同一个文件屡次引用babel的助手函数,经过external-helpers
插件,可以把这些助手函数抽出放到文件顶部,避免屡次引用。
-
//安装: cnpm install --save-dev babel-plugin-external-helpers
-
-
//配置
-
{
-
"plugins": ["external-helpers"]
-
}
-
虽然这个插件能避免一个文件屡次引用助手函数,可是并不能直接避免多个文件内重复引用,这与前面说到的经过transform添加polyfill是同样的问题,这些引用都只是module级别的,在打包工具盛行的今天,须要考虑如何减小多个模块重复引用相同代码形成代码冗余。
固然也能够在每一个须要使用helpers的js文件顶部直接引入以前生成的helpers文件既可,经过打包工具将这个公共模块进行抽离。
-
require('helpers');
-
在说完babel的helpers以后就到了插件系统的最后的一个插件:transform-runtime
。前面在transform-polyfill的时候也有提到这个插件,之因此把它放到helpers后面是由于这个插件能自动为项目引入polyfill和helpers。
cnpm install -D babel-plugin-transform-runtime babel-runtime
transform-runtime这个插件依赖于babel-runtime,因此安装transform-runtime的同时最好也安装babel-runtime,为了防止一些没必要要的错误。babel-runtime由三个部分组成:
- core-js
core-js极其强悍,经过ES3实现了大部分的ES五、六、7的垫片,做者zloirock是来自战斗名族的程序员,一我的维护着core-js,据说他最近还在找工做,上面是core-js的github地址,感兴趣能够去看看。
- regenerator
regenerator来自facebook的一个库,用于实现 generator functions。
- helpers
babel的一些工具函数,没错,这个helpers和前面使用babel-external-helpers生成的helpers是同一个东西
从babel-runtime的package.json文件中也能看出,runtime依赖了哪些东西。
安装有babel-runtime以后要引入helpers可使用以下方式:
-
require('babel-runtime/helpers');
-
使用runtime的时候还有一些配置项:
-
{
-
"plugins": [
-
["transform-runtime", {
-
"helpers": false, //自动引入helpers
-
"polyfill": false, //自动引入polyfill(core-js提供的polyfill)
-
"regenerator": true, //自动引入regenerator
-
}]
-
]
-
}
-
比较transform-runtime与babel-polyfill引入垫片的差别:
- 使用runtime是按需引入,须要用到哪些polyfill,runtime就自动帮你引入哪些,不须要再手动一个个的去配置plugins,只是引入的polyfill不是全局性的,有些局限性。并且runtime引入的polyfill不会改写一些实例方法,好比Object和Array原型链上的方法,像前面提到的
Array.protype.includes
。 - babel-polyfill就能解决runtime的那些问题,它的垫片是全局的,并且全能,基本上ES6中要用到的polyfill在babel-polyfill中都有,它提供了一个完整的ES6+的环境。babel官方建议只要不在乎babel-polyfill的体积,最好进行全局引入,由于这是最稳妥的方式。
- 通常的建议是开发一些框架或者库的时候使用不会污染全局做用域的babel-runtime,而开发web应用的时候能够全局引入babel-polyfill避免一些没必要要的错误,并且大型web应用中全局引入babel-polyfill可能还会减小你打包后的文件体积(相比起各个模块引入重复的polyfill来讲)。
presets
显然这样一个一个配置插件会很是的麻烦,为了方便,babel为咱们提供了一个配置项叫作persets(预设)。
预设就是一系列插件的集合,就好像修图同样,把上次修图的一些参数保存为一个预设,下次就能直接使用。
若是要转译ES6语法,只要按以下方式配置便可:
-
//先安装ES6相关preset: cnpm install -D babel-preset-es2015
-
{
-
"presets": ["es2015"]
-
}
-
-
//若是要转译的语法不止ES6,还有各个提案阶段的语法也想体验,能够按以下方式。
-
//安装须要的preset: cnpm install -D babel-preset-stage-0 babel-preset-stage-1 babel-preset-stage-2 babel-preset-stage-3
-
{
-
"presets": [
-
"es2015",
-
"stage-0",
-
"stage-1",
-
"stage-2",
-
"stage-3",
-
]
-
}
-
-
//一样babel也能直接转译jsx语法,经过引入react的预设
-
//cnpm install -D babel-preset-react
-
{
-
"presets": [
-
"es2015",
-
"react"
-
]
-
}
-
不过上面这些preset官方如今都已经不推荐了,官方惟一推荐preset:babel-preset-env
。
这款preset能灵活决定加载哪些插件和polyfill,不过仍是得开发者手动进行一些配置。
-
// cnpm install -D babel-preset -env
-
{
-
"presets": [
-
["env", {
-
"targets": { //指定要转译到哪一个环境
-
//浏览器环境
-
"browsers": ["last 2 versions", "safari >= 7"],
-
//node环境
-
"node": "6.10", //"current" 使用当前版本的node
-
-
},
-
//是否将ES6的模块化语法转译成其余类型
-
//参数:"amd" | "umd" | "systemjs" | "commonjs" | false,默认为'commonjs'
-
"modules": 'commonjs',
-
//是否进行debug操做,会在控制台打印出全部插件中的log,已经插件的版本
-
"debug": false,
-
//强制开启某些模块,默认为[]
-
"include": ["transform-es2015-arrow-functions"],
-
//禁用某些模块,默认为[]
-
"exclude": ["transform-es2015-for-of"],
-
//是否自动引入polyfill,开启此选项必须保证已经安装了babel-polyfill
-
//参数:Boolean,默认为false.
-
"useBuiltIns": false
-
}]
-
]
-
}
-
关于最后一个参数useBuiltIns
,有两点必需要注意:
- 若是useBuiltIns为true,项目中必须引入babel-polyfill。
- babel-polyfill只能被引入一次,若是屡次引入会形成全局做用域的冲突。
作了个实验,一样的代码,只是.babelrc
配置中一个开启了useBuiltIns
,一个没有,两个js文件体积相差70K,戳我看看。
useBuiltIns.js | 189kb |
notUseBuiltIns.js | 259kb |
最后啰嗦一句
关于polyfill还有个叫作polyfill.io的神器,只要在浏览器引入
服务器会更具浏览器的UserAgent返回对应的polyfill文件,很神奇,能够说这是目前最优雅的解决polyfill过大的方案。
前先后后写完这个差很少写了一个星期,查了不少资料(babel的官网和github都看了好几遍),总算憋出来了。