node学习笔记

663 阅读33分钟

什么是Node.js?

  • Node.js是基于V8 JavaScript引擎的JavaScript运行时环境

浏览器内核

事实上我们经常说的浏览器内核指的是浏览器排版引擎,也被称为浏览器引擎页面渲染引擎样板引擎

不同的浏览器有不同的内核组成:

  • Gecko:早期被Netscape和Mozilla Firefox浏览器使用
  • Trident: 微软开发,被IE4-IE11浏览器使用,但是Edge浏览器已经转向Blink
  • Webkit:苹果基于KHTML开发,开源的,用于Safari,Google Chrome之前也在使用
  • Blink:是Webkit的一个分支,Google开发,目前应用于Google Chrome,Edge,Opera

JavaScript引擎

事实上我们编写的JavaScript最后都是要交给CPU执行的,但是CPU只认识自己的指令集,所以我们需要JavaScript引擎帮助我们将JavaScript代码翻译成CPU指令来执行

比较常见的JavaScript引擎:

  • SpiderMonkey:第一款JavaScript引擎,由Brendan Eich开发
  • Chakra: 微软开发,用于IE浏览器
  • JavaScriptCore:Webkit中的JavaScript引擎,Apple公司开发
  • V8:Google开发的强大JavaScript引擎,帮助Chrome从众多浏览器中脱颖而出

V8引擎解析JavaScript代码过程

image-20210904193041577.png

  • Parse模块会通过词法分析,语法分析将JavaScript代码转化成AST(抽象语法树)。

    • 如果函数没有被调用,那么这个函数时不会被转换成AST的
    • Parse将JavaScript代码转化成AST是因为Ignition解释器并不直接认识JavaScript代码
  • Ignition是一个解释器,会将AST转换成字节码,同时还会收集信息(比如类型信息)供TurboFan使用

  • TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码

    • 如果一个函数执行多次,该函数会被标记为热点函数,那么该函数的字节码会被TurboFan转化成优化的机器码,以提高代码的执行性能
    • Ignition收集到的热点函数的信息可能在下次调用函数时,信息发生了变化,这个时候优化后的机器码不能够直接使用了,优化后的机器码会经过Deoptimization变成字节码,然后转为汇编代码,然后转为机器码,然后运行
    • 所以在某种程度上说,Ts的代码执行效率要比Js代码高,因为js代码的类型会变,而Ts不会

    Node版本工具(切换多个node版本)

n和nvm可以管理node版本

在cmd中使用nvm:

  • nvm version 查看nvm版本
  • nvm list 查看当前系统所有的node版本
  • nvm install node版本号 安装指定版本的node
  • nvm use node版本号 更换当前使用的node版本

Node的REPL

REPL是Read-Eval-Print-Loop的简称:翻译为“读取-求值-输出-循环”

REPL是一个简单的、交互式的编程环境

事实上我们可以把浏览器的console看成一个REPL

在命令行工具中输入node即可开启node的REPL环境

Node程序传递参数

正常情况下执行一个node程序,直接跟上我们对应的文件即可:node index.js

但是,在某些情况下执行node程序的过程中,我们可能希望给node传递一些参数

node index.js env=development cf

如果我们想要在程序中获取传递的参数:

  • 获取的参数其实在process的内置对象中的argv属性中,找到argv发现它是一个数组,而且包含了我们需要的参数
  • 为什么要叫argv呢?在c语言或者c++中,argc是 argument counter的缩写,表示传递参数的个数;argv是argument vector的缩写,表示传入的具体参数

特殊的全局对象

这些全局对象可以在模块中任意使用,但是在命令行交互中是不可以使用的

这些特殊的全局对象包括:__dirname(获取当前文件所在的路径,注意不包括后面的文件名),

__filename(获取当前文件所在的路径和文件名称),exports,module,require()

JavaScript模块化

什么是模块化?
  • 模块化的目的就是将程序划分成一个个小的结构
  • 在这个结构中编写自己的逻辑代码,有自己的作用域
  • 可以将自己希望暴露的的变量,函数,对象导出给其他结构使用
  • 可以通过某种方式,导入另外结构的变量,函数,对象
没有模块化带来的问题:命名冲突
IIFE(立即函数调用表达式)解决早期的没有模块化问题

IIFE带来的问题:

  • 想要使用别人模块的变量,必须知道该模块返回对象的命名
  • 代码写起来混乱不堪,每个文件的代码都需要包裹在一个匿名函数中来编写
  • 必须要有合适的命名规范,否则可能会出现模块名称相同
// bar 模块
var moduleBar = (function () {
    var name = 'aa'
    
    return {
        name
    }
})()
// baz 模块
(function() {
    console.log(moduleBar.name)
})()
commonJs和Node

commonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJs,后来为了体现它的广泛性,修改为commonJS,简称为CJS

node中对commonJS都进行了支持和实现

  • 在node中每一个文件都是一个单独的模块
  • 这个模块包括CJS规范的核心变量:exports,module.exports,require

模块化的核心是导出和导入,node中对其进行了实现:

  • exports,和module.exports可以负责对模块中的内容进行导出
  • require函数可以帮助我们**导入其他模块(自定义模块,系统模块,第三方模块模块)中的内容

exports导出

注意:exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出

// bar文件
exports.age = age
exports.name = name
​
// 另一个baz文件中导入
const bar = require('./bar')

上面导入代码完成了什么操作

  • require通过各种查找方式,最终找到了exports这个对象
  • 将exports这个对象赋值给了bar变量
  • bar变量就是exports对象了
  • 意味着baz文件中的bar变量等于exports对象(相当于引用赋值)

module.exports又是什么?

  • 在node中真正导出的是module.exports,而不是exports

为什么exports也可以导出?

  • 这是因为module对象的exports属性是exports对象的一个引用
  • 当module.exports = {} 时,exports对象就不能进行导入了

通过module.exports导出的对象,可以在导入文件里修改该对象,而且所有地方都会发生改变

require细节:require是一个函数,可以帮助我们引入一个文件(模块)中导入的对象

require的查找规则,导入格式 require(X):

  1. X是一个核心模块,比如path,http

    • 直接返回核心模块,并且停止查找
  2. X是以 ./ 或 ../ 或 / (根目录)开头的

    1. 如果有后缀名,按照后缀名的格式查找对应的文件

    2. 如果没有后缀名,会按照如下顺序:

      1. 直接查找文件X
      2. 查找X.js文件
      3. 查找X.json文件
      4. 查找X.node文件
    3. 没有找到对应的文件,将X作为一个目录

      1. 查找 X / index.js文件
      2. 查找 X / index.json文件
      3. 查找 X / index.node文件
      4. 没有找到报错
  3. 直接是一个X(没有路径),并且不是一个核心模块

    • 找当前目录node_modules文件夹下查找
    • 找上一层目录node_modules文件夹查找
    • 直到找到最外一层的node_modules文件夹
    • 还没有找到,则报错

模块加载过程:

  • 模块在第一次引用时,模块中的js代码会被运行一次

  • 模块被多次引用时,会缓存,最终只加载(运行)一次

    • 每一个模块对象module都有一个属性:loaded(false表示还没有被加载过,true表示加载过)
  • 如果有循环引入,加载顺序是什么?

    • node采用的是深度优先算法

commonJs规范缺点

  • cjs加载模块是同步的

    • 同步意味着只有等到对应模块加载完毕,当前模块中的内容才能被运行
    • 在服务器不会有问题,因为在服务器加载的js文件都是本地文件,加载速度非常快
  • 因为commonJs是同步的,所以通常在浏览器中不使用commonjs

ES module

ES module模块采用了export和import关键字来实现模块化

  • export负责将模块内的内容导出
  • imort负责从其他模块导入内容
  • 采用ES Module将自动采用严格模式

导出的三种方式

// 导出方式三种
// 1.方式一:
export const name = "why";
export const age = 18;
export const sayHello = function(name) {
  console.log("你好" + name);
}
​
// 2.方式二: {}中统一导出
// {}大括号, 但是不是一个对象
// {放置要导出的变量的引用列表}
export {
  name,
  age,
  sayHello
}
​
// 3.方式三: {} 导出时, 可以给变量起名别
export {
  name as fName,
  age as fAge,
  sayHello as fSayHello
}

通过import加载一个模块,是不可以放到逻辑代码中:比如if语句

为什么import加载一个模块不能放到逻辑代码中

  • 这是因为ES Module在被js引擎解析时,就必须知道它的依赖关系

如果我们希望动态来加载某一个模块

  • 我们可以通过import函数,该函数返回一个Promise对象
if (flag) {
    import('./bar.js').then(aaa => {
        console.log(aaa)
    })
}

ES Module加载过程:

  • ES Module加载js文件的过程是编译(解析)时加载,并且是异步

    • 编译时加载,意味着import不能和运行时相关的内容放在一起使用
    • 比如from后面的路径需要动态获取
    • 比如不能将import放到if语句中
    • 所以我们有时候也称ES Module是静态解析,而不是动态解析或者运行时解析
  • 异步意味着:Js引擎在遇到import时,会去获取这个js文件,但是这个过程是异步的,并不会阻塞主线程

    • 也就是说type=module的代码,相当于在script标签也加上了async
  • ES Module通过export导出的是变量本身的引用

    • export在导出一个变量时,js引擎会解析这个语法,并且创建有模块环境记录
    • 模块环境记录会和变量进行绑定,并且这个变量时实时的
  • 在导入的地方,我们是可以实时获取到绑定的最新变量

  • 在导入的地方不可以修改变量,因为它只是被绑定到这个变量上 (实际上是一个常量)

  • 如果导出的是一个对象,导入时可以修改对象的属性

Node对ES Module的支持
  • 默认情况下node是不支持ES Module的
  • 在package.json中配置 type:module
  • 文件以 .mjs结尾,表示使用的是ES Module

内置模块path

  • path模块用于对路径和文件进行处理,提供了很多好用的方法
  • 在Mac OS,Linux和window上路径是不一样的
  • 可移植操作系统接口(Portable Operaing System Interface, 缩写为POSIX)
常见的API
  • 从路径获取信息

    • dirname:获取文件的父文件夹
    • basename:获取文件名
    • extname: 获取文件扩展名
  • 路径的拼接

    • 如果我们希望将多个路径进行拼接,但是不同的操作系统可能使用的是不同的分割符

    • 这个时候我们可以使用path.join函数

      const path = require('path')
      ​
      const pathName = '123.txt'
      const filePath1 = 'user/abc'
      const filePath2 = '/user/abc'
      const filePath3 = './user/abc'
      const filePath4 = '../user/abc'
      ​
      const join1 = path.join(filePath1, pathName)//  user\abc\123.txt   
      const join2 = path.join(filePath2, pathName)//  \user\abc\123.txt  
      const join3 = path.join(filePath3, pathName)//  user\abc\123.txt  
      const join4 = path.join(filePath4, pathName)//  ..\user\abc\123.txt
      
  • 将文件和某个文件夹拼接

    • 如果我们希望将某个文件和文件夹拼接,可以使用path.resolve

    • resolve函数会判断我们拼接的路径前面是否有 / ../ ./

    • 如果有表示是一个绝对路径,会返回对应的拼接路径

    • 如果没有,那么会和当前执行文件所在的文件夹进行路径的拼接

      const path = require('path')
      const pathName = '123.txt'
      const filePath1 = 'user/abc'
      const filePath2 = '/user/abc'
      const filePath3 = './user/abc'
      const filePath4 = '../user/abc'
      const join1 = path.resolve(filePath1, pathName) 
      // D:\前端\node\node练习\04-常用的内置模块\01_path\user\abc\123.txt
      ​
      const join2 = path.resolve(filePath2, pathName) 
      // D:\user\abc\123.txt
      ​
      const join3 = path.resolve(filePath3, pathName) 
      // D:\前端\node\node练习\04-常用的内置模块\01_path\user\abc\123.txt
      ​
      const join4 = path.resolve(filePath4, pathName) 
      // D:\前端\node\node练习\04-常用的内置模块\user\abc\123.txt
      

内置模块fs

  • fs 是File System的缩写,表示文件系统
  • 任何一个为服务端服务的语言或者框架通常都会有自己的文件系统
fs API介绍

node文件系统的API非常多,但是这些API大多数都提供三种操作方式

  • 同步操作文件:代码会被阻塞,不会继续执行
  • 异步回调函数操作文件:代码不会阻塞,需要传入回调函数,当获取到结果时,回调函数被执行
  • 异步Promise操作文件:代码不会被阻塞,通过fs.promises调用方法操作,会返回一个promise对象,可以通过then,catch进行处理
案例:获取一个文件的状态
// 方式一:同步读取文件
const state = fs.statSync('../foo.txt')
console.log(state)
​
// 方式二:异步读取
fs.stat('../foo.txt', (err, state) => {
    if (err) {
        console.log(err)
        return
    }
    console.log(state)
})
​
// 方式三:promise方式
fs.promises.stat('../foo.txt').then(state => {
    console.log('state')
}).catch(err => {
    console.log(err)
})
文件描述符
  • 在POSIX系统上,对于每一个进程,内核都维护这一张当前打开着的文件和资源的表格

  • 每个打开的文件都分配了一个称为文件描述符的简单的数字标识符

  • fs.open() 方法用于分配新的文件描述符

    • 一旦被分配,则文件描述符可以用于从文件读取数据,向文件写入数据,或请求关于文件的信息

包管理工具

包管理工具npm

包管理工具npm:Node Package Manager Node包管理器

npm管理的包存放在哪?

  • 我们发布自己的包其实是发布到registry上面的
  • 当我们安装一个包时,其实是从registry上面下载的包
项目配置文件
  • 事实上,我们每一个项目都会有一个对应的配置文件,无论是前端项目还是后端项目

    • 这个配置文件会记录着你项目的名称,版本号,项目描述等
    • 也会记录着你项目所依赖的其他库的信息和依赖库的版本号
  • 这个配置文件在Node环境下面(无论是前端还是后端)就是package.json

创建配置文件:

  • npm init (创建时填写信息)
  • npm init -y(如果文件名有中文,则不能是用默认的)

常见的属性

必须填写的属性:name,version

  • name是项目的名称

  • version是当前项目的版本号

  • description是描述信息,很多时候是作为项目的基本描述

  • author是作者相关信息(发布时用到)

  • license是开源协议(发布时用到)

  • private属性:private属性记录当前的项目是否是私有的,当值为true时,npm是不能发布它的,这是防止私有项目或模块发布出去的方式

  • main属性

    • 设置程序的入口
    • 这个入口和webpack打包的入口并不冲突
    • 这个入口是这个库统一导出东西的地方
    • 比如 const axios = require('axios')
    • 实际上是找到对应的main属性查找文件的
  • scripts属性

    • scripts属性用于配置一些脚本命令,以键值对的形式存在

    • 配置后我们可以通过npm run 命令的key来执行这个命令,这里用到的库是局部的,如果直接在命令行中是全局的

    • 例如:“build”: "webpack" ,这里执行npm run build 相当于 npx webpack,而如果直接输入webpack则是会执行全局的webpack

    • npm start 和 npm run start的区别是什么?

      • 它们是等价的
      • 对于常用的start, test, stop, restart可以省略run直接通过npm start 运行
  • dependencies属性

    • dependencies属性是指定无论开发环境还是生成环境都需要依赖的包
  • devDependencies属性

    • 一些包在生成环境是不需要的,比如webpack,babel等
    • 这个时候我们会通过npm install webpack --save-dev(或者-D),将它安装到devDependencies属性中
    • 在生成环境中不需要安装一些开发依赖的包时,通过npm install --production来安装依赖
  • engines属性

    • engines属性用于指定Node好NPM的版本号
    • 在安装过程中,会先检查对应的引擎版本,如果不符合就会报错
    • 事实上也可以指定所在的操作系统 "os": ["drawin", "linux"]只是很少用到
  • browerlist属性:

    • 用于配置打包后的JavaScript浏览器的兼容情况
版本管理问题

npm的包通常需要遵从semver版本规范

semver版本规范是X.Y.X:

  • X主版本号(major):当你做了不兼容的API修改(可能不兼容之前的版本)
  • Y次版本号(minor):当你做了向下兼容的功能性新增(新功能增加,但是兼容之前的版本)
  • Z修订号(patch):当你做了向下兼容的问题修正(没有新功能,修复了之前的bug)

我们安装的依赖版本会出现:^2.0.3 或者 ~2.0.3,这是什么意思呢?

  • ^x.y.z: 表示x是保持不变的,y和z永远安装最新的版本
  • ~x.y.z:表示x和y保持不变,z永远安装最新的版本
npm install 命令
  • 安装npm 包分为两种情况:

    • 全局安装:npm install yarn -g
    • 局部安装:npm install
npm install 原理

当执行npm install 时,内部原理:

  • 首先检测是否有package-lock.json文件

  • 没有lock文件

    • 分析依赖关系,这是因为我们要下载的包可能会依赖其他的包,并且多个包之间会产生相同的依赖情况
    • 从registry仓库中下载压缩包(如果设置了镜像,那么会从镜像服务器下载压缩包)
    • 获取到压缩包后会对压缩包进行缓存(从npm5开始有的)
    • 将压缩包解压到项目的node_modules文件夹中(require的查找顺序会在该包下面查找)
    • 生成package-lock.json文件
  • 有lock文件:

    • 检测依赖一致性:检查lock文件中包的版本是否和package.json中一致(按照semver版本规范检测)
    • 如果不一致直接和没有lock文件一样
    • 一致的情况下,会优先查找缓存,没有查找到,会去registry仓库下载,直接走顶层流程
    • 查找到,会获取缓存中的压缩文件,并且将压缩文件解压到node_modules文件夹中

image-20210909173015332.png

image-20210909173307413.png

npm 其他命令
  • 卸载某个依赖包

    • npm uninstall package
    • npm uninstall package --save-dev
    • npm uninstall package -D
  • 强制重新build

    • npm rebuild
  • 清除缓存

    • npm cache clean
Yarn 工具
  • yarn是为了弥补npm的一些缺陷而出现的
  • 早期的npm存在很多的缺陷,比如安装依赖速度很慢,版本依赖混乱等等一系列的问题
  • 虽然从npm5版本开始,进行了很多的升级和改进,但是依然很多人喜欢yarn

image-20210909182620627.png

cnpm工具
  • 由于一些特殊的原因,某些情况下我们没办法很好的下载下来一些需要的包

  • 查看npm镜像

    • npm config get registry
  • 可以直接设置npm的镜像

  • 但是对于大多数人来说,并不希望将npm镜像修改了

    • 第一,不太希望随意修改npm原本从官方下载包的渠道
    • 第二,担心某天淘宝的镜像挂了或者不维护,又该修改
  • 这个时候,我们可以使用cnpm,并且将cnpm设置为淘宝镜像

npx工具
  • npx工具是npm5.2之后自带的一个命令

    • npx的作用非常多,但是比较常见的是使用它来调用项目的某个模块的指令
解决局部命令执行(以webpack为例)
  • 方式一:明确查找到node_modules下面的webpack

    • 在终端使用如下命令:./node_modules/.bin/webpack
  • 方式二:修改package.json中的scripts

    • "scripts": { "webpack": "webpack" }
  • 方式三:使用npx

    • npx weback

npx的原理非常简单,它会到当前目录下的node_modules/.bin目录下查找对应的命令

Buffer

  • 对于前端开发来说,通常很少会和二进制打交道,但是对于服务器端为了做很多的功能,我们必须直接去操作其二进制的数据

  • Node为了可以方便开发者完成更多功能,提供给了我们一个类Buffer,并且它是全局的

  • Buffer中存储的是二进制数据

    • 我们可以将Buffer看成是一个存储二进制的数组
    • 这个数组中的每一项,可以保存8位二进制
  • 为什么是8位?

    • 在计算机中,很少情况会直接操作一位二进制,因为一位二进制存储的数据是非常有限的
    • 通常会将8位合在一起作为一个单元,这个单元称为一个字节(byte)
创建Buffer数组

Buffer.from

  • Buffer.from('字符串', 编解码(默认utf8))可以创建一个buffer
  • const buffer = Buffer.from(message);
  • console.log(buffer.toString() ); 对字节进行解码
  • 使用utf8字符编码一个英文一个字节一个中文三个字节

Buffer.alloc

  • Buffer.alloc(8) 创建一个8个字节的buffer
  • 默认情况下buffer里的字节都是0

事件循环

什么是事件循环?
  • 把事件循环理解成我们编写的JavaScript和浏览器或者Node之间的一个桥梁
  • 浏览器的事件 循环是一个我们编写的JavaScript代码和浏览器API调用的一个桥梁,桥梁之间通过回调函数进行沟通
  • Node的事件循环是一个我们编写的JavaScript代码和系统调用之间的桥梁,桥梁之间通过回调函数进行沟通
进程和线程
  • 进程和线程是操作系统的两个概念

    • 进程(process):计算机已运行的程序
    • 线程(thread):操作系统能够运行调度的最小单位
  • 进程类似于工厂中的车间

  • 线程类似于车间中的工人

多进程多线程开发
  • 操作系统是如何做到同时让多个进程同时工作的?

    • 这是因为cpu的运算速度非常快,它可以快速的在多个进程之间迅速的切换
    • 当我们的进程中的线程获取到时间片时,就可以快速执行我们编写的代码
    • 对于用户来说是感受不到这种快速的切换
浏览器和JavaScript
  • 我们经常说JavaScript是单线程,但是JavaScript的线程应该有自己的容器进程:浏览器或者node

  • 浏览器是一个进程吗,它只有一个线程吗?

    • 目前多数的浏览器其实是多线程的,当我们打开一个tab页面是就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出
    • 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程
浏览器的事件循环
  • JavaScript是单线程的,如果在执行JavaScript代码过程中有异步操作呢?

    • 例如代码中有一个定时器,该定时器传入了一个函数作为参数
    • 这个定时器函数会被放入到调用栈中,执行后立即结束,不会阻塞后面的代码执行
  • 那么,定时器函数传入的回调函数会在什么时候被执行呢?

    • 事实上,setTimeout是调用了web API,在合适的时机,会将这个回调函数加入到一个事件队列中
    • 事件队列中的函数,会在JavaScript同步代码结束后放入调用栈,在调用栈中被执行
宏任务和微任务
  • 浏览器事件循环中并非只维护着一个队列,事实上有两个队列:

    • 宏任务队列(macrotask queue):ajax,setTimeout,setInterval,DOM监听,UIrendering
    • 微任务队列(microtask queue):Promise的then回调,Mutation Observer API,queueMicrotask()
  • 事件循环对于两个队列的优先级?

    • 在执行任何一个宏任务之前都会先查看微任务队列中是否有任务需要执行

      • 也就是说宏任务执行前,必须保证微任务队列是空的
      • 如果不为空,那么就先执行微任务队列中的任务
    // 面试题一
    setTimeout(function () {
      console.log("set1");
      new Promise(function (resolve) {
        resolve();
      }).then(function () {
        new Promise(function (resolve) {
          resolve();
        }).then(function () {
          console.log("then4");
        });
        console.log("then2");
      });
    });
    ​
    new Promise(function (resolve) {
      console.log("pr1");
      resolve();
    }).then(function () {
      console.log("then1");
    });
    ​
    setTimeout(function () {
      console.log("set2");
    });
    ​
    console.log(2);
    ​
    queueMicrotask(() => {
      console.log("queueMicrotask1")
    });
    ​
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then3");
    });
    ​
    // pr1 2 then1 queuemicrotask1 then3 set1 then2 then4 set2
    
    // 面试题二
    async function async1 () {
      console.log('async1 start')
      await async2();
      console.log('async1 end')
    }
     
    async function async2 () {
      console.log('async2')
    }
    ​
    console.log('script start')
    ​
    setTimeout(function () {
      console.log('setTimeout')
    }, 0)
     
    async1();
     
    new Promise (function (resolve) {
      console.log('promise1')
      resolve();
    }).then (function () {
      console.log('promise2')
    })
    ​
    console.log('script end')
    ​
    // script start
    // async1 start
    // async2
    // promise1
    // script end
    // aysnc1 end
    // promise2
    // setToueout
    
node的架构分析
  • 浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而node中是有libuv实现的
  • libuv中主要维护了一个EventLoop和worker threads(线程池)
  • EventLoop负责调用系统的一些其他操作:文件的IO,Network,child-process

image-20210910110804198.png

阻塞IO和非阻塞IO
  • 操作系统为我们提供了阻塞式调用和非阻塞式调用:

    • 阻塞式调用:调用结果返回之前,当前线程处于阻塞态(阻塞态CPU是不会分配时间片的),调用线程只有在得到调用结果之后才会继续执行
    • 非阻塞式调用:调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有返回结果即可
  • 在开发中的很多耗时操作,都可以基于这样的非阻塞式调用

非阻塞IO的问题
  • 非阻塞IO也会存在一定的问题:我们并没有获取到需要读取(以读取为例)的结果

    • 那么就意味着为了可以知道是否读取到了完整的数据,我们需要频繁的去确定读取到的数据是否是完整的
    • 这个过程我们称之为轮询操作
  • 那么这个轮训的工作由谁来完成呢?

    • 如果我们的主线程频繁的去进行轮训的工作,那么必然会大大降低性能
    • 并且开发中我们可能不只是一个文件的读写,可能是多个文件
    • 而且可能是多个功能:网络的IO,数据库的IO,子进程的调用
  • libuv提供了一个线程池(Thread Pool):

    • 线程池会负责所有相关的操作,并且会通过轮询等方式等待结果
    • 当获取到结果时,就可以将对应的回调放到事件循环(某一个队列中)
    • 事件循环就可以负责接管后续的回调工作,告知JavaScript应用程序执行对应的回调函数
node事件循环的阶段
  • 无论是文件IO,数据库,网络IO,定时器,子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环中(任务队列中)

  • 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行

  • 但是一次完整的事件循环Tick分成很多个阶段“

    • 定时器(Timers) :本阶段执行已经被setTimeout和setInterval的调度回调函数
    • 待定回调(Pending Callback) :对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED
    • idle,prepare:仅系统内部使用
    • 轮询(Poll) :检索新的I/O事件,执行与I/O相关的回调(node会在这里停留相对较长的时间,希望io的回调尽可能早的响应)
    • 检测(check) :setImmediate()回调函数在这里执行
    • 关闭的回调函数:一些关闭的回调函数,如:socket.on(‘close’)
node的微任务和宏任务
  • 宏任务(macrotask):setTimeout,setInterval,IO事件,setImmediate,close
  • 微任务(microtask):promise的then回调,process.nextTick,queueMicrotask

node中有多个队列,执行顺序如下

image-20210910202627134.png

node事件循环面试题
// 面试题一
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
​
async function async2() {
  console.log('async2')
}
​
console.log('script start') 
​
setTimeout(function () {
  console.log('setTimeout0')
}, 0)
​
setTimeout(function () {
  console.log('setTimeout2')
}, 300)
​
setImmediate(() => console.log('setImmediate'));
​
process.nextTick(() => console.log('nextTick1'));
​
async1();
​
process.nextTick(() => console.log('nextTick2'));
​
new Promise(function (resolve) {
  console.log('promise1')
  resolve();
  console.log('promise2')
}).then(function () {
  console.log('promise3')
})
​
console.log('script end')
​
// script start
// async1 start
// async2
// promise1
// promise2
// script end
// nextTick1
// nextTick2
// async1 end
//  promise3
// setTimeout0
// setImmediate
// setTimeout2
// 面试题二
setTimeout(() => {
  console.log("setTimeout");
}, 0);
​
setImmediate(() => {
  console.log("setImmediate");
});
// 可能是setTimeout  setImmediate
// 也可能是setImmediate  setTimeout

为什么呢?

  • node在源码中有一个函数决定了,poll阶段要不要阻塞在这里,阻塞在这里的目的是当有异步IO被处理时,尽可能快的让代码被执行

  • 执行setTimeout和setInterval时,node会将回调函数保存到某个地方,等到满足条件再将回调函数放到timer队列,而node会直接将setImmediate的回调函数方法放到check队列

  • 情况一:如果事件循环开启的时间是小于setTimeout放入到timer队列的时间

    • 也就意味着先开启了event-loop,但是这个时候执行到timer阶段,并没有将定时器的回调放到timer队列中
    • 所以没有被执行,后续开启定时器和检测到有setImmediate时,就会跳过这个poll阶段,向后继续执行
    • 这个时候是先检测setImmediate,第二次tick中执行了timer中的setTimeout
  • 情况二:如果事件循环开启的时间大于setTimeout函数的执行时间

    • 这就意味着在第一次tick中,已经将setTimeout的回调函数加入到timer 队列中
    • 所以会直接按照顺序执行即可

Stream

认识Stream
  • 可以这样理解流:

    • 是连续字节的一种表现形式和抽象概念
    • 流应该是可读的,也是可写的
  • 之前学习文件的读写时,我们可以直接通过readFile或者writeFile方式读写文件,为什么还需要流呢?

    • 直接读写文件的方式,虽然简单,但是无法控制一些细节的操作
    • 比如从什么位置开始读,读到什么位置,一次性读多少个字节
    • 读到某个位置后,暂停读取,某个时刻恢复读取等等
    • 或者这个文件非常大,比如一个视频文件,一次性全部读取并不合适
文件读写的Stream
  • 事实上Node中很多对象是基于流实现的:

    • http模块的Request和Response对象
    • process.stdout对象
  • 官方:另外所有的流都是EventEmitter的实例:

  • Node.js中有四种基本流类型:

    • Writable:可以向其写入数据的流
    • Readable:可以从中读取数据的流
    • Duplex:同时为Readable和Writable的流
    • Transform:Duplex可以在写入和读取数据时修改或转换数据的流

Http模块

Web服务器初体验
const http = require('http')
​
//  创建服务器方法一
const server = http.createServer((req, res) => {
    res.end('hello server')
})
//  创建服务器方法二
const server2 = new http.server((req, res) => {
    res.end('hello server2')
})
​
server.listen(8080, () => {
    console.log('服务器启动成功')
})

express框架

express安装
  • 方式一:通过express提供的脚手架,直接创建一个应用的骨架
  • 方式二:从零搭建自己的express应用结构

方式一:安装express-generator

  • npm install express-generator -g
  • 创建项目 express express-demo
  • 安装依赖 npm install
  • 启动项目 node bin/www

方式二:从零搭建自己的express应用结构

  • npm init -y 创建package.json文件
express的基本使用
const express = require('express')
​
// express 其实是一个函数:createApplication
const app = express()
​
// 监听默认路径
app.get('/', (req, res, next) => {
    res.end('hello express')
})
app.post('/', (req, res, next) => {
    
})
​
app.listen(8000, () => {
    console.log('express服务器启动成功')
})
认识中间件
  • express是一个路由和中间件的Web框架,它本身的功能非常少

    • express应用程序本质上是一系列中间件函数的调用
  • 中间件是什么?

    • 中间件的本质是传递给express的一个回调函数

    • 这个回调函数接受三个参数:

      • 请求对象(request对象)
      • 响应对象(response对象)
      • next函数(在express中定义的用于执行下一个中间件的函数)
  • 中间件中可以执行哪些任务呢?

    • 执行任何代码
    • 更改请求(request)和响应(response)对象
    • 结束请求-响应周期(返回数据)
    • 调用栈中的下一个中间件
  • 如果当前中间件功能没有结束请求-响应周期,则必须调用next()将控制器传递给下一个中间件功能,斗则,请求将被挂起

应用中间件-直接编写
  • 如何将一个中间件应用到我们的应用程序中呢?

    • express主要提供了两种方式:app/router.useapp/router.methods
    • 可以是app,也可以是router
    • methods指的是常用的请求方式,比如:app.get或者app.post
  • use的用法,methods的方法本质是use的特殊情况

    • 最普通的中间件

      app.use((req, res, next) => {
          console.log('普通中间件')
      })
      
    • path匹配中间件

      app.use('/home', (req, res, next) => {
          console.log('路径匹配中间件')
      })
      
    • path和methods匹配中间件

      app.get('/home', (req, res, next) => {
          console.log('path 和 methods中间件')
      })
      
    • 注册多个中间件

      app.get('/home', (req, res, next) => {
          next()
      }, (req, res, next) => {
          console.log('注册多个中间件')
      })
      
应用中间件-express提供
  • 当客户端发送请求时携带json数据的body参数时

image-20210911194713838.png

  • 可以自己编写解析request body中间件
app.use((req, res, next) => {
    if (req.headers['content-type' === 'application/json']) {
        req.on('data', (data) => {
            const userInfo = JSON.parse(data.toString())
            req.body = userInfo
        })
        req.on('end', () => {
            next()
        })
    } else {
        next()
    }
})
app.post('/login', (req, res, next) => {
    console.log(req,body)
    res.end('')
})
  • 但是,事实上我们可以使用express内置的中间件或者使用body-parser
// 可以帮助我们解析body参数
// body-parser: express3.x 内置express框架
// body-parser: express4.x 被分离出去
app.use(express.json())
​
// 解析application/x-www-form-urlencoded
// extended
// true:那么对urlencoded进行解析时,使用第三方库:qs
// false: 那么对urlencoded进行解析时,使用Node内置模块:querystring
app.use(express.urlencoded({ extended: true }))
应用中间件-第三中间件
  • 如果我们希望将请求日志记录下来,那么可以使用express官网开发的第三方库:morgan

    • 需要单独安装 npm install morgan
    const morgan = require(morgan)
    const writerStream = fs.createWriteStream('路径', { flags: 'a+' })
    app.use(morgan('combined', { stream: writerStream }))
    
  • 上传文件时,我们可以使用express提供的multer来完成

    // form-data 解析
    const express = require('express')
    const multer = require('multer')
    ​
    const app = express()
    ​
    app.use(express.json())
    app.use(express.urlencoded({ extended: true }))
    ​
    const upload = multer()
    ​
    app.use(upload.any())
    ​
    app.post('/login', (req, res, next) => {
       console.log(req.body)
    })
    app.listen(8000, () => {
        
    })
    
    // form-data上传文件
    const express = require('express')
    const multer = require('multer')
    ​
    const app = express()
    ​
    app.use(express.json())
    app.use(express.urlencoded({ extended: true }))
    ​
    const storage = multer.diskStorage({
        destination: (req, file, cb) => {
            cb(null, './uploads')
        },
        filename: (req, file, cb) => {
            cb(null, Date.now() + path.extname(file.originalname))
        }
    })
    const upload = multer({
        storage
    })
    ​
    app.post('/login', upload.any(), (req, res, next) => {
        
    })
    ​
    app.post('/upload', upload,array('file'), (req, res, next) => {
        
    })
    ​
    app.listen(8000, () => {
        
    })
    
传递参数params和query

请求地址:http://localhost:8000/login/abc/why

app.use('/login/:id/:name', (req, res, next) => {
    console.log(req.params) // 获取参数
    res.json(req.params)
})

请求地址:http://localhost:8000/login?username=why&password=123

app.use('/login', (req, res, next) => {
    console.log(req.query) // 获取参数
    res.json('请求成功')
})
响应数据
  • end方法:类似于http中的response.end方法,用法是一致的

  • json方法:

    • json方法中可以传入很多的类型:object,array,string,boolean,number,null等,它们会被转换成json格式返回
  • status方法

    • 用于设置状态码
express的路由
  • 如果我们将所有的代码逻辑都写在app中,那么app会变得越来越复杂“

    • 一方面完整的web服务器包含非常多的处理逻辑
    • 另一方面有些处理逻辑其实是一个整体,我们应该将它们放在一起
// 用户相关的处理
// 创建router
const userRouter = express.Router()
​
userRouter.get('/', (req, res, next) => {
    res.end('用户列表')
})
​
userRouter.post('/', (req, res, next) => {
    res.end('创建用户')
})
​
userRouter.delete('/', (req, res, next) => {
    res.end('删除用户')
})
​
app.use('/users', userRouter)  // 注册路由
静态资源服务器
  • 部署静态资源我们可以选择很多方式

    • node中也可以作为静态资源服务器,并且express给我们提供了方便部署静态资源的方法
const express = require('express')
​
const app = express()
​
app.use(express.static('./build'))
​
app.listen(8000, () => {
    console.log('静态服务器启动成功')
})
错误处理
const express = require('express');
​
const app = express();
​
const USERNAME_DOES_NOT_EXISTS = "USERNAME_DOES_NOT_EXISTS";
const USERNAME_ALREADY_EXISTS = "USERNAME_ALREADY_EXISTS";
​
app.post('/login', (req, res, next) => {
  // 加入在数据中查询用户名时, 发现不存在
  const isLogin = false;
  if (isLogin) {
    res.json("user login success~");
  } else {
    // res.type(400);
    // res.json("username does not exists~")
    next(new Error(USERNAME_DOES_NOT_EXISTS));
  }
})
​
app.post('/register', (req, res, next) => {
  // 加入在数据中查询用户名时, 发现不存在
  const isExists = true;
  if (!isExists) {
    res.json("user register success~");
  } else {
    // res.type(400);
    // res.json("username already exists~")
    next(new Error(USERNAME_ALREADY_EXISTS)); // next如果传递参数,就是传递错误的参数
  }
});
​
app.use((err, req, res, next) => { // 处理错误的中间件
  let status = 400;
  let message = "";
  console.log(err.message);  // next传递的参数会传给
​
  switch(err.message) {
    case USERNAME_DOES_NOT_EXISTS:
      message = "username does not exists~";
      break;
    case USERNAME_ALREADY_EXISTS:
      message = "USERNAME_ALREADY_EXISTS~"
      break;
    default: 
      message = "NOT FOUND~"
  }
​
  res.status(status);
  res.json({
    errCode: status,
    errMessage: message
  })
})
​
app.listen(8000, () => {
  console.log("路由服务器启动成功~");
});

Koa框架

Koa初体验

const Koa = require('koa')
​
const app = new Koa()
​
app.use((ctx, next) => { 
    next()
})
​
app.use((ctx, next) => {
    ctx.response.body = 'hello koa'
})
​
app.listen(8000, () => {
    console.log('koa服务器启动成功')
})
​
koa注册的中间件提供了两个参数
  • ctx: 上下文对象

    • koa没有像express一样,将req,res分开,而是将他们作为ctx的属性
    • ctx.request:获取请求对象
    • ctx.response:获取响应对象
  • next:本质是一个dispatch,类似于之前的next

koa中间件
  • koa通过创建的app对象,注册中间件只能通过use方法

    • koa并没有提供methods的方式来注册中间件
    • 没有提供path中间件来匹配路径
  • 但是真实开发中我们如何将路径和method分离呢

    • 方式一:根据request自己来判断
    • 方式二:使用第三方路由中间件

image-20210927182229473.png

路由的使用
  • koa官方并没有给我们提供路由的库,我们可以选择第三方库:koa-router
  • npm install koa-router
const Router = require('koa-router')
const userRouter = new Router({ prefix: '/users' }) // prefix 设置路径前缀
userRouter.get('/', (ctx, next) => {
    ctx.response.body = "..."
})
app.use(userRouter.routes())  // 将userRouter.routes注册为中间件
app.use(userRouter.allowMethods()) 
  • 上面代码中,allowMethods是用于判断某个method是否支持

    • 如果我们请求get,那么是正常请求,因为我们有实现get
    • 如果请求put,delete等等,那么就会自动报错:Method Not Allowed,状态码405;
    • 如果我们请求link,copy,lock,那么久自动报错:Not Implemented,状态码:501
参数解析:params-query
  • 获取params,通过ctx.params获取
const userRouter = new Router({ prefix: '/users' })
userRouter,get('/:id', (ctx, next) => {
    console.log(ctx.params.id)  // 
})
  • 获取query,通过ctx.request.query
app.use((ctx, next) => {
    console.log(ctx.request.query)
    ctx.body = 'hello koa'   // 设置响应数据
})
参数解析:json , x-www-form-urlencoded
  • body是json格式:
{
    "username": 'aaaa',
    "password": "123"
}
  • 获取json数据:通过ctx.request.body

    • 安装依赖:npm install koa-bodyparser
    • 使用koa-bodyparser中间件
const bodyparser = require('koa-bodyparser')
app.use(bodyparser())
app.use((ctx, next) => {
    console.log(ctx.request.body) // 可以获取到json格式和urlencoded格式的body数据
    ctx.body = "hello bodyparser"
})
  • 获取body是x-www-form-urlencoded格式:和json获取方式是一致的

image-20210927184706866.png

参数解析:form-data
  • body是form-data格式: 通过ctx.req.body获取

image-20210927185115917.png

  • 解析body中的数据,我们需要multer

    • 安装依赖:npm install koa-multer
    • 使用multer中间件
const multer = require('koa-multer')
​
const upload = multer({})
​
app.use(upload.any())
​
app.use((ctx, next) => {
    console.log(ctx.req.body)  // 获取form-data数据
})
multer上传文件
const path = require('path')
​
const Koa = require('koa')
const Router = require('koa-router')
const multer = require('koa-multer')
const bodyParser = require('koa-bodyparser')
​
const userRouter = new Router({ prefix: '/user' })
const storage = multer.diskZStorage({
    destination: (req, file, cb) => { // 设置上传时文件的存放位置
        cb(null, '/upload')   
    },
    filename: (req, file, cb) => {
        cb(null, Date.now() + path.extname(file.originalname))
        // file.originalname可以获取到上传时文件的名字
    }
})
​
const upload = multer({ storage })
​
const app = new Koa()
app.use(bodyParser())
​
userRouter.get('/:id', upload.any(), (ctx, next) => {
    
})
app.use(userRouter.routes())
​
app.listen(8000, () => {
  console.log('服务器启动成功');
})
数据的响应
  • 输出结果:body将响应主体设置为一下之一

    • string:字符串数据
    • Buffer:Buffer数据
    • Stream: 流数据
    • Object||Array:对象或者数组
    • null: 不输出任何内容
    • 如果response.status尚未设置,Koa会自动将状态设置为404
// 相应数据的方式
ctx.response.body = "string"
ctx.body = {
    name: "aaa",
    age: 18
}
ctx.body = ['aa', 'ccc']
// 设置响应状态 默认
ctx.status = 201
ctx.response.status = 204
静态资源服务器
  • koa并没有内置部署相关的功能,所以我们需要使用第三方库
  • npm install koa-static
  • 部署的过程类似于express
const Koa = require('koa')
const static = require('koa-static')
​
const app = new Koa()
app.use(static('./build'))
app.listen(8000, () => {
    
})
错误处理
const Koa = require('koa')
​
const app = new Koa()
​
app.use((ctx, next) => {
    ctx.app.emit('error', new Error('hahaha'), ctx) // 发出错误事件
})
app.on('error', (err, ctx) => { // 监听错误
    console.log(err.message)
    ctx.response.body = 'hahaha'
})
​
app.listen(8000, () => {
    console.log('错误处理服务启动成功')
})