从头开始写框架(二):孕育框架的种子_上
今天我们来聊一聊一个框架最基础的部分:种子模块。
这个词是 @司徒正美 在他的《JavaScript框架设计》一书里提出的一个词,意思是:这个模块就好像一棵大树的种子一样,其所有的模块、方法等,都根植于这个种子模块中,它包容其他的模块,使其他的模块之间联系紧密起来,并且让用户更方便的调用模块、方法。
种子模块需要坚持稳定、扩展性高、常用等原则,那么,下面我们就开始编写吧。
在@司徒正美的书中,他提到过,种子模块需要包含如下的功能:对象扩展、数组化、类型判定、简单的事件绑定与卸载、无冲突处理、模块加载与domReady。
我们只是完成一个简单的框架,所以我们不需要包含这些全部功能,只需要包含一些简单的功能即可:对象扩展、模块加载、无冲突处理。恩,大概这么多就够了。其其它的功能,有兴趣的话,可以购买《JavaScript框架设计》自己去翻看。
这部分内容会些长,所以我们分三部分展开。
好啦,下面开始编写我们的种子模块。
1.命名空间
什么是命名空间呢?来举几个例子
比如很多框架都亲睐有加的$符号、avalonjs的avalon、vuejs的Vue等等,这些都是模块的命名空间,它们有的绑定在window对象上(比如JQquey、avalonjs),有的则不绑定在window对象上(比如vuejs,使用的时候用new Vue 的方式来创建新的实例调用)。但是他们都有一个共同的特点,命名空间包含了框架的所有模块与方法,使用者只需要$.XXX这样去调用就可以直接调用模块。
使用命名空间的好处是显而易见的:首先,它防止了全局作用域的污染并且只暴漏一个出口给用户,让用户更方便的调用其内部的模块。
但需要注意的是,有的框架(比如prototypejs)拥有不止一个命名空间,这样做的目的有很多,但是我们的框架只需要一个命名空间,所以今天不展开这个话题。
命名空间相当于一个出入口,用户使用命名空间来告诉框架需要什么模块,框架在内部调用模块后将结果返回给用户。
简单的命名空间的实现是这样的:
if(window !== 'undefined'){ // 判断一下window是否为undefined window.$ = {}; //初始化为一个对象 window.$.css = {} //对象的方法... window.$.animate = {} //对象的方法... //..... }
这样就可以在命名空间上面注册各种模块与方法,使用的时候只需要$.XXX这样去使用就可以了。
但是只是这样的话,显然是不够安全的。
首先,我说过,很多框架都对$符号垂涎三尺(它确实很好用),因此如果在我们的框架之前加载了其他框架,那么就会发生$符命名空间被占用的情况。即使是我们自己单独取名的命名空间(比如我把我自己的框架称作AHjs),也有可能会发生命名空间重名的情况,只要发生这种情况,就会有后加载的框架覆盖先加载的框架的命名空间的情况。这显然不是我们想要的结果。
那么怎么才能同时保留两个框架呢?众所周知,JQquery现在几乎是(甚至可以把几乎去掉)最常用的前端框架,其命名空间就是$符,而JQuery发展初期,当时的前端框架领域的霸主是prototypejs,而其命名空间也是$符号,JQuery为了自身的发展,引入了多库共存机制。而多库共存机制几乎是现在前端框架的标配了。
让我们来看看JQuery是怎么做到的:
var _jQuery = window.jQuery , _$ = window.$ //先用两个变量把可能存在的框架保存起来。 function onConflict(deep){ window.$ = _$ //调用这个方法的时候,再把存好的其他库、框架的命名空间换回去。 if(deep){ //如果存在第二个命名空间(即jQuery) window.jQuery = _jQuery; //一并换回去 }; return jQuery; };
通过这种方式,来对框架重新定义命名空间来解决框架之间冲突的问题。
当然,细心的读者应该发现了,这种方法有一个条件,就是你的类库、框架必须最后加载进html,不然无法起到保存其它框架命名空间的作用。(如果第一个加载,则当时window.$还处于未定的状态,只会保存undefined)。
ok,无冲突处理解决了,现在我们把命名空间也完善一下:
!function(global , target){ //两个函数,分别是作用域、工厂方法 if(typeof global !== 'undefined'){ //如果作用域不是未定义 global.Cvm = global.$ = target(); //把工厂方法返回的模块绑定在命名空间上 }else{ throw new Error("Cvm requires a window with a document") //不然就抛出一个错误 } }(typeof window !== 'undefined' ? window : this , function(){}) // 判断一下,window没问题的话,就传入window 不然的话,传入this(其实也是window)
ok,到这里,我们成功的把命名空间绑定在了window对象上,至于工厂方法,我们用它来创建和返回其它模块,这就要引申出下一个话题:工厂模式
2.最适合框架构建的模式:工厂模式
工厂模式,顾名思义,就像工厂一样:用户发出订单,告诉工厂需要什么模块,什么功能。工厂接到订单后,再组装拼接好用户需要的模块,最后返回给用户。
它使我们的代码更加的工业化与规范,即使是大型框架,动辄及万行代码量,也能够做到结构清晰,维护方便。
工厂模式听起来很复杂,其实实现起来很简单:
function product(){ // 制定一个产品 return { name:"sneakers", state:"new", size:"44" } } function sneakersFactory() {} // 生产产品的工厂 sneakersFactory.prototype.product = product; // 指向产品 sneakersFactory.prototype.createSneakers = function(options){ if(options.sneakersType === "sneakers"){ //如果订单是这个类型 this.product = product; //生产一个sneakers }else{ return options.sneakersType + 'is not defined'; //抱歉,我们工厂暂时没有这个业务... } return new this.product( options ); //返回生产好的产品 } var sneakersFactory = new sneakersFactory(); var sneakers = sneakersFactory.createSneakers({ sneakersType: "sneakers", state: "new", size: 44 } ); console.log(sneakers) // 输出一个运动鞋
这样就实现了一个简单的工厂,而我们所有的模块都会注册在工厂的生产环境里,这样用户需要的时候,就生产一个模块送到他的手上。用这种方式有条不紊的搭建我们的框架,让我们的框架更加的工业化。
接下来我们把工厂模式绑定在种子模块里:
!function(global , target){ if(typeof global !== 'undefined'){ global.Cvm = global.$ = target(); }else{ throw new Error("Cvm requires a window with a document") } }(typeof window !== 'undefined'? window : this , function(){ //工厂是一个函数 return (function(modules){// 用来注册和生产用户调用的模块和方法,参数为模块的集合 })([])//所有的模块在此编写 })
好了,我们的工厂方法已经成功绑定在种子模块中了。但是离开始编写其他功能模块,还有一定的距离,让我们接着往下看。
现在我们有了工厂,可是这个工厂是个空的工厂。
恩,简单说就是它没有工人。 哦 先别管产品,我们要先有工人再去生产产品不是么。想象一下真正的工厂:要有渠道负责销售、要有库管负责提货、有工人负责生产、还需要一个大仓库来存储你拥有的产品。而你就是那个老板(想想还挺带的~)。
so,我们需要先为我们的工厂招一些人。
来看看招聘清单:渠道(负责接受订单),库管(负责运送货物),工人(负责生产产品)。除此之外还需要置办一个仓库(存储已有的产品)
为了解决这些,我们需要一个模块加载机制。
3.模块加载机制
现在市面上已经有很多完善的专注于模块管理的框架了,比如commonjs、requirejs。以及由此衍生的AMD规范。
什么是AMD规范呢?
AMD的全称是Asynchronous Module Definition 异步模块加载机制。
可以说是近几年前端领域的一次很重大的突破。具体的我们不详细展开,有兴趣的朋友可以自己查阅资料。
我们只需要了解AMD规范是怎么运作的就好。
第一次接触AMD规范的朋友,可能会被吓到。因为它是“异步”模块加载机制。 最重要的就是异步两个字,js涉及到异步处理,只能使用回调函数来处理。这就导致了JS领域著名的callback hell(回调地狱)。 恩,确实非常头疼,很多个函数回调不停的嵌套会增加函数与函数之间的耦合度,给后期维护与稳定性造成很大的冲击。
但如果你实际接触到了AMD规范,会发现它其实并没有你想象的那么可怕,即使还是会有回调函数的加入,但已经比之前好很多了。
而且AMD规范已经规定好了模块的写入加载机制,让我们更轻松的使用模块,并且降低模块之间的耦合状态,
好了,不多说,先让我们看看AMD规范需要怎么撰写:
define(id?, dependencies?, factory);
这是AMD规范制定好的撰写模块API,它有三个参数,分别是:
1.模块名 可省略
2.模块所需的依赖 可省略
3.模块的实现 必须
依赖可省略,这个好理解,可能这个模块并不需要额外的依赖就能独立运作,或者干脆这个模块就是用来被其他模块依赖的。
但是模块名可省略?这... 实际上,AMD规范恰恰推荐这种匿名模块的定义方式。而在其模块加载的API:require里,已经制定好了可靠的匿名模块查询机制,所以完全不用担心匿名模块依赖的情况。
好了,具体的我们就不展开聊了,不然可能我今晚都要坐在电脑前打字了。。。
我们只谈最基本的:定义和使用模块,剩下的留到以后展开。
并且,为了方便,我们不允许用户定义匿名模块。因为涉及到require匿名模块查询机制,如果展开来聊,篇幅会很大,所以我们这里只允许用户定义具名模块。
首先需要了解,我们不能让用户或者开发者随意的定义自己的模块:如果我定义了一个模块a 之后又有其他人定义了一个同名模块a 这样的话,就会产生模块覆盖的情况。所以我们要在用户定义的时候,判断一下:如果用户需要的模块已经存在了,就直接给用户返回已经定义过的模块。
为此,我们需要一个仓库来存储已经定义好的模块:
var modules = {}; function define(name , dependencies , fn){ //模块名、依赖、实现 if(!modules[name]){ // 如果要定义的模块不存在 var module = { // 创建一个对象, 用于保存在仓库里 name:name, dependencies:dependencies, fn:fn } // 把名字、依赖、实现保存在对象里 modules[name] = module; //把定义好的模块放进仓库里。 }; return modules[name] // 如果仓库里已经有这个模块了,就从仓库里取出需要的模块给定义者 }
这样的话,我们就可以自由的定义模块而不用害怕模块重名会覆盖已有模块的情况了。
接下来,我们需要使用定义好的模块。
需要注意的是:我们使用定义好的模块时,并不是使用模块本身,而是使用一个它的副本。好处在于,我们不需要每次都调用一次模块,而是单独把每个模块复制给使用者。也即是生产给使用者。
这部分内容我们放到后面的文章去展开。