前端组件化理解

拿简书作为例子,如果实现组件化的话,那么搜索栏就是一个组件,搜索结果列表也可以是一个组件,右侧内容同样能作为一个组件。甚至搜索栏还可以拆分为多个组件来构成。所以我的理解是组件化思想将页面进行更小粒度的拆分,让我们能更加方便地维护所有功能。

比如在React中:

import { a } form 'b'

...

class c extends Component{
  render(){
    <a>...</a>
  }
}

...

export default c

a作为b中的某一个组件被import进来,然后在c中直接用标签的方式被使用。如果之后页面中出现任何问题,可以直接定位到a组件进行修改。

以下用一个input框的例子来帮助我们更好的理解组件化,全文使用Jquery作为基础语言库。内容与 参考链接:javascript组件化 相同,我只是增加了从1.0到7.0组件化不断优化深挖的时间线、一些简单的批注和个人理解。


HTML只需要

<div id="J_test_container">
    <input type="text" id="J_input"/>
</div>

1.0

首先是最简单的完完全全的全局变量、全局函数的写法:

$(function(){
    var input = $('#J_input');

    function getNum(){
      return input.val().length;
    }

    function render(){
      var num = getNum();

      if($('#J_input_count').length == 0){
        input.after('<span id="J_input_count"></span>');
      }

      $('#J_input_count').html(num + '个字');
    }

    input.on('keyup', function(){
      render();
    });

    render();
  })

制作demo或者活动页面时采取这样的方式是可行的,但在多人协作的中大型项目中,这种方式导致变量混乱,没有作用域隔离。当页面变的复杂时很难去维护。

2.0

var textCount = {
    input: null,
    init: function(config){
      this.input = $(config.id);
      this.bind();

      //实现链式调用
      return this;
    },
    bind: function(){
      var self = this;
      this.input.on('keyup', function(){
        self.render();
      })
    },
    getNum: function(){
      return this.input.val().length;
    },
    render: function(){
      var num = this.getNum();

      if($('#J_input_count').length == 0){
        this.input.after('<span id="J_input_count"></span>');
      }

      $('#J_input_count').html(num + '个字');
    }
  }

  $(function(){
    textCount.init({id: '#J_input'}).render();
  })

这样一改造,立马变得清晰了很多,所有的功能都在一个变量下面。代码更清晰,并且有统一的入口调用方法。

但是还是有些瑕疵,这种写法没有私有的概念,比如上面的getNum,bind应该都是私有的方法。但是其他代码可以很随意的改动这些。当代码量特别特别多的时候,很容易出现变量重复,或被修改的问题。

3.0

var TextCount = (function(){
    var _bind = function(that){
      that.input.on('keyup', function(){
        that.render();
      });
    }

    var _getNum = function(that){
      return that.input.val().length;
    }

    var TextCountFun = function(config){

    }

    TextCountFun.prototype.init = function(config){
      this.input = $(config.id);
      _bind(this);
      return this;
    }

    TextCountFun.prototype.render = function(){
      var num = _getNum(this);

      if($('#J_input_count').length == 0){
        this.input.after('<span id="J_input_count"></span>');
      }

      $('#J_input_count').html(num + '个字');
    }

    return TextCountFun;
  })();

  $(function(){
    new TextCount().init({id: '#J_input'}).render();
  })

这种写法,把所有的东西都包在了一个自动执行的闭包里面,所以不会受到外面的影响,并且只对外公开了TextCountFun构造函数,生成的对象只能访问到init,render方法。这种写法已经满足绝大多数的需求了。事实上大部分的Jquery插件都是这种写法。

4.0

上面的写法已经可以满足绝大多数需求了。

但是当一个页面特别复杂,当我们需要的组件越来越多,当我们需要做一套组件。仅仅用这个就不行了。首先的问题就是,这种写法太灵活了,写单个组件还可以。如果我们需要做一套风格相近的组件,而且是多个人同时在写。那真的是噩梦。

在编程的圈子里,面向对象一直是被认为最佳的编写代码方式(也有人认为不是这样,因为有时候你只是想要一根香蕉,但却拿到了一座大山)。比如java,就是因为把面向对象发挥到了极致,所以多个人写出来的代码都很接近,维护也很方便。但是很不幸的是,javascript不支持class类的定义。但是我们可以模拟。

所以在4.0出现之前我们需要先来实现一个简单的 javascript 类:

在javascript中模拟类

var Class = (function(){
    var _mix = function(r, s){
      for(var p in s){
        if(s.hasOwnProperty(p)){
          r[p] = s[p];
        }
      }
    }

    var _extend = function(){

      //开关,用来使生成原型时,不调用真正的构成流程init,而在new构造函数时执行init
      this.initPrototype = true;
      var prototype = new this();
      this.initPrototype = false;

      var items = Array.prototype.slice.call(arguments) || [];
      var item;

      //支持混入多个属性,并且支持{}也支持 Function,也就是说支持混入对象也支持混入别的类(类就是构造函数)
      while(item = items.shift()){
        _mix(prototype, item.prototype || item);
      }

      function SubClass(){
        if(!SubClass.initPrototype && this.init){
          this.init.apply(this, arguments);
        }
      }

      SubClass.prototype = prototype;

      SubClass.prototype.constructor = SubClass;

      SubClass.extend = _extend;

      return SubClass;
    }

    //超级父类
    var Class = function(){};

    Class.extend = _extend;

    return Class;
  })()

下面是使用方法:

var Animal = Class.extend({
    init: function(opts){
      this.msg = opts.msg;
      this.type = "animal";
    },
    say: function(){
      alert(this.msg + ":i am a" + this.type);
    }
  })

  var Dog = Animal.extend({
    init: function(opts){
      Animal.prototype.init.call(this, opts);
      this.type = "dog";
    }
  })

  var blueDog = Dog.extend({
    init: function(opts){
      Dog.prototype.init.call(this, opts);
    }
  })

  new Dog({msg: 'hi'}).say();
  new blueDog({msg: 'by'}).say();

在javascript类模拟实现后,我们就可以开始4.0版本的组件实现了:

    var TextCount = Class.extend({
        init: function(config){
            this.input = $(config.id);
            this._bind();
            this.render();
        },
        render: function(){
            var num = this._getNum();
            if($('#J_input_count').length == 0){
                this.input.after('<span id="J_input_count"></span>');
            }
            $('#J_input_count').html(num + '个字');
        },
        _getNum: function(){
            return this.input.val().length;
        },
        _bind: function(){
            var self = this;
            self.input.on('keyup', function(){
                self.render();
            });
        }
    })

    $(function(){
        new TextCount({id: '#J_input'})
    });

这样如果我们需要做风格相近的组件,其可扩展性就强太多太多了。

5.0

通过4.0实现了类扩展后,我们又发现大多数组件好像都会有同样的一些方法,于是我们不如抽象出一个Base类,让所有的组件都继承于它是不是更加方便。比如:

  • init用来初始化属性
  • render用来处理渲染的逻辑
  • bind用来处理事件的绑定

当然这也是一种约定俗成的规范了。如果大家全部按照这种风格来写代码,开发大规模组件库就变得更加规范,相互之间配合也更容易。

    var Base = Class.extend({
        init: function(config){
            this.__config = config;
            this.bind();
            this.render();
        },
        get: function(key){
            return this.__config[key];
        },
        set: function(key, value){
            this.__config[key] = value;
        },
        bind: function(){

        },
        render: function(){

        },
        destroy: function(){

        }
    })

    var TextCount = Base.extend({
        _getNum: function(){
            return this.get('input').val().length;
        },
        bind: function(){
            var self = this;
            self.get('input').on('keyup', function(){
                self.render();
            });
        },
        render: function(){
            var num = this._getNum();

            if ($('#J_input_count').length == 0) {
        this.get('input').after('<span id="J_input_count"></span>');
            };

            $('#J_input_count').html(num+'个字');
        }
    })

    $(function(){
        new TextCount({
            input: $('#J_input')
        })
    })

可以看到我们直接实现一些固定的方法,bind,render就行了。其他的base会自动处理(这里只是简单处理了配置属性的赋值)。

事实上,这边的init,bind,render就已经 有了点生命周期的影子 ,但凡是组件都会具有这几个阶段,初始化,绑定事件,以及渲染。当然这边还可以加一个destroy销毁的方法,用来清理现场。

此外为了方便,这边直接变成了传递input的节点 。因为属性赋值自动化了,一般来说这种情况下都是使用getter,setter来处理。这边就不详细展开了。

6.0

有了base应该说我们编写组件更加的规范化,体系化了。
还是上面的那个例子,如果我们希望输入字的时候超过5个字就弹出警告。该怎么办呢。
有人可能会说,那简单啊直接改下bind方法:

var TextCount = Base.extend({
  ...
  bind:function(){
    var self = this;
    self.get('input').on('keyup',function(){
      if(self._getNum() > 5){
        alert('超过了5个字了。。。')
      }
      self.render();
    });
  },
  ...
})

的确也是一种方法,但是太low了,代码严重耦合。当这种需求特别特别多,代码会越来越乱。

这个时候就要引入事件机制,也就是经常说的观察者模式。

注意这边的事件机制跟平时的浏览器那些事件不是一回事,要分开来看。

!!#ff0000 什么是观察者模式呢!!

想象一下base是个机器人会说话,他会一直监听输入的字数并且汇报出去(通知)。而你可以把耳朵凑上去,听着他的汇报(监听)。发现字数超过5个字了,你就做些操作。

所以这分为两个部分,一个是通知,一个是监听。

假设通知是 fire方法,监听是on。我们首先来实现拥有这套机制的类:

    var _indexOf = function(array, key){
        if(array === null) return -1;
        var i = 0, length = array.length;
        for(; i < length; i++) if(array[i] === key) return i;
        return -1;
    }

    var Event = Class.extend({
        on: function(key, listener){
            if(!this.__events){
                this.__events = {};
            }
            if(!this.__events[key]){
                this.__events[key] = [];
            }
            if(_indexOf(this.__events[key], listener) === -1 && typeof listener === 'function'){
                this.__events[key].push(listener);
            }

            return this;
        },
        fire: function(key){
            if(!this.__events || !this.__events[key]) return;
            var args = Array.prototype.slice.call(arguments, 1) || [];
            var listeners = this.__events[key];
            var i = 0;
            var l = listeners.length;
            for(i; i < l; i++){
                listeners[i].apply(this, args);
            }
        },
        off: function(key, listener){
            if(!key && !listener){
                this.__events = {};
            }
            if(key && !listener){
                this.__events[key] = [];
            }
            if (key && listener) {
                var listeners = this.__events[key];
                var index = _indexOf(listeners, listener);
                (index > -1) && listeners.splice(index, 1);
            }
            return this;
        }
    })

    var a = new Event();

    a.on('test', function(msg){
        alert(msg)
    })

    a.fire('test', '我是第一次触发');
    a.fire('test', '我又触发了');

    a.off('test');
    a.fire('test', '你不应该触发');

fire用来触发一个事件,可以传递数据。而on用来添加一个监听。这样组件里面只负责把一些关键的事件抛出来,至于具体的业务逻辑都可以添加监听来实现。没有事件的组件是不完整的。

实现了事件机制类,我们再来完成组件化6.0

    // 面向对象的好处在这里就可以看出来,我们实现事件机制类只需要extend混入Event即可
    var Base = Class.extend(Event, {
        init: function(config){
            this.__config = config;
            this.bind();
            this.render();
        },
        get: function(key){
            return this.__config[key];
        },
        set: function(key, value){
            this.__config[key] = value;
        },
        bind: function(){

        },
        render: function(){

        },
        destroy: function(){
            this.off();
        }
    })

    var TextCount = Base.extend({
        _getNum: function(){
            return this.get('input').val().length;
        },
        bind: function(){
            var self = this;
            self.get('input').on('keyup', function(){
                self.fire('Text.input', self._getNum());
                self.render();
            });
        },
        render: function(){
            var num = this._getNum();

            if ($('#J_input_count').length == 0) {
        this.get('input').after('<span id="J_input_count"></span>');
            };

            $('#J_input_count').html(num+'个字');
        }
    })

    $(function(){
        var t = new TextCount({
            input: $('#J_input')
        });
        t.on('Text.input', function(num){
            if(num > 5){
                alert('超过了5个字');
            }
        })
    })

是的只要extend的时候多混入一个Event,这样Base或者它的子类生成的对象都会自动具有事件机制。

有了事件机制我们可以把组件内部很多状态暴露出来,比如我们可以在set方法中抛出一个事件,这样每次属性变更的时候我们都可以监听到。

到这里为止,我们的base类已经像模像样了,具有了init,bind,render,destroy方法来表示组件的各个关键过程,并且具有了事件机制。基本上已经可以很好的来开发组件了。

7.0

我们还可以继续深挖。看看我们的base,还差些什么。首先 浏览器的事件监听还很落后,需要用户自己在bind里面绑定,再然后现在的TextCount里面还存在dom操作,也没有自己的模板机制 。这都是需要扩展的,于是我们在base的基础上再继承出一个richbase用来实现更完备的组件基类。

主要实现这些功能:

  • 事件代理:不需要用户自己去找dom元素绑定监听,也不需要用户去关心什么时候销毁。
  • 模板渲染:用户不需要覆盖render方法,而是覆盖实现setUp方法。可以通过在setUp里面调用render来达到渲染对应html的目的。
  • 单向绑定:通过setChuckdata方法,更新数据,同时会更新html内容,不再需要dom操作。
    var RichBase = Base.extend({
        EVENTS: {},
        template: '',
        init: function(config){
            this.__config = config;
            this._delegateEvent();
            this.setUp();
        },
        _delegateEvent: function(){
            var self = this;
            var events = this.EVENTS || {};
            var eventObjs, fn, select, type;
            var parentNode = this.get('parentNode') || $(document.body);

            for(select in events){
                eventObjs = events[select];

                for(type in eventObjs){
                    fn = eventObjs[type];
                    parentNode.delegate(select, type, function(e){
                        fn.call(null, self, e);
                    })
                }

            }
        },
        _parseTemplate: function(str, data){
            var fn = new Function('obj',
        'var p=[],print=function(){p.push.apply(p,arguments);};' +
        'with(obj){p.push(\'' + str
            .replace(/[\r\t\n]/g, " ")
            .split("<%").join("\t")
            .replace(/((^|%>)[^\t]*)'/g, "$1\r")
            .replace(/\t=(.*?)%>/g, "',$1,'")
            .split("\t").join("');")
            .split("%>").join("p.push('")
            .split("\r").join("\\'") +
                "');}return p.join('');")
            
                return data ? fn(data) : fn;
        },
        setUp: function(){
            this.render()
        },
        setChuckData: function(key, value){
            var self = this;
            var data = self.get('__renderData')

            data[key] = value;

            if(!this.template) return;
            var newHtmlNode = $(self._parseTemplate(this.template, data));
            var currentNode = self.get('__currentNode');
            if(!currentNode) return;
            currentNode.replaceWith(newHtmlNode);

            self.set('__currentNode', newHtmlNode);
        },
        render: function(data){
            var self = this;
            self.set('__renderData', data);
            if(!this.template) return;
            var html = self._parseTemplate(this.template, data);
            var parentNode = this.get('parentNode') || $(document.body);
            var currentNode = $(html);
            self.set('__currentNode', currentNode);
            parentNode.append(currentNode);
        },
        destroy: function(){
            var self = this;
            self.off();
            self.get('__currentNode').remove();
            var events = self.EVENTS || {};
            var eventObjs, fn, select, type;
            var parentNode = self.get('parentNode');

            for(select in events){
                eventObjs = events[select];

                for(type in eventObjs){
                    fn = eventObjs[select];

                    for(type in eventObjs){
                        fn = eventObjs[type];

                        parentNode.undelegate(select, type, fn);
                    }
                }
            }
        }
    })

看到上面的实现,可以看到变得更简单清晰了:

  • 事件不需要自己绑定,直接注册在EVENTS属性上。程序会自动将事件代理到parentNode上。
  • 引入了模板机制,使用template规定组件的模板,然后在setUp里面使用render(data)的方式渲染模板,程序会自动帮你append到parentNode下面。
  • 单向绑定,无需操作dom,后面要改动内容,不需要操作dom,只需要调用setChuckdata(key,新的值),选择性的更新某个数据,相应的html会自动重新渲染。

在RichBase的基础我们来实现组件化7.0

    var TextCount = RichBase.extend({
        EVENTS: {
            'input': {
                keyup: function(self, e){
                    self.setChuckData('count', self._getNum())
                    self.fire('Text.input', self._getNum());
                }
            }
        },
        template: '<span id="J_input_count"><%=count %>个字</span>',
        _getNum: function(){
            return this.get('input').val().length || 0
        },
        setUp: function(){
            var self = this;
            var input = this.get('parentNode').find('#J_input');
            self.set('input', input);
            var num = this._getNum();
            self.render({
                count: num
            })
        }
    })

    $(function(){
        var t = new TextCount({
            parentNode: $('#J_test_container')
        })
        t.on('Text.input', function(num){
            if(num > 5){
                alert('超过了5个字');
            }
        })
    })

主要做了两件事,一个就是事件的解析跟代理,全部代理到parentNode上面。另外就是把render抽出来,用户只需要实现setUp方法。如果需要模板支持就在setUp里面调用render来渲染模板,并且可以通过setChuckdata来刷新模板,实现单向绑定。

总结

有了richbase,基本上组件开发就没啥问题了。但是我们还是可以继续深挖下去。

比如组件自动化加载渲染,局部刷新,比如父子组件的嵌套,再比如双向绑定,再比如实现ng-click这种风格的事件机制。

当然这些东西已经不属于组件里面的内容了。再进一步其实已经是一个框架了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容