node express koa 学习笔记

Enode.js 是基于v8 JavaScript引擎的 JavaScript运行时环境

任何可以使用 JavaScript来实现的应用都最终都会 使用 JavaScript 实现

1、全局对象和模块化开发

1.1 给node 程序传递参数

当我们 执行 node ./index.js 的时候其实,后面可以拼接参数,参数就是 node 的全局对象 process ,比如 node ./index.js ddg age=20

  argv: [
    'E:\\node\\node.exe',
    'F:\\web前端开发\\自学\\code\\01_learn-node\\02_给node传递参数\\index.js',
    'ddg',
    'age=20'
  ],
// console.log(process);
// process 是 node 的 全局对象
// process 第一层节点,有一个 argv 节点, 它表示的就是参数
// argv : argument vector 的缩写,表示 传入的具体参数
console.log(process.argv);

process.argv.forEach(item => {
    console.log(item);
})
1.2 node 的输出方式
  • console.log() 最常用的输入内容的方式 console.log
  • 清空控制台 console.clear
  • console.trace() 查看函数的 调用栈
// console.log(process);
// process 是 node 的 全局对象
// process 第一层节点,有一个 argv 节点, 它表示的就是参数
// argv : argument vector 的缩写,表示 传入的具体参数
console.log(process.argv);

console.log(process.argv[2]);
console.log(process.argv[3]);

console.clear() // 清空 控制台

process.argv.forEach(item => {
    console.log(item);
})

function foo() {
    bar()
}
function bar() {
    console.trace()
    // trace 是可以打印 函数 的 调用栈的 

    // 输出结构如下
    // at bar 表示 这个输出 在 bar 函数 里面执行
    // at foo 表示  bar 函数 是在 foo 函数里面被调用
    // at Object 表示 foo 在全局调用,  node 会把他当成 匿名函数调用
    /* 
    Trace
    at bar (F:\web前端开发\自学\code\01_learn-node\02_给node传递参数\02_Node程序的输出.js:20:13)
    at foo (F:\web前端开发\自学\\code\01_learn-node\02_给node传递参数\02_Node程序的输出.js:17:5)
    at Object. (F:\web前端开发\自学\code\01_learn-node\02_给node传递参数\02_Node程序的输出.js:36:1)
    at Module._compile (internal/modules/cjs/loader.js:1133:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1153:10)
    at Module.load (internal/modules/cjs/loader.js:977:32)
    at Function.Module._load (internal/modules/cjs/loader.js:877:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)
    at internal/main/run_main_module.js:18:47
    */
}
foo()

node的输出方式官方文档

1.3 node 全局对象
1.3.1 特殊的全局对象

这些全局对象实际上是模块中的变量,只是每个模块都有,看起来像是全局变量,在命令行交互中是不可以使用的

包括 __dirname、__filename、exports、module、require()

  • dirname: 获取当前文件所在的路径,不包括后面的文件名
  • filename 获取当前文件所在的路径和文件名称,包括文件名称
console.log(__dirname);
// F:\web前端开发\自学\code\01_learn-node\03_node中的全局变量
console.log(__filename);
// F:\web前端开发\自学\code\01_learn-node\03_node中的全局变量\01_特殊的全局对象.js
1.3.2 常见全局对象
process对象
  • process 提供了 node 进程中相关的信息
  • 后面在项目中,我也会讲解,如何将一些环境变量读取到 process 的 env 中
  • node 的运行环境、参数信息等等

console.log

可以看官方文档


定时器函数
  • setTimeout(callback,delay[,…args]):callback,在 delay毫秒后执行一次
  • setIneterval(callback,delay[,…args]):callback 每 delay 毫秒重复执行一次
  • setImmediate(callback[,…args]):callback I/O 事件后的回调的 “立即” 执行
  • process.nextTick(callback[,…args]):添加到下一次 tick 队列中

global 全局对象
console.log(global);
console.log(global.process);
// 定义变量
var name = "呆呆狗"
console.log(name);
console.log(global.name);// undefined
// node 的顶级对象 是 global  ,浏览器的顶级对象是 window,我们在浏览器运行的js文件中var定义的全局变量,是会被挂载到 window 上的,而 global 不会
// global 默认会挂载 process
// 再 node 中,其实每一个文件都是一个模块

2、js 的模块化

2.1 common.js 和 node

commonJS 是一个 规范,最初提出来是再浏览器以外的地方使用,并且当时被命名为 Server.js,后来为了体现他的广泛性,修改为 CommonJS , 平时我们也会简称为 CJS

  • node 是 commonJS 在服务器端 一个具有代表性的实现
  • Browserify 是 CommonJS 在浏览器中的一种实现
  • webpack 打包工具具备对 CommonJS 的支持和转换

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

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

// bar.js
const name = "ddg"
const age =20

function sayHello(name){
  console.log('hello' + name);
}

exports.name = name
exports.age = age
exports.sayHello = sayHello
// main.js
const bar = require('./bar')
// 就相当于  bar = exports
// bar  也可以 换成 { name . age .sayHello } 解构赋值
console.log(bar);//{ name: 'ddg', age: 20, sayHello: [Function: sayHello] }

console.log(bar.name);
console.log(bar.age);

bar.sayHello('ddg')
2.2 module.exports
  • commonJS 中是没有 module.exports 的概念的
  • 为了实现模块的导出,node 中使用的是 module 的类,每一个模块都是 module 的一个实例 也就是 module
  • 在node 中真正用于导出的其实根本不是 exports 而是 module.exports
  • 因为 module 才是真正的实现者

其实就是导出的 module,源码中 module.exports = exports

2.3 require

查找规则

  1. require(传入的是核心模块 fs path 等等) ,则会直接返回,并且停止查找
  2. require(./或者…/ 或者 、/ 根目录) 开头的。没有后缀名则会 直接查找 这个文件 ;查找 文件.js文件 ; 查找 文件.json 文件;查找 文件.node 文件。 如果直接传一个目录,则会先找这个目录下的 index.js 文件 ; index.json 文件 ; index.node 文件。 如果 这两者都没找到 则报错
  3. 直接是一个字符串 比如 require(‘abc’) ,abc 不是核心模块,则会从当前目录下的 node_modules 文件夹下查找,没有找到 再去上一级目录下的 node_modules 下查找,直到 根目录下的 node_modules 根目录在没找到则会报错
    paths: [
      'F:\\web前端开发\\自学\\code\\01_learn-node\\04_js-module\\02_commonjs\\node_modules',
      'F:\\web前端开发\\自学\\code\\01_learn-node\\node_modules',
      'F:\\web前端开发\\自学\\code\\node_modules',
      'F:\\web前端开发\\自学\\node_modules',
      'F:\\web前端开发\\自学\\node_modules',
      'F:\\web前端开发\\node_modules',
      'F:\\node_modules'
    ]

// console.log(module)
2.4 模块加载的过程
  • 模块在被第一次引入的时候,模块中的js代码会被运行一次。是同步执行的
  • 模块被多次引入时,会缓存,最终只会被加载(运行)一次,因为每个模块对象module 都有一个属性:loaded ,为 false 的时候,表示没有被加载,true 表示已经被加载了

此图中,就是循环引入, 就是 图 数据结构, 图结构在遍历的时候,node 采用了 深度优先搜索,所以 main => aaa => ccc => ddd => eee

走到 eee 里面没有引入了,会返回上一层,看看上一层还有没有引入其他的,没有在返回,直到 main,看到还引入了 bbb 则 引入 bbb => ccc

2.5 AMD 规范

AMD 主要是应用于浏览器的一种模块化规范

它采用的是 异步加载模块

事实上 AMD 的规范 还要早于 CommonJS,但是 CommonJS目前依然在被使用,而 AMD 使用的较少了

用起来 是 有点复杂了。需要引入一个 require.js

2.6 CMD 规范

也是一个异步加载模块

但是他将 commonJS 的 优点吸收过来了

seajs 需要用这个依赖文件

2.7 es module

他是 js 的一个模块化系统

  • 它采用了 import 和 export 关键字
  • 采用编译期的静态分析,并且也加入了动态引用的方式
  • export 负责将模块的内容导出
  • import 负责从其他模块导入内容

导出的方式

  • export const age = 20
  • export { } 这只是一个大括号,不是一个对象,这个大括号放置要导出的引用列表
  • export { name as myName } 可以给 name 起一个别名

导入的方式

  • import { name } from ‘路径’ 按需导入
  • import { name as heName } from ‘路径’ 也可以起别名
  • import * as foo from ‘路径’ 把导出的东西,都放到 foo 对象里面

在开发和封装功能的时候,通常希望将暴露 的所有接口放到一个文件中

export {  } from './bar.js'
// 这样表示的是,先导入,然后 再导出

默认导出

export default function (){}

import format from ‘./’

我们可以直接给这个默认导出的函数起一个名字


如果 把 import 加载一个模块,放到 逻辑代码中,是不可以的

因为 es module 在被 JS 引擎解析时,就必须要知道他的依赖关系,由于这个时候,无法执行,所以不能确定依赖关系,所有会报错

let flag = true
if (flag) {
  import('./bar.js').then(aaa => {
    console.log()
  })
}

3、常见内置模块

3.1 path
const path = require('path')

// 1、获取路径的信息
const filepath = '/User/ddg/abc.md'

console.log(path.dirname(filepath));
// /User/ddg
console.log(path.basename(filepath)); 
// abc.md
console.log(path.extname(filepath)); 
// .md

// 2、join 路径拼接
const basepath = "/User/ddg"
const filename= '/abc.md'
const joinUrl = path.join(basepath,filename)
console.log(joinUrl);//  \User\ddg\abc.md

// 3、resolve 路径拼接
// 它会解析第一个参数 的 / ./ ../ ,
// / 表示这个文件地址的根磁盘
// 如果第一个参数 没有加 / ./ ../ 那么,则会拼接上 绝对路径
// 如果第一个往后的参数有 / 则会 直接返回最后一个参数的路径
const joinUrl2 = path.resolve(basepath,filename)
console.log(joinUrl2);//  F:\User\ddg\abc.md

3.2 fs

读取文件信息的三种方式

const fs = require('fs')

// 读取文件的信息
const filepath = './abc.txt'

// 1、同步操作  读取文件信息
const info = fs.statSync(filepath)
// console.log('后续要执行的代码会被阻塞');
// console.log(info);

// 2、异步操作  读取文件信息
fs.stat(filepath, (err, info) => { 
  if(err){
    console.log(err);
    return
  }
  console.log(info);
})
// console.log('后续要执行的代码会被阻塞');

// 3、promise
fs.promises.stat(filepath).then(info=>{
  console.log(info);
}).catch((err)=>{{console.log(err);}})
console.log('后续要执行的代码会被阻塞');

文件描述符

const fs = require('fs')

fs.open('./abc.txt', (err, fd) => {
  if (err) {
    console.log(err);
    return
  }
  // 通过描述符获取信息
  fs.fstat(fd, (err, info) => { 
    if(err){
      console.log(err);
      return
    }
    console.log(info);
  })
})

文件的读写

const fs = require('fs')

// 文件写入
fs.writeFile('./abc.txt','呆呆狗2',{flag:"a"},err=>{
  console.log(err);
})
/* 
  1、 w 打开文件写入 默认值。 会覆盖掉原先的内容
  2、 w+ 打开文件进行读写,如果不存在则创建文件
  3、 r+ 打开文件进行读写,如果不存在那么抛出异常
  4、 r 打开文件读取,读取时的默认值
  5、 a 打开要写入的文件,将流放在文件末尾。如果不存在则创建文件
  6、 a+ 打开文件以进行读写,将流放在文件末尾,如果不存在则创建文件
*/

// 文件读取
fs.readFile('./abc.txt',{encoding:'utf-8'},(err,data)=>{
  console.log(data);
  // 不设置 encoding:'utf-8' 就显示二进制的编码
})

对文件夹的操作

const fs = require('fs')

// 1、创建文件夹
const dirname = './ddg'
if (!fs.existsSync(dirname)) {
  fs.mkdir(dirname,err=>{
    console.log(err);
  })
}

// 2、读取文件夹中的所有文件
fs.readdir(dirname,(err,files)=>{
  console.log(files);// [ 'a.txt', 'b.txt' ]
})

// 3、重命名 文件
// 旧路径  新路径  回调函数
fs.rename('./ddg','./ddg2',err=>{
  console.log(err);
})
3.3 events 模块

node中的核心API 都是基于异步事件驱动的。

在这个体系中,某些对象(发射器(Emiteers))发出某一个事件

我们可以监听这个事件 (监听器 Listeners),并且传入的回调函数,这个回调函数会在监听到事件时调用

发出事件和监听事件 都是通过 EventEmitter 类 来完成的,他们都属于 events 对象

emitter.on(eventName,listener): // 监听事件,也可以使用 addListener
emitter.off(eventName,listener): // 移除事件,也可以使用 removeListener
emitter.emit(eventName[,...args]): // 发出事件,可以携带一些参数

4、包管理工具

4.1 配置文件

每一个项目都对应一个配置文件(package.json),包括项目名称、使用的插件、版本号、项目描述等等

npm init -y
4.2 配置文件常见的属性
  • scripts属性,用于配置一些脚本命令,以键值对的形式存在,配置后我们可以通过 npm run 命令的key 来执行这个命令
  • dependencies 属性 是无论开发环境还是生产环境都需要的依赖包
  • devDependencies属性 是 生产环境不需要的依赖包
4.3 版本管理的问题

比如版本 2.23.8

  • 第一个数字,表示可能不兼容之前的版本,这个版本号一般很少修改
  • 第二个数字,向下兼容的功能性新增,就是 新功能增加,但是兼容之前的版本
  • 第三个数字,前面两个没有任何修改,但是 修复了 之前的 bug

^2.23.8 表示 第一个数保持不变的, 第二个和第三个 永远安装最新的版本

~2.23.8 表示 第一个和第二个保持不变的,第三个永远安装最新的版本

5、buffer 和 浏览器的事件循环

5.1 数据的二进制

计算机追踪所有的内容:文字、数字、图片、音频、视频都终会使用二进制来表示

5.2 buffer 和二进制

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

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

Buffer 看成是一个存储二进制的数组,数组中的每一项,可以保存 8位 二进制

5.3 Buffer 对字符串的存储
const message = "hello world"

// 第一种 1、创建 Buffer
// 打印出来的,是和 message 一一对应的,打印出来的 每一组数字,是 16进制的
//const buffer = new Buffer(message)
//console.log(buffer); //

// 第二种 2、
const buffer2 = Buffer.from(message)
console.log(buffer2);
5.4 Buffer 对文字的存储
const message = "呆呆狗"
// 其实 一个汉字 对应 三个字节码,
const buffer2 = Buffer.from(message)
console.log(buffer2);
// 

// 解码
console.log(buffer2.toString());// 呆呆狗
5.5 对 文件的处理
const fs = require('fs')

// 1、对文本文件的操作
fs.readFile("./foo.txt", { encoding: 'utf-8' }, (err, data) => {
  console.log(data);
  // 如果不传 utf-8 输出的,其实是  Buffer。本质上,我们读取到的东西都是二进制
})

// 2、对 图片的操作
fs.readFile('./one.png', (err, data) => {
  console.log(data); // 读取到的 也是 Buffer

  // 我们可以在写入 文件
  fs.writeFile('./one_copy.png', data, err => {
    // 这里的 data 表示要写入谁  就是 图片的 Buffer
    console.log(err);
  })
})

// npm i sharp 先安装依赖,然后 裁剪,然后输出
sharp('./one.png').resize(200,200).toFile('./baz.png')

6、事件循环

6.1 事件循环定义

事件循环 可以理解成 我们编写 js 代码 和 浏览器 或者 node 之间的一个桥梁

浏览器的事件循环是 一个我们编写的 js 代码和 浏览器 API 调用 的一个桥梁,桥梁之间他们通过回调函数进行沟通

node 的事件循环是一个我们编写 js 代码 和 系统调用 (file、system、network) 之间的一个桥梁,桥梁 之间他们通过回调函数进行沟通的

6.2 进程和线程

进程: 计算机已经运行的程序

线程:操作系统能够运行运算调度的最小单位

进程更像是线程的容器

操作系统就像是一个工厂,工厂里面有很多车间,这个车间就是进程,工人就是线程

6.3 多进程多线程开发
6.4 浏览器 事件循环 面试题

第一道

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

分析:

  1. 先输出 pr1 , then1 进入 微任务队列
  2. 输出 2 , queueMicrotask 创建一个微任务
  3. then3 加入微任务
  4. 主线程 执行完毕,执行 第一波微任务 依次输出 then1,queueMicrotask,then3,此时已经没有微任务了,继续执行下一个宏任务
  5. 第一个定时器, 输出 set1 ,把 第一个定时器的 then 加入微任务队列
  6. 输出 then2
  7. 这一次的宏任务执行完毕,继续执行这一次的微任务,输出 then4
  8. 执行第二个定时器

第二道

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

注意:await 下面的代码是微任务

6.5 阻塞IO 和 非阻塞 IO

如果我们 希望在程序中对一个文件进行操作,那么我们就需要打开这个文件内: 通过 文件描述符

js 可以对一个文件进行操作嘛?

看起来是可以的,但是事实上我们任何程序中的文件操作 都是 需要进行 系统调用

事实上,对文件的操作,是一个操作系统的系统调用 (IO系统,IO是输入、输出)

操作系统为我们提供了两种调用方式 :阻塞时调用 和 非阻塞式调用

阻塞式:调用结果返回之前,当前线程处于阻塞态(阻塞态CPU是不会分配时间片的),调用线程只有在得到调用结果之后,才会继续执行

非阻塞式:带哦用执行之后,当前线程不会停止执行,只需要过一段时间来检查有没有结果返回即可

所以在开发中的很多 耗时操作,都可以基于这样的 非阻塞式 调用

  • 比如 网络请求本身使用了 Socket通信,而 Socket 通信 本身提供了 select 模型,可以进行非阻塞方式的工作
  • 比如文件读写的 IO 操作,我们可以使用操作系统提供的基于事件的回调机制

非阻塞式调用 也存在一定问题,,我们不一定获取到 需要 读取的结果。不知道返回来的数据是否是完整的,所以为了知道是否读取完成,我们需要频繁的去确定读取到的数据是否是完整的,这个过程称之为 轮询操作

这个轮询工作有谁完整?

我们开发中不只是一个文件的读写,可能是多个文件,也可能是 网络的IO、数据库的IO、子线程调用

libuv 提供了一个 线程池

  • 线程池会负责所有相关的操作,并会通过轮询或者其他方式等待结果
  • 获取到结果后,就可以将对应的回调放到事件循环(某一个队列)中
  • 事件循环就可以负责接管后续的回调工作,告知 JS 应用程序执行对应的回调函数
6.6 node 的事件循环

一次事件循环,就是一次 tick

微任务队列

  • promcess.nextTick
  • promise 的 then 回调 queueMicrotask

事件循环 执行顺序就是

  1. ticks 队列 process.nextTick
  2. 其他微任务队列 promise 的then queueMicrotask
  3. timers setTimeout setInterval
  4. IO 队列
  5. check 队列 , setImmediate 队列
  6. close 队列 close 事件

面试题1

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

不一定先输出谁

执行 main script 也需要事件,执行完以后 要 初始化事件循环 也需要事件

初始化事件循环 和 main script 是同时进行的,但事件循环初始化完毕后,就要执行事件循环了

  • 假如 我们 执行 main script 需要10ms 初始化事件循环 20ms ,然后执行 事件循环,但 第一个定时器是 0MS 执行,所以 他已经放在里面,可以拿出来立即执行。 因为 初始化事件循环完毕后,main script 已经执行完毕了
  • 假如我们 我们 执行 main script 需要10ms 初始化事件循环 5ms , 定时器又是宏任务,而 setImmediate

不好理解。。。。。

7、http 开发 web 服务器

7.1 http 内置模块 创建服务器
const http = require('http')

const server = http.createServer((req, res) => {
  res.end('hello world')
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('启动成功');
})
7.2 创建 web 服务器的方式
// 第一种
const http = require('http')

const server = http.createServer((req, res) => {
  res.end('hello world')
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('server1启动成功');
})

// 第二种
const server2 = new http.Server((req, res) => {
  res.end('Server2')
})
server2.listen(8001, () => {
  console.log('server2启动成功');
  // 端口话如果不写,系统会默认分配一个,项目中一般都会写到环境变量中
  // 也可以通过  server2.address().port  来获取到系统分配的端口号
})

7.3 request 对象分析 和 request 的 url 分析
const http = require('http')
const url = require('url')
const qs = require('querystring')
const server = http.createServer((req, res) => {
  // request 对象中封装了客户端给我们服务器传递过来的 所有信息
  console.log(req.url);
  console.log(req.method);
  console.log(req.headers);// 请求头

  // if (req.url === '/login') {
  //   res.end('欢迎回来')
  // } else if (req.url === '/users') {
  //   res.end('用户列表')
  // } else {
  //   res.end('错误请求,检查~')
  // }
  // 如果 遇到/login?username=呆呆狗&password=123 呢
  // 最好的方式是用   内置模块 url
  const ressult = url.parse(req.url)
  console.log(ressult); // 有一个 pathname 这个表示基准路径,query表示?后面的东西
  // 如果我们想获取后面的查询参数,则可以根据内置模块 querystring
  const qsSecond = qs.parse(ressult.query) // 会反应一个对象,键值对形式
  console.log(qsSecond);
  res.end('hello world')
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('启动成功');
})
7.4 request对象-method 的分析
const http = require('http')
const url = require('url')
const qs = require('querystring')
const server = http.createServer((req, res) => {
  // request 对象中封装了客户端给我们服务器传递过来的 所有信息
  const { pathname } = url.parse(req.url)
  if (req.method === 'POST') {
    // 拿到 body 的数据
    req.setEncoding('utf-8')
    req.on('data', (data) => {
      console.log(data); // 这里的 data 是字符串
      let dataNew = JSON.parse(data)
    })
    // 可以通过监听这个事件,获取post 的 body值,获取的是 Buffer

    // 1、data.toString()
    // 2、在监听事件前面声明, req.setEncoding('utf-8') utf-8 表示文件
    // 图片视频 等等,要设置成 req.setEncoding('binary')

  }
  res.end('hello world')
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('启动成功');
})
7.5 request对象-headers

content-type : 是这次请求携带的数据的类型

  • application/json 表示一个json 类型
  • text/plain 表示是文本类型
  • application/xml 表示是 xml 类型文件
  • multipart/form-data 表示是上传文件

content-length :文件的大小和长度

connection : 的 keep-alive

http是基于 TCP 协议的,但是通常在进行一次请求和响应结束后会立刻中断

在 http1.0 中,如果想要继续保持链接

  • 浏览器要在请求头添加 connection : keep-alive
  • 服务器需要在响应头添加 connection:keep-alive
  • 客户端再次发起请求时,就会使用同一个链接,直接一方中断连接

在 http1.1中,所有连接默认是 connection : keep-alive 的

不同的 web 服务器会有不同的保持 keep-alive 的时间

node 默认是 5S

7.6 response 对象-响应结果
const http = require('http')

const server = http.createServer((req, res) => {

  // res.end('hello world')
  res.write("响应结果一")
  res.end()
  // res.end 其实是 执行两个东西,一个是写入结果,一个是 res.end()
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('启动成功');
})
7.7 response 对象-响应码
const http = require('http')

const server = http.createServer((req, res) => {
  // 设置状态码
  // 方式一: 直接给属性赋值
  res.statusCode = 401
  // 方式二:和 head 一起设置
  res.writeHead(503, {

  })


  // res.end('hello world')
  res.write("响应结果一")
  res.end()
  // res.end 其实是 执行两个东西,一个是写入结果,一个是 res.end()
})

server.listen(8000, () => {
  // 端口 尽量不要写 1024 以下的
  console.log('启动成功');
})
7.8 response 对象-响应的header
const http = require('http')

const server = http.createServer((req, res) => {
  // 响应的hader
  // 设置方式 1
  res.setHeader("Content-Type", "text/plain;charset=utf8")
  // 设置方式2
  res.writeHead(200, {
    // 第一个参数是状态码,第二个 可以设置  响应的hader
    "Content-Type": 'text/plain;charset=utf8',
    // 如果我们要返回一个 '

你好

' 呢?
// 这就要设置成 text/html;charset=utf-8 了 }) // res.end('hello world') res.write("响应结果一") res.end() // res.end 其实是 执行两个东西,一个是写入结果,一个是 res.end() }) server.listen(8000, () => { // 端口 尽量不要写 1024 以下的 console.log('启动成功'); })
7.9 http模板发送 网络请求
const http = require('http')

http.get('http://localhost:8888', (res) => {
  res.on('data', (data) => {
    // 获取 请求的结果
    console.log(data.toString());
  })
  res.on('end', () => {
    // 监听是否获取到了所有的结果i
    console.log('获取到了所有的结果');
  })
})

// http 没有 http.post 方法
const result = http.request({
  method: 'POST',
  hostname: 'localhost',
  port: 8888,
}, (res) => {
  res.on('data', (data) => {
    // 获取 请求的结果
    console.log(data.toString());
  })
  res.on('end', () => {
    // 监听是否获取到了所有的结果i
    console.log('获取到了所有的结果');
  })
})
result.end()
7.10 http 实现文件上传
// 传递的是一个 xxxxxxxx.png
const http = require('http');
const fs = require('fs');
const qs = require('querystring');

const server = http.createServer((req, res) => {
  if (req.url === '/upload') {
    if (req.method === 'POST') {
      req.setEncoding('binary');

      let body = '';
      const totalBoundary = req.headers['content-type'].split(';')[1];
      const boundary = totalBoundary.split('=')[1];

      req.on('data', (data) => {
        body += data;
      });

      req.on('end', () => {
        console.log(body);
        // 处理body
        // 1.获取image/png的位置
        const payload = qs.parse(body, "\r\n", ": ");
        const type = payload["Content-Type"];

        // 2.开始在image/png的位置进行截取
        const typeIndex = body.indexOf(type);
        const typeLength = type.length;
        let imageData = body.substring(typeIndex + typeLength);

        // 3.将中间的两个空格去掉
        imageData = imageData.replace(/^\s\s*/, '');

        // 4.将最后的boundary去掉
        imageData = imageData.substring(0, imageData.indexOf(`--${boundary}--`));

        fs.writeFile('./foo.png', imageData, 'binary', (err) => {
          res.end("文件上传成功~");
        })
      })
    }
  }
});

server.listen(8000, () => {
  console.log("文件上传服务器开启成功~");
})

8、express 框架

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

方式一

// 安装脚手架
npm i express-generator -g
// 创建项目
express express-demo
// 安装依赖
npm i
// 启动项目
node bin/www
8.2 express 初体验
npm init -y
npm i express

const express = require('express')
// express 其实是一个函数
const app = express()

// 监听默认路径
app.get('/', (req, res, next) => {
  res.end('hello ')
})
app.post('/', (req, res, next) => {
  res.end('hello post')
})
app.post('/login', (req, res, next) => {
  res.end('login')
})

// 开启监听
app.listen(8000, () => {
  console.log('express 初体验启动成功~');
})
8.3 普通中间件的使用
const express = require('express')
const app = express()

// app.use 注册一个全局的中间价
app.use((req, res, next) => {
  console.log('01第一个普通的中间间');
  // 不论我们发起什么请求,都会执行 这个 console.log('01第一个普通的中间间')
  //res.end('hello wolrd')// end 不会妨碍 next 但每个中间件都有 res.end 会报错

  next() // 调用下一个中间件
})
app.use((req, res, next) => {
  console.log('02第2个普通的中间间');
  res.end('hello wolrd')
})
app.listen(8000, () => {
  console.log('服务器启动成功·');
})
8.4 路径中间件
const express = require('express')
const app = express()

// 路径匹配中间件  ,路径中间件也可以有 多个相同的,只需要写一个 next 就可以执行多个
// 没有 next 永远匹配到第一个 中间件
app.use('/home', (req, res, next) => {
  console.log('home 01');
  res.end('home 01')
})
app.listen(8000, () => {
  console.log('服务器启动成功·');
})
8.5 内置中间件 解析 json 和 urlencoded 数据
const express = require('express')
const app = express()
// 配置 解析json的中间件
app.use(express.json())
// 配置解析urlencoded 的中间件
app.use(express.urlencoded({
  extended: false
}))
app.post('/login', (req, res, next) => {
  console.log(req.body);
})
app.listen(8000, () => {
  console.log('服务器启动成功·');
})
  • extended: false:表示使用系统模块querystring来处理,也是官方推荐的
  • extended: true:表示使用第三方模块qs来处理
8.6 解析 form-data 的数据

非文件的键值对数据

const express = require('express')
// 1、导入
const multer = require('multer')
const app = express()
// 配置 解析json的中间件
app.use(express.json())
// 配置解析urlencoded 的中间件
app.use(express.urlencoded({
  extended: false
}))
// 2、创建
const upload = multer()
// 3、创建
// 注意  如果解析的 form-data 不是文件,可以用 any
app.use(upload.any())
// form-data 的 请求数据怎么解析? 这里要用到 express 推荐的一个库,multer
// form-data 的数据,目前大多都是 上传文件
app.post('/login', (req, res, next) => {
  console.log(req.body);
})
app.listen(8000, () => {
  console.log('服务器启动成功·');
})

实现multer 文件上传

注意 永远不要把 multer 作为全局中间件使用

const express = require('express')
// 1、导入
const multer = require('multer')
const app = express()
// 配置 解析json的中间件
app.use(express.json())
// 配置解析urlencoded 的中间件
app.use(express.urlencoded({ extended: false }))


// 2、创建
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, './uploads/')
  },// 目的地
  filename: (req, file, cb) => {
    cb(null, "foo.png")
    // 名字不会写死的
    // Date.now() 当前的时间戳
    // path.extname(file.originalname) file.originalname 是原始文件的名字
    // path.extname 取出后缀名
  }
})
const upload = multer({
  //dest: './uploads/',
  // 上传文件的存储路径

  storage
})
// 这个创建的配置,也可以自定义,因为,不定义的话 上传的文件,是没有后缀名的


// 3、创建
// 注意  如果解析的 form-data 不是文件,可以用 any

// 注意 永远不要把 multer 作为全局中间件使用
app.use(upload.any())
// form-data 的 请求数据怎么解析? 这里要用到 express 推荐的一个库,multer
// form-data 的数据,目前大多都是 上传文件
app.post('/login', (req, res, next) => {
  console.log(req.body);
})
app.post('/upload', upload.single('file'), (req, res, next) => {
  // 如果是上传单个文件,是 single ,多个文件是 array
  console.log('文件上传成功');
  console.log(req.files);// 上传文件后的信息
  res.end('上传成功')
})
app.listen(8000, () => {
  console.log('服务器启动成功·');
})
8.7 记录请求日志
// express 官方的 morgan 库
const loggerWriter = fs.createWriteStream('./log/access.log',{ flag: 'a+'})
app.use(morgan('combined', {stream : loggerWriter}))
8.8 客户端发送请求的方式
  • 通过 get 请求中的URL 的 params
  • 通过 get 请求中的URL 的 query
  • 通过post 请求中的body 的 json 格式
  • 通过post 请求中的body 的 x-www-form-urlencoded 格式
  • 通过post 请求中的 form-data 格式
8.9 url 的参数解析 params query
const express = require('express')
// express 其实是一个函数
const app = express()

// 监听默认路径
app.get('/home/:id/:name', (req, res, next) => {
  console.log(req.params);
  res.end('hello')
})
app.get('/home', (req, res, next) => {
  console.log(req.query);
  res.end('hello')
})


// 开启监听
app.listen(8000, () => {
  console.log('express 初体验启动成功~');
})
8.10 response 响应结果
const express = require('express')
// express 其实是一个函数
const app = express()

// 监听默认路径
app.get('/', (req, res, next) => {
  res.end('hello ')
})
app.post('/', (req, res, next) => {
  res.end('hello post')
})
app.post('/login', (req, res, next) => {
  res.end('login')
})

// 开启监听
app.listen(8000, () => {
  console.log('express 初体验启动成功~');
})
8.11 express 的路由

将路由抽离为单独模块的步骤如下:

  1. 创建路由模块对应的 .js 文件 (就是自定义模块)
  2. 调用 express.Router() 函数创建路由对象 (返回的是一个路由的实例对象)
  3. 向路由对象上挂载具体的路由
  4. 使用 module.exports 向外共享路由对象
  5. 在入口文件引入它,并使用 app.use()函数注册路由模块

入口文件

const express = require('express')
const userRouter = require('./routers/users')
// express 其实是一个函数
const app = express()

app.use(userRouter)
// app.use('/api',userRouter) ,则表示添加了路由前缀 localhost:8000/api/
// 开启监听
app.listen(8000, () => {
  console.log('express 初体验启动成功~');
})

路由文件

const express = require('express')

const router = express.Router()
router.get('/', (req, res, next) => {
  res.json({ name: '呆呆狗', age: 20 })
})
router.post('/', (req, res, next) => {
  res.end('hello post')
})
router.post('/login', (req, res, next) => {
  res.end('login')
})
module.exports = router
8.12 express 的静态资源服务器
const express = require('express')
// express 其实是一个函数
const app = express()

app.use(express.static('./build'))
// 开启监听
app.listen(8000, () => {
  console.log('express 初体验启动成功~');
})
8.13 错误的中间件
app.get('/',function(req,res){    // 1、路由
  throw new Error('服务器内部发生了错误!')// 2、抛出一个自定义的错误
  // 如果没有错误级别的中间件,代码到这里就结束了
  res.send('Home Page')
})
// 错误的中间件,必须要在所有路由之后
// 也可以用 next(new Error())   抛出错误
app.use(function(err,req,res,next){ // 1、错误级别的中间件
  console.log('发生了错误'+err.message)// 2、在服务器打印错误消息
  res.send('Error!'+err.message)// 3、向客户端响应错误相关的内容
  
  // 其实在真实开发中,这里用的是  switch 语句,判断响应的错误,然后 返回
})
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));
  }
});

app.use((err, req, res, next) => {
  let status = 400;
  let message = "";
  console.log(err.message);

  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("路由服务器启动成功~");
});

8.14 express 源码
  1. 调用 express 函数到底发生了什么,就是调用了源码的 createApplication 函数

9、koa 框架

9.1 认识 koa

koa node.js的下一代 web 框架

事实上 express 和 koa 是同一个开发团队开发的

  • koa 更小、更丰富、更强大的能力
  • koa 比 express 具有更强的异步处理能力
  • koa 核心代码 只有 1600+ 行,更轻量级的框架
9.2 初体验
const Koa = require('koa')
const app = new Koa()

// koa 把所有的中间件都执行完以后,都没有返回结果的话,会返回 not found

app.use((ctx, next) => {
  // 第一个参数是上下文
  // 这个上下文 包含  request 和 response
  console.log(ctx.request);
  ctx.response.body = "hello world"
})
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.3 koa 注册中间件

koa 没有提供 app.get/post ,没有提供 匹配路径的路由,没有提供连续注册

const Koa = require('koa')
const app = new Koa()

app.use((ctx, next) => {
  // 第一个参数是上下文
  // 这个上下文 包含  request 和 response\
  if (ctx.request.url === '/login') {
    if (ctx.request.method === 'GET') {
      console.log('来到了这里面');
      ctx.response.body = "Login Suceess"
    }
  }
  ctx.response.body = "请求成功~"
})
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.4 koa 使用路由

koa 本身没有路由,需要借助第三方

npm i koa-router

路由文件

const Router = require('koa-router')

const router = new Router({
  prefix: "/users", // 路径前缀
})
router.put('/', (ctx, next) => {
  ctx.response.body = "put 成功~"
})
module.exports = router

入口文件

const Koa = require('koa')
const userRouter = require('./router/user')
const app = new Koa()

app.use(userRouter.routes())

app.use(userRouter.allowedMethods())
// allowedMethods 用于判断某一个 method 是否支持
// 如果没有实现某个请求,就会自动报错 405
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.5 参数的处理 params query
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const userRouter = new Router({
  prefix: "/users", // 路径前缀
})
userRouter.get('/:id', (ctx, next) => {
  console.log(ctx.request.params);
  console.log(ctx.request.query);
})

app.use(userRouter.routes())
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.6 解析json urlencoded
npm i koa-bodyparser
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const app = new Koa()
app.use(bodyParser())

app.use((ctx, next) => {
  console.log(ctx.request.body);
  ctx.response.body = "hello"
})
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.7 解析 form-data 的数据

npm i koa-multer

const Koa = require('koa')
const multer = require('koa-multer')

const app = new Koa()
const upload = multer()

// 真实开发中,不建议 吧 multer 写入全局中间件
app.use(upload.any())
app.use((ctx, next) => {
  // 注意,解析 form-data 这里 用的是 req.body
  console.log(ctx.req.body);
  ctx.response.body = "hello"
})
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.8 解析 form-data 的文件
const Koa = require('koa')
const Router = require('koa-router')
const multer = require('koa-multer')

const app = new Koa()
const uploadRouter = new Router({ prefix: '/upload' })
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, './uploades/')
  },// 目的地
  filename: (req, file, cb) => {
    cb(null, "foo.png")
    // 名字不会写死的
    // Date.now() 当前的时间戳
    // path.extname(file.originalname) file.originalname 是原始文件的名字
    // path.extname 取出后缀名
  }
})
const upload = multer({ storage })

uploadRouter.post('/avatar', upload.single('avatar'), (ctx, next) => {
  console.log(ctx.req.file);
  ctx.response.body = "上传头衔成功~"
})

app.use(uploadRouter.routes())
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.9 数据的响应

输出结果

  • string 字符串
  • Buffer Buffer 数据
  • Stream 流数据
  • Object | Array 对象或者数组
  • null 不输出任何内容
  • 如果 response.status 尚未设置,koa 会自动将状态设置为200 或 204
const Koa = require('koa')

const app = new Koa()

app.use((ctx, next) => {
  ctx.response.body = {
    ddg: '呆呆狗'
  }
  // 也可以 用 ctx.body = 返回数据
  // 本质上说,你写 ctx.body 其实 就是 ctx.response.body

  ctx.status = 203
  // 对象返回的是  json 格式
})
app.listen(8000, () => {
  console.log('http://localhost:8000');
})

9.10 静态资源服务器

npm i koa-static

const Koa = require('koa')
const static = require('koa-static')

const app = new Koa()

app.use(static('./build'))

app.listen(8000, () => {
  console.log('静态资源服务器启动成功~');
})
9.11 错误中间件
const Koa = require('koa')

const app = new Koa()

app.use((ctx, next) => {
  const isLogin = false
  if (!isLogin) {
    ctx.app.emit('error', new Error('你还没有登录~'), ctx)
  }
})

app.on('error', (err, ctx) => {
  // 一般来说也是先  switch 判断
  ctx.status = 200,
    ctx.body = "你还没有登录"
})
app.listen(8000, () => {
  console.log('服务器启动成功~');
})
9.12 koa洋葱模型 注意看第十节 express 和 koa的区别
  1. 中间件处理,查找匹配到的中间件,,假设有三个中间件
  2. 先执行第一个中间件, 第二个 第三个
  3. 如果 第二个 next 后面还有代码,则逆序回去,执行 next 后面的代码
  4. 第二个的next 后面执行完毕后,再去第一个的 next后面的代码
  5. 然后 返回值

10、express 和 koa 的区别

从架构上来说

  • express 是完善和强大的,帮助我们内置了很多功能
  • koa 是 自由和 简洁的,只包含最和核心的功能

需求: 假如有三个中间件,会在一次请求中匹配到,并且按照顺序执行

  1. 在middleware1 中,在 req.message 中添加一个字符串 aaa
  2. 在middleware2 中,在 req.message 中添加一个字符串 bbb
  3. 在middleware3 中,在 req.message 中添加一个字符串 ccc

当所有内容添加结束后,在 middleware1 中 通过 res 返回最终的结果

10.1 express 实现同步数据
const express = require('express')
const app = express()

const middleware1 = (req, res, next) => {
  req.message = "aaa"
  console.log(1);
  next()
  res.end(req.message)
  console.log(5);
}
const middleware2 = (req, res, next) => {
  req.message += "bbb"
  console.log(2);
  next()
  console.log(4);
}
const middleware3 = (req, res, next) => {
  req.message += "ccc"
  console.log(3);
}
app.use(middleware1, middleware2, middleware3)
app.listen(8000, (() => {
  console.log('启动成功');
}))

/*
* 1、先去 middleware1 赋值,然后 遇到 next 他会执行 middleware2 不会先执行 res.end()
* 2、执行完 middleware3 ,middleware3没有 next 就会回去,倒叙执行 middleware2 和 middleware1 剩下的代码
*/
//  输出的结果也是 12345

10.2 express 实现异步数据
const middleware3 = (req, res, next) => {
  // 假如,要在这个未知, 发起一个异步请求,然后,再拼接值,在返回出去,
  // 结果不会是我们想要的
  
  // 所以 express 处理这些异步数据,是有些乏力的
  
  // 也可以再外面定义一个函数,先请求,
  req.message += "ccc"
  console.log(3);
}
10.3 koa 实现同步数据
const Koa = require('koa');

const app = new Koa();

const middleware1 = (ctx, next) => {
  ctx.message = "aaa";
  next();
  ctx.body = ctx.message;
}

const middleware2 = (ctx, next) => {
  ctx.message += "bbb";
  next();
}

const middleware3 = (ctx, next) => {
  ctx.message += "ccc";
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);

app.listen(8000, () => {
  console.log("服务器启动成功~");
})
10.4 koa 实现异步数据

koa中的next是借助promise,可以做一些异步的处理

const Koa = require('koa');
const axios = require('axios');

const app = new Koa();

const middleware1 = async (ctx, next) => {
  ctx.message = "aaa";
  await next();
  ctx.body = ctx.message;
}

const middleware2 = async (ctx, next) => {
  ctx.message += "bbb";
  await next();
}

const middleware3 = async (ctx, next) => {
  const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876');
  ctx.message += result.data.lrc.lyric;
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);

app.listen(8000, () => {
  console.log("服务器启动成功~");
})

你可能感兴趣的:(前端学习笔记,node,node.js)