一、起步
Koa2和Koa1的主要区别在于,Koa2全面采用async/await语法,所以Node.js应该支持async/await语法才能支持Koa2。故环境要求为Node.js版本>=v7.6.0
1、安装
$ npm install koa -S
2、示例
使用Koa2编写Hello,World的代码如下:
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx) => {
ctx.body = 'Hello, World'
})
app.listen(3000)
3、koa2结构
koa2的源码中,其结构如下:
+-- lib
| |-- application.js
| |-- context.js
| |-- request.js
| |-- response.js
+-- package.json
其中:
application.js
是Koa2的入口,封装了context、request、response以及中间件处理流程context.js
处理应用的上下文,里面封装了部分request.js和response.js的方法request.js
处理HTTP请求response.js
处理HTTP响应
4、Koa2特性
- 只提供封装好的HTTP上下文、请求、响应,以及基于
async/await
的中间件容器 - 利用ES7的
async/await
代替koa1的generator/yield
- 中间件只支持
async/await
封装的,若要使用koa1的generator中间件,那么应该先用koa-convert
进行转化
5、如何开发和使用中间件
koa1的中间件,返回的应该是generator函数,编写方式如下:
function log(ctx) {
console.log(ctx.method, ctx.header.host + ctx.url)
}
module.exports = function() {
return function* (next) {
// 中间件处理流程开始
log(this)
// 中间件处理流程结束
if (next) {
yield next
}
}
}
在koa1中的使用方法:
const koa = require('koa')
const loggerGenerator = require('./logger-generator')
const app = koa()
app.use(loggerGenerator())
app.use(function* () {
this.body = 'Hello, world'
})
app.listen(3000)
而在koa2中,需要使用的是async/await
中间件,即中间件的返回值应该是个Promise,如果koa2要使用generator中间件,那么可以通过koa-convert
模块先进行转化,如下:
const convert = require('koa-convert')
app.use(convert(loggerGenerator()))
async/await
中间件的编写方式如下:
function log(ctx) {
console.log(ctx.method, ctx.header.host + ctx.url)
}
module.exports = function() {
return async function(ctx, next) {
// 中间件处理流程开始
log(ctx)
// 中间件处理流程结束
await next()
}
}
而koa2终使用async/await
中间件的方式如下:
const Koa = require('koa')
const loggerAsync = require('./loggerAsync')
const app = new Koa()
app.use(loggerAsync)
aoo.use(ctx => {
ctx.body = 'Hello, world'
})
app.listen(3000)
二、路由
1、原生实现
路由的作用是为不同的请求信息,进行处理,然后响应相应的请求结果。路由的实现,主要是利用ctx.request.url
这一信息,我们可以简单手动实现一个路由,如下:
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()
/**
* Promise封装异步读取文件方法
*/
function render(page) {
return new Promise((resolve, reject) => {
let viewUrl = `./view/${page}`
fs.readFile(viewUrl, 'binary', (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
/**
* 路由处理:根据URL获取HTML内容
*/
async function route(url) {
let view = '404.html'
switch(url) {
case '/':
view = 'index.html'
break
case '/todo':
view = 'todo.html'
break
default:
break
}
const html = await render(view)
return html
}
app.use(async (ctx) => {
const url = ctx.request.url
const html = await route(url)
ctx.body = html
})
app.listen(3000)
2、koa-router中间件
手动处理路由总是比较麻烦的,所以为了简化这一处理流程,我们可以引入koa-router
中间件模块,其用法示例如下(注意:koa2对应的koa-router版本为7.x):
const Koa = require('koa')
const Router = require('koa-router')
const fs = require('fs')
const app = new Koa()
const home = new Router()
home.get('/', async (ctx) => {
ctx.body = 'Home'
})
const page = new Router()
page.get('/hello/:string', async (ctx) => {
ctx.body = `Hello, ${ctx.params.string}`
})
const router = new Router()
router.use('/', home.routes(), home.allowedMethods())
router.use('/page', page.routes(), hone.allowedMethods())
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)
关于koa-router
的详细文档,可参见https://www.npmjs.com/package/koa-router
三、请求数据获取
1、获取GET数据
GET请求的数据,可以有下列的获取方式:
- 通过
ctx
上下文对象直接获取- 获取请求对象
ctx.query
,得到如:{ a: 1, b: 2 }
- 获取请求字符串
ctx.querystring
,得到如:a=1&b=2
- 获取请求对象
- 通过
ctx.request
对象获取- 获取请求对象
ctx.request.query
- 获取请求字符串
ctx.request.querystring
- 获取请求对象
2、获取POST数据
POST的数据是放置于HTTP请求报文主体中的,Koa本身没有封装获得POST数据的方法,所以我们可以通过解析报文主体自行获取。需要注意的是:ctx.request
是context
经过封装的请求对象,而ctx.req
是context
提供的Node.js原生HTTP请求对象,response和res则同理。处理方式如下:
function parseQueryStr(queryStr) {
let res = {}
let list = queryStr.split('&')
for (let part of list) {
let [key, val] = part.split('=')
res[key] = decodeURIComponent(val)
}
return res
}
function parsePostData(ctx) {
return new Promise((resolve, reject) => {
try {
let postData = ''
ctx.req.addListener('data', data => {
postData += data
})
ctx.req.addListener('end', () => {
let parsedData = parseQueryStr(postData)
resolve(parseData)
})
} catch (err) {
reject(err)
}
})
}
3、koa-bodyparser中间件
为了省去处理POST请求参数的过程,我们可以引入koa-bodyparser
中间件,koa-bodyparser
可以把koa2上下文中的formData
数据解析到ctx.request.body
中,使用例子:
const Koa = require('koa')
const app = new Koa()
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
app.use(async (ctx) => {
if (ctx.url === '/' && ctx.method === 'POST') {
let postData = ctx.request.body
ctx.body = `Your name is : ${postData.name}`
}
})
四、静态资源加载
1、原生实现
静态资源的处理,通常有以下的响应结果:
- 访问文本和文件,如:js、css、png、jpg、gif、zip
- 访问静态目录
- 找不到资源,抛出404错误
以下是原生实现一个简易静态资源服务器的例子,如:
mime.js
const 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
util.js
const fs = require('fs')
const path = require('path')
const mimes = require('./mimes')
/**
* 文件、目录遍历
*/
function walk(reqPath) {
let files = fs.readdirSync(reqPath)
let dirList = []
let fileList = []
for (let i=0, len=files.length; i<len; ++i) {
let file = files[i]
let fileParts = file.split('.')
let fileMime = (fileParts.length > 1)
? fileParts[fileParts.length - 1]
: 'undefined'
if (typeof mimes[fileMime] === 'undefined') {
dirList.push(file)
} else {
fileList.push(file)
}
}
let result = [...dirList, ...fileList]
return result
}
/**
* 展示目录内容
*/
function showDir(url, path) {
const files = walk(path)
let html = '<ul>'
for (let file of files) {
html += `<li><a href="${url === '/' ? '' : url}/${file}">${file}</a></li>`
}
html += '</ul>'
return html
}
/**
* 获取静态资源内容
*/
function getContent(ctx, fullStaticPath) {
const reqPath = path.join(fullStaticPath, ctx.url)
const exists = fs.existsSync(reqPath)
let content
if (!exists) {
content = '404 Not Found'
} else {
const stat = fs.statSync(reqPath)
if (stat.isDirectory()) {
content = showDir(ctx.url, reqPath)
} else {
content = fs.readFileSync(reqPath, 'binary')
}
}
return content
}
module.exports = {
walk,
showDir,
getContent
}
index.js
const Koa = require('koa')
const path = require('path')
const mimes = require('./mimes')
const { walk, showDir, getContent } = require('./util')
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) => {
const fullStaticPath = path.join(__dirname, staticPath)
const mime = parseMime(ctx.url)
const content = getContent(ctx, fullStaticPath)
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)
2、koa-static中间件
我们可以使用koa-static
中间件来处理静态资源的加载问题,例子如下:
const Koa = require('koa')
const path = require('path')
const staticMiddleware = require('koa-static')
const app = new Koa()
const staticPath = './static'
app.use(staticPath(
path.join(__dirname, staticPath)
))
app.use(async (ctx) => {
ctx.body = 'Hello world'
})
app.listen(3000, () => {
console.log('[demo] static server is starting at port 3000')
})
五、cookie/session
1、使用cookie
koa本身提供了从上下文中直接读取和写入cookie的方法,如下:
ctx.cookies.get(name, [options])
读取上下文请求中的cookiectx.cookies.set(name, value, [options])
在上下文中写入cookie
koa对cookie的处理使用的是[cookies](https://github.com/pillarjs/cookies)
模块,因此读写cookie的使用参数与该模块的使用一致。例子如下:
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx) => {
if (ctx.url === '/get') {
const str = `读取cookie:${ctx.cookies.get('username')}`
ctx.body = str
} else {
ctx.cookies.set('username', 'RuphiLau', {
domain: '127.0.0.1',
path: '/',
maxAge: 3600,
expires: new Date(new Date() + 3600),
httpOnly: true
})
ctx.body = '设置COOKIE'
}
})
app.listen(3000)
2、使用session
koa原生只提供了cookie的操作,但是没有提供session的操作。所以session需要自己实现,实现方案有:
- 如果session数据量很小,可以直接存在内存中
- 如果session数据量很大,则需要存储介质放session数据
其中,数据库存储方案如下:
- 数据库可以选用Mysql
- 使用中间件处理:
koa-session-minimal
提供存储介质的读写接口koa-mysql-session
为koa-session-minimal
中间件提供Mysql数据库的session数据读写操作- 将sessionId与对应的数据存到数据库
- sessionId保存在cookie中,通过cookie获得sessionId,从而获得session信息
编写后的例子如下:
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: 'user',
password: 'pwd',
database: 'dbname',
host: '127.0.0.1'
})
let cookie = {
maxAge,
expires,
path,
domain,
httpOnly,
overwrite,
secure,
sameSite,
signed
}
app.use(session({
key: 'SESSION_ID',
store,
cookie
}))
app.use(async (ctx) => {
// 设置session的方法
ctx.session = {
valA: 'a',
valB: 'b'
}
// 读取session的方法
ctx.session.valA
})
六、使用模板引擎
在Koa中使用模板引擎,可以引入koa-views
和相应的模板引擎,如[ejs](https://github.com/mde/ejs)
,示例如下:
模板文件:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1><%= title %></h1>
</body>
</html>
代码:
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()
app.use(views(path.join(__dirname, './view'), {
extension: 'ejs'
}))
app.use(async (ctx) => {
const title = 'Hello, world'
await ctx.render('index', {
title
})
})
app.listen(3000)
七、文件上传
busboy模块的作用是解析HTML中的formData,用法如下:
const path = require('path')
const fs = require('fs')
const Busboy = require('busboy')
// ...
// req为Node原生请求对象
const busboy = new Busboy({ headers: req.headers })
// 处理文件上传
busboy.on('file', (fieldName, file, fileName, encoding, mimetype) => {
// fieldName为表单中file控件的name
// file为文件对象
// fileName为文件名
console.log(`File [${fieldName}]: fileName: ${fileName}`)
// 保存文件到特定路径
file.pipe(fs.createWriteStream('./upload'))
// 解析文件流
file.on('data', (data) => {
console.log(`File [${fieldName}] got ${data.length} bytes`)
})
// 解析文件结束
file.on('end', () => {
console.log(`File [${fieldName}] finished`)
})
})
// 处理表单中的非文件字段
busboy.on('field', (fieldName, value, fieldNameTruncated, valTruncated) => {
console.log(`Filed [${fieldName}]: value: ${value}`)
})
// 监听结束处理
busboy.on('finish', () => {
console.log('Form has been parsed over')
res.writeHead(303, {
Connection: 'close',
Location: '/'
})
res.end()
})
req.pipe(busboy)
// ...