封装样式处理


样式处理也许是浏览器中最复杂的部份,将所有细节予以封装一定是个不错的想法,为此,可以创建一个 Style-1.0.0.js,首先来看看css函数,它可以使用对象来一次设定想要的样式:

// 可透过对象以 key : value(CSS)形式来设定样式
function css(elem, props) {     
    Object.keys(props)
          .forEach(name => elem.style[name] = props[name]);
}

接着将〈访问元素位置〉中的offset放进去:

// 获取元素确实位置
function offset(elem) {
    let x = 0;
    let y = 0;
    for(let e = elem; e; e = e.offsetParent) {
        x += e.offsetLeft;
        y += e.offsetTop;
    }

    //  修正卷轴局部的量
    for(let e = elem.parentNode; e && e != document.body; e = e.parentNode) {
        if(e.scrollLeft) {
            x -= e.scrollLeft;
        }
        if(e.scrollTop) {
            y -= e.scrollTop;
        }
    }

    return { 
        x, 
        y, 
        toString() {
            return `(${this.x}, ${this.y})`;
        }
    };
}

然后准备处理〈显示、可见度与透明度〉中hideshow函数,不过在这之前要想想,原本的hideshow函数直接在原生元素上新增了特性,这并不是个建议的方式(除非是为了兼容性而修补对象,使之有兼容于标准的新功能)。

必须要有个方法,可以为元素存储相关特性,然而并非在元素本身,这时可以用上〈Set 与 Map〉中谈过的WeakMap

// 存储元素对应的数据
let elemData = new WeakMap();
function storage(elem, data) {
    if(data === undefined) {
        return elemData.get(elem);
    } else {
        elemData.set(elem, data);
    }
}

// 设定元素的相关属性,但此实现不是直接存储在元素上
function prePropOf(elem, prop, value) {
    if(value === undefined) {
        let data = storage(elem);
        return data === undefined ? undefined : data[prop];
    } else {
        let data = storage(elem);
        if(data) {
            data[prop] = value;
        } 
        else {
            data = {[prop] : value};
        }
        storage(elem, data);
    }   
}

使用WeakMap的原因在于,如果元素已经不再被程序其他部份参考住,就可以直接 GC,WeakMap也不会再有该元素,这可以避免内存泄漏的问题。

接着,就可以实现hideshow函数,以及fadeOutfadeIn函数:

function computedStyle(elem, name, pseudoClz = null) {
    return window.getComputedStyle(elem, pseudoClz)[name];
}

// 显示元素
function show(elem, pseudoClz = null) {
    elem.style.display = prePropOf(elem, 'display') || '';
    if(computedStyle(elem, 'display', pseudoClz) === 'none') {
        // 在 DOM 树上创建元素,获取 display 默认值后移除
        let node = document.createElement(elem.nodeName);
        document.body.appendChild(node);
        elem.style.display = style(node, 'display');
        document.body.removeChild(node);
    }
}

// 隐藏元素
function hide(elem, pseudoClz = null) {
    let display = computedStyle(elem, 'display', pseudoClz);
    prePropOf(elem, 'display', display);
    elem.style.display = 'none';
}

// 获取透明度的数字
function opacity(elem, pseudoClz = null) {
    let opt = computedStyle(elem, 'opacity', pseudoClz);
    return opt === '' ? 1 : parseFloat(opt);
}

//speed 是动画总时间,step 是动画数
// 淡出
function fadeOut(elem, speed = 5000, steps = 10, pseudoClz = null) {
    let preOpacity = opacity(elem, pseudoClz);

    prePropOf(elem, 'opacity', preOpacity);

    let timeInterval = speed / steps;
    let valueStep = preOpacity / steps;

    let opt = preOpacity;
    setTimeout(function next() {
        opt -= valueStep;
        if(opt > 0) {
            elem.style.opacity = opt;
            setTimeout(next, timeInterval);
        }
        else {
            elem.style.opacity = 0;
        }
    }, timeInterval);
} 

// 淡入
function fadeIn(elem, speed = 5000, steps = 10, pseudoClz = null) {
    let targetValue = prePropOf(elem, 'opacity') || 1;

    let timeInterval = speed / steps;
    let valueStep = targetValue / steps;

    let opt = 0;
    setTimeout(function next() {
        opt += valueStep;
        if(opt < targetValue) {
            elem.style.opacity = opt;
            setTimeout(next, timeInterval);
        }
        else {
            elem.style.opacity = targetValue;
        }
    }, timeInterval);
}

接着就是将一些先前文件中看过的其他样式相关函数放进去了:

// 是否有指定类
function hasClass(elem, clz) {
    let clzs = elem.className;
    if(!clzs) {
        return false;
    } else if(clzs === clz) {
        return true;
    }
    return clzs.search(`\\b${clz}\\b`) !== -1;
}

// 新增类
function addClass(elem, clz) {
    if(!hasClass(elem, clz)) {
        if(elem.className) {
            clz = ` ${clz}`;
        }
        elem.className += clz;
    }
}

// 移除类
function removeClass(elem, clz) {
    elem.className = elem.className.replace(
      new RegExp(`\\b${clz}\\b\\s*`, 'g'), '');
}

function toggleClass(elem, clz1, clz2) {
    if(hasClass(elem, clz1)) {
        removeClass(elem, clz1);
        addClass(elem, clz2);
    }
    else if(hasClass(elem, clz2)) {
        removeClass(elem, clz2);
        addClass(elem, clz1);
    }
}

// 集中获取宽高用的方法
class Dimension {
    static screen() {
        return {
            width: screen.width,
            height: screen.height
        };
    }

    static screenAvail() {
        return {
            width: screen.availWidth,
            height: screen.availHeight
        };        
    }

    static browser() {
        return {
            width: window.outerWidth,
            height: window.outerHeight
        };
    }

    static html() {
        return {
            width: window.documentElement.scrollWidth,
            height: window.documentElement.scrollHeight
        };        
    }

    static body() {
        return {
            width: window.body.scrollWidth,
            height: window.body.scrollHeight
        };        
    }

    static viewport() {
        return {
            width: window.innerWidth,
            height: window.innerHeight
        };        
    }
}

// 集中获取座标用的方法
class Coordinate {
    static browser() {
        return {
            x: window.screenX,
            y: window.screenY
        };                
    }

    static scroll() {
        return {
            x: window.pageXOffset,
            y: window.pageYOffset
        };        
    }
}

导出的名称有以下这些:

export {css, offset, hide, show, fadeOut, fadeIn};
export {hasClass, addClass, removeClass, toggleClass};
export {Dimension, Coordinate};

再来就是处理 XD-1.2.0.js 了,首先导入相关名称:

import {css, offset, hide, show, fadeOut, fadeIn} from './Style-1.0.0.js';
import {hasClass, addClass, removeClass, toggleClass} from './Style-1.0.0.js';

ElemCollection上添增一些方法:

class ElemCollection {

    ...

    // 如果 value 为 undefined,获取元素 style 特性上对应的样式
    // 否则在元素的 style 上设定特性           
    style(name, value) { 
        let elems = this.elems;
        let propName = PROPS.has(name) ? PROPS.get(name) : name;

        if(value === undefined) {
            return elems[0] ? elems[0].style[propName] : null;
        } else {
            elems.filter(elem => !isTextNode(elems[0]) && !isCommentNode(elems[0]))
                 .forEach(elem => elem.style[propName] = value);
            return this;
        }
    }

    // 获取计算样式,不写在 style() 方法中的理由在于
    // 从计算样式与 style() 方法返回值是否为 undefined
    // 可以知道样式是来自样式表或者是 style 设定
    // 明确化来源是其目的
    computedStyle(name, pseudoClz = null) {
        let elems = this.elems;
        let propName = PROPS.has(name) ? PROPS.get(name) : name;
        return elems[0] && !isTextNode(elems[0]) && !isCommentNode(elems[0]) ?
                    window.getComputedStyle(elems[0], pseudoClz)[propName] : null;
    }

    // 可透过对象以 key : value(CSS)形式来设定样式
    css(props) {
        let standardized =
              Object.keys(props)
                    .reduce((acc, name) => {
                         acc[PROPS.has(name) ? PROPS.get(name) : name] = props[name];
                         return acc;
                    }, {});

        this.elems.forEach(elem => css(elem, standardized));
        return this;
    }

    // 获取元素确实位置 
    offset() {
        let elems = this.elems;
        return elems[0] ? offset(elems[0]) : null;
    }

    // 隐藏元素
    hide(pesudoClz = null) {
        this.elems.forEach(elem => hide(elem, pesudoClz));
        return this;
    }

    // 显示元素
    show(pesudoClz = null) {
        this.elems.forEach(elem => show(elem, pesudoClz));
        return this;
    }

    // 淡出
    fadeOut(speed = 5000, steps = 10, pseudoClz = null) {
        this.elems.forEach(elem => fadeOut(elem, speed, steps, pseudoClz));
        return this;
    }

    // 淡入
    fadeIn(speed = 5000, steps = 10, pseudoClz = null) {
        this.elems.forEach(elem => fadeIn(elem, speed, steps, pseudoClz));
        return this;
    }

    // 第一个元素是否有指定类
    hasClass(clz) {
        let elems = this.elems;
        return elems[0] ? hasClass(elems[0], clz) : null;
    }

    // 加入类
    addClass(clz) {
        this.elems.forEach(elem => addClass(elem, clz)); 
        return this;
    }

    // 移除类
    removeClass() {
        this.elems.forEach(elem => removeClass(elem, clz)); 
        return this;
    }

    // 切换类
    toggleClass(clz1, clz2) {
        this.elems.forEach(elem => toggleClass(elem, clz1, clz2));
        return this;        
    }
}

XD-1.2.0.js 实际上是作为一个门户(Facade),对于 Style-1.0.0.js 中的DimensionCoordinate直接导出就可以了:

export {Dimension, Coordinate} from './Style-1.0.0.js';

然而,XD-1.2.0.js 作为一个门户,也必须考量到的是,ElemCollection会不会担负了太多职责了?在未来你可能继续在上头添增一些方便的方法,而使得ElemCollection成为一个无所不能的超级或上帝类(God class)?

这是个必须考量的问题,就目前来说,为了简化范例才这么做,然而,实际上,应该让ElemCollection只处理一些基础事务,像hideshowfadeOutfadeIn这些是基础事务吗?虽然目前都放在ElemCollection的话,写起程序来会很爽,然而,它们应该不太算是基础事务,而算是特效之类的东西。

因此比较好的作法是,基于 XD-1.2.0.js 上,构造一个Effect模块来专门处理特效,将 Style-1.0.0.js 中hideshowfadeOutfadeIn函数放到Effect模块中,而ElemCollection上的hideshowfadeOutfadeIn方法,在Effect模块中设计一个对象或者是相关函数来处理,在必须用到特效时,可以从ElemCollection构造出特效对象,将特效的职责分离出来。

这也有助于 Style-1.0.0.js 瘦身,ES6 并不鼓励一个模块导出太多东西,而成为一个超级模块,就目前来说,Style-1.0.0.js 导出的东西算是比较多一些了,只是为了简化范例,暂且没将特效的东西分离出来而已,未来 Style-1.0.0.js 中的东西越来越多时,应避免它成为一个超级模块。

分离职责的东西,就交给你自己来试试了,你可以在XD-1.2.0.jsStyle-1.0.0.jsEvt-1.0.0.js下载到目前已封装之程序库。

现在先来看看,基于目前的程序库封装,可以如何简化先前看过的范例,首先是〈访问样式信息〉中第一个范例的改写(你的浏览器必须支持 ES6 模块):

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body>
    <div id="message">这是一段消息</div>

<script type="module">
    import {elemsById} from './js/XD-1.2.0.js';
    elemsById('message').css({
                    color : '#ffffff',
                    backgroundColor : '#ff0000',
                    width : '300px',
                    height : '200px',
                    paddingLeft : '250px',
                    paddingTop : '150px'
                });

</script>  

</body>
</html>

按我观看结果

再来是〈访问样式信息〉中最后一个范例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <style type="text/css">
        #message {
            color: #ffffff; 
            background-color: #ff0000; 
            width: 500px; 
            height: 200px; 
            padding-left: 250px; 
            padding-top: 150px;
        }
    </style>
</head>
<body>

    <div id="message">这是一段消息</div>
    <span id="console"></span>

<script type="module">
    import {elemsById} from './js/XD-1.2.0.js';

    let color = elemsById('message').computedStyle('backgroundColor');
    elemsById('console').html(color);
</script>  
</body>
</html>

按我观看结果

以下是〈访问元素位置〉的第三个范例改写:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <style type="text/css">
         #container {
             color: #ffffff;
             background-color: #ff0000;
             height: 50px;
             position: absolute;
             top: -100px;
             left:-100px;
         }
    </style>
</head>
<body>

    <div id="container">这是一段消息</div>
    <hr>
    搜寻:<input id="search" type="text">

<script type="module">
    import x from './js/XD-1.2.0.js';
    let doc = x(document);

    let input = doc.elemsById('search');
    let offsetWidth = input.attr('offsetWidth');
    let offsetHeight = input.attr('offsetHeight');
    let search = input.offset();

    doc.elemsById('container')
       .css({
           left  : `${search.x}px`,
           top   : `${search.y + offsetHeight}px`,
           width : `${offsetWidth}px`
       });

</script>

</body>
</html>

按我观看结果

以下是〈显示、可见度与透明度〉第一个范例的改写:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">

    <style type="text/css">
        #message {
            color: #ffffff;
            background-color: #ff0000;
            border-width: 10px;
            border-color: black;
            border-style: solid;
            width: 100px;
            height: 50px;
            padding: 50px;
        }
    </style>  
</head>
<body>

    <button id='toggle'>切换显示状态</button>
    <hr>
    这是一些文本!这是一些文本!这是一些文本!这是一些文本!这是一些文本!
    <div id="message">这是消息一</div>
    这是其他文本!这是其他文本!这是其他文本!这是其他文本!这是其他文本!

<script type="module">
    import {elemsById} from './js/XD-1.2.0.js';

    elemsById('toggle').addEvt('click', evt => {
        let message = elemsById('message');
        if(message.computedStyle('display') === 'none') {
             message.show();
        } else {
             message.hide();
        }
    });

</script>  

</body>
</html>

按我观看结果

以下是〈显示、可见度与透明度〉第二个范例的改写:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body>

    <button id='fadeOut'>淡出</button>
    <button id='fadeIn'>淡入</button><br>
    <img id="image" src="https://openhome.cc/Gossip/images/caterpillar_small.jpg">  

<script type="module">
    import {elemsById} from './js/XD-1.2.0.js';

    let image = elemsById('image');
    elemsById('fadeOut').addEvt('click', evt => {
        image.fadeOut();
    });

    elemsById('fadeIn').addEvt('click', evt => {
        image.fadeIn();
    });
</script>

  </body>
</html>

按我观看结果

以下是〈操作 class 属性〉中的范例改写:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <style type="text/css">
        .released {
            border-width: 1px;
            border-color: red;
            border-style: dashed;
        }

        .pressed {
            border-width: 5px;
            border-color: black;
            border-style: solid;
        }
  </style>

</head>
<body>

  <img id="logo" class='released' 
       src="https://openhome.cc/Gossip/images/caterpillar_small.jpg">  

<script type="module">
    import {elemsById} from './js/XD-1.2.0.js';

    let logo = elemsById('logo');

    logo.addEvt('click', evt => {
        logo.toggleClass('released', 'pressed');
    });

</script>  

</body>
</html>

按我观看结果

以下是〈获取窗口宽高信息〉中的范例改写:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <style type="text/css">
        #message1 {
            text-align: center;
            vertical-align: middle;
            color: #ffffff;
            background-color: #ff0000;
            width: 100px;
            height: 50px;
            position: absolute;
            top: 0px;
            left: 0px;
        }
    </style>
</head>
<body>

    这些是一些文本<br>这些是一些文本<br>这些是一些文本<br>
    <button>其他控件</button>
    <div id="message1">
        看点广告吧!<br><br>
        <button id="confirm">确定</button>
    </div>

<script type="module">

    import {elemsById, Dimension} from './js/XD-1.2.0.js';

    let {width, height} = Dimension.viewport();
    let message1 = elemsById('message1');

    message1.css({
        opacity : 0.5,
        width   : `${width}px`,
        height  : `${height / 2}px`,       
        paddingTop : `${height / 2}px`
    });

    elemsById('confirm').addEvt('click', evt => {
        message1.css({
            width   : '0px',
            height  : '0px',       
            paddingTop : '0px',
            display : 'none'
        });        
    });


</script>

</body>
</html>

按我观看结果


展开阅读全文