Koa2基础

Koa2学习笔记

Koa2简介

Koa是一款新的web框架,它是由Express原班人马打造,相比与Express,Koa更加mini、简洁、健壮。它没有绑定任何中间件,如果我们需要,我们完全可以手撸任何中间件。而且,Koa2通过使用async/await,使得代码的可读性更佳。

安装

Koa2依赖于nodeV7.6.0或者ES2015及更高的版本和async的支持。

#初始化package.json
npm init
#安装npm7
npm install 7
#安装koa2
npm install koa

async/await 小demo

async function test(time){
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('test async')
        },time)
    })
}

function getSomething(){
    return 'something'
}
//async await简单案例
async function fn(){
    const res1 = await test(2000);
    const res2 = await getSomething();
    console.log(res1,res2)
}
fn();

2s之后,依次打印test async something
await必须存在于async函数之内,且await后面的代码会被“阻塞”,等待await后面的promise执行。

  • 可以让异步逻辑用同步写法实现
  • await返回的需要是promise对象
  • 可以通过多层 async function 的同步写法代替传统的callback嵌套

    Hello world

const Koa = require('koa');
const app = new Koa();
//入门🌰
app.use(async(ctx) => {
    ctx.body = 'hello kaka'
    console.log('hello koa')
})

app.listen(3000)

启动demo,node app.js,打开http://localhost:3000/,即可看到:
image.png

中间件

kao中间件开发与使用

中间件的类型有一下四种:

  • 应用级中间件
  • 路由级中间件
  • 错误处理中间件
  • 第三方中间件

    洋葱模型

    当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。
    当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。
    看🌰:
const Koa = require('koa');

const app = new Koa();

//洋葱模型

// #1
app.use(async(ctx, next) => {
    console.log(1)
    await next();
    console.log(2)
});
// #2
app.use(async(ctx, next) => {
    console.log(3)
    await next();
    console.log(4)
});

app.use(async(ctx, next) => {
    console.log(5)
});

app.listen(3000, () => {
    console.log('koa服务器已启动')
});

app.on('error', (err) => {
    console.log(err)
});
//1 3 5 4 2

image.png

开发

//middleware/logger.js
module.exports = async(ctx, next) => {
    ctx.state.commondata = '我是应用级中间件';
    console.log("应用级中间件");
    await next()
}

使用

//app.js
const Koa = require('koa');

const logger = require('./middleware/logger');

const app = new Koa();

//应用级中间件
app.use(logger)

app.listen(3000, () => {
    console.log('koa服务器已启动')
});

app.on('error', (err) => {
    console.log(err)
})

路由

原生实现

const Koa = require('koa');
const app = new Koa();
const bodyparser = require('koa-bodyparser');
const fs = require('fs');

app.use(bodyparser());

/**
 * @param { string } 文件名
 * @return { promise } 返回文件
 * 
 */
function render(page) {
    return new Promise((resolve, reject) => {
        let pagePath = `./html/${page}`;
        console.log(pagePath)
        fs.readFile(pagePath, 'utf-8', (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        })
    })
}

/**
 * @param { string } 访问路由
 * @return { string }  html
*/
async function getHtml(url) {
    let page = '404.html';
    console.log(url)
    switch (url) {
        case '/':
            {
                page = 'index.html';
                break
            }
        case '/list':
            {
                page = 'list.html';
                break
            }
        default:
            {
                page = '404.html';
                break
            }
    }
    let html = await render(page);
    return html
}
app.use(async(ctx) => {
    //读取文件是异步
    let html = await getHtml(ctx.url);
    ctx.body = html;
})
app.listen(3000, () => {
    console.log("server is running at http://127.0.0.1:3000")
})

启动demo之后,访问http://localhost:3000/即可
image.png

koa-router

安装对应的koa-router中间件:
npm install koa-router
demo源码

const Koa = require('koa');
const bodyparser = require('koa-bodyparser');
const Router = require('koa-router');

const app = new Koa();
const router = new Router({
    //设置路由前缀 
    prefix:'/kaka'
});

router.get('/',(ctx,next) => {
    ctx.body = 'hello';
    next()
})

app.use(router.routes()).use(router.allowedMethods())

app.use(async(ctx) => {

});

app.listen(3000,() => {
    console.log("server is running at http://127.0.0.1:3000")
})

请求数据处理

Get请求

在koa中,获取GET请求数据源头是koa中request对象中的query方法或querystring方法,query返回是格式化好的参数对象,querystring返回的是请求字符串,由于ctx对request的API有直接引用的方式,所以获取GET请求数据有两个途径。

  • 是从上下文中直接获取
  • 是从上下文的request对象中获取

看🌰: 源码

const Koa = require('koa');
const app = new Koa();
const bodyParse = require('koa-bodyparser');
//使用中间件koa-bodyparser
app.use(bodyParse());

app.use(async(ctx) => {
    const request = ctx.request;
    const url = ctx.url;
    const method = request.method;
    const query = request.query; //返回的是格式化好的参数对象。
    const querystring = request.querystring; //返回的是请求字符串。
    //直接从ctx中获取query
    const ctx_query = ctx.query;
    const ctx_querystring = ctx.querystring;
    if (method === 'GET' && url === '/') {
        ctx.body = `
        <h1>Koa2 request post demo</h1>
        <form method="POST"  action="/">
            <p>userName</p>
            <input name="userName" /> <br/>
            <p>age</p>
            <input name="age" /> <br/>
            <p>webSite</p>
            <input name='webSite' /><br/>
            <button type="submit">submit</button>
        </form>
        `
    } else if (ctx.url === '/' && ctx.method === 'POST') {
        // const postDate = await handlePostData(ctx);
        const postDate = ctx.request.body;
        ctx.body = postDate;
    } else {
        //其它请求显示404页面
        ctx.body = '<h1>404!</h1>';
    }
    ctx.body = {
        url,
        methods,
        query,
        querystring,
        ctx_query,
        ctx_querystring
    }
    console.log(ctx)
});

app.listen(3000)

Post请求

对于POST请求的处理,koa2没有封装获取参数的方法,需要通过解析上下文context中的原生node.js请求对象req,将POST表单数据解析成query string,再将query string 解析成JSON格式。

//使用node中的方式处理post请求数据
function handlePostData(ctx) {
    return new Promise((resolve, reject) => {
        try {
            let querystring = '';
            ctx.req.on('data', (data) => {
                querystring += data;
            })
            ctx.req.on('end', () => {
                let bodyParse = parseStringData(querystring)
                resolve(bodyParse)
            })
        } catch (error) {
            reject(error)
        }
    })
}

function parseStringData(string) {
    let item = {};
    let stringList = string.split('&');
    for (const [key, value] of stringList.entries()) {
        console.log(key, value);
        item[value.split('=')[0]] = decodeURIComponent(value.split('=')[1]);
    }
    return item;
}

使用koa-parser中间件

对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到ctx.request.body中
安装:npm install koa-parser
使用:

const bodyParse = require('koa-bodyparser');
//使用中间件koa-bodyparser
app.use(bodyParse());

静态资源处理

koa-static中间件

安装:npm install koa-static

const Koa = require('koa');
const static = require('koa-static');
const path = require('path');

const app = new Koa();

app.use(static(
    path.join(__dirname,'./static')
));

app.use( async ( ctx ) => {
    ctx.body = 'hello world'
  })

app.listen(3000, () => {
  console.log('static-use-middleware is starting at port 3000')
})

koa提供了直接操作cookie的方法:

  • ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
  • ctx.cookies.set(name, value, [options]) 在上下文中写入cookie

使用:

const Koa =require('koa');

const app = new Koa();

app.use(async(ctx) => {
    /**
     * domain:写入cookie所在的域名
     * path:写入cookie所在的路径
     * maxAge:Cookie最大有效时长
     * expires:cookie失效时间
     * httpOnly:是否只用http请求中获得
     * overwirte:是否允许重写
    */
   if(ctx.url === '/'){
        ctx.cookies.set(
            "name","ykx",{
                domain:'127.0.0.1',
                maxAge:1000*60*60*24*30,
                expires:new Date('2020-12-30'),
                httpOnly:false,
                overwrite:true
            }
        );
        ctx.body = 'cookie is ready'
   }else{
       if(ctx.cookies.get('name')){
            const str = ctx.cookies.get('name');
            ctx.body = str;
       }else{
            ctx.body = 'cookies is none';
       }

   }

})

app.listen(3000);

session

koa原生只提供了操作cookie的方法,但是没有session操作。所以对sessino的操作需要自己实现或者使用第三方插件。
将session存储到数据库,需要使用第三方插件:

  • koa-session-minimal 适用于koa2 的session中间件,提供存储介质的读写接口 。
  • koa-mysql-session 为koa-session-minimal中间件提供MySQL数据库的session数据读写操作。

安装:

npm install koa-session-minimal koa-mysql-session --save

过程:

  • 生成session,并存储到数据库。
  • 使用插件操作数据库,并将sessionId和对应的数据存储到数据库。
  • 将数据库的sessionId写入到页面的cookie中。
  • 根据页面cookie携带的sessionId取出数据库中对应的数据。

看代码:

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: '',//密码
    database: 'test',//数据库名称
    host: '127.0.0.1',//数据库地址
});

// 存放sessionId的cookie配置
let cookie = {
    maxAge: '', // cookie有效时长
    expires: '', // cookie失效时间
    path: '', // 写cookie所在的路径
    domain: '', // 写cookie所在的域名
    httpOnly: '', // 是否只用于http请求中获取
    overwrite: '', // 是否允许重写
    secure: '',
    sameSite: '',
    signed: '',

};

app.use(session({
    key: 'SESSION_ID',
    store,
    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 at port 3000')

模版引擎ejs

ejs

具体使用请查看官方文档

koa2使用ejs

安装:

#安装koa模版使用中间件
npm install koa-views --save
#安装ejs模版引擎
npm install ejs --save

使用:

const Koa = require('koa');
const views = require('koa-views');
const path = require('path');

const app = new Koa();
//加载ejs模版 设置模版文件夹
app.use(views(path.join(__dirname,'./views'),{
    extension:'ejs'
}))

app.use(async(ctx) => {
    let title = 'hello ejs';
      //渲染模版
    await ctx.render('index',{
        title
    })
})

app.listen(3000,()=>{
    console.log('server is starting at 127.0.0.1:3000');
})

views下的模版:

<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= title %></h1>
    <p>EJS Welcome to <%= title %></p>
</body>
</html>

文件上传

busboy

busboy用来解析Post请求,读取原生node的req中的文件流。具体使用请移步
安装:
npm install busboy --save
使用:源码
app.js

//文件上传
const Koa = require('koa');
const Views = require('koa-views');
const Router = require('koa-router');
const path = require('path');
const upload = require('./util/syncUpload.js');
const static = require('koa-static');



const app = new Koa();
const router = new Router();

app.use(Views(path.join(__dirname + '/views'), {
    extension: 'ejs'
}));
app.use(static('static'))

router.get('/', async ctx => {
    await ctx.render('upload')
});

router.post('/upload', async ctx => {
    let res = await upload(ctx, {
        path: path.join(__dirname, 'static')
    });
    ctx.body = '<img src="' + res.path + '" style="max-width: 100%">';
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000)

ejs上传模版:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>文件上传</title>
</head>

<body>
    <form action="/upload" method="post" enctype="multipart/form-data">
        <label for="file">文件</label>
        <input type="file" name="file" id="file">
        <input type="submit">
    </form>
</body>

</html>

node上传处理文件

const fs = require('fs');
const path = require('path');
const BusBoy = require('busboy');

/**
 * 获取上传文件的后缀名
 * @param  {string} fileName 获取上传文件的后缀名
 * @return {string}          文件后缀名
 */
function getFileSuffixName(fileName) {
    const nameList = fileName.split('.');
    return nameList[nameList.length - 1];
}


/**
 * 同步创建文件目录
 * @param { string } filePath 文件上传路径
 * @return { boolean }         返回结果
 */
function mkdirPath(dirname) {
    if (fs.existsSync(dirname)) {
        return true;
    }
    fs.mkdirSync(dirname)
    return true;
}

/**
 * 同步上传文件
 * @param { ctx } Object 上下文对象
 * @param { options } Object 配置参数
 * @return { promise }   返回上传结果的promise
 */
module.exports = (ctx, options) => {
    return new Promise((resolve, reject) => {
        let req = ctx.req;
        let res = ctx.res;
        let busboy = new BusBoy({ headers: req.headers });
        let result = {
            success: false,
            path: null
        }

        busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
            console.log('File [' + fieldname + ']: filename: ' + filename + 'encoding: ' + encoding + 'mimetype: ' + mimetype);
            let fileName = Math.random().toString(32).substr(2) + '.' + getFileSuffixName(filename);
            // let filePath = mkdirPath(options.path) && (options.path + '/' + filename);
            let filePath = filename;
            file.on('data', data => {
                console.log('File [' + filename + '] got ' + data.length + ' bytes')
            });
            file.on('end', () => {
                console.log('File [' + filename + '] finished ');
                result.success = true;
                result.path = filePath;
                resolve(result)
            });
            file.on('error', () => {
                reject(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)
            });
            file.pipe(fs.createWriteStream(filePath));
        })
        req.pipe(busboy);
    })
}
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!