Koa2原理和使用

一、Koa洋葱模型

现象

  • 中间件的执行两次
  • 执行顺序以next函数为分界点:先use的中间件,next前的逻辑先执行,但next后的逻辑反而后执行

原理

Express的中间件是顺序执行,从第一个中间件执行到最后一个中间件,发出响应。
Koa是从第一个中间件开始执行,遇到 await next() 就进入下一个中间件,一直到执行到最后一个中间件。然后再逆序执行上一个中间件 await next() 后面的代码,一直到第一个中间件 await next() 后面的代码执行完毕才发出响应。

koa把很多async函数组成一个处理链,每个async函数都可以做一些自己的事情,然后用await next()来调用下一个async函数。我们把每个async函数称为middleware,这些middleware可以组合起来,完成很多有用的功能。

app.use(async (ctx, next) => {
    console.log('1'); 
    await next(); // 调用下一个middleware
    console.log('5')
});

app.use(async (ctx, next) => {
    console.log('2');
    await next(); // 调用下一个middleware
    console.log('4');
});

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

输出结果: 12345

在这个例子里,通过输出结果可以看出三个中间件的执行顺序是:
中间件1 -> 中间件2 -> 中间件3 -> 中间件2 -> 中间件1

二、基本使用

npm init // 初始化package.json
cnpm install koa // 安装koa2

目录结构

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

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

// koa-bodyparser模块
const bodyParser = require('koa-bodyparser');

// art-template模板引擎
const views = require('koa-views');
app.use(views('views', { map: { html: 'ejs' } }));

// koa-static静态资源中间件
const static = require('koa-static');
app.use(bodyParser());

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

// mysql数据库模块
const mysql = require('mysql')

const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: '666123',
    database: 'vue'
});

connection.connect();

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

router.get('/', function (ctx, next) {
    ctx.body = "Hello koa";
})

router.get('/news', (ctx, next) => {
    // GET请求数据以及获取GET数据
    console.log(ctx.query);
    console.log(ctx.querystring);
    console.log(ctx.request.query);
    console.log(ctx.request.querystring);
    ctx.body = "新闻页面"
});

router.get('/add', async (ctx) => {
    let title = 'hello koa2'
    await ctx.render('index', {
        title
    })
})

router.post('/api/add', async (ctx) => {
    console.log(typeof ctx.request.body);
    console.log(ctx.request.body);
    ctx.set("Content-Type", "application/json");
    ctx.body = ctx.request.body;
})

router.get("/api/list", (req, res) => {
    let obj = {
        ret: false,
        total: 0,
        rows: [],
        msg: ""
    }

    const pageSize = Number(req.query.pageSize);
    const pageNumber = Number(req.query.pageNumber) - 1;

    connection.query(`SELECT * FROM blog`, (error, results, fields) => {
        if (!error) {
            obj.total = results.length;
            // 分页查询 begin
            connection.query(`SELECT * FROM blog limit ${pageNumber * pageSize}, ${pageSize}`, (error, results, fields) => {
                if (!error) {
                    obj.ret = true;
                    obj.rows = results.map((value, key) => {
                        return {
                            id: value.id,
                            title: value.title,
                            content: value.content.toString(),
                            datetime: value.datetime
                        }
                    });
                }
            })
            // 分页查询 end
        }
    })
})

app.use(router.routes()); //作用:启动路由
app.use(router.allowedMethods()); //作用: 当请求出错时的处理逻辑
app.listen(3000, () => {
    console.log('starting at port 3000');
});
<!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>Document</title>
</head>

<body>
    <!-- <img src="/1.jpg"/> -->

    <input type="input" id="content" name="content" /><input type="button" value="submit" id="submit-button" />
    <div class="showmsg"></div>

    <script>
        document.getElementById("submit-button").onclick = function () {
            const content = document.getElementById("content").value; // 获取文件对象

            var xhr = new XMLHttpRequest();
            xhr.open("post", "/api/add");
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            xhr.send("content=" + content);
            xhr.onreadystatechange = function () {
                // 判断服务器是否响应,判断异步对象的响应状态
                if (xhr.status == 200 && xhr.readyState == 4) {
                    document.querySelector(".showmsg").innerHTML = xhr.responseText;
                }
            }
        }
    </script>
</body>

</html>

1、art-template模板引擎

cnpm install --save art-template
cnpm install --save koa-art-template
const Koa = require('koa');
const render = require('koa-art-template');

const app = new Koa();

render(app, {
  root: path.join(__dirname, 'view'),
  extname: '.html',
  debug: process.env.NODE_ENV !== 'production'
});

app.use(async function (ctx) {
  await ctx.render('user');
});

app.listen(8080);

官网:Koa - art-template 

2、 静态资源托管

app.js:

const static = require("koa-static")
const mount = require("koa-mount") // 和koa-static配合使用

// 静态资源托管
app.use(
    mount("/public", static(path.join(__dirname, "./static")))
)

 index.html:

<div><img src="/public/1.jpg"/></div>

3、异步中间件

app.js:

const Router = require("koa-router")
const fs = require("fs")
const util = require("util")

const router = new Router();

router.get("/file", async (ctx, next) => {
    const data = await util.promisify(fs.readFile)('./package.json')
    ctx.type = "html"
    ctx.body = data
})

4、重定向针对的是同步请求

router.get("/foo", async ctx => {
    ctx.redirect("/add")
})

5、mysql数据库模块

const mysql = require('mysql')

const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: '123456',
    database: 'react'
});

connection.connect();

router.get("/api/list", (req, res) => {
    let obj = {
        ret: false,
        total: 0,
        rows: [],
        msg: ""
    }

    const pageSize = Number(req.query.pageSize);
    const pageNumber = Number(req.query.pageNumber) - 1;

    connection.query(`SELECT * FROM blog`, (error, results, fields) => {
        if (!error) {
            obj.total = results.length;
            // 分页查询 begin
            connection.query(`SELECT * FROM blog limit ${pageNumber * pageSize}, ${pageSize}`, (error, results, fields) => {
                if (!error) {
                    obj.ret = true;
                    obj.rows = results.map((value, key) => {
                        return {
                            id: value.id,
                            title: value.title,
                            content: value.content.toString(),
                            datetime: value.datetime
                        }
                    });
                }
            })
            // 分页查询 end
        }
    })
})

6、异常处理

const Koa = require('koa');
const { promisify } = require("util");
const fs = require("fs");
const app = new Koa();

app.use(async (ctx, next) => {
    JSON.parse("{}");
    ctx.body = "hello koa";
    await next();
});

app.use(async (ctx, next) => {
    const data = await promisify(fs.readFile)("./test.txt");
    ctx.body = data;
})

app.on("error", err => {
    console.log("error :", err);
})

app.listen(3000, () => {
    console.log("http://localhost:3000");
});

ctx.throw()

koa 提供了 ctx.throw() 方法用来抛出错误,我们可以直接 ctx.throw(500,就是抛出500的错误,类似下面例子:

app.use(async (ctx,next) => {
    ctx.throw(500);
});

值得注意的是,如果将 ctx.response.status 设置成对应的状态码,效果跟 ctx.throw() 是一样的,比如设置成 404。

app.use(async (ctx,next) => {
    ctx.response.status = 404;
    ctx.response.body = 'Page Not Found';
});

try…catch…

为了更加方便的处理错误,使用 try…catch… 将错误捕获是比较好的,但是每个请求都写感觉太麻烦,其实我们可以定义一个中间件负责对所有的错误进行处理: 

const Koa = require('koa');
const { promisify } = require("util");
const fs = require("fs");
const app = new Koa();

// 要放在最前面
app.use(async (ctx, next) => {
    try {
        await next();
    } catch (err) {
        ctx.status = 500;
        ctx.body = err.message;
    }
});

app.use(async (ctx, next) => {
    JSON.parse("{}");
    ctx.body = "hello koa";
    await next();
});

app.use(async (ctx, next) => {
    const data = await promisify(fs.readFile)("./test.txt");
    ctx.body = data;
})

app.listen(3000, () => {
    console.log("http://localhost:3000");
});

监听 error 事件

其实,koa 还有一个 error 事件监听,如果代码运行发生错误,就会出发这个事件,我们也可以用它来处理错误。

app.on('error', (err, ctx) =>
    console.error('server error', err);
);

有一点需要注意的是,如果错误已经被 try…catch 捕获,那么就不会出发 error 事件。这是就必须释放 error 事件,调用 ctx.app.emit() ,手动释放 error 事件,koa 监听函数才会生效,否则监听事件是不会触发的。

const Koa = require('koa');
const { promisify } = require("util");
const fs = require("fs");
const app = new Koa();

// 要放在最前面
app.use(async (ctx, next) => {
    try {
        await next();
    } catch (err) {
        ctx.status = 500;
        ctx.body = err.message;
        ctx.app.emit("error", err, ctx)
    }
});

app.use(async (ctx, next) => {
    JSON.parse("{}");
    ctx.body = "hello koa";
    await next();
});

app.use(async (ctx, next) => {
    const data = await promisify(fs.readFile)("./test.txt");
    ctx.body = data;
})

app.on("error", err => {
    console.log("error :", err);
})

app.listen(3000, () => {
    console.log("http://localhost:3000");
});

 以上三种方式都可以。

7、跨域

cnpm i koa2-cors -S
const Koa = require("koa")
const cors = require('koa2-cors');

const app = new Koa()
app.use(cors())

 允许跨域,并且允许附带cookie:

app.use(cors({ credentials: true })) //允许跨域,并且允许附带cookie

8、获取POST请求数据

前端:

<!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>Document</title>
</head>

<body>
    <div>
        <input type="text" id="name" /><button id="btn">提交</button>
    </div>

    <script>
        document.getElementById("btn").onclick = function () {
            const name = document.getElementById("name").value;
            console.log(name)

            const url = "/api/user";

            const xhr = new XMLHttpRequest();
            xhr.open("post", url, true);
            xhr.onload = function () {
                alert("上传完成!");
            };
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            xhr.send('ck=bJNY&signature=intro');
            // 接收数据
            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4 && xhr.status == 200) {
                    console.log(typeof JSON.parse(xhr.responseText))
                    console.log(JSON.parse(xhr.responseText));
                } else {
                    console.log(xhr.statusText);
                }
            };
        }
    </script>
</body>

</html>

 后端:

cnpm i koa-bodyparser -S
const Router = require("koa-router");
const router = new Router();
const bodyparser = require("koa-bodyparser")

router.use(bodyparser()) // app.use(bodyparser())也行

router.post("/login", async ctx => {
    console.log(ctx.request.body);
    ctx.body = ctx.request.body; // 返回JSON数据:{ ck: 'bJNY', signature: 'intro' }
})

9、获取GET请求数据

router.get("/list", async ctx => {
    console.log(ctx.query); // 转换为对象格式
    console.log(ctx.querystring); // 原始查询字符串
})

10、设置返回数据类型

ctx.set('Content-Type', 'text/html; chartset=utf-8');
router.get('/api/file', async (ctx, next) => {
    const content = await promisify(fs.readFile)('./index.html');
    ctx.set('Content-Type', 'text/html; chartset=utf-8');
    ctx.body = content;
})

11、路由不单独文件、单独文件、路由拆分 

1)路由不单独文件

const fs = require("fs")
const Koa = require("koa")
const Router = require("koa-router")
const { promisify } = require("util")
const bodyparser = require("koa-bodyparser")
const cors = require('koa2-cors');

const app = new Koa()

const router = new Router()
router.use(bodyparser())

app.use(async (ctx, next) => {
    try {
        await next()
    } catch (err) {
        ctx.body = err.message
        // ctx.app.emit("error", err, ctx)
    }
});

router.get("/api/data", async ctx => {
    console.log("object");
    ctx.body = {
        status: 0,
        data: "hello world!"
    };
})

router.get('/news', async (ctx, next) => {
    // GET请求数据以及获取GET数据
    ctx.body = "新闻页面"
});

router.post('/api/login', async (ctx, next) => {
    ctx.body = ctx.request.body;
});

router.get('/api/file', async (ctx, next) => {
    const content = await promisify(fs.readFile)('./index.html');
    ctx.set('Content-Type', 'text/html; chartset=utf-8');
    ctx.body = content;
})

// app.use(async ctx => {
//     const data = await promisify(fs.readFile)("./add.js")
//     ctx.body = data
// })

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

app.use(router.routes()); //作用:启动路由
app.use(router.allowedMethods()); //作用: 当请求出错时的处理逻辑

app.listen(3000, () => {
    console.log("start at port 3000!")
})

2)路由单独文件

first.js

const Router = require('koa-router')
const router = new Router()

router.get('/first/bar', async (ctx, next) => {
    ctx.body = '/first/bar'
})

router.get('/first/foo', async (ctx, next) => {
    ctx.body = '/first/foo'
})

module.exports = router

app.js

const fs = require("fs")
const Koa = require("koa")
const Router = require("koa-router")
const { promisify } = require("util")
const bodyparser = require("koa-bodyparser")
const cors = require('koa2-cors')
const firstRouter = require('./router/first')

const app = new Koa()

const router = new Router()
router.use(bodyparser())

app.use(async (ctx, next) => {
    try {
        await next()
    } catch (err) {
        ctx.body = err.message
        // ctx.app.emit("error", err, ctx)
    }
});

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

app.use(firstRouter.routes()).use(firstRouter.allowedMethods())

app.listen(3000, () => {
    console.log("start at port 3000!")
})

 3)路由拆分

list.js

const Router = require('koa-router')
const router = new Router()

router.get("/foo", async (ctx, next) => {
    ctx.body = "/list/foo"
})

router.get('/bar', async (ctx, next) => {
    ctx.body = '/list/bar'
})

module.exports = router

role.js

const Router = require('koa-router')
const router = new Router()

router.get('/bar', async (ctx, next) => {
    ctx.body = '/role/bar'
})

router.get('/foo', async (ctx, next) => {
    ctx.body = '/role/foo'
})

module.exports = router;

 app.js

const fs = require("fs")
const Koa = require("koa")
const Router = require("koa-router")
const { promisify } = require("util")
const bodyparser = require("koa-bodyparser")
const cors = require('koa2-cors')
const listRouter = require('./router/list')
const roleRouter = require('./router/role')

const app = new Koa()

const router = new Router()
router.use(bodyparser())

app.use(async (ctx, next) => {
    try {
        await next()
    } catch (err) {
        ctx.body = err.message
        // ctx.app.emit("error", err, ctx)
    }
});

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


router.use('/list', listRouter.routes()).use(listRouter.allowedMethods())
router.use('/role', roleRouter.routes()).use(roleRouter.allowedMethods())

app.listen(3000, () => {
    console.log("start at port 3000!")
})

12、文件上传: koa-better-body 、koa-convert

const Koa = require("koa");
const body = require("koa-better-body");
const convert = require("koa-convert");
const path = require("path");

const app= new Koa();

app.use(convert(
    body({
        uploadDir: path.resolve(__dirname, 'upload'),
        maxFileSize: 100*1024
        // 单位kb
    })
))

server.listen(8000, error => {
    console.log(error);
});

三、return next() 与 await next():

var http = require('http')
const Koa = require('koa')
const app = new Koa()

app.use(async(ctx,next)=>{
    console.log(1)
    await next()
    ctx.body = 'hello world0'
    console.log(2)
})
app.use(async(ctx,next)=>{
    console.log(3)
    ctx.body = 'hello world1'
    return next()
    console.log(4)
})
app.use(async(ctx,next)=>{
    console.log(5)
    ctx.body = 'hello world2'
    console.log(6)
})


http.createServer(app.callback()).listen(80, () => console.log(`listening on port 80`))
依次执行:
console.log( 1 )

console.log( 3 )
ctx.body = 'hello world1'

console.log( 5 )
ctx.body = 'hello world2'
console.log( 6 )

ctx.body = 'hello world0'
console.log( 2 )

所以最后浏览器输出:hello world0

四、Express VS Koa

https://segmentfault.com/a/1190000022859488

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值