引言:
鸭子类型:
面向对象的编程思想里,有一个有趣的概念,叫鸭子类型:
“一只鸟走起来像鸭子、游起泳来像鸭子、叫起来也像鸭子,那它就可以被当做鸭子。也就是说,它不关注对象的类型,而是关注对象具有的行为(方法)”----面向接口的编程
编程思想还讲求单一原则,也就是要解耦,所以我们希望我们编写程序功能的时候,具有单一职责、和面向接口的特点。
模块化其实也是这种思想,我们赋予模块鲜明特点的功能(如jquery就是dom操作的能手),并把它们可使用的方法属性(就是一种接口)暴露出来,当然,从这个角度来说js模块化开发的引入是有点偏颇了,其实框架的出现都是为了解决一类问题产生的,
在前端开发中,众多的script标签和他们之间的依赖关系复杂化,以及全局变量的增加,使得人工的维护变得困难起来,常犯错误。
-----------------------------------------------
模块写法的演进:
多函数分散写法 --> 对象单体的写法 --> 匿名函数立即返回 --> 传入其他模块实现继承,var m1=(function(m1){ ...;return m1 })(window.module1||{})
(全局变量的污染) (暴露成员易被外部修改)
-----------------------------------------------------
规范:
commonJS
commonJS应用于服务端,服务端加载是和硬盘通信,可以轻易实现同步,只需像require,然后直接使用,但浏览器则会造成长时间等待,故适合异步加载。
在客户端:
将多引入回调参数。
AMD,(Asynchronous Module Definition)客户端异步模块定义,是一种requireJS推广过程产生的规范。
CMD, (Common Module Definition)通用模块定义,是一种seaJS推广过程产生的规范。
---------------------------------------------------------------------------------------------------------------------------------------------
解决思路:
全局变量的消除 --- 按一定规则定义的模块 --- 把方法封入define的方法体内:define([id],dependencies,factory);
依赖关系的实现 --- 延迟和按需加载
具体实现:
<script src="js/require.js" data-main="js/main" defer async="true"></script>后两个属性可以实现放在头部但是在页面最后加载 这个main就是加载完require后首先加载和运行的文件,相当于c语言里的main函数
调用环节:
require.config({
baseUrl: "js/lib",//公共路径
paths: {
"jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min",//支持在线地址
"underscore": "underscore.min",
"backbone": "backbone.min"
}
});
require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){ //注意:模块和参数要一一对应 });
定义环节:
define(['myLib'], function(myLib){ //用一个数组生明依赖
function foo(){ myLib.doSomething(); } return { //要返回 foo : foo }; });
如jquery的末尾有这样的语句:
if ( typeof define === "function" && define.amd && define.amd.jQuery ) { define( "jquery", [], function () { return jQuery; } );//第一个参数是模块id,为可选参数 }
如果一个库没有通过规则定义,则加载的时候可以通过require.config({});参数对象里添加一个属性:
require.config({
shim: { //如果一个模块 'underscore':{ exports: '_' }, 'backbone': { deps: ['underscore', 'jquery'],//生明依赖 exports: 'Backbone'//对外表明身份 } }
});
---------------------------------------------------------------------------------------------------
seaJS
上文require.js有以下特点:对于依赖的的模块,它是提前执行的(虽然从requireJS2.0开始改为延迟执行),而且必须把依赖前置。
而seaJS推崇as lazy as possible,且依赖就近。
define(function(require, exports, module) { //提供三个参数,require实现就近依赖,expotrs实现对外接口,module var a = require('./a') a.doSomething() // 此处略去 100 行 var b = require('./b') // 依赖可以就近书写 b.doSomething() // ... })
http://seajs.org/docs/#docs
使用简要:
引入-配置-入口
<script src="../sea-modules/seajs/seajs/2.2.0/sea.js"></script>
seajs.config({ //配置 base: "../sea-modules/", //设定统一目录 alias: { //设定别名 "jquery": "jquery/jquery/1.10.1/jquery.js" } });
seajs.use("examples/hello/1.0.0/main");//入口
main模块:
define(function(require) { var Spinning = require('./spinning'); var s = new Spinning('#container'); s.render(); });
spinning模块:
define(function(require, exports, module) { var $ = require('jquery'); function Spinning(container) { this.container = $(container); this.icons = this.container.children(); this.spinnings = []; } module.exports = Spinning; Spinning.prototype.render = function() { this._init(); this.container.css('background', 'none'); this.icons.show(); this._spin(); } Spinning.prototype._init = function() { var spinnings = this.spinnings; $(this.icons).each(function(n) { var startDeg = random(360); var node = $(this); var timer; node.css({ top: random(40), left: n * 50 + random(10), zIndex: 1000 }).hover( function() { node.fadeTo(250, 1) .css('zIndex', 1001) .css('transform', 'rotate(0deg)'); }, function() { node.fadeTo(250, .6).css('zIndex', 1000); timer && clearTimeout(timer); timer = setTimeout(spin, Math.ceil(random(10000))); } ); function spin() { node.css('transform', 'rotate(' + startDeg + 'deg)'); } spinnings[n] = spin; }) return this; } Spinning.prototype._spin = function() { $(this.spinnings).each(function(i, fn) { setTimeout(fn, Math.ceil(random(3000))); }); return this; } function random(x) { return Math.random() * x }; });
具体文档:
https://github.com/seajs/seajs/issues/266
*exports
仅仅是 module.exports
的一个引用。在 factory
内部给 exports
重新赋值时,并不会改变 module.exports
的值。因此给 exports
赋值是无效的,不能用来更改模块接口。
好比:
var a={x:1};b=a;b={x:2};console.log(a);//->{x:1} b和a指向关系以及断裂
------------------------------------------------------------------------------------------------------------------------
requireJS和seaJS两者的主要区别如下:
-
定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。Sea.js 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 环境中。
-
遵循的规范不同。RequireJS 遵循 AMD(异步模块定义)规范,Sea.js 遵循 CMD (通用模块定义)规范。规范的不同,导致了两者 API 不同。Sea.js 更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
-
推广理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。Sea.js 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
-
对开发调试的支持有差异。Sea.js 非常关注代码的开发调试,有 nocache、debug 等用于调试的插件。RequireJS 无这方面的明显支持。
-
插件机制不同。RequireJS 采取的是在源码中预留接口的形式,插件类型比较单一。Sea.js 采取的是通用事件机制,插件类型更丰富。
还有不少差异,涉及具体使用方式和源码实现,欢迎有兴趣者研究并发表看法。
总之,如果说 RequireJS 是 Prototype 类库的话,则 Sea.js 致力于成为 jQuery 类库。
最后,向 RequireJS 致敬!RequireJS 和 Sea.js 是好兄弟,一起努力推广模块化开发思想,这才是最重要的。
------------------------------------------------------------------------------------------
尾巴:
软件危机:1960年代中期开始爆发众所周知的软件危机是指落后的软件生产方式无法满足迅速增长的计算机软件需求,从而导致软件开发与维护过程中出现一系列严重问题的现象。
对于前端,不单是js的模块化极具意义,这个前端的模块化(工程化的实现必经)也极具意义。
没有银弹:
软件危机被提出以后,IBM大型电脑之父Fred Brooks在1987年所发表的一篇关于软件工程的经典论文
《没有银弹》,该论述中强调真正的银弹并不存在(银弹是西方驱魔驱鬼的特效武器),而所谓的没有银弹则是指
没有任何一项技术或方法可以能让软件工程的生产力在十年内提高十倍。
所以,前端工程化是一个复杂的实践,我们不能指望掌握了什么时下很流行的框架或者什么前沿知识就能掌握前端,前端是一个永无止息的动态演进过程,对于我们而言,也永远没有“学成本事”这一说,在此互相勉励大家,学无止境,多多益善。
前段工程化是未来之路,模块化开发是前端工程化的最初实践,我们应该学习这种”分而治之“的思想,故我今天做此分享。