Node的总结与框架分析,请查收。

3,042 阅读19分钟

Node基础概念

什么是node

node提供一个可以让js跑在服务端的环境

node底层用c++写。性能OK

node基于js核心(ecm) 系统及api文件操作 网络编程 来实现web服务

解决了什么问题

web 服务器瓶颈 并发

java php 每个客户链接服务器。产生一个新线程,

node不会创建新线程。发射事件 (发布订阅模式)

node 主要解决 前后端分离 写一些工具库 cli等

服务器与服务器之间发送清求。不用跨域

跨域是浏览器的

比较适合 文件读写 效率大概是java的200倍

node 核心特点

事件启动 node的api是基于事件的 异步的

Node 采用的是单线程 node 可以开启个进程

不适合 Cpu密集型 适合i/o密集型

同步异步 阻塞非阻塞

同步和异步 关注的是 消息通知的机制

阻塞和非阻塞 程序等待消息结果的状态

全局属性

能直接访问的属性被称之为全局属性

文档

有关this

node为了实现模块化。在每个文件上都包裹了一个函数。所以这个函数的this 被改变

console.log(this)  //  {}

// 在命令行中  node  直接打印this 打印出global  this === global  true

有关文件

console.log(__filename)  //  代码文件所在的绝对目录
console.log(__dirname)  //   当前文件的运行文件夹
console.log(process.cwd())  //  代表当前工作目录 可以更改

导入导出

exports require module 模块化常用属性

解决了哪些问题

  1. 可以帮助我们解决 命名冲突 问题

  2. node的规范,commonjs规范。

    1. 每个JS都是一个模块
    2. module.exports 模块的导出
    3. 模块的导入 require
  3. esModule es6模块规范 import export

  4. 还有 cmd seajs amd requirejs umd 统一模块规范

  5. node 默认不支持es模块

  6. commonjs 动态引入. import 静态引入

  7. if(true){ // 动态引入
        require(./1.js)
    }
    

模块分类

  1. 核心/内置模块 不需要引入 直接用
  2. 第三方模块 reuqirs('vue') 需要安装
  3. 自定义模块 通过路径进行引入

node采用 同步的方式 读取文件

异步代码 需要使用 回调的方式解决

通过独去文件内容,把内容包裹在一个自执行函数中。默认返回module.exports

function (exports,require,module,__filename,__dirname){
    return module.exports
}(exports,require,module,xxx,xxx)

node调试

  1. 浏览器调试 调试包
  2. vscode 进行代码调试 自己写的文件等
  3. 命令行

核心模块

fs

文档

读取文件没有指定 都是buffer格式

默认写入都会转成utf-8格式

读取文件必须采用绝对路径

  1. readFileSync 同步独去 读文件
  2. existSync 同步 判断是否存在
  3. fs.existSync 是否存在路径
  4. fs.accessSync 同步地测试用户对 path 指定的文件或目录的权限

文件拷贝

let path = require('path')
let fs = require('fs')


// w 写  有则清空 跟C语言 一样
fs.readFile(path.resolve(__dirname, 'a.txt'), function(err, data) {
  if (err) {
    console.log(err)
  } else {
    fs.writeFile('b.txt', data, { flag: 'w' }, function(err) {
      if (err) {
        console.log(err)
      } else {
        console.log('成功')
      }
    })
  }
})

不适合大文件读取。

小于64k 用这个 大于 64k 用流

同时也包含 操作文件夹的

fs.mkdirSync 创建文件夹

创建多个文件夹

//  创建多个文件夹的实现
let fs = require('fs')
//  fs.mkdirSync('a/b/c')  不能直接这样 因为 没有父不能创建子

// 同步的实现
function mkdirSync(path) {
  let arr = path.split('/')
  for (let i = 0; i < arr.length; i++) {
    let p = arr.slice(0, i + 1).join('/')
    try {
      fs.accessSync(p)
    } catch (e) {
      console.log(e)
      fs.mkdirSync(p)
    }
  }
}
mkdirSync('a/b/c')
// 异步逻辑基本相似
// 异步
function mkdir(p,cb){
  let arr = p.split('/')
  let index = 0
  function next() {
    if (index === arr.length) return cb()
    let p = arr.slice(0,index+1).join('/')
    fs.access(p,(err)=>{
      index ++
      if (err){
        fs.mkdir(p,next)
      }else {
        next()
      }
    })
  }
  next()
}
mkdir('a/b/c/d/e/f',function(){
  console.log('创建成功')
})

删除多个文件夹

let fs = require('fs')
let path = require('path')

// 同步  测试 只能删除二级目录
// function remDir(p){
//   let dirs = fs.readdirSync(p)
//   dirs = dirs.map(dir=>path.join(p,dir))
//   // console.log(dirs)
//   dirs.forEach(dir=>{
//     let stateObj = fs.statSync(dir); // 返回文件状态
//     if (stateObj.isDirectory()){ // 是文件夹的话
//       fs.rmdirSync(dir) // 删除文件夹
//     }else { // 是文件
//       fs.unlinkSync(dir) // 删除文件
//     }
//   })
//   fs.rmdirSync(p)
// }

// 同步 深度遍历
// function remDir(p) {
//   let stateObj
//   console.log(p)
//   try {
//     stateObj = fs.statSync(p) // 返回文件状态
//   } catch (e) {
//     console.log(e)
//   }
//   if (stateObj.isDirectory()) {
//     let dirs = fs.readdirSync(p)
//     dirs.forEach(dir=>{
//       let current = path.join(p,dir)
//       remDir(current)
//     })
//     fs.rmdirSync(p)
//   } else {
//     fs.unlinkSync(p)
//   }
// }


// 同步  广度
function remDir(p){
  let arr = [p]
  let index = 0
  let current
  while (current = arr[index++]){
    let stateObj = fs.statSync(current)
    if (stateObj.isDirectory()){
      let dirs = fs.readdirSync(current) // 返回数组 所有文件名称
      dirs = dirs.map(dir=> path.join(current,dir))
      arr = [...arr,...dirs]
    }
  }
  for (let i = arr.length-1; i >=0 ; i--) {
    let current = arr[i]
    let stateObj = fs.statSync(current)
    if (stateObj.isDirectory()){
      fs.rmdirSync(current)
    }else {
      fs.unlinkSync(current)
    }
  }
}
remDir('a')

边读边写

熟悉api

利用fs.open fs.read fs.write fs.close (参数太多)

// 实现一个拷贝功能
// 利用 fs.readFile  fs.writeFile
//  容易淹没用户内存。  原因  先读取 再写入  文件太大 占用内存
// new api   fs.copy  也不能用大文件

// 边读边写入
// 利用 fs.open  fs.read  fs.write fs.close  (参数太多)
//  读取文件 把文件读取到内存     写入 内存中文件读取出来
let fs = require('fs')
let path = require('path')
// 申请内存大小


// fd 文件描述符  代表拥有了读取这个文件的权限  number 类型
// 且 fd 是唯一的 有最大数量限制。  开一个需要关一个
// let buf = Buffer.alloc(3)
// fs.open(path.resolve(__dirname,'b.txt'),'r',(err,fd)=>{
//   // 1.文件描述符
//   // 2.读取到哪个内存
//   // 3.从内存的哪个位置开始写入
//   // 4.写入多少个
//   // 5.从文件的什么位置开始读
//   // 6.回调函数
//   fs.read(fd,buf,0,3,0,(err, bytesRead, buffer)=>{
//     console.log(bytesRead) // 代表真实的去读个数
//     // 读取到的内存 可以直接用buf
//     console.log(buffer.toString())
//     console.log(buf.toString())
//   })
//   fs.close(fd,()=>{
//     console.log('关闭成功')
//   })
// })


// 写入
let buf = Buffer.from('叫我阿琛')

fs.open(path.resolve(__dirname, 'c.txt'), 'w', (err, fd)=>{
  fs.write(fd, buf, 6, 6, 0, (err1, written)=>{
    console.log(buf.toString())
    console.log(written)
  })
  fs.close(fd, ()=>{
    console.log('关闭成功')
  })
})

边读边写

// 一边读取 一边写入
let Buffer_size = 3   // 3*1024 3K
let buf = Buffer.alloc(Buffer_size)
let readOffset = 0
let fs = require('fs')
let path = require('path')

fs.open(path.resolve(__dirname, 'b.txt'), 'r', (err, rfd)=>{
  fs.open(path.resolve(__dirname, 'c.txt'), 'w', (err, wfd)=>{
    function next() {
      fs.read(rfd, buf, 0, Buffer_size, readOffset, (err1, bytesRead)=>{
        if (!bytesRead) {
          fs.close(rfd, ()=>{})
          fs.close(wfd, ()=>{
            console.log('结束')})
          return
        }
        readOffset += bytesRead
        // position 可以 不写 默认递增
        fs.write(wfd, buf, 0, bytesRead, (err,written)=>{
          next()
        })
      })
    }
    next()
  })
})

这样写代码太乱。异步嵌套太多。

可以使用 发布订阅模式,把 读取写入 分开

所以引入了 的概念

文件流

createReadStream使用

// readSteam
let fs= require('fs')
let rs = fs.createReadStream('./b.txt',{
  flags:'r', // fs.open
  encoding:null,// 编码格式 默认buffer
  mode:0o666, //fs.open
  autoClose:true, // 自动关闭
  start:0, // 从哪里开始读
  end:5, //  读到哪里   0-4
  highWaterMark:2 // 每次读几个  默认 64*1024
})
let arr = []
rs.on('open',(fd)=>{
  console.log('文件打开了' + fd)
})
rs.on('data',(data)=>{
  // console.log(data.toString())
  // 读取汉字会出错
  arr.push(data)
})
rs.on('end',()=>{
  console.log(Buffer.concat(arr).toString())
  console.log('end')
})
rs.on('close',()=>{
  console.log('close')
})

createReadStream实现

let fs = require('fs')
let events = require('events')

class createReadStream extends events {
  constructor(path, options = {}) {
    super();
    this.path = path
    this.flags = options.flags || 'r'
    this.mode = options.mode || 438
    this.autoClose = options.autoClose || true
    this.start = options.start || 0
    this.end = options.end
    this.highWaterMark = options.highWaterMark || 64 * 1024

    // 读取文件要用
    this.pos = this.start
    this.open()
    this.flowing = true
    this.on('newListener', (type)=>{
      if (type === 'data') this.read()
    })
  }

  pause(){
    this.flowing = false
  }
  resume(){
    this.flowing = true
    this.read()
  }
  open() {
    fs.open(this.path, this.flags, this.mode, (err, fd)=>{
      if (err) return this.emit('error', err)
      this.fd = fd
      this.emit('open', fd)
    })
  }

  close() {
    fs.close(this.fd, ()=>{
      this.emit('close')
    })
  }

  read() {
    // 因为fs.open 是异步打开 不一定有fd所以   发布订阅解决了异步问题
    if (typeof this.fd !== 'number') {
      return this.once('open', this.read)
    }
    let buf = Buffer.alloc(this.highWaterMark)
    // 计算一下停止位置
    let end = this.end ? Math.min(this.end - this.pos + 1, this.highWaterMark) : this.highWaterMark

    fs.read(this.fd, buf, 0, end, this.pos, (err, bytesRead)=>{
      if (err) return this.emit('error', err)
      if (bytesRead) {
        this.pos += bytesRead
        this.emit('data', buf)
        if (this.flowing)
          this.read()
      } else {
        this.emit('end')
        this.close()
      }
    })
  }
}

module.exports = createReadStream

createWriteStream使用

let fs = require('fs')
let path = require('path')
// let ws = fs.createWriteStream(path.resolve(__dirname, 'd.txt'), {
//   flags: 'w',
//   encoding: 'utf8',// 编码格式 默认buffer
//   mode: 438,
//   autoClose: true,
//   start: 0,
//   highWaterMark: 10 //  代表一个预估量  默认16k
// })
// // 有返回值。如果超过 预期 即highWaterMark 返回false  这个预期是总和  不是单独
//
// // 异步 内部排队  依次执行
// let flag = ws.write('我爱你', ()=>{
//   console.log('写入成功')
// })
//
// console.log(flag)
// flag = ws.write('我也是', ()=>{
//   console.log('写入成功')
// })
// console.log(flag)
//
// // 如果关闭 不能写入
// ws.end('我也可以写内容,代表写入结束')   // = write + close方法
// ws.close()

// drain 事件
ws = fs.createWriteStream(path.resolve(__dirname, 'text.txt'), {
  highWaterMark: 1 //  代表一个预估量  默认16k
})

let index = 0
let flag = true
function write(){
  flag = true
  while (flag && index < 10){
     flag = ws.write(index++ + '')
  }
}
write()
// 当写入内容达到预估标准之后。并清空数据之后。 触发次方法
ws.on('drain',()=>{
  console.log('drain')
  write()
})
// 这样写比 直接 ws.write 好在哪里   不需要占用内存消耗

配置和上面的createReadStream差不多。有个别特例。主要方法

  1. ws.write(contain,cb) 向文件中写入内容,结束执行回调
  2. ws.end(contain,cb) 相当于write 和 close方法的结合体

如何区分两者

  1. createReadStream方法 有on('data') on('close')
  2. createWriteStream方法有write end方法。 on('drain')方法

拷贝pipe

原理 模拟实现

let r= fs.createReadStream('./b.txt',{
  highWaterMark:4
})
let w = fs.createWriteStream('./d.txt',{
  highWaterMark:1
})

r.on('data',(data)=>{
  let flag = w.write(data)
  if (!flag) r.pause()
})
w.on('drain',()=>{
  r.resume()
})

使用

// pipe  模拟 64k / 16k
let r= fs.createReadStream('./b.txt',{
  highWaterMark:4
})
let w = fs.createWriteStream('./d.txt',{
  highWaterMark:1
})
r.pipe(w)

缺点

  1. 你看不到读写过程
  2. 如果需要获取结果,需要等会pipe完成。 或者使用readFile

Readable, Writable,Duplex

用来实现自定义 可读可写流

class Myread extends Readable {

  _read() {// 子类实现这个方法
    this.push('123') // buffer 或者 字符串类型
    this.push(null) //  不加这个不能停止
  }
}

class myWrite extends Writable {
  _write(chunk, encoding, callback) {// 子类实现这个方法
    console.log(chunk.toString())
    callback() // 完成后必须调用cb
  }
}

let a = new Myread()
a.on('data', function(data) {
 console.log(data) 
})

let b = new myWrite()
b.write('ok', ()=>{
  console.log('完成')
})
b.write('ok2', ()=>{
  console.log('完成2')
})

// 双工流  需要实现这两个方法
class myDuplex extends Duplex {
  _write(chunk, encoding, callback) {

  }

  _read(size) {

  }
}

let duplex = new myDuplex()

Transform

转换流 例如压缩

class myTransform extends Transform {
  _transform(chunk, encoding, callback) { // 子类实现这个方法

  }
}

path

文档

  1. path.join 拼接路径 可以拼接 /
  2. path.resolve 解析路径 这个里面 加入这个/ 会回到 根路径
  3. path.extname 读取扩展名
  4. path.basename 获取基础名字 a.js a

vm 模块

如何让一个字符串执行

  1. eval

  2. let number = 10000
    let a = 'console.log('a')'
    let b = 'console.log(b)'
    eval(a) //  a
    eval(b) //  10000  作用域不干净
    
    
  3. new Function

  4. let fn = new function('a','b',`console.log(a)`) // 有独立干净的作用域
    console.log(fn(1,2)) // 1 undefined
    
    

npm

npm link把当前目录放到全局下 方便调试

全局的模块

必须使用pack-json里面的bin选项

指明编译 #! /usr/bin/env node

本地安装

npm i xx -S-D 仅在开发下使用

npm i xx -S 开发 上线都用

buffer

文档

0x 十六进制

0b 2进制

0开头 8进制

base64 解密

arrayBuffer 前端二进制

buffer.from 把一段内容写入到内存中

buffer.alloc 申请一段内存

原型继承

events [中文文档] (nodejs.cn/api/events.…)

简单来讲。就是帮我们实现了一个 发布订阅模式

let EventEmitter = require('events')
let util = require('util')
// util.inherits()  // 继承

// 1
function Girl() {
}

// 四种原型继承的方式
// Girl.prototype.__proto__ = EventEmitter.prototype
// Girl.prototype = Object.create(EventEmitter.prototype)
// Object.setPrototypeOf(Girl.prototype,EventEmitter.prototype)
util.inherits(Girl,EventEmitter)

let girl = new Girl()

girl.on('失恋',(name)=>{
  console.log(name + ' 失恋了')
})

girl.emit('失恋','ni')


http

状态码和含义

1XX :信息状态码

一般是websocket 升级

2XX :成功状态码

200 OK 正常返回信息 201 Created 请求成功并且服务器创建了新的资源 202 Accepted 服务器已接受请求,但尚未处理

206 范围清求

3XX :重定向

301 Moved Permanently 请求的⽹⻚已永久移动到新位置。 302 Found 临时性重定向。 303 See Other 临时性重定向,且总是使⽤ GET 请求新的 URI 。 304 Not Modified ⾃从上次请求后,请求的⽹⻚未修改过。 前端无法更改

4XX :客户端错误

400 Bad Request 服务器⽆法理解请求的格式,客户端不应当尝试再次使⽤相同的内 容发起请求。 401 Unauthorized 请求未授权。 403 Forbidden 禁⽌访问。 404 Not Found 找不到如何与 URI 相匹配的资源。

405 方法不允许

5XX: 服务器错误

500 Internal Server Error 最常⻅的服务器端错误。 503 Service Unavailable 服务器端暂时⽆法处理请求(可能是过载或维护)

url uri

url 统一资源定位符,表示资源位置

uri 统一资源标识符。标识资源

模块

let http = require('http')
let url = require('url')
// 清求  请求行 请求头 请求体
// 响应   响应行   响应头   响应体
let server = http.createServer((res, req)=>{
  // 常用请求行相关信息
  let {pathname,query} = url.parse(res.url) // 默认清求路径/ 到#后面
  console.log(pathname,query)
  console.log(res.method.toLocaleLowerCase())
  console.log(res.httpVersion) //  获取http 版本
    
  // 请求头  获取的全是小写
  console.log(res.headers)
    
  // 收到请求体
  // tcp 分段传输。先收集起来最后一起    res.on('data') 获取数据   res('end') 清求结束
  let arr= []
  // 可读流内部读取不到数据 会push(null)
  res.on('data',(data)=>{ // data 只有有数据的时候才会触发
    arr.push(data)
  })
  res.on('end',()=>{ // end一定会触发
    console.log('数据')
    console.log(Buffer.concat(arr).toString())
  })

  // 响应端  相应行
  req.statusCode = 200 // 100-500
  //content-type  返回服务端内容 需要加类型
  // 文本格式 需要设置文本格式 否则容易乱码
  req.setHeader('Content-Type','text/plain;charset=utf-8')
  req.end('hello ')  // 跟写的可写流差不多
})

// 第二种方式 推荐第一种
// server.on('request',function(res) {
//   console.log(456)
// })
let port = 3000
// 端口被占用
server.on('error', (err)=>{
  if (err.code === 'EADDRINUSE') {
    server.listen(++port) // 不用再写回调 发布订阅模式 会调用之前那的回调
  }
})

server.listen(port, ()=>{
  console.log('监听成功')
})

利用createServer API 可以创建一个服务器。res清求,req响应

上述代码打印了常用的 resreq属性

有关中间层

解决了 跨域 问题( 跨域存在于浏览器端) 实例代码如下

也可以在这里面对数据进行格式化

//  中间层  可以解决跨域  原因  跨域只存在浏览器
const http = require('http')
let options = {
  port:3000,
  hostname:'localhost',
  path:'/?a=1',
  method:'POST'
}
// get请求用这种
// http.get(options,(res)=>{

// })

// 创建一个服务器去清求别人的  模拟ajax
let server = http.createServer((req, res)=>{
  let clint = http.request(options,(response)=>{ // 是一个客户端
    let arr= []
    response.on('data',(data)=>{
      arr.push(data)
    })
    response.on('end',()=>{
      console.log(Buffer.concat(arr).toString())
      res.end(Buffer.concat(arr).toString() + 'world456789')
    })
  })
})
server.listen(3001)

可能对这段代码有点难以理解的地方。

  1. 我们先创建了一个服务器
  2. 利用http.request清求我们所需要的url 把内容取回来
  3. 获取完成之后。通过res返回给我们

head

资源防盗

referer refererr 和host 对比

多语言

accept-language

正向和反向代理

代理服务器是帮助客户端的 就是正向代理

帮助服务器的就是反向代理

http-proxy

koa框架

基本使用

const koa = require('koa')

const app = new koa()
app.use((ctx)=>{
  console.log(ctx)
  ctx.body = 'hello'
})

app.listen(3000)

源文件目录结构

开始模仿

1597323095314

package.json配置

{
    // 作用 引入的时候主文件入口
  "main": "./applaction.js"
}

第一步 搭建基本架子

// application.js
const http = require('http')

module.exports = class Application {
  constructor() {
  }
  use(cb) {
    this.cb = cb
  }
  handleRequest(req,res){
    this.cb(req,res)
  }
  listen(...args) { 
    let server = http.createServer(this.handleRequest.bind(this))
    server.listen(...args)
  }
}
  1. 我们需要实现里面的 use listen 方法,在使用的过程中,我们是new Koa。所以导出的是一个类
  2. 调用listen方法的时候,我们需要开启一个服务。同时,listen方法可能有多个参数,所以我们选择使用 解构
  3. 为什么要有handleRequest存在,因为你会发现我们使用koa的过程中。是ctx上下文,所以我们需要在这里面来一层封装。同时调用use方法,把所需要的参数传入。(这里使用了一种简写的方法)

到此,基础搭建已经完成。下一步 开始封装ctx

封装ctx

首先 做一个区别

  1. ctx.req 是原生的
  2. ctx.requestkoa给我们封装好的
  3. 见官网
  4. 1597323914389
const http = require('http')
const request = require('./request')
const response = require('./response')
const context = require('./context')

module.exports = class Application {
  constructor() {
    this.request = Object.create(request)
    this.response = Object.create(response)
    this.context = Object.create(context)
  }
// use  代码没有改变  就直接省略
  createCtx(req, res) {
    // 为了防止使用同一个对象。  保证每次清求数据独立
    let request = Object.create(this.request)
    let response = Object.create(this.response)
    let context = Object.create(this.context)
    // 处理
    context.request = request 
    context.req = request.req = req
      
    context.response = response
    context.req = response.res = res
    
    return context
  }

  handleRequest(req, res) {
    // 3. 清求到来的时候需要执行use 方法
    let ctx = this.createCtx(req, res)
    this.cb(ctx)
    // 4 执行后把结果返回
    res.end(ctx.body)
  }
 // listener
}
  1. 为了 保证数据的独立性。我们使用了Object.create进行创建,以防止每次都在同一个对象上添加属性
  2. createCtx方法。是把原生req res和自己封装的request reponse方法进行整合
  3. 在执行完回调之后。调用end方法,并把结果返回 结束这一次清求

request等文件代码

// request
const url = require('url')
module.exports = {
  get url() {
    // 这里谁调用this 指向谁
    return this.req.url
  },

  get path() {
    // 这里谁调用this 指向谁
    return url.parse(this.req.url).pathname
  },

  get query() {
    // 这里谁调用this 指向谁
    return url.parse(this.req.url).query
  }
}
// context
module.exports = {
  get url() {
    // 代理机制
    return this.request.url
  },
  get path() {
    return this.request.path
  },
  get query() {
    return this.request.query
  },
    
  get body() {
    return this.response._body
  },
  set body(val) {
    this.response._body = val
  }
}
// 嫌弃麻烦的可以使用 一个函数封装 使用 defineGetter
// response
module.exports = {
  get body() {
    return this._body
  },
  set body(val) {
    this._body = val
  }
}
  1. requestthis。因为我们在createCtx里面进行了调用。谁调用就是this就是谁
  2. context 里面代码就是做了一层代理。可以选择直接用函数封装代替。

koa-compose

作用:个人理解

  1. 把各个中间件进行组合
  2. 统一为promise处理

核心就是把n个函数组合到一起

首先先看一张图, 洋葱圈模型

1107494-20180727095843894-1809770720

解释。

app.use((ctx,next)=>{
  // 处理
  ctx.body = '123456'
})
app.use((ctx)=>{
  // 处理
  ctx.body = '123456'
})

清求到来得时候,先要经过我们use里面得函数。也就称为 中间件

中间件会一层一层得把我们得清求和响应进行处理。next代表 下一个中间件

开始改造我们得代码

// ... require导入之类
const Event = require('events')

module.exports = class Application extends Event{
  constructor() {
    super()
    this.request = Object.create(request)
    this.response = Object.create(response)
    this.context = Object.create(context)
    this.middlewares = []
  }

  use(cb) {
    this.middlewares.push(cb)
  }


  compose(ctx) {
    let index = -1
    const dispatch = (i)=>{
      let middleware = this.middlewares[i]
      if (index === i)
        return Promise.reject(new Error('next() is called multiple times'))
      index = i
      if (i === this.middlewares.length)
        return Promise.resolve()
      // 返回得这个函数即 next
        
      // 防止出错 
      try {
        return Promise.resolve(middleware(ctx, ()=>dispatch(i + 1)))
      }catch (e){
        return Promise.reject(e)
      }
    }
    return dispatch(0)
  }

  handleRequest(req, res) {
    let ctx = this.createCtx(req, res)
    this.compose(ctx).then(()=>{
      res.end(ctx.body)
    }).catch((err)=>{
      this.emit('err',err)
    })
  }
   
   // listen
}
  1. 创建一个middlewares进行存储use传入进来得 callback
  2. 开始执行每一个use里面得cb。并用promise进行包裹。
  3. index作用 提示next函数被多次调用
  4. return Promise.resolve(middleware(ctx, ()=>dispatch(i + 1)))即 执行中间件,再把下一个中间件传过去
  5. 处理错误时候,继承events事件。利用发布订阅模式。把错误抛出去
  6. 防止在执行过程中出错。要加try catch
  7. 所以use里面尽量要使用async

根据类型处理body

const stream = require('stream')

// 。。。 省略
// applaction
handleRequest(req, res) {
    let ctx = this.createCtx(req, res)
    res.statusCode = 404
    this.compose(ctx).then(()=>{
      let body = ctx.body
      if (typeof body === 'string' || Buffer.isBuffer(body)) {
        res.end(body)
      } else if (body instanceof stream) {
        // 设置下载头
        res.statusCode = 200
          // 下载文件
        res.setHeader('Content-Disposition', `attachment;filename=${ ctx.path.slice(1)?ctx.path.slice(1):'downLoad' }.js`)
        body.pipe(res)
      } else if (typeof body === 'object') {
        res.end(JSON.stringify(body))
      } else if (typeof body === 'number') {
        res.end(body + '')
      } else {
        res.end('Not Found')
      }
    }).catch((err)=>{
      this.emit('err', err)
    })
  }

// response
module.exports = {
  _body:undefined,
  get body() {
    return this._body
  },
  set body(val) {
    this.res.statusCode = 200
    this._body = val
  }
}

思路

  1. 我们首先设置默认为 404
  2. 如果设置了body 属性。把satauscode 设置为200返回
  3. 应为res.end()只接受bufferString类型。 所以我们要对类型进行分布处理

koa-bodyparser

npm

const koa = require('koa')
const bodyparser = require('koa-bodyparser')


const app = new koa()
app.use(bodyparser())

app.use((ctx, next)=>{
  if (ctx.method === 'GET' && ctx.path === '/form') {
    ctx.body = '<form action="/form" method="post">\n' +
        '    用户名<input type="text" name="username">\n' +
        '    <br>\n' +
        '    密码<input type="password" name="username">\n' +
        '    <button>提交</button></form>'
  } else {
    next()
  }
})
app.use(async (ctx, next)=>{
  if (ctx.method === 'POST' && ctx.path === '/form') {
    ctx.body = await ctx.request.body
  } else {
    next()
  }
})

app.listen(3000)

简单模仿

const bodyparser = ()=>{
  return async (ctx,next)=>{
    await new Promise((resolve, reject)=>{
      let arr = []
      ctx.req.on('data',(data)=>{
        arr.push(data)
      })
      ctx.req.on('end',()=>{
        ctx.request.body = Buffer.concat(arr).toString()
        resolve()
      })
    })
    await next()
  }
}
module.exports = bodyparser
//  导入自己得就可以

方法

  1. 首先需要返回一个函数
  2. 把数据收集起来。再赋值给ctx.request.body
  3. 记得调用next()方法

koa-static

npm

使用

app.use(KoaStatic(__dirname))
app.use(KoaStatic(path.resolve(__dirname,'node_modules')))
// 可以直接访问静态资源目录

简易模仿

let fs = require('fs').promises
let path = require('path')

const koaStatic = (filePath)=>{
  return async (ctx, next)=>{
    let pathUrl = path.join(filePath, ctx.path)
    let stats = await fs.stat(pathUrl)
    if (stats.isFile()) {
      ctx.set('Content-type', 'text/html;charset=utf-8')
      ctx.body = await fs.readFile(pathUrl)
    } else {
      await next()
    }
  }
}
module.exports = koaStatic

  1. require('fs').promises是可以用promise 而不用回调得方式来写
  2. 实现就是拼接路径 读取文件。把ctx.body赋值

以下只作为提及一下

koa-router

路由管理

koa/multer

文件上传

koa-genetator

脚手架

express框架

基本使用

来自官网

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => res.send('Hello World!'))
app.post('/', function (req, res) {
  res.send('Got a POST request')
})

app.listen(port, () => console.log(`Example app listening on port ${port}!`))

多个回调函数的使用

express里面有类似koa``next的函数。作用也几乎相同

const express = require('express')
const app = express()
const port = 3001


// 访问输出  1234
app.get('/', function(req, res, next) {
  console.log(1)
  next()
},function(req, res, next) {
  console.log(2)
  next()
  console.log(4)
},function(req, res, next) {
  console.log(3)
  next()
  res.end('Hello World!')
})
app.listen(port, ()=>console.log(`Example app listening on port ${ port }!`))

中间件的使用

官网例子

const express = require('express')
const app = express()
const port = 3001


app.use(function(req, res, next) {
  console.log('Time:', Date.now())
  next()
})
app.get('/', function(req, res, next) {
  // 这里面可以做一些   路由中间件处理   如 处理路径之类
  console.log(1)
  next()
}, function(req, res, next) {
  console.log(2)
  next()
  console.log(4)
}, function(req, res) {
  console.log(3)
  res.end('Hello World!')
})

app.listen(port, ()=>console.log(`Example app listening on port ${ port }!`))

先输出时间戳,再后来是 1 2 3 4 .则证明 use中间件的使用在路由之前

与koa区别

相同点

  1. 都具有 中间件的概念

不同点

  1. koa里面使用的 promise async await,而express使用的是 回调 的方式
  2. express内置了很多中间件,比如路由 静态服务,模板等。而Koa核心很小
  3. express使用了ES5koa 使用的基本是ES6

开始模仿

测试代码

const express = require('./myExpress')
const app = express()
const port = 3001

app.get('/', (req, res)=>res.end('Hello World!'))

app.listen(port, ()=>console.log(`Example app listening on port ${ port }!`))

主代码

首先有一个思路。

  1. 我们需要创建一个服务。需要http模块,解析路由,需要url模块
  2. 可以访问.get方法。证明返回的是一个 对象
  3. getlisten方法
  4. 路由匹配原则。把路由存到数组里。接收到request之后。遍历数组进行匹配。成功之后执行对应的handler.失败执行对应的处理函数
const http = require('http')
const url = require('url')

let routers = [{ // 用来处理 匹配不到路由
  path: '*',
  method: 'all',
  handler(req, res) {
    res.end(`Cannot ${ req.method } ${ req.url }`)
  }
}]

function createApplication() {
  // 返回的应该是一个对象
  return {
    get(path, handler) {
      routers.push({
        path,
        method: 'get',
        handler
      })
    },
    listen(...args) {
      let server = http.createServer((req, res)=>{
        let { pathname } = url.parse(req.url)
        let m = req.method.toLocaleLowerCase()
        for (let i = 0; i < routers.length; i++) {
          let { path, method, handler } = routers[i]
          if (path === pathname && method === m) {
            return handler(req, res)
          }
        }
        routers[0].handler(req, res)
      })
      server.listen(...args)
    }
  }
}
module.exports = createApplication

开始拆分

作用。写在一起不容易维护。单独抽离,更加方便管理

目录

|-- myExpress
    |-- package.json
    |-- lib
        |-- createApplication.js
        |-- express.js
        |-- router
            |-- index.js

express

const Application = require('./createApplication')

function createApplication() {
  return new Application()
}

module.exports = createApplication

createApplication

const http = require('http')
const Router = require('./router')


function Application() {
  this.routers = new Router()
}

Application.prototype.get = function(path, handler) {
  this.routers.get(path,handler)
}
Application.prototype.listen = function(...args) {
  let server = http.createServer((req, res)=>{
    function done(){
      res.statusCode = 404
      res.end(`Cannot ${ req.method } ${ req.url }`)
    }
    this.routers.match(req,res,done)
  })
  server.listen(...args)
}

module.exports = Application

router/index

const url = require('url')


function Router() {
  this.stack = []
}

Router.prototype.get = function(path, handler) {
  this.stack.push({
    path,
    method: 'get',
    handler
  })
}

Router.prototype.match = function(req, res, done) {
  let { pathname } = url.parse(req.url)
  let m = req.method.toLocaleLowerCase()

  for (let i = 0; i < this.stack.length; i++) {
    let { path, method, handler } = this.stack[i]
    if (path === pathname && method === m) {
      return handler(req, res)
    }
  }
  done()
}
module.exports = Router

多个回调的模仿

先给一个流程图

UTOOLS1598083780444.png

解释一下流程图

  1. 请求到来的时候在 Router Stack 里面匹配路径
  2. 如果匹配成功,调用 dispatch方法
  3. dispatchroute Stack里面吧对应的回调函数执行

通俗理解。就是用了一个数组吧所有的回调函数存了起来。当匹配到这个路径的时候,把这个数组里面的回调函数 按条件 依次执行

// router / index
const url = require('url')
const Route = require('./route')
const Layer = require('./layer')


function Router() {
  this.stack = []
}

Router.prototype.route = function(path) {
  let route = new Route()
  let layer = new Layer(path, route.dispatch.bind(route))
  layer.route = route
  this.stack.push(layer)
  return route
}


Router.prototype.get = function(path, handler) {
  let route = this.route(path)
  route.get(handler) // 传入的是数组
}

Router.prototype.match = function(req, res, done) {
  let { pathname } = url.parse(req.url)
  let m = req.method.toLocaleLowerCase()
  let idx = 0
  let next = ()=>{
    if (idx >= this.stack.length) return done()
    let layer = this.stack[idx++]
    if (layer.path === pathname) {
      layer.handler(req, res, next)
    } else {
      next()
    }
  }
  next()
}
module.exports = Router
// router layer
function Layer(path,handler){
  this.path = path
  this.handler = handler
}

module.exports = Layer
// router route.js
const Layer = require('./layer')

function Route() {
  this.stack = []
}


Route.prototype.get = function(handler) {
  handler.forEach(handle=>{
    let layer = new Layer('/', handle)
    layer.method = 'get'
    this.stack.push(layer)
  })
}
Route.prototype.dispatch = function(req, res, done) {
  let idx = 0
  let next = ()=>{
    if (idx > this.stack.length) return done()
    let layer = this.stack[idx++]
    if (layer.method === req.method.toLowerCase()) {
      layer.handler(req, res, next)
    } else {
      next()
    }
  }
  next()
}
module.exports = Route

测试结果和原生的一样

优化

延迟加载路由

​ 在我们写的过程中,当创建了express的时候,就初始化了路由。当使用者不使用的时候,这就会造成了浪费,所以我们应该延迟加载路由系统,当需要使用的时候在进行使用

// lib/createApplication
const http = require('http')
const Router = require('./router')


function Application() {
}

Application.prototype.lazy_router = function() {
  if (!this.routers) {
    this.routers = new Router()
  }
}
Application.prototype.get = function(path, ...handler) {
  this.lazy_router()
  this.routers.get(path, handler)
}
Application.prototype.listen = function(...args) {
  let server = http.createServer((req, res)=>{
    this.lazy_router()

    function done() {
      res.statusCode = 404
      res.end(`Cannot ${ req.method } ${ req.url }`)
    }

    this.routers.match(req, res, done)
  })
  server.listen(...args)
}

module.exports = Application

就是在原型上加了一个lazy_router方法,在使用的过程中先调用一下这个方法,以防止万一没有路由系统

补全请求方式

在我们代码中,我们只写了一个get请求。所以我们需要补全所有请求方式。一个个写当然麻烦。肯定是用到循环,这里推荐一个包methods

这里还是以createApplication作为模板,了解一下用法。其他需要修改的请自行修改

// lib/createApplication
const http = require('http')
const Router = require('./router')
const methods = require('methods')

function Application() {
}
// ..........
methods.forEach(method=>{
  Application.prototype[method] = function(path, ...handler) {
    this.lazy_router()
    this.routers[method](path, handler)
  }
})
//    ....
module.exports = Application

减少循环次数

按照我们写的来说。如果当前路径没有对应的请求方法。依旧会循环所有handler。这样的花效率不高,所以我们应该加一个标识,标识这个回调处理的何种方法的请求。当请求方式不匹配的时候。直接return 这样就减少了次数

中间件实现

思路流程

  1. 首先在Application原型上挂在一个 use方法。(只做分发,发给路由)
  2. 在路由上创建use方法。根据所传入参数做处理
  3. 创建layer 丢入 stack
  4. 更改match匹配规则。判断条件后执行

代码实现

// createApplication
Application.prototype.use = function(path,handler) {
  this.lazy_router()
  this.routers.use(path,handler)
}

//Layer方法扩展
Layer.prototype.match = function(pathname) {
  if (this.path === pathname) {
    return true
  }
  if (!this.route) {
    if (this.path === '/') return true
    return pathname.startsWith(this.path + '/')  //  /aa/b   /a  不能执行
  }
  return false
}
module.exports = Layer

// router/index
//  。。。。省略部分代码
Router.prototype.use = function(path, handler) {
  if (typeof handler !== 'function') {
    handler = path
    path = '/'
  }
  let layer = new Layer(path, handler)
  layer.route = undefined  // 证明是中间件
  this.stack.push(layer)
}

Router.prototype.match = function(req, res, done) {
  let { pathname } = url.parse(req.url)
  let m = req.method.toLocaleLowerCase()
  let idx = 0
  let next = ()=>{
    if (idx >= this.stack.length) return done()
    let layer = this.stack[idx++]
    // 开始改造
    if (layer.match(pathname)) { // 首先匹配路径
      if (!layer.route) { // 中间件
        layer.handler(req, res, next)
      } else {  // 路由  需要匹配方法
        if (layer.route.methods[m]) {
          layer.handler(req, res, next)
        } else {
          next()
        }
      }
    } else {
      next()
    }
  }
  next()
}
module.exports = Router

具体实现过程

  1. 因为用户可能传入一个或者两个参数,在use里进行判断。如果传入的是一个函数,则path默认是/
  2. layer.route = undefined这句代码是为了 标识中间件. 路由中 这里面放的是route
  3. 接下来就是匹配规则。重点说一下 Layer / match方法
  4. 如果传入的不匹配。观察是不是中间件 ,是的话看有没有路由限制。在选择执行
  5. 注意,中间件的handler 就是传入的handler 不是dispatch。因为中间件不支持传入多个函数参数

错误中间件

官网

app.use(function(req, res, next) {
  if (req.url === '/a') next('error')
  next()
})

// 错误中间件
app.use(function (err, req, res, next) {
  console.error(err.stack)
})

即 在next()里面写入内容。如果发生错误 就走到下面这个 错误中间件

未命名文件.png

当 发生错误之后。跳过。直接 执行错误中间件里面的代码

由此我们可以知道。主要修改的代码就是 match过程中。还有中间件的dispatch

// router/route
Route.prototype.dispatch = function(req, res, done) {
  let idx = 0
  let next = (err)=>{
    if (err) return done(err)
    if (idx > this.stack.length) return done()
    let layer = this.stack[idx++]
    if (layer.method === req.method.toLowerCase()) {
      layer.handler(req, res, next)
    } else {
      next()
    }
  }
  next()
}

//  router/index

Router.prototype.match = function(req, res, done) {
  let { pathname } = url.parse(req.url)
  let m = req.method.toLocaleLowerCase()
  let idx = 0
  let next = (err)=>{
    if (idx >= this.stack.length) return done()
    let layer = this.stack[idx++]
    if (err) { // 如果有错误 找错误处理中间件
      if (!layer.route) { // 判断参数时候是4个
        layer.handler(err, req, res, next)
      } else {
        next(err)
      }
    } else {
      if (layer.match(pathname)) { // 首先匹配路径
        if (!layer.route) { // 是中间件 且非错误中间件
          if (layer.handler.length !== 4) {
            layer.handler(req, res, next)
          } else {
            next()
          }
        } else {  // 路由  需要匹配方法
          if (layer.route.methods[m]) {
            layer.handler(req, res, next)
          } else {
            next()
          }
        }
      } else {
        next()
      }
    }
  }
  next()
}
  1. 其实就是判断是否有err,如果在 路由系统中匹配到了话,直接跳过
  2. 在中间件中。调用next方法。把err一直往下传递
  3. layer.handler.length是指 函数的参数个数,错误中间件的参数个数为四个,且必须放在最后