ππ Blog


  • 首页

  • 归档

  • 标签

浅谈Javascript模块化

发表于 2017-06-03

Javascript模块化编程演变进程

模块好比一个高度独立的功能类,需要时可以随时引入,不需要时可以随时移除而不会影响到整个系统。使用模块有这样几个好处:

1
2
3
1.可维护性。 因为模块是独立的,一个设计良好的模块会让外面的代码对自己的依赖越少越好,这样自己就可以独立去更新和改进。
2.命名空间。 在 JavaScript里面,如果一个变量在最顶级的函数之外声明,它就直接变成全局可用。因此,常常不小心出现命名冲突的情况。使用模块化开发来封装变量,可以避免污染全局环境。
3.重用代码。 我们有时候会喜欢从之前写过的项目中拷贝代码到新的项目,这没有问题,但是更好的方法是,通过模块引用的方式,来避免重复的代码库。我们可以在更新了模块之后,让引用了该模块的所有项目都同步更新,还能指定版本号,避免 API 变更带来的麻烦。

直接定义依赖

最早的时候,我们可能是这样编写代码的

1
2
3
4
5
6
function foo() {
//...
}
function bar() {
//...
}

这种方式会在全局对象中加入多个变量,在多人协作时容易造成命名冲突,在代码量多时不利于维护。

命名空间模式

这种模式的思路就是将变量定义在一个规定的对象内,然后这个对象再挂靠在全局对象内。实现方式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function namespace() {
var a = arguments, o = null, i, j ,d;
for (i = 0; i < a.length; i++) {
d = a[i].split('.');
o = window;
for(j = 0; j < d.length; j++) {
o[d[j]] = o[d[j]] || {};
o = o[d[j]];
}
}
return o;
}
namespace("nav.index");
nav.index = function() {
return {
foo: function() {
//...
},
bar: function() {
//...
}
}
}()

通过开发者工具可以看到window对象下创建了nav对象,对象内又包含index对象,一次类推。由于就是在全局上定义,所有人都可以对这个对象进行修改,一点也不安全。

闭包模块化模式

闭包函数拥有独立的作用域,其中声明的变量只在该作用域里,可以通过暴露一些方法来访问和操作闭包内的变量。

1
2
3
4
5
6
7
8
9
10
11
var module = (function() {
var _private = 'hello private';
var foo = function() {
return _private;
}
return {
foo:foo
}
})()
module.foo() //hello private
module._private //undefined

同是,不同模块之间的应用可以通过参数的形式传递。

1
2
3
4
5
6
7
8
9
10
var module = (function($) {
var _$body = $('body');
var foo = function() {
console.log(_$body);
}
return {
foo:foo
}
})()
module.foo(Jquery)

这些方案虽然解决了依赖关系的问题,但是没有解决如何管理这些模块,或者说在使用时清晰描述出依赖关系。有兴趣的小伙伴可以去了解下LABjs和YUI,这些都是优秀的模块管理工具。

CommonJS

nodejs的出现使得前端开发者可以使用js开发服务端,并且随着nodejs的普及,CommonJS方案也被人们熟知。CommonJs是一套同步的方案。每个文件都是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类都是私有的,对其他文件是不见的。

1
2
3
4
5
6
7
//math.js
export.add = function(a,b) {
return a + b;
}
//main.js
var math = require('math');
console.log(math.add(1,2); //3

由于服务器上通过require加载资源是直接读取文件的,因此中间所需的时间可以忽略不计,但是在浏览器这种需要依赖HTTP获取资源的就不行,资源的获取所需时间的不确定,这就导致必须使用移除机制。

AMD

与Module/1.0(CommonJS)的差异:
1执行时机的差异

1
2
3
4
5
6
7
8
9
Module/1.0
var a = require('./a'); // 执行到此处时,a.js 才同步下载并执行
AMD
define(['require'],function(require) {
// 在这里,模块 a 已经下载并执行好
// ...
var a = require('./a'); //此处仅仅是取模块 a 的 exports
})

2.书写风格的差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Module/1.0
var a = require("./a"); // 依赖就近
a.doSomething();
// AMD recommended style
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面申明并初始化了要用到的所有模块
if (false) {
// 即便压根儿没用到某个模块 b,但 b 还是提前执行了
b.foo()
}
})

CMD

CMD模式的代表就是SeaJS。seaJS的书写风格类似CommonJS,与RequireJS有不少不同点。
1.遵循的规范不同
RequireJS遵循的是Modules/AMD规范。
SeaJS遵循的是Modules/wrappings规范的define形式。
2.factory的执行时机不同

1
2
3
4
5
6
7
8
9
10
在Requirejs里模块有多种书写格式,推荐的是:
define(['./a','./b'],function(a,b) {
a.dosomething(); // 依赖前置,提前执行
b.dosomething();
});
在 SeaJS 里,模块只有一种书写格式:
define(function(require, exports, module){
require('./a').doSomething();
require('./b').doSomething(); // 依赖就近,延迟执行
})

ES6

ES6模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Button.js
function button() {
//...
}
export {button}
//components.js
import {button} from './Button'
使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。另外一种方式使用export default命令,为模块指定默认输出。
//Button.js
export dafault function() {
//...
}
//components.js
import button from './Button'

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。

ES6模块和CommonJS模块的差异

CommonJS模块输出的是值的拷贝,ES6模块输出的是值的引用

CommonJS 模块输出的是值的拷贝,如果这个值类型是简单数据类型,模块内部的变化就影响不到这个值;如果这个是类型是引用数据类型,模块内部的变化是会影响到这个值的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//lib.js
var counter = 1;
function incCounter() {
counter++
}
var obj = {
init:1
};
function changeObj() {
obj.init += 1;
}
module.exports = {
counter: counter,
obj: obj,
incCounter: incCounter,
changeObj: changeObj
};
//main.js
var mod = require('./lib');
console.log(mod.counter); // 1
console.log(mod.obj); //{init:1}
mod.incCounter();
mod.changeObj();
console.log(mod.counter); // 1
console.log(mod.obj); //{init:2}

ES6 模块的运行机制与CommonJS不一样。JS引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6的import有点像Unix系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

1
2
3
4
5
6
7
8
9
10
11
// lib.js
export let counter = 1;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 1
incCounter();
console.log(counter); // 2

CommonJS模块是运行时加载,ES6模块是编译时输出接口

CommonJS加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

12
sujinyang

sujinyang

11 日志
© 2019 sujinyang
由 Hexo 强力驱动
主题 - NexT.Pisces