前端组件总结

235 阅读8分钟

前言

开发前端这么久,虽然每天面对各种各样的需求,但是大部分情况下是在做重复性事情,而且是高密度的重复性的劳动,而且每天加班做这么多事情,对自己没有任何提升,鉴于此,我就一直在琢磨,能不能把这些重复性的工作,交给程序去处理,如果能够总结这些工作内容的共同规律,编写成为一套程序,交给程序去处理,那么我们就可以解放自己的时间,让自己可以做更加有意义的事情,而不是,每天像个机器,重复的做那些类似事情。

机缘巧合下,我来到了极客大学前端训练营,学习组件化时,我感觉这个体系和我原来的开发方式不一样了,他就是将共同规律封装成为一个个的模块来实现复用的。我感觉前端就应该是这样来开发的。下面我将分享组件化体系的一些内容,这只是我自己体会的一部分,如果有什么不完善的地方,还请大佬拍砖指正。

组件体系

组件概括

我们的页面是由一个个的标签组成的,但是这些标签有一定的局限性,有时候我们会想,要是可以自定以标签的行为和特征就好了,三大框架已经有不少自定义的组件,能满足大部分的需求,但是如果遇到一些自己定制化的需求,就满足不了了,还是得自己制作自己自定义的组件。

我理解的组件体系是:根据自己的需求,将对应的标签组合起来,形成一个独立的模块,这个模块有自己的属性和行为特征,而组件体系就是多个这样的组件,可以互相结合,就像搭积木一样,拼接搭配完成我们想要的城堡。

组件构成

生命周期

组件有自己的生命周期,创建(create),挂载(mounted),更新(update), 卸载(unmounted):

export default class Text {
    constructor(text){
        this[PROPERTY_SYMBOL] = Object.create(null);
        this[ATTRIBUTE_SYMBOL] = Object.create(null);
        this[EVENT_SYMBOL] = Object.create(null);
        this[STATE_SYMBOL] = Object.create(null);
        
        this.created(text);
    }

    created(text){
        this.root = document.createElement("span");
        this.root.style.whiteSpace = 'normal';
        this.root.style.fontSize = '30px'; 
        this.root.innerText = text;
    }
    mounted(){

    }
    unmounted(){

    }
    update(){

    }

  • create钩子用来创建自己的结构体系,并执行一些初始化操作。
  • mouted钩子用来执行一些挂载dom时的行为操作,比如渲染样式。
  • update钩子用来更新数据并重新渲染页面,当"观察者模式"监听到数据发生变化时。
  • unmounted钩子用来执行卸载组件时的一些行为,比如清除没用的数据结构,释放内存空间等。

私有属性

组件有自己的一些独有属性,不希望被外界篡改,以破坏扰乱组件的行为特征。我们有必要加一些保护措施,来防止被干扰:

const PROPERTY_SYMBOL = Symbol("property");
const ATTRIBUTE_SYMBOL = Symbol("attribute");
const EVENT_SYMBOL = Symbol("event");
const STATE_SYMBOL = Symbol("state");

export default class Text {
    constructor(text){
        this[PROPERTY_SYMBOL] = Object.create(null);
        this[ATTRIBUTE_SYMBOL] = Object.create(null);
        this[EVENT_SYMBOL] = Object.create(null);
        this[STATE_SYMBOL] = Object.create(null);
        
        this.created(text);
    }

es6引入了symbol数据结构,帮我们解决了这个问题,上面我用Symbol定义了这些私有属性,这样就可以放心的使用这些属性,而不用担心被别人篡改了。

get,set

取值和赋值会用到get和set方法,我们可以自定义get和set方法,用来拦截默认行为,定制一些自己的行为:

get width(){
    return this[PROPERTY_SYMBOL].width;
}
set width(value){
    this.root.style.width = value;
    this.update();
    return this[PROPERTY_SYMBOL].width = value;
}
get urls(){
	return this[PROPERTY_SYMBOL].urls;
}
set urls(value){
	return this[PROPERTY_SYMBOL].urls = value;
}

这里我们可以利用set来实现一个“发布-订阅模式”,来实现,当数据变化时,页面的及时更新功能。

attribute

当我们为我们的组件标签设置一些属性时,我们可以自定义setAttribute函数,拦截默认的setAttribute函数,来设置一些定制行为:

setAttribute(name, value){
    if(name == "style") {
        this.root.setAttribute('style',value);
        this.root.style.width = "100%";
        this.root.style.height = "100%";
        this.root.style.display = "inline-block";
        this.root.style.verticalAlign = "top";
    }
    return this[ATTRIBUTE_SYMBOL][name] = value;
}

react把attribute和property同步了,我们这儿attribute可以用来记录标签上的一些属性。当然也可以做一些定制化的操作。比如如果是以“on-” 开头的,就可以添加对应的监听事件。

property

我们的property属性是用来记录js操作的一些状态,来辅助整个代码逻辑的:

if(!this[PROPERTY_SYMBOL]['isDone']){
    this.triggerEvent('scrollToBottom', '加载更多......');
    this.root.appendChild(this.placeHolder);
    this[PROPERTY_SYMBOL]['isDone'] = true;

    setTimeout(() => {
        this[PROPERTY_SYMBOL]['isDone'] = false;
        window.getJSON("../data.json").then( data => {
            this.root.removeChild(this.placeHolder);
            this.render([data[1], data[2]]).appendTo(this.root);
            this.triggerEvent('scrollToBottom', '人家也是有底线的!');
            this.root.appendChild(this.placeHolder);
        }).catch(
            err => {
                console.log(err);
                return err;
            }
        )
    }, 2000);
}

上面代码中,用property的isDone属性,来控制是不是要执行相应的逻辑。

组件的大部分功能都是通过attribute和property来完成的,这两个属性至关重要。

事件机制

我们可以自定义事件机制,如下:

addEventListener(type, listener){
    if(!this[EVENT_SYMBOL][type])
        this[EVENT_SYMBOL][type] = new Set;
    this[EVENT_SYMBOL][type].add(listener);
}
removeEventListener(type, listener){
    if(!this[EVENT_SYMBOL][type])
        return;
    this[EVENT_SYMBOL][type].delete(listener);
}
triggerEvent(type, ...args){
    if(!this[EVENT_SYMBOL][type])
        return;
    for(let event of this[EVENT_SYMBOL][type])
        event.call(this, ...args);
}

如上所示,我们可以添加事件队列,删除事件队列中的事件,触发事件队列中的事件。

由于篇幅有限,组件的构成就讲这么多吧。

组件的样式处理

组件的样式可以用js的style书写,尤其是对于核心构成样式,这是必要的,但是对于常规的修饰样式,都要拿js来写,想想就觉得繁琐,所以写了一个loader,来引入和处理css,使得可以使用css文件,代码如下:

css loader 处理 (依赖了css)

var css = require('css');

module.exports = function(source, map){

    let filename = this.resourcePath.match(/([^\/ | \\ ]+)\.css$/)[1];
    var classname = filename.replace(
        /^[A-Z]/,
        l => l.toLowerCase()
    ).replace(
        /[A-Z]/,
        l => '-' + l.toLowerCase()
    );
    
    var obj = css.parse(source);

    let jsObj = {};

    for(var rule of obj.stylesheet.rules) {
        if(rule.type !== "rule")
            continue;  
        
        rule.selectors = rule.selectors.map( selector =>{
          if(selector.match(/filename/)){
            selector = selector.replace(/filename/, l => {
                return classname;
            });
            return selector;
          }
          return  '.' + classname + ' ' + selector;
        });
    }

    return "export default " + JSON.stringify(css.stringify(obj));
}

如上所示:引入了css-loader来处理css内容并转化为字符,导出给引用者,

  • var obj = css.parse(source)这段代码就是转化css内容的
  • let filename = this.resourcePath.match(/([^\/ | \\ ]+).css$/)[1];这是用来获取css文件名,注意**[^\/ | \\ ]**,由于不同的操作系统resourcePath所获得的斜杠不同,所以这儿用了兼容性的写法。
  • filename.replace方法将驼峰式的文件名转换为了css常用的 key-code 中划线 方式的名称,赋值给了className.
  • for循环了把css文件里所有的类名,都追加到了以该文件名转换后的className类名后了,保证了css样式的局部性,不会影响全局样式。
  • 注意循环里的filename,这个是用来处理根元素样式的,这个会直接返回className.
  • 最后return "export default" + + JSON.stringify(css.stringify(obj)),将导出的内容转换为字符串,return 出去,方便引用者调用。

css部分

.filename.root {
  width: 100%;
  height: 110px;
}

.container {
    width: 100%;
    height: 100%;
}

.left-img-box {
    width: 110px;
    height: 110px;
    border-radius: 50%;
    display: inline-block;
    position: absolute;
    left: 35px;
    background-color: rgba(0, 0, 0, 0.03);
}

注意:这个filename是给根元素添加的样式。

在浏览器里面效果如下:

引入添加部分

import css from './ListHead.css';

if(!window.LIST_VIEW_STYLE_ELEMENT){
    let styleElement = document.createElement('style');
    styleElement.innerHTML = css;
    document.getElementsByTagName('head')[0].appendChild(styleElement);
    window.LIST_VIEW_STYLE_ELEMENT = true;
}


export default class ListHead{
	constructor(config){
		this[PROPERTY_SYMBOL] = Object.create(null);
		this[ATTRIBUTE_SYMBOL] = Object.create(null);
		this[EVENT_SYMBOL] = Object.create(null);
		this[STATE_SYMBOL] = Object.create(null);
		this[PROPERTY_SYMBOL].urls = config.urls;
		this[STATE_SYMBOL].position = 0;
		this.created();
	}

	created() {
        this.root = document.createElement('div');
        this.root.classList.add('list-head');
		this.root.classList.add('root');
		
		this.container = document.createElement('div');
		this.container.classList.add('container');
		
                this.leftImgBox = document.createElement('div');
		this.leftImgBox.classList.add('left-img-box');
		
		this.render();

	}

如上所示:引入了css导出的内容,当然导出的内容是由loader处理的。并将导出的内容追加到style里面,并追加到document.head里了。

  • 注意create函数里的calsslist添加类的方式,根节点会添加文件名转换为 key-code 方式的名称的类,还会添加一个名为root的类。

组件应用部分

组件应用使用了babel-loader处理,将组件导出的模块可以向标签一样使用。 package.json相关依赖配置如下:

  "devDependencies": {
    "@babel/cli": "^7.6.0",
    "@babel/core": "^7.6.0",
    "@babel/plugin-transform-react-jsx-source": "^7.5.0",
    "@babel/preset-env": "^7.6.0",
    "babel-loader": "^8.0.6",
    "babel-plugin-transform-react-jsx": "^6.24.1",
    "css": "^2.2.4",
    "css-loader": "^3.2.0",
    "to-string-loader": "^1.1.5",
    "file-loader": "^4.2.0",
    "url-loader": "^2.2.0",
    "webpack": "^4.39.3"
  },
  "dependencies": {
    "@babel/polyfill": "^7.6.0",
    "babel-preset-spritejsx": "^1.0.7",
    "webpack-cli": "^3.3.8",
    "webpack-dev-server": "^3.8.0"
  }

webpack.config.js配置如下:

{
    test: /\.js$/,
    use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'],
          plugins: [['babel-plugin-transform-react-jsx', {pragma:"create"}]]
        }
    }
}, 

如上:js文件,将被 “babel-plugin-transform-react-jsx” 转换处理,支持jsx语法。对应的处理函数为"create",这个名字是可以根据喜好更改的。

组件实际应运如下:

import {create} from "./create.js";
import Tabview from "./tabView.js";
import Div from "./div.js";
import Carousel from "./carousel.js";
import ListView from "./ListView.js";
import ListHead from "./ListHead.js";
import ListFrame from "./ListFrame.js";

var c = <Tabview>
	<Div tab-title="推介"
	    placeHolder="加载更多......" 
	    on-scrollToBottom={loadMore} >
		    <Carousel >
		    </Carousel>
    		<div>
    		</div>
    		<ListFrame  data={ obj } ></ListFrame>
		
	</Div>
	<Div tab-title="有趣的店" placeHolder="load more..." 
	    on-scrollToBottom={loadMore} > 
	</Div>
	<Div tab-title="品牌新店" >
		<div>
			<ListHead ></ListHead>
		</div>
		<ListView ></ListView>
	</Div>
</Tabview>;

c.appendTo(document.body);

如上:组件被引入之后,以标签的方式引用,还可以嵌套使用,这就实现了我开头所说的自定以标签,将一个模块,用原生的一些标签组合起来,形成了一个新的自定以标签,及组件,整个业务都可以用这些组件以对应的方式组合来完成,如同搭积木一般,自有拼接,组合成我们想要的样子。

每个组件初始化时,都会调用create初始化,create初始化代码如下:

import Text from './text.js';
import Wrapper from './wrapper.js';

let urls = [
    "https://gw.alicdn.com/imgextra/i3/1618197344/O1CN01ojvFXL247bENVZDBI_!!1618197344-0-beehive-scenes.jpg_790x10000Q75.jpg_.webp",
    "https://gw.alicdn.com/imgextra/i4/1114322414/TB2Jq3lxlmWBuNkSndVXXcsApXa_!!1114322414-2-beehive-scenes.png_790x10000.jpg_.webp",
    "https://gw.alicdn.com/imgextra/i1/3402647387/TB2Mr64rQCWBuNjy0FaXXXUlXXa_!!3402647387-0-beehive-scenes.jpg_790x10000Q75.jpg_.webp",
    "https://gw.alicdn.com/imgextra/i3/1618197344/O1CN01ojvFXL247bENVZDBI_!!1618197344-0-beehive-scenes.jpg_790x10000Q75.jpg_.webp"
];

let config = {
	urls: urls
}

export function create(Class, attributes, ...children){
    var object;
    if(typeof Class === 'string')
        object = new Wrapper(Class);
    else    
        object = new Class(config);

    for(let name in attributes){
        if(name.match(/^on-([\s\S]+)$/)){
            object.addEventListener(RegExp.$1, attributes[name]);
        }else{
            object.setAttribute(name, attributes[name]);
        }
    }
        
    for(let child of children){
        if(child instanceof Array){
            for(let c of child){
                if(typeof(c)  === "string"){
                    object.appendChild(new Text(c));
                }else {
                    object.appendChild(c);
                }
            }
        }else if(typeof(child)  === "object"){
			object.appendChild(child);
		} else {
                        object.appendChild(new Text(child.toString()));
		}
	}
    	
    return object; 
}

上面代码中,引入了 wrapper 和 text,用来处理原始标签和文本内容。

  • 首先通过class判断标签是自定义的,还是原始的,如果是原始的,那么就用wrapper包装class处理。
  • 通过attributes参数,处理attributes,如上代码,如果是以on-开头的,那么就调用addEventListener添加事件方法,否则调用setAttribute方法。
  • 对children的处理,如果children的元素是字符串,那么调用Text组件包装处理。

wrapper.js 代码如下:


const PROPERTY_SYMBOL = Symbol("property");
const ATTRIBUTE_SYMBOL = Symbol("attribute");
const EVENT_SYMBOL = Symbol("event");
const STATE_SYMBOL = Symbol("state");

export default class Wrapper {
    constructor(type){
        this[PROPERTY_SYMBOL] = Object.create(null);
        this[ATTRIBUTE_SYMBOL] = Object.create(null);
        this[EVENT_SYMBOL] = Object.create(null);
        this[STATE_SYMBOL] = Object.create(null);

        this[PROPERTY_SYMBOL].children = [];

        this.root = document.createElement(type);
    }

    appendTo(element){
        element.appendChild(this.root);
    }

    
    get width(){
        return this[PROPERTY_SYMBOL].width;
    }
    set width(value){
        return this[PROPERTY_SYMBOL].width = value;
    }
    get urls(){
    	return this[PROPERTY_SYMBOL].urls;
    }
    set urls(value){
    	return this[PROPERTY_SYMBOL].urls = value;
    }
    get children(){
        return this[PROPERTY_SYMBOL].children;
    }

    appendChild(child){
        this.children.push(child);
        child.appendTo(this.root);

    }

    getAttribute(name){
        return this.root.getAttribute(name);
    }
    setAttribute(name, value){
        this.root.setAttribute(name, value);
    }
    addEventListener(type, listener){
        this.root.addEventListener(...arguments);
    }
    removeEventListener(type, listener){
        this.root.removeEventListener(...arguments);
    }
}

如上:和其他的组件其实是一样的,

  • this.root = document.createElement(type); 注意这儿只创建一个根元素,并且传进来什么标签,就创建什么。

text.js代码如下:


const PROPERTY_SYMBOL = Symbol("property");
const ATTRIBUTE_SYMBOL = Symbol("attribute");
const EVENT_SYMBOL = Symbol("event");
const STATE_SYMBOL = Symbol("state");

export default class Text {
    constructor(text){
        this[PROPERTY_SYMBOL] = Object.create(null);
        this[ATTRIBUTE_SYMBOL] = Object.create(null);
        this[EVENT_SYMBOL] = Object.create(null);
        this[STATE_SYMBOL] = Object.create(null);
        
        this.created(text);
    }

    appendTo(element){
        element.appendChild(this.root);
        this.mounted();
    }

    created(text){
        this.root = document.createElement("span");
        this.root.style.whiteSpace = 'normal';
        this.root.style.fontSize = '30px'; 
        this.root.innerText = text;
    }
    mounted(){

    }
    unmounted(){

    }
    update(){

    }

    
    log(){
        console.log("width:", this.width);
    }
    get width(){
        return this[PROPERTY_SYMBOL].width;
    }
    set width(value){
        return this[PROPERTY_SYMBOL].width = value;
    }
    get urls(){
    	return this[PROPERTY_SYMBOL].urls;
    }
    set urls(value){
    	return this[PROPERTY_SYMBOL].urls = value;
    }
    appendChild(child){
        child.appendTo(this.root);

    }

    getAttribute(name){
        return this[ATTRIBUTE_SYMBOL][name]
    }
    setAttribute(name, value){
        if(name == "style") {
            this.root.setAttribute('style',value);
            this.root.style.width = "100%";
            this.root.style.height = "100%";
            this.root.style.display = "inline-block";
            this.root.style.verticalAlign = "top";
        }
        return this[ATTRIBUTE_SYMBOL][name] = value;
    }
    addEventListener(type, listener){
        if(!this[EVENT_SYMBOL][type])
            this[EVENT_SYMBOL][type] = new Set;
        this[EVENT_SYMBOL][type].add(listener);
    }
    removeEventListener(type, listener){
        if(!this[EVENT_SYMBOL][type])
            return;
        this[EVENT_SYMBOL][type].delete(listener);
    }
    triggerEvent(type){
        for(let event of this[EVENT_SYMBOL][type])
            event.call(this);
    }
}

他和wrapper.js 的主要区别在于,这儿的根元素始终是span标签,用来存放文本内容。

本文从组件的应运场景,组件的组成,以及组件的样式和使用来介绍组件。希望对大家能起到帮助,如果有什么纰漏还请留言讨论指正,欢迎大家一起来讨论。