zoukankan      html  css  js  c++  java
  • 整理轻量级的前端分离规范

        背景    

     
        支持的业务需要做客户端的页面嵌入,并且此类页面大部分是单页应用。为了能够是组内的人都能快速的入手,并且可以分工开发,制订了这么一个规范,主要做的就是能够快速入手,便于分工协作;另一方面清晰的思路便于查找问题。

        为什么要做分离?
        
        我们知道一个网页的展示,离不开一下部分: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对结构的梳理。
  • 相关阅读:
    脚本,网络配置,指令
    拦截TextBox 双击消息
    VB指针操作和消息钩子
    文件和文件夹操作
    常见反编译产生错误 k__BackingField 解决办法
    关机/重启/注销
    通用命名前缀
    语言区域代码
    常用数据库链接字符串
    用VB实现COM+组件配置
  • 原文地址:https://www.cnblogs.com/xiaoheimiaoer/p/4213351.html
Copyright © 2011-2022 走看看