Node.JS


前言

更新日志

2021/12/15:
虽然说还有N个项目没有写完,考试也迫在眉睫,但是我啊依旧不慌,还是学技术最令人心情愉悦了!

当然,话是这么说,后面该复习还是得复习啊,哦——是预习。总之在学习考试科目的同时,我也会抽空持续学习技术的!

2021/12/16
明天还要做数据库的上机实验,但是我还是选择通宵了,没错,现在是凌晨四点,说实话感觉有点累,但是还是再学会儿吧,

四点半就回寝室…

2021/12/17
爆肝三日终于Node入门,学到FS文件操作的时候当时我就意识到了,或许项目中前后端传递图片的技术已经近在眼前!真的太强了,伟哉JS!

2022/02/07
补充关于AMD和CMD的少量知识点

2022/02/25
补充Node常用模块及其API

2022/05/06
考试终于是告一段落了
更新async和net模块部分

多余的话

Node.JS真是了不起啊,想想就觉得刺激!

这里只是皮毛,学完了最多算是入门的入门~~

基础知识

终端

小黑窗、命令行窗口、命令提示符、DOS窗口、shell、CMD窗口、终端说的都是一个东西啊…

这个东西过于基础,放在这里只是为了让大家知道要学这个,此处就不再赘述

环境变量

格式是键值对。

以path变量为例,其值是多个路径,路径间以分号隔开

这些路径,能够在任意目录下访问

比如配置一个D:/hello

这里的hello是一个txt文件

我们在终端里执行如下内容:

C:\User>hello.txt

系统会先到User目录下寻找hello.txt,如果没找到,那么就会去环境变量path中寻找,这样就访问到了hello.txt

环境变量可以分为两类

用户变量

当前用户能够使用

系统变量

这台计算机上的所有用户都能够使用

进程和线程

概念不再赘述,详见操作系统笔记

简单提下为什么浏览器主进程(渲染进程)是单线程

如果原生变为多线程,那么渲染页面和JS解析同时进行,

由于并发和异步可能会导致最终页面出现和预期不符的结果

Node.JS基础

概念

Node.JS还没有诞生的时候,JS顶天就是操作浏览器.

Node.JS是一个能够在服务器端运行JavaScript的开源跨平台JavaScript运行环境。

Node采用Google开发额度V8引擎运行JS代码,使用事件驱动、非阻塞、异步I/O等技术提高性能。

Node大部分内容都用JS编写。

但是Node.JS诞生了,JS能够撼动整个计算机的世界.

性能提升的原理

浏览器作为客户端,和服务端交互存在两个过程:请求和响应。而服务端和数据库之间存在I/O操作。

客户端对于硬件层面的I/O操作就是无能为力,但是可以优化请求和响应相关的逻辑代码可以提升程序的性能。

客户端(此处指传统浏览器)每发出一个请求,客户端都会为之创建一个线程,而在其I/O完成之前,该线程会一直等待,即阻塞

当有大量请求时,会出现大量请求阻塞,内存资源会被极大的消耗

Node.JS的解决方案就是Node模式

避免一个线程忙等待,当请求执行到I/O操作时,该线程不会阻塞而是选择去服务其他请求,从而避免了大量线程白白浪费资源。

当然,要注意Node服务器单线程,意思是只有一个线程忙活,所以它需要超高速地服务不同的请求。

如果有做多线程的需求,可以考虑分布式,但这个概念不在话下。

为什么是JavaScript

有如此多的语言,但是为什么Ryan Dahl选择在JavaScript的基础上进行改动,这是因为C/C++,Java等传统服务端语言的模式已经根深蒂固,

想要进行大刀阔斧的改革变得尤其困难,而JavaScript在这方面还像是一片从未开发的沃土,而且正值Google推出V8引擎,这使得JavaScript变成了最好的选择。

也正是从此开始,JavaScript从一个需要浏览器支撑的脚本语言变成了独立自主的面向对象语言,也就是说,JavaScript从此站了起来!

应用实例

Node.JS虽然能够做服务端,但是大数据的能力依旧不如Java。

淘宝的后台服务器采用Java服务器,但是会在客户端和后台服务器之间加一层Node.JS服务器,因为Node.JS解析渲染页面的能力要更强一些,

这样的结构可以加速页面的解析和渲染。

Node.JS发展史

时间 事件
2009 瑞安·达尔发布Node.JS的最初版本
2011 npm诞生
Node.JS发布Windows版本
Node超越Ruby on Rails(一个Web框架)
2012 瑞安·达尔离开Node项目
2014 Node的分支IO.JS诞生
2015 Node.JS基金会成立
Node.JS合并IO.JS成为Node 4.0(合并后凭空出现的版本号)
2016 Node 6.0发布
2017 Node 8.0发布
2021 某佑开始学习Node.JS

朝着前辈们的背影,踏上这伟大的道路!!!

简单使用Node.JS

终端运行Node.JS

win + r打开命令提示符终端,输入node后回车开启,此后便可以开始输入JS代码

在这里插入图片描述

输入.exit或ctrl + c两次后退出

当然,这样写十分不方便,所以我们还可以使用下面这种形式来用node执行JS代码

在这里插入图片描述

在这里插入图片描述

集成环境运行Node.JS

当然,也可以在webstorm或者vscode提供的终端执行以上操作

在这里插入图片描述

COMMONJS规范

啊不是吧,这几天才知道TypeScript还有几个稀奇古怪的Script,这里又来个COMMONJS…

注意,现在已经采用ES6标准了,但COMMONJS还是要会

概念

ECMAScript5的缺陷

COMMONJS提出时,还没有ES6,所以下述内容是针对ES5的

1.没有模块系统(ES6有了,现在可以忽略这一条)

2.标准库较少

3.没有标准接口

4.缺乏管理系统

COMMONJS的提出

最开始是用于服务端的标准而不是浏览器环境

这个规范希望JS能够在任何地方运行

它也提出了JS模块化早期的一些内容,这些内容在Node中有较为广泛的应用

1.模块引用

通过require()来引用模块,参数是一个路径,返回值是引入的对象

这里需要注意,当参数为相对路径时,必须以.或者..开头

2.模块定义

在Node中,一个JS文件就是一个模块,并且每一个JS文件中的代码都是独立运行在一个函数中的,所以一个模块中的变量和函数在其他模块中无法访问(除非exports)

3.模块标识

通过exports.v这种形式来暴露内容,exports相当于该模块暴露出去的部分

能够通过模块标识找到该模块,具体模块标识参见下述内容

模块分为两个大类:

核心模块

由node引擎提供的模块,其标识是模块的名字

(意味着不需要使用相对路径,直接把模块名字作为require的参数即可)

文件模块

由用户自己创建的模块,其标识是文件的路径

示例

建立这样一个目录结构

暴露test2中的内容

在这里插入图片描述

test引入

在这里插入图片描述

运行结果

在这里插入图片描述

AMD

在COMMONJS之后呢,还出现了一种叫做AMD(Asynchronous Module Definition,异步模块定义)的规范,形如:

define('getSum', ['math'], function (math) {
    return function (a, b) {
        console.log('sum: ' + math.sum(a, b))
    }
})
define(function(){
     var exports = {};
     exports.method = function(){...};
     return exports;
})

它只有一个接口,那就是define,把要模块化的内容包起来,Node就可以看做是做了隐式的define包装

RequireJS就是实现了AMD的著名例子(注意AMD只是规范,Require.JS是其实现)

CMD

提到了AMD就顺带一提CMD,CMD很多地方和AMD类似

define(function(require,exports,module){...})

Sea.JS就是实现了CMD的著名例子

模块的函数本质

global全局对象

a = 5
// 记住这种声明是直接变成全局变量
// 全局变量保存在global中

在这里插入图片描述

证明模块的函数本质

上面的内容有提到过,模块内的变量和函数相当于是放到一个函数中的,那么如何证明呢?

答案是函数特有的伪数组对象arguments

在这里插入图片描述

那么我们能不能瞅瞅这个函数呢?

当然可以,arguments里有一个叫callee的方法,查看当前执行的函数对象

在这里插入图片描述

橙色的部分是通过字符串拼接,让对象调用了toString方法变成字符串达到展开的效果,也就是说效果和下面这段代码等价

console.log(arguments.callee.toString())

总结一下就是,模块会把内容(包括注释)装进一个这样的函数里面

function (exports, require, module, __filename, __dirname) {
    //这是模块的内容 
}

所以除了隐式声明之外,模块中写的内容都是局部的。

五个参数

通过观察参数,我们发现exports,require是通过参数传递进来的,而剩下三个参数则是

在这里插入图片描述

总结就是

名称 介绍
exports 用来将变量或函数暴露到外部的对象
requires 用来引入外部模块的函数
module 代表当前模块本身的对象,exports是其属性
__filename 当前模块的完整路径
__dirname 当前模块所在的目录文件的完整路径

上面提到了,exports是module的一个属性,那么我们就要讨论一下module.exports和直接exports

比较两种导出方式

首先复习一下栈内存堆内存

栈内存

以键值对的形式存放变量名和变量值

堆内存

存放引用数据类型的内容,比如对象的内容和对象的地址

(栈内存中存放对象名和对象的地址)

那么,以下代码的执行结果是?

let obj = new Object()
let obj2 = obj
obj.name = '帅'
obj2.name = '俊'
console.log(obj.name, obj2.name)

在这里插入图片描述

let obj = new Object()
let obj2 = obj
obj.name = '帅'
obj2.name = '俊'
obj2 = null
console.log(obj, obj2)

在这里插入图片描述

总结就是修改obj.name是修改的对象,修改obj只是修改指向

那么同样的,对于exports和module.exports也是一样

首先在test3.js里写下如下代码:

exports.a = '试图导出一条信息a'

module.exports = {
    b: 'module.exports修改的是导出对象本身'
}

exports = {
    c: '直接exports修改的是exports的指向'
}

在这里插入图片描述

在test.js中则写下如下代码:

let test3 = require('./test3.js')

console.log(test3)

在这里插入图片描述

我们先来分析一下,

之前我们可以通过exports.v这种形式添加暴露的内容,是因为exports指向了module.exports,require返回的是module.exports,所以我们的结果是

在这里插入图片描述

但是如果我们把test3.js中的代码顺序调整为


module.exports.b = 'module.exports修改的是导出对象本身'

exports = {
    c: '直接exports修改的是exports的指向'
}

exports.a = '试图导出一条信息a'

在这里插入图片描述

运行结果则是

在这里插入图片描述

所以建议都使用对象调用内部成员的方式来暴露信息

概念

COMMONJS的包规范由包结构包描述文件两个部分组成

包结构

用于组织包中的各种文件

包描述文件

描述包的相关信息,以供外部读取分析

包规范允许我们将一组先关的模块组合到一起,形成一组完整的工具

包结构

包实际上就是一个压缩文件,解压后应该还原为目录。

一个标准的目录应该包含如下文件:

1.package.json 描述文件(必有)

2.bin 可执行二进制文件

3.lib js代码

4.doc 文档

5.test 单元测试

包描述文件

包描述文件用于表达非代码相关的信息,

它是一个JSON文件,也就是package.json,这是包的标志。

位于包的根目录下,是包的重要的组成部分。

举个例子,打开一个vue项目就能发现package.json

在这里插入图片描述

其中,会发现版本号前面可能有~或者^符号,这是版本号的语义化(虽然我没看出来哪里语义化)

~代表强调小版本,指的是形如x.y.z的版本号上的z这一位,更新时会尽可能增大z来更新

^同理,代表中版本,指的是y这一位,

NPM

概念

Node Package Manager,Node包管理工具,

COMMONJS包规范是理论,NPM是一种实践,

对于Node而言,NPM帮助其完成了第三方模块的发布、安装和依赖等。

借助NPM,Node与第三方模块之间形成了很好的一个生态系统

由于经常使用NPM,所以这部分稍微讲其他生疏一些的内容

初始化包

执行下面这行shell指令开启初始化流程

npm init

在这里插入图片描述

经过上述的一番操作之后,就会产生一个package.json文件,之后Node就会把当前文件夹视为一个包

在这里插入图片描述

下载包

其中的xxx是具体的包名

npm install xxx

这里以math为例

在这里插入图片描述

此后新增内容:

1.node_modules文件夹

其中包括了下载的包,比如这里下载的math。

2.package-lock.json

锁定安装时的版本号,并上传到git,保证其他用户在npm install下载的时候依赖保持一致。

package.json文件只能锁定大版本,也就是版本号的第一位,并不能锁定小版本,每次npm install就会拉取该大版本下最新的小版本。

package-lock.json则是在每次安装一个依赖的时候就锁定在安装这个版本。

使用包

在初始化的时候,我们设置了entry point为main.js,这就要求我们在根目录下新建一个main.js作为主函数(不知道该怎么称呼,暂时称为主函数好了)

// 因为是从NPM下载的,可以视为Node提供的,即核心模块,所以可以直接写名字引入
let math = require('math')
console.log('这是math:', math)
console.log('====================================')
console.log('调用math的add方法:', math.add(1,1))
console.log('====================================')
console.log('调用math的sum方法:', math.sum([1, 2, 3]))

注意Node搜索包的过程是,首先搜索当前目录下的node_modules,在其中搜到了目标内容则直接使用;

否则进入上一级目录重复上述过程,递归直至搜索成功或搜索完根目录。

常用操作指令

指令 功能
npm -v 查看npm的版本
npm version 查看所有模块的版本
npm search [包名] 搜索包
npm install [?包名] 安装包,不加参数则安装所有依赖的包
如果在指令末尾加上–save
那么会把该包添加进package.json的依赖
如果在指令末尾加上 -g
那么会全局安装包(一般是全局安装工具)
npm remove [包名] 删除包
npm init 初始化包

CNPM

NPM的服务器是在国外,下载速度有时比较慢。

CNPM则是在国内的镜像服务器,相对更稳定。

cnpm下载的内容不会覆盖npm下载的内容,其名称会有相应的标识。

Buffer缓冲区

简单使用

从结构上来看,Buffer和数组很像。但是相较于数组,Buffer性能更好,而且能够存储图片音频等二进制文件数据。

使用Buffer不需要引入模块,直接使用即可:

let str = 'Hello World'
let buf = Buffer.from(str)

console.log(buf)

在这里插入图片描述

数值对应的是unicode编码而不是ascii编码

存储的是二进制,但是打印出来的时候是十六进制,(因为二进制太长可读性太差),由于是两位数,所以取值范围是00-ff,即00-255,分别对应8位二进制0和1,即一个字节

缓冲区的长度计算

结合上面的内容,我们来分析下述代码:

let str = '你好,世界'
let buf = Buffer.from(str)

console.log('缓冲区大小',buf.length)
console.log('字符串大小',str.length)

str的长度肯定是5,但是buf的长度又如何呢?

其中逗号采用的是英文逗号,而buf存储的又是字节,读入字符串时,默认采用UTF-8,而在UTF-8,汉字占3个字节,所以结果应该是4*3+1 = 13

在这里插入图片描述

当然,我们可以创建指定长度的buffer

// let buf2 = new Buffer(10) // 这种写法已经被删除
let buf3 = Buffer.alloc(10)  // 推荐使用这种写法

注意,Buffer的构造方法都已经废除了(就是别new了)

缓冲区的内容

let buf3 = Buffer.alloc(10)  // 推荐使用这种写法

buf3[0] = 15
buf3[1] = 0x15
buf3[2] = 255, buf3[3] = 256, buf3[4] = 257
buf3[15] = 0x11

console.log(buf3)
console.log(buf3[15])

在这里插入图片描述

观察结果,我们可以发现

1.支持下标访问,自动转化为十六进制

2.静态且连续,不会动态扩容,越界非法访问无效

3.值会对最大值(255)进行取模运算(只保留二进制后八位)

并且在单独打印某个值时,它一定是十进制

buf3[1] = 0x15
console.log(buf3[1])

输出结果是1 * 16 + 5,即 21

那么,要怎么指定进制输出呢?

(竟然是toString方法,完了忘完了)

console.log('这是十进制:', buf3[1])
console.log('这是二进制:', buf3[1].toString(2))
console.log('这是十六进制:', buf3[1].toString(16))

输出结果分别是:21,10101,15

接下来,我们来看看Buffer的不安全分配:

let buf4 = Buffer.alloc(10)
let buf5 = Buffer.allocUnsafe(10)

console.log('安全分配:', buf4)
console.log('不安全分配:', buf5)

输出结果是:

安全分配: <Buffer 00 00 00 00 00 00 00 00 00 00>
不安全分配: <Buffer 20 91 0b d1 46 02 00 00 60 92>

安全分配会将内容全部清空(归零)

不安全分配则会保留该部分内存的内容,省略一步操作其实性能还会好些(但是确实不安全啊)

其他操作

太多了懒得写,直接看官方文档

应用

服务器接收用户的二进制数据都先存到缓冲区等待处理

其余内容参见操作系统部分

FS文件模块

终于,要操作文件了

file system文件系统

所有操作都会有两种形式可供选择:同步和异步

之后的方法都会有两种形式:

一种名为xxxSync(同步),则一定有另一种名为xxx(异步)

同步写入

首先想一下文件写入的步骤:

1.打开文件

openSync(path,flags [, mode])

  • path 文件路径
  • flags 操作的类型: r读; w写;
  • mode 可选参数,设置文件权限,一般不写

没有该文件则会创建该文件。

返回值是一个该文件的描述符,可以通过该描述符操作对应文件。通常是一个数字

2.向文件中写入内容

writeSync(fd, string [,position [, encoding]])

  • fd 文件描述符
  • string 写入内容
  • position 写入起始位置,一般不写
  • encoding 编码格式,默认utf-8

3.保存并关闭文件

fs.closeSync(fd)

  • fd 同上,文件描述符
let fd = fs.openSync('text.txt', 'w')
fs.writeSync(fd, '今天天气真不戳')
fs.writeSync(fd, '我要从第五十格开始写', 50)
// 其实是从五十一格开始写
fs.close(fd)

在这里插入图片描述

如果不指定位置,那么会直接清空原内容,写入新内容

当然有时候会发生两部分内容重叠导致的乱码…

异步写入

1.打开文件

open(path,flags [, mode],callback)

  • path 文件路径
  • flags 操作的类型: r读; w写;
  • mode 可选参数,设置文件权限,一般不写
  • callback 回调函数:两个参数,第一个为err(错误优先思想),没有错误则为null;第二个为fd,文件描述符

没有该文件则会创建该文件。

没有返回值。

2.向文件中写入内容

writeSync(fd, string [,position [, encoding]])

  • fd 文件描述符
  • string 写入内容
  • position 写入起始位置,一般不写
  • encoding 编码格式,默认utf-8

3.保存并关闭文件

closeSync(fd)

  • fd 同上,文件描述符
let fs = require('fs')

fs.open('text2.txt', 'w', (err, fd) => {
    if (!err) {
        fs.write(fd, '啊,这个兽偶好好看的哇~', (err) => {
            if(!err) {
                console.log('文件写入成功') 
            } else {
                console.log('文件写入出错', err)
            }
            fs.close(fd, (err) => {
                if(!err) {
                    console.log('文件关闭成功')
                } else {
                    console.log('文件关闭出错', err)
                }
            })
        })
    } else {
        console.log('文件打开出错', err)
    }
})

学到这里才意识到,一般而言异步函数是没有返回值的,多半是把结果放到参数里面来保存的

上面这个结构也让我更好的认识到了回调地狱

简单写入(异步封装)

fs.writeFile('text3.txt', '来啊放纵啊,反正有——大把时光~~', (err) => {
   if(!err) {
       console.log('成功写入,稳如老狗')
   } 
})

这是一个对异步写入的封装

当然,简单文件写入方法还有其更多参数,比如可以传一个options对象,包括encoding编码格式,mode权限以及flag操作模式等等(可以通过修改flag为r来把方法变为读)

这个方法似乎没法指定从某一个位置开始写

在这里插入图片描述

那如何追加而不是替换内容,我们参考上表,将flag操作模式的值设置为a再次尝试

fs.writeFile('text3.txt', '来啊,快活啊~~', {flag:'a'}, (err) => {
   if(!err) {
       console.log('成功写入,稳如老狗')
   } 
})

在这里插入图片描述

流式写入

同步、异步、简单写入都不适合大文件的写入,性能都比较差,容易导致内存溢出

流式写入则可以持续的、一部分一部分地写入文件

比喻就是,从水池A向水池B引水,流式输入就是两个水池间连接的管道——请记住这个比喻

最简单步骤是

1.创建一个可写流

createWriteStream(path [,options])

返回一个可写流对象

2.写入

write(string)

3.关闭可写流

close() 或者 end()

这里比喻一下:两个方法都是把管道拔掉,但是close是拔掉对面那头,end是拔掉我们这头。

区别在于,当我们把水都引入管道后,我们并不知道水是否已经进入对面的水池,拔掉对面那头的话可能导致还在管道中的水丢失;所以我们一般是先拔掉我们这边这头。

但是随着版本更迭,close可能出现的弊端似乎也得到了很好的修正(?管他的反正先end吧)

let fs = require('fs')

let ws = fs.createWriteStream('text3.txt')
console.log('这是可写流对象',ws)

for(let i = 0; i < 10; i ++)
ws.write(i + ' ')

ws.close()

在这里插入图片描述

默认的操作模式是w,也是对内容进行覆盖

在这里插入图片描述

我们还可有更多操作

监听

let fs = require('fs')
let ws = fs.createWriteStream('text3.txt', {flag:'a'})

ws.once('open', () => {
    console.log('流打开了')
})
// console.log('这是可写流对象',ws)
for(let i = 0; i < 10; i ++)
ws.write(i + ' ')

ws.once('close', () => {
    console.log('流关闭了')
})

ws.close()

once和on一样,能够添加时间,但是once是只能触发一次的

简单读取(异步封装)

文件读取和写入也是一一对应的,也分为四种:同步 异步 简单 流式,前两种几乎和写入没有区别,所以不再赘述

流程也不再赘述,直接上代码

fs.readFile('text2.txt', (err, data) => {
    //才想起来啊,throw直接抛出错误啊
    if(err) 
        throw err
    console.log('这是数据', data)
})

输出结果如下

这是数据 <Buffer e5 a4 a9 e6 b0 94 e7 9c 9f e4 b8 8d e6 88 b3>

是十六进制的嘛,这咋看哦——这个时候就得归功于我们万能的toString了,原来它不只是能够转进制,还能直接给翻译回去,是真滴牛啤啊

let fs = require('fs')

fs.readFile('text2.txt', (err, data) => {
    if(err) 
        throw err
    console.log('这是数据:', ...data)
    console.log('处理一手:', data.toString())
})

而且我还发现,可以对buffer用展开运算符,展开之后直接变十进制,简直帅死了

输出结果如下:

这是数据: 229 164 169 230 176 148 231 156 159 228 184 141 230 136 179
处理一手: 天气真不戳

注意,这里可不只是能够读文本,还可以读图片等超文本

比如像下面这样

fs.readFile('pic.png', (err, data) => {
    if(err)
        throw err
    let biPic = data  
    console.log(biPic)   
    fs.open('pic2.png', 'w', (err, fd) => {  
        if(err)
            throw err
        fs.write(fd, biPic, (err) => {
            if(err)
                throw err
            fs.close(fd)    
        })
    })   
})

流式读取

大家或许会觉得依葫芦画瓢,写是怎么样读就怎么样,但是流式读取还真不是那样的——所以这里单独拎出来说一说

监听data事件

监听可读流访问数据

懒得多说了直接上代码


let rs = fs.createReadStream('video.mp4')

rs.once('open', () => {
    console.log('开启流')
})

rs.on('data', (data) => {
    console.log(data)
})

rs.once('close', () => {
    console.log('关闭流')
})

可读流似乎不用手动关闭,一旦读取完毕自动关闭

在这里插入图片描述

总之就是非常大的数据啦

结合可写流,我们能够实现视频的复制:

let fs = require('fs')

let rs = fs.createReadStream('video.mp4')
let ws = fs.createWriteStream('video2.mp4')

rs.once('open', () => {
    console.log('可读流---开启')
})
rs.once('close', () => {
    console.log('可读流---关闭')
    // 可读流关闭,那么可写流也可以关闭了
    ws.end()
})
ws.once('open', () => {
    console.log('可写流---开启')
})
ws.once('close', () => {
    console.log('可写流---关闭')
})

rs.on('data', (data) => {
    console.log(data)
})

这样就实现了一个文件的复制

当然上述内容太麻烦了,这里还有一种更为简单的方法管道

管道的具体概念参见操作系统,使用方式如下:

rs.pipe(ws)

这样就建立起了rs和ws之间的管道,将可读流的内容输出到可写流中,是对之前的代码的一次封装

其他操作

验证路径是否存在

fs.exists(path, callback) //已经废止,用fs.stat替代
fs.existsSync(path)

获取文件信息

fs.stat(path, callback) 
fs.statSync(path)

删除文件

fs.unlink(path, callback)
fs.unlinkSync(path)

当然还有更多方法,具体使用参阅官方文档

常用核心模块及其API

url模块

import url = require('url')

url.parse()

第一个参数是url字符串,返回一个URL对象,具有protocol、hostname、query、path等属性

第二个参数如果为true,则会将query再解析为对象

url.format()

将URL对象转为URL字符串

url.resolve()

用于URL拼接,能够识别相对路径等

var a = url.resolve('/one/two/three', 'four') 
var b = url.resolve('http://example.com/', '/one')
var c = url.resolve('http://example.com/one', '/two');
var d = url.resolve('http://example.com/one/ddd/ddd/ddd', './two');
var e = url.resolve('http://example.com/one/ddd/ddd/ddd', '../two');
var f = url.resolve('http://example.com/one/ddd/ddd/ddd', '.../two');
console.log(a +","+ b +","+ c+','+d+','+e+','+f);

//输出结果:
/one/two/four,
http://example.com/one,
http://example.com/two,
http://example.com/one/ddd/ddd/two,
http://example.com/one/ddd/two
http://example.com/one/ddd/ddd/.../two

querystring模块

querystring.escape()

编码,在接下来的stringify等方法中会自动调用这个方法

作用是将中文转化为编码字符

querystring.unescape()

解码,能够将编码的字符还原为原本的文字

querystring.parse()

反序列化,将query字符串转化为Query对象

querystring.stringify()

序列化,将Query对象转化为query字符串

querystring.stringify ({name:"a", age: 1}, ",", ":")

第一个参数是操作的对象,
第二个参数是指定键值对间分隔方式,
第三个参数可省略,默认值为:,指定键值间分隔方式

第四个参数可省略,用于配置,详情查阅官方文档

结果

“name:a,age:1”

queryString常用于处理post请求,用于规范化(序列化)数据

所谓的序列化,就是处理成对象的形式

之所以要这样“多此一举”,是因为总有像Y70一样的大佬,用postman绕开前端的输入合法性检验,直接发送一些稀奇古怪的字符串过来——这导致JSON.parse()无法将其转换为对象,直接引发后端的崩溃

https模块

和http的方法基本上都一样

https爬虫

非常容易,代码如下

const https = require ('https')
const url = 'https://uland.taobao.com/sem/tbsearch'

https.get (url, (res) => {
    let html = ''
    res.on ('data', (chunk) => {
        html += chunk
    })
    res.on ('end', () => {
        console.log(html)
    })
    res.on ('err', (err) => {
        console.log(err)
    })
})

cheerio

用于内容筛选,是第三方包,需要先npm安装

其语法类似JQ,筛选方式是用CSS选择器的形式筛选:

const cheerio = require ('cheerio')

function filter (html) {
    const $ = cheerio.load (html)
    const footer = $('#alimama-footer')
    const text = []
    // console.log(footer)
    footer.each((index, value) => {
        text.push ($(value).text())   
    })
    return text
}

部分运行结果

request

可以看做封装的AJAX,能发起请求,也能拿到响应

const https = require ('https')

const options = {
    hostname: 'api.douban.com',
    port: 443,
    method: 'GET',
    path: 'v2/movie/top250'
}

const request = https.request (options, (response) => {
    console.log (response)
})

request.on ('error', (error) => {
    console.error (error)
})

request.end()

拿到了response的所有信息

当然也可以这样拿

const request = https.request (options, (response) => {
    // console.log (response) 
    response.setEncoding('utf8')
    // 不设置编码格式的话默认是buffer
    // 设置为utf8之后返回的是JSON
    response.on ('data', (chunk) => {
        console.log(chunk)
    })
    response.on('end', () => {})
})

如果我们要发起post请求,那么需要像下面这样

这是一个自动评论b站视频的脚本,这里由于个人隐私,部分数据以xxx代替

const https = require ('https')
const qs = require ('querystring')
const postData = qs.stringify({
    'oid': 'xxx',
    'type':'1',
    'message':'发个评论测试一下',
    'plat':' 1',
    'ordering':'heat',
    'jsonp':'jsonp',
    'csrf':'xxx'
})

const options = {
    hostname: 'api.bilibili.com',
    port: '443',
    method: 'POST',
    path: '/x/v2/reply/add',
    header: {
        // xxx
    }
}

const request = https.request (options, (response) => {
    console.log ('请求结果', response.statusCode)
})

request.on ('error', (err) => {
    console.error (err)
})


request.write (postData)

request.end ()

事件模块

Events类

注意这个是一个类,所以我们继承一下方便扩展

const Events = require ('events')
class Player extends Events {}

const player = new Player ()

emit()

player.on ('play', (title) => {
    console.log(`正在播放:《${title}`)
})

player.emit ('play', 'JAVA零基础到入坟')

emit就是触发事件,第二个参数是作为事件的参数,可以考虑封装成对象然后传多个参数

这里用on的话,那么通过emit能够多次触发;

如果用once的话,那么就只能触发一次;

文件模块

也就是fs模块,我们在之前的FS文件模块章节已经说过了

这里再做一点补充,内容比较多,建议参考官方文档

关键词 名称
stat 得到文件与目录的信息
mkdir 创建一个目录
writeFile
appendFile
创建文件并写入内容
readFile 读取文件的内容
readdir 列出目录的东西
rename 重命名目录
rmdir
unlink
删除目录与文件

async模块

需要npm安装

串行无关联

串行和并行
大概可以这么理解,比如,并行是每次发送1B,串行则是把1B拍烂为8b发送,这样的好处是:

  1. 可以减少对带宽的占用量,减少成本——没错,理想情况下只占用原带宽的1/8,8b排成一个队列传输(这意味着一个队列中是同步的)
  2. 就算传输过程中丢包了,相较于一次损失1B而言,能够减少数据的损失量

先看一段普通的异步代码

console.time('test1') 
// 开始计时
setTimeout(() => {
    console.log('test1-begin')
}, 2000)

setTimeout(() => {
    console.log('test1-end')
    console.timeEnd('test1') 
    // 结束计时, 结果约为4s
}, 4000)

可以这么理解,setTimeout本身是同步的,其中的回调才是异步的

同步执行期间,将setTimeout定时任务放入异步任务队列,并开始计时,时间到后就触发,所以到4s的时候就触发了

知道了这点,那么下面这段代码的结果也不难推测了:

console.time('test1')
setTimeout(() => {
    console.log('test1-begin')
}, 2000)

setTimeout(() => {
    setTimeout(() => {
        console.log('test1-end')
        console.timeEnd('test1') // 10s
    }, 6000)
}, 4000)

那么,接下来看看串行无关联代码?

使用async.series()执行串行无关联代码,

第一个参数是数组,其内放置需要执行的任务

第二个参数则是回调,获取执行结果

const async = require('async')

console.time('test2')
async.series([
    // 任务1
    function (callback) { 
        setTimeout(() => {
            // 执行后将'one'放到res中
            callback(null, 'one') 
        }, 2000)
    },
    // 任务2
    function (callback) {
        setTimeout(() => {
            // 执行后将'two'放到res中
            callback(null, 'two')
        }, 4000)
    }
    // 回调
], function (err, res) {
     // ['one', 'two']
    console.log(res)
    // 约6s
    console.timeEnd('test2')
})

可以看到,两个异步的方法似乎产生了某种同步执行,耗时是两者之和

并行无关联

并行,已经是猜到结果了,不过,这里并行N个任务,是否会z真的开启N条线程,或者是模拟N个线程?——这是个问题,有知道的大佬还请指点一下

const async = require('async')

console.time('test3')

async.parallel([
    function (callback) {
        setTimeout(() => {
            callback(null, 'one')
        }, 2000)
    },
    function (callback) {
        setTimeout(() => {
            callback(null, 'one')
        }, 4000)
    }
], function (err, res) {
    console.log(res)
    // 约6s
    console.timeEnd('test3')
})

诶嘿,好啊又回到了最初的起点了,4s

串行有关联

这个有关联说的是参数之间的传递,前面2个例子中,callback都会将内容传给最后的回调函数——这就是说数组中的几个方法之间无关联

而有关联则是,第一个方法callback将把数据传给下一个方法,这样一直到最后一个方法,最后一个方法的callback将把所有数据传给回调函数

console.time('test3')

async.waterfall([
    function (callback) {
        callback(null, 'one')
    },
    // data1即上一个方法的callback的one
    function (data1, callback) {
       callback(null, data1,  'three')
    },
    function (data1, data2, callback) {
        callback(null, [data1, data2, 'three'])
     }
    ], function (err, res) {
    // [ 'one', 'three', 'three' ]
    console.log(res) 
    console.timeEnd('test3')
})

Net模块

Net模块主要是为了实现socket

Soket

网络上两个程序通过一个双向的通信连接实现数据的交换,连接的一端称为一个socket

建立socket服务端连接的流程一般是六步:

  1. 创建socket
  2. 将socket绑定某个端口
  3. 开始监听这个端口
    (这之后可以开始接受其他socket的连接请求了)
  4. 接受请求
  5. 从socket中读取字符
  6. 关闭(当然,可以不关闭,一直处于连接状态)

而建立客户端的流程则是:

  1. 创建socket
  2. 请求连接另一个socket
  3. 发送数据
  4. 关闭(当然,也可以不关闭)

之后写了一个 聊天服务端-客户端 的代码,后面放到Node后端开发那篇里面吧

WebSocket

WebSocket和Socket
这是一个需要着重注意的关键知识点

Socket是TCP/IP的网络API——想想看,你的浏览器是怎么连上网的?浏览器是一个应用程序(应用层),它调用操作系统提供的Socket来使用协议栈TCPUDP等传输层协议
(Socket是为了方便应用层操作传输层而抽象出来的一层,是一组API)这样一来,浏览器等应用程序才有了数据收发能力

WebSocket是一个应用层协议——它是基于TCP的,是HTML5更新的内容之一,意义是以全双工的即时通讯替代了高频轮询。使用WebSocket而不是直接调用Socket的好处就是,能够更亲和web环境,可以把通信的UI由终端的小黑窗换到了浏览器窗口

另外,这里还有一个Socket.io可以了解一下。这是一个兼容方案,兼容那些不支持H5的浏览器。(当然,它是一个第三方包,使用的话记得先要手动npm一下)

但是现在都2022了,大部分情况可以不用在意这个东西了

后记

前方是星辰大海,寻找知识,让世界成为你的画布!


文章作者: Serio
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Serio !
  目录