前言:站在巨人的肩膀上,感謝前輩們的付出與貢獻
安裝 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()
async/await
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')
})
GET
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表單請求 | 請求響應結果 |
---|---|
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')
})
cookie
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 = ''
}
},
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')
})
各章節代碼存放在對應的分支中:所有源碼