Koa2超詳細入門教程

Koa2 快速開始

 

環境準備

 

快速開始

 

安裝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

運行效果如下

訪問http://localhost:3000/index 

 

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

 

訪問http://localhost:3000/index.html

 

訪問http://localhost:3000/js/index.js

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章