这里主要通过Koa官方文档推荐的一个项目来学 Koa

—— Koa文档)
—— Koa官方文档推荐的学习项目
image.png

这个项目还是比较老旧的教程,它里面使用的还是Koa1.0(2014年)时代的东西。
不过我这里用新的语法来完成它的课程。
找了下网上,没有发现有它的题目答案,是太简单了,都不屑做吗?官网竟然也不提供个答案的分支。。。

首先来讲解一下它的教学思路:
生成性学习!

不得不说,西方的这种教学方式确实是好。与《认知天性》的学习方式不谋而合。 想起我们初高中的早读课,大多是一些有意义但是作用较小的复读。 我写这片文档,也是想用一种,生成性笔记的方式,来巩固我学习的知识。

Koa简介
image.png

  • Koa是流式操作的(这样的类型有很多了,我们能想到gulp的打包也是流式的,jQuery的链式思想也很像,linux命令中的grep)
  • 只有通用的方法被集成到Koa项目里(它想表达Koa是小的)
  • Koa没有绑定中间件(它想表达Koa是纯粹的,不像Express那样打包了一堆东西)

image.png
其他的几个项目也该好好地看下

Koa推荐项目学习(https://github.com/koajs/workshop

该项目每一章会有一个介绍,然后出题。 每一章都会有一个写好的测试js,通过Mocha实现。 生成性学习——自己写代码,完成题目,它是没有给出答案的,答案需要由你来完成。

看懂下面的每一题、每个答案,你首先还需要知道Mocha(前端-单元测试)是怎么使用的。

以下是你必须要知道的,也是本文档的一些用法。

  • npm安装mocha和koa以及文档其他特殊用到的一些包。
  • 考卷代码创建 test.js,用 mocha . 就能跑起来了测试了。
  • 答案代码创建 index.js,然后写下你的答案。

image.png
image.png

01 - 关于co

  • 介绍

    想要完整地理解koa,你需要理解co的工作方式。 co使用ES6的generator特性。

  • 考卷(测试用例,测试驱动开发):实现函数满足以下要求的函数

    • 获取当前文件的文件大小
    • 读取不存在的文件的时候,需要报错
    • 校验一个文件是否存在
    • 校验一个文件是否不存在 ```

var co = require(‘co’); var assert = require(‘assert’);

var fs = require(‘./index.js’);

describe(‘.stats()’, function () { it(‘should stat this file’, co(function* () { var stats = yield fs.stat(__filename); assert.ok(stats.size); }))

it(‘should throw on a nonexistent file’, co(function* () { try { yield fs.stat(__filename + ‘lkjaslkdjflaksdfasdf’); // the above expression should throw, // so this error will never be thrown throw new Error(‘nope’); } catch (err) { assert(err.message !== ‘nope’); } })) })

describe(‘.exists()’, function () { it(‘should find this file’, co(function* () { assert.equal(true, yield fs.exists(__filename)) }))

it(‘should return false for a nonexistent file’, co(function* () { assert.equal(false, yield fs.exists(__filename + ‘kljalskjdfklajsdf’)) })) })

  1. - 答案
  2. 先来学习下fs.stat [Node-fs]([http://nodejs.cn/api/fs.html](http://nodejs.cn/api/fs.html))<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/555873/1615642301648-b863f66c-030b-4658-bb69-d36d5784301e.png#align=left&display=inline&height=175&margin=%5Bobject%20Object%5D&name=image.png&originHeight=349&originWidth=1182&size=30345&status=done&style=none&width=591)<br />明白了 stat 的用法就好写了,同时我们还要理解 co (ES6-generator的用法,这里我本人确实没细学generator,我直接把他看做 async/await了)

var fs = require(‘fs’); exports.stat = function (filename) { return new Promise((resolve) => { fs.stat(filename, (err, res) => { resolve(res) }) }) };

  1. 然后是第二个测试用例,我们需要把异常抛出去!!<br />这里我体会到单元测试mocha的好处了(不过前端的单元测试就不知道是否必要了,后端这类接口确实是经常需要单元测试)

var fs = require(‘fs’); exports.stat = function (filename) { return new Promise((resolve, reject) => { fs.stat(filename, (err, res) => { if (err) { reject(err) } else { resolve(res) } }) }) };

  1. 第三、四题我们还是可以用 fs.stat 实现,据说 fs.exist已经被弃用了(写文时间2021-03-13

var fs = require(‘fs’); exports.stat = function (filename) { return new Promise((resolve, reject) => { fs.stat(filename, (err, res) => { if (err) { reject(err) } else { resolve(res) } }) }) };

exports.exists = function (filename) { return new Promise((resolve, reject) => { fs.stat(filename, (err, res) => { if (err) { resolve(false) } else { resolve(true) } }) }) };

  1. <a name="OFhHe"></a>
  2. #### 02 - koa的hello,world
  3. - 简介
  4. > Koa使用 getter/setter 的方式书写。
  5. > app.use(function* () {
  6. > this.response.body = 'hello world';
  7. > })
  8. - 考卷
  9. - 接口返回内容长度为 11
  10. - 接口返回指定的编码格式为 text/plain; charset=utf-8

var request = require(‘supertest’); var testKoa = require(‘./index.js’);

describe(‘Hello World’, function () { it(‘should return hello world’, function (done) { request(testKoa.listen()) .get(‘/‘) .expect(200) .expect(‘Content-Length’, ‘11’) .expect(‘Content-Type’, ‘text/plain; charset=utf-8’) .end(done) }) })

  1. - 答案

var Koa = require(‘koa’); var app = new Koa()

app.use(async (ctx) => { ctx.status = 200 ctx.body = ‘hello world’ });

module.exports = app

  1. 这里我们主要是要理解Koa的书写方式(getter/setter),不用像Express那样,res.send('我是返回内容'),而是直接 ctx.body = '我是返回内容'.
  2. <a name="3VO2x"></a>
  3. #### 03 - routing
  4. - 简介
  5. > Koa不像Express,使用了router类的第三方插件,express写法 router.get('/path'),koa是淳朴的(我想也许这不太方便!不过这给了我们灵活的自由度,我们可以自己用第三方的,可选的第三方)。
  6. - 考卷
  7. - / 路由,返回 hello,world
  8. - 404 路由, 返回 page not found
  9. - 500 路由,返回 internal server error

var request = require(‘supertest’); var app = require(‘./index.js’);

describe(‘Routing’, function () { it(‘GET / should return “hello world”‘, function (done) { request(app.listen()) .get(‘/‘) .expect(‘hello world’, done); })

it(‘GET /404 should return “page not found”‘, function (done) { request(app.listen()) .get(‘/404’) .expect(‘page not found’, done); })

it(‘get /500 should return “internal server error”‘, function (done) { request(app.listen()) .get(‘/500’) .expect(‘internal server error’, done); }) })

  1. - 答案

const koa = require(‘koa’); var app = module.exports = new koa();

app.use(async ctx => { if (ctx.request.url === ‘/‘) { ctx.body = ‘hello world’ } else if (ctx.request.url === ‘/404’) { ctx.body = ‘page not found’ } else if (ctx.request.url === ‘/500’) { ctx.body = ‘internal server error’ } });

  1. 可以看到,koa的这种写法确实是很淳朴。<br />也确实使用 getter/setter 方式来写的。<br />对于以前写 Express 的写法还是很不相同的。<br />不过我感觉项目一大,该用 router 还是要用 router
  2. <a name="qO7Mq"></a>
  3. #### 04 - bodies
  4. - 简介
  5. > 很久以前,我们使用“字符串”作为返回体,Koa支持以下的格式:字符串、BuffersStreamsJSON Object
  6. > 如果没有指定具体内容,Koa默认会使用 JSON
  7. 关于BuffersStreams更多的学习,摘自网络。他们是相关的!!<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/555873/1615644570505-6845323a-f5df-435d-8f1b-e9cae8d3d968.png#align=left&display=inline&height=345&margin=%5Bobject%20Object%5D&name=image.png&originHeight=689&originWidth=1002&size=115712&status=done&style=none&width=501)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/555873/1615644589832-95921036-d384-4996-b9fd-536690eee296.png#align=left&display=inline&height=131&margin=%5Bobject%20Object%5D&name=image.png&originHeight=262&originWidth=931&size=47456&status=done&style=none&width=465.5)
  8. - 考卷
  9. - 对于 /stream 路由,接口返回一个stream流响应
  10. - 对于 /json 路由,接口返回一个body响应

var fs = require(‘fs’); var path = require(‘path’); var assert = require(‘assert’); var request = require(‘supertest’);

var app = require(‘./index.js’);

describe(‘Bodies’, function () { it(‘GET /stream should return a stream’, function (done) { var body = fs.readFileSync(path.join(__dirname, ‘index.js’), ‘utf8’);

  1. request(app.listen())
  2. .get('/stream')
  3. .expect('Content-Type', /application\/javascript/)
  4. .expect(body, done);

})

it(‘GET /json should return a JSON body’, function (done) { request(app.listen()) .get(‘/json’) .expect(‘Content-Type’, /application\/json/) .end(function (err, res) { if (err) return done(err);

  1. assert.equal(res.body.message, 'hello world');
  2. done();
  3. })

}) })

  1. - 答案

var fs = require(‘fs’); var koa = require(‘koa’); const path = require(‘path’)

var app = module.exports = new koa();

/**

  • Create the GET /stream route that streams this file.
  • In node.js, the current file is available as a variable __filename. */

app.use(async (ctx, next) => { if (ctx.request.path === ‘/stream’) { ctx.body = fs.readFileSync(path.join(__dirname, ‘index.js’), ‘utf8’); ctx.set(‘Content-Type’, ‘application/javascript’) } else { next() } });

/**

  • Create the GET /json route that sends {message:'hello world'}. */

app.use(async (ctx, next) => { if (ctx.request.path === ‘/json’) { ctx.body = { message: ‘hello world’ } } else { next() } });

  1. <a name="0ijao"></a>
  2. #### 05 - error handing
  3. - 简介
  4. > koa的错误处理大多使用 try-catch 来完成(除了event emitters)
  5. - 考卷
  6. - 对于 / 路由,接口返回状态为 500
  7. - 对于 / 路由,抛出一个异常

var request = require(‘supertest’);

var app = require(‘./index.js’);

describe(‘Error Handling’, function () { it(‘should return “internal server error”‘, function (done) { request(app.listen()) .get(‘/‘) .expect(500) .expect(‘internal server error’, done); })

it(‘should emit an error event’, function (done) { app.once(‘error’, function () { done(); });

  1. request(app.listen())
  2. .get('/')
  3. .end(function noop(){});

}) })

  1. - 答案
  2. 首先这里我学到了他们的执行顺序,也是所谓的“洋葱模型”的入门开始<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/555873/1615648394698-f4df6998-a9ae-49b5-96be-c42914a8e2fd.png#align=left&display=inline&height=186&margin=%5Bobject%20Object%5D&name=image.png&originHeight=371&originWidth=499&size=21432&status=done&style=none&width=249.5)<br />顺序为 1 -> 2 -> 3 -> 4<br />这题我用了2小时。<br />这里非常坑爹的是:500的默认错误是 'Internal Server Error',它是首字母大写的,而这里的要求是返回小写的。我就一直琢磨为啥会这样,同时又要求把错误上报。<br />最后得知了 app.emit 函数是可以强制上报的。<br />同时!!它的第三个参数为此时的返回 Body,是可以自定状态、和错误信息的!

var koa = require(‘koa’); var app = module.exports = new koa();

app.use(async (ctx, next) => { try { await next() } catch (error) { ctx.status = 500 ctx.message = ‘internal server error’ app.emit(‘error’, error, ctx) } })

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

  1. 如果不对返回body特殊处理,它会为 'Internal Server Error',注意,大写的这个并不符合我们题目的要求。
  2. <a name="tucXQ"></a>
  3. #### 06 - content-negotiation
  4. - 简介
  5. > 内容协商是http重要的内容,Accept-EncodingContent-Encoding协商是否、如何来压缩返回体的内容
  6. - 考卷
  7. - 当请求gzip,则返回gzip压缩方式的内容,内容为 hello world
  8. - 当为identity,则不压缩

var assert = require(‘assert’); var request = require(‘supertest’);

var app = require(‘./index.js’);

describe(‘Content Negotiation’, function () { it(‘when “Accept: gzip” it should return gzip’, function (done) { request(app.listen()) .get(‘/‘) .set(‘Accept-Encoding’, ‘gzip’) .expect(200) .expect(‘Content-Encoding’, ‘gzip’) .expect(‘hello world’, done); })

it(‘when “Accept: identity” it should not compress’, function (done) { request(app.listen()) .get(‘/‘) .set(‘Accept-Encoding’, ‘identity’) .expect(200) .expect(‘hello world’, function (err, res) { if (err) return done(err);

  1. assert.equal(res.headers['content-encoding'] || 'identity', 'identity');
  2. done();
  3. });

}) })

  1. - 答案
  2. 耗时间 1.5 小时,<br />!!!和上题一样,网上完全找不到答案。。。果然真正用 Koa 写服务器的也太少了,都用了统一封装的第三方,纯粹的简单压缩反而没有人使用了(特别是到了 koa2,旧的方案没用了)

const koa = require(‘koa’); const zlib = require(‘zlib’) const app = module.exports = new koa();

app.use(async (ctx, next) => { if (ctx.request.path === ‘/‘) { if (ctx.acceptsEncodings(‘gzip’)) { ctx.status = 200 ctx.set(‘content-encoding’, ‘gzip’) ctx.body = ‘hello world’ ctx.gzip = true } else if (ctx.acceptsEncodings(‘identity’)) { ctx.status = 200 ctx.set(‘Content-Encoding’, ‘identity’) ctx.body = ‘hello world’ } } await next()

})

app.use(gzip = async (ctx) => { if (ctx.gzip) { await (async () => { return new Promise(resolve => { zlib.deflate(new Buffer(ctx.body), (err, buffer) => { ctx.body = buffer resolve(buffer) }) }) })() } })

  1. 本题精髓,关于 zlib 的应用,其压缩 gzip 有两种方案
  2. - 1、用一个步骤完成。输入流文件(new Buffer()),输出gzip压缩后的结果,我使用的是第一种!!
  3. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/555873/1615661453831-20b84bc5-dbdf-42c7-b53e-45c54877e174.png#align=left&display=inline&height=361&margin=%5Bobject%20Object%5D&name=image.png&originHeight=721&originWidth=596&size=37728&status=done&style=none&width=298)
  4. - 2、压缩或者解压流(例如一个文件)通过 `zlib` `Transform` 流将源数据流传输到目标流中来完成。
  5. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/555873/1615661633077-7057a0db-0213-411a-8364-3c6019f39f65.png#align=left&display=inline&height=400&margin=%5Bobject%20Object%5D&name=image.png&originHeight=800&originWidth=641&size=46985&status=done&style=none&width=320.5)
  6. <a name="C64oj"></a>
  7. #### 07 - content-headers
  8. - 简介
  9. > 请求头、响应头都有很多参数。包括不限于content-type/content-length/content-encoding。学习使用this.request.is()。
  10. - 考卷

var assert = require(‘assert’); var request = require(‘supertest’);

var app = require(‘./index.js’);

describe(‘Content Headers’, function () { describe(‘when sending plain text’, function () { it(‘should return “ok”‘, function (done) { request(app.listen()) .get(‘/‘) .set(‘Content-Type’, ‘text/plain’) .set(‘Content-Length’, ‘3’) .send(‘lol’) .expect(200) .expect(‘ok’, done); }) })

describe(‘when sending JSON’, function () { it(‘should return that JSON’, function (done) { // just a random JSON body. don’t bother parsing this. var body = JSON.stringify({});

  1. request(app.listen())
  2. .get('/')
  3. .set('Content-Type', 'application/json; charset=utf-8')
  4. .set('Content-Length', Buffer.byteLength(body))
  5. .send(body)
  6. .expect(200)
  7. .expect('Content-Type', /application\/json/)
  8. .expect('Content-Length', '17')
  9. .end(function (err, res) {
  10. if (err) return done(err);
  11. assert.equal('hi!', res.body.message);
  12. done();
  13. })
  14. })

}) })

  1. - 答案

const koa = require(‘koa’); const app = module.exports = new koa();

app.use(async (ctx) => { if (ctx.request.is(‘text/plain’)) { ctx.status = 200 ctx.body = ‘ok’ } else if (ctx.request.is(‘application/json’)) { ctx.body = { message: ‘hi!’ } ctx.status = 200 ctx.length = 17 } })

  1. 这题还算简单。<br />需要注意的是:<br />如果返回体是application/json,我们的 ctx.length = 17 实际上是没有意义的。源码里会删掉你强行定义的(如果你是 json 格式返回的话)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/555873/1615663369278-10185bd7-b25e-46d2-8e82-e29cff4381b5.png#align=left&display=inline&height=50&margin=%5Bobject%20Object%5D&name=image.png&originHeight=100&originWidth=368&size=4536&status=done&style=none&width=184)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/555873/1615663315000-b4a563a1-f97b-49cb-a47b-98bd8802593b.png#align=left&display=inline&height=86&margin=%5Bobject%20Object%5D&name=image.png&originHeight=172&originWidth=1024&size=28444&status=done&style=none&width=512)
  2. <a name="wSzPq"></a>
  3. #### 08 - decorators
  4. - 简介
  5. > 所有的中间件都是下一个中间件的装饰器
  6. - 考卷

var request = require(‘supertest’);

var app = require(‘./index.js’);

describe(‘Decorators’, function () { it(‘should return the string escaped’, function (done) { request(app.listen()) .get(‘/‘) .expect(200) .expect(‘this following HTML should be escaped: <p>hi!</p>’, done); }) })

  1. - 答案

var koa = require(‘koa’); var escape = require(‘escape-html’); var app = module.exports = new koa();

app.use(async (ctx, next) => { await next() })

app.use(async (ctx) => { ctx.response.body = escape(‘this following HTML should be escaped:

hi!

‘) });

  1. <a name="8Gp2v"></a>
  2. #### 09 - templating
  3. - 简介
  4. > 使用jade搭配koa,用于返回静态页面。
  5. - 考卷

var request = require(‘supertest’);

var app = require(‘./index.js’);

describe(‘Templating’, function () { it(‘should return the template’, function (done) { var html = ‘<!DOCTYPE html>

Hello!

‘;

  1. request(app.listen())
  2. .get('/')
  3. .expect(200)
  4. .expect('Content-Type', /text\/html/)
  5. .expect(html, done);

}) })

  1. - 答案

var koa = require(‘koa’); var jade = require(‘jade’); var path = require(‘path’); var app = module.exports = new koa();

app.use(async (ctx) => { jade.renderFile(path.join(__dirname, ‘homepage.jade’), { options: ‘here’ }, function(err, html) { ctx.body = html }); });

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/555873/1615691478168-302086e7-6a9c-4117-ad3a-438db864dcf8.png#align=left&display=inline&height=262&margin=%5Bobject%20Object%5D&name=image.png&originHeight=523&originWidth=1011&size=47335&status=done&style=none&width=505.5)<br />这题难度较低,查一下api马上就能实现,jade是很早的东西了,现在改名叫 pug,曾经网上有个 node.js 视频的教程还是用它做的(2014-2018年了,现在大概过时了)
  2. <a name="qjkON"></a>
  3. #### 10 - authentication
  4. - 简介
  5. 鉴权内容,是大多数前端应该比较需要了解。
  6. - 考卷
  7. - /:无权限-401
  8. - /login:登录页面-200
  9. - /logout:状态-303

var assert = require(‘assert’); var request = require(‘supertest’);

var app = require(‘./index.js’); var server = app.listen();

request = request.agent(server);

describe(‘Authentication’, function () { describe(‘when not authenticated’, function () { it(‘GET / should 401’, function (done) { request.get(‘/‘).expect(401, done); })

  1. it('GET /login should 200', function (done) {
  2. request.get('/login')
  3. .expect('Content-Type', /text\/html/)
  4. .expect(/^<form/, done);
  5. })
  6. it('GET /logout should 303', function (done) {
  7. request.get('/logout').expect(303, done);
  8. })

})

describe(‘logging in’, function () { it(‘GET /login should render a CSRF token’, function (done) { request.get(‘/login’) .expect(‘Content-Type’, /text\/html/) .expect(200, function (err, res) { if (err) return done(err);

  1. var html = res.text;
  2. csrf = /name="_csrf" value="([^"]+)"/.exec(html)[1];
  3. done();
  4. })
  5. })
  6. it('POST /login should 403 without a CSRF token', function (done) {
  7. request.post('/login')
  8. .send({
  9. username: 'username',
  10. password: 'password'
  11. })
  12. .expect(403, done);
  13. })
  14. it('POST /login should 403 with an invalid CSRF token', function (done) {
  15. request.post('/login')
  16. .send({
  17. username: 'username',
  18. password: 'password',
  19. _csrf: 'lkjalksdjfasdf'
  20. })
  21. .expect(403, done);
  22. })
  23. it('POST /login should 400 with bad auth details', function (done) {
  24. request.post('/login')
  25. .send({
  26. _csrf: csrf,
  27. username: 'klajklsdjfasdf',
  28. password: 'lkjlakjsdlkfja'
  29. })
  30. .expect(400, done);
  31. })
  32. it('POST /login should 303 with good auth details', function (done) {
  33. request.post('/login')
  34. .send({
  35. _csrf: csrf,
  36. username: 'username',
  37. password: 'password'
  38. })
  39. .expect(303)
  40. .expect('Location', '/', done);
  41. })
  42. it('GET / should return hello world', function (done) {
  43. request.get('/')
  44. .expect(200)
  45. .expect('hello world', done);
  46. })

})

describe(‘logging out’, function () { it(‘GET /logout should 303 to /login’, function (done) { request.get(‘/logout’) .expect(303) .expect(‘Location’, ‘/login’, done); })

  1. it('GET / should 401', function (done) {
  2. request.get('/').expect(401, done);
  3. })

}) })

```

  • 答案

总结和一些想法

  • Koa确实很纯粹。
  • Koa和Express除了写法的区别外,最大区别就是纯粹程度了。
  • Koa缺点:事实上,做题的过程中,能够发现,很多koa的框架,还停留在 koa v1的版本。比如 koa-gzip/koa-session 这种非常必要的、常用的。第三方的支持是不靠谱的,作为个人长期维护的项目,你不能保证它的稳定更新。
  • Koa缺点:它太纯粹了,导致很多功能它没有自己去实现(比如gzip需要引入koa-gzip,session需要引入koa-session,body-parser也经常要引入,还有很多…)。对于一个像我这样的新人来说,不太可能架构出一个好的体系。所有会有egg.js这类东西诞生(继承了koa社区的最佳实践)。

image.png

事实证明,一个纯粹的东西,扩展性是很强的,但是功能的完备性肯定是受到限制的。