模块化结构

模块化代码结构

  • Node采用的模块化结构是commonJS规范
  • 模块与文件是一一对应关系,即加载一个模块,实际上就是加载对应的一个模块文件

CommonJS模块的特点

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

模块的分类

  • 文件模块

    就是我们自己写的功能模块文件

  • 核心模块

    Node平台自带的一套基本的功能模块,也有人称之为Node平台的API

  • 第三方模块

    社区或者第三方个人开发好的功能模块,可以直接拿回来用

模块化开发的流程

  1. 创建模块(foo.js)
  2. 导出成员(module.exports = {})
  3. 载入模块 (let foo = require(“./foo.js”)
  4. 使用模块 (foo.bar())

模块内的(伪)全局环境

我们在文件操作中必须使用绝对路径

  • __dirname
    • 用于获取当前文件所在目录的完整路径;
    • 在REPL环境无效
  • __filename
    • 用来获取当前文件的完整路径;
    • 在REPL环境无效;
  • module
    • 模块对象
  • exports
    • 映射到module.exports的别名
  • require()
    • require.cache
    • require.extensions
    • require.main
    • require.resolve()

module对象

Node内部提供一个module构建函数。所有模块都是module的实例。有以下属性:

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块定义的文件的绝对路径。
  • module.loaded返回一个布尔值,表示模块是否已经加载完毕。
  • module.parent 返回一个对象,表示调用该模块的模块
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 表示模块对外输出的值。

载入一个模块就是构建一个module实例

看个例子

|— module

  |— module.js

|— 05-module.js

module/module.js

1
console.log(module);

05-module.js

1
2
3
4
const foo = require("./module/module");
console.log("----------------------------------------");
console.log(module);

然后我们执行05-module.js

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
$ node 05-module.js
Module {
id: '/Users/melody0z/github/nodejs/second/module/module.js',
exports: {},
parent:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/melody0z/github/nodejs/second/05-module.js',
loaded: false,
children: [ [Circular] ],
paths:
[ '/Users/melody0z/github/nodejs/second/node_modules',
'/Users/melody0z/github/nodejs/node_modules',
'/Users/melody0z/github/node_modules',
'/Users/melody0z/node_modules',
'/Users/node_modules',
'/node_modules' ] },
filename: '/Users/melody0z/github/nodejs/second/module/module.js',
loaded: false,
children: [],
paths:
[ '/Users/melody0z/github/nodejs/second/module/node_modules',
'/Users/melody0z/github/nodejs/second/node_modules',
'/Users/melody0z/github/nodejs/node_modules',
'/Users/melody0z/github/node_modules',
'/Users/melody0z/node_modules',
'/Users/node_modules',
'/node_modules' ] }
----------------------------------------
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/melody0z/github/nodejs/second/05-module.js',
loaded: false,
children:
[ Module {
id: '/Users/melody0z/github/nodejs/second/module/module.js',
exports: {},
parent: [Circular],
filename: '/Users/melody0z/github/nodejs/second/module/module.js',
loaded: true,
children: [],
paths: [Object] } ],
paths:
[ '/Users/melody0z/github/nodejs/second/node_modules',
'/Users/melody0z/github/nodejs/node_modules',
'/Users/melody0z/github/node_modules',
'/Users/melody0z/node_modules',
'/Users/node_modules',
'/node_modules' ] }

模块的定义

  • 一个新的js文件就是一个模块
  • 一个合格的模块应该是有导出成员的,否则模块就失去了定义的价值。
  • 模块内部是一个独立(封闭)的作用域(模块与模块之间不会冲突)
  • 模块之间必须通过导出或导入的方式协同
  • 导出方式:
    • exports.name = value;
    • module.exports = {name:value}
  • module.exports和exports
  • module.exports是用于为模块导出成员的接口
  • exports是指向module.exports的别名,相当于在模块开始的时候执行:var exports = module.exports;
  • 一旦为module.exports赋值,就会切断之前两者的相关性;
  • 最终模块的导出成员以module.exports为准。

看个例子

|— module

  |— module.exports.js

|— 06-module.exports.js

module/module.exports.js

1
2
3
4
5
6
7
8
9
let a = 10;
let msg = "hello";
function foo() {
return "我是函数"
}
exports.a = a;
exports.msg = msg;
exports.foo = foo;

06-module.exports.js

1
2
3
4
const bar = require("./module/module.exports");
console.log(bar.a);
console.log(bar.msg);
console.log(bar.foo());

然后我们执行06-module.exports.js

1
2
3
4
$ node 06-module.exports.js
10
hello
我是函数

然后我们用module.exports的方式改写一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let a = 10;
let msg = "hello";
function foo() {
return "我是函数"
}
- exports.a = a;
- exports.msg = msg;
- exports.foo = foo;
+ module.exports = {
+ a:a,
+ msg:msg,
+ foo:foo
+ };

执行06-module.exports.js

1
2
3
4
$ node 06-module.exports.js
10
hello
我是函数

我们再用ES6的语法简化一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let a = 10;
let msg = "hello";
function foo() {
return "我是函数"
}
- module.exports = {
- a:a,
- msg:msg,
- foo:foo
- };
+ module.exports = {
+ a,
+ msg,
+ foo
+ };

执行结果和上面的一样。

载入模块

  • Node使用commonJS模块规范,内置的require函数用于加载模块文件。
  • require的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。
  • 如果没有发现执行模块,会报错。

模块的加载机制

id:路径的情况就是直接以相对路径的方式找文件

require实现机制

  • 将传入的模块ID通过加载规则找到对应的模块文件
  • 读取这个文件里面的代码
  • 通过拼接的方式为该段代码构建私有空间
  • 执行该代码
  • 拿到module.exports返回

我们手动写一个$require函数

|— module

  |— module.exports.js

|— 07-$require.js

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
27
28
29
30
31
32
33
34
//自己写一个require函数,目的是了解require的实现机制
function $require(id) {
//1.先找到文件,文件不存在报错
//2.读取文件内容,内容是js代码
const fs = require("fs");
const path = require("path");
//要加载js的路径(完整路径)
const filename = path.join(__dirname,id);
const dirname = path.dirname(filename);
let code = fs.readFileSync(filename,"utf8"); //同步读取文件,不会加入到事件队列
//3.执行代码,所要执行的代码需要营造一个私有空间
let module = {
id:filename,
exports:{}
};
let exports = module.exports;
code = `(function ($require,module,exports,__dirname,__filename) {
${code}
})($require,module,exports,dirname,filename)`
eval(code);
//4.返回值
return module.exports;
}
const bar = $require("./module/module.exports.js");
console.log(bar.a);
console.log(bar.msg);
console.log(bar.foo());

然后执行

1
2
3
4
$ node 07-\$reqiure.js
10
hello
我是函数

require扩展名

require加载文件时可以省略扩展名:

  • require("./module")
  • require("./module.js")此时文件按js文件执行
  • require("./module.json")此时文件按JSON文件解析
  • require("./module.node")此时文件预编译好的C++模块执行

优先级是js>json>node

require加载文件规则

  • 通过./后../开头:则按照相对路径从当前文件所在文件夹开始寻找模块;

    require("../file.js") 上级目录下找file.js文件

  • 通过/开头:则以系统根目录开始寻找模块;

    require("/Users/melody0z/github/nodejs/second/module/file.js")

    以绝对路径的方式寻找

  • 如果参数字符串不以”./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于node的系统安装目录中)

    require("fs") 加载核心模块中的文件系统(file system)模块

  • 或者从当前目录向上搜索node_modules目录中的文件:

    require("my_module"); 各级node_modules文件夹中搜索my_module.js文件;

require加载目录规则

  • 如果require传入的是一个目录的路径,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件

  • 如果package.json文件没有main字段,或者根本就没有package.json文件,则默认找目录下的index.js文件作为模块

    require("./module");当前目录下找module文件夹中的index.js文件

模块的缓存

  • 第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接冲缓存取出该模块的module.exports属性(不会再次执行该模块)
  • 如果需要多次执行模块中的代码,一般可以让模块暴露行为(函数)
  • 模块的缓存可以通过require.cache拿到,同样也可以删除

验证模块有缓存

目录结构

|— date.js

|— 08.cache.js

date.js

1
module.exports = new Date();

08-cache.js

1
2
3
4
5
const date = require("./date");
setInterval(() => {
console.log(date.getTime());
},1000);

执行08-cache.js

1
2
3
4
5
6
7
$ node 08-cache.js
1500932695278
1500932695278
1500932695278
1500932695278
1500932695278
^C

看,每次输出的时间戳一样,说明结果被缓存起来了。

我们再来看

目录结构

|— date.js

|— 09.cache2.js

09-cache2.js

1
2
3
4
5
6
7
let last;
setInterval(() => {
const date = require("./date");
console.log(last === date);
last = date;
console.log(date.getTime());
},1000);

执行

1
2
3
4
5
6
7
8
9
10
$ node 09-cache2.js
false
1500933712763
true
1500933712763
true
1500933712763
true
1500933712763
^C

同样证明有缓存

缓存长什么样?

目录结构

|— date.js

|— 10.cache3.js

10-cache3.js

1
2
const date = require("./date")
console.log(require.cache);

我们打印一下缓存看看

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
27
28
29
30
31
32
33
34
35
36
37
38
{ '/Users/melody0z/github/nodejs/second/10-cache3.js':
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/melody0z/github/nodejs/second/10-cache3.js',
loaded: false,
children: [ [Object] ],
paths:
[ '/Users/melody0z/github/nodejs/second/node_modules',
'/Users/melody0z/github/nodejs/node_modules',
'/Users/melody0z/github/node_modules',
'/Users/melody0z/node_modules',
'/Users/node_modules',
'/node_modules' ] },
'/Users/melody0z/github/nodejs/second/date.js':
Module {
id: '/Users/melody0z/github/nodejs/second/date.js',
exports: 2017-07-24T22:09:00.024Z,
parent:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/melody0z/github/nodejs/second/10-cache3.js',
loaded: false,
children: [Object],
paths: [Object] },
filename: '/Users/melody0z/github/nodejs/second/date.js',
loaded: true,
children: [],
paths:
[ '/Users/melody0z/github/nodejs/second/node_modules',
'/Users/melody0z/github/nodejs/node_modules',
'/Users/melody0z/github/node_modules',
'/Users/melody0z/node_modules',
'/Users/node_modules',
'/node_modules' ] } }

我们发现,缓存和module对象类似

了解了缓存之后,我们就知道如何清除缓存了

目录结构

|— date.js

|— 11-swipecache.js

11-swipecache.js

1
2
3
4
5
6
7
setInterval(() => {
Object.keys(require.cache).forEach((key) => {
delete require.cache[key];
});
const date = require("./date");
console.log(date.getTime());
},1000);

执行11-swipecache.js

1
2
3
4
5
6
$ node 11-swipecache.js
1500935467664
1500935468692
1500935469699
1500935470701
^C

注意:

先清除缓存,再require。

下面我们将自己写的$require加上缓存

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
27
28
29
30
31
32
33
34
35
36
function $require(id) {
const fs = require("fs");
const path = require("path");
const filename = path.join(__dirname, id);
$require.cache = $require.cache || {};
if ($require.cache[filename]) {
return $require.cache[filename].exports;
}
//第一次没有缓存
const dirname = path.dirname(filename);
let code = fs.readFileSync(filename, "utf8");
//定义一个数据容器,用容器去装模块导出的成员
let module = {
id: filename,
exports: {}
};
let exports = module.exports;
code = `
(function($require, module, exports, __dirname, __filename) {
${code}
})($require, module, exports, dirname, filename);`;
eval(code);
//缓存起来
$require.cache[filename] = module;
return module.exports;
}
setInterval(() => {
const date = $require("./date.js");
console.log(date.getTime());
},1000);

执行

1
2
3
4
5
$ node 12-\$require-cache.js
1500937649769
1500937649769
1500937649769
^C

本文结束,感谢阅读。

本文作者:melody0z
本文链接:https://melodyvoid.github.io/Node/modular-structure.html
欢迎转载,转载请注明文本链接

坚持原创技术分享,您的支持将鼓励我继续创作!