代码要写成别人看不懂的样子(二十三)

本篇文章参考书籍《JavaScript设计模式》–张容铭

前言

  同学们好,技巧类的设计模式我们告一段落了,本届开始,我们一起学习一个新的种类,架构型设计模式,之前我们学习的都是解决具体问题的相关设计模式,本节开始,我们学习项目框架,这就涉及多人共同开发大型复杂项目了。

  架构型设计模式是一类框架结构,通过提供一些子系统,指定他们的职责,并将他们条理清晰的组织在一起。

同步模块模式

  模块化:将复杂的系统分解成高内聚、低耦合的模块,使系统开发变得可控,可维护,可扩展,提高模块的复用率。

  同步模块模式—SMD:请求发出后,无论模块是否存在,立即执行后续的逻辑,实现模块开发中对模块的立即引用。

  当我们在开发一些大型复杂项目的时候,不能可等一个人完成了,然后下一个人再继续开发,一定是所有人同时开发的,那这就会涉及一个问题,比如我需要写一个导航栏功能,而别人需要写导航栏提示,正常来开发的话,需要先完成导航栏才能再开发导航提示,但这会很影响效率。

  那么我们引入模块化就是为了解决上面的问题。

  模块化的重点在于需要一个模块管理器,它管理着模块的创建与调度,对于模块的调用分为两类,第一类同步模块的调度实现比较简单,不需要考虑模块间的异步加载。第二类异步模块调度的实现就比较繁琐了。它可以实现对模块的加载调度。

  首先我们先实现同步模块模式。

//定义模块管理器单体对象
var F = F || {
    
    };
/**
* 定义模块方法(理论上模块方法应该放在闭包中实现,可以隐藏内部信息,这里我们简化一下)
* @param str 模块路由
* @param fn 模块方法
*/
F.define = function(str, fn) {
    
    
	//解析模块路由
	var parts = str.split('.'),
		//old 当前模块的祖父模块, parent 当前模块父模块
		//如果在闭包中,为了屏蔽对模块直接访问,建议将模块添加给闭包内部私有变量
		old = parent = this,
		//i 模块层级,len 模块层级长度
		i = len = 0;
	//如果第一个模式是模块管理器单体对象,则移除
	if(parts[0] === 'F') {
    
    
		parts = parts.slice(1);
	}
	//屏蔽对 define 与 module 模块方法的重写
	if(parts[0] === 'define' || parts[0] === 'moudle') {
    
    
		return;
	}
	//遍历路由模块并定义每层模块
	for(len = parts.length; i < len; i++) {
    
    
		//如果父模块中不存在当前模块
		if(typeof parent[parts[i] === 'undefined'])	{
    
    
			//声明当前模块
			parent[parts[i]] = {
    
    };
		}
		//缓存下一级的祖父模块
		old = parent;
		//缓存下一级父模块
		parent = parent[parts[i]];
	}
	//如果给定模块方法则定义该模块方法
	if(fn) {
    
    
		//此时 i 等于parts.length, 固减一
		old[parts[--i]] = fn();
	}
	//返回该模块管理器单体对象
	return this;
}

  创建模块的方法 define 我们实现了,下面我们创建一些模块,首先我们创建 String 模块,对于 String 模块,要为我们提供 trim 方法。

//F.string 模块
F.define('string', function() {
    
    
	//接口方法
	return {
    
    
		//清除字符串两边空白
		trim: function(str) {
    
    
			return str.replace(/^\s+|\s+$/g, '');
		}
	}
});

  我们简单测试一下。

/**
* 注意:在真正的模块开发中,是不允许这样直接调用的,有两点原因:
* 1.技术上
* 
* 模块通常保存在闭包内部的私有变量里,而不会保存在 F 上
* 因此是获取不到的,而我们这里简化了闭包,也为了方便测试。
* 
* 2.类似如下调用不符合模块开发规范
*/
F.string.trim('测试用例。')

  对于模块的回调函数,我们也可以以构造函数的形式返回接口,比如我们创建 DOM 模块,其中包括 dom() 获取元素方法、 html() 获取或者设置元素 innerHTML 内容方法等。

F.define('dom', function() {
    
    
	//简化获取元素方法(重复获取可被替代,此设计用于演示模块添加)
	var $ = function(id) {
    
    
		$.dom = document.getElementById(id);
		//返回构造函数对象
		return $;
	}
	//获取或设置元素内容
	$.html = function(html) {
    
    
		//如果传参则设置元素内容,否则获取元素内容
		if(html) {
    
    
			this.dom.innerHTML = html
			return this;
		} else {
    
    
			return this.dom.innerHTML;
		}
	}
	//返回构造函数
	return $;
});
//测试用例(页面元素: <div id="test"> test </div>)
F.dom('test').html();  //"test"

  对于模块的创建,我们也可以先声明后创建,如添加 addClass() 为元素添加 calss 方法。

//为 dom 模块添加 addClass 方法
//注意,此种添加模式之所以可行,是因为将模块添加到 F 对象上,模块化开发中只允许上面的添加方式
F.define('dom.addClass');
F.dom.addClass = function(type, fn) {
    
    
	return function(className) {
    
    
		//如果不存在该类
		if(!~this.dom,calssName.indexOf(calssName)) {
    
    
			//简单添加类
			this.dom.calssName += ' ' + className;
		}
	}
} ();
//测试用例
F.dom('test').addClass('test')

  接下来我们就需要用已有的模块去完成需求,使用模块的话我们需要创建一个“使用”模块方法—— moudle

//模块调用方法
F.moudle = function() {
    
    
		//将参数转化为数组
	var args = [].slice.call(arguments),
		//获取回调执行函数
		fn = args.pop(),
		//获取依赖模块,如果 args[0] 是数组,则依赖模块为 args[0]。否则依赖模块为 arg
		parts = args[0] && args[0] instanceof Array ? args[0] : args,
		//依赖模块列表
		moudles = [];
		//模块路由
		modIDs = '',
		//依赖模块索引
		i = 0//依赖模块长度
		ilen = parts.length,
		//父模块,模块路由层级索引,模块路由层级长度
		parent, j, jlen;
	//遍历依赖模块
	while(i < ilen) {
    
    
		//如果是模块路由
		if(typeof parts[i] == 'string') {
    
    
			//设置当前模块父对象 (F)
			parent = this;
			//解析模块路由, 并屏蔽掉模块父对象
			modIDs = parts[i].replace(/^F\./, '').split('.');
			//遍历模块路由层级
			for(j = 0, jlen = modIDs.length; j < jlen; j++) {
    
    
				//重置父模块
				parent = parent[modIDs[j]] || false;
			}
			//将模块添加到依赖模块列表中
			modules.push(parent);
		//如果是模块对象
		} else {
    
    
			//直接加入依赖模块列表中
			modules.push(parts[i]);
		}
		//取下一个依赖模块
		i++;
	}
	//执行回调执行函数
	fn.apply(null, modules);
}

  对于模块调用方法,参数可分为两部分,依赖模块与回调执行函数(最后一个参数), 它的实现原理是,首先遍历并获取所有的依赖模块,并一次保存在依赖模块列表中,然后将这些依赖模块作为参数传入执行函数中执行。

//引用 dom 模块与 document 模块对象(注意,依赖模块对象通常为已创建的模块对象)
F.module(['dom', document], function(dom, doc) {
    
    
	//通过 dom 模块设置元素内容
	dom('test').html('new add!');
	//通过 document 设置 body 元素背景色
	doc.body.style.background = 'red';
});

  上面我们通过数组来声明了依赖模块,其实我们在定义 module 方法时,依赖模块还可以以字符串形式传入。比如:

//依赖引用 dom 模块,string,trim 方法
F.module('dom', 'string.trim', function(dom, trim) {
    
    
	//测试元素<div id="test"> test </div>
	var html = dom('test').html();   //获取元素内容
	var str = trim(html);            //去除字符串两边空白符
	console.log(`*${
      
      html}*`,`*${
      
      str}*`);  // * test *  *test*
});

  这种变成方式,卸载 module 回调函数里面的功能安全可靠,模块间的组织条例清晰,这样以后我们自己负责自己的模块就可以了。

总个小结

  模块发开发是以分而治之的思想,实现对复杂系统的分解,使系统随着其功能的增加而变得可控、可拓展、可维护。这就要求我们对模块细化。随着系统功能的增加模块的数量也随之增加。模块开发的成本随之减少,但是模块的接口数量却随之增加,接口的使用成本和开发成本与维护成本也随之增加,所以合理的模块分割显得尤为重要。

  模块化开发其实也像以组合模式对模块的组合。因此这也使得系统中的问题一般出现在局部,使得开发人员处理相应模块即可,而不用考虑整个系统。因此相对于整个复杂系统,对局部模块的升级、改造甚至是替换所需成本要小得多。组合的灵活性也使得我们可以实现更复杂、多样化的功能。

  同步模块模式是模块化开发的一种最简单的形式,这种模式使得以来的模块无论加载,无论有无,模块创建即执行,这就要求依赖的模块必然是创建过的。这就限制了同步模块的发展,不过这种模式却很适合服务端如 nodejs 等,服务端文件都存在本地,可以很方便访问到。




猜你喜欢

转载自blog.csdn.net/EcbJS/article/details/111668909