Node.js系列八 - koa开发web服务器

1、 Koa初体验

1.1. 认识Koa

Koa官方的介绍:

  • koa:next generation web framework for node.js;
  • koa:node.js的下一代web框架;

事实上,koa和express是同一个团队开发的一个新的Web框架:

  • 目前团队的核心开发者TJ的主要精力也在维护Koa,express已经交给团队维护了;
  • Koa旨在为Web应用程序和API提供更小、更丰富和更强大的能力;
  • koa相对于express具有更强的异步处理能力
  • Koa的核心代码只有1600+行,是一个更加轻量级的框架,我们可以根据需要安装和使用中间件;

1.2. koa初体验

koa与express的基本开发模式是比较相似的

// koa 的Web服务器
const Koa = require('koa');

const app = new Koa();

app.use((ctx, next) => {
  console.log("middleware 01");
  next();
})

app.use((ctx, next) => {
  console.log("middleware 02");
  ctx.response.body = "Hello World";
})


app.listen(8000, () => {
  console.log("服务器启动成功~");
});

koa注册的中间件提供了两个参数:

  • ctx:上下文(Context)对象;
    • koa并没有像express一样,将req和res分开,而是将它们作为ctx的属性;
    • ctx代表每次请求的上下文对象;
    • ctx.request:获取请求对象;
    • ctx.response:获取响应对象;
  • next:本质上是一个dispatch,类似于express的next;

koa通过创建的app对象,注册中间件只能通过use方法:

  • Koa并没有提供methods的方式来注册中间件;
  • 也没有提供path中间件来匹配路径;

但是真实开发中我们如何将路径和method分离呢?

  • 方式一:根据request自己来判断;
  • 方式二:使用第三方路由中间件;
// 方式一:根据request自己判断
app.use((ctx, next) => {
  if (ctx.request.path === '/users') {
    if (ctx.request.method === 'POST') {
      ctx.response.body = "Create User Success~";
    } else {
      ctx.response.body = "Users List~";
    }
  } else {
    ctx.response.body = "Other Request Response";
  }
})

整个代码的逻辑是非常复杂和混乱的,真实开发中我们会使用路由。


1.3. 路由的使用

koa官方并没有给我们提供路由的库,我们可以选择第三方库:koa-router

安装koa-router

npm install koa-router

koa-router基本使用

// user.router.js
const Router = require('koa-router');

const userRouter = new Router();

userRouter.get('/users', (ctx, next) => {
  ctx.response.body = "user list~";
});

userRouter.post('/users', (ctx, next) => {
  ctx.response.body = "create user info~";
});

module.exports = userRouter;

在app中将router.routes()注册为中间件:

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

注意 : allowedMethods用于判断某一个method是否支持:

  • 如果我们请求 get,那么是正常的请求,因为我们有实现get;
  • 如果我们请求 put、delete、patch,那么就自动报错:Method Not Allowed,状态码:405;
  • 如果我们请求 link、copy、lock,那么就自动报错:Not Implemented,状态码:501;.

router的前缀

通常一个路由对象是对一组相似路径的封装,那么路径的前缀都是一直的,所以我们可以直接在创建Router时,添加前缀:

const userRouter = new Router({ prefix: '/users' });

userRouter.get('/', (ctx, next) => {
  ctx.response.body = "user list~";
});

userRouter.post('/', (ctx, next) => {
  ctx.response.body = "create user info~";
});

module.exports = userRouter;

1.4. 请求解析

常见的客户端传递到服务器参数的5种方法:

  • 方式一:通过get请求中的URL的params;
  • 方式二:通过get请求中的URL的query;
  • 方式三:通过post请求中的body的json格式;
  • 方式四:通过post请求中的body的x-www-form-urlencoded格式;
  • 方式五:通过post请求中的form-data格式;

方式一:params

请求地址:http://localhost:8000/users/123

获取params:

const userRouter = new Router({prefix: "/users"})

userRouter.get("/:id", (ctx, next) => {
  console.log(ctx.params.id);
  ctx.body = "Hello World";
})

方式二:query

请求地址:http://localhost:8000/login?username=why&password=123

获取query:

app.use((ctx, next) => {
  console.log(ctx.request.query);
  ctx.body = "Hello World";
})

方式三:json

请求地址:http://localhost:8000/login

body是json格式:

{
    "username": "Tom",
    "password": "123456"
}

获取json数据:

  • 安装依赖:npm install koa-bodyparser;
  • 使用 koa-bodyparser的中间件;
app.use(bodyParser());

方式四:x-www-form-urlencoded

请求地址:http://localhost:8000/login

body是x-www-form-urlencoded格式:

获取json数据:(和json是一致的)

  • 安装依赖:npm install koa-bodyparser;
  • 使用 koa-bodyparser的中间件;
app.use(bodyParser());

方式五:form-data

请求地址:http://localhost:8000/login

body是form-data格式:

解析body中的数据,我们需要使用multer

  • 安装依赖:npm install koa-multer;
  • 使用 multer中间件;
const upload = multer({});

app.use(upload.any());

app.use((ctx, next) => {
  console.log(ctx.req.body);
  ctx.body = "Hello World";
});

multer还可以实现文件的上传:

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "./uploads/")
  },
  filename: (req, file, cb) => {
    cb(null, Date.now() + path.extname(file.originalname))
  }
})

const upload = multer({
  storage
});

const fileRouter = new Router();

fileRouter.post("/upload", upload.single('avatar'), (ctx, next) => {
  console.log(ctx.req.file);
})

app.use(fileRouter.routes());


1.5. 响应方式

输出结果:body

将响应主体设置为以下之一:

  • string :字符串数据
  • Buffer :Buffer数据
  • Stream :流数据
  • Object|| Array:对象或者数组
  • null :不输出任何内容

如果response.status尚未设置,Koa会自动将状态设置为200或204。

比较常见的输出方式:

ctx.response.body = "Hello World";
ctx.body = {
  name: "Tom",
  age: 18,
  height: 1.88
};
ctx.body = ["abc", "cba", "nba"];

疑惑:ctx.response.body和ctx.body之间的区别:

  • 事实上,我们访问ctx.body时,本质上是访问ctx.response.body;

请求状态:status

请求状态我们可以直接给ctx设置,或者给ctx.response设置也是一样的效果:

ctx.status = 201;
ctx.response.status = 204;

1.6. 错误处理

const Koa = require('koa');

const app = new Koa();

app.use((ctx, next) => {
  ctx.app.emit('error', new Error("出错啦"), ctx);
})

app.on('error', (err, ctx) => {
  console.log(err.message);
  ctx.response.body = "出错啦";
})

app.listen(8000, () => {
  console.log("错误处理服务启动成功~");
})

1.7. 静态服务器

koa并没有内置部署相关的功能,所以我们需要使用第三方库:

npm install koa-static

部署的过程类似于express:

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

const app = new Koa();

app.use(static('./build'));

app.listen(8000, () => {
  console.log("静态服务器启动成功~");
});

2、koa 与 express的比较

在学习了两个框架之后,我们应该已经可以发现koa和express的区别:

从架构设计上来说:

  • express是完整和强大的,其中帮助我们内置了非常多好用的功能;
  • koa是简洁和自由的,它只包含最新的功能,并不会对我们使用其他中间件进行任何的限制。
    • 甚至是在app中连最基本的get、post都没有给我们提供;
    • 我们需要通过自己或者路由来判断请求方式或者其他功能;

因为express和koa框架他们的核心其实都是中间件:

  • 但是事实上它们的中间件的执行机制是不同的,特别是针对某个中间件中包含异步操作时;

  • 所以,接下来,我们再来研究一下express和koa中间件的执行顺序问题;

  • 当中间件中没有异步操作时,express中间件 与 koa中间件 的执行顺序其实相同,执行结果也相同

  • 但是当中间件中包含一步操作时express 中间件实现是基于 Callback 回调函数同步的,它不会去等待异步(Promise)完成

  • Koa 使用的是一个洋葱模型,它的一个特点是级联,通过 await next() 控制调用 “下游” 中间件,直到 “下游” 没有中间件且堆栈执行完毕,最终在流回 “上游” 中间件。

  • 同步执行

const express = require('express');

const app = express();

const middleware1 = (req, res, next) => {
  req.message = "middleware1";
  next();
  res.end(req.message);
}

const middleware2 = (req, res, next) => {
  req.message = req.message + 'middleware2';
  next();
}

const middleware3 = (req, res, next) => {
  req.message = req.message + 'middleware3';
}

app.use(middleware1, middleware2, middleware3);

app.listen(8000, () => {
  console.log("启动成功~");
})

// middleware1middleware2middleware3
const Koa = require('koa');

const app = new Koa();

const middleware1 = (ctx, next) => {
  ctx.message = "middleware1";
  next();
  ctx.body = ctx.message;
}

const middleware2 = (ctx, next) => {
  ctx.message = ctx.message + 'middleware2';
  next();
}

const middleware3 = (ctx, next) => {
  ctx.message = ctx.message + 'middleware3';
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);

app.listen(8001, () => {
  console.log("启动成功~");
})

// middleware1middleware2middleware3
  • 异步操作
const express = require('express');
const axios = require('axios');
const app = express();

const middleware1 = async (req, res, next) => {
  req.message = "middleware1";
  await next();
  res.end(req.message);
}

const middleware2 = async (req, res, next) => {
  req.message = req.message + 'middleware2';
  await next();
}

const middleware3 = async (req, res, next) => {
  const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876');
  req.message = req.message + result.data.lrc.lyric;
  console.log(req.message);
}

app.use(middleware1, middleware2, middleware3);

app.listen(8000, () => {
  console.log("启动成功~");
})

// middleware1middleware2 
// express 中 next()本身是一个同步函数 不等待异步请求的返回
// express 底层是不支持Async/Await 的
const Koa = require('koa');
const axios = require('axios');
const app = new Koa();

const middleware1 = async (ctx, next) => {
  ctx.message = "middleware1";
  await next();
  ctx.body = ctx.message;
}

const middleware2 = async (ctx, next) => {
  ctx.message = ctx.message + 'middleware2';
  await next();
}

const middleware3 = async (ctx, next) => {
  const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876');
  console.log(result)
  ctx.message = ctx.message + result.data.lrc.lyric;
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);

app.listen(8001, () => {
  console.log("启动成功~");
})

// Koa2 底层已经原生支持Async/Await 

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容