近期,在波洞星球的PC官网项目中,咱们采用了新版的 babel7 做为 ES 语法转换器。而 babel7 中的一大变动就是对配置文件的加载逻辑进行了改进,然而实际上对于不熟悉 babel 配置逻辑的朋友每每会带来更多问题。本文就是 babel7 配置文件的中文指南,它是英语渣渣的救星,是给懒人送到口边的一道美味。若有错误 概不负责 欢迎指正。前端
babel7 从 2018年3月开始进入 alpha 阶段,时隔5个月直到 2018年8月份 release 第一个版本,目前的最新版是2019年2月26号发布的 7.3.4. 时光如梭,在这美好的 9012 年,ES2019 都快要发布了的时刻,我想: 是时候用一用 babel7 了。vue
本文不是 babel7 的升级教程,而是对 babel7 的新变化和配置逻辑的一点心得。babel7 对monorepo 结构项目的优化刚好符合咱们目前项目架构的预期,这简化了咱们配置的复杂度,但其难以理解的配置加载逻辑,却让我踩了很多坑,这也正是本文的来源。node
在开始讲 babel7 的配置逻辑以前,咱们先从如下几个方面来啰嗦几句 babel7 所作的变动及其逻辑意义。react
在历史上(babel6)的时代,人们一般使用 babel 提供的 preset-stage 预设来体验 ES6 以后的处于建议阶段的语法特性。例如作以下的 babel 配置:webpack
"presets": ["es2015", "react", "stage-0"]
其中,es2015 预设会包含 ES6 标准中全部语法特性;stage-0预设会包含当前(安装该预设npm包的时刻) 的 ES 语法进展中的 stage 0到3的特性(数字小的包含数字大的)。但事实上 babel 官方这样提供 stage 预设,会有很多问题git
例如:github
现在,babel 官方认为,把不稳定的 stage0-3 做为一种预设是不太合理的,所以废弃了 stage 预设,转而让用户本身选择使用哪一个 proposal 特性的插件,这将带来更多的明确性(用户无须理解 stage,本身选的插件,本身便能明确的知道代码中可使用哪一个特性)。全部建议特性的插件,都改变了命名规范,即相似 @babel/plugin-proposal-function-bind
这样的命名方式来代表这是个 proposal 阶段特性。web
对于正经的 ES 标准特性,babel从6开始就建议使用 babel-preset-env 这个能根据环境进行自动配置的预设。到了 babel7,咱们就能够彻底告别这几个历史预设了: preset-es2015/es2016/es2017/latestnpm
为何 preset-env 要更好呢?json
我认为,对于开发者而言,关注目标用户平台(兼容哪些浏览器)要比关注 "编译为哪份ES标准" 要更易理解。把选择编译插件的事情交给 preset-env 就行了。它会根据 compat table 和你设置的目标用户平台选择正确的插件。
跟 stage 预设的结局同样,对于处于建议阶段的特性,polyfill里面也移除了对他们的支持。
之前的 babel-polyfill 是这么实现的:
import "core-js/shim"; // included < Stage 4 proposals import "regenerator-runtime/runtime"
如今的 @babel/polyfill 就直接引入 core-js v2 的属于ES正式标准的模块。这意味着,若是你须要使用处于 proposal 阶段的语法特性,你须要手工 import core-js 中的对应模块。
从 babel7 开始,全部的官方插件和主要模块,都放在了 @babel 的命名空间下。从而能够避免在 npm 仓库中 babel 相关名称被抢注的问题。有必要说一下的,好比 @babel/node @babel/core @babel/clil @babel/preset-env
之前的 babel-transform-runtime
是包含了 helpers 和 polyfill。而如今的 @babel/runtime
只包含 helper,若是须要 polyfill,则需主动安装 core-js 的 runtime版本 @babel/runtime-corejs2
。并在 @babel/plugin-transform-runtime
的插件中作配置。
这是本文的重点,先来看一段 babel7 对配置的变动说明
Babel has had issues previously with handling node_modules, symlinks, and monorepos. We've made some changes to account for this: Babel will stop lookup at the package.json boundary instead of looking up the chain. For monorepo's we have added a new babel.config.js file that centralizes our config across all the packages (alternatively you could make a config per package). In 7.1, we've introduced a rootMode option for further lookup if necessary.
段落的意思大概有这么几点:
除此以外,babel7 还有一个特性是:
然而,对上面的解释,你可能: 每一个字都认识,连在一块儿殊不知道在说什么。下面咱们来剖析一下
为了理解 babel7 的配置逻辑,咱们就以 babel7 真正所解决的痛点 [monorepo 类型的项目] 为例来剖析。在此以前,咱们须要预先肯定几个概念。
monorepo。这是个自造词。按个人理解,它的含义是说 单个大项目可是包含多个子项目
的含义。若是仍是不能理解的话,就把 项目 二字 换成 npm模块包
(以package.json文件做为分界线)。即 单个npm包中又包含多个子npm包
的项目。
例如,波洞的 PC 版采用的是 Node.js 做为前端接入层的方式,在咱们的项目结构组织上,是这样的:
|- backend |-package.json |- frontend |-package.json |- node_modules |- config.js |- babel.config.js |- package.json
这就是典型的 monorepo 结构。
懂了几种配置文件的概念和做用范围以后,咱们就能够来根据文档和代码测试结果来精确描述 babel7 的配置规则。这里咱们直接以 monorepo 类型项目为例来讲,由于普通项目会更简单。
下文中可能用到的名词解释:
咱们用 package 来代指一个具备独立 package.json 的项目,如上面案例中的 frontend 能够称做一个 package,backend也能够称做一个package; 咱们用 相对配置 这个名词来表达所谓的 .babelrc 和 .babelrc.js,用全局配置来代指 babel.config.js这份配置
对monorepo类型项目,babel7 的处理逻辑是:
【全局配置】全局配置 babel.config.js 里的配置默认对整个项目生效,包括node_modules。除非经过 exclude 配置进行剔除。
【全局配置】全局配置中若是没有配置 babelrcRoots 字段,那么babel 默认状况下不会加载任何子package中的相对配置(如.babelrc文件)。除非在全局配置中经过 babelrcRoots 字段进行配置。
【全局配置】babel 全局配置文件所在的位置就决定了你的项目根目录在哪里,默认就是执行babel的当前工做目录,例如上面的例子,你在根目录执行babel,babel才能找到babel.config.js,从而肯定该monorepo的根目录,进而将配置对整个项目生效
【相对配置】相对配置可被加载的前提是在 babel.config.js 中配置了 babelrcRoots. 如 babelrcRoots: ['.', './frontend'],这表示要对当前根目录和frontend这个子package开启 .babelrc 的加载。(注意: 项目根目录除了能够拥有一个 babel.config.js,同时也能够拥有一个 .babelrc 相对配置)
【相对配置】相对配置加载的边界是当前package的最顶层。假设上文案例中要编译 frontend/src/index.js 那么,该文件编译时能够加载 frontend 下的 .babelrc 配置,但没法向上检索总项目根目录下的 .babelrc
仍是以上面的代码结构为例。
|- backend |-package.json |- frontend |-package.json |- node_modules |- config.js |- babel.config.js |- package.json
该案例中,咱们思考发现,咱们须要利用 babel7 的全局配置能力
。缘由在于,monrepo 中存在多个 子 package。因为 babel7 默认检索 babelrc 的边界是 当前package。所以每一个package中撰写的babelrc只会对当前package生效,这会致使咱们的frontend中依赖根目录的config.js时没法获得正确的编译;另外一个问题是: frontend和backend中的相同的babel配置部分没法共享 存在必定冗余。为此,咱们须要在项目根目录设置一个 babel.config.js的配置,用它再配合babelrc来作babel配置的共享和融合。
可是,问题很快来了:当工做目录不在根目录时,没法加载到全局配置
。咱们的前端编译脚本一般放置在 frontend目录下,(咱们执行编译的工做目录是在 frontend 中),此时 babel build 行为的 工做目录 即是 frontend. 因为 babel 默认只在当前目录寻找 babel.config.js 这个全局配置,所以会致使没法找到根目录的 babel.config.js,这样咱们所设想的整个项目的全局配置就没法生效。 幸亏,babel7 提供了 rootMode 选项,能够将它指定为 "upward", 这样babel 会自动向上寻找全局配置,并肯定项目的根目录位置。
设置方法:
CLI: babel --rootMode=upward
webpack: 在 babel-loader 的配置上设置 rootMode: 'upward'
如今,全局配置有了,咱们能够在里面配置 babel 转译规则,它能够对全项目生效,frontend下的 vue.js 编译天然没有问题了。
不过,假设咱们 backend 项目中也要使用 babel 转译(目前咱们实际在 backend 中并无使用,由于咱们认为只图esmodule而多加一层编译得不偿失),那么必然 backend 与 frontend 中的编译配置是不一样的
,frontend 须要加载 vue 的 jsx 插件和polyfill (useBuiltIns: usage,modules: false),而backend只须要转译基本模块语法(modules: true, useBuiltIns: false)。该场景的解决方案即是,为每一个子 package 提供独立的 .babelrc 相对配置,在全局 babel.config.js 中设置共用的配置。此时项目组织结构以下:
|- backend |- .babelrc.js |-package.json |- frontend |- .babelrc.js |-package.json |- node_modules |- config.js |- .babelrc.js // 这份配置在本场景下不须要(若是根目录下的代码有区别于子package的babel配置,则须要使用) |- babel.config.js |- package.json
根目录的 babel.conig.js 配置应该以下:
const presets = [ // 根、frontend、backend 的公共预设 ] const plugins = [ // 根、frontend、backend 的公共插件 ] module.exports = { presets, plugins, babelrcRoots: ['.', './frontend', './backend'] // 容许这两个子 package 加载 babelrc 相对配置 }
觉得此时已经高枕无忧了?navie,因为咱们前端 Vue.js 采用 webpack 打包。实际开发过程当中发现,这种配置会形成 webpack 打包模块时出现故障,故障缘由在于:同一个模块中错误混用 esmodule 和 commonjs 语法会形成 webpack故障
。
前文讲到 全局配置 global.config.js 会做用到 整个项目
,甚至包括 node_modules。所以babel编译时会同时编译 node_modules 下的模块,虽然模块做者不可能在一个js文件中混用不一样模块语法,但他们做为释出包 一般是commonjs的模块语法。 而preset-env预设在编译时会经过 usage
方式 默认注入import语法的 polyfill
Since Babel defaults to treating files are ES modules, generally these plugins/presets will insert import statements
这即是蛋疼的来源:babel加载过的node_modules模块会变成 同一个js文件里既有commonjs语法又有esmodule语法。
解决方案:不要对 node_modules 下的模块采用babel编译。咱们须要在 babel.config.js 配置中增长选项:
exclude: /node_modules/
至此,咱们的 monorepo 项目就可使用一份 全局配置+两份相对配置,实现分别对 前端和后端 进行合理的ES6+语法的编译了。这是咱们配置工程师的一小步,可是前端走向将来语法的一大步。
总结 babel7 的配置加载逻辑以下:
待编译文件
生效的配置,子package若想加载.babelrc是须要babel配置babelrcRoots才能够(父package自身的babelrc是默承认用的)。