Node.js 系列 - 模块机制

作为还在漫漫前端学习路上的一位自学者。我以学习分享的方式来整理自己对于知识的理解,同时也希望能够给大家作为一份参考。希望能够和大家共同进步,如有任何纰漏的话,希望大家多多指正。感谢万分!


什么是模块?

以编程角度来说, "模块" 指的是能够提供一定功能或数据的程序语句集合. 模块具备和外部联系的接口 (其他模块或程序调用该模块的方式)

在 Node.js 中, 每个文件就被视为一个模块. 这个文件可能是 JavaScript 编写的文件、JSON 或者用 C/C++ 编译的二进制文件. 通过对外接口来向外部暴露功能或者数据, 模块之间可以互相调用.

为什么要用模块?

随着开发复杂度的提升, 将代码都写在一处的传统开发方式, 显现出了很多问题:

  • 很容易出现代码重复. 开发人员很容易将一个功能的代码重复地写了好多遍. 这导致了如果日后功能需求出现了变更, 就要有多处代码需要被更改. 随着应用规模的增大, 代码会变得难以维护.
  • 难以确保代码质量. 所有代码都混在一起, 实现不同功能的代码全都被写在一个文件中, 使得对于单个功能的独立测试变得困难.
  • 难以查错. 所有代码都混在一起, 程序运行出现 BUG 了, 很难快速定位.
  • 性能浪费. 因为代码都写在一个文件中, 在只调用文件中一段代码的时候, 也会导致整个文件都加载一遍. 这会使很多根本用不到的代码对性能造成浪费.
  • 难以多人写协作. 所有代码都放在一个文件中, 使得多人协作变得困难. 开发人员难以确认其他人做了什么修改, 添加了什么东西. 很容易一个人出了错误, 导致整个程序崩溃.
  • 等等...

模块化开发

通过使用模块机制, 我们可以把一个复杂程序的各个功能拆分, 分别封装到不同的模块. 每个模块职责单一 (各管一件事, 之间没交集) 通过开发新模块, 和对已有模块的复用来实现各种功能. 这种开发方式被称为 "模块化开发".

应用模块化开发, 使得各个功能都封装在独立的文件中, 分而治之, 互不干扰. 使得代码易于维护和复用. 同时每个模块中的变量也不会污染全局作用域, 避免了命名冲突.

CommonJS

Node.js 参照 CommonJS 标准实现了模块机制. CommonJS 是一套代码规范, 目的是为了构建 JavaScript 在浏览器之外的生态系统 (服务器端, 桌面端). JavaScript 诞生之初只是为了写网页小脚本, 并不作为开发大型复杂应用的语言, 其自身有很多不足. 并且, 官方规范 (ECMAScript) 制定的时间较早, 涵盖范围较小, 对于后端开发而言, 例如文件系统, I/O 流, 模块系统, 等等方面都没有相应的标准. 基于种种的不足, CommonJS 规范致力于弥补 JavaScript 没有标准的缺陷, 让 JavaScript 有能力去开发复杂应用, 同时具备跨平台能力.

下面是一个 Node.js 的模块使用示例:

在代码中, 开头通过 require 方法引入了 Node.js 自带的 http 模块. 并用此模块实现了一个 HTTP 服务器.

const http = require('http');

function myNodeServer(req, res){
res.writeHead(200, {'Content-type':'text/plain'});
res.write('Hello World'); 
res.end();
}

http.createServer(myNodeServer).listen(3000); //监听 3000 端口

console.log('Server is running!'); 
复制代码

模块分类

前文说, 在 Node.js 中, 每个文件就被视为一个模块. 这个文件可能是 JavaScript 编写的文件、JSON 或者用 C/C++ 编译的二进制文件.

模块可以分成三类:

  • 核心模块 』: Node.js 自带的原生模块. 比如, http, fs, url. 其分为 C/C++ 编写的和 JavaScript 编写的两部分. C/C++ 模块存放在 Node.js 源代码目录的 src/ 目录下. JavaScript 模块存放在 lib/ 目录下.
  • 文件模块 』: 开发人员在本地写的模块. 加载时通过相对路径, 绝对路径来定位模块所在位置.
  • 第三方模块 』: 别人编写的模块, 通过包管理工具, 比如 npm, yarn, 可以将其从网络上引入到本地项目, 供己使用.

NPM 包管理器

NPM 是随同 Node.js 一起安装的 "包管理工具". 通过它, 全世界开发者们可以简单方便地互相分享和借鉴各自的 Node.js 模块. 其让整个 Node.js 社区生态变得繁荣热闹.

NPM 常见的使用场景有以下几种:

  • 允许用户从 NPM 服务器下载别人编写的第三方模块到本地使用。
  • 允许用户从 NPM 服务器下载并安装别人编写的命令行程序到本地使用。
  • 允许用户将自己编写的模块或命令行程序上传到NPM服务器供别人使用。

具体的使用方法网上有很多教程, 这里就不赘述了. 不想自行查阅的话, 可以直接参考下面的链接:

npm 官方文档

npm 使用介绍 - 菜鸟教程


使用模块

在了解了什么是模块之后, 让我们来看看如何在 Node.js 中实际应用模块机制. 在使用上, 可以很简单的分为三个步骤: 创建, 导出, 引入. 先创建一个模块, 然后导出功能或数据, 模块之间可以互相引入导出的内容.

Node.js 提供了 exportsrequire 两个对象,其中 exports 用于导出模块, require 用于从外部引入另一个模块, 即获取模块的 exports 对象.

创建 & 导出模块

先让我们来看看如何创建并把模块的内容导出. 在 Node.js 中, 一个文件就是一个模块. 创建模块的方法就是创建一个文件.

通过 exports 对象来指定一个模块的导出内容.

示例:

// 文件名: nameModule.js
var name = 'Garrik';

exports.setName = function(newName) {
name = newName;
}

exports.getName = function() {
return name;
}
复制代码

在以上示例中, nameModule.js 文件通过 exports 对象将 setNamegetName 作为模块的访问接口. 其他的模块可以引入导出的 exports 对象, 直接访问 exports 对象的成员函数.

引入模块

在 Node.js 中, 通过 require 函数来引入外界模块导出的内容. require 函数接受一个字符串作为路径参数, 函数根据这个字符串参数来进行模块查找. 找到后会返回目标模块导出的 exports 对象.

示例:

// 文件名: showNameModule.js
var nameModule = require('./nameModule.js');

console.log(nameModule.getName()); 
// 显示: Garrik

nameModule.setName('Xiang');

console.log(nameModule.getName());
// 显示: Xiang
复制代码

上面示例中, 通过 require 引入了当前目录下 nameModule.js 导出的 exports 对象, 并让一个本地变量指向引入模块的 exports 对象. 之后在 showNameModule.js 文件中就可以使用 getNamesetName 这两个方法了.

module.exports 和 exports 的区别

在使用 exports 对象导出内容时, 所有作为对外访问接口的属性和方法都是定义在 exports 属性上的. 上面的例子中 setNamegetName 方法都直接定义在 exports 对象上. 那如果想直接导出一个对象, 或者基础类型值可不可以呢?

可能有人会想可不可以这样写:

var name = 'Garrik';

exports = name;
复制代码

如果你试一下的话会发现, 最后引入的是一个空对象, 而不是你定义在 exports 上的东西.

在使用 exports 的时候只能往这个对象里添加新的属性和方法, 而不能对其直接赋值. 如果想直接导出一个对象, 或者基础类型值要使用 module.exports 对象. 例如上面例子就可以改写成:

// 文件名: nameModule.js
var name = 'Garrik';

module.exports = {
    setName: function(newName) {
        name = newName;
    }, 
    getName: function() {
        return name;
    }
} 
复制代码

这样写的话, 就导出了一整个对象, setNamegetName 方法是这个对象的成员函数. 而不是之前的 exports 对象了.

除此之外 module.exports 还可以直接导出基础类型值:

// 文件名: numMoule.js

var num = 123456;

module.exports = num;
复制代码
// 文件名: showNum.js
var getNum = require('./numModule.js'); // showNum.js 和 numModule.js 在同一目录下

console.log(getNum); // 结果: 123456
复制代码

这种方式下, 导出的就直接是基础类型的值.

可能还是很多人在疑惑 exportsmodule.exports 区别和关系.

上面我说, 一个文件被另一个模块引入时, 会被做一些处理. 文件中代码并不被 Node 执行, 而是被打包进一个函数中, 然后 Node 执行这个函数. 打包函数会被传入 exportsrequiremodule__filename__dirname 这五个参数. 所有的这些参数都在 Node.js 执行函数时赋值, 并且只在当前的函数作用域中有效. 打包函数执行到最后, 返回 module.exports 对象.

其中, exportsmodule.exports 的引用, module 对象代表被打包进去的代码本身. moduleexports 对象用于指定一个模块的导出内容.

在模块中定义外部可访问接口的时候, 有两个方法:

exports.name = 'Garrik';
复制代码
module.exports = {name: 'Garrik'};
复制代码

在使用 exports 的时候只能往这个对象里添加新的属性和方法, 而不能对其直接赋值. 因为直接赋值会打破其对 module.exports 的引用.

// 这是可以的:
exports.name = 'Garrik';
exports.gender = 'Male';

// 这是不可以的:
exports = {name: 'Garrik', gender: 'Male'};

// 应该用 module.exports:
module.exports = {name: 'Garrik', gender: 'Male'}
复制代码

如果想直接导出一个对象, 或基本类型值, 应该使用 module.exports.

// 导出函数
module.exports = function(num) {
return num + 1;
};

// 导出基本类型值
module.exports = 123;
复制代码

require 的路径参数

在用 require 引入模块时, 路径参数可能有下面三种形式:

  • 相对路径: ./ 开头 或 ../ 开头
  • 绝对路径: / 开头
  • 模块名 (例如: http, fs, url)

根据参数不同, 加载方式也有区别.

绝对路径, 或相对路径

在指定了模块路径的情况下, Node.js 会去指定的位置加载模块. 但因为用 require 来加载模块时可以省略文件后缀, 在省略的情况下, Node.js 会去猜测文件的类型.

比方说我要去 ./modules/ 目录下加载一个 haha 模块.

var haha = require('./modules/haha');
复制代码

因为 haha 没写文件后缀, Node.js 将执行的操作顺序为:

  • 按 js 文件来执行(先找对应路径当中是否有 haha.js 文件, 有就加载)
  • 按 json 文件来解析(若上面的 js 文件找不到时,则找对应路径当中的 haha.json 文件来加载)
  • 按照预编译好的 C++ 模块来执行(还没有, 寻找对应路径当中的 haha.node 文件来加载)
  • 若参数字符串为一个目录的路径, 就是说 haha 为一个目录, 则先查找该文件夹下的 package.json 文件,然后再加载该文件当中 main 字段所指定的入口文件. 若 package.json 文件当中没有 main 字段,或者根本没有 package.json 文件,则再默认查找该文件夹下的 index.js 文件, 并作为模块来载入.
  • 要是还没有就拉倒吧!

无路径, 直接模块名:

在没有路径, 参数值直接为一个模块名的情况下:

var haha = require('haha');
复制代码
  • 如果 haha 是 Node.js 核心模块就直接加载.
  • 如果是第三方模块, 则依次从当前目录中的 node_modules 目录, 父级目录中的 node_modules 目录, 一直到根目录下的 node_modules 目录下去查找 haha 的所在. 若有两个同名文件,则遵循就近原则。优先引入目录顺序靠前的模块.
  • 如果找到的 haha 为一个目录, 则先查找该文件夹下的 package.json 文件,然后再加载该文件当中 main 字段所指定的入口文件. 若 package.json 文件当中没有 main 字段,或者根本没有 package.json 文件,则再默认查找该文件夹下的 index.js 文件, 并作为模块来载入.
  • 要是还没有就拉倒吧!

? 好啦,今天的分享就告一段落啦。下一篇中,我会介绍如何实现一个 "Hello World" HTTP 服务器。

传送门: Node.js 系列 - 搭建 "Hello World" HTTP 服务器

如果喜欢的话就点个关注吧!O(∩_∩)O 谢谢各位的支持❗️

你可能感兴趣的:(Node.js 系列 - 模块机制)