node的框架koa,能帮我们做些什么

1,175 阅读13分钟

今天我们来简单地聊一聊node的一个框架,koa,它是node中一个非常轻量级的框架。官方文档对它的定义是:

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

我们一起来看看它封装了一些什么样的方法供我们使用,为我们减轻了哪些负担。

1. 原生node开发

那我们先来回顾一下不使用框架是怎么写node的,然后我们再使用koa框架写node。这样做一个对比,你就能更加清晰地认识到框架开发的好处。

那我们用node直接写后端应该先启动一个web服务,我们需要引入一个http模块。然后让这个模块生效,创建一个web服务。

const http = require('http');

http.createServer((req, res) => {
    console.log(req.headers);

}).listen(3000, () => {
    console.log('server is running');
})

这是最简单的创建一个服务的方式,用createServer方法创建一个服务、用listen监听3000端口。createServer接收两个参数,req是前端发来的请求体,res是后端的响应体。我们输出一下请求头看看。然后我们在自己的设备将这个服务运行起来,你就会看到:

image.png

此时这个服务已经在我们设备上运行起来了。哎,怎么没有看到输出语句的输出呢?这不是因为没有人向我们的这个服务发送请求嘛。

我们可以人为地向我们这个web服务发送一个请求,去做一个测试。我们可以使用postman去向我们这个服务发送请求,这个网站是专门用来做测试的,它可以向后端接口发送请求,看看这个接口是否能正常运行。

那我们用postman向我们的3000端口发送一个请求:

PixPin_2024-12-23_10-37-59.gif

好,我们已经发送了一个请求,下面一直在读条,如果我们写了响应体的话,在下方就能看到。

这时我们就能在终端看到输出结果了,输出了一个请求头:

image.png

那现在我们去写一个响应体看看:

const http = require('http');

http.createServer((req, res) => {
    res.end('后端返回数据 hello world')

}).listen(3000, () => {
    console.log('server is running');
})

我们重启一下后端,然后用postman发送一个请求:

image.png

这时就拿到了后端返回的数据,前端向后端发送请求,后端传回数据,一个最基本的交互。

那如果我想让前端向'/home'路径发送请求才能拿到数据应该怎么写呢?那就要去判断请求体的url了。

const http = require('http');

http.createServer((req, res) => {
    if (req.url === '/home') {
        // 做数据库的连接
        // 执行sql语句
        res.end('首页的数据')
    }
}).listen(3000, () => {
    console.log('server is running');
})

我们写了一个if语句做判断。这样只有当前端向'/home'路径发送请求时才能拿到数据。我们用postman来试试看:

PixPin_2024-12-23_11-01-26.png

看,访问'/home'路径时拿到了响应体的数据。

那如果还有一个接口呢,是不是可以接着else if判断啊,那如果有10个接口呢,100个接口呢?难道我们还继续写if判断语句去区分不同的请求体吗?那样是不是太麻烦了,既不美观又不实用。

原生node就只能这样写,但好在有人去做了封装,写了一些好用的方法,让我们可以不再去写这种很恶心的代码,这就是koa框架。

2. koa

那现在我们来聊聊用上koa框架是怎么写后端的。

那我们想用它先安装一下它,在终端输入一下指令安装:

npm install koa

安装完了之后我们就能使用它了,先引入一下,然后创建一个koa的实例对象。

const koa = require('koa');
const app = new koa();

然后怎么使用呢?直接app监听3000端口,接收一个回调函数。

const koa = require('koa');
const app = new koa();

app.listen(3000, () => {
    console.log('服务启动成功');
})

这就启动了一个服务,然后我们运行一下:

image.png

服务启动成功。

那我们想在koa中写一个响应体应该怎么写呢?

我们这样写,我们会定义一个函数,main。然后让实例对象app use掉它。

const koa = require('koa');
const app = new koa();

const main = (ctx) => {

}
app.use(main)

app.listen(3000, () => {
    console.log('服务启动成功');
})

koa框架就是主打一个函数化编程,你想实现一个什么功能就自己定义一个函数,然后让app use它。

app这个实例对象会帮我们调用我们自己写的这个函数main,main函数就会被赋予一个参数,这个参数可以随便叫,我们习惯于叫ctx,上下文对象。

ctx是什么呢?它是koa提供的上下文对象,包含了 request 和 response 两个对象,也就是原生node的请求体和响应体。

我们可以输出一下ctx.url看看。

const koa = require('koa');
const app = new koa();

const main = (ctx) => {
    console.log(ctx.url);
}
app.use(main)

app.listen(3000, () => {
    console.log('服务启动成功');
})

我们用postman向3000端口发送请求。

image.png

一个斜杠,说明输出了根路径。因为我们就是向根路径发送了请求,所以ctx.url就是根路径。这个ctx集req和res于一体。

我们再用它来写一个响应体:

const koa = require('koa');
const app = new koa();

const main = (ctx) => {
    ctx.body = 'Hello world';
}
app.use(main)

app.listen(3000, () => {
    console.log('服务启动成功');
})

直接ctx.body为响应的内容,就不再是用原生node的end方法了。

我们用postman发送一个请求。

image.png

你看,前端就拿到了响应的内容。

我们还可以这样写,向前端返回一个html的标签,比如:

const koa = require('koa');
const app = new koa();

const main = (ctx) => {
    ctx.body = '<h2>hello</h2>'
}
app.use(main)

app.listen(3000, () => {
    console.log('服务启动成功');
})

你说这时前端会把它当作字符串来看还是当作h2标签来看呢?

我们用浏览器访问一下3000端口来看看:

image.png

它就直接当作h2标签来看了。那如果我就是想把它当作字符串呢?那我们就要这么写:

const koa = require('koa');
const app = new koa();

const main = (ctx) => {
    ctx.response.type = 'text'
    ctx.body = '<h2>hello</h2>'
}
app.use(main)

app.listen(3000, () => {
    console.log('服务启动成功');
})

我们提前设置一下响应头的类型为text,告诉浏览器我们想把它当作字符串。那浏览器就会真的把它当作字符串了。

image.png

你看,输出的就是一个字符串了。如果还是想把它当作html标签的话,就ctx.response.type = 'html'就行了。

那我们就可以这样写,当用户访问'/home'路径时,我们给他返回一个页面。

我们提前准备一个html文件,当用户访问'/home'路径时,就读取这个html文件。那要对文件进行操作,就要引入node的一个模块,fs。这个模块可以让js对文件进行操作。

const koa = require('koa');
const fs = require('fs');
const app = new koa();

const main = (ctx) => {
    if (ctx.url === '/home') {
        ctx.response.type = 'html'
        ctx.body = fs.readFileSync('./assets/template.html', 'utf8')
    }
}
app.use(main)

app.listen(3000, () => {
    console.log('服务启动成功');
})

其实这就是前后端不分离的写法。在之前,前端和后端是不分离的,都由后端来完成,前端写好了html文件后,就打包发给后端,后端使用他们的编程语言把html文件拿到页面上展示。

使用fs模块自带的readFileSync方法去读取html文件。

所以我们来看一下效果,当用户访问'/home'路径时,应该给他展示我们准备的页面。

image.png

成功展示了页面。

那如果有很多个页面要展示呢?还要去写很多个if else语句去判断吗?当然不用,koa给我们封装了好用的方法。

那我们现在要写两个接口,一个首页的接口,一个登陆的接口。

那首先我们要装一个插件了,koa-router。我们先在项目中输入以下指令安装:

npm install @koa/router

装好之后我们引入一下它:

const Koa = require('koa')
const app = new Koa()
const Router = require('@koa/router')
const router = new Router()

// 生效路由
app.use(router.routes())

app.listen(3000, () => {
    console.log('服务启动成功')
})

创建一个路由的实例对象router,然后让app.use(router.routes())使路由生效。

那接下来我们就能去写我们的接口了。

const Koa = require('koa')
const app = new Koa()
const Router = require('@koa/router')
const router = new Router()

// 首页的接口
// 登录页面的接口
router.get('/home', (ctx, next) => {
    ctx.body = {
        code: 200,
        msg: 'success',
        data: {
            name: '张三',
            age: 20,
        }
    }
})

// 生效路由
app.use(router.routes())

app.listen(3000, () => {
    console.log('服务启动成功')
})

我们随便给'/home'接口添加一些数据,现在我们去访问'/home'路径应该就能拿到这个对象了。

image.png

看,前端就拿到了这份数据。那后端能不能接收到前端传过来的参数呢?假设前端向后端传了一个参数id,拼在了url上。那我们就去这样写:

const Koa = require('koa')
const app = new Koa()
const Router = require('@koa/router')
const router = new Router()

// 首页的接口
// 登录页面的接口
router.get('/home', (ctx, next) => {
    console.log(ctx.query);
    const { id } = ctx.query  // get 请求

    ctx.body = {
        code: 200,
        msg: 'success',
        data: {
            name: '张三',
            age: 20,
            id
        }
    }
})

// 生效路由
app.use(router.routes())

app.listen(3000, () => {
    console.log('服务启动成功')
})

image.png

image.png

前端向后端传来了一个参数id值为123,我们需要拼在url路径上。我们可以输出ctx.query看看,是一个对象,我们把id从对象中解构传来,原封不动的传给前端。

后端是能接收到前端传来的参数的。

那我们再来写一个登陆接口,这个登陆接口需要发送post请求才能访问,我们就会这样写:

const Koa = require('koa')
const app = new Koa()
const Router = require('@koa/router')
const router = new Router()

// 首页的接口
// 登录页面的接口
router.get('/home', (ctx, next) => {
    console.log(ctx.query);
    const { id } = ctx.query  // get 请求

    ctx.body = {
        code: 200,
        msg: 'success',
        data: {
            name: '张三',
            age: 20,
            id
        }
    }
})

router.post('/login', (ctx) => {
    console.log(ctx.request.body);
})

// 生效路由
app.use(router.routes())

app.listen(3000, () => {
    console.log('服务启动成功')
})

get请求前端向后端传过来的参数直接拼在url路径上。在post请求中,前端给后端传过来的参数是会放在请求体里的,我们输出一下请求体看看。

我们使用postman向'/login'路径发送一个post请求,并携带两个参数。

image.png

post请求的参数是会放在响应体里的,所以我们不能拼接在url路径上,在postman上我们可以选中Body模块,在这里我们可以模拟前端向后端传了参数。

然后我们看看输出结果:

image.png

我们发现是undefined。这是因为koa没有办法直接解析post请求传来的参数,所以我们还要安装一个插件,这个插件可以帮我们解析post请求传来的参数。

我们在项目的终端输入以下指令安装一个工具:

npm i @koa/bodyparser

安装完成之后引入它,并使用app use掉它。

const Koa = require('koa')
const app = new Koa()
const Router = require('@koa/router')
const router = new Router()
const { bodyParser } = require('@koa/bodyparser')

// 解析请求体
app.use(bodyParser())

// 首页的接口
// 登录页面的接口
router.get('/home', (ctx, next) => {
    console.log(ctx.query);
    const { id } = ctx.query  // get 请求

    ctx.body = {
        code: 200,
        msg: 'success',
        data: {
            name: '张三',
            age: 20,
            id
        }
    }
})

router.post('/login', (ctx) => {
    console.log(ctx.request.body);
})

// 生效路由
app.use(router.routes())

app.listen(3000, () => {
    console.log('服务启动成功')
})

这样koa就能读懂post请求传来的参数了。我们再用postman发送一次post请求:

image.png

后端成功拿到了前端传过来的参数。

那我们随便在'/login'的响应体上写点数据:

const Koa = require('koa')
const app = new Koa()
const Router = require('@koa/router')
const router = new Router()
const { bodyParser } = require('@koa/bodyparser')

// 解析请求体
app.use(bodyParser())

// 首页的接口
// 登录页面的接口
router.get('/home', (ctx, next) => {
    console.log(ctx.query);
    const { id } = ctx.query  // get 请求

    ctx.body = {
        code: 200,
        msg: 'success',
        data: {
            name: '张三',
            age: 20,
            id
        }
    }
})

router.post('/login', (ctx) => {
    console.log(ctx.request.body);
    ctx.body = {
        code: 200,
        msg: '登录成功',
        user: ctx.request.body.username
    }
})

// 生效路由
app.use(router.routes())

app.listen(3000, () => {
    console.log('服务启动成功')
})

现在我们用postman访问'/login'路径应该就能拿到这份数据了。

image.png

那如果我们想继续写接口的话,直接router并列着去写,就不用去写一堆if语句了,就更实用更优雅。

3. 洋葱模型

那聊到koa就不得不聊到洋葱模型了。

在聊洋葱模型之前我们需要先聊一下中间件。中间件其实就是一个函数,在koa中我们自己定义的、去实现某个功能的函数,被app use调用了之后它就是一个中间件。比如当时我们自己定义的main函数。

而中间件函数是有执行顺序的,它的执行顺序就是遵照洋葱模型。

我们来举个例子看看:

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

const one = (ctx, next) => {
    console.log(1);
    next() 
    console.log(2);
}
const two = (ctx, next) => {
    console.log(3);
    next()
    console.log(4);
}
const three = (ctx, next) => {
    console.log(5);
    next()
    console.log(6);
}

app.use(one)
app.use(two)
app.use(three)

app.listen(3000)

我们写了3个中间件函数one、two和three,分别用app use掉了它们。app就会赋予这三个函数两个参数,一个是ctx,我们已经讲过的;还有一个是next,这个是next是干吗用的呢?我们在每个中间件函数中都调用了它。请问此时六条输出语句的执行顺序是什么?

会是1、2、3、4、5、6吗?我们来试一下:

image.png

你看是1、3、5、6、4、2。这是为什么呢?这是因为当中间件函数碰到next函数的调用时它直接会调转到下一个中间件函数去调用。所以它在输出完1之后碰到了next函数调用,于是它就跳到two函数去输出了3,又碰到next函数于是又跳到three函数去输出了5,又碰到了next函数,但因为此时没有下一个中间件函数了,它就输出6.此时最后一个中间件函数执行完了,它就会返回上一个没有执行完的中间件函数继续执行,因为上一个中间件函数是执行当一半去执行下一个中间件函数了,所以接着输出4,最后跑回第一个中间件函数执行输出2。

这就是洋葱模型了,就像我们用一根筷子插进一个洋葱,它就是先从外到里,再从里到外。有点像递归的调用吧。

如果我们不写next的调用这三个函数会怎么执行呢?

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

const one = (ctx, next) => {
    console.log(1);
    //next() // 直接调用下一个中间件
    console.log(2);
}
const two = (ctx, next) => {
    console.log(3);
    //next()
    console.log(4);
}
const three = (ctx, next) => {
    console.log(5);
    //next()
    console.log(6);
}

app.use(one)
app.use(two)
app.use(three)

app.listen(3000)

image.png

你看它就只会执行第一个中间件函数,其它中间件函数就不会执行。所以next能让我们人为地去控制中间件函数地调用顺序。

4. 总结

本次我们一起简单地学习了一下node的框架koa,它是一个轻量级的框架,能帮我们减轻很多繁琐的操作。还讲解了一下洋葱模型。

如果对你有帮助的话请点个赞吧!