nodejs笔记
1 邂逅Node
1.1 什么是nodejs
Node.js是一个基于V8 JavaScript引擎的JavaScript运行时环境

Node的版本工具n,nvm(Window均不可使用……)
- nvm有window版本,可以使用nvm-window
基本使用
1 | # 查看nvm可用的nodejs版本(所有发布的) |
Node的REPL:REPL是一个简单的、交互式的编程环境。事实上,外面浏览器的console就可以看成一个REPL
1.2 Node程序传递参数
在某些情况下执行node程序的过程中,我们可能希望给node传递一些参数:
1 | node index.js env=development coderwhy |
如果我们这样来使用程序,就意味着我们需要在程序中获取到传递的参数:
- 获取参数其实是在process的内置对象中的;如果我们直接打印这个内置对象,它里面包含特别的信息
1 | console.log(process.argv); |
C/C++中main函数会接受两个参数
- argc(argument counter):传递参数的个数
- argv(argument vector):传入的具体参数
1.3 Node的输出
1 | // 输出内容 |
1.4 常见的全局对象
process对象
process提供了Node进程中相关的信息: 比如Node的运行环境、参数信息等
console对象
提供了简单的调试控制台
定时器函数
在node中使用定时器有好几种方式
setTimeout(callback, delay[, ...args]):callback在delay毫秒后执行一次setInterval(callback, delay[, ...args]):callback每delay毫秒重复执行一次setImmediate(callback[, ...args]):callbackI / O事件后的回调的“立即”执行- 这里先不展开讨论它和
setTimeout(callback, 0)之间的区别 - 因为它涉及到事件循环的阶段问题,我会在后续详细讲解事件循环相关的知识
- 这里先不展开讨论它和
process.nextTick(callback[, ...args]):添加到下一次tick队列中
global对象
全局对象,process、console、setTimeout等都有被放到global中
global和window的区别:
在浏览器中,全局变量都是在window上的,比如有document、setInterval、setTimeout、alert、console等等
在Node中,我们也有一个global属性,并且看起来它里面有很多其他对象。
但是在浏览器中执行的JavaScript代码,如果我们在顶级范围内通过var定义的一个属性,默认会被添加到window对象上
1
2 var name = 'sjx';
console.log(window.name) // sjx
- 但是在node中,我们通过var定义一个变量,它只是在当前模块中有一个变量,不会放到全局中:
1
2 var name = 'sjx';
console.log(global.name) // undefined
官方文档中的全局对象:

1.5 特殊的全局对象
可以在模块中任意使用,但是在命令行交互中是不可以使用的:包括:__dirname、__filename、exports、module、require()
__dirname:获取当前文件所在的路径,不包括后面的文件名
__filename:获取当前文件所在的路径和文件名称,包括后面的文件名称
1 | console.log(__dirname); |
2 JavaScript模块化
2.1 什么是模块化
事实上模块化开发最终的目的是将程序划分成一个个小的结构;这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构。这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用,也可以通过某种方式,导入另外结构中的变量、函数、对象等
按照这种结构划分开发程序的过程,就是模块化开发的过程(ES6推出),JavaScript中一个文件即一个模块
2.2 CommonJS规范
Node是CommonJS在服务器端一个具有代表性的实现;
Browserify是CommonJS在浏览器中的一种实现;
webpack打包工具具备对CommonJS的支持和转换;
Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:
在Node中每一个js文件都是一个单独的模块;
这个模块中包括CommonJS规范的核心变量:exports、module.exports、require;
我们可以使用这些变量来方便的进行模块化开发;
模块化的核心是导出和导入,Node中对其进行了实现:
- exports和module.exports可以负责对模块中的内容进行导出;
- require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
1、export
1 | // a.js |
1 | // bar.js |
node bar.js 执行该文件
实际上为一个浅拷贝(引用赋值,仅保存了地址,和原来的对象指向同一块内存空间)
默认exports为一个空对象,但会开辟一个内存空间,exports存的是这个内存的地址,添加exports属性时即在这个内存空间中加入东西(原理:对象的引用赋值)。字符串是值赋值
1 | var name = 'sjx' |
exports与module.exports:
module对象的exports属性也是exports对象的一个引用,即module.exports = exports(源码中实现),所以可以理解为exports和module.exports等价,均指向一个内存空间
2、require
require(X)引入细节:
- X是一个核心模块,比如path,http(直接返回核心模块,并且停止查找)
X是以./或../或/开头的
- 第一步:将X当作一个文件在对应的目录下查找:如果有后缀名,按照后缀名的格式查找对应的文件;如果没有后缀名,会按照如下顺序:直接查找文件X,查找X.js文件,查找X.json文件,查找X.node文件
- 第二步:没有找到对应的文件,将X作为一个目录,查找目录下面的index文件
直接是一个X(没有路径),并且X不是一个核心模块,则会在node_modules文件夹中查找,若找不见则会不断从上一层的node_modules查找,如果都没有找到,则会报错not found
3、模块的加载过程
使用require引入js文件时,会先运行一次这个js文件(同步加载),并进行缓存。当在程序运行过程中多次引入同一个js文件,这个js文件只会运行一次
结论一:模块在被第一次引入时,模块中的js代码会被运行一次
结论二:模块被多次引入时,会缓存,最终只加载(运行)一次
- 每个模块对象module都有一个属性:loaded:为false表示还没有加载,为true表示已经加载
1 | console.log(module) |
结论三:如果有循环引入,那么加载顺序为深度优先顺序
- Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb

4、CommonJS规范缺点
CommonJS加载模块是同步的,应用于浏览器需要等js文件从服务器上下载下来才能运行,所以在浏览器中通常不使用CommonJS规范
2.3 AMD规范
AMD主要是应用于浏览器的一种模块化规范:AMD(Asynchronous Module Definition)采用的是异步加载模块。事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了
规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用:AMD实现的比较常用的库是require.js和curl.js
require.js
第一步:下载require.js
找到其中的require.js文件;
第二步:定义HTML的script标签引入require.js和定义入口文件:
- data-main属性的作用是在加载完src的文件后会加载执行data-main中的文件
1 | <script src="./lib/require.js" data-main="./index.js"></script> |
index.js
1 | (function() { |
module/bar.js
1 | define(function() { |
module/foo.js
1 | define(['bar'], function(bar) { |
输出
1 | coderwhy |
2.4 CMD规范
CMD规范也是应用于浏览器的一种模块化规范:
- CMD (Common Module Definition)也采用了异步加载模块,但是它将CommonJS的优点吸收了过来,但是目前CMD使用也非常少了
CMD也有自己比较优秀的实现方案:
- SeaJS
SeaJS的使用
第一步:下载SeaJS
找到dist文件夹下的sea.js
第二步:引入sea.js和使用主入口文件
- seajs是指定主入口文件的
区别:模块都是接受三个参数
1 | <script src="./lib/sea.js"></script> |
index.js
1 | define(function(require, exports, module) { |
module/foo.js
1 | define(function(require, exports, module) { |
2.5 ES Module
ES Module和CommonJS的模块化有一些不同之处:
一方面它使用了import和export关键字
另一方面它采用编译期的静态分析,并且也加入了动态引用的方式
ES Module模块采用export和import关键字来实现模块化:
export负责将模块内的内容导出;
import负责从其他模块导入内容;
采用ES Module将自动采用严格模式:use strict(如果你不熟悉严格模式可以简单看一下MDN上的解析;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode)
如果通过本地加载HTML文件,将会遇到CORS错误,因为JavaScript模块安全性需要,故需要通过一个服务器来测试(可以用vscode中的插件Live Server或者在电脑上安装apache或者nginx并启动服务)
1、export
export关键字将一个模块中的变量、函数、类等导出;
我们希望将其他中内容全部导出,它可以有如下的方式:
- 方式一:在语句声明的前面直接加上export关键字
- 方式二:将所有需要导出的标识符,放到export后面的 {}中
注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的,所以export {name: name},是错误的写法
- 方式三:导出时给标识符起一个别名
1 | export { |
2、import
import关键字负责从另外一个模块中导入内容
导入内容的方式也有多种:
- 方式一:import {标识符列表} from ‘模块’;
注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容
方式二:导入时给标识符起别名
方式三:通过 将模块功能放到一个模块功能对象( as a)上
3、export和import结合使用
1 | export { sum as barSum } from './bar.js' |
在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中,这样方便指定统一的接口规范,也方便阅读,这个时候,我们就可以使用export和import结合使用
4、default用法
默认导出export时可以不需要指定名字,在导入时不需要使用{},并且可以自己来指定名字,它也方便我们和现有的CommonJS等规范相互操作
在一个模块中,只能有一个默认导出
5、import函数
通过import加载一个模块,是不可以在其放到逻辑代码中的
ES Module在被JS引擎解析时,就必须知道它的依赖关系。由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况
但是某些情况下,我们确确实实希望动态的来加载某一个模块,这个时候我们需要使用 import() 函数来动态加载
1 | let flag = true; |
export导出的属性为常量,import引入后不可直接更改,如果属性为对象,则可以更改,因为对象导出为引用赋值;而export导出的属性为引用赋值,可以实现实时更新
index.js
1 | var name = 'sjx' |
foo.js
1 | import { name, age, info } from './index.js' |
对比CommonJS和ES Module加载过程
CommonJS模块加载js文件的过程是运行时加载的,并且是同步的
- 运行时加载意味着是js引擎在执行js代码的过程中加载模块
- 同步的就意味着一个文件没有加载结束之前,后面的代码都不会执行
ES Module加载js文件的过程是编译(解析)时加载的,并且是异步的
- 编译时(解析)时加载,意味着import不能和运行时相关的内容放在一起使用
- 异步的意味着:JS引擎在遇到import时会去获取这个js文件,但是这个获取的过程是异步的,并不会阻塞主线程继续执行,export导出的是对应变量的引用而非对应变量的值
通常情况下,CommonJS不能加载ES Module;多数情况下,ES Module可以加载CommonJS
3 Node常用内置模块
3.1 内置模块path
1、path出现的原因
window一般使用\和\\作为文件路径的分隔符,目前也支持/;但在Mac、linux的Unix操作系统上使用/来作为文件路径的分隔符。
为了屏蔽它们之间的差异,在开发中对于路径的操作我们可以使用path模块
2、path常见的API
从路径中获取信息
dirname:获取文件的父文件夹
basename:获取文件名
extname:获取文件扩展名
路径的拼接
如果我们希望将多个路径进行拼接,但是不同的操作系统可能使用的是不同的分隔符,这个时候我们可以使用path.join函数
将文件和某个文件夹拼接
如果我们希望将某个文件和文件夹拼接,可以使用 path.resolve,resolve函数会判断我们拼接的路径前面是否有 /或../或./:如果有表示是一个绝对路径,会返回对应的拼接路径;如果没有,那么会和当前执行文件所在的文件夹进行路径的拼接(相对智能)
1 | const path = require('path') |
1 | const path = require('path') |
resolve在webpack中获取路径或者起别名的地方也可以使用(相对与字符串拼接安全)
3.2 内置模块fs
1、介绍
fs是File System的缩写,表示文件系统。
对于任何一个为服务器端服务的语言或者框架通常都会有自己的文件系统:因为服务器需要将各种数据、文件等放置到不同的地方,比如用户数据可能大多数是放到数据库中的;比如某些配置文件或者用户资源(图片、音视频)都是以文件的形式存在于操作系统上的
Node也有自己的文件系统操作模块,就是fs:
借助于Node帮我们封装的文件系统,我们可以在任何的操作系统(window、Mac OS、Linux)上面直接去操作文件
这也是Node可以开发服务器的一大原因,也是它可以成为前端自动化脚本等热门工具的原因
2、常见的API
https://nodejs.org/dist/latest-v14.x/docs/api/fs.html
这些API大多数都提供三种操作方式:
方式一:同步操作文件:代码会被阻塞,不会继续执行;
方式二:异步回调函数操作文件:代码不会被阻塞,需要传入回调函数,当获取到结果时,回调函数被执行;
方式三:异步Promise操作文件:代码不会被阻塞,通过 fs.promises 调用方法操作,会返回一个Promise,可以通过then、catch进行处理;
案例:获取一个文件的状态
1 | const fs = require('fs') |
3、文件描述符
在 POSIX 系统上,对于每个进程,内核都维护着一张当前打开着的文件和资源的表格。每个打开的文件都分配了一个称为文件描述符的简单的数字标识符。在系统层,所有文件系统操作都使用这些文件描述符来标识和跟踪每个特定的文件。Windows 系统使用了一个虽然不同但概念上类似的机制来跟踪资源。为了简化用户的工作,Node.js 抽象出操作系统之间的特定差异,并为所有打开的文件分配一个数字型的文件描述符。
f.open()方法用于分配新的文件描述符。一旦被分配,则文件描述符可用于从文件读取数据,向文件写入数据或请求关于文件的信息
1 | fs.open("../foo.txt", 'r', (err, fd) => { |
4、文件的读写
fs.readFile(path[, options], callback):读取文件的内容
fs.writeFile(file, data[, options], callback):在文件中写入内容
1 | const fs = require('fs') |
{}中为写入时填写的option参数:
flag:写入的方式
- w打开文件写入,默认值(替换文件原有的内容)
- w+打开文件进行读写,如果不存在则创建文件
- r+打开文件进行读写,如果不存在那么抛出异常
- r打开文件读取,读取时的默认值
- a打开要写入的文件,将流放在文件末尾。如果不存在则创建文件(在文件后面加入内容)
- a+打开文件以进行读写,将流放在文件末尾。如果不存在则创建文件
encoding:字符的编码
flag选项
https://nodejs.org/dist/latest-v14.x/docs/api/fs.html#fs_file_system_flags
- w打开文件写入,默认值
- w+打开文件进行读写,如果不存在则创建文件
- r+打开文件进行读写,如果不存在那么抛出异常
- r打开文件读取,读取时的默认值
- a打开要写入的文件,将流放在文件末尾。如果不存在则创建文件
- a+打开文件以进行读写,将流放在文件末尾。如果不存在则创建文件
encoding
网上一篇讲解字符编码很好的文章:详解字符编码 - 简书 (jianshu.com)
1 | const fs = require('fs') |
5、文件夹操作
新建一个文件夹
使用fs.mkdir()或fs.mkdirSync()创建一个新文件夹
1 | const dirname = '../why' |
获取文件夹的内容
1 | function readFolderd(folder) { |
文件重命名
1 | fs.rename('../why', '../coder', err => { |
文件夹的复制
1 | const fs = require('fs') |
3.3 events模块
Node中的核心API都是基于异步事件驱动,发出事件和监听事件都是通过EventEmitter类来完成的,它们都属于events对象
emitter.on(eventName, listener):监听事件,也可以使用addListeneremitter.off(eventName, listener):移除事件监听,也可以使用removeListeneremitter.emit(eventName[, ...args]):发出事件,可以携带一些参数
1 | const EventEmitter = require("events"); |
1、常见的属性
emitter.eventName():返回当前EventEmitter对象注册的事件字符串数组emitter.getMazListener():返回当前EventEmitter对象的最大监听器数量,可以通过setMaxListeners()来修改,默认是10emitter.listenerCount(eventName):返回当前EventEmitter对象某一个事件名称,监听器的个数emitter.listeners(eventName):返回当前EventEmitter对象某一个事件监听器上所有的监听器的数组
1 | console.log(bus.eventNames()) |
1 | const EventEmitter = require('events'); |
2、常见的方法
emitter.once(eventName, listener):事件监听一次
1 | emitter.once('click', (args) => { |
emitter.prependListener():将监听事件添加到最前面
1 | emitter.prependListener('click', (args) => { |
emitter.prependOnceListener():将监听事件添加到最前面,但是只监听一次
1 | emitter.prependOnceListener('click', (args) => { |
emitter.removeAllListener([eventName]):移除所有的监听器
1 | emitter.removeAllListener('click', (args) => { |
1 | const EventEmitter = require("events"); |
4 包管理工具深入解析
另一篇笔记写了这个:前端常用包管理工具
5 开发脚手架工具
5.1 创建项目
5.2 自定义终端命令过程
6 Buffer的使用
7 深入事件循环
8 Stream
9 http模块
9.1 如何创建服务
1、Web服务器初体验
当应用程序(客户端)需要某一个资源时,可以向一台服务器通过http请求获取到这个资源,提供资源的这个服务器,就是一个Web服务器
目前有很多开源的Web服务器:Nginx、Apache、Apache Tomcat(静态、动态)、Nodejs
1 | const http = require('http') |
2、创建服务器
创建服务器对象,我们是通过 createServer 来完成的
http.createServer会返回服务器的对象;- 底层其实使用直接 new Server 对象。
1 | function createServer(opts, requestListener) { |
那么,当然,我们也可以自己来创建这个对象:
1 | const server2 = new http.Server((req, res) => { |
上面我们已经看到,创建Server时会传入一个回调函数,这个回调函数在被调用时会传入两个参数:
- req:request请求对象,包含请求相关的信息
- res:response响应对象,包含我们要发送给客户端的信息
3、监听端口和主机
Server通过listen方法来开启服务器,并且在某一个主机和端口上监听网络请求:也就是当我们通过 ip:port的方式发送到我们监听的Web服务器上时,我们就可以对其进行相关的处理;
listen函数有三个参数:
端口port: 可以不传, 系统会默认分配端口,后续项目中我们会写入到环境变量中
主机host: 通常可以传入localhost、ip地址127.0.0.1、或者ip地址0.0.0.0,默认是0.0.0.0
- 监听IPV4上所有的地址,再根据端口找到不同的应用程序。比如我们监听
0.0.0.0时,在同一个网段下的主机中,通过ip地址是可以访问的 - 正常的数据库包经常 应用层 - 传输层 - 网络层 - 数据链路层 - 物理层,而回环地址,是在网络层直接就被获取到了,是不会经常数据链路层和物理层的。比如我们监听
127.0.0.1时,在同一个网段下的主机中,通过ip地址是不能访问的 - localhost:本质上是一个域名,通常情况下会被解析成127.0.0.1
- 127.0.0.1:回环地址(Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被自己接收
- 0.0.0.0:
- 监听IPV4上所有的地址,再根据端口找到不同的应用程序。比如我们监听
回调函数:服务器启动成功时的回调函数
1 | server.listen(() => { |
9.2 request请求对象
在向服务器发送请求时,我们会携带很多信息,比如:
- 本次请求的URL,服务器需要根据不同的URL进行不同的处理;
- 本次请求的请求方式,比如GET、POST请求传入的参数和处理的方式是不同的;
- 本次请求的headers中也会携带一些信息,比如客户端信息、接受数据的格式、支持的编码格式等;
- 等等…
这些信息,Node会帮助我们封装到一个request的对象中,我们可以直接来处理这个request对象:
1 | const server = http.createServer((req, res) => { |
1、URL的处理
客户端在发送请求时,会请求不同的数据,那么会传入不同的请求地址:
- 比如
http://localhost:8000/login; - 比如
http://localhost:8000/products;
服务器端需要根据不同的请求地址,作出不同的响应:
1 | const server = http.createServer((req, res) => { |
那么如果用户发送的地址中还携带一些额外的参数呢?
- 比如:
http://localhost:8000/login?name=why&password=123; - 这个时候,url的值是
/login?name=why&password=123;
我们如何对它进行解析呢?
- 使用内置模块url;
1 | const url = require('url'); |
解析结果:
1 | Url { |
我们会发现 pathname就是我们想要的结果。
但是 query 信息如何可以获取呢?
- 方式一:截取字符串;
- 方式二:使用querystring内置模块;
1 | const { pathname, query } = url.parse(req.url); |
2、Method的处理
3、header属性
9.3 response响应对象
1、返回响应结果
2、返回状态码
3、响应头文件
9.4 文件上传的使用
9.5 http发送网络请求
10 express框架
10.1 express的安装
方式一
1 | npm install -g express-generator |
创建项目
1 | express express-demo |
项目目录如下:
1 | ├── app.js |
安装依赖,将项目跑起来
1 | npm install |
方式二
1 | # 初始化一个新项目 |
10.2 express初体验
1 | const express = require('express'); |
10.3 请求和响应
请求的路径中如果有一些参数,可以这样表达:
/users/:userId;- 在
request对象中要获取可以通过req.params.userId;
返回数据,我们可以方便的使用json:
res.json(数据)方式;- 可以支持其他的方式,可以自行查看文档;
- https://www.expressjs.com.cn/guide/routing.html
1 | const express = require('express'); |
10.4 Express中间件
1、中间件
中间件本质是一个回调函数,这个回调函数接受三个参数:请求对象、响应对象、next函数
中间件可以执行任何代码,更改请求和响应对象,结束请求响应周期,调用栈中的下一个中间件
如果当前中间件功能没有结束请求-响应周期,则必须调用next()将控制权传递给下一个中间件功能,否则,请求将被挂起。
中间件函数调用的元素:
2、
11 koa框架
12 Node中使用MySQL
13 coderhub项目
14 云服务器
- 本文标题:nodejs笔记
- 本文作者:馨er
- 创建时间:2022-04-11 17:52:07
- 本文链接:https://sjxbbd.vercel.app/2022/04/11/5a7c9fba07b6/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!