一、IE8基礎(chǔ)兼容問題
1.1、CSS兼容
舊引擎不支持太多CSS3特性,在無法使用Flex布局的情況下盡量使用原始的表格排版代替,使用表格基本上不用考慮兼容性問題。
IE8不支持漸變和透明通道,做全局半透明遮罩的時(shí)候尤其蛋疼,DXImageTransform濾鏡雖然能對容器進(jìn)行著色但不能防止事件穿透,即便是一個(gè)全屏著色的遮罩層其背后的各種控件依然是可以點(diǎn)擊的,建議放棄線性漸變,使用小尺寸PNG圖片作為容器背景元素,設(shè)置background-repeat進(jìn)行平鋪。
1.2、JavaScript兼容
IE8非調(diào)試環(huán)境下不支持console,執(zhí)行到console.log之類的代碼會(huì)直接報(bào)錯(cuò),只有調(diào)出控制臺(tái)時(shí)才會(huì)生成window.console對象,因此首要引入console-polyfill,具體作用可參見其源代碼。
1.2.1、保留字問題
ECMAScript定義了一套關(guān)鍵字和保留字,根據(jù)規(guī)定,關(guān)鍵字是保留的,不能用作變量名或函數(shù)名。在實(shí)現(xiàn)ECMAScript 3的JavaScript引擎中使用關(guān)鍵字作標(biāo)識(shí)符,會(huì)導(dǎo)致"Identifier Expected"(缺少標(biāo)識(shí)符)錯(cuò)誤。而使用保留字作標(biāo)識(shí)符可能會(huì)也可能不會(huì)導(dǎo)致相同的錯(cuò)誤,具體取決于特定的引擎。
| 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對使用關(guān)鍵字和保留字的規(guī)則進(jìn)行了少許修改。關(guān)鍵字和保留字雖然仍然不能作為標(biāo)識(shí)符使用,但可以用作對象的屬性名,IE8自然是不支持新特性的,會(huì)將屬性名當(dāng)作保留字處理,定位“缺少標(biāo)識(shí)符”錯(cuò)誤時(shí)基本上都會(huì)指向某個(gè)保留字,使用插件es3ify-webpack-plugin可轉(zhuǎn)換對象訪問方式,將點(diǎn)運(yùn)算符訪問改為使用中括號(hào)運(yùn)算符訪問。
// In
var x = {class: 2,};
x.class = [3, 4,];
// Out:
var x = {"class": 2};
x["class"] = [3, 4];
混淆插件uglifyjs-webpack-plugin開啟ie8支持能達(dá)到同樣效果,因此可以只保留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可擴(kuò)展出大部分ES5 API。
唯一重要且不能模擬的是Object.defineProperty,直接通過變量賦值的方式觸發(fā)事件分發(fā),已經(jīng)超出舊引擎的能力范圍了,像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,引入現(xiàn)版本babel-polyfill已經(jīng)不需要前置引入es5-shim。
- [必要]
devtool設(shè)為source-map,不想生成js.map文件可以設(shè)成false。 - [非必要] 引入
es5-shim,不適用復(fù)雜場景,babel-polyfill可以代勞。 - [非必要] 引入
es5-sham,不適用復(fù)雜場景。 - [非必要] 引入
es3ify-webpack-plugin,不使用React情況下,想觀察轉(zhuǎn)碼結(jié)果可以單獨(dú)引入。 - [必要] 引入
uglifyjs-webpack-plugin,并開啟ie8支持。 - [必要] 引入
console-polyfill。 - [必要] 引入
babel-polyfill。
Webpack 4.x
改動(dòng)大致如下:
- [必要]
@babel/polyfill,全盤引入,省時(shí)省力,不要多想。 - [必要]
anujs@1.5.2,額,用1.5.3以上版本可能會(huì)翻車,針對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回調(diào)方法。
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下具有不同適用性,安裝的時(shí)候注意版本號(hào)。
| 插件 / 版本 | 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 |
測不出版本上限的插件全部用>=標(biāo)注,可以嘗試用最新版。
react能兼容ie8的最后版本是0.14.9。
redux版本需要<3.6.0,我不清楚3.5.x到3.6.0版本之間發(fā)生了什么。
react-router需要<2.0。
react-redux@4.4.10是最后支持React 0.14的版本。
webpack-dev-middleware<3.0.0。
history用于手動(dòng)路由路轉(zhuǎn),配合react-router。
anujs@1.5.2比較穩(wěn)定。
Webpack 4.x
React全家桶保留Webpack 2.x / 3.x的兼容版本,其他插件基本上使用最新的即可。
因?yàn)獒槍?code>IE8,需要使用uglifyjs-webpack-plugin代替默認(rèn)的terser-webpack-plugin進(jìn)行代碼壓縮。
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
}
})
]
}
因?yàn)?code>IE8不支持WebSocket,IE8預(yù)覽網(wǎng)頁需要devServer使用生產(chǎn)環(huán)境配置實(shí)時(shí)watch編譯,但代碼被壓縮不容易排錯(cuò),因此還需要啟動(dòng)另外一個(gè)devServer在Chrome下進(jìn)行開發(fā)。
2.2、入口文件順序
在entry項(xiàng)定義入口文件,其中包含必需的兼容庫,視情況甚至還需要引入jQuery。
main為自定義入口文件,在main.js完成ReactDOM.render等啟動(dòng)操作,視個(gè)人習(xí)慣而言。
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默認(rèn)注入順序是不可預(yù)測的,可能會(huì)出現(xiàn)入口文件引入順序不是兼容庫填充順序的情況出現(xiàn)。
<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配置,手動(dòng)整理出需要加載的靜態(tài)資源,然后用<script>標(biāo)簽在模版文件中依次注入,這樣的話就不需要配置entry了,只留一個(gè)入口文件main.js就夠了。
另外一種方法是指定html-webpack-plugin插件的配置項(xiàng)chunksSortMode為"manual",打包時(shí)入口文件會(huì)按配置項(xiàng)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預(yù)處理類似于管道,上一個(gè)loader的輸出作為下一個(gè)loader的輸入,用數(shù)組表示就是索引大的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、預(yù)處理和分離
對于less文件的編譯,less-loader將less文件編譯輸出為css代碼,css代碼中的url會(huì)在css-loader中被轉(zhuǎn)換,樣式代碼隨入口文件一并打包,style-loader在運(yùn)行時(shí)生成<style>標(biāo)簽,并將樣式代碼注入到<style>標(biāo)簽中。
Webpack 2.x / 3.x
為了提高頁面加載效率,需要使用插件extract-text-webpack-plugin將樣式代碼從入口文件中分離存儲(chǔ)為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
]
};
無論用什么預(yù)處理器,style-loader與css-loader總是固定存在的,配置
也是基本相同的,以至于可以編寫一個(gè)模版函數(shù)來統(tǒng)一生成這些預(yù)處理配置。
Webpack 4.x
到了Webpack 4.x,extract-text-webpack-plugin已被mini-css-extract-plugin代替。
跟2.x / 3.x不同,4.x提取css在預(yù)處理中間階段進(jìn)行,因此與style-loader互斥。
因此進(jìn)行css分離時(shí),不需要添加style-loader。
如配置開發(fā)環(huán)境時(shí),此時(shí)不需要分離css文件:
{
test: /\.less$/i,
use: [ "style-loader","css-loader","less-loader"]
}
到了生產(chǎn)環(huán)境,需要從js文件中分離樣式:
{
test: /\.less$/i,
use: [
{
loader :require("mini-css-extract-plugin").loader
},
"css-loader",
"less-loader"
]
}
///////// 生產(chǎn)環(huán)境需要在插件項(xiàng)配置 "mini-css-extract-plugin"
plugins:[
new MiniCssExtractPlugin({
filename: outputPublicPath("[hash].css", "stylesheet")
})
]
相當(dāng)于生產(chǎn)環(huán)境下,mini-css-extract-plugin/loader代替了style-loader。
樣式壓縮,4.x提供了統(tǒng)一的入口插件optimize-css-assets-webpack-plugin,css-loader已經(jīng)移除了minimize選項(xiàng)。
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
....
optimization: {
minimize: true,
minimizer: [
new OptimizeCSSAssetsPlugin()
]
}
3.2、模塊化
Webpack 2.x / 3.x
css-loader自帶模塊化功能,其實(shí)就是混淆,需要指定配置項(xiàng)modules和localIdentName才會(huì)生效。
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}
模塊機(jī)制下的css編寫與傳統(tǒng)方式有些許不同,圖片url是相對源代碼目錄的,經(jīng)Webpack編譯處理后才會(huì)轉(zhuǎn)為網(wǎng)絡(luò)資源路徑,但當(dāng)使用到less-loader這類預(yù)處理器時(shí),這個(gè)環(huán)節(jié)會(huì)出錯(cuò),css-loader不能處理上一級(jí)的輸出,例下less文件:
.app
{
background-image: url("./img/pic.png");
.red
{
color: red;
}
}
.redColor{
color: red;
}
當(dāng)啟動(dòng)模塊化功能時(shí),css-loader會(huì)直接提示"Module not found",找不"pic.png",需要在中間加一層resolve-url-loader作轉(zhuǎn)換,完整的配置如下:
{
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開啟混淆只需要設(shè)置modules為true。
{
test: /\.less$/i,
use:[
{
loader: 'css-loader',
options: {
modules:true
}
},
"less-loader"
]
}
3.3、全局沖突問題
模塊化解決了全局污染問題,但是也可能導(dǎo)致全局樣式失效,只要途徑同一預(yù)處理管道的樣式文件,選擇器名稱無例外都會(huì)被混淆,導(dǎo)致與容器的class匹配不上,因此需要把全局作用的樣式文件從模塊化管道中排除掉,最簡單的方法是分開目錄存放,然后修改配置項(xiàng)的正則表達(dá)式,通過目錄名稱來過濾。
/* 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)的語法,聲明一個(gè)全局規(guī)則。凡是這樣聲明的class,都不會(huì)被編譯成哈希字符串。
.title {
color: red;
}
:global(.title) {
color: green;
}
CSS Modules還提供一種顯式的局部作用域語法:local(.className),等同于.className,所以上面的css也可以寫成下面這樣。
:local(.title) {
color: red;
}
:global(.title) {
color: green;
}
3.3.2、方案二
通過文件后綴來區(qū)分是否是全局作用的樣式文件,擴(kuò)展名為“.less”則視為全局作用,后綴為“.scope.less”則視為局部作用,通過區(qū)分后綴是否包含scope字眼來分開編譯,但正則表達(dá)式不擅長“不包含”的識(shí)別,不要企圖通過/(scope){0}.less/來區(qū)分,你會(huì)發(fā)現(xiàn)路徑?jīng)_突根本不會(huì)通過編譯。
預(yù)處理配置項(xiàng)test除了可以接受正則表達(dá)式外,使用回調(diào)函數(shù)也是可以的,指定一個(gè)傳入?yún)?shù)為文件物理路徑的function,通過返回布爾值來代替正則表達(dá)式的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參數(shù),例如create-react-app腳手架默認(rèn)文件名后綴為module.less為模塊化樣式文件,那么全局樣式通道可設(shè)置exclude排除模塊化樣式文件,實(shí)現(xiàn)分流。
// 模塊化
{
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
模塊機(jī)制下,樣式文件導(dǎo)入后實(shí)際為一個(gè)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只接受字符串,需自行拼接各個(gè)鍵值后賦值給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包裝,實(shí)質(zhì)仍然是輸出字符串,但不用寫String.join或加號(hào)了:
import styles from "app.less"
import classNames from "classnames";
<div className={classNames(styles.app,styles.red)}>
<p className={styles.red}></p>
</div>
不想書寫“styles.”,可以再進(jìn)一步包裝:
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,因?yàn)楣俜皆创a中的isPlainObject方法性能超差
// 如果引用了 prop-types 或 create-react-class
// 需要添加如下別名
'prop-types': 'anujs/lib/ReactPropTypes',
'create-react-class': 'anujs/lib/createClass'
//如果你在移動(dòng)端用到了onTouchTap事件
'react-tap-event-plugin': 'anujs/lib/injectTapEventPlugin',
}
}
使用官方配置進(jìn)行替換,即可順利編譯和運(yùn)行,無須額外依賴。
nervjs
京東的輪子
https://github.com/NervJS/nerv
https://www.npmjs.com/package/nervjs
https://nervjs.github.io/docs/
使用時(shí)需要對引用作一些替換:
/*
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環(huán)境下nervjs自身可以正常運(yùn)行,但不能整合react-router@1.0.3。
五、其它坑
-
IE8不支持Websocket,只能使用現(xiàn)代瀏覽器進(jìn)行日常開發(fā),編譯后再觀察IE8運(yùn)行效果。 -
React編譯樣式比較慢,尤其通過import方式引用樣式時(shí),dev-server經(jīng)常因爆內(nèi)存而崩潰。 - 沒有支持
IE8的完善組件庫,極有可能仍然離不開JQuery。
https://www.npmjs.com/package/jquery
封裝JQuery組件時(shí)盡量在componentWillUnmount中釋放資源,但并不是所有第三方控件都有資源釋放接口的。