我们知道,Node.js 选用 JavaScript 语言来编写代码。JavaScript 这门语言呢,之前主要用于前端应用,并没有相应的模块管理功能,而是以 script 标签为单位,直接引入即可运行。Node.js 主要运行在后端,这怎么办呢?好在它借鉴了 CommonJS 中的 Modules 规范,实现了一套易用的模块系统。今天,我们就来介绍一下 Node.js 中的模块。

举个栗子

下面我们写一段代码,封装一个模块,这个模块包含一个方法,对指定的人返回一句问候语:

// greeting.js

// 问候一声
exports.greet = (name) => {
return `hello ${name}`;
};

看上去挺简单的,直接在 exports 上面定义一个 greet 方法,即可将该方法暴露出去。

然后,我们在主文件中引入该模块,并调用它的 greet 方法:

// main.js

const greeting = require('./greeting');

console.log(greeting.greet('Scott'));

在引入该模块时,我们使用了相对路径 ./greeting,Node.js 运行时会根据这个路径,找到我们自定义模块文件,然后编译执行。

最后,在命令行中运行 main.js,程序会输出运行结果:

$ node main.js
hello Scott

一探究竟

下面,我们就来分析一下 Node.js 中的模块系统。

Node.js 中的模块分为 内置模块自定义模块 两大类,而引用一个模块时一般会经历 路径分析文件定位编译执行 三个步骤。

需要注意的是,如果我们引入内置模块,只需要 路径分析 这一个步骤,这是因为,内置模块在 Node.js 进程启动时,就已经被加载进内存了,可以直接引用,并且优先进行路径分析,所以不再需要 文件定位编译执行 了。

引入内置模块时,只需指定模块名即可:

const http = require('http');

如果是引入自定义模块,则包含以下几种形式:

  • 使用 ... 起始的相对路径
  • 使用 / 起始的绝对路径
  • 使用第三方模块名

如果使用了 相对路径绝对路径require(module) 会根据模块路径读取相应的文件,然后编译执行,并将结果放入缓存,下次引入时直接从缓存中取结果。

如果我们引入一个第三方模块,例如 connect 模块,这时候,我们的引入方式和内置核心模块是类似的:

const connect = require('connect');

首先,Node.js 会先按内置模块查找,但发现它并不在内置模块名单中,所以接下来,要在 模块路径 中查找该模块对应的包。

这里提到了一个概念:模块路径。它是 Node.js 定位文件模块的一种查找策略,表现形式是包含多个路径的一个数组。

为了验证这个数据结构,我们在代码中添加下面一行:

console.log(module.paths);

运行代码后,我们会得到下面输出内容:

[ '/Users/Scott/learning/node_modules',
'/Users/Scott/node_modules',
'/Users/node_modules',
'/node_modules' ]

可以看到,模块路径列举了这些目录:当前目录下的 node_modules 目录、父级目录下的 node_modules 目录、祖先目录下的 node_modules 目录、根目录下的 node_modules 目录。

在加载过程中,Node.js 会按照这个路径数组,逐个进行尝试,直到找到目标模块文件或包为止。

这里需要提醒一下,在使用 require(module) 时,如果参数不包含后缀名,Node.js 会按照 .js.node.json 次序补足后缀名,然后逐个尝试以单线程同步形式加载,所以,对于非 .js 后缀名的文件,引入时最好加上后缀名,以提高加载的速度。

回到我们最开始的一个小例子,这个程序中的 greeting.js 模块,通过 exports.greet 的形式导出一个方法,为什么能这样写呢,exports 是从哪里来的呢?

原来,Node.js 在加载一个模块时,会首先对它进行编译,在这个过程中,进行了一次头尾的包装,我们上面例子中的模块,经过包装之后是这个样子的:

(function (exports, require, module, __filename, __dirname) {
exports.greet = (name) => {
return `hello ${name}`;
};
});

前面我们直接拿来用的 exportsrequire 以及 module,原来是这么来的,还有 __filename__dirname,它们并不是全局变量,而是由包装函数传递进来的,表示当前模块文件的路径和目录。

经过上面的包装,我们的每个模块都有了自己的作用域,包装之后的代码会由 vm 原生模块的 runInThisContext() 方法执行,同时将包装函数所需的参数传递进去。

默认情况下,exportsmodule.exports 都指向一个空对象引用,即:

module.exports = exports = {};

所以上面我们给 exports 添加了一个方法,同时也会改变 module.exports

当然,我们也可以使用下面这种方式,改变导出的引用对象:

module.exports = {
greet: (name) => {
return `hello ${name}`;
}
};

但注意,不要在模块内使用下面这种方式:

// 无效的导出方式
exports = {
greet: (name) => {
return `hello ${name}`;
}
};

原因在于它更改的只是形参的引用,却对实参引用没有做任何更改,所以是无效的导出方式。

Node: 模块的更多相关文章

  1. node模块加载层级优化

    模块加载痛点 大家也或多或少的了解node模块的加载机制,最为粗浅的表述就是依次从当前目录向上级查询node_modules目录,若发现依赖则加载.但是随着应用规模的加大,目录层级越来越深,若是在某个 ...

  2. node模块系统常用命令

    node模块系统常用命令 命令 示例 备注 安装模块 npm install commander 最新版本 npm install commander@1.0.0 指定版本 npm install c ...

  3. Commonjs规范及Node模块实现

    前面的话 Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性.本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于java ...

  4. 模块机制 之commonJs、node模块 、AMD、CMD

    在其他高级语言中,都有模块中这个概念,比如java的类文件,PHP有include何require机制,JS一开始就没有模块这个概念,起初,js通过<script>标签引入代码的方式显得杂 ...

  5. NW.js安装原生node模块node-printer控制打印机

    1.安装原生node模块 #全局安装nw-gyp npm install -g nw-gyp #设置目标NW.js版本 set npm_config_target=0.31.4 #设置构建架构,ia3 ...

  6. 深入了解Node模块原理

    深入了解Node模块原理 当我们编写JavaScript代码时,我们可以申明全局变量: var s = 'global'; 在浏览器中,大量使用全局变量可不好.如果你在a.js中使用了全局变量s,那么 ...

  7. 【转】Commonjs规范及Node模块实现

    前言: Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性.本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于javas ...

  8. [转]模块化——Common规范及Node模块实现

    Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性.本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于javascrip ...

  9. node 模块正确暴露方法

    一个node模块,为了能够服用,就需要将其暴露,那么如何正确写呢?(参考:https://developer.mozilla.org/zh-CN/docs/Learn/Server-side/Expr ...

  10. Electron结合React,在渲染进程中使用 node 模块

    Electron结合React,在渲染进程中使用 node 模块 问题 将create-react-app与electron集成在了一个项目中.但是在React中无法使用electron.当在Reac ...

随机推荐

  1. 深入解读阿里云Redis开发规范

    Key命名设计:可读性.可管理性.简介性 规范建议使用冒号即:进行分割拼接,因为很多Redis客户端是根据冒号分类的.比如有几个Key:apps:app:1.apps:app:2和apps:app:3 ...

  2. cent os 部署blade

    一:安装docker 二:安装docker-compose 三:安装Harbor 四:配置Harbor使用https和2376端口 在/etc/docker 目录下面创建文件:create_tls_c ...

  3. php异常处理小总结

    2019年8月23日10:56:31 php很多开发不习惯使用异常处理,因为web开发,重在于快速开发,易用性,高性能,不强调程序健壮性 php的异常使用其实不是太完善,易用性也差点,当然这个对比其他 ...

  4. c# .net framework 4.5.2 , Quartz.NET 3.0.7

    参考了:https://www.cnblogs.com/personblog/p/11277527.html, https://www.jianshu.com/p/b8e7e4deb60a .NET ...

  5. Kubernetes 使用 Weave Scope 监控集群(十七)

    目录 一.安装 二.使用 Scope 2.1.拓扑结构 2.2.在线操作 2.3.强大的搜索功能 创建 Kubernetes 集群并部署容器化应用只是第一步.一旦集群运行起来,我们需要确保一起正常,所 ...

  6. 自定义注解实现简单的orm映射框架

    package com.mj; import javax.xml.bind.Element; import java.lang.annotation.*; import java.lang.refle ...

  7. Vue(七)整合vue-router&Vuex&Axios

    整合vue-router&Vuex 先创建工程 vue create vue-axios 然后选择 勾选 回车,出现是否使用history mode?选择y,代表URL地址里面不会出现#.选择 ...

  8. mac下php配置

    打开/关闭服务 sudo apachectl start sudo apachectl stop 查看apche版本 apacectl -v 修改项目默认路径: 打开配置文件目录/private/et ...

  9. SQLite数据库简介和使用

    一.Sqlite简介: SQLite (http://www.sqlite.org/),是一款轻型的数据库,是遵守ACID的关联式数据库管理系统,它的设计目标是嵌入式的,而且目前已经在很多嵌入式产品中 ...

  10. delphi10.2断点调试dll

    因为工作需要接触delphi10.2,需要调试dll,但是从网上查找的资料写的不是很清楚,我折腾了半天,我就动手写清楚操作步骤: 步骤1:用delphi10.2打开需要调试的dll,需要先打开,然后需 ...