Webpack项目中如何解决浏览器兼容性问题
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 提供了多种部署 target。target
配置的默认值是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是各司其职的。像const
到var
这种语法层面的就只能通过转译来做,像String.prototype.includes
这种API方法就需要polyfill。另外,polyfill的一个好处是,如果浏览器中存在原生方法时将优先使用,这样性能与安全性更好。
@babel/preset-env
可以根据浏览器兼容性配置一次性做好语法转换与Polyfill。但为了使用Polyfill,我们需要配置useBuiltIns选项,同时安装core-js,使用npm install core-js@3
即可(注意这里包含的是运行时代码,所以不放到devDependency中)。
在
@babel/preset-env
7.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,如autoprefixer,postcss-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