前言
这篇是 node.js 模块化开发基础的学习笔记。内容主要包括 node.js 模块化开发的简要介绍、如何进行模块化开发、常用原生模块的使用、常用的第三方模块的使用(使用时会遇到的问题)、与模块相关的node_modules 文件夹、 package.json文件、package-lock.json 文件的介绍和模块加载的机制。
1. Node.js 中模块化开发
实现模块化开发可以解决文件的依赖和变量命名冲突的问题。在复杂程序开发时将不同的功能模块抽离出来,一个功能就是一个模块,多个模块最后可以组成完整的应用。这样在抽离一个模块或者其中一个模块出错时能减少或者避免对其他功能模块造成影响。Node.js 模块化实现标准是 CommomJS。
1、Node.js 模块化开发规范
- Node.js 规定一个 js 文件就是一个模块,模块内部定义的变量和函数默认在外部无法访问。
- 模块内部可以使用 module.exports 对象进行模块成员导出,使用 require 方法导入其他模块。
- module.exports 对象默认是空对象(默认没有成员导出)。
// amodule.js 模块(文件)内容
console.log('amoduleStart');
const add = (...agrs) => {
let sum = 0;
agrs.forEach(item => {
sum += item;
});
return sum;
};
console.log('amoduleEnd');
module.exports.add = add; // 成员导出
// bmodule.js 文件(模块)的内容
const a = require('./amodule'); // 引入amodule模块
console.log(a.add(23, 80, 1));
// 执行 bmodule.js 文件(模块)
//输出 amoduleStart\n amoduleEnd\n 104
2、require() 函数总结
- require 会获得(引入)参数模块中的 module.exports 对象,然后通过这个对象我们就可以在本模块中使用另一个模块通过该对象导出的元素。
- 在 require 首次导入某个模块的过程中,会解释执行该模块中的代码,并将模块文件进行缓存。
- 当再次引入该模块时,会通过缓存读取导出的内容,模块内的代码不会重新执行。
// 在 bmodule.js 文件(模块)的后面再次引入amodule模块,结果不会改变
const a = require('./amodule'); // 引入amodule模块
console.log(a.add(23, 80, 1));
const aModule = require('./amodule');
3、另一个与模块成员导出有关的对象 exports
exports 变量是在文件级作用域内可用的,且在模块执行之前赋值为 module.exports 。即 exports 并不是一个全局变量,只是在模块文件内有效。并且在每个模块文件(js 文件)执行前将 module.exports 的值赋值给 exports 。所以 exports 默认与 module.exports 指向同一片内存空间,当为 exports 或 module.exports 重新赋值后,它们将不再指向同一个引用。但是有一点非常重要,模块导出的对象一直都是 module.exports 对象,而模块导入时导入的也是该对象。
// 每个 js 文件的开头都会执行如下操作
exports = module.exports;
// .....
// 因此在对 exports 重新赋值时,它们不在指向同一个引用
// amodule.js 模块(文件)内容
console.log('amoduleStart');
const add = (...agrs) => {
// ...
};
console.log('amoduleEnd');
exports.add = add; // 成员正常导出
exports = {add: '我已不是曾经的我'};
// 后面再重新赋值对 module.exports 没有影响
// bmodule.js 文件(模块)的内容
const a = require('./amodule'); // 引入amodule模块
console.log(a.add(23, 80, 1));
// 执行 bmodule.js 文件(模块)
//输出 amoduleStart\n amoduleEnd\n 104
4、node.js 中的原生模块和文件模块
node.js 中的模块可以根据存在的形式分为两种:
- 原生模块(又称系统模块或核心模块,由 node.js 执行环境提供的模块)
- 文件模块 (开发者编写的模块)
原生模块:Node 运行环境提供的 API 。因为这些 API 都是以模块化的方式进行开发的,所以称这些 API 为原生模块。又称核心模块或者系统模块。原生模块在 Node 源代码编译过程中就已经编译成了二进制可执行文件。在 Node 进程启动时原生模块就已经被加载进内存中。所以在引入原生模块过程中,没有文件定位和编译执行这两个步骤。
文件模块:由开发者根据需要编写的模块(第三方模块),引入时需要完整的路径分析、文件定位、编译执行过程。
2. 系统模块
常用的系统模块有以下几个:fs 、path 、http 、 url 等模块。当然现阶段本人的学习目标只是会实现简单的前后端交互,所以不会深入地学习它们。以后有用到其他东西再补上,暂时先记下这些。
2.1 fs 模块
fs 模块是 Node.js 文件系统模块,其提供的方法与文件操作有关。例如 fs 模块提供了读取文件的方法 fs.readFile() 和写入文件的方法 fs.writeFile() 。
Node.js 文件系统(fs 模块)模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。
异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。
建议大家使用异步方法,比起同步,异步方法性能更高,速度更快,而且没有阻塞。
2.1.1 读取文件
读取文件常见需求是在我们接受浏览器客户端的请求时,需要读取请求的页面的 html 文件并返回文件内容给客户端。读取文件内容:
fs.readFile('文件路径/文件名称'[, '文件编码方式'],callback);
- 文件路径可以使用绝对路径或者相对路径。
- 一般需要标明文件的编码方式,否则读取内容将返回一个表示编码字符的序列Buffer 实例。
- callback 为回调函数,fs.readFile() 方法会自动给其传递两个参数,第一个是错误信息,第二个是读取的文件内容。
// test.text 文件
test.text Start
待操作文件的内容
test.text End
// fileOperate.js 文件
const fs = require('fs');
fs.readFile('test.text', 'utf8', (err, doc) => {
if (err) {
console.log(err);
} else {
console.log(doc);
}
});
// 运行fileOperate.js 文件 输出如下
test.text Start
待操作文件的内容
test.text End
// 将读取文件的文件名更改(搞出一个错误),则会输出错误信息
2.1.2 写入文件
写入文件的需求常见的有程序运行时可能会发生一些意想不到的异常或者错误。因此我们需要对程序的运行进行监控,在发生错误时将错误信息写入日志,方便开发人员维护。将某些内容写入文件:
fs.writeFile('文件路径/文件名称', '写入的内容', callback);
此时回调函数只会自动传递一个错误信息参数。但是还需要注意一点就是,写入的内容会覆盖原先的内容,这跟默认打开文件的方式有关。
// test.text 文件
test.text Start
待操作文件的内容
test.text End
// fileOperate.js 文件
let writeContent = `write Start
写入的内容...
write End`;
fs.writeFile('test.text', writeContent, err => {
if (err) {
console.log(err);
return;
}
console.log('文件写入成功');
});
// 运行fileOperate.js 文件后test.text 文件的内容
write Start
写入的内容...
write End
如果在写入文件代码的前面编写上面那个读取文件的代码,你会发现虽然读取文件程序写在前面,但是读取的内容会是写入的内容,而不是原来的内容。这跟读写文件的实现机制有关。
2.2 path 模块
Node.js path 模块提供了一些用于处理文件路径的小工具。这里只学习使用 path 模块进行路径拼接。因为在不同的系统中使用的路径拼接符是不一样的,有些使用 “/” 有些则使用 “\”。为保证路径的正确性,path.join() 方法可以根据系统使用的拼接符对路径进行拼接。
const path = require('path');
let finialPath = path.join('F', 'tkop', 'demo');
console.log(finialPath); // -> F\tkop\demo 在我的window系统下的输出
在需要使用绝对路径时可能会遇到一些问题?比如我们将用户的头像图像保存在一个文件夹中。现在需要在某个文件内得到该头像的绝对路径,假设我们只知道相对路径。那么我们可以使用全局变量 __dirname 拼接相对路径得到绝对路径。
2.3 http 模块
Node.js 提供了 http 模块,http 模块主要用于搭建 HTTP 服务端和客户端,使用 HTTP 服务器或客户端功能必须调用 http 模块。创建代码的示例如下节所示。
2.4 url 模块
url 模块主要是用于处理请求路径相关的需求。例如:将 url 路径字符串解析为对象形式数据。
const url = require('url');
const http = require('http');
const app = http.createServer();
app.on('request', (req, res) => {
let {pathname, query} = url.parse(req.url);
res.end('ok');
})
let {pathname, query} = url.parse(request.url);
3. 第三方模块
第三方模块是指别人写好的、具有特定功能的、我们下载后能够直接是用的模块(类似于插件)。由于第三方模块通常都是由多个文件组成并且被放在一个文件夹中,所以又叫为包(package)。
第三方模块根据用途又可以分为两种:一种是提供实现项目具体功能的 API 接口,以模块文件的形式使用。另一种用于辅助项目开发,以命令行工具的形式使用。
3.1 获取第三方模块
第三方模块是由全球的开发者开发并分享的,它们会被保存在 npmjs.com 这个网站上(第三方模块的存储和分发仓库)。
当我们需要使用某些第三方模块时,需要使用 npm(node package manager ,node 第三方模块管理工具) 进行下载。npm 其实也是 node 的一个模块,它在我们安装 node 运行环境时已经自动帮我们下载下来了。当然它用于辅助项目开发,以命令行工具的形式使用。基本用法如下:
- 下载:npm install 模块名称 [-g]
- 卸载:npm uninstall package 模块名称 [-g]
后面是否加上 ‘’-g’’ 表示是否全局安装,命令行工具一般需要全局安装。如果不加 ‘’-g’’ ,则表示本地安装,库文件一般只需要本地安装。
3.2 nodemon 模块
nodemon 是一个命令行工具,用于辅助项目开发。它主要是为了解决在 Node.js 中,每次修改文件后为查看结果都需要在命令行重新执行该文件的问题。使用 nodemon 命令代替 node 命令执行某个文件后,命令行挂起并会一直监控该文件的保存操作。每保存文件都会自动执行一次文件。
- 使用 npn install nodemon -g 全局安装 nodemon 模块。
- 使用 nodemon 命令代替 node 打开某个 js 文件。
- 不再需要监控打开文件的保存操作时使用 ctrl + c 快捷键退出即可。
下载 nodemon 使用的过程中会遇到一个问题:我们的系统默认禁止自动运行脚本,所以无法使用 nodemon 模块。解决方法:
- 以管理员的身份打开命令行去更改系统执行策略。
- 执行更改命令 set-ExecutionPolicy RemoteSigned 后输入 Y 回车表示同意更改。
- 可以使用 get-ExecutionPolicy 命令查看系统执行策略。
- 再执行 Set-ExecutionPolicy RemoteSigned -Scope Process 后输入 Y回车即可。
系统脚本执行策略:
- Restricted,默认设置,不允许运行任何脚本。
- AllSigned,仅运行受信任脚本。
- RemoteSigned,运行本地脚本,不管这些脚本是否受信任;如果是从 Internet 下载的脚本,则必须是受信任的脚本才能够运行。
- Unrestricted,允许运行所有脚本,甚至是不受信任的脚本。
3.3 nrm 模块
npm 库管理工具默认是从国外的第三方模块存储和分发仓库(下载地址在国外)下载安装第三方模块,下载速度较慢。但是国内某些企业也提供了第三方方模块存储和分发仓库(每大概过十分钟就会和国外的进行同步),所以我们可以使用国内的下载地址来提高下载速度。
nrm(npm registry manager,npm 下载地址切换工具)是可以切换包下载地址的一个命令行工具模块。使用步骤如下:
- 使用 npm install nrm -g 全局下载。
- 使用 nrm ls 查询可使用的下载地址。
- 使用 nrm use 下载地址名称 切换 npm 下载使用的地址。
下载完成后使用 nrm ls 命令会出现如下报错
解决方案是找到对应的文件将17行代码注释掉改为如下代码
// const NRMRC = path.join(process.env.HOME, '.nrmrc');
const NRMRC = path.join(process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'], '.nrmrc');
再查看可使用的下载地址结果如下(前面带星号的地址为当前使用的地址),并将下载地址换为国内下载地址。之后使用 npm 下载第三方模块默认的下载地址则改为了该地址。
3.4 gulp 模块
gulp 模块是基于 node 平台开发的一个自动化前端构建工具,主要用来设定程序自动处理静态资源的工作。简单的说,gulp就是用来打包项目的。前端的构建工具常见的有 Grunt、Gulp、Webpack 三种,Grunt比较老旧,功能少,更新少,插件少。这里我们首先了解一下 Gulp(没有详细学习) 。想学个透彻可以去看官方文档。
3.4.1 Gulp 能做什么
什么是前端构建工具呢?例如当一个项目线下开发完成,在部署上线前为了提高网站访问的速度,通常需要将 HTML、CSS、JS 文件进行合并压缩操作。但这些繁琐、没有技术要求且浪费时间的操作如果让开发者去手动完成,显然不太合理。因此诞生了前端构建工具,它可以将机械化的操作编写成任务,需要执行这些操作时,只需要执行与之对应的命令行命令即可让系统自动执行该操作。
- 项目上线前,HTML、CSS、JS 文件的压缩合并。
- 语法转换(ES6、Less …)
- 公共文件抽离
- 修改文件浏览器自动刷新。
3.4.2 Gulp 常用插件
gulp 是一个轻内核第三方模块,只提供了少量的 API 。其他完成特定功能的 API 以插件的形式实现。常用的插件如下:
- gulp-htmlmin : html 文件压缩
- gulp-csso : 压缩 css 文件
- gulp-babel : JavaScript 语法转化
- gulp-less : less 语法转化
- gulp-uglify :压缩混淆 JavaScript
- gulp-file-include :公共文件包含
- browsersync :浏览器实时同步
下载插件并引入后即可使用里面提供的 API 。每个插件实现特定的功能,要了解它们具体的使用可以到官网进行下载和参考使用语法。
例如使用 npm install gulp-csso gulp-uglify --save-dev 命令同时下载 gulp-csso 和 gulp-uglify 插件。然后将它们引入需要使用的文件即可使用它们提供的 API 。
3.4.3 Gulp 的使用
1、使用前的准备工作
- 使用 npm install --save-dev gulp 下载 gulp 库文件。
- 在项目根目录下创建 gulpfile.js 文件(文件名不能随意改)。
- 重构项目的文件夹结构: src 目录放置源代码文件,dist 目录放置重构后的文件。
- 在 gulpfile.js 文件中使用 gulp 模块提供的 API 编写任务。
- 在命令行中执行 gulp 任务。
2、Gulp 中提供的几种常用方法
- gulp.src() :获取任务要处理的文件。
- gulp.dest() :输出文件。
- gulp.task() :创建 gulp 任务。
- gulp.watch() :监控文件的变化。
// gulpfile.js 文件
const gulp = require('gulp');
gulp.task('firstTast',() => { // 创建一个名为firstTast
gulp.src('./src/css/index.css')
.pipe(gulp.dest('./dist/index.css'));
});
3、如何编写和执行 Gulp 任务,以下是压缩 css 文件代码和压缩 js 文件代码的使用示例,分别使用了前面下载好的两个插件提供的 API (具体使用参照官网)。
const gulp = require('gulp');
const gulpCsso = require('gulp-csso');
const gulpUglify = require('gulp-uglify');
gulp.task('task1', () => {
return gulp.src('./src/css/*.css')// *表示该路径下所有的css文件
.pipe(gulpCsso())
.pipe(gulp.dest('./dist/css'));
})
gulp.task('task2', () => {
return gulp.src('./src/js/*.js') // * 表示该路径下所有的 js 文件
.pipe(gulpUglify())
.pipe(gulp.dest('./dist/js'))
})
在 gulpfile.js 文件按照 gulp 规范编写任务后在命令行并不能直接执行。需要下载 gulp 的同名命令行工具 gulp-cli 。所以在执行上面的任务时需要使用 npm install gulp-cli -g 命令行命令全局下载安装该工具。之后便可以在命令行执行我们自定义的命令操作。如下:
执行任务后在 dist 文件夹里面相应的文件夹和文件(如果没有则会自动新建)就会保存压缩后的文件,如下图是我随意压缩的一个 js 文件
在定义了多个任务后,可以将一系列相关的任务进行组合成一个新的任务。当执行这个组合任务时,所有包含在里面的任务都会执行。并且存在一个默认的任务 default,在 gulp 命令后面不跟任务名时会默认执行该任务。任务的组合和默认任务的执行示例如下:
// gulp.task('default', ['task1', 'task2']);
// gulp4 不支持上面的写法了
// 在执行 default 任务时,task1 task2 均会执行
gulp.task('default', gulp.series(['task1', 'task2']));
4. package.json 和 package-lock.json 文件
在使用 npm 包管理工具时系统会自动在项目根目录生成存储本地安装的包的文件夹 node_modules 和与包管理有关的文件 package-lock.json 。我们还可以手动生成一个与包管理有关的重要文件 package.json 。以下内容分别介绍它们。
4.1 node_modules 文件夹
node_modules 文件夹在我们使用 npm 包管理工具时自动生成,里面保存了我们本地下载(全局下载的包会被保存在 C:\Users\你的用户名\AppData\Roaming\npm\node_modules 中)的所有包和其依赖的包。
但是打开文件夹我们可以看到里面的文件夹及文件非常多且零碎(即使你只下载了很少几个包,因为它们还有很多依赖的包)。这就会出现以下两个问题。
- 文件夹以及文件过多过碎导致我们在将项目整体拷贝给别人或者部署上线时,传输速度会很慢。
- 复杂的模块依赖关系需要被记录,确保模块的版本和当前版本保持一致,否则导致当前项目运行报错。
package.json 与 package-lock.json 文件解决了上述两个问题。具体如何实现请看下面的章节。
4.2 package.json 文件
既然文件过多过碎导致传输过慢,或者有些包根本就不用传输过去(例如 gulp 我们只在开发阶段使用它,而线上运行并没有用到它)。那我们索性就不用拷贝过去了,将项目拷贝过去后再下载相关依赖包。那怎么知道需要下载哪些包呢?也不可能要一个一个下载吧?
4.2.1 package.json 文件的使用
package.json 文件是项目描述文件,记录了当前项目信息。例如项目名称、版本、作者、github 地址、当前项目依赖了哪些第三方模块等信息。该文件不会自动生成,在需要时命令行执行命令 npm init [-y] 生成。后面使用 -y 参数时文件内所有信息都使用默认值,不加参数系统会询问这些信息的值并在文件中设定该值(当然直接回车也可以将其设置为默认值)。如下是生成的 package.json 文件的内容。
// package.json
{
"name": "usegulp", // 项目名,使用了默认值
"version": "1.0.0", // 版本
"description": "gulp 模块的使用", // 描述,这是我自己写的
"main": "index.js", // 程序主入口文件,后面会用到
"dependencies": { // 项目依赖的包,自动生成更新
"formidable": "^1.2.2",
"mongoose": "^5.11.19"
},
"devDependencies": { // 开发依赖的包,自动生成更新
"gulp": "^4.0.2",
"gulp-csso": "^4.0.1",
"gulp-uglify": "^3.0.2"
},
"scripts": { // 命令别名,后面会用到
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "TKOP_", // 作者,当然是我
"license": "ISC" // 项目遵循的协议,默认开源 isc
}
注意:上面有两个字段 dependencies 和 devDependencies 分别保存着两类第三方模块(包)。为什么要将它们区分开来呢?手动生成该文件之前下载(不包含全局下载)的所有包都会放在 dependencies 字段。所以我们需要在下载包前就手动生成它。
4.2.1 项目依赖
上面的 package.json 文件中有一个字段 dependencies 的值表示的是项目依赖的包,什么是项目依赖?如何下载项目依赖?
- 在项目的开发阶段和线上运营阶段都需要使用的第三方包,称为项目依赖。
- 使用 npm install 包名 命令下载的文件会默认被添加到 package.json 文件的 dependencies 字段中,即默认为项目依赖。
在我们将整个项目拷贝给别人或者布置上线时,不会拷贝任何第三方包文件。因此要运行项目时需要重新下载这些项目依赖。下载项目依赖使用到的命令是 npm install --production 。
4.2.2 开发依赖
- 在项目的开发阶段需要使用,线上运营阶段不需要使用的第三方包,称为开发依赖。
- 使用 npm install --save-dev 包名 命令下载的包会将包名添加到 package.json 文件的 devDependencies 字段中。
在我们将整个项目拷贝给别人继续进行开发时,不会拷贝任何第三方包文件。因此要运行项目时需要重新下载这些开发依赖和项目依赖。同时下载开发依赖和项目依赖使用到的命令是 npm install 。
综上,区分项目依赖和开发依赖的目的是为了方便开发人员在不同的环境中下载不同的依赖。比如在线下开发环境下载开发依赖和项目依赖,在线上运行环境只需下载项目依赖。
示例如下,首先我删掉项目根目录下的 node_modules 文件夹,即删掉了所有依赖。然后假设下载是在线下进行项目开发,那我既要下载项目依赖又要下载开发依赖(全部依赖),下载如下:
然后我又删掉项目根目录下的 node_modules 文件夹,即删掉了所有依赖。然后假设现在是线上运行环境(不再需要开发依赖),那我只需要下载项目依赖,下载如下:
从下载结果我们可以看到在下载项目依赖时一共下载了 35 个依赖包。下载全部依赖时下载了 359 个依赖包,也就是开发依赖有 324 个包。你在上述步骤下完项目依赖后再使用 npm install 命令就可以看到下载的是开发依赖的 324 个包(项目依赖已下好)。如下图:
4.3 package-lock.json 文件的作用
在上述的依赖下载时你是否会想到一个问题。一个项目使用到的包或者其中依赖的模块可能会随时升级到不同的版本,升级后的模块提供的 API 可能会发生改变。这样在有需要重新下载这些依赖时如何确保我们下载的版本与当前项目所用的版本一致?如果不一致可能会导致项目运行出错。
package-lock.json 文件是使用 npm 包管理工具时自动生成的一个详细描述模块之间的依赖关系的文件。该文件还记录了当前项目使用的第三方模块的版本。其作用如下:
- 锁定包的版本,确保再次下载时不会因为版本不同而产生问题。
- 加快下载速度,因为该文件中已经记录了项目所依赖的第三方包的树状结构和包的下载地址,重新安装时只需要下载即可,减少了一些步骤。
// 这是 package-lock.json 文件中我随便复制出来的一个依赖包的信息。
// 可以看到有版本、下载地址、是开发依赖还是项目依赖和该包依赖了那些模块等信息。
"anymatch": {
"version": "2.0.0",
"resolved": "https://registry.npm.taobao.org/anymatch/download/anymatch-2.0.0.tgz",
"integrity": "sha1-vLJLTzeTTZqnrBe0ra+J58du8us=",
"dev": true,
"requires": {
"micromatch": "^3.1.4",
"normalize-path": "^2.1.1"
},
5. Node.js 中模块的加载机制
1、当模块拥有路径但是没有后缀时
const testModule = require('./testModule');
- require 方法根据模块路径查找,如果是完整路径,直接引入模块。
- 如果模块后缀名省略,先找同名 JS 文件再找同名 Js 文件夹。
- 如果找到的是同名文件夹则会引入文件夹中的 index.js 文件。
- 如果该同名文件夹中没有 index.js 文件,则会去当前文件夹中的 package.json 文件中查找 main 字段中的入口文件。
- 如果指定的入口文件不存在或者根本就没有指定则报错,模块没有找到。
2、当文件没有路径且没有后缀时
const testModule = require('testModule');
- Node.js 首先会查找是否有该原生模块。
- 若不是原生模块,会去 node_modules 文件夹查找是否有该名字的 JS 文件。
- 再看是否有该名字的文件夹并查看该文件夹是否有 index.js 文件。
- 如果没有 index.js 文件则查看该文件夹中的 package.json 文件 mian 字段是否指明模块的入口文件。
- 如果像上述一样没有则报错。
这些知识应该和以后模块化开发时文件夹的结构有关。