对于模块化的一些见解

模块化
本文主要包含以下知识点:

  • 使用模块的好处
  • 模块加载规则
  • 模块缓存
  • CommonJS 规范
  • 模块原理

使用模块化的好处
模块化是一种设计思想,利用模块化可以把一个非常复杂的系统结构细化到具体的功能点,每个功能看
作一个模块,然后通过某种规则把这些⼩的模块组合到一起,构成模块化系统。
在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易
维护。
为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就
相对较少,很多编程语言都采用这种组织代码的方式。在 Node.js 环境中,一个 JavaScript 文件就称之
为一个模块。
从生产⻆度来看,模块化开发有如下 2 个特点:

  1. 生产效率高
    灵活架构,焦点分离
    多人协作,互不干扰
    方便模块间组合,分解
  2. 维护成本低
    可分的单元测试
    方便单个模块功能的调试和升级
    模块加载规则
    在 Node.js 中,使用 require() 来进行模块的加载。但是加载模块有一定的加载规则,在介绍具体的
    加载规则之前,我们先来看一下模块的分类。主要可以分为 3 大类:文件模块,核心模块以及第三方模
    块。
    文件模块
    使用 require() 函数加载文件模块时,需要使用两种模块标识:
    以 / 开头的模块标识,指向当前文件所属盘符的跟路径。
    以 ./ 或者 …/ 开头的相对路径模块标识。
    加载文件模块的语法如下:
require("路径.扩展名");

例如,加载不同路径下的.js文件,其语法如下:

require("/example.js"); // 如果当前文件在 C 盘,将加载 C:\example.js
require("./example.js"); // 当前目录下的 example.js
require("../example.js"); // 上一级目录下的 example.js

在上面的代码中,可以省略文件的扩展名 .js ,写作 require("/example") ,Node.js 会尝试为文件
名添加 .js , .json , .node 来进行查找。
核心模块
核心模块可以看作是 Node.js 的心脏,它由一些精简而高效的库组成,为 Node.js 提供了基本的 API。
常用的核心模块有:

  • 全局对象
  • 常用工具
  • 事件机制
  • 文件系统访问

HTTP 服务器与客户端
由于 Node.js 的模块机制,这些 Node.js 中内置的核心模块被编译成二进制文件,保存在 Node.js 源码
的 lib 文件夹下,在本质上也是文件模块,但是在加载方式上与文件模块有所区别。
核心模块是唯一的,所以在加载核心模块的时候,不需要书写 ./ , …/ 或者 / 这些开头,直接书写模
块名即可,如下:

require("模块名");

例如,Node.js 模块中提供了一个 OS 核心模块,在该模块中提供了一些与操作系统相关的 API,如
下:

// 核心模块就是一个固定标识
// 如果写错就无法加载
const os = require('os');
// 输出 CPU 信息
console.log(os.cpus());

效果:

NPM_test node index
[ { model: 'Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz',
 speed: 2700,
 times:
 { user: 7304340, nice: 0, sys: 6596550, idle: 85524090, irq: 0 } },
{ model: 'Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz',
speed: 2700,
times:
{ user: 3011400, nice: 0, sys: 1714130, idle: 94697450, irq: 0 } },
{ model: 'Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz',
speed: 2700,
times:
{ user: 7244960, nice: 0, sys: 4380050, idle: 87797960, irq: 0 } },
{ model: 'Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz',
speed: 2700,
times:
{ user: 2767370, nice: 0, sys: 1446820, idle: 95208790, irq: 0 } } ]

第三方模块
社区或第三方开发的功能模块,这种模块在 Node.js 里面本身没有,需要通过 NPM 的方式下载之后再
引入。比如要操作 mysql 数据库,则需要引入 mysql 这个模块。

例如:

const express = require('mysql');

加载规则
在了解了模块的分类以后,我们就可以来具体地看一下模块的加载规则了。使用 require() 进⾏模块
加载时,需要经历 3 个步骤:

  • 路径分析
  • 文件定位
  • 编译执行

先来看路径分析
当require() 当中的参数字符串以 ./ 或 …/ 开头,表示按照相对路径,从当前文件所在的文件夹开始
寻找要载入的模块文件。当 require() 当中的参数字符串以 / 开头,则表示从系统根目录开始寻找该
模块文件。不能直接写文件名。若在参数字符串当中直接写文件名,则代表载入的是一个模块包,模块
包必须放在一个特定名字的文件夹当中,即 node_modules。
使用 require() 来加载文件时可以省略扩展名。比如 require(’./module’) ,此时会作出如下的匹
配操作:
按 js 文件来执行(先找对应路径当中的 module.js 文件来加载)
按 json 文件来解析(若上面的 js 文件找不到时,则找对应路径当中的 module.json 文件来加载)
按照预编译好的c++模块来执行(寻找对应路径当中的module.node文件来加载)
若参数字符串为一个目录(文件夹)的路径,则自动先查找该文件夹下的 package.json 文件,然后再加载该文件当中 main 字段所指定的入口文件。
注:若 package.json 文件当中没有 main 字段,或者根本没有 package.json 文件,则再默认查
找该文件夹下的 index.js 文件作为模块来载入。
上面所介绍的是加载文件的路径分析。如果参数字符串不以 ./ , …/ 或 / 开头,说明要加载的不是一个文件,而是一个默认提供的核心模

块。此时则先在 Node.js 平台所提供的核心模块当中找,然后再寻找 NPM 模块(即第三方模块包,或自己写的模块包)。在寻找 NPM 模块包时,
会从当前目录出发,向上搜索各级当中的 node_modules 文件夹当中的文件,但若有两个同名文件,则遵循就近原则。
路径分析完毕后,就会根据路径去定位文件,然后进行编译执行。但是不同类型的模块也存在一个优先
级的问题。其中 Node.js 的系统模块的优先级最高,一旦有第三方模块包与系统模块重名,则以系统模
块为准。总的来讲,其优先顺序从上往下依次为:

  • 核心模块,如 http、fs、path
  • 以 . 或 … 开始的相对路径文件模块
  • 以 / 开始的绝对路径文件模块
  • 非路径形式的文件模块

模块缓存
在模块加载的过程中,对于多次加载同一个模块的情况,Node.js 只会加载一次。这是由于第一次加载
某模块时,Node.js 会缓存该模块,再次加载时将从缓存中获取。所有缓存的模块保存
在 require.cache 中,可以自动删除模块缓存。下面我们来演示模块的缓存,如下:
首先在项目根目录下创建 2.js ,代码如下:

console.log("模块被加载了");

接下来在 index.js 中使用 require() 方法来引入5次 2.js 文件:

require("./2.js");
require("./2.js");
require("./2.js");
require("./2.js");
require("./2.js");

效果:

模块被加载了

可以看到,在上述代码中,虽然加载了 5 次 2.js 模块,但是只打印一次"模块被加载了",这就说
明 2.js 模块只被加载了一次。
我们可以在 REPL 模式下输入 require 来查看当前的模块缓存情况,如下:

NPM_test node
> require
{ [Function: require]
 resolve: { [Function: resolve] paths: [Function: paths] },
 main: undefined,
 extensions:
 { '.js': [Function], '.json': [Function], '.node': [Function] },
 cache: {} }

此时会返回一个对象,该对象的具体信息如下

require(): 加载外部模块
require.resolve(): 将模块名解析到一个绝对路径
require.main: 指向主模块
require.cache: 指向所有缓存的模块
require.extensions: 根据文件的后缀名,调用不同的执行函数

在实际开发中,有些时候开发者并不希望加载的模块被缓存,这个时候可以选择删除缓存操作,在被加
载的模块下面添加如下的代码:

//删除指定模块的缓存
delete require.cache[module.filename];
// or
// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
 delete require.cache[key];
})

这⾥我们来示例一个就好了。例如这里的 2.js 是被加载的模块,所以在该模块的下面添加如下的代
码:

console.log("foo模块被加载了");
delete require.cache[module.filename];

之后我们再次访问 index.js ,结果如下:

模块被加载了
模块被加载了
模块被加载了
模块被加载了
模块被加载了

可以看到,加载了 2.js 模块后,模块并没有被缓存,所以输出了 5 次"模块被加载了",这说明缓存成
功被清除了。
CommonJS 规范
上面我们所介绍的模块加载机制属于 CommonJS 规范。这⾥简单介绍一下什么是 CommonJS 规范。
Node.js 并不是第一个尝试使 JavaScript 运行在浏览器之外的项目。追根溯源,在 JavaScript 诞生之
初,网景公司就实现了服务端的 JavaScript,但由于需要支付一大笔授权费用才能使用,服务端
JavaScript 在当年并没有像客户端 JavaScript 一样流行开来。真正使大多数人见识到 JavaScript 在服务
器开发威力的,是微软的 ASP。
2000年左右,也就是 ASP 蒸蒸日上的年代,很多开发者开始学习 JScript。然而 JScript 在当时并不是很
受欢迎,一方面是早期的 JScript 和 JavaScript 兼容较差,另一方面微软大力推广的是 VBScript,而不
是 JScript。随着后来 LAMP 的兴起,以及 Web 2.0 时代的到来,Ajax 等一系列概念的提出,JavaScript
成了前端开发的代名词,同时服务端 JavaScript 也逐渐被人遗忘。
直至几年前,JavaScript 的种种优势才被重新提起,JavaScript 又具备了在服务端流行的条件,Node.js
应运而生。与此同时,RingoJS 也基于 Rhino 实现了类似的服务端 JavaScript 平台,还有像CouchDB、
MongoDB 等新型非关系型数据库也开始用 JavaScript 和 JSON 作为其数据操纵语言,基于 JavaScript
的服务端实现开始遍地开花。
CommonJS 规范与实现
正如当年为了统一 JavaScript 语言标准,人们制定了 ECMAScript 规范一样,如今为了统一JavaScript
在浏览器之外的实现,CommonJS 诞生了。CommonJS 试图定义一套普通应用程序使用的 API,从而
填补 JavaScript 标准库过于简单的不足。
CommonJS 的终极目标是制定一个像 C++ 标准库一样的规范,使得基于 CommonJS API 的应用程序可
以在不同的环境下运行,就像用 C++ 编写的应用程序可以使用不同的编译器和运行时函数库一样。为了
保持中立,CommonJS 不参与标准库实现,其实现交给像 Node.js 之类的项目来完成。

CommonJS 规范包括了模块(modules)、包(packages)、系统(system)、二进制(binary)、
控制台(console)、编码(encodings)、⽂件系统(filesystems)、套接字(sockets)、单元测试
(unit testing)等部分。
目前大部分标准都在拟定和讨论之中,已经发布的标准有 Modules/1.0、Modules/1.1、
Modules/1.1.1、Packages/1.0、System/1.0。
Node.js 是目前 CommonJS 规范最热门的一个实现,它基于 CommonJS 的 Modules/1.0 规范实现了
Node.js 的模块,同时随着 CommonJS 规范的更新,Node.js 也在不断跟进。由于目前 CommonJS 大
部分规范还在起草阶段,Node.js 已经率先实现了一些功能,并将其反馈给 CommonJS 规范制定组
织,但 Node.js 并不完全遵循 CommonJS 规范。这是所有规范制定者都会遇到的尴尬局面,因为规范
的制定总是滞后于技术的发展。
模块原理
Node.js 应⽤是由模块组成的,遵循了 CommonJS 的模块规范,来隔离每个模块的作用域,使每个模
块在它自身的命名空间中执行。
CommonJS 规范的主要内容:
模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当
前模块作用域中。
CommonJS 模块的特点:

  • 所有代码运行在当前模块作用域中,不会污染全局作用域
  • 模块同步加载,根据代码中出现的顺序依次加载
  • 模块同步加载,根据代码中出现的顺序依次加载
  • 就直接读取缓存结果。要想让模块再次运行,必须清除缓存。

一个简单的例子:
我们在 2.js 中导出一些属性和方法,如下:

module.exports.name = 'Aphasia';
module.exports.sayHello = function(){
 console.log('Hello World');
};

接下来我们就可以在 index.js 中引用该模块,如下:

const test = require('./2.js');
console.log(test.name); // Aphasia
test.sayHello(); // Hello World

module 对象

根据 CommonJS 规范,每一个文件就是一个模块,在每个模块中,都会有一个 module 对象,这个对
象就指向当前的模块。 module 对象具有以下属性:

  • id:当前模块的id
  • exports:表示当前模块暴露给外部的值
  • parent: 是一个对象,表示调用当前模块的模块
  • children:是一个对象,表示当前模块调用的模块
  • filename:模块的绝对路径
  • paths:从当前文件目录开始查找node_modules目录;然后依次进入父目录,查找父目录下的
  • node_modules目录;依次迭代,直到根目录下的node_modules目录
  • loaded:一个布尔值,表示当前模块是否已经被完全加载

下面我们可以在导出的模块和引入的模块中分别打印这个 module 对象,如下:
2.js 作为导出的模块:

module.exports.name = 'Aphasia';
module.exports.sayHello = function(){
 console.log('Hello World');
};
console.log(module);
// 打印结果如下
NPM_test node 2
Module {
 id: '.',
 exports: { name: 'Aphasia', sayHello: [Function] },
 parent: null,
 filename: '/Users/Desktop/NPM_test/2.js',
 loaded: false,
 children: [],
 paths:
 [ '/Users/Desktop/NPM_test/node_modules',
 '/Users/Desktop/node_modules',
 '/Users/node_modules','/node_modules' ] }

index.js 引入了 2.js 这个模块,当然自己本身也会存在 module 对象:

const test = require('./2.js');
console.log(module);
// 打印结果如下
Module {
 id: '.',
 exports: {},
 parent: null,
 filename: '/Users/Desktop/NPM_test/index.js',
 loaded: false,
 children:
 [ Module {
 id: '/Users/Desktop/NPM_test/2.js',
 exports: [Object],
parent: [Circular],
 filename: '/Users/Desktop/NPM_test/2.js',
 loaded: true,
 children: [],
 paths: [Array] } ],
 paths:
 [ '/Users/Desktop/NPM_test/node_modules',
 '/Users/Desktop/node_modules',
 '/User/node_modules','/node_modules' ] }

从上面的例子我们也能看到, module 对象具有一个 exports 属性,该属性就是用来对外暴露变量、方
法或整个模块的。当其他的文件 require 进来该模块的时候,实际上就是读取了该模块 module 对象
的 exports 属性。
exports 对象
为了让开发者使用起来更方便,Node.js 还提供了一个 exports 对象。它是一个指向的module.exports 的引用, module.exports 的初始值为一个空对象 {} ,所以 exports 的初始值也是
一个 {} 。
我们在 2.js 中打印这个 exports :

// 通过 module.exports 的方式导出属性
module.exports.name = 'Aphasia';
// 直接通过 exports 的方式导出方法
exports.sayHello = function(){
 console.log('Hello World');
};
console.log('module:',module);
console.log('exports:',exports);
// 打印结果如下:
module: Module {
 id: '.',
 exports: { name: 'Aphasia', sayHello: [Function] },
 parent: null,
 filename: '/Users/Jie/Desktop/NPM_test/2.js',
 loaded: false,
 children: [],
 paths:
 [ '/Users/Desktop/NPM_test/node_modules',
 '/Users/Desktop/node_modules',
 '/Users/node_modules','/node_modules' ] }
exports: { name: 'Aphasia', sayHello: [Function] }

可以看到该对象就是指向了 module 对象的 exports 属性。
虽然这 2 种方式都可以对外暴露变量、方法或整个模块的。但是其实两者还是有细微的区别。
module.exports 可以单独定义,返回数据类型,而exports 只能是返回一个 object 对象。
例如,我们在 2.js 中将 module.exports 单独定义成一个数组:

// 现在导出的整个模块不再是一个对象,而是一个数组
module.exports = ['zhangsan','lisi','wangwu'];

然后在 index.js 中引入该模块时,也就变成了数组,如下:

const test = require('./2.js');
console.log(test); // ['zhangsan','lisi','wangwu']

但是 exports 就不能单独定义,因为它只能返回一个 object 对象,例如我现在在 2.js 中将 exports
也单独定义成一个数组:

// exports 的指向已经被改变,已经切断了 exports 与 module.exports 的联系
exports = ['zhangsan','lisi','wangwu'];

然后在 index.js 中引入该模块时,因为始终引入的是 module.exports ,所以仍然为空,如下:

const test = require('./2.js');
console.log(test); // {}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码De搬运工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值