无处不在的Babel

小龙 2017-4-18 HACK 0 0

背景

海子在《面朝大海,春暖花开》中写到:“从明天起,做一个幸福的人”,Babel在介绍自己时说到:“从今天起,做一个幸福的人,使用下一代javascript”。

JavaScript学名叫ECMAScript,主要版本有2000年发布的ES3,2010年发布的ES5,以及2015年发布的ES2015。其中,ES2015相对于ES5简直是翻天覆地的变化,大量新特性极好地解决了开发人员面临的问题。然而市场份额占比极高的Internet Explorer 9发布于2011年,对ECMAScript的支持只到第5版。

难道为了兼容低版本浏览器用户而就此放弃?不!在新达达,我们拥抱社区,跟随潮流,始终走在技术前沿。有了Babel,使用下一代JavaScript成为可能。如今,从手机应用到桌面网站,再到后端服务,Babel的身影无处不在。但通往幸福的路上,我们遇到不少困难。随着项目复杂度增加,文件打包体积变大,程序运行性能变差。这个问题在高性能设备上还好,在低端安卓手机上表现尤为明显。

困难

以IE9为例,它对ECMAScript的支持只到第5版,为了能使用ES2015,我们需要将ES2015转译成ES5,这样才能被IE9正确执行。在这个过程中,有些特性能用ES5直接实现,比如箭头函数:

// ES2015const square = n => n * n// ES5"use strict";var square = functionsquare(n){return n * n;};

但有些特性则不行,比如创建类:

// ES2015classPerson{}// ES5"use strict";function_classCallCheck(instance, Constructor){ if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }var Person = functionPerson(){_classCallCheck(this, Person);};

类似 _classCallCheck 的函数叫helper,每当遇到创建类这种无法直接实现的特性时,Babel都会在该文件内嵌入helper,这导致整个项目包含大量重复代码。对于这个问题,大多数人推荐的做法是引入 babel-polyfill ,对已有执行环境进行扩展,但这样做一会导致文件打包体积变大,二会导致特性检测失败。另一种做法是使用 transform-runtime 插件,改变Babel默认行为,将文件内嵌入helper改为引入外部模块,这样整个项目只存在一份helper的代码,文件打包体积变大的问题迎刃而解。由于是引入外部模块而非扩展执行环境,特性检测失败的问题也就不复存在。

一切看起来都很美好,但新的问题出现了。在使用Babel转译代码时,通常只对项目源码进行处理,第三方模块保持不变。当第三方模块依赖一个ES2015特性时,由于不被处理,自然无法执行。这时需要引入额外的模块,比如GitHub出品的 whatwg-fetch :

import 'es6-promise';import 'whatwg-fetch';

babel-presets-es2015 本身包含Promise,现在为了保证 whatwg-fetch 正常执行而重复引入,有没有什么办法能够避免?

除此之外,还有一个问题比较隐蔽。在使用ES2015语法引入模块时,通常会这样写:

import { find } from 'lodash'

看似没有问题,实际上问题很大。上面的代码经Babel转译后:

// ES2015import { find } from 'lodash'// ES5'use strict';var _lodash = require('lodash');

原本只是想引入1kb的 find 函数,最终却变成引入540kb的 lodash ,完全无法接受。类似的问题还存在其它地方,怎样解决?

基础

所有这些问题的解决都离不开对Babel的深入理解,就从Babel的安装、配置开始,逐步深入到 plugins 和 presets 的区别,源码转译顺序, ECMAScript Modules 和 CommonJS 的区别等问题上。

安装

Babel的安装主要针对浏览器和Node.js两种环境,构建方式主要是命令行和构建工具两种,这里只关注浏览器环境下基于Webpack的构建方式。新建项目,并执行以下命令:

$ npm i -D webpack babel-loader babel-core

其中 babel-loader 的作用是让Webpack能够通过Babel转译JavaScript,而 babel-core 才是真正负责JavaScript转译的工具。

安装完后,对Webpack进行如下配置,就能自动转译 main.js 及其引入的其它模块:

module.exports = {entry: './main.js',output: {filename: 'bundle.js'},module: {loaders: [{test: /\.js$/,loader: 'babel-loader',exclude: /node_modules/}]}}配置

有了上述配置,Babel就能自动转译,但如何转译特定的语言特性仍需配置。通常会将Babel的配置保存为单独的 .babelrc 文件,该文件的内容为JSON格式,主要包含 presets 和 plugins 两部分。

{"presets": [],"plugins": []}Plugins vs. Presets

简单来说, plugins 是包含规则的函数,在转译时用来处理输入内容。通常以 babel-plugin- 作为前缀,因此在书写时可以省略:

$ npm i -D babel-plugin-transform-es2015-arrow-functions{"presets": [],"plugins": ["transform-es2015-arrow-functions"]}

而 presets 则是 plugins 的合集,省却一个个安装的繁琐过程,比如 babel-preset-es2015 包含所有ES2015的语言特性:

$ npm i -D babel-preset-es2015{"presets": ["es2015"],"plugins": ["transform-es2015-arrow-functions"]}转译顺序

当 presets 和 plugins 同时存在时,按照什么顺序进行转译?Babel官方文档提及如下规则:

plugins 先于 presets 执行plugins 的执行顺序是从头到尾presets 的执行顺序和 plugins 相反

假定配置如下:

{"presets": ["es2015"],"plugins": ["transform-async-to-generator"]}

源码如下:

const beautify = data => JSON.stringify(data, null, 2)async functionfetchUserDetail(userID){return await fetch(`/user/${userID}`)}

第一步:

let fetchUserDetail = (() => {var _ref = _asyncToGenerator(function* (userID){return yield fetch(`/user/${ userID }`);});return functionfetchUserDetail(_x){return _ref.apply(this, arguments);};})();function_asyncToGenerator(fn){...}const beautify = data => JSON.stringify(data, null, 2);

第二步:

let fetchUserDetail = (() => {var _ref = _asyncToGenerator(function* (userID){return yield fetch(`/user/${ userID }`);});return functionfetchUserDetail(_x){return _ref.apply(this, arguments);};})();function_asyncToGenerator(fn){...}var beautify = functionbeautify(data){return JSON.stringify(data, null, 2);};

由于Async函数的转译规则存在于 plugins 中,所以在第一步中先被转译;箭头函数转译规则存在于 presets 中,所以在第二步中才被转译。

ECMAScript Modules vs. CommonJS

从ES2015开始,JavaScript终于有了标准的模块定义。尽管暂时没有任何一个环境实现原生支持,但这并不能阻止大家对它的热爱。在ECMAScript Modules规范出来之前,存在着AMD、CommonJS、UMD等规范,其中CommonJS规范最受欢迎。

一个符合CommonJS规范的模块,当使用 require 引入的时候,它的执行流程是这样的:
无处不在的Babel

简单来说,首先找到这个模块在系统中的路径;然后根据它是指向内置模块还是本地文件来进行加载;而后,如果是本地文件,则将文件内容包装成一个函数;接下来执行这个函数;最后,缓存执行的结果。整个流程中最重要的环节是包装。假定有如下代码:

const bar = 1module.exports.bar = bar

包装后:

function(exports, require, module, __filename, __dirname){const bar = 1module.exports.bar = bar}

在执行之前,无法得知该模块是否导出bar或foo(不存在)。而ECMAScript Modules则不同:

export const bar = 1

根据规范,ECMAScript Modules支持静态分析,无需执行就能从词法上得知bar会被导出,而foo(不存在)不会。

解决按需引入

有了这些基础,我们知道要解决文件打包体积大,程序运行性能差这个问题,按需引入helper是最佳方案。配置如下:

$ npm i -D babel-plugin-transform-runtime$ npm i -S babel-runtime{"plugins": ["transform-runtime","transform-es2015-classes"]}

其中 babel-runtime 包含所有 helper 及其他相关库,而 babel-plugin-transform-runtime 则可以改变Babel默认行为,使其始终从 babel-runtime 引入所需模块。假定有以下代码:

classPerson{sayHi() {}}

转译后:

import _classCallCheck from "babel-runtime/helpers/classCallCheck";import _createClass from "babel-runtime/helpers/createClass";let Person = function(){functionPerson(){_classCallCheck(this, Person);}_createClass(Person, [{key: "sayHi",value: functionsayHi(){}}]);return Person;}();特殊处理

但这种方案带来的重复问题如何解决?比如:

import 'es6-promise';import 'whatwg-fetch';

前面提到,默认情况下,我们只处理项目源码,不管第三方模块。那么如果同样处理第三方模块,这个问题能否解决?经过一番研究,我们发现Webpack有个不起眼的功能正好解决这个问题:

const path = require('path')module.exports = {module: {loaders: [{test: /\.js$/,include: [path.resolve(__dirname, 'src'),/whatwg-fetch/],loader: 'babel-loader'}]}}

除了 exclude 选项,Webpack还支持 include ,含义正如其名:只包含特定目录,而且这个选项还支持正则匹配。那么问题就简单了,让所有项目源码通过Babel进行转译,同时包含特定第三方模块。

自动修正

在知道ECMAScript Modules和CommonJS的区别后,这个问题的解决思路也就变得清晰起来。既然通过ES2015解构的方式引入会导致文件体积大,那么改为单个文件引入不就好了?

// 错误引入import { find, pick } from 'lodash'// 正确引入import find from 'lodash/find'import pick from 'lodash/pick'

引入内容不多的时候还好,一旦数量上去了,这将是一个极为繁琐的过程。既然Babel可以转译源码,自然就可以改写源码,如果能让Babel自动完成上述过程,岂不是完美?

npm i -D babel-plugin-transform-imports{"plugins": [["transform-imports", {"lodash": {"transform": "lodash/${member}","preventFullImport": true}}]]}

借助 transform-imports ,通过几行简单配置,自动简化繁琐过程,并阻止完全引入,有什么理由不爱Babel?

总结和展望

在解决文件打包体积大,程序运行性能差这个问题的过程中,我们意识到以下几点:

积极拥抱社区,跟随技术潮流,避免闭门造车,交流和分享总能带来新的思路习以为常的东西并不见得真懂,不要因为暂时没用就不去了解,保持好奇心往往能带来意想不到的收获基础、基础、基础,在日新月异的前端大时代,不要迷失在各种变化中,多花些时间和精力在不变的领域

在新达达,前端日益重要,每一个高速发展且影响重大的产品中都少不了前端的身影。欢迎感兴趣的同学加入新达达,让我们共同搭乘新达达这艘火箭探索未知的世界!

更多关于新达达技术的文章,敬请关注新达达技术公众号。DADA TECH

转载请注明来自华盟网,本文标题:《无处不在的Babel》

喜欢 (0) 发布评论