Koa笔记

721 阅读4分钟

初始化项目

# 初始化package.json
npm init

# 安装koa2 
npm install koa

hello world 代码

 const Koa = require('koa');	//引入Koa框架

 const app = new Koa();

app.use( async ctx => {
	ctx.body = 'Hello Koa';
})

// 监听3000端口
 app.listen(3000,() => {
 	console.log('Server running at port 3000!');
 })
node app.js			// 启动服务

async/await的使用

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()

从上述例子可以看出 async/await 的特点:

  • 可以让异步逻辑用同步写法实现
  • 最底层的await返回需要是Promise对象
  • 可以通过多层 async function 的同步写法代替传统的callback嵌套

koa-router中间件

安装koa-router中间件

npm i koa-router --save

koa-router的使用

 const Koa = require('koa');	//引入Koa框架
 const Router = require('koa-router');	//引入koa-router 中间件

// 实例化 app
 const app = new Koa();


// 路由
const router = new Router();

router.get('/', async (ctx) => {
    ctx.body = 'Router 请求';
});

// 配置路由
app.use(router.routes()).use(router.allowedMethods());


// 监听3000端口
 app.listen(3000,() => {
 	console.log('Server running at port 3000!');
 })

请求数据获取

GTET请求数据获取

 const Koa = require('koa');	//引入Koa框架
 const Router = require('koa-router');	//引入koa-router 中间件

// 实例化 app
 const app = new Koa();


// 路由
const router = new Router();

router.get('/user', async (ctx) => {
    let url = ctx.url
    // 从上下文的request对象中获取
    let request = ctx.request
    let req_query = request.query
    let req_querystring = request.querystring

    // 从上下文中直接获取
    let ctx_query = ctx.query
    let ctx_querystring = ctx.querystring

    ctx.body = {
      url,
      req_query,
      req_querystring,
      ctx_query,
      ctx_querystring
    }
});

// 配置路由
app.use(router.routes()).use(router.allowedMethods());


// 监听3000端口
 app.listen(3000,() => {
 	console.log('Server running at port 3000!');
 })

image20200324142458926.png

POST请求数据获取

koa-bodyparser中间件

npm install koa-body --save
 const Koa = require('koa');	//引入Koa框架
 const Router = require('koa-router');	//引入koa-router 中间件

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

// 实例化 app
 const app = new Koa();

 // 使用ctx.body解析中间件
app.use(bodyParser());
/* 支持json,form,text类型
app.use(bodyparser({
  enableTypes:['json', 'form', 'text']
}))
*/
// 路由
const router = new Router();


router.post('/login', async ctx => {
	console.log(ctx.request.body);
})

// 配置路由
app.use(router.routes()).use(router.allowedMethods());


// 监听3000端口
 app.listen(3000,() => {
 	console.log('Server running at port 3000!');
 })

静态资源加载

koa-static中间件使用

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

const app = new Koa();

// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'

app.use(static(
  path.join( __dirname,  staticPath)
))


app.use( async ( ctx ) => {
  ctx.body = 'hello world'
})


app.listen(3000);

koa-static-router中间件, 实现多个&&多层路由加载静态资源

npm install koa-static-router
简单配置
app.use(static('public'))      //默认配置: {dir:public  route:'/public'}
单个路由
const static = require('koa-static-router');
 app.use(static({
     dir,  //静态资源目录对于相对入口文件index.js的路径
     router   //路由命名
 }))
多个路由

访问 localhost:3000/public/image/dir/1.png

访问 localhost:3000/static/image/dir/2.png

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


// 单个路由
// app.use(static({
//     dir:'public',
//     router:'/static/'     //路由长度 =1
// }))


//多个路由
app.use(static([
    {
    dir:'public',    //静态资源目录对于相对入口文件index.js的路径
    router:'/public/image/'   //路由命名   路由长度 =2
},{
    dir:'static',   //静态资源目录对于相对入口文件index.js的路径
    router:'/static/image/'    //路由命名  路由长度 =2
}
]))

app.use( async ( ctx ) => {
  ctx.body = 'hello world'
})

app.listen(3000, () => {
  console.log('build success')
})

cookie/session

koa2使用cookie

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

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

koa2 中操作的cookies是使用了npm的cookies模块,源码在github.com/pillarjs/co…

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')
})

image20200324145954133.png

koa2实现session

koa2原生功能只提供了cookie的操作,但是没有提供session操作。session就只用自己实现或者通过第三方中间件实现。在koa2中实现session的方案有一下几种

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

koa-session的使用

npm install koa-session --save

const Koa = require('koa')
const app = new Koa()
const Koa_Session = require('koa-session');      

// 这个是配合signed属性的签名key
const session_signed_key = ["some secret hurr"]; 
// 配置
const session_config = {
    key: 'koa:sess', /**  cookie的key。 (默认是 koa:sess) */
    maxAge: 10*60*1000,   /**  session 过期时间,以毫秒ms为单位计算 。*/
    autoCommit: true, /** 自动提交到响应头。(默认是 true) */
    overwrite: true, /** 是否允许重写 。(默认是 true) */
    httpOnly: true, /** 是否设置HttpOnly,如果在Cookie中设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能有效的防止XSS攻击。  (默认 true) */
    signed: true, /** 是否签名。(默认是 true) */
    rolling: true, /** 是否每次响应时刷新Session的有效期。(默认是 false) */
    renew: false, /** 是否在Session快过期时刷新Session的有效期。(默认是 false) */
};

// 实例化
const session = Koa_Session(session_config, app)
app.keys = session_signed_key;

// 使用中间件,注意有先后顺序
app.use(session);

app.use( async ( ctx ) => {

  let n = ctx.session.views || 0;
	ctx.session.views = ++n;
	ctx.session.name = 'chen';
	ctx.body = n + ' views';

})

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

image20200324152342592.png

使用ejs模板

npm i koa-ejs --save

/**
 * 项目入口文件
 */

const Koa = require('koa');
const app = new Koa();
const bodyParser = require('koa-bodyparser');
const Router = require('koa-router');
const router = new Router();
const render = require('koa-ejs');
const path = require('path');
app.use(bodyParser());

// 初始化ejs,设置后缀为html,文件目录为`views`
render(app, {
    root: path.join(__dirname, 'views'),
    layout: false,
    viewExt: 'html',
    cache: false,
    debug: false
});


// 渲染首页
router.get('/',async (ctx,next)=>{
    await ctx.render('index',{
        title: '我是首页',
        body: '我是内容啊'
    });
})

app.use(router.routes());
app.use(router.allowedMethods());
// 监听3000端口
app.listen(3000);

连接mysql数据库

安装Sequelize

 npm install mysql2 --save
 npm install sequelize --save

使用 Sequelize 初始化连接池

8520950bfdd4022f71bdd11.webp

创建数据表模型

image20200324154643856.png

数据表控制器

image20200324154913167.png

数据路由表

image20200324155028791.png

解决跨域问题

npm i koa2-cors --save
var koa = require('koa');
var app = new koa();
var router = require('koa-router')();
const cors = require('koa2-cors');
app.use(cors({
  origin: '*',	// 允许来自所有域名请求
   /* origin: function (ctx) {
        if (ctx.url === '/test') {
            return "*"; // 允许来自所有域名请求
        }
        return 'http://localhost:8080'; / 这样就能只允许 http://localhost:8080 这个域名的请求了
    },*/
  exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
  maxAge: 5,
  credentials: true,
  allowMethods: ['GET', 'POST', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}));
router.post('/', async function (ctx) {
    ctx.body = '恭喜 __小简__ 你成功登陆了'
});
 
app
    .use(router.routes())
    .use(router.allowedMethods());
 
app.listen(3000);

实现文件上传

使用koa-multer 中间件

npm install koa-multer --save
const multer = require('koa-multer');//加载koa-multer模块
// 上传 图片
var storage = multer.diskStorage({
        //文件保存路径
        destination: function(req, file, cb) {
            cb(null, 'public/uploads/')
        },
        //修改文件名称
        filename: function(req, file, cb) {
            var fileFormat = (file.originalname).split(".");
            cb(null, Date.now() + "." + fileFormat[fileFormat.length - 1]);
        }
    })
    //加载配置
var upload = multer({
    storage: storage
});
router.post('/upload', upload.single('file'), async(ctx, next) => {
    ctx.body = {
        filename: ctx.req.file.filename //返回文件名
    }
})

使用 koa-body 代替 koa-bodyparser 和 koa-multer

npm i koa-body -S
const koaBody = require('koa-body');
const Router = require('koa-router');
const app = new koa();
app.use(koaBody({
  multipart:true, // 支持文件上传
  encoding:'gzip',
  formidable:{
    uploadDir:path.join(__dirname,'public/upload/'), // 设置文件上传目录
    keepExtensions: true,    // 保持文件的后缀
    maxFieldsSize:2 * 1024 * 1024, // 文件上传大小
    onFileBegin:(name,file) => { // 文件上传前的设置
      // console.log(`name: ${name}`);
      // console.log(file);
    },
  }
}));

const router = new Router();
router.post('/',async (ctx)=>{
  console.log(ctx.request.files);	//上传的文件信息
  console.log(ctx.request.body);	//上传的所有信息
  ctx.body = JSON.stringify(ctx.request.files);
});

koa-body 的基本参数

参数名描述类型默认值
patchNode将请求体打到原生 node.js 的ctx.reqBooleanfalse
patchKoa将请求体打到 koa 的 ctx.requestBooleantrue
jsonLimitJSON 数据体的大小限制String / Integer1mb
formLimit限制表单请求体的大小String / Integer56kb
textLimit限制 text body 的大小String / Integer56kb
encoding表单的默认编码Stringutf-8
multipart是否支持 multipart-formdate 的表单Booleanfalse
urlencoded是否支持 urlencoded 的表单Booleantrue
text是否解析 text/plain 的表单Booleantrue
json是否解析 json 请求体Booleantrue
jsonStrict是否使用 json 严格模式,true 会只处理数组和对象Booleantrue
formidable配置更多的关于 multipart 的选项Object{}
onError错误处理Functionfunction(){}
stict严格模式,启用后不会解析 GET, HEAD, DELETE 请求Booleantrue

formidable 的相关配置参数

参数名描述类型默认值
maxFields限制字段的数量Integer1000
maxFieldsSize限制字段的最大大小Integer2 * 1024 * 1024
uploadDir文件上传的文件夹Stringos.tmpDir()
keepExtensions保留原来的文件后缀Booleanfalse
hash如果要计算文件的 hash,则可以选择 md5/sha1Stringfalse
multipart是否支持多文件上传Booleantrue
onFileBegin文件上传前的一些设置操作Functionfunction(name,file){}

图片上传成功后,获取图片

const fs = require('fs')
const app = new Koa()
const router = new Router()
const serve = require('koa-static')
const koaBody = require('koa-body')

app
    .use(serve(__dirname + '/files')) // files文件夹用于保存上传的文件,也是静态资源地址
    .use(router.routes())

// 前端使用formData方式组装数据
router.post('/api/upload-files', koaBody({ jsonLimit: '2mb', multipart: true }), async (ctx) => {
    const data = ctx.request.body.files.data;
    const savePath = path.join(`./files`, data.name)
    const reader = fs.createReadStream(data.path)
    const writer = fs.createWriteStream(savePath)

    const pro = new Promise( (resolve, reject) => {
        var stream = reader.pipe(writer);

        stream.on('finish', function () {
            resolve(`http://当前服务器地址${data.name}`);
        });
    })
    
    ctx.response.body =  await pro
    
})

koa-send实现文件下载

const send = require('koa-send');

router.get('/download/:name', async (ctx) => {
    const name = ctx.params.name;
    const path = `upload/${name}`;
    ctx.attachment(path);
    await send(ctx, path);
});

koa中间件记录

koa的错误处理 koa-onerror

npm install koa-onerror --save
var onerror = require('Koa-onerror');
onerror(app);

koa日志 koa-logger

app.use(require('koa-logger'));
// logger
app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
});

koa-json

美观的输出JSON response的Koa中间件 有两种使用方式: 一种是总是返回美化了的json数据:

const json = require('koa-json');
app.use(json());

另一种是默认不进行美化,但是当地址栏传入pretty参数的时候,则返回的结果是进行了美化的。

app.use(json({ pretty: false, param: 'pretty' }));

koa-jwt和jsonwebtoken进行token验证

koa-jwt 主要提供路有权限控制的功能,它会对需要限制的资源请求进行检查

token 默认被携带在Headers 中的名为Authorization的键值对中,koa-jwt也是在该位置获取token

app.use(jwt({ secret: 'shared-secret', key: 'jwtdata' })) 可以使用另外一ctx key来表示解码数据,然后就可以通过ctx.state.jwtdata代替ctx.state.user获得解码数据

  • secret 的值可以使用函数代替,以此产生动态的加密秘钥
  • koa-jwt 依赖于jsonwebtokenkoa-unless两个库的
app.use(jwt({
	secret:'chambers'
}).unless({path:[/^\/api/]}));

生成token

const router = require('koa-router')();
const jwt = require('jsonwebtoken');
const userModel = require('../models/userModel.js');

router.post('/login', async (ctx) => {
	const data = ctx.request.body;
	if(!data.name || !data.password){
		return ctx.body = {
			code: '000002',
			data: null,
			msg: '参数不合法'
		}
	}
	const result = await userModel.findOne({
		name: data.name,
		password: data.password
	})
	if(result !== null){
		const token = jwt.sign({
			name: result.name,
			_id: result._id
		}, 'my_token', { expiresIn: '2h' });
		return ctx.body = {
			code: '000001',
			data: token,
			msg: '登录成功'
		}
	}else{
		return ctx.body = {
			code: '000002',
			data: null,
			msg: '用户名或密码错误'
		}
	}
});

module.exports = router;

在验证了用户名密码正确之后,调用 jsonwebtoken 的 sign() 方法来生成token,接收三个参数,第一个是载荷,用于编码后存储在 token 中的数据,也是验证 token 后可以拿到的数据;第二个是密钥,自己定义的,验证的时候也是要相同的密钥才能解码;第三个是options,可以设置 token 的过期时间。

jwt.decode(token); 可以解析出sign()中第一个参数。

获取token

前端请求头在Authorization中携带token,用于后台验证

每次请求都要获取 localStorage 中的 token,这样很麻烦,这里使用了 axios 的请求拦截器,对每次请求都进行了取 token 放到 headers 中的操作。

axios.interceptors.request.use(config => {
    const token = localStorage.getItem('token');
    config.headers.common['Authorization'] = 'Bearer ' + token;
    return config;
})

验证token

通过 koa-jwt 中间件来进行验证

const koa = require('koa');
const koajwt = require('koa-jwt');
const app = new koa();

// 错误处理
app.use((ctx, next) => {
    return next().catch((err) => {
        if(err.status === 401){
            ctx.status = 401;
      		ctx.body = 'Protected resource, use Authorization header to get access\n';
        }else{
            throw err;
        }
    })
})

app.use(koajwt({
	secret: 'my_token'
}).unless({
	path: [/\/user\/login/]
}));

通过 app.use 来调用该中间件,并传入密钥 {secret: 'my_token'},unless 可以指定哪些 URL 不需要进行 token 验证。token 验证失败的时候会抛出401错误,因此需要添加错误处理,而且要放在 app.use(koajwt()) 之前,否则不执行。

如果请求时没有token或者token过期,则会返回401。

解析koa-jwt

上面使用 jsonwebtoken 的 sign() 方法来生成 token 的,那么 koa-jwt 做了些什么帮我们来验证 token。

resolvers/auth-header.js

module.exports = function resolveAuthorizationHeader(ctx, opts) {
    if (!ctx.header || !ctx.header.authorization) {
        return;
    }
    const parts = ctx.header.authorization.split(' ');
    if (parts.length === 2) {
        const scheme = parts[0];
        const credentials = parts[1];
        if (/^Bearer$/i.test(scheme)) {
            return credentials;
        }
    }
    if (!opts.passthrough) {
        ctx.throw(401, 'Bad Authorization header format. Format is "Authorization: Bearer <token>"');
    }
};

在 auth-header.js 中,判断请求头中是否带了 authorization,如果有,将 token 从 authorization 中分离出来。如果没有 authorization,则代表了客户端没有传 token 到服务器,这时候就抛出 401 错误状态。

verify.js

const jwt = require('jsonwebtoken');

module.exports = (...args) => {
    return new Promise((resolve, reject) => {
        jwt.verify(...args, (error, decoded) => {
            error ? reject(error) : resolve(decoded);
        });
    });
};

在 verify.js 中,使用 jsonwebtoken 提供的 verify() 方法进行验证返回结果。jsonwebtoken 的 sign() 方法来生成 token 的,而 verify() 方法则是用来认证和解析 token。如果 token 无效,则会在此方法被验证出来。

index.js

const decodedToken = await verify(token, secret, opts);
if (isRevoked) {
	const tokenRevoked = await isRevoked(ctx, decodedToken, token);
	if (tokenRevoked) {
		throw new Error('Token revoked');
	}
}
ctx.state[key] = decodedToken;  // 这里的key = 'user'
if (tokenKey) {
	ctx.state[tokenKey] = token;
}

在 index.js 中,调用 verify.js 的方法进行验证并解析 token,拿到上面进行 sign() 的数据 {name: result.name, _id: result._id},并赋值给 ctx.state.user,在控制器中便可以直接通过 ctx.state.user 拿到 name_id

安全性

  • 如果 JWT 的加密密钥泄露的话,那么就可以通过密钥生成 token,随意的请求 API 了。因此密钥绝对不能存在前端代码中,不然很容易就能被找到。
  • 在 HTTP 请求中,token 放在 header 中,中间者很容易可以通过抓包工具抓取到 header 里的数据。而 HTTPS 即使能被抓包,但是它是加密传输的,所以也拿不到 token,就会相对安全了。

bcryptjs密码加密

npm install bcryptjs --save

同步用法(Sync)

生成hash密码

const bcrypt = require('bcryptjs');
const salt = bcrypt.genSaltSync(10);
// hash 加密后的密码       "B4c0/\/"加密前的密码   
var hash = bcrypt.hashSync("B4c0/\/", salt);

密码验证

bcrypt.compareSync("B4c0/\/", hash); // true 
bcrypt.compareSync("not_bacon", hash); // false

快速生成hash值

var hash = bcrypt.hashSync('bacon', 8);

异步用法(Async)

生成hash密码

var bcrypt = require('bcryptjs');
bcrypt.genSalt(10, function(err, salt) {
    bcrypt.hash("B4c0/\/", salt, function(err, hash) {
        // Store hash in your password DB. 
    });
});

密码验证

// Load hash from your password DB. 
bcrypt.compare("B4c0/\/", hash, function(err, res) {
    // res === true 
});
bcrypt.compare("not_bacon", hash, function(err, res) {
    // res === false 
});
 
// As of bcryptjs 2.4.0, compare returns a promise if callback is omitted: 
bcrypt.compare("B4c0/\/", hash).then((res) => {
    // res === true 
});

快速生成hash值

bcrypt.hash('bacon', 8, function(err, hash) {
});