koa2特性
只提供封装好http上下文、请求、响应,以及基于async/await的中间件容器。
利用ES7的async/await的来处理传统回调嵌套问题和代替koa@1的generator,但是需要在node.js 7.x的harmony模式下才能支持async/await。
中间件只支持 async/await 封装的,如果要使用koa@1基于generator中间件,需要通过中间件koa-convert封装一下才能使用。
generator中间件在koa@1中的使用
generator 中间件在koa v1中可以直接use使用
1 2 3 4 5 6 7 8 9 10 11 12 const koa = require ('koa' ) const loggerGenerator = require ('./middleware/logger-generator' )const app = koa ()app.use (loggerGenerator ()) app.use (function *( ) { this.body = 'hello world!' }) app.listen (3000 ) console.log ('the server is starting at port 3000' )
generator中间件在koa@2中的使用
generator 中间件在koa v2中需要用koa-convert封装一下才能使用
1 2 3 4 5 6 7 8 9 10 11 12 13 const Koa = require ('koa' ) const convert = require ('koa-convert' )const loggerGenerator = require ('./middleware/logger-generator' )const app = new Koa ()app.use (convert (loggerGenerator ())) app.use (( ctx ) => { ctx.body = 'hello world!' }) app.listen (3000 ) console .log ('the server is starting at port 3000' )
原生方法解析出POST请求上下文中的表单数据
原理:对于POST请求的处理,koa2没有封装获取参数的方法,需要通过解析上下文context中的原生node.js请求对象req,将POST表单数据解析成query string(例如:a=1&b=2&c=3),再将query string 解析成JSON格式(例如:{“a”:”1”, “b”:”2”, “c”:”3”})
注意:ctx.request是context经过封装的请求对象,ctx.req是context提供的node.js原生HTTP请求对象,同理ctx.response是context经过封装的响应对象,ctx.res是context提供的node.js原生HTTP请求对象。
具体koa2 API文档可见 https://github.com/koajs/koa/blob/master/docs/api/context.md#ctxreq
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 function parsePostData ( ctx ) { return new Promise ((resolve, reject ) => { try { let postdata = "" ; ctx.req .addListener ('data' , (data ) => { postdata += data }) ctx.req .addListener ("end" ,function ( ){ let parseData = parseQueryStr ( postdata ) resolve ( parseData ) }) } catch ( err ) { reject (err) } }) } function parseQueryStr ( queryStr ) { let queryData = {} let queryStrList = queryStr.split ('&' ) console .log ( queryStrList ) for ( let [ index, queryStr ] of queryStrList.entries () ) { let itemList = queryStr.split ('=' ) queryData[ itemList[0 ] ] = decodeURIComponent (itemList[1 ]) } return queryData }
栗子🌰
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 const Koa = require ('koa' ) const app = new Koa() app.use( async ( ctx ) => { if ( ctx.url === '/' && ctx.method === 'GET' ) { // 当GET请求时候返回表单页面 let html = ` <h1>koa2 request post demo</h1> <form method ="POST" action ="/" > <p > userName</p > <input name ="userName" /> <br /> <p > nickName</p > <input name ="nickName" /> <br /> <p > email</p > <input name ="email" /> <br /> <button type ="submit" > submit</button > </form > ` ctx.body = html } else if ( ctx.url === '/' && ctx.method === 'POST' ) { // 当POST请求的时候,解析POST表单里的数据,并显示出来 let postData = await parsePostData( ctx ) ctx.body = postData } else { // 其他请求显示404 ctx.body = '<h1>404!!! o(╯□╰)o</h1>' } }) // 解析上下文里node原生请求的POST参数function parsePostData( ctx ) { return new Promise ((resolve, reject) => { try { let postdata = "" ; ctx.req.addListener('data' , (data) => { postdata += data }) ctx.req.addListener("end" ,function(){ let parseData = parseQueryStr( postdata ) resolve( parseData ) }) } catch ( err ) { reject(err) } }) } // 将POST请求参数字符串解析成JSON function parseQueryStr( queryStr ) { let queryData = {} let queryStrList = queryStr.split('&' ) console.log( queryStrList ) for ( let [ index, queryStr ] of queryStrList.entries() ) { let itemList = queryStr.split('=' ) queryData[ itemList[0 ] ] = decodeURIComponent (itemList[1 ]) } return queryData } app.listen(3000 , () => { console.log('[demo] request post is starting at port 3000' ) })
原生koa2实现静态资源服务器
代码目录: ├── static # 静态资源目录 │ ├── css/ │ ├── image/ │ ├── js/ │ └── index.html ├── util # 工具代码 │ ├── content.js # 读取请求内容 │ ├── dir.js # 读取目录内容 │ ├── file.js # 读取文件内容 │ ├── mimes.js # 文件类型列表 │ └── walk.js # 遍历目录内容 └── index.js # 启动入口文件
index
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 const Koa = require ('koa' )const path = require ('path' )const content = require ('./util/content' )const mimes = require ('./util/mimes' )const app = new Koa ()const staticPath = './static' function parseMime ( url ) { let extName = path.extname ( url ) extName = extName ? extName.slice (1 ) : 'unknown' return mimes[ extName ] } app.use ( async ( ctx ) => { let fullStaticPath = path.join (__dirname, staticPath) let _content = await content ( ctx, fullStaticPath ) let _mime = parseMime ( ctx.url ) if ( _mime ) { ctx.type = _mime } if ( _mime && _mime.indexOf ('image/' ) >= 0 ) { ctx.res .writeHead (200 ) ctx.res .write (_content, 'binary' ) ctx.res .end () } else { ctx.body = _content } }) app.listen (3000 ) console .log ('[demo] static-server is starting at port 3000' )
util/content.js
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 const path = require ('path' )const fs = require ('fs' )const dir = require ('./dir' )const file = require ('./file' )async function content ( ctx, fullStaticPath ) { let reqPath = path.join (fullStaticPath, ctx.url ) let exist = fs.existsSync ( reqPath ) let content = '' if ( !exist ) { content = '404 Not Found! o(╯□╰)o!' } else { let stat = fs.statSync ( reqPath ) if ( stat.isDirectory () ) { content = dir ( ctx.url , reqPath ) } else { content = await file ( reqPath ) } } return content } module .exports = content
util/dir.js
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 url = require ('url' )const fs = require ('fs' )const path = require ('path' )const walk = require ('./walk' )function dir ( url, reqPath ) { let contentList = walk ( reqPath ) let html = `<ul>` for ( let [ index, item ] of contentList.entries () ) { html = `${html} <li><a href="${url === '/' ? '' : url} /${item} ">${item} </a>` } html = `${html} </ul>` return html } module .exports = dir
util/file.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const fs = require ('fs' )function file ( filePath ) { let content = fs.readFileSync(filePath, 'binary' ) return content } module.exports = file
util/walk.js
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 const fs = require ('fs' )const mimes = require ('./mimes' )function walk ( reqPath ){ let files = fs.readdirSync ( reqPath ); let dirList = [], fileList = []; for ( let i=0 , len=files.length ; i<len; i++ ) { let item = files[i]; let itemArr = item.split ("\." ); let itemMime = ( itemArr.length > 1 ) ? itemArr[ itemArr.length - 1 ] : "undefined" ; if ( typeof mimes[ itemMime ] === "undefined" ) { dirList.push ( files[i] ); } else { fileList.push ( files[i] ); } } let result = dirList.concat ( fileList ); return result; }; module .exports = walk;
util/mime.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let mimes = { 'css' : 'text /css', 'less' : 'text /css', 'gif' : 'image /gif', 'html' : 'text /html', 'ico' : 'image /x-icon', 'jpeg' : 'image /jpeg', 'jpg' : 'image /jpeg', 'js' : 'text /javascript', 'json' : 'application /json', 'pdf' : 'application /pdf', 'png' : 'image /png', 'svg' : 'image /svg+xml', 'swf' : 'application /x-shockwave-flash', 'tiff' : 'image /tiff', 'txt' : 'text /plain', 'wav' : 'audio /x-wav', 'wma' : 'audio /x-ms-wma', 'wmv' : 'video /x-ms-wmv', 'xml' : 'text /xml' } module.exports = mimes
koa2使用cookie koa提供了从上下文直接读取、写入cookie的方法
ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
ctx.cookies.set(name, value, [options]) 在上下文中写入cookie
koa2 中操作的cookies是使用了npm的cookies模块,源码在https://github.com/pillarjs/cookies ,所以在读写cookie的使用参数与该模块的使用一致。
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 Koa = require ('koa' )const app = new Koa ()app.use ( async ( ctx ) => { if ( ctx.url === '/index' ) { ctx.cookies .set ( 'cid' , 'hello world' , { domain : 'localhost' , path : '/index' , maxAge : 10 * 60 * 1000 , expires : new Date ('2017-02-15' ), httpOnly : false , overwrite : false } ) ctx.body = 'cookie is ok' } else { ctx.body = 'hello world' } }) app.listen (3000 , () => { console .log ('[demo] cookie is starting at port 3000' ) })
koa2实现session koa2原生功能只提供了cookie的操作,但是没有提供session操作。session就只用自己实现或者通过第三方中间件实现。在koa2中实现session的方案有一下几种
如果session数据量很小,可以直接存在内存中
如果session数据量很大,则需要存储介质存放session数据
数据库存储方案
将session存放在MySQL数据库中
需要用到中间件 1 koa-session-minimal 适用于koa2 的session中间件,提供存储介质的读写接口 。 2 koa-mysql-session 为koa-session-minimal中间件提供MySQL数据库的session数据读写操作。 3 将sessionId和对于的数据存到数据库
将数据库的存储的sessionId存到页面的cookie中
根据cookie的sessionId去获取对于的session信息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 const Koa = require ('koa' )const session = require ('koa-session-minimal' )const MysqlSession = require ('koa-mysql-session' )const app = new Koa ()let store = new MysqlSession ({ user : 'root' , password : 'abc123' , database : 'koa_demo' , host : '127.0.0.1' , }) let cookie = { maxAge : '' , expires : '' , path : '' , domain : '' , httpOnly : '' , overwrite : '' , secure : '' , sameSite : '' , signed : '' , } app.use (session ({ key : 'SESSION_ID' , store : store, cookie : cookie })) app.use ( async ( ctx ) => { if ( ctx.url === '/set' ) { ctx.session = { user_id : Math .random ().toString (36 ).substr (2 ), count : 0 } ctx.body = ctx.session } else if ( ctx.url === '/' ) { ctx.session .count = ctx.session .count + 1 ctx.body = ctx.session } }) app.listen (3000 ) console .log ('[demo] session is starting at port 3000' )
单元测试
测试是一个项目周期里必不可少的环节,开发者在开发过程中也是无时无刻进行“人工测试”,如果每次修改一点代码,都要牵一发动全身都要手动测试关联接口,这样子是禁锢了生产力。为了解放大部分测试生产力,相关的测试框架应运而生,比较出名的有mocha,karma,jasmine等。虽然框架繁多,但是使用起来都是大同小异。
安装测试相关框架 1 npm install --save -dev mocha chai supertest
supertest 模块是http请求测试库,用来请求API接口
mocha 模块是测试框架
chai 模块是用来进行测试结果断言库,比如一个判断 1 + 1 是否等于 2
例子目录
1 2 3 4 5 . ├── index.js ├── package.json └── test └── index.test.js
所需测试demo
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 const Koa = require('koa' )const app = new Koa()const server = async ( ctx, next ) => { let result = { success: true , data: null } if ( ctx.method === 'GET' ) { if ( ctx.url === '/getString.json' ) { result.data = 'this is string data' } else if ( ctx.url === '/getNumber.json' ) { result.data = 123456 } else { result.success = false } ctx.body = result next && next () } else if ( ctx.method === 'POST' ) { if ( ctx.url === '/postData.json' ) { result.data = 'ok' } else { result.success = false } ctx.body = result next && next () } else { ctx.body = 'hello world' next && next () } } app.use(server ) module.exports = app app.listen(3000 , () => { console.log('[demo] test-unit is starting at port 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 const supertest = require ('supertest' ) const chai = require ('chai' ) const app = require ('./../index' ) const expect = chai.expect const request = supertest ( app.listen () ) describe ( '开始测试demo的GET请求' , ( ) => { it ('测试/getString.json请求' , ( done ) => { request .get ('/getString.json' ) .expect (200 ) .end (( err, res ) => { expect (res.body).to .be .an ('object' ) expect (res.body .success).to .be .an ('boolean' ) expect (res.body .data).to .be .an ('string' ) done () }) }) })
执行测试用例
1 2 3 4 5 ./node_modules/ .bin/mocha --harmony ./node_modules/ .bin/mocha
注意: 1.如果是全局安装了mocha,可以直接在当前项目目录下执行 mocha –harmony 命令 2.如果当前node.js版本低于7.6,由于7.5.x以下还直接不支持async/awiar就需要加上–harmony