Nodejs学习记录: koa2

图片描述

koa list in github : https://github.com/topics/koa

image.png

异常处理

图片描述
图片描述

并发

图片描述

async / await

特点

  • 让异步逻辑用同步写法实现
  • 最底层的await返回需要是Promise对象
  • 可以通过多层 async function 的同步写法代替传统的callback嵌套
function getSyncTime() {
  return new Promise((resolve, reject) => {
    try {
      let startTime = new Date().getTime()
      setTimeout(() => {
        let endTime = new Date().getTime()
        let data = endTime - startTime
        resolve( data )
      }, 500)
    } catch ( err ) {
      reject( err )
    }
  })
}

async function getSyncData() {
  let time = await getSyncTime()
  let data = `endTime - startTime = ${time}`
  return data
}

async function getData() {
  let data = await getSyncData()
  console.log( data )
}

getData()

图片描述
了解更多异步编程,可以戳鲸鱼之前的笔记Nodejs学习记录:异步编程

现在我们实现异步编程是用 async/await 加上 Promise, 那么我们使用Promise如何兼容以前的回调呢?--> async awarit


const fs = require("fs");

const readFilePromise = filename => {
   new Promise((resolve, reject) => {
      fs.readFile(filename, (err, data) => {
         if(err) {
            reject(err)
            return
         }
         resolve(data)
     })
  })
}


async function main() {
  const txt = await readFilePromise("mock.txt")
  console.log(txt.toString())
}

main()

cookie

HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:Cookie。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。

cookie经常用于做登录信息的储存,当然我们在后端经常喜欢用它,在前端的单页应用一般喜欢用localstorage

通过 ctx.cookies,我们可以在 controller 中便捷、安全的设置和读取 Cookie。

来个简单的案例,看看如何写入cookie

koa提供了从上下文直接读取、写入cookie的方法

  • ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
  • ctx.cookies.set(name, value, [options]) 在上下文中写入cookie

设置Cookie其实是通过在HTTP响应中设置set-cookie头完成,每个set-cookie都会让浏览器在Cookie中存一个键值对。在设置Cookie值同时,协议还支持许多参数来配置这个Cookie的传输、储存和权限

  • {Number} maxAge: 设置这个键值对在浏览器的最长保存时间。是一个从服务器当前时刻开始的毫秒数。
  • {Date} expires: 设置这个键值对的失效时间,如果设置了 maxAge,expires 将会被覆盖。如果 maxAge 和 expires 都没设置,Cookie 将会在浏览器的会话失效(一般是关闭浏览器时)的时候失效。
  • {String} path: 设置键值对生效的 URL 路径,默认设置在根路径上(/),也就是当前域名下的所有 URL 都可以访问这个 Cookie。
  • {String} domain: 设置键值对生效的域名,默认没有配置,可以配置成只在指定域名才能访问。
  • {Boolean} httpOnly: 设置键值对是否可以被 js 访问,默认为 true,不允许被 js 访问。
  • {Boolean} secure: 设置键值对只在 HTTPS 连接上传输,框架会帮我们判断当前是否在 HTTPS 连接上自动设置 secure 的值。
  • {Boolean} overwrite

图片描述
感兴趣的可以看看 cookie的实现源码

koa2 中操作的cookies是使用了npm的cookies模块,所以在读写cookie的使用参数与该模块的使用一致。
源码在:https://github.com/pillarjs/c...

const Koa = require('koa')
const app = new Koa()
app.use(async(ctx) => {
    if(ctx.url === '/index'){
        ctx.cookies.set(
            'cid',
            'hello world',
            {
                domain: 'localhost', //写cookie所在的域名
                path: '/index',// 写cookie所在的路径
                maxAge:10 * 60 * 1000, // cookie有效时长
                expires: new Date('2017-02-15'), // cookie失效时间
                httpOnly: false, // 是否只用于http请求中获取
                overwrite:false // 是否允许重写
            }
        )
        ctx.body = 'cookie is ok'
    } else {
        ctx.body = 'hello world'
    }
})

app.listen(3000, () => {
    console.log('[demo] cookie is starting at port 3000')
})

在设置 Cookie 时我们需要思考清楚这个 Cookie 的作用,它需要被浏览器保存多久?是否可以被 js 获取到?是否可以被前端修改?

访问http://localhost:3000/index

  • 可以在控制台的cookie列表中中看到写在页面上的cookie
  • 在控制台的console中使用document.cookie可以打印出在页面的所有cookie(需要是httpOnly设置false才能显示)

图片描述
图片描述

更改下代码

const Koa = require('koa')
const app = new Koa()
app.use(async(ctx) => {
    if(ctx.url === '/index'){
        ctx.cookies.set(
            'cid',
            'hello world',
            {
                domain: 'localhost', //写cookie所在的域名
                path: '/index',// 写cookie所在的路径
                maxAge:10 * 60 * 1000, // cookie有效时长
                expires: new Date('2017-02-15'), // cookie失效时间
                httpOnly: false, // 是否只用于http请求中获取
                overwrite:false // 是否允许重写
            }
        );
        ctx.body = 'cookie is ok';
    } else {
        if(ctx.cookies.get('cid')){
            ctx.body= ctx.cookies.get('cid');
        }else {
            ctx.body = 'cookie is none';
        }
    }
})

app.listen(3000, () => {
    console.log('[demo] cookie is starting at port 3000')
})

重启服务器node cookie.js,浏览器分别输入
http://localhost:3000/index/ http://localhost:3000
http://localhost:3000/index/aa
图片描述
图片描述
因为我们配置了

{path:'/index'}

图片描述

session

Cookie 在 Web 应用中经常承担标识请求方身份的功能,
但是cookie信息会被储存在浏览器本地或硬盘中,这样会有安全问题,如果有人能够访问你的电脑就能分析出你的敏感信息,用户名、密码等等。为了解决这个隐患,所以 Web 应用在 Cookie 的基础上封装了 Session 的概念,专门用做用户身份识别。

既然服务器渲染又需要用户登录功能,那么用session去记录用户登录态是必要的

koa2原生功能只提供了cookie的操作,但是没有提供session操作。session就只能自己实现或者通过第三方中间件实现。
但是基于koa的egg.js框架内置了 Session 插件,给我们提供了 ctx.session 来访问或者修改当前用户 Session 。----> cookie & session
在koa2中实现session的方案有:

  • 如果session数据量很小,可以直接存在内存中
  • 如果session数据量很大,则需要存储介质存放session数据

 数据库存储方案

 储存在MySQL

需要用到中间件:

  • koa-session

图片描述

  • koa-session-minimal 适用于koa2 的session中间件,提供存储介质的读写接口 。
  • koa-mysql-session 为koa-session-minimal中间件提供MySQL数据库的session数据读写操作。

然后,我们将将sessionId和对应的数据存到数据库

将数据库的存储的sessionId存到页面的cookie中

根据cookie的sessionId去获取对于的session信息

图片描述
在mysql数据库创建 Koa_session_demo数据库
图片描述

const Koa = require('koa')
const session = require('koa-session-minimal');
const MysqlSession = require('koa-mysql-session')

const app = new Koa()

//配置存储session信息的mysql

let store = new MysqlSession({
    user: 'root',
    password: 'wyc2016',
    database: 'koa_session_demo',
    host: '127.0.0.1',
})

//存放sessionId的cookie配置

let cookie = {
    maxAge: '',// cookie有效时长
    expires: '',// cookie失效时间
    path: '', // 写cookie所在的路径
    domain: '', // 写cookie所在的域名
    httpOnly: '', // 是否只用于http请求中获取
    overwrite: '',  // 是否允许重写
    secure: '',
    sameSite: '',
    signed: '',
}


// 使用session中间件

app.use(session({
    key: 'SESSION_ID',
    store: store,
    cookie: cookie
}))

app.use(async (ctx) => {
    // 设置session

    if(ctx.url === '/set') {
        ctx.session = {
            user_id: Math.random().toString(36).substr(2),
            count:0
        }
        ctx.body = ctx.session
    }else if (ctx.url === '/'){
        // 读取session信息
        ctx.session.count = ctx.session.count + 1
        ctx.body = ctx.session
    }
})

app.listen(3000, ()=> {
    console.log('[demo] session is starting at port 3000');
})

图片描述
图片描述
图片描述

 储存在redis

在express中我们用的是express-session,那么在koa2中用的是哪些模块:

注意:一旦选择了将 Session 存入到外部存储中,就意味着系统将强依赖于这个外部存储,当它挂了的时候,我们就完全无法使用 Session 相关的功能了。因此我们更推荐大家只将必要的信息存储在 Session 中,保持 Session 的精简并使用默认的 Cookie 存储,用户级别的缓存不要存储在 Session 中。

模板引擎

文件上传(upload)

busboy模块

npm install --save busboy

busboy 模块是用来解析POST请求,node原生req中的文件流。
更多详细API可以访问npm官方文档 https://www.npmjs.com/package...

const inspect = require('util').inspect 
const path = require('path')
const fs = require('fs')
const Busboy = require('busboy')

// req 为node原生请求
const busboy = new Busboy({ headers: req.headers })

// ...

// 监听文件解析事件
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
  console.log(`File [${fieldname}]: filename: ${filename}`)


  // 文件保存到特定路径
  file.pipe(fs.createWriteStream('./upload'))

  // 开始解析文件流
  file.on('data', function(data) {
    console.log(`File [${fieldname}] got ${data.length} bytes`)
  })

  // 解析文件结束
  file.on('end', function() {
    console.log(`File [${fieldname}] Finished`)
  })
})

// 监听请求中的字段
busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated) {
  console.log(`Field [${fieldname}]: value: ${inspect(val)}`)
})

// 监听结束事件
busboy.on('finish', function() {
  console.log('Done parsing form!')
  res.writeHead(303, { Connection: 'close', Location: '/' })
  res.end()
})
req.pipe(busboy)

上传文件简单实现

#index.js
const Koa = require('koa');
const path = require('path');
const app = new Koa();

const {uploadFile} = require('./util/upload')

app.use( async (ctx) => {
    if(ctx.url === '/' && ctx.method === 'GET') {
        //当GET请求时候返回表单页面

        let html = `
          <h1>koa2 upload demo</h1>
            <form method="POST" action="/upload.json" enctype="multipart/form-data">
            <p>file upload</p>
            <span>picName:</span><input name="picName" type="text" /><br/>
            <input name="file" type="file" /><br/><br/>
            <button type="submit">submit</button>
            </form>
        ` 
        ctx.body = html
    }else if (ctx.url === '/upload.json' && ctx.method ==='POST'){
        // 上传文件请求处理
        let result = {success: false}
        let serverFilePath = path.join(__dirname, 'upload-files')

        //上传文件事件
        result = await uploadFile(ctx, {
            fileType: 'album',
            path: serverFilePath
        })
        ctx.body = result
    }else {
        // 其他请求显示404
        ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
    }

  })

app.listen(3000, () => {
    console.log('[demo] upload-simple is starting at port 3000');
})
# util/upload.js
const inspect = require('util').inspect
const path = require('path')
const fs = require('fs')
const Busboy = require('busboy')

/**
 * 同步创建文件目录
 * @param  {string} dirname 目录绝对地址
 * @return {boolean}        创建目录结果
 */
function mkdirsSync( dirname ) {
  if (fs.existsSync( dirname )) {
    return true
  } else {
    if (mkdirsSync( path.dirname(dirname)) ) {
      fs.mkdirSync( dirname )
      return true
    }
  }
}

/**
 * 获取上传文件的后缀名
 * @param  {string} fileName 获取上传文件的后缀名
 * @return {string}          文件后缀名
 */
function getSuffixName( fileName ) {
  let nameList = fileName.split('.')
  return nameList[nameList.length - 1]
}

/**
 * 上传文件
 * @param  {object} ctx     koa上下文
 * @param  {object} options 文件上传参数 fileType文件类型, path文件存放路径
 * @return {promise}
 */
function uploadFile( ctx, options) {
  let req = ctx.req
  let res = ctx.res
  let busboy = new Busboy({headers: req.headers})

  // 获取类型
  let fileType = options.fileType || 'common'
  let filePath = path.join( options.path,  fileType)
  let mkdirResult = mkdirsSync( filePath )

  return new Promise((resolve, reject) => {
    console.log('文件上传中...')
  let result = {
    success: false,
    formData: {},
  }

  // 解析请求文件事件
  busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
    let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
    let _uploadFilePath = path.join( filePath, fileName )
    let saveTo = path.join(_uploadFilePath)

    // 文件保存到制定路径
    file.pipe(fs.createWriteStream(saveTo))

    // 文件写入事件结束
    file.on('end', function() {
      result.success = true
      result.message = '文件上传成功'
      console.log('文件上传成功!')
    })
  })

  // 解析表单中其他字段信息
  busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
    console.log('表单字段数据 [' + fieldname + ']: value: ' + inspect(val));
    result.formData[fieldname] = inspect(val);
  });

  // 解析结束事件
  busboy.on('finish', function( ) {
    console.log('文件上结束')
    resolve(result)
  })

  // 解析错误事件
  busboy.on('error', function(err) {
    console.log('文件上出错')
    reject(result)
  })

  req.pipe(busboy)
})

}


module.exports =  {
  uploadFile
}

图片描述
图片描述
图片描述

异步上传图片实现

源码:https://github.com/JXtreehous...
图片描述

通过stream的方式上传文件

参考中间件-> aliyun-oss-upload-stream

为什么使用``stream?
  • 使用stream的方式上传文件可以很大程度上降低服务器内存开销。Aliyun官方SDK并没有对stream进行一个完美的封装,所以通常上传文件(Put Object)的流程是客户端上传文件到服务器,服务器把文件数据缓存到内存,等文件全部上传完毕后,一次性上传到Aliyun Oss服务。这样做一旦瞬间上传文件的请求过多,服务器的内存开销会直线上升。而使用stream的方式上传文件的流程是客户端在上传文件数据到服务器的过程中,服务器同时也在把文件数据往Aliyun Oss服务传送,而不需要在服务器上缓存文件数据。
  • 可以上传大文件,根据上传数据方式不同而不同, Put Object 方式 文件最大不能超过 5GB,而使用stream的方式,文件大小不能超过 48.8TB
  • 更快的速度,由于传统方式(Put Object方式)是客户端上传完毕文件后,统一上传到Aliyun Oss,而stream的方式基本上客户端上传完毕后,服务器已经把一大半的文件数据上传到Aliyun了,所以速度要快很多
  • 使用更简单,经过封装后,stream的方式使用起来非常的方便,1分钟就可以学会如何使用

数据库

sequelize 连接池

ORM(Object Relational Mapping)框架,提供了了PostgreSQL, MySQL, SQLite and MSSQL 数据库连接池
import Sequelize from "sequelize";
const sequelize = new Sequelize('mock_server', 'root', '123456', {
    host: 'localhost',
    dialect: 'mysql',
    dialectOptions: {
    charset: "utf8mb4",
    collate: "utf8mb4_unicode_ci",
    supportBigNumbers: true,
    bigNumberStrings: true
},
pool: {
    max: 5,
    min: 0,
    idle: 10000
}
});
const sequelize = ne

Sequelize ORM

this.dao = sequelize.define('Api', {
    project_id: Sequelize.INTEGER,
    url: Sequelize.STRING,
    request_body: Sequelize.TEXT,
    response_body: Sequelize.TEXT,
    user_id: Sequelize.INTEGER,
    description: Sequelize.TEXT,
    host: Sequelize.STRING,
});
this.dao.sync();
let api = this.dao.findOne({
    where: {
        user_id: user_id,
        id: api_id,
        project_id: pid,
    }
});

将查询结果封装成Promise

Sequelize 事务

Database Transaction

  • 托管事务:根据 Promise 链的结果⾃自动提交或回滚事

务,如果遇到⼀一⾏行行,⾃自动回滚所有操作

  • 非托管事务:如果出现异常,需要⼿手动处理理commit或

者执⾏行行rollback操作

mysql

npm install --save mysql

mysql模块是node操作MySQL的引擎,可以在node.js环境下对MySQL数据库进行建表,增、删、改、查等操作。

创建数据库会话

onst mysql      = require('mysql')
const connection = mysql.createConnection({
  host     : '127.0.0.1',   // 数据库地址
  user     : 'root',    // 数据库用户
  password : '123456'   // 数据库密码
  database : 'my_database'  // 选中数据库
})
 
// 执行sql脚本对数据库进行读写 
connection.query('SELECT * FROM my_table',  (error, results, fields) => {
  if (error) throw error
  // connected! 
  
  // 结束会话
  connection.release() 
});
注意:一个事件就有一个从开始到结束的过程,数据库会话操作执行完后,就需要关闭掉,以免占用连接资源。

创建数据连接池

一般情况下操作数据库是很复杂的读写过程,不只是一个会话,如果直接用会话操作,就需要每次会话都要配置连接参数。所以这时候就需要连接池管理会话。

const mysql = require('mysql')

// 创建数据池
const pool  = mysql.createPool({
  host     : '127.0.0.1',   // 数据库地址
  user     : 'root',    // 数据库用户
  password : '123456'   // 数据库密码
  database : 'my_database'  // 选中数据库
})
 
// 在数据池中进行会话操作
pool.getConnection(function(err, connection) {
   
  connection.query('SELECT * FROM my_table',  (error, results, fields) => {
    
    // 结束会话
    connection.release();
 
    // 如果有错误就抛出
    if (error) throw error;
  })
})

更多详细API可以访问npm官方文档 https://www.npmjs.com/package...

async/await封装使用mysql

由于mysql模块的操作都是异步操作,每次操作的结果都是在回调函数中执行,现在有了async/await,就可以用同步的写法去操作数据库
Promise封装mysql模块
Promise封装 ./async-db

const mysql = require('mysql')
const pool = mysql.createPool({
  host     :  '127.0.0.1',
  user     :  'root',
  password :  '123456',
  database :  'my_database'
})

let query = function( sql, values ) {
  return new Promise(( resolve, reject ) => {
    pool.getConnection(function(err, connection) {
      if (err) {
        reject( err )
      } else {
        connection.query(sql, values, ( err, rows) => {

          if ( err ) {
            reject( err )
          } else {
            resolve( rows )
          }
          connection.release()
        })
      }
    })
  })
}

module.exports = { query }

async/await使用

const { query } = require('./async-db')
async function selectAllData( ) {
  let sql = 'SELECT * FROM my_table'
  let dataList = await query( sql )
  return dataList
}

async function getData() {
  let dataList = await selectAllData()
  console.log( dataList )
}

getData()

建表初始化

通常初始化数据库要建立很多表,特别在项目开发的时候表的格式可能会有些变动,这时候就需要封装对数据库建表初始化的方法,保留项目的sql脚本文件,然后每次需要重新建表,则执行建表初始化程序就行

├── index.js # 程序入口文件
├── node_modules/
├── package.json
├── sql   # sql脚本文件目录
│   ├── data.sql
│   └── user.sql
└── util    # 工具代码
    ├── db.js # 封装的mysql模块方法
    ├── get-sql-content-map.js # 获取sql脚本文件内容
    ├── get-sql-map.js # 获取所有sql脚本文件
    └── walk-file.js # 遍历sql脚本文件

https://chenshenhai.github.io...
https://github.com/ChenShenha...

图片描述
图片描述
图片描述
图片描述

Redis

nodejs调用redis服务

import redis from "redis";
const client = redis.createClient(({
    port: "19002",
    host: "localhost"
}));

使用场景

  • 首⻚页数据列列表,top N列列表
  • 日志
  • 计数信息(如接⼝口访问次数等)
  • mock数据

难点

  • 数据同步
  • 内存飙升

JSONP

图片描述
图片描述

原生koa2实现jsonp

在项目复杂的业务场景,有时候需要在前端跨域获取数据,这时候提供数据的服务就需要提供跨域请求的接口,通常是使用JSONP的方式提供跨域接口。
https://chenshenhai.github.io...
https://github.com/ChenShenha...

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

app.use(async (ctx) => {
    // 如果jsonp 的请求为GET
    if(ctx.method === 'GET' && ctx.url.split('?')[0] ==='/getData.jsonp'){

      // 获取jsonp的callback
      let callbackName = ctx.query.callback || 'callback'
      let returnData = {
          success: true,
          data: {
              text: 'this is a jsonp api',
              time: new Date().getTime(),
          }
      }

      // jsonp的script字符串
      let jsonpStr = `;${callbackName}(${JSON.stringify(returnData)})`

      // 用text/javascript,让请求支持跨域获取
      ctx.type = 'text/javascript'

      //输出jsonp字符串
      ctx.body = jsonpStr
    }else{
      ctx.body = 'hello jsonp'
    }
})

app.listen(3000, () => {
    console.log('[demo] jsonp is starting at port 3000')
})

前端部分

$.ajax({
  url: 'http://localhost:3000/getData.jsonp',
  type: 'GET',
  dataType: 'JSONP',
  success: function(res) {
    console.log(res)
  }
})

koa-jsonp中间件

npm install --save koa-jsonp

https://www.npmjs.com/package...
https://github.com/chenshenha...

markdown

OAuth 2.0

OAuth 2.0深入了解:以微信开放平台统一登录为例
微信开放平台开发——网页微信扫码登录(OAuth2.0)

常用中间件

图片描述
Koa Static Cache: https://www.npmjs.com/package...

更多中间件
koa-onerror
koa-safe-jsonp

常用脚手架

hello-koa2-mongodb

koa-generator
yi-ge/Koa2-API-Scaffold

- Express-style
- Support koa 1.x(supported)
- Support koa 2.x(koa middleware supported,need Node.js 7.6+ ,babel optional)

restful api

uri:

 http://localhost:7001/game/62231163?channel=1323

router:

clipboard.png

controller.game.detail

const { gameId, channel } = ctx.query;
const _gameId = ctx.params.id || gameId; // 一级分类

 console.log('-------------------1', ctx.query)
  console.log('-------------------2', gameId)
  console.log('-------------------3', ctx.url)
  console.log('---------------------4', ctx.params)
  console.log('---------------------5', ctx.querystring)
  console.log('--------------------6', ctx.origin)
      
      

clipboard.png

https://www.jianshu.com/p/d0b...

ssrf

看我如何利用NodeJS SSRF漏洞获得AWS完全控制权限

Web安全漏洞之SSRF
Web 安全漏洞 SSRF 简介及解决方案
welefen/ssrf-agent

sms(短信服务、邮箱服务 )

参考项目

egg-commerce
koajs/examples
johndatserakis/koa-vue-notes-api
使用koa2+wechaty打造个人微信小秘书
https://github.com/jtyjty9999...

常见问题

koaexpress区别

关于区别详解 戳我这篇文章express中间件 文末
图片描述
图片描述

koa原理
  • 中间件(Middleware)
  • 上下文(Context)

图片描述

参考

Koa.js 设计模式-学习笔记
koa2进阶学习笔记
koa2 源码分析
npm koa-session-minimal
koa2中的session及redis
egg.js Cookie and Session
《HTTP权威指南》
七天学会NodeJS

Koa2 之文件上传下载
node消息队列
快速搭建可用于实战的koa2+mongodb框架
https://chenshenhai.github.io...
KOA2框架原理解析和实现
koa 介绍 ppt
image

阅读 5.3k

推荐阅读
镜心的小树屋
用户专栏

方寸湛然GitHub组织地址:[链接]

47 人关注
123 篇文章
专栏主页