Show me the code,babel 7 最佳实践!

前言

本文首发于 github 博客
如对你有帮助是个人荣幸,你的 star 是对我最大的支持!javascript

你们都知道 babel 是兼容对 ES6 支持不完善的低版本浏览器的转换编译器。vue

而 babel 其实主要作的只有两件事情:java

  • 语法转换
  • 新 API 的 polyfill 兼容

那么废话少说,咱们直接点,直接说说常见几个场景下兼容旧版浏览器的方案。react

跳过直接看结论webpack

实践方案

polyfill.io

若是你的工程是用的语法是 ES5,可是用了一些 ES6+ 的API特性,那么能够直接引入:git

<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
复制代码

来兼容 Web 应用不支持的 API。es6

原理大概是 polyfill.io 会读取每一个请求的User-Agent标头,并返回适合请求浏览器的polyfill。具体的还能够本身指定加载哪些 特性的 polyfill,具体想了解更多的你们能够看看 官方文档github

优势:每一个浏览器的设备加载的 polyfill 都不同,最新的彻底兼容ES6栋浏览器基本加载的 polyfill 大小为0。web

缺点:vue-cli

  1. 必须先进行语法转换,用了 async 语法在新浏览器上能够运行,可是在旧版浏览器就直接抛出错误了。
  2. 不能按照代码所用到的新特性按需进行 polyfill,也就是说即使你的 Web 应用只用到了 es6.array.from 特性,polyfill.io 依然可能会把该浏览器全部不支持的特性(如:es6.promise,es6.string.includes等特性)所有加载进来。

@babel/preset-env 按需加载

上面提到了 polyfill.io 的一个缺点是没法按需引入,那么如今就介绍下 babel7 @babel/preset-env

@babel/preset-env 默认根据 .browserslist 所填写的须要兼容的浏览器,进行必要的代码语法转换和 polyfill

// .babelrc.js
module.exports = {
    presets: [
        [
            "@babel/preset-env",
            
            {
                "modules": false, // 模块使用 es modules ,不使用 commonJS 规范,具体看文末附录
                "useBuiltIns": 'usage', // 默认 false, 可选 entry , usage
            }
        ]
    ]
}
复制代码

此处重点介绍一下其新推出的 useBuiltIns 选项:

  1. false : 不启用polyfill, 若是在业务入口 import '@babel/polyfill', 会无视 .browserslist 将全部的 polyfill 加载进来。
    image

    polyfill 所有加载进来有 284 个特性包
  2. entry : 启用,须要手动 import '@babel/polyfill' 才生效(不然会抛出错误:regeneratorRuntime undefined), 根据 .browserslist 过滤出 须要的 polyfill (相似 polyfill.io 方案)
    image

    使用entry根据browserslist(ie>10)加载进来的有 238 个特性包
  3. usage : 不须要手动import '@babel/polyfill'(加上也无妨,编译时会自动去掉), 且会根据 .browserslist + 业务代码使用到的新 API 按需进行 polyfill
    image

    使用usage根据browserslist(ie>10)+代码用到的,加载进来的只有 51 个特性包

    usage 风险项:因为咱们一般会使用不少 npm 的 dependencies 包来进行业务开发,babel 默认是不会检测 依赖包的代码的。

    也就是说,若是某个 依赖包使用了 Array.from, 可是本身的业务代码没有使用到该API,构建出来的 polyfill 也不会有 Array.from, 如此一来,可能会在某些使用低版本浏览器的用户出现 BUG。

    因此避免这种状况发生,通常开源的第三方库发布上线的时候都是转换成 ES5 的。


上面提到的 useBuiltIns:'usage' 彷佛已经很完美解决咱们的须要了,可是咱们构建的时候发现:

// es6+ 源码:
const asyncFun = async ()=>{
  await new Promise(setTimeout, 2000)
  
  return '2s 延时后返回字符串'
}
export default asyncFun
复制代码

根据上述的 useBuiltIns:'usage' 配置编译后:

import "core-js/modules/es6.promise";
import "regenerator-runtime/runtime";

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }

function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }

var asyncFun =
/*#__PURE__*/
function () {
  var _ref = _asyncToGenerator(
  /*#__PURE__*/
  regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return new Promise(setTimeout, 2000);

          case 2:
            return _context.abrupt("return", '2s 延时后返回字符串');

          case 3:
          case "end":
            return _context.stop();
        }
      }
    }, _callee, this);
  }));

  return function asyncFun() {
    return _ref.apply(this, arguments);
  };
}();

export default asyncFun;

复制代码

上述代码中,咱们看到,asyncGeneratorStep, _asyncToGenerator 这两个函数是被内联进来,而不是 import 进来的。

也就是说,若是你有多个文件都用到了 async,那么每一个文件都会内联一遍 asyncGeneratorStep, _asyncToGenerator 函数

这代码明显是重复了,那么有什么方法能够进行优化呢? 答案是 @babel/plugin-transform-runtime

@babel/plugin-transform-runtime

babel 在每一个须要的文件的顶部都会插入一些 helpers 代码,这可能会致使多个文件都会有重复的 helpers 代码。 @babel/plugin-transform-runtime 的 helpers 选项就能够把这些模块抽离出来

// .babelrc.js
module.exports = {
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": false, // 默认值,能够不写
                "helpers": true, // 默认,能够不写
                "regenerator": false, // 经过 preset-env 已经使用了全局的 regeneratorRuntime, 再也不须要 transform-runtime 提供的 不污染全局的 regeneratorRuntime
                "useESModules": true, // 使用 es modules helpers, 减小 commonJS 语法代码
            }
        ]
    ],
    presets: [
        [
            "@babel/preset-env",
            
            {
                "modules": false, // 模块使用 es modules ,不使用 commonJS 规范 
                "useBuiltIns": 'usage', // 默认 false, 可选 entry , usage
            }
        ]
    ]
}
复制代码
// 添加新配置后编译出来的代码
import "core-js/modules/es6.promise";
import "regenerator-runtime/runtime";
import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator";

var asyncFun =
/*#__PURE__*/
function () {
  var _ref = _asyncToGenerator(
  /*#__PURE__*/
  regeneratorRuntime.mark(function _callee() {
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return new Promise(setTimeout, 2000);

          case 2:
            return _context.abrupt("return", '2s 延时后返回字符串');

          case 3:
          case "end":
            return _context.stop();
        }
      }
    }, _callee, this);
  }));

  return function asyncFun() {
    return _ref.apply(this, arguments);
  };
}();

export default asyncFun;
复制代码

能够看到,已经没有了内联的 helpers 代码,大功告成。

总结

若是没有什么特殊的需求,使用 babel 7 的最佳配置是:

  1. 首先安装依赖包: npm i -S @babel/polyfill @babel/runtime && npm i -D @babel/preset-env @babel/plugin-transform-runtime

  2. 配置 .babelrc.js

// .babelrc.js
module.exports = {
    "plugins": [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": false, // 默认值,能够不写
                "helpers": true, // 默认,能够不写
                "regenerator": false, // 经过 preset-env 已经使用了全局的 regeneratorRuntime, 再也不须要 transform-runtime 提供的 不污染全局的 regeneratorRuntime
                "useESModules": true, // 使用 es modules helpers, 减小 commonJS 语法代码
            }
        ]
    ],
    presets: [
        [
            "@babel/preset-env",
            {
                "modules": false, // 模块使用 es modules ,不使用 commonJS 规范 
                "useBuiltIns": 'usage', // 默认 false, 可选 entry , usage
            }
        ]
    ]
}
复制代码

PS: 若是想要了解更多有关 @babel/preset-env 和 @babel/plugin-transform-runtime 的选项配置用途,能够参考个人我的总结

思考与探索(Modern Build)

上述的方案,其实还一直隐藏着一个不算问题的问题,那就是若是使用最新的浏览器,其实不须要任何的语法转换和polyfill。

那么参考下上述的 polyfill 方案,能不能实现若是低版本浏览器,就使用usage方案按需 transform + polyfill 的代码,若是是较新浏览器,就不进行任何的语法转换和 polyfill 呢?

必须能!

参考这篇文章 deploying es2015 code in production today,其中提出了基于 script 标签的 type="module"nomodule 属性 区分出当前浏览器对 ES6 的支持程度。

具体原理体如今,对于如下代码:

<script type="module" src="main.js"></script>
<script nomodule src="main.legacy.js"></script>
复制代码

支持 ES Module 的浏览器可以识别 type="module"nomodule,会加载 main.js 忽略 main.legacy.js

还未支持 ES module 的浏览器则偏偏相反,只会加载main.legacy.js

那么怎么实现优化就很清晰了:

  1. 经过配置上述 babel 最佳实践的,给这类的代码文件的 script 标间加上 nomodule 属性
  2. 经过配置 @babel/preset-env 的选项 target.esmodules = true,不转换全部的语法也不添加 polyfill,生成 ES6+ 的能被现代浏览器识别解析的代码,并给这类代码文件的 script 标签加上 type="module"

vue-cli 3.0 官方提供 modern build 功能

create-react-app 预计在下一个版本3.0的迭代中才实现。 现阶段实现须要本身写 webpack 插件来实现 module/nomodule 插入

相关文章
相关标签/搜索