最近有小伙伴发现某页面在 iOS8 微信环境下显示空白页,跑过来问我可能导致此问题的原因,因为这是个前端模板渲染的页面,所以首先想到的是 js 抛错了,找个 iOS8 来试试吧。

手里有了 iOS8, 也打开了这个不正常的页面,却发现有点手足无措,曾经各种远程调试真机的招数居然一个都想不起来了,于是翻到了这篇文章温习了下,mac 插上数据线连接到手机,然后在 mac 的 safari 中就能调试到 iOS 里的页面了,如果是首次调试还需要在 iOS 设备上打开:设置 →Safari→高级→web 检查器。

发现确实是报错了:

TypeError:undefined is not a function(evaluating 'v.includes(":8080")')

应该是用来检查路由中的端口的,这里为什么会有一个字符串的高级 API 呢,经过和小伙伴确认,他确实是在代码里使用了 includes 方法,但它不是应该被编译环境转成浏览器识别的代码吗?我们先来了解下这个方法吧:includes

文档上写着:

This method has been added to the ECMAScript 2015 specification and may not be available in all JavaScript implementations yet.

iOS 端仅支持到 safari9, 很不幸 iOS8 上运行的是 safari8, 所以就报了上面的错。那出现这个问题是谁的锅呢?为什么编译工具没有去转换这个方法呢,编译工具对方法的转化是依据着什么样的规则呢?带着这几个问题,我们去测试下以下两大主流框架的主流构建环境。

我们发现 create-react-appvue-cli 生成的项目模版默认都没有对该方法进行 polyfill. 并且前者的官方 github 还有个关于此事的讨论,最终结果是官方不予默认支持。

要解决这个问题也不难,在自己的代码中引入官方提供的 polyfill 就可以了:

if (!String.prototype.includes) {
    String.prototype.includes = function(search, start) {
        'use strict';
        if (typeof start !== 'number') {
            start = 0;
        }
        if (start + search.length > this.length) {
            return false;
        } else {
            return this.indexOf(search, start) !== -1;
        }
    };
}

但我们怎么才能通过 webpack/babel 实现编译支持,而非自己去管理这部分代码呢。看了 babel 的官方文档,才发现原来自己一直对 babel 的理解也是有偏差的,以为配置了类似 es2015 这种,就会支持它的所有特性,包括语法和 API, 但人家明确说了只支持语法转换,如果想要支持所有的 API 还需要在代码中引入 babel-polyfill:

Since Babel only transforms syntax (like arrow functions), you can use babel-polyfill in order to support new globals such as Promise or new native methods like String.padStart (left-pad).

但这个 babel-polyfill 好像口碑没那么好,污染全局变量、压缩后 80k、不能按依赖引入,所以大家一致推荐使用 babel-runtime, 但请注意它并不能模拟实例方法,即内置对象原型上的方法,如 String.prototype.includesArray.prototype.find 等,只能模拟类似 Object.assign 等非内置对象原型上的方法。

虽然 babel-runtime 使用起来比较麻烦,但它能按模块注入:

//比如要使用Promise
import Promise from 'babel-runtime/core-js/promise';

//注入山寨includes
import includes from 'babel-runtime/core-js/string/includes';
const abc = 'abc';
includes(abc, 'a');//true

但这个 babel-runtime 还有更高级的用法,结合 webpack, 就能在编译阶段搞定这些 API 了,与之对应的编译插件叫做: babel-plugin-transform-runtime

我们在 .babelrc 中开启:

{
    "plugins": ["transform-runtime"]
}

这样除了上文提到的内置对象原型上的方法以外的所有语法和 API, 我们就都能愉快的使用了。如果一定要使用这些方法那只能在开发代码中引 入babel-polyfill:

//此方法会引入整个babel-polyfill,不推荐这么使用
import 'babel-polyfill';
const abc = 'abc';
abc.includes('a');//true

//此方法是按需引入babel-polyfill,但依赖的模块是core-js
import 'core-js/modules/es6.string.includes';
const bcd = 'bcd';
bcd.includes('b');//true

所以,综上所述:

  1. babel-runtime 和 babel-polyfill 都是需要直接在开发代码中当作模块来引入使用的
  2. babel-plugin-transform-runtime 是个编译工具,经过它编译的代码可以认为是直接注入了 babel-runtime 的 API(前提是开启了 helper 和 polyfill, 这两者都是默认开启的), 但是想要兼容类似 String.prototype.includes 的方法还需要手动在代码中引入 babel-polyfill

我的小伙伴就是使用了 babel-plugin-transform-runtime 这个编译插件,但是并没有在代码中引入 babel-polyfill, 导致了低版本浏览器不兼容 String.prototype.includes 而报错。