React.js前端webpack专场

IE8环境下Webpack+React全家桶配置总结

2018-06-26  本文已影响17人  sitorhy

一、IE8基础兼容问题


某些人群是不讲道理的,办公用着IE8却要带头搞大数据,互联网+,这些老古董严重阻碍现有生产模式,为了预防不测先一步弄一套兼容到IE8的配置。

https://github.com/sitorhy/react-webpack-boilerplate

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转换对象访问方式,将点运算符访问改为使用中括号运算符访问,但该插件似乎并不能在Webpack 4.x上工作。

// In
var x = {class: 2,};
x.class = [3, 4,];
 
// Out:
var x = {"class": 2};
x["class"] = [3, 4];

1.2.2、ECMAScript5

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

基本上引入babel-polyfill足够了,主要是兼容Promise,但要注意引入顺序,在进行ES5兼容处理之前引入会报诸如"Promise未定义"之类的错误。

二、Webpack配置


部分关键插件由于兼容问题,Webpack 4.x以上方案暂时无解。

2.1、核心插件

某些插件在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.0.1

测不出版本上限的插件全部用>=标注,可以尝试用最新版。
react能兼容ie8的最后版本是0.14.9。
redux版本需要<3.6.0,我不清楚3.5.x到3.6.0版本之间发生了什么。
react-router需要<2.0。
webpack-dev-middleware<3.0.0。
至于混淆压缩插件uglifyjs-webpack-plugin不是必需的,配置不当可能会引起诡异的错误,建议持保留态度。

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默认注入顺序是不可预测的,可能会出现ES6兼容库执行先于ES5兼容库的情况。

<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 Modules


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>标签中。

为了提高页面加载效率,还需要使用插件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总是固定存在的,配置
也是基本相同的,以至于可以编写一个模版函数来统一生成这些预处理配置。

3.2、模块化

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:"../"
    })
}

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、Solution 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、Solution 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:"../"
    })
}

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

https://github.com/RubyLouvre/anu

//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
截止至版本1.3.0,IE8环境下nervjs自身可以正常运行,但似乎不能整合react-router。

五、其它坑


上一篇下一篇

猜你喜欢

热点阅读