likes
comments
collection
share

Webpack项目中如何解决浏览器兼容性问题

作者站长头像
站长
· 阅读数 71

Webpack本身的兼容性

既然是基于Webpack,那么首先要看Webpack的Browser端代码的兼容性。

Webpack 支持所有符合 ES5 标准 的浏览器。webpack 的 import() 和 require.ensure() 依赖 Promise。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要 提前加载 polyfill

即在支持ES5标准的浏览器(几乎所有现代浏览器)上,Webpack自身的代码是可以正常运行的。但如果使用到了import()require.ensure,则需要我们额外引入Promise(ES6语法)的polyfill,才能在一些低版本浏览器上运行Webpack。

构建目标 & browserlist

接下来就是处理我们项目代码的兼容性了。但在这之前,我们需要了解一个概念,Webpack的构建目标 target。由于JavaScript既可以编写浏览器代码也可以编写服务器端代码,所以 webpack 提供了多种部署 targettarget配置的默认值是browserslist,如果项目是在浏览器上运行,那么保持这个默认配置就可以了。

重点来了,browserslist是什么呢?它是一个浏览器兼容性配置,并且它被多个我们常用的JS工具使用,比如Babel,Eslint。具体怎么用呢?Browserlist有一些方便理解的查询语法,如:

> 5% // 全球用户量大于5%的浏览器
last 2 versions // 各浏览器的最近2个版本
not dead // "dead"指超过24个月没有官方支持或更新的浏览器

通过这些查询语句就能找到符合要求的厂商与版本的浏览器,从而Babel等工具可以利用它来做针对性的语法转换或其他工作。

browserslist可以在项目的package.json或.browserlistrc文件中配置,如create-react-app在package.json中的配置为:

"browserslist": {
  "production": [
    ">0.2%",
    "not dead",
    "not op_mini all"
  ],
    "development": [
    "last 1 chrome version",
    "last 1 firefox version",
    "last 1 safari version"
  ]
}

JavaScript代码兼容性

语法转换

在不配置任何Loader的情况下,Webpack可以将不同模块的JS文件打包在一起。但是不会进行任何兼容性处理,比如const var1 = 1,在打包后仍然是const,而不会是ES5语法的var

要进行语法转换,我们需要引入合适的loader。这里使用专门用来处理JS语法转换的一个工具:Babel。Babel是一个JavaScript编译器,它主要用来将ES6+的高级语法转换为兼容性更好的语法,从而支持一些老版本的浏览器。

在Webpack中,我们可以添加对应的babel-loader。安装npm install -D babel-loader @babel/core @babel/preset-env。配置如下:

module: {
  rules: [
    {
      test: /.m?js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

先看看我们所预期的转换结果:const,数组解构等ES6语法都被转换为了ES5的语法!

// 转换前
const var1 = 1;
const [a, b] = [1, 2];

// 转换后
var var1 = 1;
var a = 1,
  b = 2;

具体是如何做的呢?注意到我们前面我们安装了三个库:babel-loader,@babel/core@babel/preset-env

  • babel-loader:babel的Webpack loader。
  • @babel/core:babel编译器的核心库。包含源码转AST,AST转源码等核心工具函数。
  • @babel/preset-env:一个babel preset。它可以根据浏览器兼容性配置来做针对性的语法转换。

@babel/preset-env是将Babel转换与浏览器兼容性衔接起来的重要一环。@babel/preset-env利用了browserslist等工具提供的数据维护了一份JS特性到支持浏览器/平台的映射。内容如下所示:

{
  "transform-class-static-block": {
      "chrome": "94",
      "opera": "80",
      "edge": "94",
      "firefox": "93",
      "node": "16.11",
      "deno": "1.14",
      "samsung": "17",
      "electron": "15.0"
  },
  "transform-private-property-in-object": {
      "chrome": "91",
      "opera": "77",
      "edge": "91",
      "firefox": "90",
      "safari": "15",
      "node": "16.9",
      "deno": "1.9",
      "ios": "15",
      "samsung": "16",
      "electron": "13.0"
  }
}

同时,它还维护了一份这些JS特性到对应的Babel转换插件以及core-js polyfills的映射,从而实现在各个平台上应用合适的Babel插件。这样用户只需要按browserslist的语法指定需要的浏览器版本,@babel/preset-env就能将源码编译为平台支持的语法了。

Polyfill

在处理JS兼容性问题上,除了语法转换,还有一件重要事情是polyfill。polyfill的目的是注入一些API,比如Object.assign,以在浏览器本身不支持的情况下调用。注入的示例如下,即在原有对象或全局对象上做一套方法实现。

// from mdn
if (!String.prototype.includes) {
  String.prototype.includes = function(search, start) {
    'use strict';

    if (search instanceof RegExp) {
      throw TypeError('first argument must not be a RegExp');
    } 
    if (start === undefined) { start = 0; }
    return this.indexOf(search, start) !== -1;
  };
}

语法转换与Polyfill是各司其职的。像constvar这种语法层面的就只能通过转译来做,像String.prototype.includes这种API方法就需要polyfill。另外,polyfill的一个好处是,如果浏览器中存在原生方法时将优先使用,这样性能与安全性更好。

@babel/preset-env可以根据浏览器兼容性配置一次性做好语法转换与Polyfill。但为了使用Polyfill,我们需要配置useBuiltIns选项,同时安装core-js,使用npm install core-js@3即可(注意这里包含的是运行时代码,所以不放到devDependency中)。

@babel/preset-env7.4.0版本中,推荐直接使用core-js而不是@babel/polyfill。并且需要在core-js选项中指定core-js的版本。

配置方法如下:

presets: [
  [
    "@babel/preset-env",
    {
      useBuiltIns: "entry",
      corejs: "3",
    },
  ],
]

useBuiltIns有三个选项:

  • entry

将把源代码中import "core-js"import "core-js/stable"指令转换为拆分的import。如:

require("core-js/modules/es.symbol.js");
require("core-js/modules/es.symbol.description.js");
require("core-js/modules/es.symbol.async-iterator.js");
require("core-js/modules/es.symbol.has-instance.js");
// ...

这些拆分的引入是根据指定的浏览器版本来确定的,从而既做到了兼容性,又减小了体积。但要注意,使用它的前提是你需要在代码中先引入core-js

  • usage

相比于entry它不会引入全部polyfill,而只会引入代码中用到了的API的polyfill,这样代码体积就更小了。另外,它不需要你在源代码中手动引入core-js。比如你只用到了Object.assign,则编译后的代码中只会增加一行:

require("core-js/modules/es.object.assign.js");
  • false

不引入任何polyfill。

问题又来了,实际项目中到底使用哪个选项呢?貌似看起来usage是最佳选择。但实际上,create-react-app脚手架与umi框架中默认都是用的entry。原因可能是usage还不太完善,在一些特殊情况下,它无法识别出需要polyfill的API,有兴趣大家可以babel的issue里看看。

CSS代码兼容性

CSS也像JS一样,由于不同浏览器的支持程度不一致,所以也存在兼容性问题。

postcss是一个可以转换CSS语法的JS插件。在Webpack中使用postcss-loader示例如下:

module: {
  rules: [
    {
      test: /.css$/i,
      use: ["style-loader", "css-loader", "postcss-loader"],
    }
  ]
}

postcss与Babel相似,它本身只包含一些核心API,但不处理具体的转换工作,所以需要添加一些postcss的plugin,如autoprefixerpostcss-preset-env。这些插件可以在项目下的postcss.config.js文件中配置:

const postcssPresetEnv = require("postcss-preset-env");

module.exports = {
  plugins: [require("autoprefixer"), postcssPresetEnv(/* pluginOptions */)],
};

比如autoprefixer可以根据提供的目标浏览器来添加对应的CSS前缀,以解决一些兼容性问题。如下面示例所示:

/* 转换前 */
.example {
  display: grid;
  transition: all .5s;
  user-select: none;
  background: linear-gradient(to bottom, white, black);
}

/*
* 转换后
* Prefixed by https://autoprefixer.github.io
* PostCSS: v8.4.14,
* Autoprefixer: v10.4.7
* Browsers: last 4 version
*/
.example {
    display: -ms-grid;
    display: grid;
    -webkit-transition: all .5s;
    -o-transition: all .5s;
    transition: all .5s;
    -webkit-user-select: none;
       -moz-user-select: none;
        -ms-user-select: none;
            user-select: none;
    background: -webkit-gradient(linear, left top, left bottom, from(white), to(black));
    background: -o-linear-gradient(top, white, black);
    background: linear-gradient(to bottom, white, black);
}

create-react-app与一些流行的前端框架中都用到了postcss,有兴趣的读者可以继续探索啦~

参考

转载自:https://juejin.cn/post/7214314627520692261
评论
请登录