原文链接:https://github.com/AlloyTeam/AlloyTouch/wiki/Scoped-CSS

写在前面

问:什么是Scoped CSS规范?

Scoped CSS规范是Web组件产生不污染其他组件,也不被其他组件污染的CSS规范。

面对组件化的普及,组件的复用很普遍的需求,然而CSS相互污染是经常遇见的问题,建立规范让开发者放心使用各种组件,甚至跨生态的组件是很有必要的一件事情。

目前业界的一些方案

方案一:
如果用webpack的话,可以参考css-loader的这个功能:

一段hash + 组件名,这个可能兼顾了辨识度 + 命名污染的问题。

方案二:

用webpack和scss,less写成模块化css就可以一定程度避免CSS污染,不能完全避免

方案三:样式规范上,使用与组件同名的嵌套命名空间

如果只用自己的生态可以这么搞,但是有的时候会引入第三方生态,第三方和自己的命名空间一样还是很有可能,比如scroller插件,社区里也有很多scroller插件loading uplader插件等等。

现有方案的局限性

这里还是会有污染的情况,因为:

  • 模块化的粒度是大于等于组件化粒度,意思就是一个模块可能有多个组件

  • 非less和sass项目下的组件怎么保证

  • 难以保证不污染第三方组件

  • 难以保证不被第三方组件污染

  • 同名组件的问题

  • 组件在第三方项目使用的问题

  • 组件自身生态闭环的问题

所以得出:

用意念或者规范约定不然注入程序自动化避免冲突

好处:

  • 能保证不污染别的组件并且不被被的组件污染可以更放心的复用

  • Scoped CSS规范是运行时产生唯一id~~ 永远不会css碰撞

  • 返回的这个id那个指定给组件的顶层div就行,实施简单

如果把这个过程放在构建过程就是工程问题。但是组件单独抽离出来给第三方用,其实就是组件本身的问题。总之要保证:

  • 不污染第三方的项目或组件

  • 不被第三组件或项目污染(由于是层叠样式,这个无法完全保证)

Scoped CSS代码

;(function () {

    function scoper(css) {
        var id = generateID();
        var prefix = "#" + id;
        css = css.replace(/\/\*[\s\S]*?\*\//g, '');
        var re = new RegExp("([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)", "g");
        css = css.replace(re, function(g0, g1, g2) {

            if (g1.match(/^\s*(@media|@keyframes|to|from|@font-face)/)) {
                return g1 + g2;
            }

            if (g1.match(/:scope/)) {
                g1 = g1.replace(/([^\s]*):scope/, function(h0, h1) {
                    if (h1 === "") {
                        return "> *";
                    } else {
                        return "> " + h1;
                    }
                });
            }

            g1 = g1.replace(/^(\s*)/, "$1" + prefix + " ");

            return g1 + g2;
        });

        addStyle(css,id+"-style");
        return id;
    }

    function generateID() {

        var id =  ("scoped"+ Math.random()).replace("0.","");
        if(document.getElementById(id)){
            return generateID();
        }else {
            return id;
        }
    }

    var isIE = (function () {

        var undef,
            v = 3,
            div = document.createElement('div'),
            all = div.getElementsByTagName('i');

        while (
            div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
                all[0]
            );

        return v > 4 ? v : undef;

    }());

    function addStyle(cssText, id) {
        var d = document,
            someThingStyles = d.createElement('style');
        d.getElementsByTagName('head')[0].appendChild(someThingStyles);
        someThingStyles.setAttribute('type', 'text/css');
        someThingStyles.setAttribute('id', id);
        if (isIE) {
            someThingStyles.styleSheet.cssText = cssText;
        } else {
            someThingStyles.textContent = cssText;
        }
    }


    window.scoper = scoper;
})();

Scoped CSS实施

var id = scoper("h1 {\
               color:red;\
            /*color: #0079ff;*/\
                }\
        \
                /*  h2 {\
                color:green\
                }*/");

scoper返回的id,在组件的JS里面赋给包裹的DOM便可以。这里详细说下生成id的过程:

function generateID() {
    var id =  ("scoped"+ Math.random()).replace("0.","");
    if(document.getElementById(id)){
        return generateID();
    }else {
        return id;
    }
}

通过Math.random得到随机数并经过处理,然后通过document.getElementById去查询页面上有没有同名ID,有的话则继续重新生成,没有的话就使用当前id。这里需要特别注意的是,比如一些弹出层插件,display hide的时候有的组件是直接从body里面移除,所以这就带来了CSS碰撞的可能性,所以这里Scoped CSS 规范强行约定:后插入的HTML,一定要经过scoper过程重新生成唯一id。
最后,Scoped CSS规范已经在AlloyTouch插件里开始实施,并打算推广开来。

你有什么好的想法可以让跨生态跨项目跨技术栈的组件复用更加惬意,可以交流交流。


dntzhang
634 声望74 粉丝

腾讯高级工程师