js模块化历程

@吕大豹

今天

不聊:模块化工具的使用方法

聊:模块化开发思想的历程

无模块时代

一个页面没多少js代码,简单从上到下罗列

if(xx){
     //.......
}
else{
     //xxxxxxxxxxx
}
for(var i=0; i<10; i++){
     //........
}
element.onclick = function(){
     //.......
}

模块萌芽时代

  • 2006年ajax被发明
  • “富客户端”思想开始流行

    页面上的js代码越来越多,于是开始暴露一些问题...

1.全局变量的灾难

有一片很长很长的js代码:

  1. 小明定义了 i=1
  2. 小光在后续的代码里:i=0
  3. 小明在接下来的代码里:if(i==1){...} //悲剧

2.函数命名冲突

为了封装一些通用的方法,我们通常在项目中有如下文件:

util.js、common.js

小明定义了一个函数:function formatData(){ }

小刚想实现类似功能,于是这么写:function formatData2(){ }

小光又有一个类似功能,于是:function formatData3(){ }


只能靠人肉来避免命名冲突

3.依赖关系不好管理

b.js依赖a.js,标签的书写顺序必须是

<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
  1. 多人协作开发时不好管理
  2. 我不用的文件我不敢删(可能别人需要)
  3. 页面中冗余的代码越来越多

萌芽时代的解决方案之

自执行函数包装代码

modA = function(){
     var a,b; //变量a、b外部不可见
     return {
          add : function(c){
               a + b + c;
          },
          format: function(){
               //......
          }
     }
}()

最终还是向全局添加了变量modA

萌芽时代的解决方案之

java风格的命名空间

app.util.modA = xxx;
app.tools.modA = xxx;
app.tools.modA.format = xxx;

调用的时候很难受

app.tools.modA.format(data);

萌芽时代的解决方案之

jQuery风格的匿名自执行函数

(function(window){
    //代码

    window.jQuery = window.$ = jQuery;//通过给window添加属性而暴漏到全局
})(window);

向$上扩展方法

$.modA = xxxxx
$.fn.extend(xxx)

未解决根本问题:所依赖的模块由全局提供,向全局添加了变量

模块化面临什么问题

  1. 如何安全的包装一个模块的代码?(不污染模块外的任何代码)
  2. 如何唯一标识一个模块?
  3. 如何优雅的把模块的API暴漏出去?(不能增加全局变量)
  4. 如何方便的使用所依赖的模块?

Modules/1.0规范

  1. 模块的标识应遵循的规则(书写规范)
  2. 定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为别的模块暴漏出来的API
  3. 如果被require函数引入的模块中也包含依赖,那么依次加载这些依赖
  4. 如果引入模块失败,那么require函数应该报一个异常
  5. 模块通过变量exports来向往暴漏API,exports只能是一个对象,暴漏的API须作为此对象的属性。

commonjs模块书写

//math.js
exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};
//increment.js
var add = require('math').add;
exports.increment = function(val) {
    return add(val, 1);
};
var inc = require('increment').increment;
var a = 1;
inc(a); // 2

服务端向前端进军

commonjs(原名serverJs)规范无法在浏览器端直接使用:

  1. 没有function包裹,模块无法形成作用域
  2. 资源需要从服务端下载,无法同步执行

    所以,serverJs更名commonjs,欲统一两端

1.Modules/1.x(保皇派)

主张通过工具将commonjs模块转化为可以在浏览器端使用的模块 制定Modules/Transpor规范

browserify:nodejs模块转化工具

2.Modules/Async(革新派)

主张制定新的规范,通过异步加载,回调执行的方式处理浏览器端的模块

最终分裂出去形成AMD规范


规范的主导者亲自写了实现:requirejs

Modules/2.0(中间派)

主张融合commonjs和AMD的优点,维持commonjs的语法标准,将模块进行包装,使之可以在浏览器端运行

制定了Modules/Wrappings规范

内容大致如下:

1. 全局有一个module变量,用来定义模块
2. 通过module.declare方法来定义一个模块
3. module.declare方法只接收一个参数,那就是模块的factory,次factory可以是函数也可以是对象,如果是对象,那么模块输出就是此对象。
4. 模块的factory函数传入三个参数:require,exports,module,用来引入其他依赖和导出本模块API
5. 如果factory函数最后明确写有return数据(js函数中不写return默认返回undefined),那么return的内容即为模块的输出。

AMD/RequireJs的崛起与妥协

  1. 用全局函数define来定义模块,用法为:define(id?, dependencies?, factory);
  2. id为模块标识,遵从CommonJS Module Identifiers规范
  3. dependencies为依赖的模块数组,在factory中需传入形参与之一一对应
  4. 如果dependencies的值中有"require"、"exports"或"module",则与commonjs中的实现保持一致
  5. 如果dependencies省略不写,则默认为["require", "exports", "module"],factory中也会默认传入require,exports,module
  6. 如果factory为函数,模块对外暴漏API的方法有三种:return任意类型的数据、exports.xxx=xxx、module.exports=xxx
  7. 如果factory为对象,则该对象即为模块的返回值

AMD模块书写

//a.js
define(function(){
     console.log('a.js执行');
     return {
          hello: function(){
               console.log('hello, a.js');
          }
     }
});
//b.js
define(function(){
     console.log('b.js执行');
     return {
          hello: function(){
               console.log('hello, b.js');
          }
     }
});
//main.js
require(['a', 'b'], function(a, b){
     console.log('main.js执行');
     a.hello();
     $('#b').click(function(){
          b.hello();
     });
})

AMD的槽点

上面代码的执行结果:

a.js执行
b.js执行
main.js执行
hello, a.js
点击按钮后:hell, b.js
  1. b.js被预先加载并执行(性能消耗)
  2. 依赖模块的预先声明写起来难受:
    define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){  ..... })
    

不预先声明依赖的写法

define(function(){
     console.log('main2.js执行');

     require(['a'], function(a){
          a.hello();    
     });

     $('#b').click(function(){
          require(['b'], function(b){
               b.hello();
          });
     });
});

AMD向commonjs的妥协

本阵营内也有一些人不爱写回调

于是AMD宣布兼容commonjs的写法,部分兼容Modules/Wrappings规范

称之为:Simplified CommonJS wrapping

Simplified CommonJS wrapping写法

//d.js
define(function(require, exports, module){
     console.log('d.js执行');
     return {
          helloA: function(){
               var a = require('a'); //此处不需写回调
               a.hello();
          },
          run: function(){
               $('#b').click(function(){
                    var b = require('b'); //此处不需写回调
                    b.hello();
               });
          }
     }
});
  1. dependencies数组为空
  2. 但是factory函数的形参必须手工写上require,exports,module
  3. 这不同于之前的dependencies和factory形参全不写

强烈槽点

仅仅是语法支持

并没有真正实现模块的延后执行

require(['d'], function(d){
   //此处未执行任何代码
});

输出

a.js执行
b.js执行
d.js执行

给人造成理解上的误差!

兼容并包的CMD/seajs

  1. 全面拥抱Modules/Wrappings规范
  2. 坚决不写回调
  3. 用define定义模块,而不用declare(容易理解混淆)
  4. 即可以exports.xxx = xxx也可以return来暴露接口

seajs书写

//a.js
define(function(require, exports, module){
     console.log('a.js执行');
     return {
          hello: function(){
               console.log('hello, a.js');
          }
     }
});
//b.js
define(function(require, exports, module){
     console.log('b.js执行');
     return {
          hello: function(){
               console.log('hello, b.js');
          }
     }
});
//main.js
define(function(require, exports, module){
     console.log('main.js执行');

     var a = require('a');
     a.hello();    

     $('#b').click(function(){
          var b = require('b');
          b.hello();
     });

});

seajs的优势

上述代码执行结果:

main.js执行
a.js执行
hello, a.js

点击按钮后:

b.js执行
hello, b.js
  1. commonjs风格的代码
  2. 就近书写,延后执行

能不一开始就下载资源吗?

require.async API

var b = require.async('b');//执行到此处才开始下载b.js
b.hello();



海纳百川的胸襟~

至此,seajs已经兼容了AMD的所有特性

于是玉伯自立门户,创立CMD规范

面向未来的ES6模块标准

ECMA于2015年6月份发布了ES6正式版,也称ES2015

  • 移除了关于模块如何加载/执行的内容
  • 只保留了定义、引入模块的语法
  • ES6 Module还是个雏形

ES6模块的定义

//方式一, a.js
export var a = 1;
export var obj = {name: 'abc', age: 20};
export function run(){....}
//方式二, b.js
var a = 1;
var obj = {name: 'abc', age: 20};
function run(){....}
export {a, obj, run}

ES6模块的引入

//通过import引入模块中的某个接口
import {run as go} from  'a'
go()
//通过module关键字,引入模块的所有接口
module foo from 'a'
console.log(foo.obj);
a.run();

前途是光明的,未来是值得期待的! Thanks!

Powered By nodePPT v1.3.1