Koa2 快速開始
環境準備
- 因爲node.js v7.6.0開始完全支持async/await,不需要加flag,所以node.js環境都要7.6.0以上
- node.js環境 版本v7.6以上
- 直接安裝node.js 7.6:node.js官網地址https://nodejs.org
- nvm管理多版本node.js:可以用nvm 進行node版本進行管理
- Mac系統安裝nvm https://github.com/creationix/nvm#manual-install
- windows系統安裝nvm https://github.com/coreybutler/nvm-windows
- Ubuntu系統安裝nvm https://github.com/creationix/nvm
- npm 版本3.x以上
快速開始
安裝koa2
# 初始化package.json
npm init
# 安裝koa2
npm install koa
Hello World 代碼
const Koa = require('koa')
const app = new Koa()
app.use( async ( ctx ) => {
ctx.body = 'hello koa2'
})
app.listen(3000)
console.log('[demo] start-quick is starting at port 3000')
啓動demo
由於koa2是基於async/await操作中間件,目前node.js 7.x的harmony模式下才能使用,所以啓動的時的腳本如下:
node index.js
訪問http:localhost:3000,效果如下
Async/await使用
快速上手理解
先複製以下這段代碼,在粘貼在chrome的控制檯console中,按回車鍵執行
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()
在chrome的console中執行結果如下
async/await使用
快速上手理解
先複製以下這段代碼,在粘貼在chrome的控制檯console中,按回車鍵執行
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()
在chrome的console中執行結果如下
從上述例子可以看出 async/await 的特點:
- 可以讓異步邏輯用同步寫法實現
- 最底層的await返回需要是Promise對象
- 可以通過多層 async function 的同步寫法代替傳統的callback嵌套
從上述例子可以看出 async/await 的特點:
- 可以讓異步邏輯用同步寫法實現
- 最底層的await返回需要是Promise對象
- 可以通過多層 async function 的同步寫法代替傳統的callback嵌套
koa2簡析結構
源碼文件
├── lib
│ ├── application.js
│ ├── context.js
│ ├── request.js
│ └── response.js
└── package.json
這個就是 GitHub
https://github.com/koajs/koa上開源的koa2源碼的源文件結構,核心代碼就是lib目錄下的四個文件
- application.js 是整個koa2 的入口文件,封裝了context,request,response,以及最核心的中間件處理流程。
- context.js 處理應用上下文,裏面直接封裝部分request.js和response.js的方法
- request.js 處理http請求
- response.js 處理http響應
koa2特性
- 只提供封裝好http上下文、請求、響應,以及基於
async/await
的中間件容器。 - 利用ES7的
async/await
的來處理傳統回調嵌套問題和代替koa@1的generator,但是需要在node.js 7.x的harmony模式下才能支持async/await
。 - 中間件只支持
async/await
封裝的,如果要使用koa@1基於generator中間件,需要通過中間件koa-convert封裝一下才能使用。
- © 2020 GitHub, Inc.
koa2 中的 async/await 使用
舉個栗子
- Promise封裝 fs 異步讀取文件的方法
// code file: util/render.js
// Promise封裝 fs 異步讀取文件的方法
const fs = require('fs')
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 )
}
})
})
}
module.exports = render
- koa2 通過async/await 實現讀取HTML文件並執行渲染
// code file : index.js
// koa2 通過async/await 實現讀取HTML文件並執行渲染
const Koa = require('koa')
const render = require('./util/render')
const app = new Koa()
app.use( async ( ctx ) => {
let html = await render('index.html')
ctx.body = html
})
app.listen(3000)
console.log('[demo] start-async is starting at port 3000')
koa中間件開發和使用
注:原文地址在我的博客issue裏https://github.com/ChenShenhai/blog/issues/15
- koa v1和v2中使用到的中間件的開發和使用
- generator 中間件開發在koa v1和v2中使用
- async await 中間件開發和只能在koa v2中使用
generator中間件開發
generator中間件返回的應該是function * () 函數
/* ./middleware/logger-generator.js */
function log( ctx ) {
console.log( ctx.method, ctx.header.host + ctx.url )
}
module.exports = function () {
return function * ( next ) {
// 執行中間件的操作
log( this )
if ( next ) {
yield next
}
}
}
generator中間件在koa@1中的使用
generator 中間件在koa v1中可以直接use使用
const koa = require('koa') // koa v1
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封裝一下才能使用
const Koa = require('koa') // koa v2
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')
async 中間件開發
/* ./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()
}
}
async 中間件在koa@2中使用
async 中間件只能在 koa v2中使用
const Koa = require('koa') // koa v2
const loggerAsync = require('./middleware/logger-async')
const app = new Koa()
app.use(loggerAsync())
app.use(( ctx ) => {
ctx.body = 'hello world!'
})
app.listen(3000)
console.log('the server is starting at port 3000')
koa2 原生路由實現
簡單例子
const Koa = require('koa')
const app = new Koa()
app.use( async ( ctx ) => {
let url = ctx.request.url
ctx.body = url
})
app.listen(3000)
訪問 http://localhost:3000/hello/world 頁面會輸出 /hello/world,也就是說上下文的請求request對象中url之就是當前訪問的路徑名稱,可以根據ctx.request.url 通過一定的判斷或者正則匹配就可以定製出所需要的路由。
定製化的路由
demo源碼
https://github.com/ChenShenhai/koa2-note/tree/master/demo/route-simple
源碼文件目錄
.
├── index.js
├── package.json
└── view
├── 404.html
├── index.html
└── todo.html
demo源碼
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()
/**
* 用Promise封裝異步讀取文件方法
* @param {string} page html文件名稱
* @return {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內容
* @param {string} url koa2上下文的url,ctx.url
* @return {string} 獲取HTML文件內容
*/
async function route( url ) {
let view = '404.html'
switch ( url ) {
case '/':
view = 'index.html'
break
case '/index':
view = 'index.html'
break
case '/todo':
view = 'todo.html'
break
case '/404':
view = '404.html'
break
default:
break
}
let html = await render( view )
return html
}
app.use( async ( ctx ) => {
let url = ctx.request.url
let html = await route( url )
ctx.body = html
})
app.listen(3000)
console.log('[demo] route-simple is starting at port 3000')
運行demo
執行運行腳本
node -harmony index.js
運行效果如下
koa-router中間件
如果依靠ctx.request.url去手動處理路由,將會寫很多處理代碼,這時候就需要對應的路由的中間件對路由進行控制,這裏介紹一個比較好用的路由中間件koa-router
安裝koa-router中間件
# koa2 對應的版本是 7.x
npm install --save koa-router@7
快速使用koa-router
demo源碼
https://github.com/ChenShenhai/koa2-note/tree/master/demo/route-use-middleware
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()
const Router = require('koa-router')
let home = new Router()
// 子路由1
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] route-use-middleware is starting at port 3000')
})
GET請求數據獲取
使用方法
在koa中,獲取GET請求數據源頭是koa中request對象中的query方法或querystring方法,query返回是格式化好的參數對象,querystring返回的是請求字符串,由於ctx對request的API有直接引用的方式,所以獲取GET請求數據有兩個途徑。
- 1.是從上下文中直接獲取
- 請求對象ctx.query,返回如 { a:1, b:2 }
- 請求字符串 ctx.querystring,返回如 a=1&b=2
- 2.是從上下文的request對象中獲取
- 請求對象ctx.request.query,返回如 { a:1, b:2 }
- 請求字符串 ctx.request.querystring,返回如 a=1&b=2
舉個例子
demo源碼
https://github.com/ChenShenhai/koa2-note/blob/master/demo/request/get.js
例子代碼
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] request get is starting at port 3000')
})
執行程序
node get.js
執行後程序後,用chrome訪問 http://localhost:3000/page/user?a=1&b=2 會出現以下情況
注意:我是用了chrome的json格式化插件纔會顯示json的格式化
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
解析出POST請求上下文中的表單數據
demo源碼
https://github.com/ChenShenhai/koa2-note/blob/master/demo/request/post.js
// 解析上下文裏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
}
舉個例子
源碼在 /demos/request/post.js中
例子代碼
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')
})
啓動例子
node post.js
訪問頁面
提交表單發起POST請求結果顯示
koa-bodyparser中間件
原理
對於POST請求的處理,koa-bodyparser中間件可以把koa2上下文的formData數據解析到ctx.request.body中
安裝koa2版本的koa-bodyparser@3中間件
npm install --save koa-bodyparser@3
舉個例子
例子代碼
demo源碼
https://github.com/ChenShenhai/koa2-note/blob/master/demo/request/post-middleware.js
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 = `
<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請求的時候,中間件koa-bodyparser解析POST表單裏的數據,並顯示出來
let postData = ctx.request.body
ctx.body = postData
} else {
// 其他請求顯示404
ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
}
})
app.listen(3000, () => {
console.log('[demo] request post is starting at port 3000')
})
啓動例子
node post-middleware.js
訪問頁面
提交表單發起POST請求結果顯示
原生koa2實現靜態資源服務器
前言
一個http請求訪問web服務靜態資源,一般響應結果有三種情況
- 訪問文本,例如js,css,png,jpg,gif
- 訪問靜態目錄
- 找不到資源,拋出404錯誤
原生koa2 靜態資源服務器例子
demo源碼
https://github.com/ChenShenhai/koa2-note/blob/master/demo/static-server/
代碼目錄
├── static # 靜態資源目錄 │ ├── css/ │ ├── image/ │ ├── js/ │ └── index.html ├── util # 工具代碼 │ ├── content.js # 讀取請求內容 │ ├── dir.js # 讀取目錄內容 │ ├── file.js # 讀取文件內容 │ ├── mimes.js # 文件類型列表 │ └── walk.js # 遍歷目錄內容 └── index.js # 啓動入口文件
代碼解析
index.js
const Koa = require('koa') const path = require('path') const content = require('./util/content') const mimes = require('./util/mimes') const app = new Koa() // 靜態資源目錄對於相對入口文件index.js的路徑 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) // 獲取靜態資源內容,有可能是文件內容,目錄,或404 let _content = await content( ctx, fullStaticPath ) // 解析請求內容的類型 let _mime = parseMime( ctx.url ) // 如果有對應的文件類型,就配置上下文的類型 if ( _mime ) { ctx.type = _mime } // 輸出靜態資源內容 if ( _mime && _mime.indexOf('image/') >= 0 ) { // 如果是圖片,則用node原生res,輸出二進制數據 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
const path = require('path') const fs = require('fs') // 封裝讀取目錄內容方法 const dir = require('./dir') // 封裝讀取文件內容方法 const file = require('./file') /** * 獲取靜態資源內容 * @param {object} ctx koa上下文 * @param {string} 靜態資源目錄在本地的絕對路徑 * @return {string} 請求獲取到的本地內容 */ async function content( ctx, fullStaticPath ) { // 封裝請求資源的完絕對徑 let reqPath = path.join(fullStaticPath, ctx.url) // 判斷請求路徑是否爲存在目錄或者文件 let exist = fs.existsSync( reqPath ) // 返回請求內容, 默認爲空 let content = '' if( !exist ) { //如果請求路徑不存在,返回404 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
const url = require('url') const fs = require('fs') const path = require('path') // 遍歷讀取目錄內容方法 const walk = require('./walk') /** * 封裝目錄內容 * @param {string} url 當前請求的上下文中的url,即ctx.url * @param {string} reqPath 請求靜態資源的完整本地路徑 * @return {string} 返回目錄內容,封裝成HTML */ 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
const fs = require('fs') /** * 讀取文件方法 * @param {string} 文件本地的絕對路徑 * @return {string|binary} */ function file ( filePath ) { let content = fs.readFileSync(filePath, 'binary' ) return content } module.exports = file
util/walk.js
const fs = require('fs') const mimes = require('./mimes') /** * 遍歷讀取目錄內容(子目錄,文件名) * @param {string} reqPath 請求資源的絕對路徑 * @return {array} 目錄內容列表 */ 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
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
運行效果
啓動服務
node index.js
效果
訪問http://localhost:3000/index.html
訪問http://localhost:3000/js/index.js