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

  • 使用模块的好处
  • 模块加载规则
  • 模块缓存
  • 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); // {}

对于模块化的一些见解相关推荐

  1. browserify使用手册

    简介 这篇文档用以说明如何使用browserify来构建模块化应用 browserify是一个编译工具,通过它可以在浏览器环境下像nodejs一样使用遵循commonjs规范的模块化编程. 你可以使用 ...

  2. 前端模块化工具--webpack学习心得

    话说前头 webpack前段时间有听说一下,现在已经到了3.x的版本,自己没去接触.因为之前使用gulp来作为自己的项目构建工具.现在感觉gulp使用的趋势在减少.现在这段时间去接触了webpack, ...

  3. 代码模块化和可读性的tradeoff

    本来想写一篇叫<由尝试Java synchronized锁所引发的一些关于代码结构的思考>的文章,涉及java的synchronized的机制本身(主要针对锁this和this.getCl ...

  4. 模块化建筑全球市场分析

    模块化市场分析 市场摘要 全球模块化建筑市场,预计从2019年的1199亿6千万美元,到2027年的1916亿2千万,以6.4%的年复合成长率成长.人口增加,快速城市化和基础设施发展投资增加是预测期内 ...

  5. 静息态下大脑的动态模块化指纹

    摘要:人脑是一个动态的模块化网络,可以分解为一系列模块,其活动随时间不断变化.静息状态下,在亚秒级的时间尺度上会出现几个脑网络,即静息态网络(RSNs),并进行交互通信.本文尝试探究自发脑模块化的快速 ...

  6. 软件架构师 第一部分 基础篇 第二章 模块化

    首先,我们想弄清在围绕模块化的架构的讨论中使用和经常使用的一些通用术语,并提供在本书中使用的定义. [关于软件架构]的词语中有95%用于赞扬"模块化"的好处,而关于如何实现&quo ...

  7. 声控助手_我从构建声控机器人获得的见解

    声控助手 by Mithi 由Mithi 我从构建声控机器人获得的见解 (Insights I gained from building a voice-activated robot) For al ...

  8. 将超过5000万行JS代码迁移到TypeScript,我们得到的10大见解

    Python实战社群 Java实战社群 长按识别下方二维码,按需求添加 扫码关注添加客服 进Python社群▲ 扫码关注添加客服 进Java社群▲ 作者丨Rob Palmer 译者丨王强 策划丨蔡芳芳 ...

  9. 微信小程序 运营的特性—模块化

    早前读了一篇有关微信小程序能力特性相关的文章,突发了一点自己对小程序的见解.小程序越来越火,但是不少用户又因为部分小程序功能太单一而望而却步,大放异彩的都是一些功能突出的工具类应用和互动社交类应用.反 ...

最新文章

  1. Android 金钱计算BigDecimal 的使用
  2. ASP.NET服务器端控件原理分析
  3. java EF6,EF Core 2.0和EF6(Entity Framework 6)中配置实体映射关系
  4. linux 下安装部署mq,RocketMQ在linux下安装部署
  5. 原生Java高仿抖音短视频APP双端源码
  6. SpringBoot写后端接口,看这一篇就够了!
  7. php生成饼状图 柱形图,求一个饼状图或柱状图php生成类或例子
  8. 广搜最短路径变形,(POJ3414)
  9. C++数字与字符串的相互转换
  10. CSS:实现跳动小球蒙版效果
  11. 拒绝卡顿——在WPF中使用多线程更新UI
  12. 中国互联网今日正式满 20 岁
  13. 凸优化学习笔记(三):凸优化问题
  14. echarts地图map下钻到镇街、KMZ文件转GeoJson、合成自定义区域
  15. 基于FPGA的GV7600驱动控制器设计,按照BT1120协议传输YCbCr数据
  16. VLAN介绍、工作原理以及配置
  17. Anki 批量编辑替换插件
  18. 图灵——如迷的解谜者
  19. QQ再次被大规模盗号
  20. Deepin+win7双系统启动项问题解决

热门文章

  1. 欧盟商标注册后可以转让吗?如何转让,转让流程是什么?
  2. iOS13升级后的第一感觉:旧版iPhone重生,并向您提供了20个隐藏功能!
  3. 几分钟教你批量重命名文件,批量更改数据的方法
  4. linux音频服务器,在Ubuntu Linux上配置MPD音乐服务器
  5. 【最经典的79个】软件测试面试题(内含答案)提前备战“金九银十”
  6. 007--python--英制单位英寸和公制单位厘米的互换
  7. 南非、马来西亚和印度尼西亚比欧洲对数字货币的熟悉程度更高
  8. 干掉“我的电脑”中超级解霸V8的图标
  9. 高频丙类谐振功率放大器【Multisim】【高频电子线路】
  10. 【ESP32】16.RFID门禁系统实验(SPI总线 / MFRC522库)