Node.js基础---一文了解Node.js

221 阅读1小时+

一、前言

Node.js是一个开源的、基于Chrome V8引擎的服务器端JavaScript运行时环境,可以在浏览器环境以外的主机上解释和运行JavaScript代码,它发布于2009年5月,由谷歌工程师Ryan Dahl开发。

关于node下载:现在Node.js官网的界面发生了改变,直接去点击下载然后一直下一步就好,之后可以使用node -v来检测是否安装完毕;如果在使用的过程中提示你没找到node这个命令,那你就需要将其添加到环境变量中去了

image.png

image.png

二、了解Node.js

(1)基本组成

==> 标准库 <==

说明: 标准库提供开发人员能够直接进行调用并使用的一些API,如http模块、stream模块、fs文件系统模块等,可以使用JavaScript代码直接调用

==> 中间层 <==

说明: 由于底层库采用C/C++实现,而标准库中的JavaScript代码无法直接与C/C++进行通信,因此提供了中间层,它在标准库和底层库之间起到了桥梁的作用,它封装了底层库中V8引擎和libuv等的实现细节,并向标准库提供基础API服务

==> 底层库 <==

说明: 它是Node.js运行的关键,由C/C++实现,其主要包括以下几个部分

  • V8引擎: Google的一个开源的JavaScript和webAssembly引擎,使用C++语言编写,用于Chrome浏览器和Node.js等,V8引擎主要是为了提高JavaScript的运行效率,因此它采用了提前编译的方式,将JavaScript编译为原生机器码,这样在执行阶段程序的执行效率可以完全媲美二进制程序

  • libuv: 一个专门为Node.js量身打造的跨平台异步I/O库,使用C语言编写,提供了非阻塞的文件系统、DNS、网络、子进程、管道、信号轮训以及流式处理机制,这使得libuv使用各平台提供的事件驱动模块实现异步,使其可以支持Node.js应用的非文件I/O模块,并把相应的事件和回调封装成I/O观察者放到底层的事件驱动模块中,当事件触发时,libuv会执行I/O观察者中的回调;Node.JS会通过中间层将用户的JavaScript代码传递给底层库的V8引擎进行解析,然后通过libuv进行循环调度,最后再返回给调用Node.js标准库的应用,因此libuv实现了一个线程池来支持Node.js中的文件I/O、DNS以及用户异步等操作

  • C-ares: 一个用来处理异步DNS请求的库,使用C语言编写,对应Node.js中dns模块提供的resolve()系列方法

  • OpenSSL: 一个通用的加密库,通常用于网络传输中的TLS和SSL协议实现,对应Node.js中的tls、crypto模块

  • zlib: 一个提供压缩和解压支持的底层模块

(2)工作原理

==> 事件驱动 <==

说明: Node.js采用一种独特的事件驱动程序,将I/O操作作为事件响应,而不是阻塞操作,从而实现了事件函数的快速执行与错误处理,由于Node.js能够采用异步非阻塞的方式访问文件系统、数据、网络等外部资源,因此它能够高效的处理海量的迸发请求,极大的提高了应用程序的吞吐量

==> 单线程 <==

说明: Node.js采用单线程模型,只需要轻量级的线程就可以处理大量的请求,与多线程的模型相比,这种模型消除了线程之间的竞争,使得程序的稳定性大幅提升,在Node.js的单线程模型中,所有的I/O操作都被放在事件队列中,一旦事件出现,Node.js就会依次处理它们,事实上,大多数网站的服务器端都不会做太多的计算,它们接受到请求以后,把请求交给其它服务来处理,然后等待结果返回,再把结果发回客户端,因此,Node.js针对这一事实采用了单线程模型处理,它不会为每一个接入的请求分配一个线程,而是用一个主线程处理所有的请求,然后对I/O操作进行异步处理,避开了创建、销毁线程以及在线程间切换所需的开销和复杂性

==> 非阻塞I/O <==

说明: 在传统的I/O操作中,当数据读取或者写入操作发生的时候,程序会被阻塞,等数据读取或者写入操作完成后才能进入下一步的操作,但是,在Node.js中,所有的I/O操作都是非阻塞的,当某个I/O操作发生的时候,不是等待其操作完成才进行下一步操作,而是直接回调相应的函数,从而实现了对外部资源的高效访问

==> 事件循环 <==

说明: Node.js采用了一种特殊的设计方式,也就是事件循环,它在工作线程池中维护一个任务队列,当接到请求后,将该请求作为一个事件放入这个队列中,然后继续接受其它的请求,同时Node.js程序会不断的从工作队列中获取要执行的事件,并通过事件循环流程对其进行处理;在事件循环中,主要有计数器、回调、轮训、检查以及关闭回调这几个阶段组成

  • 计时器: 处理由setTimeout()和setInterval()设置的回调
  • 回调: 运行挂起的回调函数
  • 轮询: 检索传入的I/O事件并运行与其相关的回调函数
  • 检查: 完成轮询后立即运行回调
  • 关闭回调: 关闭事件和回调

image.png

(3)npm包管理工具

说明: npm是Node.js的标准软件包管理器,其仓库中存在超过130万个软件包并且都是免费的;最初npm是作为下载和管理Node.js包的依赖方式,但现在已经成为前端JavaScript中使用的通用工具,随着Node.js的安装成功,npm也会自动安装完毕

==> 常用命令 <==

  • npm -v:查看 npm 版本

  • npm init: 初始化一个 package.json 配置文件,可以在后面加上-y,可以生成默认的配置文件

  • npm install 包名@版本号 / npm i 包名: 前者会安装指定版本的软件包,后者是直接使用最新版本的软件包

  • npm insall 包名 --sava-dev:安装的包只用于开发环境,不用于生产环境,会出现在 package.json文件中的 devDependencies 属性中

  • npm insall 包名 --save:安装的包需要发布到生产环境的,会出现在 package.json 文件中的dependenceies 属性中

  • npm insall 包名 -g: 在全局环境中安装,也就是将软件包下载到你的计算机上,而不是单纯的在你的项目中下载,除了这种安装以外,其它的全是本地安装,也就是下载到你的项目中去

  • npm list:查看当前目录下已安装的node包

  • npm list -g:查看全局已经安装过的node包

  • npm --help:查看npm帮助命令

  • npm update包名:更新指定包

  • npm uninstall 包名:卸载指定包

  • npm config list:查看配置信息

  • npm 指定命令--help:查看指定命令的帮助

  • npm info 指定包名:查看远程npm上指定包的所有版本信息

  • npm root:查看当前包的安装路径

  • npm root -g:查看全局的包的安装路径

  • npm ls 包名:查看本地安装的指定包及版本信息,没有显示empty

  • npm ls 包名 -g:查看全局安装的指定包及版本信息,没有显示empty

  • npm config set registry https://registry.npm.taobao.org: 将npm设置为淘宝镜像

  • npm config set registry https://registry.npmjs.org:切换回默认全局镜像

  • npm config get registry:查看npm镜像设置

  • npm config list:查看npm配置

==> package.json <==

说明: 一般在每一个项目中都会存在一个package.json文件,这个文件主要用来对项目进行配置或者储存所有已安装软件包的名称和版本等信息;当然,这个文件也可以自己通过npm init -y自动生成默认的package.json文件,下面就是默认生成的文件内容

  • 对于Node.js程序,这个文件中的内容没有固定的要求,唯一要求就是需要遵循json的格式,尝试以编程的方式访问其属性的程序无法读取它
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
<== 相关属性 ==>
  • name: 应用程序或者软件包的名称,名称必须少于214个字符,且不能包含空格,只能包含小写字母、连字符( - )或者下划线( _ )

  • version: 当前的版本,该属性的值遵循语义版本控制法,这意味着版本始终以3个数字表示---X.X.X,其中,第一个数字是主版本号,第二个数字是次版本号,第三个数字是补丁版本号

  • description: 应用程序或者软件包的简短描述,如果要将软件包发布到npm,则这个属性特别有用,使用者可以知道该软件包的具体作用

  • main: 设置应用程序的入口点

  • scripts: 定义一组可以运行的Node.js脚本,这些脚本是命令行应用程序,可以通过调用npm run xxx来运行它们,其中xxx是命令的名称,比如vite项目中的npm run dev

  • keywords: 包含与软件包功能相关的关键字数组

  • author: 列出软件包的作者名称

  • license: 指定软件包的许可证

  • contributors: 除作者外,该项目可以有一个或者多个贡献者,此属性是列出他们的数组

  • bugs: 链接到软件包的问题跟踪器,最常用的是GitHub的issues页面

  • homepage: 设置软件包的主页

  • repository: 指定程序包仓库的所在位置

  • private: 如果设置为true,则可以防止应用程序或者软件包被意外的发布到npm上

  • dependencies: 设置作为依赖安装的npm软件包列表,也就是这些软件包会被打包至项目中去;当使用npm或者yarn安装软件包的时候,该软件包会被自动插入此列表中

  • devDependencies: 设置作为开发依赖安装的npm软件包的列表,这个不同于上面的列表,因为它们只需要安装在开发机器上面,而无需打包到项目中去,当使用npm或者yarn安装软件包的时候,该软件包会被自动插入此列表中

  • engines: 设置软件包或者应用程序要运行的Node.js或者其它命令的版本

  • browserslist: 用于告知要支持哪些浏览器以及浏览器版本

<== package-lock.json ==>

说明: 这个文件存在于npm5以上的版本,它的作用在于跟踪被安装的每个软件包的确切版本,以便产品可以以相同的方式被100%复制;由于package.json文件可以使用语义化版本表示法(~ ^)来设置要升级到的版本,如果多个人在不同的时间获取同一个项目,可能安装的软件包的版本会不一样导致不可预知的错误,而通过package.json文件可以固化当前安装的每个软件包的版本,从而确保任何时间安装的版本都是一致的,从而避免了使用package。json文件时可能出现的版本不同的问题

  • 如果版本号是~0.13.0,如果有更新的内容,也只能更新补丁版本,也就是0.13.1可以,但是0.14.0就不行

  • 如果版本号是^0.13.0,如果有更新的内容,则补丁版本和次版本都可以更新,也就是0.13.1,0.14.0都可以

  • 如果版本号是0.13.0,则始终会使用0.13.0这个版本,就算有更新也不会变的

注意: 如果项目是公开的或者有合作者或者将Git作为部署源,在使用package.json文件时,需要将其提交到Git仓库,以便被其他人获取,这样在运行npm update时,这个文件中的依赖版本也会被更新

三、Node.js基础

(1)Node.js全局对象

==> 全局变量 <==

  • __filename: 表示当前正在执行的脚本的文件名,包含文件所在的绝对路径,但该路径和命令行参数所指定的文件名不一定相同,如果在模块中,则返回的值是模块文件的路径

  • __dirname: 表示当前执行的脚本所在的目录

console.log('当前文件名', __filename);
console.log('当前目录', __dirname);

image.png

==> 全局对象 <==

<== console对象的常用方法 ==>

console.log(): 向标准输出流打印字符并以换行符结束,该方法可以接收若干个参数,如果只有一个参数,则输出这个参数的字符形式,如果有多个参数,则类似C语言中的printf()命令的格式输出,在这个方法中,可以使用占位符输出变量,然后使用相关内容去替换这些占位符就可以了,其常用的占位符如下,以数字为例

  • %d: 输出数字变量
  • %s: 输出字符串变量
  • %j: 输出JSON变量
// 这里数字占位符是3个,然后后面给了三个数字,直接替换,
// 就得到了'10 + 10 = 30'
console.log('%d + %d = %d', 10, 10, 30);

// 这里是多了一个数字,多出来的内容会原样输出,
// 所以得到'10 + 10 = 30 40'
console.log('%d + %d = %d', 10, 10, 30, 40);

// 这里是少一个数字,没有匹配的数字变量也会原样输出,
// 所以得到'10 + 10 = 30 & %d'
console.log('%d + %d = %d & %d', 10, 10, 30);

image.png

console.time() 和 console.timeEnd(): 这两个方法用来记录程序的执行时间段,console.time()用来开始计时,其参数只是起到标识作用;console.timeEnd()用来结束计时,并输出程序运行所需的时间,它在显示结果的时候,会在标识参数后面自动添加以毫秒为单位的时间

注意: 程序的运行时间不是固定的,它与计算机的配置有关

console.time('计算10!运算所需的时间')

let ouptut = 1

for(let i = 1; i <= 10; i++) {
  ouptut *= i
}

console.timeEnd('计算10!运算所需的时间')

image.png

<== process对象 ==>

说明: 这个对象用于描述当前程序的状态,与console对象不同的是,这个对象只存在于Node.js中,在JavaScript中并不存在

常用属性:

属性描述
argv返回一个数组,由命令行执行脚本时的各个参数组成
env返回当前系统的环境变量
version返回当前Node.js的版本
versions返回当前Node.js的版本号以及依赖包
arch返回当前CPU的架构
platform返回当前运行程序所在的平台系统
ExecPath返回执行当前脚本的Node二进制文件的绝对路径
execArgv返回一个数组,成员是命令行下执行脚本时在Node可执行文件与脚本文件之间的命令行参数
exitCode进程退出时的代码,如果进程通过process.exit()退出,则不需要指定退出码
config一个包含用来编译当前Node执行文件的JavaScript配置选项的对象,它与运行./configure脚本生成的config.gypi文件相同
pid当前进程的进程号
ppid当前进程的父进程的进程号
title进程名
mainModulerequire.main的备选方法,不同点在于如果主模块在运行时改变,require.main可能会继续返回老的模块,可以认为,这两者引用了同一个模块

常用方法:

方法说明
exit([code])使用指定的code结束进程,如果忽略,将会使用code()
memoryUsage()返回一个对象,描述了Node进程所用的内存状况,单位为字节
uptime()返回Node已经运行的秒数

==> 全局函数 <==

说明: 这里的函数主要是定时器函数,下面的前四个的使用跟在JavaScript中使用没什么区别,主要说一下后面两个

  • setTimeout(cb, ms): 添加一个定时器,在指定的ms毫米之后执行cb函数

  • clearTimeout(t): 取消定时器,停止一个之前调用setTimeout()创建的定时器

  • setInterval(cb, ms): 添加一个定时器,每隔ms毫秒就执行一次cb函数

  • clearInterval(t): 取消定时器,停止一个之前调用setInterval()创建的定时器

  • setImmediate(callback[, ...args]): 安排在I/O事件的回调之后立即执行的callback;这里的I/O指的是输入/输出,通常指数据在内部储存器与外部储存器或其它周边设备之间的输入和输出

  • clearImmediate(immediate): 取消由serImmediate创建的Immediate对象

console.log('正常执行1')

setImmediate(() => {
  console.log('延迟执行的3');
})

console.log('正常执行2')

image.png

(2)模块化编程

说明: Node.js主要使用模块系统进行编程,所谓模块,是指为了方便调用功能,预先将相关方法和属性封装在一起的集合体,模块和文件是一一对应的,即一个Node.js文件就是一个模块,这个文件可以是JavaScript代码、JSON或者编译过的C/C++扩展等;其中常用模块化编程的对象是module对象exports对象

==> exports对象 <==

举例:

// 在until.js文件中定义取绝对值的方法

exports.abs = function(number) {
  if(0 < number) {
    return number;
  } else {
    return -number;
  }
}
// 然后在index.js中导入并使用
const until = require('./until')

console.log(until.abs(-10));

image.png

==> module对象 <==

常用属性:

属性说明
id模块的标识符,通常是完全解析后的文件名,默认输出
pathNode.js运行js模块所在的文件路径
exports公开的内容,也就是导出的对象,引入该模块会得到这个对象
filename当前模块文件名,包含路径
loaded模块是否加载完毕
parent当前模块的父模块对象
children当前模块的所有子模块对象

举例:

// 在until.js中定义一个构造函数并导出

function hello() {
  let name = "hello";

  this.setName = function (theName) {
    name = theName;
  };

  this.sayHello = function () {
    console.log("Hello " + name);
  };
}

module.exports = hello;
// 在index.js中导入并实例化使用

const hello = require('./until')

const Hello = new hello()
Hello.setName('zhangsan')
Hello.sayHello()

image.png

四、事件的监听与触发

(1)EventEmitter

说明: 在JavaScript中,通过事件可以处理许多用户的交互,比如鼠标的点击、键盘按键的按下等,在Node.js中也提供了类似的事件驱动,主要通过events模块实现,该模块提供了EventEmitter类,用于处理事件;在Node.js中,可以添加监听事件的对象都是继承自EventEmitter对象,后者提供用于处理Node.js事件的方法

// 导入events模块
const EventEmitter = require('events')

// 创建eventEmitter对象
const eventEmitter = new EventEmitter()

常用方法:

  • addListener(eventName, listener): 添加监听事件,将回调函数listener添加到名为eventName的事件监听器的末尾

  • on(eventName, listener): 添加监听事件,与addListener()方法等效

  • emit(eventName[, ...args]): 触发事件,它可以给回调函数传入不定量的参数;这个方法的返回值是一个布尔值,表示是否成功触发事件

  • setMaxListeners(limit): 设置可以监听的最大回调函数数量

  • removeListener(eventName, listener): 删除指定名称的监听事件和对应的事件回调函数

  • removeAllListeners([eventName]): 删除所有的监听事件

  • off(eventName, listener): 删除指定事件名称的监听事件,与removeListener()方法等效

  • once(eventName, listener): 添加单次监听事件

  • prependListener(eventName, listener): 可以将事件回调函数添加到监听器数组的开头

注意: EventEmitter对象的addListener()方法和on()方法都用来添加监听事件,它们的使用是等效的,实际上,on方法内部实现时调用了addListener()方法;在Node.js中推荐使用on()方法添加监听事件

举例:

// 导入events模块
const EventEmitter = require('events')

// 创建eventEmitter对象
const eventEmitter = new EventEmitter()

// 监听事件
eventEmitter.on('tick', () => {
  console.log('事件已经触发');
})

// 使用emit方法触发事件
eventEmitter.emit('tick')

image.png

(2)添加和触发监听事件

==> 添加监听事件 <==

<== on ==>

说明: 在使用on()方法向事件监听器数组中添加函数的时候,不会检查其是否已经被添加,也就是如果多次调用并传入相同的eventName与listener,会导致listener被重复添加多次

// 导入events模块
const EventEmitter = require('events')

// 创建eventEmitter对象
const eventEmitter = new EventEmitter()

// 监听事件
eventEmitter.on('tick', () => {
  console.log('事件已经触发1次');
})

eventEmitter.on('tick', () => {
  console.log('事件已经触发2次');
})

eventEmitter.on('tick', () => {
  console.log('事件已经触发3次');
})

// 使用emit方法触发事件
eventEmitter.emit('tick')

image.png

<== prependListener ==>

说明: 在默认情况下,事件监听器会按照添加的顺序依次添加,但如果想改变添加的顺序,可以使用prependListener()方法,该方法可以将事件回调函数添加到监听器数组的开头

// 导入events模块
const EventEmitter = require('events')

// 创建eventEmitter对象
const eventEmitter = new EventEmitter()

// 监听事件
eventEmitter.on('tick', () => {
  console.log('事件已经触发1次');
})

eventEmitter.prependListener('tick', () => {
  console.log('事件已经触发2次');
})

eventEmitter.on('tick', () => {
  console.log('事件已经触发3次');
})

// 使用emit方法触发事件
eventEmitter.emit('tick')

image.png

<== setMaxListeners ==>

说明: 如果为同一个事件添加的回调函数超过10个,程序可以正常运行,但是会在运行完毕后出现警告提示,为了避免这个警告提示,可以通过setMaxListeners方法设置可以监听的最大回调函数的数量

image.png

==> 添加单次监听事件 <==

说明: 使用前面的on()方法添加事件时,事件一旦添加就会一直存在,但如果遇到只想执行一次监听事件的情况,可以使用once方法,该方法用来将单词监听器listener添加到名为eventName的事件上,当eventName事件触发一次后,其监听器会被移除,导致下次就不能够再次触发了

// 导入events模块
const EventEmitter = require('events')

// 创建eventEmitter对象
const eventEmitter = new EventEmitter()

// 监听事件
eventEmitter.once('tick', () => {
  console.log('事件已经触发');
})

// 这里使用定时器想多次触发事件,但是结果只能触发一次
setInterval(() => {
  // 使用emit方法触发事件
  eventEmitter.emit('tick')     
})

image.png

==> 触发监听事件 <==

说明: 看前面的内容就知道这里说的是emit方法,前面使用的都是不携带参数的用法,而实际的开发过程,可能需要定义带参数的回调函数,这是使用emit方法触发监听事件时,传入响应个数的参数就好。

// 导入events模块
const EventEmitter = require('events')

// 创建eventEmitter对象
const eventEmitter = new EventEmitter()

// 监听事件
eventEmitter.on('tick', (arg) => {
  console.log('事件已经触发,其参数是', arg);
})

eventEmitter.on('tick', (...arg) => {
  console.log('事件已经触发,其参数是', arg);
})

  // 使用emit方法触发事件
eventEmitter.emit('tick', 1, '23')     

image.png

(3)删除监听事件

说明: 如果监听的事件不需要了,可以将它删除,对于removelistener来说,它每次只能删除一个事件,也就是说如果一个事件被添加多次,那么就需要多次调用该方法来进行删除,这显得很麻烦,此时,可以选择removeAlllisteners来进行一步到位

// 导入events模块
const EventEmitter = require('events')

// 创建eventEmitter对象
const eventEmitter = new EventEmitter()

const listener = () => {}

// 监听事件
eventEmitter.on('tick', listener)

// 输出删除事件前的监听事件名称
console.log(eventEmitter.eventNames());

eventEmitter.removeListener('tick', listener)

// 输出删除事件后的监听事件名称
console.log(eventEmitter.eventNames());

image.png

// 导入events模块
const EventEmitter = require('events')

// 创建eventEmitter对象
const eventEmitter = new EventEmitter()

const listener = () => {
  console.log('事件已经触发');
}

// 添加多次相同的监听事件
eventEmitter.on('tick', listener)
eventEmitter.on('tick', listener)
eventEmitter.on('tick', listener)


// 输出删除事件前的监听事件名称
console.log(eventEmitter.eventNames());

// 使用removeListener方法删除监听事件
eventEmitter.removeListener('tick', listener)
// 输出删除事件后的监听事件名称
console.log(eventEmitter.eventNames());

// 使用removeAllListeners方法删除所有监听事件
eventEmitter.removeAllListeners('tick')
// 输出删除事件后的监听事件名称
console.log(eventEmitter.eventNames());

image.png

  • 对于off方法,该方法实际上相当于removeListener方法的别名,也可以删除指定名称的监听事件,其使用方法与removeListener完全一样

五、util工具模块

说明: 这是Node.js内置的一个工具模块,它提供了常用方法的集合,这主要是为了弥补核心JavaScript功能过于精简的不足,其主要目的是满足Node.js内部API的需求

常用方法:

  • callbackify(async_function): 将async异步函数或者一个返回值为Promise的函数转换成遵循错误优先回调风格的函数

  • inherits(constructor, superConstructor): 实现对象间的原型继承

  • inspect(object[, showhidden[, depth[, colors]]]): 将任意对象转换为字符串,通常用于调试和错误输出

  • format(format, [...]): 格式化字符串

  • promisify(original): 传入一个遵循常见的错误优先回调风格的函数,然后返回一个返回值为Promise的函数

(1)格式化输出字符串

说明: 这里需要使用到format函数,用它来对字符串进行格式化,使用起来有点像前面提到的console.log();这个函数的参数format表示接受零个或者多个占位符的字符串,每个占位符都是一个以%字符开始,并最终被对应的参数转换的字符串值所取代。如果还不清楚看下面的例子可能就明白了

占位符类型:

占位符说明
%s指定字符串
%d指定数值
%i转换为整数
%f转换为小数
%j转换为JSON字符串
%o转换为具有通用JavaScript对象格式的字符串表现形式,与inspect()类似,它显示了完整的对象以及不可枚举的属性
%O与%o类似,但没有选项,它不包含不可枚举的属性
%%输出%

举例:

// 导入util工具模块
const Util = require('util')

// 创建一个构造函数生成一个对象
function Person() {
  this.name = '张三'
  this.age = 18
}

// 创建一个对象
const man = new Person()

console.log(Util.format('整数:%i', 26.01))
console.log(Util.format('小数:%f', '26.01'))
console.log(Util.format('百分数:%d%%', 26.01))
console.log(Util.format('对象格式化为json:%j', man))

image.png

(2)将对象转换为字符串

说明: 这里使用的是inspect()方法,用于将任意对象转换为字符串,这个方法通常用于调试和错误输出

关于参数:

  • object: 必须参数,用来指定一个对象

  • showHidden: 为true的时候,将会显示更多关于object对象的隐藏信息

  • depth: 表示最大递归层数,用于对象比较复杂时指定对象的递归层数

  • colors: 为true的时候,输出格式将会以ANSI颜色编码,通常用于在终端上显示更漂亮的效果

举例:

const util = require('util');

function Person() {
  this.name = '张三';
  this.age = 18;
}

const man = new Person();

// 用配置对象和直接使用参数都可以
console.log(util.inspect(man, {
  showHidden: true,
  depth: null,
  colors: true
}));

console.log(util.inspect(man, {
  showHidden: true,
  depth: null,
  colors: false
}));

console.log(util.inspect(man, true, null, true));

image.png

(3)实现对象间的原型继承

说明: 这里使用的是inherits方法,用于实现对象间的原型继承,这里只能继承原型上面的东西,其它的比如构造函数里面的方法属性啥的是不继承的

关于参数:

  • constructor: 要从原型继承的任何对象

  • superConstructor: 要继承的原型对象

// 创建一个原型对象bar和一个继承自bar对象的foo对象
const util = require('util');

function bar() {
  this.name = 'bar';
}

bar.prototype.sayName = function() {
  console.log('我是' + this.name);
}

function foo() {
  this.name = 'foo';
}

util.inherits(foo, bar);

const objBar = new bar();
const objFoo = new foo();

objBar.sayName();
objFoo.sayName();

image.png

(4)判断是否为指定类型的内置对象

说明: 这里使用的是types类型,通过调用该类型的一些方法可以为不同类型的内置对象提供类型检查,这个等有用到的时候看一下就好。

常用方法:

  • util.types.isAnyArrayBuffer(value): 判断value是否为内置的ArrayBuffer或者SharedArrayBuffer实例

  • util.types.isArrayBufferView(value): 判断value是否为ArrayBuffer视图的实例

  • util.types.isArrayBuffer(value): 判断value是否为内置的ArrayBuffer实例

  • util.types.isAsyncFunction(value): 判断value是否为异步函数

  • util.types.isBooleanObject(value): 判断value是否为布尔类型

  • util.types.isBoxedPrimitive(value): 判断value是否为原始对象

  • util.types.isDate(value): 判断value是否为Date的实例

  • util.types.isNumberObject(value): 判断value是否为Number对象

  • util.types.isRegExp(value): 判断value是否为一个正则表达式

  • util.types.isStringObject(value): 判断value是否为一个String对象

六、fs文件系统模块

说明: fs模块是Node.js中内置的一个文件系统模块,使用它可以对本地的文件以及文件夹进行操作

注意: fs模块提供对文件与目录进行操作的方法的时候,通常分别提供了同步和异步方法,其中,同步方法名通常是在异步方法名后面加了Sync后缀,但除了文件读写操作,一般默认使用异步方法

(1)操作文件内容

==> 检查文件 <==

说明: fs模块内置许多方法,用以对文件进行相关操作;具体使用的时候,有的方法如果发现文件不存在,可以创建文件,而有的方法不能,这时就会出现错误,为了避免这类错误,在对文件进行操作之前,可以使用access(path, mode, callback)来检测文件是否存在,也可以根据需要检查文件的可读可写等属性

参数:

  • path: 文件的路径

  • mode: 要执行的可访问性检查,默认值为fs.constants.F_OK,查看文件访问常量以获取可能的mode值,具体文件访问常量如下

  • callback: 回调函数,使用一个可能的错误参数进行调用,如果检查可访问性失败,则错误参数将是Error对象,常见的Error对象如下


文件访问常量说明
F_OK指示文件对调用进程可见的标识,这对于确定文件是否存在很有用,但没有说明rwx权限
R_OK指示文件可以被调用进程读取的标识
W_OK指示文件可以被调用进程写入的标识
X_OK指示文件可以被调用进程执行的标识,在Windows系统中等效于fs.constants.F_OK

Error对象值说明
EPERM操作不允许
ENOENT文件或者路径不存在
ESRCH进程不存在
EINTR系统调用中断
EIOI/O错误
ENXIO设备或地址不存在
EBIG参数列表过长
ENOEXEC执行格式错误
EBADF文件编号错误
ECHILD子进程不存在
EAGAIN重试
ENOMEM内存不足
EBUSY资源繁忙或者被锁定
EACCES拒绝访问
EFAULT地址错误
EEXIST文件已经存在
ENODEV设备不存在
ENOTDIR路径不存在
EISDIR是一个路径
EINVAL参数无效
ENFILE文件表溢出
EMFILE打开的文件过多
EFBIG文件太大
ENOSPC剩余空间不足
EROFS只读文件系统
ENOTEMPTY非空目录

举例:

const fs = require('fs');

// 检测当前文件夹下面的demo.txt文件是否存在
fs.access('./demo.txt', fs.constants.F_OK, (err) => {
    if (err) {
        console.log('文件不存在');
    } else {
        console.log('文件存在');
    }
})

image.png

image.png

  • 如果需要使用多个mode值,那么多个值之间使用|进行分隔

  • access方法不仅可以检测文件是否存在,也可以检测文件夹是否存在

==> 读取文件 <==

说明: fs模块为读取文件提供了两个方法,即readFile(file, encoding, callback)方法和readFileSync(file, encoding)方法,后者为同步读取,前者为异步读取

参数:

  • file: 文件名
  • encoding: 文件的编码格式
  • callback: 回调函数

举例:

image.png

image.png

const fs = require('fs');

// 异步读取
fs.readFile('demo.txt', 'utf8', (err, data) => {
    if (err) {
        console.log(err);
    } else {
        console.log(data);
    }
})

// 同步读取
const text = fs.readFileSync('demoSync.txt', 'utf8')
console.log(text);

image.png

  • 同步与异步的区别在于同步方法立即返回操作结果,在使用同步方法执行的操作结束前,不能执行后续代码;而异步方法将操作结果作为回调函数的参数进行返回,在方法调用之后,可以理解执行后续代码,在大多数情况下,应该调用异步方法,在很少的场景中,比如读取配置文件启动服务器的操作中,应该使用同步方法

==> 文件写入 <==

<== 覆盖内容 ==>

方法:

  • fs.writeFile(file, data[, options], callback)
  • fs.writeFileSync(file, data[, options])

参数:

  • file: 文件名或者文件描述符

  • data: 写入文件的内容,可以是字符串也可以是缓冲区

  • options: 可选参数,可以是以下内容

    • [ option ] encoding: 编码方式,默认为utf-8,如果data为缓冲区,则忽略encoding参数
    • [ option ] mode: 文件的模式,默认值为0o666
    • [ option ] flag: 文件系统标志,默认值为w
    • [ option ] signal: 允许中止正在进行的写入文件操作
  • callback: 回调函数

举例:

const fs = require('fs')

const data = '这是文件写入部分'

// 这里依然使用上面的demo.txt文件
fs.writeFile('demo.txt', data, 'utf-8', (error) => {
  if(error) {
    throw error
  }
});

image.png

  • 这里可以看见文件内容被覆盖掉了,也就是这两个写入的方法会将原始文件的内容覆盖,并且如果在写入内容的时候发现文件并不存在,则会自动创建文件并将内容写入进去
<== 追加内容 ==>

方法:

  • fs.appendFile(file, data[, options], callback)
  • fs.appendFileSync(file, data[, options])

参数:

  • file: 文件路径

  • data: 写入文件的内容

  • options: 可选参数,可以是以下内容

    • [ option ] encoding: 编码方式,默认为utf-8
    • [ option ] mode: 文件的模式,默认值为0o666
    • [ option ] flag: 文件系统标志,默认值为a
  • callback: 回调函数

举例:

const fs = require('fs')

const data = '\n这是文件追加部分'

// 这里依然使用上面的demo.txt文件,可以看到这里的效果
// 是添加而不是覆盖
fs.appendFile('demo.txt', data, 'utf-8', (error) => {
  if(error) {
    throw error
  }
});

image.png

(2)操作文件

==> 截断文件 <==

说明: 这里的截断指的是删除文件内的一部分内容,以改变文件的大小;为了完成这个任务,可以使用truncate(path[, len], callback)这个函数来实现

参数:

  • path: 用于指定要被截断的文件的完整路径以及文件名

  • len: 一个整数数值,用于指定被截断后的文件的大小,以字节为单位;让值为0的时候,表示文件的内容为空

  • callback: 用于指定截断文件操作完毕时执行的回调函数,该回调函数中使用一个参数,参数值为截断文件操作失败时触发的错误对象

// 依然以上面的demo.txt为例
const fs = require('fs')

// 使用stat方法查看文件的大小
fs.stat('demo.txt', (err, stats) => {
  console.log('截断前文件的大小为:' + stats.size + '字节');
})

// 截断文件
fs.truncate('demo.txt', 12, (err) => {
  if (err) {
    console.log('文件截断失败');
  } else {
    fs.stat('demo.txt', (err, stats) => {
      console.log('截断后文件的大小为:' + stats.size + '字节');
    })
  }
})

image.png

image.png

==> 删除文件 <==

函数:

  • fs.unlink(path, cabbback)

参数:

  • path: 用于指定被删除文件的路径

  • callback: 回调函数

举例:

image.png

const fs = require('fs');

// 这里删除上面的demo.txt文件
fs.unlink('demo.txt', (error) => {
  if(error) {
    console.log(error);
  }
  console.log('文件删除成功');
});

image.png

image.png

==> 复制文件 <==

函数:

  • fs.copyFile(src, dest[, mode], callback)
  • fs.copyFileSync(src, dest[, mode])

参数:

  • src: 要复制那个文件

  • dest: 将文件复制到那里

  • mode: 复制操作的修饰符,默认值为0

  • callback: 回调函数

const fs = require('fs');

// 依然使用上面的demo.txt文件
fs.copyFile('demo.txt','demo1.txt', (error) => {
  if(error) {
    console.log(error);
  }
  console.log('文件复制成功');
});

image.png

  • 当然,这里也可以使用前面的readFile()writeFile()来实现一样的效果,只不过这种是复制内容然后将其添加到文件中完成复制的任务,而这里是直接复制文件完成任务;同步的也是如此,都使用同步的方法就好了

==> 重命文件 <==

说明: 这里说的是重命名文件,可以使用rename(oldPath, newPath, callback)方法来完成;不过使用的时候要注意一下重命名文件的同时就会更改文件的路径

参数:

  • oldPath: 原文件名
  • newPath: 新文件名
  • callback: 回调函数

举例:

const fs = require('fs');

// 重命名上面的demo.txt文件
fs.rename('demo.txt','测试文件.txt', (error) => {
  if(error) {
    console.log(error);
  }
  console.log('重命名成功');
});

image.png

image.png

(3)操作文件夹

==> 创建文件夹 <==

函数:

  • fs.mkdir(path[, options], callback)
  • fs.mkdirSync(path[, options])

参数:

  • path: 要被创建的目录的完整路径以及目录名

  • options: 指定目录的权限,默认值为0777,表示任何人可读可写该目录

  • callback: 指定创建目录操作完毕时调用的回调函数,该回调函数只有一个参数,参数值为创建目录操作失败时触发的错误对象

举例:

滁州西涧
春夜喜雨
登鹳雀楼
凉州词
饮湖上初晴后雨
// 给上面的txt文件中的每一句创建一个txt文件
const fs = require('fs');

// 检测demo文件夹是否存在
fs.access('demo', fs.constants.F_OK, (err) => {
  if(err) {
    // 如果不存在就创建demo文件夹
    fs.mkdir('demo', (err) => {
      if(err) {
        console.log('创建demo文件夹失败');
      }
    })
  }

  // 如果存在就读取test.txt文件的内容
  let data = fs.readFileSync('test.txt', 'utf-8').split('\r')[0].replace(/\n/g, ',').split(',');

  // 然后给文件的每一句创建一个文件
  for(let i = 0; i < data.length; i++) {

    fs.writeFile("demo\\" + data[i] + ".txt", "", (err) => {
      if(err) {
        console.log('创建文件失败');
      }
    })
  }
})

image.png

  • 对于文件目录的创建需要一层一层来,不能一次创建多层,因为创建下一层目录的前提是上一层目录是存在的

==> 读取目录 <==

函数:

  • fs.readdir(path[, options], callback)
  • fs.readdirSync(path[, options])

参数:

  • path: 文件名或者文件修饰符

  • options: 可选参数,可以有以下值

    • [ options ] encoding: 编码方式
    • [ options ] flag: 文件系统标志
    • [ options ] signal: 允许中止正在进行的读取文件操作
  • callback: 回调函数,其参数有二,其中err表示出现错误时的错误信息,而data表示调用成功的返回值

举例:

// 读取上面的demo文件夹中的内容
const fs = require('fs');

fs.readdir('demo', (err, files) => {
    console.log(files);
})

image.png

==> 删除空目录 <==

函数:

  • fs.rmdir(path[, options], callback)
  • fs.rmdirSync(path[, options])

参数:

  • path: 用于指定要被删除目录的完整路径以及目录名

  • options: 可选参数,可以有以下值

    • [ options ] recursive: 如果为true,则执行递归目录删除操作,在递归模式下,操作将在失败时重试,默认值为false
    • [ options ] retryDelay: 重试之间等待的时间,以毫秒为单位,如果recursive选项不为true,则忽略此选项,将使用默认值100
    • [ options ] maxRetries: 表示重试次数,如果遇到EBUSY、EMFILF、ENFILF、ENOTEMPTY或者EPERM错误,Node.js将在每次尝试的时候,以retryDelay毫秒的线性退避等待时间重试该操作,如果recursive选项不为true,则忽略此选项,使用默认值0
  • callback: 用于指定删除目录操作完毕时调用的回调函数

举例:

// 如果删除的目录不为空,则会报错
const fs = require('fs');

fs.rmdir('demo', (err) => {
  if(err) {
    console.log(err);
  }
})

image.png

==> 查看目录信息 <==

函数:

  • fs.stat(path, callback): 查看目录或者文件信息
  • fs.lstat(path, callback): 查看链接文件信息

参数:

  • path: 指定要被查看的目录或者文件的完整路径

  • options: 指定查看目录或者文件操作完毕时调用的回调函数,函数的参数有两个,一个是err表示出现错误时的错误信息,另一个是stats对象,表示文件的相关属性

举例:

const fs = require('fs');

fs.stat('demo', (err, stats) => {
  console.log(stats);
})

image.png

属性说明:

属性说明
dev文件或者目录所在的设备ID
mode文件或者目录的权限
nlink文件或者目录的硬链接数量
uid文件或者目录的所有者的用户ID
gid文件或者目录所属组的数字标识
rdev字符设备文件或者块设备文件所在的设备ID
blksize文件或者目录中I/O操作的块大小(以字节为单位)
ino文件或者目录的索引编号
size文件或者目录的大小(即文件中的字节数)
blocks分配给文件或者目录的块数
atimeMs最后一次访问文件或者目录时的时间戳(以毫秒为单位)
mtimeMs最后一次修改文件或者目录时的时间戳(以毫秒为单位)
ctimeMs最后一次更改文件或者目录时的时间戳(以毫秒为单位)
birthtimeMs创建文件或者目录时的时间戳(以毫秒为单位)
atime上次访问文件或者目录时的时间戳(以毫秒为单位)
mtime上次修改文件或者目录时的时间戳(以毫秒为单位)
ctime上次更改文件或者目录时的时间戳(以毫秒为单位)
birthtime文件或者目录创建时的时间戳(以毫秒为单位)

stats常用方法:

方法说明
isFile()判断是否为文件
isDirectory()判断是否为目录
isBlockDevice()判断是否为块设备
isCharacterDevice()判断是否为字符设备
isFIFO()判断是否为FIFO储存器
isSocket()判断是否是socket协议

==> 获取绝对路径 <==

函数:

  • fs.realpath(path[, options], callback)
  • fs.realpathSync(path[, options])

参数:

  • path: 路径,可以为字符串或者url

  • options: 一般为encoding,用于指定编码格式,默认为utf-8

  • callback: 回调函数,函数有两个参数,一个是err表示发生错误时的错误信息,一个是resolvedPath表示绝对路径

举例:

const fs = require('fs');

fs.realpath('demo', (err, resolvedPath) => {
  console.log(resolvedPath);
})

image.png

七、os操作系统模块

(1)内存相关

==> 获取系统剩余内存 <==

说明: 可以使用freemem()方法获取空闲的系统内存量,该方法返回一个整数,其单位是字节

const os = require('os');

console.log('剩余内存为:' + os.freemem());

image.png

==> 获取系统总内存 <==

说明: 可以使用totalmen()方法获取系统总内存量,这个方法也会返回一个整数,其单位也是字节

const os = require('os');

console.log('总内存为:' + os.totalmem());

image.png

  • 上面运行的结果跟自己的计算机当前使用的状态有关,这会导致多次运行的结果不一样,这都是正常现象

(2)网络相关

说明: 可以使用networkInterfaces()方法获取计算机的网络信息,这个方法的返回值是一个对象,该对象包含已分配了网络地址的网络接口信息,其包含的相关属性如下

网络接口信息说明
address一个字符串,用于指定分配的网络地址,也就是IPv4或者IPv6
netmask一个字符串,指定IPv4或者IPv6网络掩码
family指定Family字符串,值为IPv4或者IPv6之一
mac一个字符串,指定网络接口的MAC地址
internal布尔值,如果网络接口是不可远程访问的环回接口或者类似接口,则为true,否则为false
scopeid一个数字,指定IPv6的作用域ID
cird一个字符串,用于指定分配的IPv4或者Ipv6地址以及CIDR表示法中的路由前缀,如果网络掩码无效,则将其设置为null
const os = require('os');

console.log(os.networkInterfaces());

image.png

(3)系统目录相关

==> 获取用户主目录 <==

说明: 可以使用homedir()方法获取当前用户的主目录,其返回值类型为字符串,表示当前用户的主目录

const os = require('os');

console.log(os.homedir());

image.png

==> 获取临时文件目录 <==

说明: 可以使用temdir()方法获取本地计算机的临时文件目录,其返回值类型为字符串,表示默认的临时文件目录

const os = require('os');

console.log(os.tmpdir());

image.png

(4)系统相关

说明: 可以使用下面这些方法来获取与操作系统相关的信息

  • hostname(): 返回操作系统的主机名

  • type(): 返回操作系统名

  • platform(): 返回编译时的操作系统名

  • arch(): 返回操作系统的CPU架构

  • release(): 返回操作系统的发行版本

  • version(): 返回标识操作系统内核版本的字符串

  • cpus(): 返回一个对象数组,其中包含各CPU内核信息,每个对象都包含如下三个属性

    • [ cpus options ] model: 一个字符串,表示CPU内核的型号
    • [ cpus options ] speed: 一个整数,以兆赫兹为单位,表示CPU内核速度
    • [ cpus options ] times: 一个对象,包含如下属性
      • [ time options ] user: 表示CPU在用户模式下花费的毫秒数
      • [ time options ] times: 表示CPU在正常模式下花费的毫秒数
      • [ time options ] times: 表示CPU在系统模式下花费的毫秒数
      • [ time options ] times: 表示CPU在空闲模式下花费的毫秒数
      • [ time options ] times: 表示CPU在正中断请求模式下花费的毫秒数
  • uptime(): 返回操作系统的运行时间

  • getPriority([pid]): 获取指定进程的调度优先级,参数为指定进程的PID,如果省略pid或者pid为0,该方法会返回当前进程的优先级;对于进程的PID值的查看,可以在任务管理器的详细信息选项卡中查看

  • setPriority([pid,] priority): 为指定的进程设置调度优先级,其中PID表示指定进程的PID,如果省略pid或者pid为0,则表示当前进程;priority为分配给该进程的调度优先级,该值的范围是-20 - 19的整数

(5)常用属性

属性:

  • os.EOL: 操作系统特定的行末标志,在POSIX上是\n,在Windows上是\r\n

  • os.constants: os常量列表,包含信号常量、错误常量、dlopen常量、优先级常量以及libvu常量,如果要查看某一类常量列表,可以使用以下属性值

    • [ constants options ] os.constants.signals: 信号常量列表
    • [ constants options ] os.constants.errno: 错误常量列表
    • [ constants options ] os.constants.dlopen: dlopen常量列表
    • [ constants options ] os.constants.priority: 优先级常量列表

注意: os模块中的libvu常量无法单独查看,需要通过constants属性查看,该常量仅包含UV_UDP_REUSEADDR这一项

八、I/O流操作

说明: I/O流,也就是输出、输入流,它提供了一种向储存设备写入数据和从储存设备中读取数据的方式,它是Node.js中执行读写文件操作的一种非常重要的方式,在开始之前,先了解一下流和Buffer的概念

  • 流: 程序中的流是一个抽象的概念,当程序需要从某个数据源读取数据的时候,就会开启一个数据流,数据源可以是文件、内存或者网络等,而当程序将数据写入某个数据源的时候,也会开启一个数据流,而数据源的目的地也可以是文件、内存或者网络等

  • Buffer: 一个Buffer就是开辟一块内存区域,Buffer的大小就是开辟的内存区域的大小,在流中,Buffer的大小取决于传入流构造函数的highWaterMark选项参数,该参数指定了字节总数或者对象总数,当可读缓冲区的总大小达到highWaterMark指定的阈值时,流会暂时停止从数据源读取数据,直到当前缓冲区的数据被释放

(1)可读流

==> 读取模式与状态 <==

读取模式:

  • 可读流有两种读取模式,分别是流动模式暂停模式

  • 所有可读流都是从暂停模式开始,当流处于暂停模式时,可以通过read()方法从流中按需读取数据

  • 而可读流的流动模式指的是一旦开始读取文件,会按照highWaterMark的值依次读取,直到读取完为止,当流处于流动模式的时候,数据是持续变化的,所以需要使用监听事件来进行处理

  • 暂停模式和流动模式之间是可以相互切换的,比如通过添加data事件、使用resume()方法或者pipe()方法等都可以将可读流从暂停模式切换为流动模式;使用paused()方法或者unpipe()方法可以将可读流从流动模式切换为暂停模式

状态:

  • 在实际使用可读流的时候它一共有三种状态,分别是初始状态流动状态非流动状态

  • 当流处于初始状态的时候,由于没有数据使用者,所以流不会产生数据,这时如果监听data()事件、调用pipe()方法或者resume()方法,都会将当前状态切换为流动状态,这样可读流即可开始主动地产生数据并触发事件

  • 如果调用pause()方法或者unpipe()方法,就会将可读流的状态切换为非流动状态,这将暂停流,但不会暂停数据的产生,此时如果再为data()事件设置监听器,就不会再将状态切换为流动状态了

==> 创建可读流的方式 <==

方式:

  • new stream.Readable()
  • fs.createReadStream(path[, options])

相关参数:

  • path: 读取文件的文件路径,可以是字符串、缓冲区或者网址

  • options: 读取文件时的可选参数,如下

    • [ options ] flags: 指定文件系统的权限,默认值为r
    • [ options ] encoding: 编码方式,默认为null
    • [ options ] fd: 文件描述符,默认为null
    • [ options ] mode: 设置文件模式,默认为0o666
    • [ options ] autoClose: 文件出错或者结束时,是否自动关闭文件,默认值为true
    • [ options ] emitClose: 流销毁时,是否发送close事件,默认为true
    • [ options ] start: 指定开始读取的位置
    • [ options ] end: 指定结束读取的位置
    • [ options ] highWaterMark: 可读取阈值,一般设置在16 - 100KB的范围内

举例:

const stream = require('stream')
const fs = require('fs')

// 这两种方法都可以
const readable = new stream.Readable()
const read = fs.createReadStream('test.txt')

==> 属性、方法和事件 <==

常用属性:

  • destoryed: 可读流是否已经被销毁,如果已经调用了destory()方法,则该属性值为true

  • readable: 可读流是否被破坏、报错或者结束

  • readableEncoding: 获取可读流的encoding属性

  • readableEnded: 可读流是否已经没有数据,如果触发了end事件,则该属性值为true

  • readableFlowing: 可读流的当前状态

  • readableHighWaterMark: 构造可读流时传入的highWaterMark的值

  • readableLength: 获取准备读取的队列中的字节数或者对象数

常用方法:

  • read([size]): 从流中读取数据

  • setEncoding(encoding): 设置从流中读取的数据所使用的编码

  • pause(): 暂停从可读流对象发出的data事件

  • isPause(): 获取可读流的当前操作状态

  • destory(): 销毁可读流

  • resume(): 恢复从可读流对象发出的data事件

  • pipe(destination[, options]): 把可读流的输出传输到一个由destination参数指定的可写流对象中去

  • unpipe([destination]): 分离附加的可写流对象

  • filter(fn[, options]): 筛选流

  • forEach(fn[, options]): 迭代遍历流

常用事件:

  • close: 当流或者其数据源被关闭的时候,触发该事件

  • data: 在流将数据块传递给使用者后触发

  • end: 当流中没有数据可供使用时触发

  • error: 当流由于底层内部故障而无法生产数据或者尝试推送无效数据块时触发

  • pause: 当调用pause()方法并且readsFlowing不为false时触发

  • readable: 当有数据可以从流中读取时触发

  • resume: 当调用resume()方法并且readsFlowing不为true时触发

==> 常见操作 <==

<== 读取数据 ==>

方法:

  • 可读流对象.read([size]): 这里的参数size表示需要读取的数据的字节数;函数的返回值可能是字符串、Buffer或者是null,所以需要注意返回值为null的情况

举例:

const fs = require('fs')

const read = fs.createReadStream('静夜思.txt')

read.on('readable', (chunk) => {
  // 这里排出读取数据块的值是null的情况
  while (null !== (chunk = read.read())) {
    console.log(chunk.toString())
  }
})

image.png

<== 设置编码格式 ==>

说明: 在可读流读取数据的时候,默认情况下没有设置字符编码,流数据返回的是Buffer对象,如果设置了字符编码,则流数据返回指定编码的字符串;在设置可读流中的数据编码格式的时候需要使用setEncoding(encoding)方法,其中的参数encoding用来设置编码格式

举例:

const fs = require('fs')

const read = fs.createReadStream('静夜思.txt')

// 默认返回Buffer对象
read.on('readable', (chunk) => {
  while (null !== (chunk = read.read())) {
    console.log(chunk)
  }
})

image.png

const fs = require('fs')

const read = fs.createReadStream('静夜思.txt')
// 设置编码格式
read.setEncoding('utf8')

read.on('readable', (chunk) => {
  while (null !== (chunk = read.read())) {
    console.log(chunk)
  }
})

image.png

<== 暂停与恢复流 ==>

方法:

  • 可读流对象.pause(): 可以使流动模式的可读流停止触发data事件,并切换为非流动模式

  • 可读流对象.resume(): 可以恢复从可读流对象发出data事件,将可读流切换为流动模式

举例:

const fs = require('fs')

const read = fs.createReadStream('静夜思.txt', {
  highWaterMark: 16
})
read.setEncoding('utf8')

// 使用pause()方法在读取每行数据后暂停,然后每隔1秒使用resume()方法恢复数据流的读取
read.on('data', (chunk) => {
  console.log(chunk.toString());
  read.pause()
  setTimeout(() => {
    read.resume()
  }, 1000)
})

read.on('end', () => {
  console.log('读取完毕')
})

image.png

<== 获取流的运行状态 ==>

方法:

  • 可读流对象.isPaused(): 判断流当前的操作状态,该方法不需要参数,返回结果为true或者是false;这个方法主要用于pipe()底层的机制,大多数情况下不会直接使用这个方法
const fs = require('fs')

const read = fs.createReadStream('静夜思.txt')
console.log(read.isPaused())
read.pause()
console.log(read.isPaused())

image.png

<== 销毁流 ==>

方法:

  • 可读流对象.destory([error]): 参数error用于在处理错误事件时发出错误;由于可读流在销毁后,会将读取的数据清空,所以通常在程序可能出现异常时,才会在处理异常的过程中使用这个方法
const fs = require('fs')

const read = fs.createReadStream('静夜思.txt')

read.on('data', (chunk) => {
  console.log(chunk.toString());
  read.pause()
  setTimeout(() => {
    read.resume()
  }, 1000)
})

read.on('end', () => {
  console.log('读取完毕')
})

read.destroy()

image.png

==> 绑定可写流至可读流 <==

方法:

  • 可读流对象.pipe(destination[, options]): 可以将可写流绑定到可读流,并将可读流自动切换到流动模式,同时将可读流的所有数据推送到绑定的可写流

参数:

  • destination: 要绑定到可读流的可写流对象

  • options: 保存管道选项,通常为end参数,其参数值为true,表示如果可读流触发end事件,可写流也调用end()方法结束写入,如果参数值为false,则目标流就会保持打开

注意:

  • 关于pipe: pipe的含义是管道,比如要从A桶中向B桶倒水,如果直接使用A桶来倒水,那么水流可能忽大忽小,而B桶中的水有可能因为溢出或者来不及使用而浪费,那么如何让水不浪费呢?此时就可以还用一根水管连接A桶和B桶,这样A桶中的水可以通过管道匀速的流向B桶,B桶中的水就可以及时使用而不会造成浪费,流中的pipe也是如此,它是连接可读流和可写流的一条管道,可以实现读取数据和写入数据的一致性,这里需要注意的是,pipe是可读流的方法,只能将可写流绑定到可读流,反过来却是不可以的

举例:

const fs = require('fs')

const write = fs.createWriteStream('静夜思.txt', {flags: 'a'})
// 就多加一句‘这首诗的作者是李白’
const read = fs.createReadStream('说明.txt')

read.pipe(write)

image.png

image.png

==> 解绑可写流 <==

说明: 使用pipe([destination])方法将已经绑定的可写流进行解绑,这个方法中的destination是一个可选参数,表示要解绑的可写流,如果该参数省略,则会解绑所有的可写流

举例:

const fs = require('fs')

const write = fs.createWriteStream('静夜思.txt', {flags: 'a'})
const read = fs.createReadStream('说明.txt')

read.pipe(write)
read.unpipe(write)

image.png

注意:由于解绑可写流操作会将已经绑定至可读流的可写流清除,所以为了保持数据的一致性,通常在写入或者追加操作出现异常的时候,才使用unpipe()方法对可写流执行解绑操作

(2)可写流

==> 创建可写流的方式 <==

方式:

  • new stream.Writable()
  • fs.createWriteStream(path[, options])

相关参数:

  • path: 写入文件的文件路径

  • options: 写入文件时的可选参数,如下

    • [ options ] flags: 指定文件系统的权限,默认值为w,如果要修改文件内容,而不是替换,需要将该值设置为a
    • [ options ] encoding: 编码方式,默认为null
    • [ options ] fd: 文件描述符,默认为null
    • [ options ] mode: 设置文件模式,默认为0o666
    • [ options ] autoClose: 文件出错或者结束时,是否自动关闭文件,默认值为true
    • [ options ] emitClose: 流销毁时,是否发送close事件,默认为true
    • [ options ] start: 指定开始写入的位置
    • [ options ] end: 指定结束写入的位置
    • [ options ] highWaterMark: 可读取阈值,一般设置在16 - 100KB的范围内

==> 属性、方法和事件 <==

常用属性:

  • destoryed: 可写流是否已经被销毁,如果已经调用了destory()方法,则该属性值为true

  • writable: 可写流是否被破坏、报错或者结束

  • writableEnded: 可写流是否已经没有数据,如果触发了end事件,则该属性值为true

  • writableCorked: 获取完全uncork流需要调用uncork()的次数

  • writableFinished: 可写流中的数据是否已经传输完,在触发finish事件之前需要将其设置为true

  • writableHighWaterMark: 返回构造可写流时传入的highWaterMark的值

  • writableLength: 获取准备写入的队列中的字节数或者对象数

  • writableNeedDrain: 如果流的缓冲区已满并且流将发出drain,则这个值为true

常用方法:

  • write(): 写入数据

  • end(): 通知可写流对象写入结束

  • setDefaultEncoding: 为可写流设置默认的编码方式

  • destory(): 销毁可写流

  • cork(): 强制把所有写入的数据都缓冲到内存中去

  • uncork(): 将调用cork()方法使缓冲的所有数据输出到目标

常用事件:

  • close: 当流或者其数据源被关闭的时候,触发该事件

  • open: 创建可写流的同时会打开文件,而打开文件就会触发该事件

  • drain: 当写入缓冲区为空时触发该事件

  • error: 写入或者管道数据发生错误时触发该事件

  • finish: 调用end()方法且缓冲区数据都已经传递给底层系统之后触发该事件

  • pipe: 当在可读流上调用pipe()方法时会触发该事件,并将此可写流添加到其目标集内

  • unpipe: 当在可读流上调用unpipe()方法时会触发该事件,并将此可写流移除到其目标集内

==> 常见操作 <==

<== 写入数据 ==>

方法:

  • 可写流对象.write(chunk[, encoding, callback])

参数:

  • chunk: 要写入的数据,其值可以是字符串、缓冲区或者是数组等

  • encoding: 可选参数,表示写入数据时的编码格式

  • callback: 可选参数,是一个回调函数,写入数据完成后执行

举例:

const fs = require('fs');

const text = '这首诗写的真不错'

const poem = fs.createWriteStream('静夜思.txt', {
  // 在源文件后面追加内容,需要将文件权限设置为a
  flags: 'a' 
})
poem.write('\n评论:' + text, 'utf8')

image.png

<== 设置默认编码格式 ==>

说明: 可以使用setDefaultEncoding(encoding)方法来设置可写流的默认编码方式,其中的encoding表示要设置的编码方法;在使用上跟可读流的操作是一样的,这里就不再举例

<== 关闭流 ==>

方法:

  • 可写流对象.end(chunk[, encoding][, callback]): 这个方法可以用来标识已经没有需要写入流中的数据了,因此通常用来关闭流

参数:

  • chunk: 可选参数,表示关闭流之前要写入的数据

  • encoding: 如果chunk为字符串,那么encoding为编码格式

  • callback: 流结束或者报错时的回调函数

举例:

const fs = require('fs');

const text = '这首诗写的真不错'

const poem = fs.createWriteStream('静夜思.txt')
poem.write('测试数据')
poem.end('完成写入')

image.png

  • 使用end()方法关闭流后,无法再向流中写入数据,否则将会产生异常
<== 销毁流 ==>

方法:

  • 可写流对象.destory([error]): 使用这个方法可以销毁所创建的写入流,并且流被销毁以后,无法再向流中写入数据;其中的参数error是可选参数,表示使用error事件触发的错误

举例:

const fs = require('fs');

const text = '这首诗写的真不错'

const poem = fs.createWriteStream('静夜思.txt')
poem.write('测试数据')

// 虽然使用了write方法写入了数据,但由于紧接着销毁了写入流,
// 这将导致使用该流执行的任何操作都会失效,因此,在使用写入
// 流销毁操作的时候,通常在异常处理中使用该操作
poem.destroy()

image.png

  • 一旦流被销毁,就无法对其进行任何操作,并且销毁流时,使用write()方法写入的数据可能并没有完成使用,这可能触发ERR_STREAM_DESTROYED错误,因此如果数据在关闭之前需要刷新,建议使用end()方法而不是destory()方法
<== 将数据缓冲到内存 ==>

说明: 使用写入流的cork()方法可以强制把所有写入的数据都缓存到内存中,它的主要目的是为了适应将几个数据快速连续的写入流的情况,cork()方法不会立即将它们转发到底层目标处,而是缓存所有数据块,直到调用uncork()方法,当使用uncork()方法或者是end()方法的时候,缓冲区数据都将被刷新

举例:

const stream = require('stream');

const write = new stream.Writable({
  // 自定义一个方法,这个方法会在数据被写入流的时候调用,
  // 其中chunk是写入的数据,encoding是编码格式,
  // next是回调函数,在next()被调用之前,不要执行任何会
  // 阻塞事件循环的操作
  write: (chunk, encoding, next) => {
    console.log(chunk.toString());
    next();
  }
})

write.write('天气晴朗');
// 这个方法会阻止流立即输出所有缓冲的数据,而是将它们累积起来,
// 直到uncork()或end()方法被调用。
write.cork()
// 所以这一行不会被打印出来
write.write('风和日丽');

image.png

<== 输出缓冲后的数据 ==>

说明: 可以使用uncork()方法将调用cork()方法后缓存的所有数据输出到目标处

举例:

// 继续在上面例子的最后一行写一句这个
write.uncork()

image.png

九、Web应用构建基础

(1)url模块

作用: 主要用于对URL地址进行解析

常用方法:

  • parse(): 将URL字符串转换成URL对象

  • format(urlObj): 将URL对象转换成URL字符串

  • resolve(from, to): 为URL插入或者替换原有的标签

举例:

const url = require('url');

const paresdObject = url.parse('https://juejin.cn/editor/drafts/7428815747287367706')
console.log(paresdObject);

image.png

属性说明:

  • protocol: 协议

  • slashes: 是否含有协议的 "//"

  • auth: 认证信息,如果有密码,为username:passwd,否则为null

  • host: IP地址、域名或主机名

  • port: 端口,如果是默认的8080则不显示

  • hostname: 主机名字

  • hash: 锚点值

  • search: 查询字符串参数,包含?

  • query: 查询字符串参数,不包含?

  • pathname: 访问的资源路径名

  • path: 访问的资源路径

  • href: 完整的URL地址

(2)querystring模块

作用: 用于实现URL参数字符串与参数对象之间的相互转换

常用方法:

  • stringify(): 将对象转换成URL查询字符串,这个对象一般是JSON对象

  • parse(): 将URL查询字符串转换成对象,这个对象一般是JSON对象

举例:

const url = require('url');
const querystring = require('querystring');

const paresdObject = url.parse('https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=%E4%BB%80%E4%B9%88%E6%98%AFnodejs&fenlei=256&rsv_pq=0xee7e84aa020d87c3&rsv_t=ccad6mKFn%2FdQkBypv6G4xKL6p8HVejDycaYreJkgoEzvRotAtgckEIsD1Y9Q&rqlang=en&rsv_enter=1&rsv_dl=tb&rsv_sug3=17&rsv_sug1=15&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&prefixsug=%25E4%25BB%2580%25E4%25B9%2588%25E6%2598%25AFnodejs&rsp=4&inputT=5769&rsv_sug4=7439')
// 使用parse()方法获取URL中的查询字符串
console.log(querystring.parse(paresdObject.query));

image.png

(3)http模块

说明: http模块允许Node.js通过HTTP传输数据,它使开发Web应用变得更加容易,对于http模块来说,它主要有server对象、response对象和request对象

==> server对象 <==

说明: server对象用来创建一个服务。在Node.js中,使用http模块中的createServer()方法,可以创建一个server对象。

常用方法:

  • listen(port): 启动服务器;其中port参数表示端口,端口是计算机与计算机之间的信息通道,计算机的端口从0开始,一共有65535个

  • close(): 关闭服务器

举例:

const server = require('http').createServer();

server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

// 1s后使用close方法关闭服务器
setTimeout(() => {
  server.close();
  console.log('Server is closed');
}, 1000)

image.png

==> response对象 <==

作用: 输出响应内容,发送给客户端

常用方法:

  • writeHead(statusCode[, statusMessage][, headers]): 返回响应头信息
  • end([data][, encoding]): 返回响应内容

参数:

    • [ writeHead ] statusCode: 接受数字类型的状态码;状态码有五类,分别是1xx(处理中)、2xx(成功)、3xx(重定向)、4xx(客户端错误)和5xx(服务器错误)
    • [ writeHead ] statusMessage: 接受任何显示状态消息的字符串
    • [ writeHead ] headers: 接受任何函数、字符串或者参数
    • [ end ] data: 执行完毕后要输出的字符串
    • [ end ] encoding: 对应data的字符编码

举例:

const http = require('http');

const server = http.createServer((request, response) => {
  response.writeHead(200, {
    'content-type': 'text/html'// 用来显示图片文件或者是视频文件
    // 'content-type': 'image/jpeg'
  })
  response.end('<h1>Hello World</h1>')
})

server.listen(3000, () => {
  console.log('服务器监听地址是http://127.0.0.1:3000')
})

image.png

  • 在平时的开发过程中,对于end()方法中使用的内容并不单单是字符串,也可以是其它的,比如HTML文件、多媒体文件等,只不过要显示这样的内容需要更改一下content-type的值就好

  • 有时候也能碰到网页自动跳转的需求,此时需要使用Location属性,这个属性后面接需要跳转的网址;需要注意这种操作的状态码不再是2XX了,而是3XX表示重定向

==> request对象 <==

说明: request对象表示了HTTP请求,包含了请求查询字符串、参数、内容、HTTP头等属性,其常用属性如下

属性说明
method返回客户端请求方法,而http请求有两种,分别是post请求和get请求,get请求是用来获取数据的,post请求是用来提交数据的;分辨这两种请求最直观的方式就是get请求的参数在URL中,而post请求通过请求体传递参数
url返回客户端请求url
headers返回请求信息头,可能包含以下值;content-type表示请求携带的数据类型、accept表示客户端可接受文件的格式类型、user-agent表示客户端相关信息、content-length表示文件的大小和长度、host表示主机地址
trailers返回请求网络
httpVersion返回HTTP协议版本

(4)path模块

==> 绝对路径和相对路径 <==

绝对路径: 这是指文件在计算机本地的实际路径,比如D:/images/a.png这种;在使用绝对路径的时候需要确保文件路径与实际位置一致,比如开发项目的时候,使用绝对路径引用了图片,项目完成后,如果将该项目部署到服务器上,要想访问使用了绝对路径的图片,则服务器上必须存在与之相符的路径。否则图片将无法显示,比如项目中使用了D:/images/a.png这个绝对路径,那么在部署项目的服务器上也必须存在D盘,并且D盘中需要存在images文件夹,该文件夹中必须存在a.png文件,这样可以看出使用绝对路径有很大的局限性

相对路径: 相对路径是指当前文件所在路径与其它文件或者文件夹的路径关系,其使用方法有以下的三种,以下面这个结构来进行说明

image.png

  • 目标文件与当前路径同级: 此时访问格式为”.\“ + 目标文件名称,其中”.\“可以省略;比如上面的在2.txt中使用3.txt,可以直接使用.\3.txt或者是3.txt

  • 目标文件位于当前路径下一级: 此时访问格式为目标文件的父文件夹名称 + ”\\“ + 目标文件名称;比如上面的想在1.txt中使用2.txt,由于1.txt与2.txt的父文件夹test位于同一级,因此需要使用test\2.txt来访问;然后每多下一级就多一层”\\“

  • 目标文件位于当前路径上一级: 此时访问格式为”..\\“ + 目标文件名称,比如上面的想在2.txt中使用1.txt的内容,由于2.txt的父文件夹test与1.txt位于同一级,因此需要使用..\1.txt来访问;然后每多下一级就多一层”..\\“

==> 常用方法 <==

  • dirname(p): 用于获取文件所在的目录,其中参数p表示字符串,用于表示包含文件名的路径

  • basename(p[, ext]): 获取指定路径中的文件名,其中的参数p表示路径字符串,ext是可选参数,表示文件的扩展名

  • extname(p): 获取文件扩展名,其中参数p为路径字符串

  • parse(p): 用于将指定路径解析为一个路径对象,这个路径对象包含以下属性,分别是root(路径所属的盘符)、dir(路径所属的文件夹)、base(路径对应的文件名)、ext(路径对应的文件扩展名)、name(文件对应的文件名称,不包含扩展名)

  • format(pathObject): 这个方法可以将路径对象转换为路径字符串,其中参数pathObject表示路径对象,再为这个对象添加属性的时候,如果同时出现dir属性和root属性,则忽略root属性;当存在base属性的时候,会忽略name属性和ext属性

  • isAbsolute(p): 用于判断路径是否为绝对路径,是就会返回true,不是就会返回false

  • resolve([...paths]): 可以将一个或者多个路径解析为绝对路径;在使用这个方法的时候,会使用系统分隔符从左到右将其拼接起来将其构成绝对路径,如果无法构成,则会将拼接好的字符串与当前的工作目录进行拼接来得到一个绝对路径;当然,也可以不传递参数,则会返回当前的工作路径

  • join([...path]): 使用平台特定的路径分隔符将多个路径拼接成一个路径字符串

十、WebSocket网络编程

说明: 对于WebSocket,这是一个从HTML5开始提供的一种浏览器与服务器之间进行通信的网络技术,使用WebSocket的时候,浏览器和服务器只需要做一个握手的动作,然后浏览器与服务器之间就形成了一条快速通道,两者之间就可以直接进行数据的互相传送了

(1)基本实现

说明: 在Node.js中主要通过socket.io模块进行WebSocket网络编程,该模块提供了服务器端和客户端相关的组件,可以很方便的为应用加入WebSocket支持;而在使用socket.io模块进行WebSocket网络编程的时候,通常需要三个步骤,分别是创建Websocket服务器、创建Websocket客户端和服务器端和客户端的通信

==> 服务器端实现 <==

步骤: 首先借助http模块的createServer()方法创建一个Web服务器,然后导入socket.io模块,并在WebSocket服务器构造函数中传入创建的Web服务器,从而创建一个WebSocket服务器,最后监听connection事件,判断是否有客户端链接

相关事件:

  • connection: 客户端成功连接到服务器
  • disconnect: 客户端断开连接
  • message: 捕获客户端发送的消息
  • error: 发送错误

举例:

const fs = require('fs');
const http = require('http');

// 创建web服务器
const server = http.createServer((request, response) => {
  if(request.url === '/') {
    // 读取客户端文件
    fs.readFile('index.html', (err, data) => {
      response.end(data);
    })
  }
})

// 监听端口
server.listen(3000, () => {
  console.log('监听地址为:http://127.0.0.1:3000/');
})

// 创建Websocket服务器
let io = require('socket.io');
io = io(server)

// 监听客户端连接(如果有客户端链接,就会触发此处的回调函数)
io.sockets.on('connection', (socket) => {
  console.log('客户端连接成功');
})

image.png

==> 客户端实现 <==

说明: 在创建WebSocket客户端的时候,需要加载socket.io客户端代码文件,也就是socket.io.js文件,然后通过socket.io模块中的全局对象io的connect(url)方法来向服务器端发起链接请求;这里connect方法中的url参数是可选参数,表示要连接的WebSocket服务器地址,当然,这个地址也可以是WebSocket服务器的http完整地址、也可以是相对地址,如果省略,则表示默认连接当前路径

相关事件:

  • connect: 成功连接到服务器
  • connecting: 正在连接
  • disconnect: 断开连接
  • connect_failed: 连接失败
  • error: 连接错误
  • message: 捕获服务器端发送的信息
  • reconnect_failed: 重新连接失败
  • reconnect: 重新连接成功

举例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>客户端的实现</title>
</head>
<body>
  <h1>这就是与服务器链接的客户端了</h1>

  <!-- 引入服务器上的socket.io库文件 -->
  <script type="text/javascript" src="/socket.io/socket.io.js"></script>
  <script type="text/javascript">
    // 之后重新运行服务器文件然后进入监听的链接中会看到
    // 控制台发生了变化
    const socket = io.connect()
  </script>
</body>
</html>

image.png

image.png

==> 客户端与服务端通信 <==

说明: 创建好服务器端和客户端之后,就可以在服务器端和客户端之间传输数据了;socket.io模块使用事件的方式进行数据传输,其相关事件在上面已经提到过;当然,要实现监听和发送事件,同样需要使用on()方法和emit()方法

举例:

// 服务器文件

const fs = require('fs');
const http = require('http');

// 创建web服务器
const server = http.createServer((request, response) => {
  // 读取客户端文件index.html
  fs.readFile('index.html', (err, data) => {
    response.writeHead(200, { 'Content-Type': 'text/html' });
    response.end(data);
  })
}).listen(3000, () => {
  console.log('监听的端口是:http://localhost:3000');
})

// 创建WebSocket服务器
let io = require('socket.io')(server);
io.sockets.on('connection', (socket) => {
  console.log('客户端连接成功');
  // 监听客户端的clientData事件,该事件是客户端的一个自定义事件,如果监听到该事件
  // 就输出客户端传输的数据
  socket.on('clientData', (data) => {
    // 输出客户端传输的数据
    console.log('客户端传输的数据是:' + data);
    // 向客户端发送serverData自定义事件和数据
    socket.emit('serverData', '谢谢');
  })
})
<!-- 客户端文件 -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script type="text/javascript" src="/socket.io/socket.io.js"></script>
  <title>客户端的实现</title>
</head>
<body onload="start()">
  <fieldset>
    <legend>发送消息</legend>
    <div>
      <label for="text">发送内容</label>
      <input type="text" id="text">
    </div>
    <div>
      <input type="button" id="button" value="确定">
    </div>
  </fieldset>


  <script>
    const socket = io.connect();

    function start() {
      // 监听服务器端的事件和数据
      socket.on('serverData', (data) => {
        alert('这是来自服务器端的消息' + '\n' + data);
      })

      // 创建表单点击事件
      document.getElementById('button').onclick = () => {
        // 获取输入框的内容
        const text = document.getElementById('text').value;
        // 向服务器端发送消息
        socket.emit('clientData', text);
      }
    }
  </script>
</body>
</html>

image.png

image.png

(2)三种数据通信类型

==> public通信类型 <==

说明: 这种通信类型会向所有的客户端传递数据,实现起来使用io.sockets.emit(event, data)方法就可以,其中event表示要发送的事件,参数data表示要发送的数据

举例:

const fs = require('fs');
const http = require('http');

// 创建web服务器
const server = http.createServer((request, response) => {
  fs.readFile('index.html', (err, data) => {
    response.writeHead(200, { 'Content-Type': 'text/html' });
    response.end(data);
  })
}).listen(3000, () => {
  console.log('监听的端口是:http://localhost:3000');
})

// 创建WebSocket服务器
const io = require('socket.io')(server);

io.sockets.on('connection', (socket) => {
  socket.on('receiveData', (data) => {
    console.log('客户端传输的数据是:' + data);
    // 把获取到的数据广播给所有连接的客户端
    io.sockets.emit('serverData', data);
  })
})
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    .bold {
      font-weight: bold;
    }

    ul {
      list-style: none;
    }
  </style>
  <title>客户端的实现</title>
  <script type="text/javascript" src="/socket.io/socket.io.js"></script>
</head>

<body>
  <form action="">
    <fieldset style="width: 360px; margin: 0 auto;">
      <legend>发布公告</legend>
      <textarea id="text" style="width: 320px; height: 90px; margin-left: 10px;"></textarea>
      <div style="text-align: center;">
        <button type="button" id="btn">发送</button>
      </div>
    </fieldset>

    <ul id="box"></ul>
  </form>


</body>
<script>
  window.onload = function() {
    let box = document.getElementById("box");
    const socket = io.connect();

    socket.on("serverData", (data) => {
      console.log(data);
      let spans = `<span class="bold">${data}</span>`;
      let lis = document.createElement("li");
      lis.innerHTML += spans;
      box.append(lis);
    })

    document.getElementById("btn").onclick = () => {
      const text = document.getElementById("text").value;
      socket.emit("receiveData", text);
    }
  }
</script>

</html>

image.png

==> broadcast通信类型 <==

说明: 这个就是群发的效果,也就是其他人都可以收到但是自己收不到,使用起来也很简单,直接使用socket.broadcast.emit(event, data)方法就好,参数和上面的是一样的

io.sockets.on('connection', (socket) => {
  socket.on('receiveData', (data) => {
    // 上面例子中就更改这一个地方就变成群发的效果了
    socket.broadcast.emit('serverData', data);
  })
})

image.png

==> private通信类型 <==

说明: 这个就是私发的效果,使用起来就更简单了,使用io.to(socket.id).emit(event, data)方法就好;每当有一个客户端连接,这个客户端就存在一个id,使用这个id就能够指定哪一个客户端了,也就能失效私发的效果了

举例:

// ...

// 创建WebSocket服务器
const io = require('socket.io')(server);
// 这里保存客户端的id供后面使用
const list = []

io.sockets.on('connection', (socket) => {
  list.push(socket.id);
  
  socket.on('receiveData', (data) => {
    console.log('客户端传输的数据是:' + data);
    // 然后只跟第一次连接的客户端发送消息,也就达到了私发的效果
    io.to(list[0]).emit('serverData', data)
  })
})

image.png

(3)客户端分组实现

说明: 在服务器向客户端发送消息的时候,可以对接收消息的客户端进行分组,这样便于管理

常用方法:

  • socket.join(room): 用于创建分组,其中的room表示分组的组名;值得注意的是一个客户端可以进入多个分组

  • socket.leave(data.room): 用于退出分组,其中data表示要退出分组的客户端,room表示要退出分组的组名

  • io.sockets.in(room).emit(event,data): 向分组中的用户发送消息(包括自己)

  • io.sockets.broadcast(room).emit(event,data): 向分组中的用户发送消息(不包括自己)

十一、常用Web模板引擎

说明: 模板引擎是指将页面模板和要显示的数据结合起来生成HTML页面的工具

(1)ejs模块

说明: ejs翻译为嵌入式JavaScript,它是一种高效的JavaScript模板引擎,它可以通过解析JavaScript代码生成HTML页面,并且直接支持在标签内书写简介的JavaScript代码,以让JavaScript输出所需要的HTML,使得代码后期维护更加容易;在使用之前需要安装ejs模块

==> 渲染方法 <==

方法与参数:

  • render(str, data, options): 对ejs代码进行渲染
    • [ args ] str: 要渲染的ejs代码
    • [ args ] data: 可选参数,表示渲染的数据,可以是对象或者数组
    • [ args ] options: 可选参数,指定一些用于解析模板的变量,比如编码方式等
  • compile(str, options): 对指定数据进行渲染
    • [ args ] str: 渲染的数据展示区域
    • [ args ] options: 可选参数,表示一些额外的参数配置
  • renserFile(filename, data, options, (err, str) => {}): 对模板文件进行渲染
    • [ args ] filename: ejs文件路径
    • [ args ] data: 渲染的数据,可以是对象或者数组
    • [ args ] options: 额外的参数配置,比如编码格式等
    • [ func args ] err: 渲染文件失败时的错误信息
    • [ func args ] str: 渲染的数据展示区

举例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>使用ejs渲染</title>
</head>
<body>
  <h3>看上去是一个html文件,实际上是ejs文件转换而来的</h3>
</body>
</html>
const http = require('http');
const fs = require('fs');
const ejs = require('ejs');

// 创建服务器
http.createServer((request, response) => {
  response.writeHead(200, {
    'content-type': 'text/html'
  })
  // 渲染ejs文件
  ejs.renderFile('index.ejs', 'utf-8', (err, data) => {
    response.end(data)
  })
}).listen(3000, () => {
  console.log('服务器启动成功')
})

image.png

==> 渲染标识 <==

常用标识:

  • <%:脚本标签,用于流程控制,没有输出
  • <%_:删除其前面的空格符
  • <%=:输出数据到模板,输出是转义HTML标签
  • <%-:输出非转义的数据到模板
  • <%#:注释标签,不执行、不输出内容
  • <%%:输出字符串'<%'
  • %>:一般结束标签
  • -%>:删除紧随其后的换行符
  • _%>:将结束标签后面的空格符删除

举例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>使用ejs渲染</title>
</head>
<body>
  <%
    let today = new Date();
  %>

  <p>
    <span>现在是:</span>
    <span style="color: red;">
      <%=today%>
    </span>
  </p>
</body>
</html>

image.png

  • 一般来说传递数据的方式就是使用<%=value%>的形式来进行,但是如果想动态的往ejs页面传递数据的时候需要先在ejs.render()方法中定义需要渲染的数据,之后再用上面传递的形式进行传值

十二、了解Express框架

说明: Express框架在express模块的基础上,引入了express-generator模块,以便让项目的开发更加方便和快捷

(1)了解express模块

说明: express模块与http模块很相似,都可以创建服务器,不同之处在于express模块将更多Web开发服务功能封装起来,让Web应用开发更加便捷;使用它创建一个服务器也很简单,就像下面这样就好

// 导入模块
const express = require('express');

// 创建web服务器
const app = express();

// 启动web服务器
app.listen(3000, () => {
  console.log('Server is running on port 3000');
})

==> 响应对象 <==

说明: express模块提供了response对象,用来完成服务器端的响应操作

常用方法:

  • response.send([body]): 根据参数类型返回对应数据,如果是参数是字符串类型,则返回HTML格式的数据,如果参数是数组或者对象类型,则返回JSON格式的类型

  • response.json([body]): 返回JSON数据

  • response.redirect([status,]path): 强制跳转到指定页面

举例:

// ...

// 监听请求与响应
app.use((request, response) => {
  let output = []

  for(let i = 0; i < 3; i++) {
    output.push({
      count: i
    });
  }

  // 这里输出一个数组类型的参数但返回给客户端的却是一个JSON格式
  response.send(output)
})

// ...

image.png

==> 请求对象 <==

说明: express模块提供了request对象,用来完成客户端的请求操作

常用属性和方法:

  • request.params: 返回路由参数
  • request.query: 返回请求变量
  • request.headers: 返回请求头信息
  • request.header(): 设置请求头信息
  • request.accepts(type): 判断请求accept属性信息
  • request.is(type): 判断请求Content-type属性信息

举例:

// ...

// 监听请求与响应
app.use((request, response) => {
  // 获取客户端的user-agent
  const agent = request.header('user-agent');
  // 判断客户端浏览器的类型
  if(agent.toLowerCase().match(/chrome/)) {
    // 发送响应信息
    response.send('Hello, Chrome!');
  } else {
    // 发送响应信息
    response.send('Hello, Other!');
  }
})

// ...

image.png

(2)中间件

使用方法:

  • use([path,]callback[,callback]): 注册全局中间件

    • [ params ] path: 可选参数,指定中间件函数的路径或者是路由地址
    • [ params ] callback: 指定的中间件函数,可以是多个,并且这些回调函数可以调用next()方法

说明: 所谓中间件就是指业务流程的中间处理环节,在express中使用use()方法将指定的中间件功能放到指定的路径下面,当请求的路径地址与定义的路由相同的时候就会执行指定的中间件功能;在实际开发的过程中,代码数量和模块数量很多,为了提高代码的使用效率,可以将常用的功能函数提取出来做成中间件的形式,这样可以让更多的模块重复使用中间件

调用流程: 当一个请求到达express的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理,在这中间,next()函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或者路由

举例:

// ...

// 设置中间件
app.use((request, response, next) => {
  // 定义变量
  request.name = '张三';
  request.age = 18;
  next();
})

app.use((request, response, next) => {
  response.send('我的名字是:' + request.name + ',我的年龄是:' + request.age);
})

// ...

image.png

==> router 中间件 <==

说明: router中间件用来处理页面路由,在http模块中,通常需要使用if语句来处理页面的路由跳转,而在express中,使用router中间件就可以很方便的实现页面的路由跳转

常用方法:

  • get(path,callback[,callback]): 处理GET请求
  • post(path,callback[,callback]): 处理POST请求
  • pull(path,callback[,callback]): 处理PULL请求
  • delete(path,callback[,callback]): 处理DELETE请求
  • all(path,callback[,callback]): 处理所有请求

举例:

// ...

// 设置中间件
app.get('/page/:id', (request, response, next) => {
  // 使用通过路由传进来的参数
  response.send(`当前页面的id是${request.params.id}`);
})

// ...

image.png

==> static 中间件 <==

说明: 这个是express模块内置的托管静态文件的中间件,可以非常方便的将图片、视频、CSS文件和JavaScript文件等资源导入到项目中;这需要使用到express.static(root[,options]):方法,其中root指定提供静态资源的根目录,而options是一个可选参数,用于指定一些配置选项

举例:

// ...

app.use(express.static(__dirname + '/images'))
app.use((request, response, next) => {
  response.writeHead(200, {
    'Content-Type': 'text/html;'
  })
  response.end('<img src="/image.png" />')
})

// ...

image.png