一、IE8基础兼容问题
1.1、CSS兼容
旧引擎不支持太多CSS3
特性,在无法使用Flex
布局的情况下尽量使用原始的表格排版代替,使用表格基本上不用考虑兼容性问题。
IE8
不支持渐变和透明通道,做全局半透明遮罩的时候尤其蛋疼,DXImageTransform
滤镜虽然能对容器进行着色但不能防止事件穿透,即便是一个全屏着色的遮罩层其背后的各种控件依然是可以点击的,建议放弃线性渐变,使用小尺寸PNG
图片作为容器背景元素,设置background-repeat
进行平铺。
1.2、JavaScript兼容
IE8
非调试环境下不支持console
,执行到console.log
之类的代码会直接报错,只有调出控制台时才会生成window.console
对象,因此首要引入console-polyfill
,具体作用可参见其源代码。
1.2.1、保留字问题
ECMAScript
定义了一套关键字和保留字,根据规定,关键字是保留的,不能用作变量名或函数名。在实现ECMAScript 3
的JavaScript
引擎中使用关键字作标识符,会导致"Identifier Expected"
(缺少标识符)错误。而使用保留字作标识符可能会也可能不会导致相同的错误,具体取决于特定的引擎。
break | do | instanceof | typeof | case | else |
---|---|---|---|---|---|
new | var | catch | finally | return | void |
continue | for | switch | while | debugger | function |
this | with | default | if | throw | delete |
in | try | abstract | enum | int | short |
boolean | export | interface | static | byte | extends |
long | super | char | final | native | synchronized |
class | float | package | throws | implements | protected |
volatile | double | import | public | package | let |
yield |
ECMAScript 5
对使用关键字和保留字的规则进行了少许修改。关键字和保留字虽然仍然不能作为标识符使用,但可以用作对象的属性名,IE8
自然是不支持新特性的,会将属性名当作保留字处理,定位“缺少标识符”错误时基本上都会指向某个保留字,使用插件es3ify-webpack-plugin
可转换对象访问方式,将点运算符访问改为使用中括号运算符访问。
// In
var x = {class: 2,};
x.class = [3, 4,];
// Out:
var x = {"class": 2};
x["class"] = [3, 4];
混淆插件uglifyjs-webpack-plugin
开启ie8
支持能达到同样效果,因此可以只保留uglifyjs-webpack-plugin
插件。
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
new UglifyJsPlugin({
uglifyOptions: {
ie8: true
}
})
1.2.2、ECMAScript 5
IE8
不支持ECMAScript5
,不能使用Array.map
、Function.bind
等方法,引入es5-shim
可扩展出大部分ES5 API
。
唯一重要且不能模拟的是Object.defineProperty
,直接通过变量赋值的方式触发事件分发,已经超出旧引擎的能力范围了,像Vue.js
这些使用到该特性的第三方库均不可能兼容IE8
。
<html>
<head></head>
<body>
<p id="time"></p>
<script>
// run in internet explorer 9+
var obj = {};
var text = "";
Object.defineProperty(obj, "a", {
set: function(newValue) {
text = newValue;
document.getElementById("time").innerText = text;
},
get: function() {
return text;
}
});
setInterval(function() {
obj.a = new Date().toString();
},
1000);
</script>
</body>
</html>
1.2.3、ECMAScript 6
Webpack 2.x / 3.x
基本上引入
babel-polyfill
足够了,主要是兼容Promise
,引入现版本babel-polyfill
已经不需要前置引入es5-shim
。
- [必要]
devtool
设为source-map
,不想生成js.map
文件可以设成false
。 - [非必要] 引入
es5-shim
,不适用复杂场景,babel-polyfill
可以代劳。 - [非必要] 引入
es5-sham
,不适用复杂场景。 - [非必要] 引入
es3ify-webpack-plugin
,不使用React
情况下,想观察转码结果可以单独引入。 - [必要] 引入
uglifyjs-webpack-plugin
,并开启ie8
支持。 - [必要] 引入
console-polyfill
。 - [必要] 引入
babel-polyfill
。
Webpack 4.x
改动大致如下:
- [必要]
@babel/polyfill
,全盘引入,省时省力,不要多想。 - [必要]
anujs@1.5.2
,额,用1.5.3
以上版本可能会翻车,针对ie8
这种老古董,没事还是不要折腾核心库。 - [必要] 不要引入
es5-shim
,堆栈溢出。 - [必要] 不要引入
es5-sham
,堆栈溢出。 - [必要] 引入插件
@babel/plugin-transform-modules-commonjs
,能绕过'Accessors not supported!'
异常。
{
"presets": [
[
"@babel/env",
{
"loose": true
}
],
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-transform-modules-commonjs",
"@babel/plugin-proposal-class-properties"
]
}
- [必要] 不能使用
splitChunk
分包功能。 - [必要] 引入
uglifyjs-webpack-plugin
,见下文。 - [非必要] 引入
@babel/plugin-proposal-class-properties
,支持如下写法,不用到处bind
回调方法。
class Test extends React.Component
{
selfMethod = () => {
/// do something
};
render()
{
return ( <button onClick={this.selfMethod}>Button</button> );
}
}
二、Webpack配置
2.1、核心插件
Webpack 2.x / 3.x
某些插件在Webpack2.x
和3.x
下具有不同适用性,安装的时候注意版本号。
插件 / 版本 | webpack 2.7.0 | webpack 3.12.0 |
---|---|---|
react | 0.14.9 | 0.14.9 |
react-dom | 0.14.9 | 0.14.9 |
react-router | 1.0.3 | 1.0.3 |
redux | 3.5.2 | 3.5.2 |
webpack-dev-server | 2.11.2 | 2.11.2 |
webpack-dev-middleware | 2.0.6 | 2.0.6 |
webpack-hot-middleware | >=2.22.2 | >=2.22.2 |
url-loader | 0.6.2 | >=1.0.1 |
extract-text-webpack-plugin | 2.1.2 | 3.0.2 |
history | 1.7.0 | 1.7.0 |
es3ify-webpack-plugin | >=0.0.1 | >=0.1.0 |
测不出版本上限的插件全部用>=
标注,可以尝试用最新版。
react
能兼容ie8
的最后版本是0.14.9
。
redux
版本需要<3.6.0
,我不清楚3.5.x
到3.6.0
版本之间发生了什么。
react-router
需要<2.0
。
react-redux@4.4.10
是最后支持React 0.14
的版本。
webpack-dev-middleware
<3.0.0
。
history
用于手动路由路转,配合react-router
。
anujs@1.5.2
比较稳定。
Webpack 4.x
React
全家桶保留Webpack 2.x / 3.x
的兼容版本,其他插件基本上使用最新的即可。
因为针对IE8
,需要使用uglifyjs-webpack-plugin
代替默认的terser-webpack-plugin
进行代码压缩。
optimization: {
minimize: true,
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
warnings: false,
parse: {},
compress: false,
mangle: true,
output: null,
toplevel: false,
nameCache: null,
ie8: true,
keep_fnames: false
}
})
]
}
因为IE8
不支持WebSocket
,IE8
预览网页需要devServer
使用生产环境配置实时watch
编译,但代码被压缩不容易排错,因此还需要启动另外一个devServer
在Chrome
下进行开发。
2.2、入口文件顺序
在entry
项定义入口文件,其中包含必需的兼容库,视情况甚至还需要引入jQuery
。
main
为自定义入口文件,在main.js
完成ReactDOM.render
等启动操作,视个人习惯而言。
entry: {
"console-polyfill": "console-polyfill",
"es5-shim": "es5-shim/es5-shim.js",
"es5-sham": "es5-shim/es5-sham.js",
"babel-polyfill": "babel-polyfill",
main: [path.resolve("src", "index.js")]
}
由于html-webpack-plugin
默认注入顺序是不可预测的,可能会出现入口文件引入顺序不是兼容库填充顺序的情况出现。
<script type="text/javascript" src="./scripts/babel-polyfill.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/main.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/es5-shim.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/es5-sham.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/console-polyfill.js?fbe5d7c0c4f12cdfda8c"></script>
解决办法之一是不使用entry
配置,手动整理出需要加载的静态资源,然后用<script>
标签在模版文件中依次注入,这样的话就不需要配置entry
了,只留一个入口文件main.js
就够了。
另外一种方法是指定html-webpack-plugin
插件的配置项chunksSortMode
为"manual"
,打包时入口文件会按配置项chunks
指定的顺序注入。
plugins: [
new HtmlWebpackPlugin(
{
template: path.join("src", "index.html"),
favicon: path.join("src", "favicon.ico"),
minify: false,
hash: true,
inject: true,
chunks: ["console-polyfill", "es5-shim", "es5-sham", "babel-polyfill", "main"],
chunksSortMode: "manual"
})
]
<script type="text/javascript" src="./scripts/console-polyfill.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/es5-shim.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/es5-sham.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/babel-polyfill.js?fbe5d7c0c4f12cdfda8c"></script>
<script type="text/javascript" src="./scripts/main.js?fbe5d7c0c4f12cdfda8c"></script>
三、CSS模块化
Webpack
的loader
预处理类似于管道,上一个loader
的输出作为下一个loader
的输入,用数组表示就是索引大的loader
的输出作为索引小loader
的输入。
{
test: /\.less$/,
use: [{
loader: "style-loader" // creates style nodes from JS strings
},
{
loader: "css-loader" // translates CSS into CommonJS
},
{
loader: "less-loader" // compiles Less to CSS
}]
}
3.1、预处理和分离
对于less
文件的编译,less-loader
将less
文件编译输出为css
代码,css
代码中的url
会在css-loader
中被转换,样式代码随入口文件一并打包,style-loader
在运行时生成<style>
标签,并将样式代码注入到<style>
标签中。
Webpack 2.x / 3.x
为了提高页面加载效率,需要使用插件extract-text-webpack-plugin
将样式代码从入口文件中分离存储为css
文件。
const ExtractTextPlugin = require('extract-text-webpack-plugin');
// Create multiple instances
const extractCSS = new ExtractTextPlugin('stylesheets/[name]-one.css');
const extractLESS = new ExtractTextPlugin('stylesheets/[name]-two.css');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: extractCSS.extract([ 'css-loader', 'postcss-loader' ])
},
{
test: /\.less$/i,
use: extractLESS.extract([ 'css-loader', 'less-loader' ])
},
]
},
plugins: [
extractCSS,
extractLESS
]
};
无论用什么预处理器,style-loader
与css-loader
总是固定存在的,配置
也是基本相同的,以至于可以编写一个模版函数来统一生成这些预处理配置。
Webpack 4.x
到了Webpack 4.x
,extract-text-webpack-plugin
已被mini-css-extract-plugin
代替。
跟2.x / 3.x
不同,4.x
提取css
在预处理中间阶段进行,因此与style-loader
互斥。
因此进行css
分离时,不需要添加style-loader
。
如配置开发环境时,此时不需要分离css
文件:
{
test: /\.less$/i,
use: [ "style-loader","css-loader","less-loader"]
}
到了生产环境,需要从js
文件中分离样式:
{
test: /\.less$/i,
use: [
{
loader :require("mini-css-extract-plugin").loader
},
"css-loader",
"less-loader"
]
}
///////// 生产环境需要在插件项配置 "mini-css-extract-plugin"
plugins:[
new MiniCssExtractPlugin({
filename: outputPublicPath("[hash].css", "stylesheet")
})
]
相当于生产环境下,mini-css-extract-plugin/loader
代替了style-loader
。
样式压缩,4.x
提供了统一的入口插件optimize-css-assets-webpack-plugin
,css-loader
已经移除了minimize
选项。
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
....
optimization: {
minimize: true,
minimizer: [
new OptimizeCSSAssetsPlugin()
]
}
3.2、模块化
Webpack 2.x / 3.x
css-loader
自带模块化功能,其实就是混淆,需要指定配置项modules
和localIdentName
才会生效。
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}
模块机制下的css
编写与传统方式有些许不同,图片url是相对源代码目录的,经Webpack
编译处理后才会转为网络资源路径,但当使用到less-loader
这类预处理器时,这个环节会出错,css-loader
不能处理上一级的输出,例下less
文件:
.app
{
background-image: url("./img/pic.png");
.red
{
color: red;
}
}
.redColor{
color: red;
}
当启动模块化功能
时,css-loader
会直接提示"Module not found"
,找不"pic.png"
,需要在中间加一层resolve-url-loader
作转换,完整的配置如下:
{
test:/\.less$/,
use:extractLESS.extract({
fallback:{
loader:"style-loader",
options:{
sourceMap:true
}
},
use:[{
loader:"css-loader",
options:{
sourceMap:true,
minimize:config.compress,
localIdentName:"[name]-[local]-[hash:base64:5]",
modules:true
}
},{
loader:"resolve-url-loader"
},{
loader:"less-loader",
options:{
sourceMap:true
}
}],
publicPath:"../"
})
}
Webpack 4.x
4.x
开启混淆只需要设置modules
为true
。
{
test: /\.less$/i,
use:[
{
loader: 'css-loader',
options: {
modules:true
}
},
"less-loader"
]
}
3.3、全局冲突问题
模块化解决了全局污染问题,但是也可能导致全局样式失效,只要途径同一预处理管道的样式文件,选择器名称无例外都会被混淆,导致与容器的class
匹配不上,因此需要把全局作用的样式文件从模块化管道中排除掉,最简单的方法是分开目录存放,然后修改配置项的正则表达式,通过目录名称来过滤。
/* In */
.app
{
background-image: url("./img/pic.png");
.red
{
color: red;
}
}
/* Out */
.src-components-app-styles---app---1-oEM
{
background-image: url("./images/pic.png");
}
.src-components-app-styles---app---1-oEM .src-components-app-styles---red---12lu-
{
color: red;
}
import React from 'react';
import './app.less';
export default () => {
return (
// app -> src-components-app-styles---app---1-oEM
<div className="app">
<p className="red">Hello World</p>
</div>
);
};
3.3.1、方案一
CSS Modules
允许使用:global(.className)
的语法,声明一个全局规则。凡是这样声明的class
,都不会被编译成哈希字符串。
.title {
color: red;
}
:global(.title) {
color: green;
}
CSS Modules还提供一种显式的局部作用域语法:local(.className)
,等同于.className
,所以上面的css也可以写成下面这样。
:local(.title) {
color: red;
}
:global(.title) {
color: green;
}
3.3.2、方案二
通过文件后缀来区分是否是全局作用的样式文件,扩展名为“.less”
则视为全局作用,后缀为“.scope.less”
则视为局部作用,通过区分后缀是否包含scope
字眼来分开编译,但正则表达式不擅长“不包含”
的识别,不要企图通过/(scope){0}.less/
来区分,你会发现路径冲突根本不会通过编译。
预处理配置项test
除了可以接受正则表达式外,使用回调函数也是可以的,指定一个传入参数为文件物理路径的function
,通过返回布尔值
来代替正则表达式的test
操作:
// custom function
const lang= "less"; // lang = "less" 、"sass" .....
const suffix = `.scope.${lang}`;
const ext = `.${lang}`;
// if path include 'scope.less'
function test(path) {
return path.lastIndexOf(ext) == path.length - ext.length && path.lastIndexOf(suffix) == path.length - suffix.length;
}
// webpack config
{
test:test,
use:extractLESS.extract({
fallback:{
loader:"style-loader",
options:{
sourceMap:true
}
},
use:[{
loader:"css-loader",
options:{
sourceMap:true,
minimize:config.compress,
localIdentName:"[name]-[local]-[hash:base64:5]",
modules:true
}
},{
loader:"resolve-url-loader"
},{
loader:"less-loader",
options:{
sourceMap:true
}
}],
publicPath:"../"
})
}
或者使用include
/exclude
参数,例如create-react-app
脚手架默认文件名后缀为module.less
为模块化样式文件,那么全局样式通道可设置exclude
排除模块化样式文件,实现分流。
// 模块化
{
test: /\.module\.(less)$/i,
use: [
{
loader:"css-loader",
options:{ modules:true }
},
"less-loader"
]
}
// 全局作用
{
test: /\.(less)$/i,
exclude: /\.module\.(less)$/i,
use: [
{
loader:"css-loader"
},
"less-loader"
]
}
3.3、classnames插件
JSX
书写className
十分繁琐,可以使用插件classnames
简化操作,具体使用方法参看官方文档。
https://github.com/JedWatson/classnames
https://www.npmjs.com/package/classnames
npm:
npm install classnames --save
Bower:
bower install classnames --save
Yarn (note that yarn add automatically saves the package to the dependencies in package.json):
yarn add classnames
模块机制下,样式文件导入后实际为一个map
:
{
app: "src-components-app-styles---app---1-oEM",
red: "src-components-app-styles---red---12lu-",
big: "src-components-app-styles---big---Y9ObK"
}
JSX
的className
只接受字符串,需自行拼接各个键值后赋值给className
:
import styles from "app.less"
<div className={styles.app+ " " +styles.red}>
<p className={styles.red}></p>
</div>
// or
<div className={String.join(styles.app,styles.red," ")}>
<p className={styles.red}></p>
</div>
使用classnames
包装,实质仍然是输出字符串,但不用写String.join
或加号了:
import styles from "app.less"
import classNames from "classnames";
<div className={classNames(styles.app,styles.red)}>
<p className={styles.red}></p>
</div>
不想书写“styles.”
,可以再进一步包装:
import styles from "app.less"
import classNames from "classnames/bind";
var cx = classNames.bind(styles);
<div className={cx('app','red')}>
<p className={styles.red}></p>
</div>
理解其原理后,甚至可以:
function bindStyles(styles)
{
return function(...argv){
let _cx=classNames.bind(styles);
let keys=argv.filter((i)=>typeof i =="string");
console.log(keys)
let maps=argv.filter((i)=>typeof i =="object");
return [
Object.entries(styles).filter((i)=>keys.indexOf(i[0])>=0).map((i)=>i[1]).join(" "),
_cx(Object.assign({},...maps))
].join(" ");
};
}
//////
import styles from "app.less"
import classNames from "classnames/bind";
let cx=bindStyles(styles);
<div className={cx("app",{"red":true})}>
<p className={styles.red}></p>
</div>
四、React-Like
React 0.14
过于老旧,部分特性已被警告使用。部分第三方React-Like
框架可以兼容IE8
,并且支持React15/16
的特性,但redux/react-router
还是要使用兼容版本。
anujs
//webpack配置
resolve: {
alias: {
'react': 'anujs',
'react-dom': 'anujs',
// 若要兼容 IE 请使用以下配置
// 'react': 'anujs/dist/ReactIE',
// 'react-dom': 'anujs/dist/ReactIE',
// 'redux': 'anujs/lib/ReduxIE',//这主要用于IE6-8,因为官方源码中的isPlainObject方法性能超差
// 如果引用了 prop-types 或 create-react-class
// 需要添加如下别名
'prop-types': 'anujs/lib/ReactPropTypes',
'create-react-class': 'anujs/lib/createClass'
//如果你在移动端用到了onTouchTap事件
'react-tap-event-plugin': 'anujs/lib/injectTapEventPlugin',
}
}
使用官方配置进行替换,即可顺利编译和运行,无须额外依赖。
nervjs
京东的轮子
https://github.com/NervJS/nerv
https://www.npmjs.com/package/nervjs
https://nervjs.github.io/docs/
使用时需要对引用作一些替换:
/*
import React from "react"
import ReactDom from "react-dom"
replace with
import Nerv from "nervjs"
*/
import Nerv from "nervjs"
class App extends Nerv.Component {
render() {
return <TodoBox />;
}
}
具体配置参考官方的IE8模版:
https://github.com/NervJS/nerv-webpack-boilerplate
IE8
环境下nervjs
自身可以正常运行,但不能
整合react-router@1.0.3
。
五、其它坑
-
IE8
不支持Websocket
,只能使用现代浏览器进行日常开发,编译后再观察IE8运行效果。 -
React
编译样式比较慢,尤其通过import
方式引用样式时,dev-server
经常因爆内存而崩溃。 - 没有支持
IE8
的完善组件库,极有可能仍然离不开JQuery
。
https://www.npmjs.com/package/jquery
封装JQuery
组件时尽量在componentWillUnmount
中释放资源,但并不是所有第三方控件都有资源释放接口的。