此文所描述的场景,在现代浏览器以及项目中应该是遇不到的。所描述的问题是:老项目需要动态换肤、且存在组件资源懒加载的情况下可能遇到预设的皮肤样式无效的问题。

在一个css文件中,如果存在相同的选择器,且内部设置了相同的css属性值,最终生效的为最后一个(属性值无 important),简单地可以理解为覆盖。 在不同的css文件中,这个现象依然存在,比如a、b两个css文件中都有相同的规则定义,如果b后引用(加载),则生效的为b。

/* a.css */
body {
    background: red;
}

/* b.css */
body {
    background: blue;
}

比如如上的两个css文件,和以下的html中的引入片段

<link rel="stylesheet" href="./a.css">
<link rel="stylesheet" href="./b.css">

如无其他css的影响,最终页面的中展现的body区域应该是 蓝色 (blue) 。

既然有这样的规则,那么我我们将一个css文件以 link 标签的形式插入到另一个已经存在的css中会是什么情况呢?代码示意如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="./reset.css">
    <!-- 这是一个已经预设了所有的情况的皮肤文件 -->
    <link rel="stylesheet" id="skin-all" href="./skin-all.css">
</head>
<body>
    <!-- elements -->
    <script>
        function loadComponentStyle(componentId) {
            var link = document.createElement('link');
            link.rel = 'stylesheet';
            link.id = 'component-style-' + componentId;
            link.href = 'xxx了路径' + componentId + '.css';
            var skinLink = document.getElementById('skin-all');

            if (skinLink) {
                skinLink.insertAdjacentElement('beforebegin', link);
            } else {
                document.getElementsByTagName('head')[0].appendChild(link);
            }
        }

        // 在某个特殊情况,需要动态载入某个组件,并引入其样式文件
        loadComponentStyle('component-a');
    </script>
</body>
</html>

简单来说其使用场景就是,一个皮肤文件中已经预设了所有需要使用的皮肤样式并已经直接使用。但是一些组件使用频率不高,无需直接引入,而且这组件需要也需要应用已经预设的皮肤。

以上代码中,陌生的可能是 insertAdjacentElement ,这是一个dom原生操作的api,兼容IE8。简介如下:

elementA.insertAdjacentElement(position, elementB); 可以将 elementB 插入在 elementA 的指定位置。其中参数 position 的取值为以下值之一:

  • beforebegin : 在该元素本身的前面。
  • afterbegin :只在该元素当中, 在该元素第一个子孩子前面。
  • beforeend :只在该元素当中, 在该元素最后一个子孩子后面。
  • afterend : 在该元素本身的后面。

可视化位置如下:

<!-- beforebegin -->
<p>
<!-- afterbegin -->
foo
<!-- beforeend -->
</p>
<!-- afterend -->

所以这个需求就简单这样实现了,所利用就是前面说描述的,link引入资源的先后位置,验证一切(chrome、firefox、safari)正常,然而直到遇到了IE。

IE下的问题是生效的是后续动态加载的组件中样式,而非预想中的皮肤样式。

而问题的根源应该就是在IE中,将link元素动态插入到某一个之前,并不能、是的后面的优先级较高。

排坑进行时:

猜想1:IE下并非link的先后位置顺序决定,而是加载顺序决定。 即上例中的皮肤样式是直接加载的,而某组件的样式是动态引入的,先引入的皮肤样式,后引入的组将样式,因此组件样式优先级更高。

为了验证这个猜想,只用在加载组件资源完成后,再重新加载一次皮肤样式即可。

我们将上面的例子中的 loadComponentStyle 方法进行改造,在新插入的样式加载完成后重新加载皮肤样式文件:

function loadComponentStyle(componentId) {
    var link = document.createElement('link');
    link.rel = 'stylesheet';
    link.id = 'component-style-' + componentId;

    var skinLink = document.getElementById('skin-all');

    link.onload = function () {
        link.onload = null;
        // 在新插入的样式加载完成后重新加载皮肤样式文件
        reloadSkinStyle();
    }
    link.href = 'component-id-path';

    if (skinLink) {
        skinLink.insertAdjacentElement('beforebegin', link);
    } else {
        document.getElementsByTagName('head')[0].appendChild(link);
    }
}

function reloadSkinStyle() {
    var skinStyle = document.getElementById('skin-all');
    var path = skinStyle.getAttribute('href');
    skinStyle.href = '';
    setTimeout(function () {
        skinStyle.href = path;
    }, 17);
}

验证结果:失败,最终生效的效果依然是组件的样式。

猜想2:在IE下link的引入样式文件优先级是由加入页面的位置决定的。 开始的情况,以及猜想1中验证的情况都是组件的样式后加入页面,因此优先级较高。

我们对上面代码中的 reloadSkinStyle 再做改进即可:

function reloadSkinStyle() {
    var skinStyle = document.getElementById('skin-all');
    var head =document.getElementsByTagName('head')[0];
    head.removeChild(skinStyle);
    head.appendChild(skinStyle);
}

验证结果:通过,通过将skin移除后再重新放入,优先级变高。

因此,大概估计IE与众不同,优先级的决定并非位置顺序或者是加载顺序,而是相应的link标签引入dom树的先后顺序。

至此,问题原因已经基本确定,但是猜想中的验证代码并不能作为解决方案。

那么IE下这样的需求要如何处理呢?最终应用的方案是下面这样的:

利用css原生的 @import 。预先在页面的皮肤link之前埋入一个空的style标签。然后动态加载组件样式时,通过 @import component-style-path 的形式在这个style中引入组件样式。代码示意如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="./reset.css">
    <!-- 预先放置一个style -->
    <style id="lazy-load-style"></style>
    <!-- 这是一个已经预设了所有的情况的皮肤文件 -->
    <link rel="stylesheet" id="skin-all" href="./skin-all.css">
</head>
<body>
    <!-- elements -->
    <script>
        function loadComponentStyle(componentId) {
            var link = document.createElement('link');
            link.rel = 'stylesheet';
            link.id = 'component-style-' + componentId;

            var style = document.getElementById('skin-all');
            // ie-8 下不支持style的innerText操作
            if ('styleSheet' in style) {
                style.setAttribute('type', 'text/css');
                style.styleSheet.cssText += style.styleSheet.cssText + '@import "component-id-path";';
            } else {
                style.innerText += style.innerText + '@import "component-id-path";'
            }
        }

        // 在某个特殊情况,需要动态载入某个组件,并引入其样式文件
        loadComponentStyle('component-a');
    </script>
</body>
</html>

这个方案基本可以覆盖解决前面的问题,只在IE下使用此方案,其他浏览器仍然走正常的link插入到皮肤样式之前即可。

只是这种方案在IE下还有一个坑,那就是每次有新的组件中动态加载,即每次对这个占位的style的内容进行修改时,都会涉及整个style便签引入的所有css资源的重新加载(其他浏览器则不会)。唉,与众不同的IE,就只能这样着吧。