前言:站在巨人的肩膀上,感谢前辈们的付出与贡献
安装 koa 模块
koa 需要 node v7.6.0 及以上版本,提供 ES6 和 async 函数支持
$ npm install koa
新建 hello.js
const Koa = require('koa');
const app = new Koa();
// response
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
中间件
普通函数
app.use((ctx, next) => {
const start = Date.now();
return next().then(() => {
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
});
async 函数(node v7.6.0+)
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
中间件开发
/* ./middleware/logger-async.js */
function log( ctx ) {
console.log( ctx.method, ctx.header.host + ctx.url )
}
module.exports = function () {
return async function ( ctx, next ) {
log(ctx);
await next()
}
}
/* index.js */
const Koa = require('koa')
const loggerAsync = require('./middleware/logger-async')
var app = new Koa()
app.use(loggerAsync())
app.use((ctx) => {
ctx.body = 'hello world'
})
app.listen(3000, 'localhost', () => {
console.log('starting on port: ', 3000)
})
//控制台
PS D:\workspace\koa2demo> node .\index.js
starting on port: 3000
GET localhost:3000/
理解 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()
koa2特性
- 利用ES7的async/await的来处理传统回调嵌套问题和代替koa@1的generator
- 中间件只支持 async/await 封装,如果要使用koa@1基于generator中间件,需要通过中间件koa-convert封装一下才能使用
路由中间件 koa-router
npm install koa-router --save
/* index.js */
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()
const Router = require('koa-router')
//子路由1
let home = new Router()
home.get('/', async (ctx) => {
let html = `
<ul>
<li><a href="/page/helloworld">/page/helloworld</a></li>
<li><a href="/page/404">/page/404</a></li>
</ul>
`
ctx.body = html
})
//子路由2
let page = new Router()
page
.get('/404', async (ctx) => {
ctx.body = '404 page'
})
.get('/helloworld', async (ctx) => {
ctx.body = 'helloworld page'
})
//装载所有子路由
let router = new Router()
router.use('/', home.routes(), home.allowedMethods())
router.use('/page', page.routes(), page.allowedMethods())
//加载路由中间件
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000, () => {
console.log('[demo] koa-router is starting on port: 3000')
})
//console
PS D:\workspace\koa2demo> node .\index.js
[demo] koa-router is starting on port: 3000
请求获取数据
GET请求
const Koa = require('koa')
const app = new Koa()
app.use(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.listen(3000, () => {
console.log('[demo] get request is starting on port: 3000')
})
POST请求获取数据
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx) => {
if (ctx.url === '/' && ctx.method === 'GET') {
//get请求时返回表单
let html = `
<h2>koa@2 post request</h2>
<form action="/" method="POST">
<p>userName</p>
<input name="username" type="text"><br>
<p>userPwd</p>
<input name="userPwd" type="password"><br>
<button type="submit">submit</button>
</form>
`
ctx.body = html
} else if (ctx.url === '/' && ctx.method === 'POST') {
//post请求时,解析表单里数据,并显示
let postData = await parsePostData(ctx)
ctx.body = postData
} else {
//其他请求显示404
ctx.body = '<h1>404 page</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', () => {
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] post request is starting on port: 3000')
})
POST表单请求 | 请求响应结果 |
---|---|
koa-bodyparser 中间件
const Koa = require('koa')
const app = new Koa()
const bodyParser = require('koa-bodyparser')
//使用ctx.body解析中间件
app.use(bodyParser())
app.use(async (ctx) => {
if (ctx.url === '/' && ctx.method === 'GET') {
//get请求时返回表单
let html = `
<h2>koa@2 post request</h2>
<form action="/" method="POST">
<p>userName</p>
<input name="username" type="text"><br>
<p>userPwd</p>
<input name="userPwd" type="password"><br>
<button type="submit">submit</button>
</form>
`
ctx.body = html
} else if (ctx.url === '/' && ctx.method === 'POST') {
//post请求时,解析表单里数据,并显示
let postData = ctx.request.body
ctx.body = postData
} else {
//其他请求显示404
ctx.body = '<h1>404 page</h1>'
}
})
app.listen(3000, () => {
console.log('[demo] koa-bodyparser is starting on port: 3000')
})
静态资源加载
koa-static中间件
const Koa = require('koa')
const path = require('path')
const static = require('koa-static')
const app = new Koa()
//静态资源相对路径
const staticPath = './public'
app.use(static(path.join(__dirname, staticPath)))
app.use(async (ctx) => {
ctx.body = 'hello koa@2'
})
app.listen(3000, () => {
console.log('[demo] koa-static middleware is starting on port: 3000')
})
koa2使用cookie
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: 20 * 60 * 1000,//cookie有效时长
expires: new Date('2018-10-24'),//cookie失效时间
httpOnly: false,//是否只用于http请求中获取
overwrite: false//是否允许重写
})
ctx.body = 'cookie is ok'
} else {
ctx.body = 'hello koa@2'
}
})
app.use(async (ctx) => {
ctx.body = 'hello koa@2'
})
app.listen(3000, () => {
console.log('[demo] cookie is starting on port: 3000')
})
koa2实现session
存放mysql中
//创建mysql数据库名为koademo
CREATE DATABASE IF NOT EXISTS koademo DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
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: 'root',
database: 'koademo',
host: '127.0.0.1'
})
//存放sessionId的cookie配置
let cookie = {
maxAge: '',
expires: '',
path: '',
domain: '',
httpOnly: '',
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 on port: 3000')
})
加载模板引擎
koa-views中间件
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()
//加载模板引擎
app.use(views(path.join(__dirname, './views'), {
extension: 'ejs'
}))
app.use(async (ctx) => {
let title = 'hello koa@2'
await ctx.render('index', {
title
})
})
app.listen(3000, () => {
console.log('[demo] koa-views ejs is starting on port: 3000')
})
文件上传
busboy模块
busboy模块是用来解析post请求,node原生req中的文件流
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', (data) => {
console.log(`File [${fieldname}] got ${data.length} bytes`)
})
//解析文件结束
file.on('end', () => {
console.log(`File [${fieldname}] finished`)
})
})
//监听请求中的字段
busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated) => {
console.log(`Field [${fieldname}]: value: ${inspect(val)}`)
})
//监听结束事件
busboy.on('finish', () => {
console.log('Done parsing form!')
res.writeHead(303, { Connection: 'close', Location: '/' })
res.end()
})
req.pipe(busboy)
上传文件简单实现
封装上传文件到写入服务方法
const inspect = require('util').inspect
const path = require('path')
const os = require('os')
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('文件上传成功!')
resolve(result)
})
}) // 解析表单中其他字段信息
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 }
入口文件
const Koa = require('koa')
const path = require('path')
const app = new Koa()
// const bodyParser = require('koa-bodyparser')
const { uploadFile } = require('./util/upload')
// app.use(bodyParser())
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', // common or 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')
})
异步上传图片
入口文件
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const convert = require('koa-convert')
const static = require('koa-static')
const { uploadFile } = require('./util/upload')
const app = new Koa()
/**
* 使用第三方中间件 start
*/
app.use(views(path.join(__dirname, './views'), {
extension: 'ejs'
}))
// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './public'
// 由于koa-static目前不支持koa2
// 所以只能用koa-convert封装一下
app.use(convert(static(path.join(__dirname, staticPath))))
/**
* 使用第三方中间件 end
*/
app.use(async (ctx) => {
if (ctx.method === 'GET') {
let title = 'upload pic async'
await ctx.render('index', {
title,
})
}
else if (ctx.url === '/api/picture/upload.json' && ctx.method === 'POST') {
// 上传文件请求处理
let result = { success: false }
let serverFilePath = path.join(__dirname, 'public/image')
// 上传文件事件
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-async is starting at port 3000')
})
上传图片流写操作
const inspect = require('util').inspect
const path = require('path')
const os = require('os')
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, message: '', data: null }
// 解析请求文件事件
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 = '文件上传成功'
result.data = {
pictureUrl: `//${ctx.host}/image/${fileType}/${fileName}`
}
console.log('文件上传成功!')
resolve(result)
})
})
// 解析结束事件
busboy.on('finish', function () {
console.log('文件上结束')
resolve(result)
})
// 解析错误事件
busboy.on('error', function (err) {
console.log('文件上出错')
reject(result)
})
req.pipe(busboy)
})
}
module.exports = { uploadFile }
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<title><%= title%></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<button class="btn" id="J_UploadPictureBtn">上传图片</button>
<hr/>
<p>上传进度<span id="J_UploadProgress">0</span>%</p>
<p>上传结果图片</p>
<div id="J_PicturePreview" class="preview-picture"> </div>
<script src="js/index.js"></script>
</body>
</html>
上传操作代码
(function () {
let btn = document.getElementById('J_UploadPictureBtn')
let progressElem = document.getElementById('J_UploadProgress')
let previewElem = document.getElementById('J_PicturePreview')
btn.addEventListener('click', function () {
uploadAction({
success: function (result) {
console.log(result)
if (result && result.success && result.data && result.data.pictureUrl) {
previewElem.innerHTML = '![](' + result.data.pictureUrl + ')'
}
},
progress: function (data) {
if (data && data * 1 > 0) {
progressElem.innerText = data
}
}
})
})
/**
* 类型判断
* @type {Object}
*/
let UtilType = {
isPrototype: function (data) {
return Object.prototype.toString.call(data).toLowerCase();
}, isJSON: function (data) {
return this.isPrototype(data) === '[object object]';
}, isFunction: function (data) {
return this.isPrototype(data) === '[object function]';
}
}
/**
* form表单上传请求事件
* @param {object} options 请求参数
*/
function requestEvent(options) {
try {
let formData = options.formData
let xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
options.success(JSON.parse(xhr.responseText))
}
}
xhr.upload.onprogress = function (evt) {
let loaded = evt.loaded
let tot = evt.total
let per = Math.floor(100 * loaded / tot)
options.progress(per)
}
xhr.open('post', '/api/picture/upload.json')
xhr.send(formData)
} catch (err) { options.fail(err) }
}
/**
* 上传事件
* @param {object} options 上传参数
*/ function uploadEvent(options) {
let file
let formData = new FormData()
let input = document.createElement('input')
input.setAttribute('type', 'file')
input.setAttribute('name', 'files')
input.click()
input.onchange = function () {
file = input.files[0]
formData.append('files', file)
requestEvent({ formData, success: options.success, fail: options.fail, progress: options.progress })
}
}
/**
* 上传操作
* @param {object} options 上传参数
*/
function uploadAction(options) {
if (!UtilType.isJSON(options)) {
console.log('upload options is null')
return
}
let _options = {}
_options.success = UtilType.isFunction(options.success) ? options.success : function () { }
_options.fail = UtilType.isFunction(options.fail) ? options.fail : function () { }
_options.progress = UtilType.isFunction(options.progress) ? options.progress : function () { }
uploadEvent(_options)
}
})()
创建mysql数据库连接池
const mysql = require('mysql')
//创建数据连接池
const pool = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: 'root',
database: 'koademo'
})
//在数据池中进行会话操作
pool.getConnection((err, conn) => {
conn.query('SELECT * FROM test', (err, rs, fields) => {
//结束会话
conn.release()
if (err) throw err
})
})
async/await封装使用mysql
/* ./async-db.js */
const msyql = require('mysql')
const pool = msyql.createPool({
host: '127.0.0.1',
user: 'root',
password: 'root',
database: 'koademo'
})
let query = (sql, values) => {
return new Promise((resolve, reject) => {
pool.getConnection((err, conn) => {
if (err) {
reject(err)
} else {
conn.query(sql, values, (err, rows) => {
if (err) {
reject(err)
} else {
resolve(rows)
}
conn.release()
})
}
})
})
}
module.exports = {
query
}
/* index.js */
const { query } = require('./async-db')
async function selectAllData() {
let sql = 'SELECT * FROM test'
let dataList = await query(sql)
return dataList
}
async function getData() {
let dataList = await selectAllData()
console.log(dataList)
}
getData()
jsonp
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 tarting on port 3000')
})
koa-jsonp中间件
const Koa = require('koa')
const jsonp = require('koa-jsonp')
const app = new Koa()
//使用中间件
app.use(jsonp())
app.use(async (ctx) => {
let returnData = {
success: true,
data: {
text: 'this is a jsonp api',
time: new Date().getTime()
}
}
//直接输出json
ctx.body = returnData
})
app.listen(3000, () => {
console.log('[demo] koa-jsonp is tarting on port 3000')
})
各章节代码存放在对应的分支中:所有源码