精通前端 polyfill ,兼容各浏览器运行E6语法

ES6 在2015正式发布已经多年。最新浏览器们逼近100% 的支持率,但为了少数用户体验,咱们极可能须要兼容IE9。 Babel 默认只转码 ES6 的新语法(syntax),而不转换新的 API,好比 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(好比 Object.assign、Array.from)都不会转码,这时咱们就须要提供polyfill。vue

babel 和 polyfill

刚接触 babel 的同窗可能都认为在使用了 babel 后就能够无痛的使用 ES6 了,以后被各类 undefined 的报错无情打脸。一句话归纳, babel 的编译不会作 polyfill。那么 polyfill 是指什么呢? 翻译: 一种用于衣物、床具等的填充材料node

const foo = (a, b) => {
    return Object.assign(a, b);
};
复制代码

当咱们写出上面这样的代码,交给 babel 编译时,咱们获得了:react

"use strict";
 var foo = function foo(a, b) {
    return Object.assign(a, b);
 };
复制代码

箭头 function 被编译成了普通函数,但丫的 Object.assign 还没变身,而它做为 ES6 的新方法,并不能在IE9等浏览器上。为何不把 Object.assign 编译成 (Object.assign||function() { /*...*/}) 这样的替代方法呢?好问题!编译为了保证正确的语义,只转换语法而不是去增长或修改原有的属性和方法。因此 babel 不处理 Object.assign 反却是最正确的作法。而处理这些方法的方案则称为 polyfill。webpack

babel-plugin-transform-xxx

这个问题最原始解决思路是缺什么补什么,babel 提供了一系列 transform 的插件来解决这个问题,例如针对 Object.assign,咱们可使用 babel-plugin-transform-object-assign:git

npm i babel-plugin-transform-object-assign

# in .babelrc
{
  "presets": ["latest"],
  "plugins": ["transform-object-assign"]
}
复制代码

方便你尝试,这里准备了一些测试的代码。编译以前的代码,咱们获得了:github

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; };

var foo = exports.foo = function foo(a, b) {
  return _extends(a, b);
};
复制代码

babel-plugin-transform-object-assign 在咱们用到 Object.assign 方法以前使用ES5或更早的写法替换了。看上去效果不错,但细细考究一下会发现这样的问题:web

// another.js
export const bar = (a, b) => Object.assign(a, b);

// index.js
import { bar } from './another';

export const foo = (a, b) => Object.assign(a, b);
复制代码

被编译成了:chrome

/***/ index.js:
/***/ (function(module, exports, __webpack_require__) {

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.foo = undefined;

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; };

var _another = __webpack_require__(212);

var foo = exports.foo = function foo(a, b) {
  return _extends(a, b);
};

/***/ }),

/***/ another.js:
/***/ (function(module, exports, __webpack_require__) {

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});

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; };

var bar = exports.bar = function bar(a, b) {
  return _extends(a, b);
};

/***/ })
复制代码

plugin-transform 的引用是 module 级别的,意味着在多个 module 使用时会重复的引用,这在多文件的项目里可能带来灾难。且也不想一个个的去添加须要用的 plugin,若是能自动引入该多好。npm

babel-runtime & babel-plugin-transform-runtime

前面提到问题主要在于方法的引入方式是内联的,直接插入了一行代码从而没法优化。鉴于这样的考虑,babel 提供了 babel-plugin-transform-runtime,从一个统一的地方 core-js 自动引入对应的方法。redux

npm i -D babel-plugin-transform-runtime
npm i babel-runtime

# .babelrc
{
  "presets": ["latest"],
  "plugins": ["transform-runtime"]
}
复制代码
  • 安装开发时的依赖 babel-plugin-transform-runtime。
  • 安装生产环境的依赖 babel-runtime (是否要在生产环境也依赖它取决于你发布代码的方式,简单点直接放在 dependency 里总没错)

一切就绪,编译时它会自动引入你用到的方法。但自动就意味着不必定精确

export const foo = (a, b) => Object.assign(a, b);

export const bar = (a, b) => {
    const o = Object;
    const c = [1, 2, 3].includes(3);
    return c && o.assign(a, b);
};
复制代码

会编译成:

var _assign = __webpack_require__(214);
var _assign2 = _interopRequireDefault(_assign);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var foo = exports.foo = function foo(a, b) {
    return (0, _assign2.default)(a, b);
};

var bar = exports.bar = function bar(a, b) {
    var o = Object;
    var c = [1, 2, 3].includes(3);
    return c && o.assign(a, b);
};
复制代码

foo 中的 assign 会被替换成 require 来的方法,而 bar 中这样非直接调用的方式则无能为力了。同时,由于 babel-plugin-transform-runtime 依然不是全局生效的,所以实例化的对象方法则不能被 polyfill,好比 [1,2,3].includes 这样依赖于全局 Array.prototype.includes 的调用依然没法使用。

babel-polyfill

上面两种 polyfill 方案共有的缺陷在于做用域。所以 babel 直接提供了经过改变全局来兼容 es2015 全部方法的 babel-polyfill,安装 babel-polyfill 后你只须要在main.js加一句 import 'babel-polyfill' 即可引入它,若是使用了 webpack 也能够直接在 entry 中添加 babel-polyfill 的入口。

import 'babel-polyfill';

export const foo = (a, b) => Object.assign(a, b);
复制代码

加入 babel-polyfill 后,打包好的 pollyfill.js 一会儿增长到了 251kb(未压缩),(建议感兴趣的同窗把代码拉下来运行一下,以后提到的全部方式也均可以看到打包结果)搜索一下 polyfill.js 不难找到这样的全局修改:

//polyfill
`$export($export.S + $export.F, 'Object', {assign: __webpack_require__(79)});
复制代码

babel-polyfill 在项目代码前插入全部的 polyfill 代码,为你的程序打造一个完美的 es2015 运行环境。babel 建议在网页应用程序里使用 babel-polyfill,只要不在乎它略有点大的体积(min 后 86kb),直接用它确定是最稳妥的。值得注意的是,由于 babel-polyfill 带来的改变是全局的,因此无需屡次引用,也有可能所以产生冲突,因此最好仍是把它抽成一个 common module,放在项目 的 vendor 里,或者干脆直接抽成一个文件放在 cdn 上。

若是你是在开发一个库或者框架,那么 babel-polyfill 的体积就有点大了,尤为是在你实际使用的只有一个 Object.assign 的状况下。更可怕的是对于一个库来讲,改变全局环境是使不得的。谁也不但愿使用了你的库,还附带了一家老少的 polyfill 改变了全局对象。这时不污染全局环境的 babel-plugin-transform-runtime 才是最合适的。

babel-preset-env

回到应用开发。经过babel-runtime自动识别代码引入 polyfill 来优化不太靠谱,那是否是就无从优化了呢?并非。还记得 babel 推荐使用的 babel-preset-env 么?它能够根据指定目标环境判断须要作哪些编译。babel-preset-env 也支持针对指定目标环境选择须要的 polyfill 了,只需引入 babel-polyfill,并在 babelrc 中声明 useBuiltIns,babel 会将引入的 babel-polyfill 自动替换为所需的 polyfill。

  1. targets 指定须要兼容的浏览器类型和版本,
  2. 若是用 Node.js 开发,也一样能够指定 Node 的版本, 也能够直接写成 "node": "current",将自动采用你当前用来运行 Babel 的 Node.js 版本
  3. modules 用来指定模块化方式,支持 AMD、UMD、SystemJS、CommonJS 等。固然在 Webpack 2/3 的时代,推荐将 modules 设置为 false,即交由 Webpack 来处理模块化,经过其 TreeShaking 特性将有效减小打包出来的 JS 文件大小
# .babelrc
{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["last 2 versions", "safari >= 7", "IE >= 9" ],
        "node": "current", // 自动采用你当前用来运行 Babel 的 Node.js 版本
        "modules": false 
      },
      "useBuiltIns": "entry", // entry usage false
      include: []
    }],
    "stage-2"
  ],
  "plugins": ["transform-vue-jsx", "transform-runtime"],
  "env": {
    "test": {
      "presets": ["env", "stage-2"],
      "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"]
    }
  }
}
复制代码

对比 "IE >= 9" 和 "chrome >= 59" 环境下编译后的文件大小:

Asset     Size  Chunks           
         polyfill.js   252 kB       0  [emitted]  [big]
              ie9.js   189 kB       1  [emitted]
           chrome.js  30.5 kB       2  [emitted]
transform-runtime.js  17.3 kB       3  [emitted]
transform-plugins.js  3.48 kB       4  [emitted]
复制代码

在目前 IE9 的需求下能节省到将近 30%,但想不到浏览器之神 chrome 也还须要 30kb 的 polyfill,多是为了修正那些 v8 的一些细小的规范问题吧。

polyfill.io

以上本应该已经够用了,但本质上仍是让那些愿意使用最新浏览器的优质用户们作了牺牲。聪明的你可能已经想到了一种优化方案,针对浏览器来选择 polyfill。没错!polyfill.io 给出的一项服务。

你能够尝试在不一样的浏览器下请求 https://cdn.polyfill.io/v2/polyfill.js 这个文件,服务器会判断浏览器 UA 返回不一样的 polyfill 文件,你所要作的仅仅是在页面上引入这个文件,polyfill 这件事就自动以最优雅的方式解决了。更加让人喜悦的是,polyfill.io 不旦提供了 cdn 的服务,也开源了本身的实现方案 polyfill-service。简单配置一下,即可拥有本身的 polyfill service 了。

看上去一切都很美好,但在使用以前还请你多考虑一下。polyfill.io 面对国内奇葩的浏览器环境能不能把 UA 算准,若是缺失了 polyfill 还有没有什么补救方案,也许都是你须要考虑的。但不管如何,这是个优秀的想法和方案,或许将来也会有更多的网站采用 polyfill.io 的思路的。好比 theguardian 和 redux 做者 Dan 在 create-react-app 上的提议(虽然没被接受哈~)。

相关文章
相关标签/搜索