Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit c466d1d
Showing
39 changed files
with
1,119 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
|
||
Koa uses [co](https://github.com/visionmedia/co) under the hood, | ||
so to fully understand how Koa works, | ||
you must understand co. | ||
|
||
Co uses ES6 generators. | ||
You can tell if a function is a generator if it has a star: | ||
|
||
```js | ||
function* () { | ||
|
||
} | ||
``` | ||
|
||
`yield` is a keyword specific to generators and allows users to __arbitrarily suspend__ functions at any `yield` point. | ||
`yield` is not a magical async function - co does all that magic behind the scenes. | ||
|
||
You can think of `co`'s use of generators like this with node callbacks: | ||
|
||
```js | ||
function* () { | ||
var val = yield /* breakpoint */ function (next) { | ||
// #pause at the break point | ||
|
||
// execute an asynchronous function | ||
setTimeout(function () { | ||
// return the value node.js callback-style | ||
next(null, 1); | ||
}, 100); | ||
} | ||
|
||
assert(val === 1); | ||
} | ||
``` | ||
|
||
This workshop will not cover all the intricacies of generators as that alone would | ||
be its own workshop, and most people (including myself) wouldn't be able | ||
to understand generators in less than a day. | ||
|
||
## Yieldables | ||
|
||
You can only `yield` a few types of "async" things in Co. We call these "yieldables".: | ||
|
||
### Thunks | ||
|
||
Thunks are asynchronous functions that only allow a callback: | ||
|
||
```js | ||
function (done) { | ||
setTimeout(function () { | ||
done(/* error: */ null, true); | ||
}, 100) | ||
} | ||
``` | ||
|
||
If there are additional arguments on this function, | ||
a neat trick is to simply wrap it in a thunk or return a thunk. | ||
You may be interested in [thunkify](https://github.com/visionmedia/node-thunkify), | ||
but we will learn how to "thunkify" node.js-style callbacks in this exercise. | ||
|
||
### Promises | ||
|
||
We won't show you how to write code with promises, | ||
but you can `yield` them! | ||
|
||
## Creating yieldables | ||
|
||
In this exercise, we will learn how to create "yieldables" from callbacks. | ||
We will not go into depth of how generators and `co` works as it is unnecessary for Koa. | ||
|
||
## Learning more | ||
|
||
- [thunkify](https://github.com/visionmedia/node-thunkify) | ||
- [mz](https://github.com/normalize/mz) | ||
- [co-fs](https://github.com/visionmedia/co-fs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
|
||
var fs = require('fs'); | ||
|
||
/** | ||
* Create a yieldable version of `fs.stat()`: | ||
* | ||
* app.use(function* () { | ||
* var stats = yield exports.stat(__filename); | ||
* }) | ||
* | ||
* Hint: you can return a yieldable. | ||
*/ | ||
|
||
exports.stat = function (filename) { | ||
|
||
}; | ||
|
||
/** | ||
* Create a yieldable version of `fs.exists()`: | ||
* | ||
* | ||
* app.use(function* () { | ||
* var exists = yield exports.exists(__filename); | ||
* }) | ||
* | ||
* Note that `fs.exists()` simply wraps `fs.stat()`. | ||
* If `fs.stat()` does not return an error, then the file exists! | ||
* | ||
* Hint: don't use fs.exists() as it's not a proper callback. | ||
* In other words, the first argument returned in its callback | ||
* is not an error object, unlike node callbacks. | ||
*/ | ||
|
||
exports.exists = function (filename) { | ||
|
||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
|
||
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')) | ||
})) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
|
||
Now that you sort of have the idea of generators, | ||
the next step is to make the simplest Koa app, ever. | ||
|
||
Unlike Express where you use node.js' `req` and `res` object, | ||
Koa exposes its own very similar `this.request` and `this.response` objects. | ||
Also unlike Express and node.js, | ||
Koa uses getters and setters instead of methods. | ||
|
||
For example, | ||
in node.js, you might be used to the following: | ||
|
||
```js | ||
res.statusCode = 200; | ||
res.setHeader('Content-Type', 'text/plain'); | ||
res.end('hello world'); | ||
``` | ||
|
||
In Express, there is a shortcut: | ||
|
||
```js | ||
res.send('hello world'); | ||
``` | ||
|
||
However, in Koa, we use getter/setter: | ||
|
||
```js | ||
app.use(function* () { | ||
this.response.body = 'hello world'; | ||
}) | ||
``` | ||
|
||
## Exercise | ||
|
||
Make an app that returns `hello world` for every response. | ||
Verify the following are correct: | ||
|
||
- Status Code: `200` | ||
- `Content-Length`: `11` | ||
- `Content-Type`: `text/plain; charset=utf-8` | ||
|
||
> Hint: Koa sets these headers for you with strings! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
|
||
var koa = require('koa'); | ||
|
||
var app = module.exports = koa(); | ||
|
||
/** | ||
* Return "hello world" as a string on every request. | ||
* Hint: this only requires a single line of code. | ||
*/ | ||
|
||
app.use(function* () { | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
|
||
var request = require('supertest'); | ||
|
||
var app = require('./index.js'); | ||
|
||
describe('Hello World', function () { | ||
it('should return hello world', function (done) { | ||
request(app.listen()) | ||
.get('/') | ||
.expect(200) | ||
.expect('Content-Length', 11) | ||
.expect('Content-Type', 'text/plain; charset=utf-8') | ||
.end(done) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
|
||
Unlike Express and many other frameworks, Koa does not include a router. | ||
Without a router, routing in Koa can be done by using `this.request.path` and `yield next`. | ||
To check if the request matches a specific path: | ||
|
||
```js | ||
app.use(function* (next) { | ||
if (this.request.path === '/') { | ||
|
||
} | ||
}) | ||
``` | ||
|
||
To skip this middleware: | ||
|
||
```js | ||
app.use(function* (next) { | ||
if (skip) return yield next; | ||
}) | ||
``` | ||
|
||
This is very similar to Express' `next()` call. | ||
|
||
Combining this together, | ||
you can route paths like this: | ||
|
||
```js | ||
app.use(function* (next) { | ||
// skip the rest of the code if the route does not match | ||
if (this.request.path !== '/') return yield next; | ||
|
||
this.response.body = 'we are at home!'; | ||
}) | ||
``` | ||
|
||
By `return`ing early, | ||
we don't need to bother having any nested `if` and `else` statements. | ||
Note that we're checking whether the path __does not match__, | ||
then early returning. | ||
|
||
Of course, you can write this the long way: | ||
|
||
```js | ||
app.use(function* (next) { | ||
if (this.request.path === '/') { | ||
this.response.body = 'hello!'; | ||
} else { | ||
yield next; | ||
} | ||
}) | ||
``` | ||
|
||
## Exercise | ||
|
||
Create an app that returns the following responses from the following routes: | ||
|
||
- `/` - `hello world` | ||
- `/404` - `page not found` | ||
- `/500` - `internal server error`. | ||
|
||
In a real app, having each route as its own middleware is more ideal | ||
as it allows for easier refactoring. | ||
|
||
## Learn More | ||
|
||
There are more properties you're probably interested in when routing: | ||
|
||
- `this.request.method` | ||
- `this.request.query` | ||
- `this.request.host` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
|
||
var koa = require('koa'); | ||
|
||
var app = module.exports = koa(); | ||
|
||
app.use(function* () { | ||
|
||
}); | ||
|
||
app.use(function* () { | ||
|
||
}); | ||
|
||
app.use(function* () { | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
|
||
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); | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
|
||
So far, we've only used strings as bodies. | ||
Koa supports the following types of bodies: | ||
|
||
- Strings | ||
- Buffers | ||
- Streams (node) | ||
- JSON Objects | ||
|
||
If the body is not treated as the first three, | ||
Koa will assume that you want to send a JSON response, | ||
treating REST APIs as first-class citizens. | ||
|
||
```js | ||
app.use(function* (next) { | ||
this.response.body = { | ||
message: 'this will be sent as a JSON response!' | ||
}; | ||
}) | ||
``` | ||
|
||
When setting a stream as a body, | ||
Koa will automatically add any error handlers so you don't have to worry about error handling. | ||
|
||
```js | ||
var fs = require('fs'); | ||
|
||
app.use(function* (next) { | ||
this.response.body = fs.createReadStream('some_file.txt'); | ||
// koa will automatically handle errors and leaks | ||
}) | ||
``` | ||
|
||
## Exercise | ||
|
||
Create an app that returns a stream when the client requests `/stream` and a JSON body when the client requests `/json`. | ||
|
||
## Bonus | ||
|
||
When setting the body of the stream, Koa can't infer the `Content-Length` of the | ||
body. Set the `Content-Length` header yourself using `this.response.length=` | ||
and the "yieldable" version of `fs` you've created in exercise 1. |
Oops, something went wrong.