koa@2學習筆記

weixin_34007291發表於2017-10-25

前言:站在巨人的肩膀上,感謝前輩們的付出與貢獻

安裝 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()
7425269-7bdee9357442b160.png
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')
})
7425269-ad6372d725ba4cb9.png
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表單請求 請求響應結果
7425269-46b287dfe47fa253.png
POST表單
7425269-fa1ed2727031bd94.png
提交結果

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')
})
7425269-ed382327d922885d.png
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 = '![](' + 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')
})

各章節程式碼存放在對應的分支中:所有原始碼