Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
SunshowerC opened this issue Dec 1, 2018 · 10 comments
Open

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

SunshowerC opened this issue Dec 1, 2018 · 10 comments

Comments

@SunshowerC
Copy link
Owner

SunshowerC commented Dec 1, 2018

前言

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

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

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

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

实践方案

polyfill.io

如果你的工程是用的语法是 ES5,但是用了一些 ES6+ 的API特性,那么可以直接引入:

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

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

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

优点:每个浏览器的设备加载的 polyfill 都不一样,最新的完全兼容ES6栋浏览器基本加载的 polyfill 大小为0。

缺点:

  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 插入。此处可以推荐使用 create-react-app 模板: react-scripts-modern ,来实现 Modern Build

  • 常规webpack打包流程图
    image

  • Modern Build 打包构建流程
    image

Modern Build 注意事项

  1. Safari 或者 非常老的浏览器(chrome43),有可能会触发 es5 + es6 的代码都下载,但是只执行es5。 这是因为 常规的script标签(没有type=module/nomodule),会把这个之后的所有 nomodule 标签加载进来
  2. safari 必须所有 script 标签有 type=module / nomodule , css link 标签需要有 <link href="app.css" rel="stylesheet" onload="this.media='all'" media="nope!"> 动态加载,不然也会下载两份代码只执行一份
  • 都下载的情况:

    1. 当nomodule 在 常规script标签之后的情况,safari 会下载两份,nomodule 在之前则下载一份
        <script type="module" src="./ddds/dd.js"></script>
        <script src="./dd.js"></script>
        <script nomodule src="./js/es/5.js?v2"></script>
        <script type="module" src="./js/e/s6.js?v2"></script>
    
  • 只下载一份的情况

<body>
    <script type="module" src="./js/e/s6.js?v2"></script>
    <script nomodule src="./js/es/5.js?v2"></script>
    <script src="./dd.js"></script>
</body>

参考文献: type module 执行顺序

@imshgga
Copy link

imshgga commented Dec 23, 2018

针对polyfill.io,会不会因为某些浏览器UA不正确而造成引入的polyfill不正确,然后报错呢...

@SunshowerC
Copy link
Owner Author

SunshowerC commented Dec 24, 2018

针对polyfill.io,会不会因为某些浏览器UA不正确而造成引入的polyfill不正确,然后报错呢...

@imshgga 这是有可能的,尤其是在 Webview 场景下 UA 可能会被改得面目全非,polyfill.io 官方的 polyfill-service/issues 也有人遇到类似问题。这也是 polyfill.io 存在的问题,把主动权都交给第三方服务了,Web App 的稳定性会在一定程度上依赖 polyfill.io 服务的稳定性

@winnieBear
Copy link

请问,文中的可以搜索core-js被加载了多少个模块是什么插件实现的,谢谢

@SunshowerC
Copy link
Owner Author

SunshowerC commented May 10, 2019

请问,文中的可以搜索core-js被加载了多少个模块是什么插件实现的,谢谢

@winnieBear
webpack-bundle-analyzer

@winnieBear
Copy link

请问,文中的可以搜索core-js被加载了多少个模块是什么插件实现的,谢谢

@winnieBear
webpack-bundle-analyzer

多谢,之前也用了这个插件,没发现那个输入框是干嘛用的,呵呵

@cqgsm
Copy link

cqgsm commented Aug 8, 2019

@babel/polyfill为啥还需要安装这个库啊?

@shownoso
Copy link

你好,我于近日使用"@babel/preset-env": "^7.7.1",babel.config.js配置如下:

const presets = [
	[
		"@babel/env",
		{
			useBuiltIns: 'usage',
			corejs: 3,
			modules: false,
			targets: {
				"esmodules": false
			}
		},
	],
];

module.exports = {
	presets,
};

当使用async语法时,看到转换后的文件中的处理为import "regenerator-runtime/runtime";,并没有产生helpers。
第一个问题: 当前版本的preset-env以modules为标准对垫片进行引入的,而不是helpers?

然而,当设置targets: {"esmodules": true}后再次进行转换,出现了如下helpers

function asyncGeneratorStepcatch 
function _asyncToGenerator

第二个问题:是否可以这样认为:对于targets: {"esmodules": true}的这种情况,当使用高于es6的语法(例如async)时,当前版本的preset-env会对regenerator-runtime/runtime进行按需引入垫片的处理,且处理方式为内联的helpers?

@NeilYiu6
Copy link

usage和plugin-transform-runtime不能混用的...

babel/babel#10271 (comment)

@wwenj
Copy link

wwenj commented Dec 14, 2021

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

babel7里都已经废弃了@babel/polyfill,你这最佳配置还需要使用它?我一直理解babel7对于polyfill的使用,应该是用@babel/plugin-transform-runtime和@babel/runtime做按需引入吧,useBuiltIns还有必要用吗?没什么用处吧

@imshgga
Copy link

imshgga commented Dec 14, 2021 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants