js模块化进程

模块化进程

模块就是实现特定功能的一组方法。

1、无模块时代:通过script标签来导入一个个的js文件,而一个js文件中,代码简单的堆在一起,把不同的函数简单地放在一起,就算是一个模块,只要能从上往下依次执行就可以了。带来的问题是:全局变量命名冲突、模块成员看不出关系、依赖关系不好管理。
2、模块萌芽时代:

  • 把模块写成一个对象,所有的模块成员都放到这个对象里面。
var module1 = new Object({
    _count : 0,
    m1 : function (){
      //...
    },
    m2 : function (){
      //...
    }
  });
module1.m1();

这样的写法会暴露所有模块成员,内部状态可以被外部改写。

  • 用自执行函数来包装代码
modA = function(){
     var a,b; //变量a、b外部不可见
     return {
          add : function(c){
               a + b + c;
          },
          format: function(){
               //......
          }
     }
}()

这样function内部的变量就对全局隐藏了,达到是封装的目的。但是这样还是有缺陷的,modA这个变量还是暴漏到全局了,随着模块的增多,全局变量还是会越来越多。

  • java风格的命名空间
    为了避免全局变量造成的冲突,人们想到或许可以用多级命名空间来进行管理。用程序员的名字作为前缀:
xiaoming.num=1;
xiaohong.num=2;
  • jQuery风格的匿名自执行函数
//jquery的封装

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

jQuery的封装风格曾经被很多框架模仿,通过匿名函数包装代码,所依赖的外部变量传给这个函数,在函数内部可以使用这些依赖,然后在函数的最后把模块自身暴漏给window。
这种风格虽然灵活了些,但并未解决根本问题:所需依赖还是得外部提前提供、还是增加了全局变量。

模块化要解决的问题
- 封装,模块内的代码不能污染其他模块。
- 标识,唯一的标识一个模块。
- 导出,将模块内的API暴露出去。
- 引入,引入其他模块的API。

3、CommonJS–服务器端的模块化方案
node.js的产生,将javascript语言用于服务器端编程。
在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。

//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);
};
//program.js
var inc = require('increment').increment;
var a = 1;
inc(a);
  • 这使得一个js文件就是一个模块,文件名作为模块标识。
  • 模块通过变量exports来向外暴漏API,exports只能是一个对象,暴漏的API须作为此对象的属性。
  • 定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为别的模块暴漏出来的API。
  • 如果被require函数引入的模块中也包含依赖,那么依次加载这些依赖。

4、CommonJS不能用于浏览器端。

  • 在浏览器端,外层没有function包裹,变量全暴漏在全局。
  • 浏览器端资源的加载方式与服务端完全不同。服务端require一个模块,直接就从硬盘或者内存中读取,消耗的时间可以忽略。而浏览器需要从服务端下载这个文件,然后运行里面的代码才能得到API,需要花费一个http请求。
  • 浏览器端需要function包装,需要异步加载。

5、前端模块化方案
(1)保守派:开发时用CommonJS写法来写代码,执行时用工具将代码转换为适合浏览器使用的代码。如browserify、webpack
(2)革新派:模块在定义的时候指明所依赖的模块,然后把本模块的代码写在回调函数里。模块的加载通过下载-回调这样的过程来进行。形成了AMD这种浏览器端的js模块化规范。
(3)中间派:采纳了CommonJS和AMD的部分优点。CommonJS的通过require来声明依赖,AMD的模块预先加载以及通过return可以暴漏任意类型的数据,而不是像commonjs那样exports只能为object。最终他们制定了一个Modules/Wrappings规范。

  • 全局有一个module变量,用来定义模块。
  • 通过module.declare方法来定义一个模块。
  • module.declare方法只接收一个参数,那就是模块的factory,此factory可以是函数也可以是对象,如果是对象,那么模块输出就是此对象。
  • 模块的factory函数传入三个参数:require,exports,module,用来引入其他依赖和导出本模块API。
  • 如果factory函数最后明确写有return数据(js函数中不写return默认返回undefined),那么return的内容即为模块的输出。
//可以使用exprots来对外暴漏API
module.declare(function(require, exports, module)
{
    exports.foo = "bar";
});
//也可以直接return来对外暴漏数据
module.declare(function(require)
{
return { foo: "bar" };
});

AMD和require.js

异步加载所需的模块,然后在回调函数中执行主逻辑。
1. 用全局函数define来定义模块。用法为:define(dependencies, factory);
2. 文件名为模块标识,遵从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为对象,则该对象即为模块的返回值。

<script data-main="./js/a.js" src="./js/require.js" ></script>

//a.js
require(['b','c','jquery'], function (b,c,$){
    console.log('I am a');
    b.hello();
    $('#header-logo').click(function(){
        c.hello();
    });
});
//b.js
define(function(){
    console.log('I am b');

    return {
        hello:function(){
            console.log('click,b.js');
        }
    }
})

//c.js
define(function(){
    console.log('I am c');

    return {
        hello:function(){
            console.log('click,c.js');
        }
    }
})

引入require.js这个解析器(识别define、require等),即使要上线,用r.js合并js模块后,也依然要引入require.js。
data-main指明入口文件。
执行结果:

I am b
I am c
I am a
click,b.js
//在点击按钮后,会输出:
click,c.js

可见,依赖的模块被预先加载并且预先执行,如果用户不点击按钮,c.js是否应该被执行呢?由于浏览器的环境特点,被依赖的模块肯定要预先下载的。是否需要预先执行?如果一个模块依赖了十个其他模块,那么在本模块的代码执行之前,要先把其他十个模块的代码都执行一遍,不管这些模块是不是马上会被用到。这个性能消耗是不容忽视的。而且,在定义模块的时候,要把所有依赖模块都罗列一遍,还要在factory中作为形参传进去,要写两遍很大一串模块名称。这是require.js被吐槽的两个点。

解决方法:
require.js保留了CommonJS中的require、exprots、module这三个功能。可以不把依赖罗列在dependencies数组中。而是在代码中用require来引入,require依然使用异步加载、回调。

//a.js
define(function (){//默认传入了require、exprots、module
    console.log('I am a');
    require(['b'],function(b){
        b.hello();
    });
    require(['jquery'],function($){
        $('#header-logo').click(function(){
            require(['c'],function(c){
                c.hello();
            });
        });
    })

});
//b.js和c.js不变

以上方法解决了上述两个吐槽点。点击按钮后,才加载和执行c.js,实现了懒加载。缺点是实时下载代码然后在回调中才能执行,用户体验不好,用户的操作会有明显的延迟卡顿。但这样的现实并非是无法接受的,毕竟是浏览器环境,我们已经习惯了操作网页时伴随的各种loading。
执行结果:

I am a
I am b
click,b.js
//在点击按钮后,会输出:
I am c
click,c.js

require.js的另一个吐槽点是使用回调函数,这种写法编程不方便。
解决方法:
require.js借鉴了“中间派”的部分优点,兼容Modules/Wrappings规范。引入模块var a=require('a.js');a.hello();
但只是部分兼容,例如并没有使用module.declare来定义模块,而还是用define,模块的执行时机也没有改变,依旧是预先执行。

//a.js
define(function (require,exports,module){
    console.log('I am a');
    var b=require('b');
    b.hello();
    require('jquery');
    $('#header-logo').click(function(){
        var c=require('c');
        c.hello();
    })
});
//b.js、c.js不变

执行结果:

I am b
I am c
I am a
click,b.js
//点击按钮,输出
click,c.js

注意定义模块时候的轻微差异,dependencies数组为空,但是factory函数的形参必须手工写上require,exports,module,(这不同于之前的dependencies和factory形参全不写),这样写即可使用Simplified CommonJS wrapping风格,与commonjs的格式一致了。
虽然使用上看起来简单,然而在理解上却给后人埋下了一个大坑。因为AMD只是支持了这样的语法,而并没有真正实现模块的延后执行。上面的代码,正常来讲应该是预先下载b.js、c.js,然后在执行模块的b.hello()方法的时候开始执行b.js里面的代码,在点击按钮的时候开始执行c.js中的方法。实际却不是这样,只要此模块被别的模块引入,a.js和b.js中的代码还是被预先执行了。

require.js中入口文件使用define和require函数的区别:
都能执行。但是写法不一样,使用require函数要写明依赖的所有模块和回调函数,如果回调函数内使用了require、exports、module,也要显示写在依赖和回调函数的参数中。而使用define函数,require、exports、module可以省略不写。

关于require.js的更详细用法,会在另一篇文章中介绍。

CMD和seajs

seajs全面拥抱Modules/Wrappings规范,不用requirejs那样回调的方式来编写模块。而它也不是完全按照Modules/Wrappings规范,seajs并没有使用declare来定义模块,而是使用和requirejs一样的define。
教程:
http://blog.codinglabs.org/articles/modularized-javascript-with-seajs.html

//html文件中
<script src="./js/sea.js"></script>
<script>
    seajs.use('./js/a');//入口文件
</script>
//或者
<script src="./sea.js" data-main="./js/a"></script>

//a.js
define(function(require, exports, module){
     console.log('a.js执行');

     var b = require('b');
     b.hello();    
     require('jquery');
     $('#header-logo').click(function(){
          var c = require('c');
          c.hello();
     });

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

执行结果:

a.js执行
b.js执行
hello, b.js
//点击按钮,输出
c.js执行
hello, c.js

以上代码实现预先下载,延迟执行。
定义模块时无需罗列依赖数组,在factory函数中需传入形参require,exports,module,然后它会调用factory函数的toString方法,对函数的内容进行正则匹配,通过匹配到的require语句来分析依赖,这样就真正实现了commonjs风格的代码。

如果要实现点击下载、执行,只要改成以下代码即可

var c = require.async('c');
c.hello();

关于模块对外暴漏API的方式,seajs也是融合了各家之长,支持commonjs的exports.xxx = xxx和module.exports = xxx的写法,也支持AMD的return写法,暴露的API可以是任意类型。

ES6模块标准

对外提供API,用export导出。
使用模块,用import关键字。

//a.js
import {hello,ok} from './b'
hello();
ok();
//b.js
export function hello(){
    console.log('hello b.js');
}
export function ok(){
    console.log('ok b.js');
}

ES6 Module的基本用法就是这样,目前还没有浏览器能支持。
我们可以使用Babel来对ES6进行编译,转化为可以使用的ES5代码。Babel 所做的只是帮你把‘ES6 模块化语法’转化为‘CommonJS 模块化语法’,转换后其中有require exports 等,是 CommonJS 在具体实现中所提供的变量。任何实现 CommonJS 规范的环境(如 node 环境)可以直接运行这样的代码,而浏览器环境并没有实现对 CommonJS 规范的支持,所以我们需要使用打包工具来进行打包,说的直观一点就是把所有的模块组装起来,形成一个常规的 js 文件。这就需要我们额外使用模块打包工具,为我们的代码做一些包裹,让它能在浏览器端使用。 比如 Browserify, Webpack。

用Browserify 和 Babel转化和打包ES6 Module

  • 全局安装Browserify
  • 项目内安装babelify和babel-preset-es2015
  • 项目的根目录下新建.babelrc配置文件
    presets字段设定转码规则
{
    "presets":["es2015"]
}
  • 在package.json设置下面的代码,作为命令参数。
{
  "browserify": {
    "transform": [["babelify", { "presets": ["es2015"] }]]
  }
}
  • 执行命令browserify script.js -o bundle.js
    上面代码将ES6脚本script.js和所有引入的模块,都打包转为bundle.js,浏览器直接加载后者就可以了。

Babel其他知识

  • babel-cli 命令行转码
    npm install --global babel-cli
# 转码结果输出到标准输出
$ babel example.js

# 转码结果写入一个文件
# --out-file 或 -o 参数指定输出文件
$ babel example.js --out-file compiled.js
# 或者
$ babel example.js -o compiled.js

# 整个目录转码
# --out-dir 或 -d 参数指定输出目录
$ babel src --out-dir lib
# 或者
$ babel src -d lib

# -s 参数生成source map文件
$ babel src -d lib -s
  • babel-node ES6的REPL环境
  • 在线转换
    https://babeljs.io/repl/
  • 浏览器环境
    法1:使用babel-standalone模块提供的浏览器版本,将其插入网页。
    网页中实时将ES6代码转为ES5,对性能会有影响。生产环境需要加载已经转码完成的脚本。用户的ES6脚本放在script标签之中,但是要注明type=”text/babel”。
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.4.4/babel.min.js"></script>
<script type="text/babel">
// Your ES6 code
</script>

法2:从Babel 6.0开始,不再直接提供浏览器版本,而是要用构建工具构建出来。如果你没有或不想使用构建工具,可以通过安装5.x版本的babel-core模块获取。
npm install babel-core@5
运行上面的命令以后,就可以在当前目录的node_modules/babel-core/子目录里面,找到babel的浏览器版本browser.js(未精简)和browser.min.js(已精简)。
然后,将下面的代码插入网页。

<script src="node_modules/babel-core/browser.js"></script>
<script type="text/babel">
// Your ES6 code
</script>

上面代码中,browser.js是Babel提供的转换器脚本,可以在浏览器运行。

参考资料:
http://www.cnblogs.com/lvdabao/p/js-modules-develop.html
http://www.ruanyifeng.com/blog/2016/01/babel.html
http://www.ruanyifeng.com/blog/2012/10/javascript_module.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值