整理輕量級的前端分離規范


    背景    

 
    支持的業務需要做客戶端的頁面嵌入,並且此類頁面大部分是單頁應用。為了能夠是組內的人都能快速的入手,並且可以分工開發,制訂了這么一個規范,主要做的就是能夠快速入手,便於分工協作;另一方面清晰的思路便於查找問題。

     為什么要做分離?
    
    我們知道一個網頁的展示,離不開一下部分:UI(包括結構和樣式)、UI事件(DOM上綁定的鍵盤或者是鼠標事件)、邏輯處理(各個事件有各自相應的處理邏輯,發送相關請求或DOM操作)、數據(包括UI自身的數據傳遞、接口數據、客戶端數據)。如果將這幾個部分進行分離,一方面可以清晰結構,另一方面出現問題也可以很方便的查找,之間通過一種通信方式進行通信,各部分不用關注各自的具體實現,只需要在自己想要使用的時候進行調用即可。如果沒做做分離,那么邏輯層是溝通UI和數據的橋梁,這樣的話,所有的內容都會混在一起,就是普通的一種開發模式。
    
     確定通信方式    
 
    作為前端,我們最熟悉的就是DOM的事件了,jquery在實現事件的時候,有這么一套東西,可以做到事件綁定($.fn.on)、解綁($.fn.off)、觸發($.fn.trigger或$.fn.triggerHandler)。那么我們能不能自己封裝一下這個事件,讓它作為我們通信的一種方式呢?答案當然是可以的。既然已經確定了通信方式,那我們就可以實現上述分離開的各部分進行通信了,當然最主要的就是邏輯層和UI層的通信。
    
     期望呈現的是形式
 
    我們期望達到的一種狀態是:我們所有使用的數據(客戶端數據和接口返回數據)都寫在一個文件里,這樣如果是數據層面的調整,如:接口調整,只需要更換接口地址即可,不需要在每次調用的地方找接口地址進行替換,做到一改全改的效果;所有DOM綁定的事件需要做的事情,都通過邏輯層按需發送相應請求,當請求返回后,我們肯定是需要拿到數據然后對DOM進行操作(重繪、重排),在邏輯層里,我們並不關注DOM到底應該怎么操作,我們關注的是達到一定的階段需要通知UI做相應的操作,至於UI是否響應對於邏輯層來說並不關注。此時,我們可以使用這個通信事件做很多事情,如:打點統計,暴漏鈎子。。。方便業務在使用的時候,監聽某個步驟發生了什么,在單元測試或者是代碼使用率測試上都有作用。

     基礎結構具體實現(配合代碼分析)
 
    下面就以業務中實現的一個例子(QClient,客戶端開發工具)來分析一下前端分離規范的實現過程吧。
    
     1. 核心模塊(core.js)
    
    做的就是創建可能會使用到的命名空間,便於其他模塊使用。
 
 
/**
 * @Overview 核心模塊
 * 1.創建命名空間
 * 2.初始化公共事件
 */
(function(window) {
	'use strict';

	var QClient = window.QClient = {
		//框架類型
		$ : jQuery,
		//工具類
		utils: {},
		//UI集合
		ui: {},
		//數據集合
		sync: {},
		//事件
		events: {},
		//調試模式
		DEBUG: false
	};
	
})(window);
 
     2. 數據模塊(data.js)
 
    將常見的數據獲取方式放在這里處理,常見的數據方式有:接口請求返回數據和客戶端返回數據(這個在客戶端內嵌頁面會比較常用)。可以處理get、post請求以及客戶端請求,同時處理同域和跨域問題。這樣在調用的時候就不用關注這個請求是什么形式了,暴漏相應的API方便調用即可。這里使用promise方式封裝,使用起來更加方便。這里使用的接口主要是get的跨域請求和客戶端數據,如果想實現其他請求可以參考之前的一篇文章, 跨域請求解決方案
 
  
/**
 * @Overview  數據接口模塊
 * 數據交互,如果接口很少可以放到logic中
 */
(function(Q){

	var $ = Q.$;

    /**
     * 交互類
     * @param {object} param 要提交的數據
     * @param {Object} [ajaxOpt] ajax配置
     * @constructor
     */
    var Sync = function(param, ajaxOpt) {
		if(!param) {
            return;
        }
        var protocol = this.protocol = 'http';
 
        var ajaxOptDefault = {
            url: protocol + '://'+location.host,
            type: 'GET',
            dataType: 'jsonp',
            timeout: 20000
        };
 
        this.protocol = protocol;
        this.param = $.extend({}, param);
        this.ajaxOpt = $.extend({data: this.param}, ajaxOptDefault, ajaxOpt);
        this.HOST = protocol + '://'+location.host;
    };
	
	/* 示例:window.external.getSID(arg0)需要改為 external_call("getSID",arg0) 的形式進行調用 */
	function external_call(extName,arg0,arg1,arg2){
		var args = arguments, fun = args.callee, argsLen = args.length, funLen = fun.length;
		if(argsLen>funLen){
			throw new Error("window.external_call僅接受"+funLen+"個形參,但當前(extName:"+extName+")傳入了"+argsLen+"個實參,請適當調整external_call,以保證參數正常傳遞,避免丟失!");
		}
		if(window.external_call_test){
			return window.external_call_test.apply(null,[].slice.apply(args));
		}
		/* 這里的參數需要根據external_call的形參進行調整,以保證正常傳遞
		 *   IE系列external方法不支持apply、call...
		 *   甚至部分客戶端對參數長度也要求必須嚴格按約定傳入
		 *   所以保證兼容性就必須人肉維護下面這么一坨..
		*/
		if(argsLen==1)return window.external[extName]();  
		if(argsLen==2)return window.external[extName](arg0);  
		if(argsLen==3)return window.external[extName](arg0,arg1);  
		if(argsLen==4)return window.external[extName](arg0,arg1,arg2);  
	}
 
    $.extend(Sync.prototype, {
        /**
         * 通過get方式(jsonp)提交
         * @param {String} [url] 請求鏈接
         * @return {Object} promise對象
         */
        get: function(url) {
            var self = this;
            var send = $.ajax(url, this.ajaxOpt);
            return send.then(this.done, function(statues) {
                return self.fail(statues);
            });
        },
		/**
		 * 通知客戶端
		 */
		informClient: function() {
			var self = this;
			var deferred = $.Deferred();
			var args = [].slice.apply(arguments);
			try {
				var data = external_call.apply(null, args);
				deferred.resolve(data);
			}catch (e) {
				deferred.reject({
					errno: 10000,
					errmsg: '通知客戶端異常'
				});
			}
			return deferred.promise()
				.then(self.done, self.fail);
		},
        /**
         * 收到響應時默認回調
         * @param {Object} data 數據
         * @return {Object}
         */
        done: function (data) {
            var deferred = $.Deferred();
			deferred.resolve(data);
            return deferred.promise();
        },
        /**
         * 未收到響應時默認回調
         * @param {Object} error 錯誤信息
         * @return {Object}
         */
        fail: function(error) {
            var deferred = $.Deferred();
            deferred.reject({
                errno: 999999,
                errmsg: '網絡超時,請稍后重試'
            });
            return deferred.promise();
        }
    });
	
	QClient.Sync = Sync;

})(QClient);
 
     3. 邏輯模塊(logic_factory.js)
 
    主要關聯UI和邏輯層,這里主要做了這么一些事情:第一,作為整個應用的入口,傳入相關參數后,初始化UI;第二:處理整個邏輯內的數據傳遞;第三:根據實際情況暴漏相關接口給外部調用;第四:建立基礎的通信方式,實現邏輯層與UI層的事件通信。具體實現方式和解釋,如下:
 
  
/**
 * @Overview  邏輯模塊工廠
 * 1.定義各功能公用的功能
 * 2.單功能間數據緩存
 */
(function(Q) {
	//'use strict';

	var $ = Q.$;
	var $events = $(Q.events);

	var Logic = function(props) {
		this.name = 'func_' + Q.utils.getGuid();
		this.extend(props);

		this._initFlag = false;
		this._data = {};
	};

	$.extend(Logic.prototype, {
		/**
		 * 初始化函數
		 */
		init : function() {
			var self = this;
			if (!self._initFlag) {
				self._initFlag = true;
				Q.ui[self.name].init(self);
				self.initJsBind();
			}
			return self;
		},
		/**
		 * 獲取是否已經初始化的標記
		 * @returns {boolean}
		 */
		isInit: function() {
			return this._initFlag;
		},
		/**
		 * 獲取數據
		 * @param {String} key
		 * @param {*} defaultValue
		 * @returns {*}
		 */
		get : function(key, defaultValue) {
			var value = this._data[key];
			return value !== undefined ? value : defaultValue;
		},
		/**
		 * 設置數據
		 * @param {String|Object} key
		 * @param {*} value
		 */
		set : function(key, value) {
			if ($.isPlainObject(key)) {
				$.extend(this._data, key);
			} else {
				this._data[key] = value;
			}
			return this;
		},
		/**
		 * 清理數據
		 */
		clear : function() {
			this._data = {};
			return this;
		},
		/**
		 * 客戶端調用頁面JS
		 */
		initJsBind: function () {
			var self = this;
			window.jsBind = function(funcName) {
				var args = [].slice.apply(arguments, [1]);
				return self[funcName].apply(self, args);
			};
		},
		
		/**
		 * 擴展實例方法
		 * @param {...object} - 待mixin的對象
		 */
		extend : function() {
			var args = [].slice.apply(arguments);
			args.unshift(this);
			$.extend.apply(null, args);
		}
	});
	
        //創建事件通信方式
	$.each(['on', 'off', 'one', 'trigger'], function(i, type) {
		Logic.prototype[type] = function() {
			$.fn[type].apply($events, arguments);
			return this;
		};
	});

	Q.getLogic = function(props) {
		return new Logic(props);
	};
})(QClient);
 
     4. 工具模塊(utils.js)
 
    這個模塊兒沒什么特殊的含義,只是存放一些工具方法。可以在基礎包,也可以自己在使用的時候定義。
 
     如何使用?
 
     1. 引入上面所講到的基礎文件
 
     2.分別創建上面對應的功能文件,如:data.js、ui.js、logic.js、utils.js,當然如果項目並不是很大,也可以放在一個文件里實現,這里分開是為了結構更加的清晰
 
    2.1 創建data.js,存放業務將要使用到的所有數據接口
    
  
(function(Q) {
	'use strict';

	var Sync = Q.Sync;
	
	Q.sync = {
		//獲取class
		getClassify: function(catgoryConf) {
			var sync = new Sync();
			return sync.informClient('onGetClassify', catgoryConf);
		},
		//獲取當前皮膚
		getCurrentSkin: function() {
			var sync = new Sync();
			return sync.informClient('GetCurrentSkinName');
		}
	};
}(QClient));
 
    2.2 創建logic.js,建議每個功能創建一個,可以更好的組裝和分離
 
  
/**
 * @Overview  邏輯交互
 * 接收UI狀態,通知UI新操作
 */
(function(Q){
	'use strict';
	
	var utils = Q.utils;
	
	var logic = Q.getLogic({
		name: 'changeSkin',
		
		run: function(opts){
			var _this = this;
			var catgoryConf = opts.catgoryConf;
			
			_this.init();
			
			Q.sync.getClassify(utils.stringify(catgoryConf));
				
			Q.sync.getCurrentSkin()
				.done(function(data) {
					var currKey = data.extra_info;
					_this.setCurrentSkin( currKey );
				});
		},
		//通過事件進行通信
		setCurrentSkin: function (key) {
			this.trigger('setCurrentSkin', key);
		},
		
		setDownLoadStart: function(key) {
			this.trigger('setDownLoadStart', key);
		},
		
		setDownLoadSuccess: function(key) {
			this.trigger('setDownLoadSuccess', key);
		},
		
		setDownLoadFailed: function(key) {
			this.trigger('setDownLoadFailed', key);
		}
      //也可以通過promise對結果進行處理,方便UI直接調用邏輯操作,同時在這里可以保證UI使用到的數據是完全可信的狀態,UI不用判斷數據是否為空等異常情況
	});

	Q.changeSkin = function(opts) {
		logic.run(opts);
	};

})(QClient);

 
 
    2.3 創建ui.js,和邏輯層配合使用
 
  
/**
 * @Overview  UI模塊
 * 頁面交互,通知狀態
 */
(function(Q){
	'use strict';

	var $ = Q.$;
	var $skinlist = $('.skin-list');
	
	var ui = {
		init : function(model) {
			this.model = model;
			this.initEvent();
			this.initModelEvent();
		},
		
		initModelEvent: function() {
			var _this = this;
			//監聽邏輯層觸發的事件
			this.model
				.on('setDownLoadFailed', function( e, key ){
					var $item = _this.getCurrentItem( key );
					_this.stopLoading( $item );
					$item.addClass('err');
				})
				.on('setDownLoadSuccess', function( e, key ){
					var $item = _this.getCurrentItem( key );
					_this.stopLoading( $item );
				})
				.on('setDownLoadStart', function( e, key ){
					var $item = _this.getCurrentItem( key );
					var $loading = $item.find('i span');
					var i = 0;
					$item.addClass('loading').removeClass('err hover');
					$item[0].timer = setInterval(function(){
						i = i >= 12 ? 0 : i;
						var x = -i*32 ;
						$loading.css('left' , x );
						i++;
					},100);
				})
				.on('setCurrentSkin', function( e, key ){
					var $item = _this.getCurrentItem( key );
					_this.stopLoading( $item );
					$item.addClass('selected').siblings().removeClass('selected');
				});
		},
		
		initEvent: function() {
			var _this = this;
			//Q.utils.disabledKey();
			
			$skinlist.on('click','a',function(){
				var $item = $(this).parent();
				if( $item.hasClass('loading') || $item.hasClass('selected')){
					return false;
				}
			});
			
			//hover狀態
			$skinlist.on('mouseover','a',function(){
				var $parent = $(this).parent();
				(!$parent.hasClass('loading') && !$parent.hasClass('selected')) && $parent.addClass('hover');
			}).on('mouseout','a',function(){
				$(this).parent().removeClass('hover');
			});

			//圖片延遲加載
			var $img = $skinlist.find('img');

			$img.lazyload({
				container: $skinlist
			});

			//初始化滾動條
			_this.scrollBar = new CusScrollBar({
				scrollDir:"y",
				contSelector: $skinlist ,
				scrollBarSelector:".scroll",
				sliderSelector:".slider",
				wheelBindSelector:".wrapper",
				wheelStepSize:151
				
			});
			_this.scrollBar._sliderW.hover(function(){ 
				$(this).addClass('cus-slider-hover');
			}, function(){
				$(this).removeClass('cus-slider-hover');
			});
			_this.scrollBar.on("resizeSlider",function(){
				$(".slider-bd").css("height",this.getSliderSize()-10);
			}).resizeSlider();
		},
		
		reload: function () {
			var _this = this;
			var $cur = [];
			$(".item").each(function(){
				if( $(this).css('display') == 'block' ){
					$cur.push($(this));
				}
			});
			$.each( $cur , function( index ){
				if( index <= 9 ){
					var $img = $(this).find('img');
					$img.attr('src',$img.attr('data-original'));
				}
			});
			if( $cur.length <=6 ){
				$(_this.scrollBar.options.scrollBarSelector).hide();
			}
			else{
				$(_this.scrollBar.options.scrollBarSelector).show();
			}
			$(_this.scrollBar.options.sliderSelector).css('top',0);
			_this.scrollBar.resizeSlider().scrollToAnim(0);
		},
		
		LoadCatgory : function( type ){
			if( type && type!="all" ){
				var $items = $skinlist.find('.item[data-type="'+ type +'"]');
				$skinlist.find('.item').hide();
				$items.fadeIn(100);
			}
			else{
				$skinlist.find('.item').fadeIn(100);
			}
			this.reload();
		},
		
		setErrByLevel : function(){
			console&&console.log('等級不符,快去升級吧!');
		},
		
		getCurrentItem: function( key ){
			return $skinlist.find('.item[data-key="'+ key +'"]');
		},
		
		stopLoading : function( $item ){
			if( $item.hasClass('loading') ){
				clearInterval($item[0].timer);
				$item[0].timer = null;
				$item.removeClass('loading');
				$item.find('i span').css('left','0');
			}
		}
	};

	Q.ui.changeSkin = {
		init : function() {
			ui.init.apply(ui, arguments);
		}
	};
})(QClient);
 
    2.4 創建utils.js,除了基礎包中存在的工具,自己業務可能使用到的可以放在這里(可有可無,非必須),以下是舉例這個項目使用到的工具方法
   
  
/**
 * @Overview  工具方法
 * 各種子功能方法:cookie、滾動條、屏蔽按鍵等等
 */
(function(Q){
	'use strict';

	var utils = Q.utils;
	var guid = parseInt(new Date().getTime().toString().substr(4), 10);

	/**
	 * 獲取唯一ID
	 * @returns {number}
	 */
	utils.getGuid = function() {
		return guid++;
	};

	/**
	 * 通用回調解析函數
	 * @param {String|Function|Boolean} callback 回調函數 或 跳轉url 或 true刷新頁面
	 * @returns {Function} 解析后的函數
	 */
	utils.parseCallback = function(callback) {
		if ($.type(callback) == 'function') {
			return callback;
		} else if (callback === true) {
			return function() {
				location.reload();
			};
		} else if ($.type(callback) == 'string' && callback.indexOf('http') === 0) {
			return function() {
				location.href = callback;
			};
		} else {
			return function() {};
		}
	};
	
	/**
	 * 阻止各種按鍵
	 */
	utils.disabledKey = function() {
		document.onkeydown = function(e){
            //屏蔽刷新  F5  Ctrl + F5  Ctrl + R Ctrl + N
            var event = e || window.event;
            var k = event.keyCode;
            if((event.ctrlKey === true && k == 82) || (event.ctrlKey === true && k == 78) || (k == 116) || (event.ctrlKey === true && k == 116))
            {
                event.keyCode = 0;
                event.returnValue = false;
                event.cancelBubble = true;
                return false;
            }
        };
        document.onclick = function( e ){
            //屏蔽 Shift + click Ctrl + click

            var event = e || window.event;

            var tagName = '';
            try{
                tagName = (event.target || event.srcElement).tagName.toLowerCase();
            }catch(error){}

            if( (event.shiftKey || event.ctrlKey) && tagName == 'a' ){
                event.keyCode = 0;
                event.returnValue = false;
                event.cancelBubble = true;
                return false;
            }
        };
        document.oncontextmenu = function(){
            //屏右鍵菜單
            return false;
        };
        document.ondragstart = function(){
            //屏蔽拖拽
            return false;
        };
        document.onselectstart = function( e ){
            //屏蔽選擇,textarea 和 input 除外
            var event = e || window.event;
            var tagName = '';
            try{
                tagName = (event.target || event.srcElement).tagName.toLowerCase();
            }catch(error){}

            if( tagName != 'textarea' && tagName != 'input'){
                return false;
            }
        };
	};
	/**
	 * 對象轉字符串
	 * @param {Object} obj
	 */
	utils.stringify = function(obj) {        
		if ("JSON" in window) {
			return JSON.stringify(obj);
		}

		var t = typeof (obj);
		if (t != "object" || obj === null) {
			// simple data type
			if (t == "string") obj = '"' + obj + '"';

			return String(obj);
		} else {
			// recurse array or object
			var n, v, json = [], arr = (obj && obj.constructor == Array);

			for (n in obj) {
				v = obj[n];
				t = typeof(v);
				if (obj.hasOwnProperty(n)) {
					if (t == "string") {
						v = '"' + v + '"';
					} else if (t == "object" && v !== null){
						v = Safe.stringify(v);
					}

					json.push((arr ? "" : '"' + n + '":') + String(v));
				}
			}

			return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
		}
	};

})(QClient);

  

     3. 通過入口方法進行調用
 
  
QClient.changeSkin({
	catgoryConf: CATGORY_CONF //需要從頁面初始化的參數
});
 
 
     優勢
 
  • 實現了前端各功能結構上的分離,使得結構上更加清晰
  • UI和邏輯分離使用事件通信,可以更好的進行分工開發
  • 便於擴展,方便代碼的重構和升級
 
     劣勢
 
    最明顯的是一個功能可能產出多個文件,不過可以按照上面提到的方式(各模塊都放在一個文件里)解決;也或者搞一個工具來做文件的合並處理,這個現在也不是什么難事。這里針對我們的業務實現了一個專門處理這個的工具,僅供參考( QClient開發工具,該工具集成了上述前端分離規范)【由於第一次開發這種工具,代碼組織的還不是很好,大概就是說明有這么一種形式】
 
    感謝
 
    感謝摩天營的同學提出的改進建議,特別感謝 @jedmeng對結構的梳理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM