浅谈前端模块化

首先,为什么会有模块化的概念?

前端最开始的时候,所有的JS文件都写在一个文件里面,只要加载这一个文件就可以。但是随着JS的迅猛发展,网页中嵌入的JS代码越来越多,一个文件很难管理和维护,必须分成多个文件一次加载。但是一次加载多个JS文件的时候,浏览器会停止网页的渲染,加载文件越多,网页失去响应的时间就会越长。至此,出现了模块化的概念,根据依赖保证加载顺序。

模块的定义

一个模块就是实现一个特定功能的文件。

模块化的好处

1.避免变量污染,命名冲突 2.提高代码复用率 3.提高维护性 4.依赖模块间的管理更方便

模块的分类

  1. Common.js规范

    由于Node.js的诞生,js也可用于服务端编程,而服务端开发的复杂性,模块化是个必须的理念。而Node.js的模块系统就是参照Common.js实现的。

Common.js规范中,一个单独的文件就是一个模块,一个模块有自己的作用域,只向外暴露特定的变量和函数。也就是说在这个模块内定义的变量,无法被其他模块读取。

Common.js规范有四个重要的环境变量:module、exports、require、global。用module.exports定义当前模块对外输出的接口,用require加载模块。

比如下面就是一个简单的模块文件example.js

1
2
3
4
5
6
7
var message = 'hi';

var sayHi = function sayHi() {
return this.message;
}

module.exports = sayHi;

下面是一个引入模块的代码块:

1
2
3
4
var example = require('./example.js');

// 引入核心模块时,不需要带路径
var http = require('http);

Common.js用同步的方式加载模块,也就是说,只有加载完成,才能执行后面的操作。Node.js主要用于服务端,模块文件都存在本地磁盘,读取速度非常快,加载起来也非常快,不用考虑异步加载的方式;但是在浏览器端,要从服务器端加载模块,这时更合理的方案是使用异步加载。

  1. AMD规范

AMD规范采用异步加载模块的方式,模块的加载不影响它后面语句的的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。require.js用来实现AMD规范的模块化。

AMD规范用require.config()指定一用路径,用define()定义模块,用require()加载模块。define()第一个参数表示依赖的模块数组,第二个表示加载完依赖的模块数组后要执行的函数。require()第一个参数是一个参数数组,里面的成员是要加载的模块,第二个参数是callback,就是加载成功后的回调函数。

下面是用define定义模块的例子

1
2
3
4
5
6
7
8
9
define(['lib/dependency'], function(dependency) {
function sayHi() {
console.log('hello world');
}

return {
sayHi: sayHi
};
});

下面是用require加载模块的例子

1
2
3
require(['modules/moduleA'], function(moduleA) {
moduleA.sayHi();
});

AMD运行时的核心思想是提前执行依赖,换句话说,在解析和执行当前模块之前,模块作者必须指明当前模块所依赖的模块,表现在require函数的调用结构上

AMD的优缺点:

尽早执行依赖可以今早发现错误,上例中,假如dependency模块中抛出异常,那么main.js在调用function sayHi方法之前一定会收到错误,sayHi不会执行;今早执行依赖可以让用户的等待时间更短,但是如果依赖的模块最后没被用到,就会浪费带宽和内存。

  1. CMD规范

CMD推崇依赖就近、延迟执行,CMD规范是在sea.js推广的过程中产生的。

下面是CMD的定义模块的写法:

1
2
3
4
5
6
7
8
9
define(function(require, exports, module) {
var a = require('./a');
a.doSomething();

if(false) {
var b = require('./b');
b.doSomething();
}
});

CMD规范可以把依赖写进代码中的任意一行,如上例所示,代码在运行的时候,首先是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。具体的做法是将function toString之后,用正则匹配出require关键字后面的依赖。

  1. ES6 Module(Webpack中使用)

每一个ES6模块都是一个包含JS代码的文件,模块本质上就是一段脚本,而不是用module关键字定义一个模块

  • 在ES6模块中,无论你是否加入’use strict;’语句,默认情况下模块都是在严格模式下运行。
  • 在模块中可以使用import和export关键字,import用来引入模块,export用来导出模块,可以导出所有的最外层函数、类以及var、let或const声明的变量。
  • 所有的声明都被限定在模块的作用域中,对所有脚本和模块全局不可见。

运行的模块中如果包含一条import声明时,首先会加载被导入的模块,然后依赖图的深度优先遍历按顺序执行每一个模块的主体代码,并且所有已执行的模块都会被忽略。

下面是定义一个模块的例子:

1
2
3
4
5
function sayHi() {console.log('hello');}

class setNum {...}

export {sayHi, setNum}

下面是引入模块的代码:

1
import {sayHi, setNum} from 'example';