javascript組件化

做爲一名前端工程師,寫組件的能力相當重要。雖然javascript常常被人嘲笑是個小玩具,可是在一代代大牛的前仆後繼的努力下,漸漸的也摸索了一套組件的編寫方式。javascript

下面咱們來談談,在現有的知識體系下,如何很好的寫組件。html

好比咱們要實現這樣一個組件,就是一個輸入框裏面字數的計數。這個應該是個很簡單的需求。 <img src="http://www.jqhtml.com/wp-content/uploads/2018/01/jszjh123-1.gif" />前端

咱們來看看,下面的各類寫法。java

<blockquote> 爲了更清楚的演示,下面所有使用jQuery做爲基礎語言庫。 </blockquote>react

<h2>最簡陋的寫法</h2>jquery

嗯 所謂的入門級寫法呢,就是完徹底全的全局函數全局變量的寫法。(就我所知,如今好多外包仍是這種寫法)git

代碼以下:github

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>test</title>
  <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
  <script>
    $(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();


    })
  </script>
</head>
<body>
<input type="text" id="J_input"/>
</body>
</html>

這段代碼跑也是能夠跑的,可是呢,各類變量混亂,沒有很好的隔離做用域,當頁面變的複雜的時候,會很難去維護。目前這種代碼基本是用不了的。固然少數的活動頁面能夠簡單用用。編程

<h2>做用域隔離</h2>數組

讓咱們對上面的代碼做些改動,使用單個變量模擬命名空間。

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() {
  //在domready後調用
  textCount.init({id:'#J_input'}).render();
})

這樣一改造,立馬變的清晰了不少,全部的功能都在一個變量下面。代碼更清晰,而且有統一的入口調用方法。

可是仍是有些瑕疵,這種寫法沒有私有的概念,好比上面的getNum,bind應該都是私有的方法。可是其餘代碼能夠很隨意的改動這些。當代碼量特別特別多的時候,很容易出現變量重複,或被修改的問題。

因而又出現了一種函數閉包的寫法:

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插件都是這種寫法。

<h2>面向對象</h2>

上面的寫法已經能夠知足絕大多數需求了。

可是呢,當一個頁面特別複雜,當咱們須要的組件愈來愈多,當咱們須要作一套組件。僅僅用這個就不行了。首先的問題就是,這種寫法太靈活了,寫單個組件還能夠。若是咱們須要作一套風格相近的組件,並且是多我的同時在寫。那真的是噩夢。

在編程的圈子裏,面向對象一直是被認爲最佳的編寫代碼方式。好比java,就是由於把面向對象發揮到了極致,因此多我的寫出來的代碼都很接近,維護也很方便。可是很不幸的是,javascript不支持class類的定義。可是咱們能夠模擬。

下面咱們先實現個簡單的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
    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)//調用init真正的構造函數
    }

    // 賦值原型鏈,完成繼承
    SubClass.prototype = prototype

    // 改變constructor引用
    SubClass.prototype.constructor = SubClass

    // 爲子類也添加extend方法
    SubClass.extend = _extend

    return SubClass
  }
  //超級父類
  var Class = function() {}
  //爲超級父類添加extend方法
  Class.extend = _extend

  return Class
})()

這是拿John Resig的class簡單修改了下。

<blockquote> 這邊只是很簡陋的實現了類的繼承機制。若是對類的實現有興趣能夠參考我另外一篇文章<a href="http://purplebamboo.github.io/2014/07/13/javascript-oo-class/" target="_blank">javascript oo實現</a> </blockquote> 咱們看下使用方法: ``` //繼承超級父類,生成個子類Animal,而且混入一些方法。這些方法會到Animal的原型上。 //另外這邊不只支持混入{},還支持混入Function var Animal = Class.extend({ init:function(opts){ this.msg = opts.msg this.type = "animal" }, say:function(){ alert(this.msg+":i am a "+this.type) } })

//繼承Animal,而且混入一些方法 var Dog = Animal.extend({ init:function(opts){ //並未實現super方法,直接簡單使用父類原型調用便可 Animal.prototype.init.call(this,opts) //修改了type類型 this.type = "dog" } })

//new Animal({msg:'hello'}).say()

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

使用很簡單,超級父類具備extend方法,能夠繼承出一個子類。子類也具備extend方法。

這邊要強調的是,繼承的父類都是一個也就是單繼承。可是能夠經過extend實現多重混入。詳見下面用法。

有了這個類的擴展,咱們能夠這麼編寫代碼了:

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" }); })

這邊可能還沒看見class的真正好處,不急咱們繼續往下。


<h2>抽象出base</h2>

能夠看到,咱們的組件有些方法,是大部分組件都會有的。
<ul>
<li>好比init用來初始化屬性。</li>
<li>好比render用來處理渲染的邏輯。</li>
<li>好比bind用來處理事件的綁定。</li>
</ul>


固然這也是一種約定俗成的規範了。若是你們所有按照這種風格來寫代碼,開發大規模組件庫就變得更加規範,相互之間配合也更容易。

這個時候面向對象的好處就來了,咱們抽象出一個Base類。其餘組件編寫時都繼承它。

var Base = Class.extend({ init:function(config){ //自動保存配置項 this.__config = config this.bind() this.render() }, //可使用get來獲取配置項 get:function(key){ return this.__config[key] }, //可使用set來設置配置項 set:function(key,value){ this.__config[key] = value }, bind:function(){ }, render:function() {

}, //定義銷燬的方法,一些收尾工做都應該在這裏 destroy:function(){

} })

base類主要把組件的通常性內容都提取了出來,這樣咱們編寫組件時能夠直接繼承base類,覆蓋裏面的bind和render方法。

因而咱們能夠這麼寫代碼:

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的節點了,由於屬性的賦值都是自動的。 input:$("#J_input") }); })

能夠看到咱們直接實現一些固定的方法,bind,render就好了。其餘的base會自動處理(這裏只是簡單處理了配置屬性的賦值)。

事實上,這邊的init,bind,render就已經有了點生命週期的影子,但凡是組件都會具備這幾個階段,初始化,綁定事件,以及渲染。固然這邊還能夠加一個destroy銷燬的方法,用來清理現場。

此外爲了方便,這邊直接變成了傳遞input的節點。由於屬性賦值自動化了,通常來講這種狀況下都是使用getter,setter來處理。這邊就不詳細展開了。




<h2>引入事件機制(觀察者模式)</h2>

有了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了,代碼嚴重耦合。當這種需求特別特別多,代碼會愈來愈亂。

這個時候就要引入事件機制,也就是常常說的觀察者模式。
<blockquote>
注意這邊的事件機制跟平時的瀏覽器那些事件不是一回事,要分開來看。
</blockquote>
什麼是觀察者模式呢,官方的解釋就不說了,直接拿這個例子來講。

想象一下base是個機器人會說話,他會一直監聽輸入的字數而且彙報出去(通知)。而你能夠把耳朵湊上去,聽着他的彙報(監聽)。發現字數超過5個字了,你就作些操做。

因此這分爲兩個部分,一個是通知,一個是監聽。

假設通知是 fire方法,監聽是on。因而咱們能夠這麼寫代碼:

var TextCount = Base.extend({ ... bind:function(){ var self = this; self.get('input').on('keyup',function(){ //通知,每當有輸入的時候,就報告出去。 self.fire('Text.input',self._getNum()) self.render(); }); }, ... })

$(function() { var t = new TextCount({ input:$("#J_input") }); //監聽這個輸入事件 t.on('Text.input',function(num){ //能夠獲取到傳遞過來的值 if(num>5){ alert('超過了5個字了。。。') } }) })

fire用來觸發一個事件,能夠傳遞數據。而on用來添加一個監聽。這樣組件裏面只負責把一些關鍵的事件拋出來,至於具體的業務邏輯均可以添加監聽來實現。沒有事件的組件是不完整的。

下面咱們看看怎麼實現這套事件機制。

咱們首先拋開base,想一想怎麼實現一個具備這套機制的類。

//輔組函數,獲取數組裏某個元素的索引 index var _indexOf = function(array,key){ if (array === null) return -1 var i = 0, length = array.length for (; i < length; i++) if (array[i] === item) return i return -1 }

var Event = Class.extend({ //添加監聽 on:function(key,listener){ //this.__events存儲全部的處理函數 if (!this.__events) { this.__events = {} } if (!this.__events[key]) { this.__events[key] = [] } if (_indexOf(this.__events,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)
}

return this

}, //取消監聽 off:function(key,listener){

if (!key && !listener) {
  this.__events = {}
}
//不傳監聽函數,就去掉當前key下面的全部的監聽函數
if (key && !listener) {
  delete 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()

//添加監聽 test事件 a.on('test',function(msg){ alert(msg) })

//觸發 test事件 a.fire('test','我是第一次觸發') a.fire('test','我又觸發了')

a.off('test')

a.fire('test','你應該看不到我了')

實現起來並不複雜,只要使用this.__events存下全部的監聽函數。在fire的時候去找到而且執行就好了。

這個時候面向對象的好處就來了,若是咱們但願base擁有事件機制。只須要這麼寫:

var Base = Class.extend(Event,{ ... destroy:function(){ //去掉全部的事件監聽 this.off() } }) //因而能夠 //var a = new Base() // a.on(xxx,fn) // // a.fire()

是的只要extend的時候多混入一個Event,這樣Base或者它的子類生成的對象都會自動具備事件機制。

有了事件機制咱們能夠把組件內部不少狀態暴露出來,好比咱們能夠在set方法中拋出一個事件,這樣每次屬性變動的時候咱們均可以監聽到。

到這裏爲止,咱們的base類已經像模像樣了,具備了init,bind,render,destroy方法來表示組件的各個關鍵過程,而且具備了事件機制。基本上已經能夠很好的來開發組件了。


<h2>更進一步,richbase</h2>

咱們還能夠繼續深挖。看看咱們的base,還差些什麼。首先瀏覽器的事件監聽還很落後,須要用戶本身在bind裏面綁定,再而後如今的TextCount裏面還存在dom操做,也沒有本身的模板機制。這都是須要擴展的,因而咱們在base的基礎上再繼承出一個richbase用來實現更完備的組件基類。

主要實現這些功能:
<ul>
<li>事件代理:不須要用戶本身去找dom元素綁定監聽,也不須要用戶去關心何時銷燬。</li>
<li>模板渲染:用戶不須要覆蓋render方法,而是覆蓋實現setUp方法。能夠經過在setUp裏面調用render來達到渲染對應html的目的。</li>
<li>單向綁定:經過setChuckdata方法,更新數據,同時會更新html內容,再也不須要dom操做。</li>
</ul>


咱們看下咱們實現richbase後怎麼寫組件:

var TextCount = RichBase.extend({ //事件直接在這裏註冊,會代理到parentNode節點,parentNode節點在下面指定 EVENTS:{ //選擇器字符串,支持全部jQuery風格的選擇器 'input':{ //註冊keyup事件 keyup:function(self,e){ //單向綁定,修改數據直接更新對應模板 self.setChuckdata('count',self._getNum())

}
}

}, //指定當前組件的模板 template:'<span id="J_input_count"><%= count %>個字</span>', //私有方法 _getNum:function(){ return this.get('input').val().length || 0 }, //覆蓋實現setUp方法,全部邏輯寫在這裏。最後可使用render來決定需不須要渲染模板 //模板渲染後會append到parentNode節點下面,若是未指定,會append到document.body 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() { //傳入parentNode節點,組件會掛載到這個節點上。全部事件都會代理到這個上面。 new TextCount({ parentNode:$("#J_test_container") }); })

/**對應的html,作了些修改,主要爲了加上parentNode,這邊就是J_test_container

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

*/

看下上面的用法,能夠看到變得更簡單清晰了:
<ul>
<li>事件不須要本身綁定,直接註冊在EVENTS屬性上。程序會自動將事件代理到parentNode上。</li>
<li>引入了模板機制,使用template規定組件的模板,而後在setUp裏面使用render(data)的方式渲染模板,程序會自動幫你append到parentNode下面。</li>
<li>單向綁定,無需操做dom,後面要改動內容,不須要操做dom,只須要調用setChuckdata(key,新的值),選擇性的更新某個數據,相應的html會自動從新渲染。</li>
</ul>
下面咱們看下richebase的實現:

var RichBase = Base.extend({ EVENTS:{}, template:'', init:function(config){ //存儲配置項 this.__config = config //解析代理事件 this._delegateEvent() this.setUp() }, //循環遍歷EVENTS,使用jQuery的delegate代理到parentNode _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)
    })
  }

}

}, //支持underscore的極簡模板語法 //用來渲染模板,這邊是抄的underscore的。很是簡單的模板引擎,支持原生的js語法 _parseTemplate:function(str,data){ /** * http://ejohn.org/blog/javascript-micro-templating/ * https://github.com/jashkenas/underscore/blob/0.1.0/underscore.js#L399 / 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() }, //用來實現刷新,只須要傳入以前render時的數據裏的key還有更新值,就能夠自動刷新模板 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)

}, //使用data來渲染模板而且append到parentNode下面 render:function(data){ var self = this //先存儲起來渲染的data,方便後面setChuckdata獲取使用 self.set('__renderData',data)

if (!this.template) return;

//使用_parseTemplate解析渲染模板生成html
//子類能夠覆蓋這個方法使用其餘的模板引擎解析
var html = self._parseTemplate(this.template,data)

var parentNode = this.get('parentNode') || $(document.body)

var currentNode = $(html)
//保存下來留待後面的區域刷新
//存儲起來,方便後面setChuckdata獲取使用
self.set('__currentNode',currentNode)
parentNode.append(currentNode)

}, destroy:function(){

var self = this
//去掉自身的事件監聽
self.off()
//刪除渲染好的dom節點
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[type]

    parentNode.undelegate(select,type,fn)
  }

}

} })

<h2>結語</h2>

有了richbase,基本上組件開發就沒啥問題了。可是咱們仍是能夠繼續深挖下去。

好比組件自動化加載渲染,局部刷新,好比父子組件的嵌套,再好比雙向綁定,再好比實現ng-click這種風格的事件機制。

固然這些東西已經不屬於組件裏面的內容了。再進一步其實已是一個框架了。實際上最近比較流行的react,ploymer還有咱們的brix等等都是實現了這套東西。受限於篇幅,這個之後有空再寫篇文章詳細分析下。
<blockquote>
鑑於有人跟我要完整代碼,其實上面都列出來了。好吧 那我就再整理下,放在github了包含具體的demo,請點<a href="https://github.com/purplebamboo/demo-richbase/tree/master/example" target="_blank">這裏</a>。不過僅僅做爲理解使用最好不要用於生產環境。若是以爲有幫助就給我個star吧。
</blockquote>

<p class="source">原文地址:<a href="https://github.com/purplebamboo/blog/issues/16" rel="noopener" target="_blank">https://github.com/purplebamboo/blog/issues/16</a></p>
相關文章
相關標籤/搜索