1. Koa 入门
1.1 创建 Koa2工程
首先,我们创建一个目录hello-koa
并作为工程目录用 VS Code 打开。然后,我们创建app.js
,输入以下代码:
// 导入koa,和koa 1.x不同,在koa2中,我们导入的是一个class,因此用大写的Koa表示:
const Koa = require('koa');
// 创建一个Koa对象表示web app本身:
const app = new Koa();
// 对于任何请求,app将调用该异步函数处理请求:
app.use(async (ctx, next) => {
await next();
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello, koa2!</h1>';
});
// 在端口3000监听:
app.listen(3000);
console.log('app started at port 3000...');
对于每一个 http 请求,koa 将调用我们传入的异步函数来处理:
async (ctx, next) => {
await next();
// 设置response的Content-Type:
ctx.response.type = 'text/html';
// 设置response的内容:
ctx.response.body = '<h1>Hello, koa2!</h1>';
}
其中,参数ctx
是由koa
传入的封装了request
和response
的变量,我们可以通过它访问request
和response
,next
是koa
传入的将要处理的下一个异步函数。
上面的异步函数中,我们首先用await next();
处理下一个异步函数,然后,设置response
的Content-Type
和内容。
由async
标记的函数称为异步函数,在异步函数中,可以用await
调用另一个异步函数,这两个关键字将在 ES7 中引入。
现在我们遇到第一个问题:koa
这个包怎么装,app.js
才能正常导入它?
方法一
可以用npm
命令直接安装koa
。先打开命令提示符,务必把当前目录切换到hello-koa
这个目录,然后执行命令:
$ npm install koa@2.0.0
npm
会把koa2以及koa2依赖的所有包全部安装到当前目录的node_modules
目录下。
方法二
在hello-koa
这个目录下创建一个package.json
,这个文件描述了我们的 hello-koa 工程会用到哪些包。完整的文件内容如下:
{
"name": "hello-koa2",
"version": "1.0.0",
"description": "Hello Koa 2 example with async",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"keywords": [
"koa",
"async"
],
"author": "Michael Liao",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/michaelliao/learn-javascript.git"
},
"dependencies": {
"koa": "2.0.0"
}
}
其中,dependencies
描述了我们的工程依赖的包以及版本号。其他字段均用来描述项目信息,可任意填写。
然后,我们在hello-koa
目录下执行npm install
就可以把所需包以及依赖包一次性全部装好:
$ npm install
很显然,第二个方法更靠谱,因为我们只要在package.json
正确设置了依赖,npm 就会把所有用到的包都装好。
注意,任何时候都可以直接删除整个node_modules
目录,因为用npm install
命令可以完整地重新下载所有依赖。并且,这个目录不应该被放入版本控制中。
现在,我们的工程结构如下:
hello-koa/
|
+- .vscode/
| |
| +- launch.json <-- VSCode 配置文件
|
+- app.js <-- 使用koa的js
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的所有依赖包
紧接着,我们在package.json
中添加依赖包:
"dependencies": {
"koa": "2.0.0"
}
然后使用npm install
命令安装后,在 VS Code 中执行app.js
,调试控制台输出如下:
node --debug-brk=40645 --nolazy app.js
Debugger listening on port 40645
app started at port 3000...
我们打开浏览器,输入http://localhost:3000
,即可看到效果:
还可以直接用命令
node app.js
在命令行启动程序,或者用npm start
启动。npm start
命令会让 npm 执行定义在package.json
文件中的start
对应命令:
"scripts": {
"start": "node app.js"
}
1.2 Koa Middleware
让我们再仔细看看 koa 的执行逻辑。核心代码是:
app.use(async (ctx, next) => {
await next();
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello, koa2!</h1>';
});
每收到一个 http 请求,koa 就会调用通过app.use()
注册的async
函数,并传入ctx
和next
参数。
我们可以对ctx
操作,并设置返回内容。但是为什么要调用await next()
?
原因是 koa 把很多 async 函数组成一个处理链,每个 async 函数都可以做一些自己的事情,然后用await next()
来调用下一个 async 函数。我们把每个 async 函数称为middleware
,这些middleware
可以组合起来,完成很多有用的功能。
例如,可以用以下 3 个middleware
组成处理链,依次打印日志,记录处理时间,输出 HTML:
app.use(async (ctx, next) => {
console.log(`${ctx.request.method} ${ctx.request.url}`); // 打印URL
await next(); // 调用下一个middleware
});
app.use(async (ctx, next) => {
const start = new Date().getTime(); // 当前时间
await next(); // 调用下一个middleware
const ms = new Date().getTime() - start; // 耗费时间
console.log(`Time: ${ms}ms`); // 打印耗费时间
});
app.use(async (ctx, next) => {
await next();
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello, koa2!</h1>';
});
middleware
的顺序很重要,也就是调用app.use()
的顺序决定了middleware
的顺序。
此外,如果一个middleware
没有调用await next()
,会怎么办?答案是后续的middleware
将不再执行了。这种情况也很常见,例如,一个检测用户权限的middleware
可以决定是否继续处理请求,还是直接返回403
错误:
app.use(async (ctx, next) => {
if (await checkUserPermission(ctx)) {
await next();
} else {
ctx.response.status = 403;
}
});
理解了middleware
,我们就已经会用 koa了!
最后注意ctx
对象有一些简写的方法,例如ctx.url
相当于ctx.request.url
,ctx.type
相当于ctx.response.type
。
原文链接:廖雪峰 - Koa 入门
参考源码:hello-koa
2. 处理 URL
在hello-koa
工程中,我们处理 http 请求一律返回相同的 HTML,这样虽然非常简单,但是用浏览器一测,随便输入任何 URL 都会返回相同的网页。
正常情况下,我们应该对不同的 URL 调用不同的处理函数,这样才能返回不同的结果。例如像这样写:
app.use(async (ctx, next) => {
if (ctx.request.path === '/') {
ctx.response.body = 'index page';
} else {
await next();
}
});
app.use(async (ctx, next) => {
if (ctx.request.path === '/test') {
ctx.response.body = 'TEST page';
} else {
await next();
}
});
app.use(async (ctx, next) => {
if (ctx.request.path === '/error') {
ctx.response.body = 'ERROR page';
} else {
await next();
}
});
这么写是可以运行的,但是好像有点蠢。
应该有一个能集中处理 URL 的middleware
,它根据不同的 URL 调用不同的处理函数,这样,我们才能专心为每个 URL 编写处理函数。
2.1 koa-router
为了处理 URL ,我们需要引入koa-router
这个middleware
,让它负责处理URL映射
。
我们把上一节的hello-koa
工程复制一份,重命名为url-koa
。
先在package.json
中添加依赖项:
"koa-router": "7.0.0"
然后用npm install
安装。
接下来,我们修改app.js
,使用koa-router
来处理 URL:
const Koa = require('koa');
// 注意require('koa-router')返回的是函数:
const router = require('koa-router')();
const app = new Koa();
// log request URL:
app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
await next();
});
// add url-route:
router.get('/hello/:name', async (ctx, next) => {
var name = ctx.params.name;
ctx.response.body = `<h1>Hello, ${name}!</h1>`;
});
router.get('/', async (ctx, next) => {
ctx.response.body = '<h1>Index</h1>';
});
// add router middleware:
app.use(router.routes());
app.listen(3000);
console.log('app started at port 3000...');
注意导入koa-router
的语句最后的()
是函数调用:
const router = require('koa-router')();
相当于:
const fn_router = require('koa-router');
const router = fn_router();
然后,我们使用router.get('/path', async fn)
来注册一个 GET 请求。可以在请求路径中使用带变量的/hello/:name
,变量可以通过ctx.params.name
访问。
再运行app.js
,我们就可以测试不同的 URL:
输入首页:http://localhost:3000/
输入:http://localhost:3000/hello/koa
2.2 处理 POST 请求
用router.get('/path', async fn)
处理的是 GET 请求。如果要处理 POST 请求,可以用router.post('/path', async fn)
。
用 POST 请求处理 URL 时,我们会遇到一个问题:POST 请求通常会发送一个表单,或者 JSON,它作为request
的body
发送,但无论是 Node.js 提供的原始request
对象,还是 koa 提供的request
对象,都不提供解析request
的body
的功能!
所以,我们又需要引入另一个middleware
来解析原始request
请求,然后,把解析后的参数,绑定到ctx.request.body
中。
koa-bodyparser
就是用来干这个活的。
我们在package.json
中添加依赖项:
"koa-bodyparser": "3.2.0"
然后使用npm install
安装。
下面,修改app.js
,引入koa-bodyparser
:
const bodyParser = require('koa-bodyparser');
在合适的位置加上:
app.use(bodyParser());
由于middleware
的顺序很重要,这个koa-bodyparser
必须在router
之前被注册到app
对象上。
现在我们就可以处理 POST 请求了。写一个简单的登录表单:
router.get('/', async (ctx, next) => {
ctx.response.body = `<h1>Index</h1>
<form action="/signin" method="post">
<p>Name: <input name="name" value="koa"></p>
<p>Password: <input name="password" type="password"></p>
<p><input type="submit" value="Submit"></p>
</form>`;
});
router.post('/signin', async (ctx, next) => {
var
name = ctx.request.body.name || '',
password = ctx.request.body.password || '';
console.log(`signin with name: ${name}, password: ${password}`);
if (name === 'koa' && password === '12345') {
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
} else {
ctx.response.body = `<h1>Login failed!</h1>
<p><a href="/">Try again</a></p>`;
}
});
注意到我们用var name = ctx.request.body.name || ''
拿到表单的name
字段,如果该字段不存在,默认值设置为''
。
类似的,PUT、DELETE、HEAD 请求也可以由router
处理。
2.3 重构
现在,我们已经可以处理不同的 URL 了,但是看看app.js
,总觉得还是有点不对劲。
所有的 URL 处理函数都放到app.js
里显得很乱,而且,每加一个 URL,就需要修改app.js
。随着 URL 越来越多,app.js
就会越来越长。
如果能把 URL 处理函数集中到某个 js 文件,或者某几个 js 文件中就好了,然后让app.js
自动导入所有处理 URL 的函数。这样,代码一分离,逻辑就显得清楚了。最好是这样:
url2-koa/
|
+- .vscode/
| |
| +- launch.json <-- VSCode 配置文件
|
+- controllers/
| |
| +- login.js <-- 处理login相关URL
| |
| +- users.js <-- 处理用户管理相关URL
|
+- app.js <-- 使用koa的js
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的所有依赖包
于是我们把url-koa
复制一份,重命名为url2-koa
,准备重构这个项目。
我们先在controllers
目录下编写index.js
:
var fn_index = async (ctx, next) => {
ctx.response.body = `<h1>Index</h1>
<form action="/signin" method="post">
<p>Name: <input name="name" value="koa"></p>
<p>Password: <input name="password" type="password"></p>
<p><input type="submit" value="Submit"></p>
</form>`;
};
var fn_signin = async (ctx, next) => {
var
name = ctx.request.body.name || '',
password = ctx.request.body.password || '';
console.log(`signin with name: ${name}, password: ${password}`);
if (name === 'koa' && password === '12345') {
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
} else {
ctx.response.body = `<h1>Login failed!</h1>
<p><a href="/">Try again</a></p>`;
}
};
module.exports = {
'GET /': fn_index,
'POST /signin': fn_signin
};
这个index.js
通过module.exports
把两个 URL 处理函数暴露出来。
类似的,hello.js
把一个 URL 处理函数暴露出来:
var fn_hello = async (ctx, next) => {
var name = ctx.params.name;
ctx.response.body = `<h1>Hello, ${name}!</h1>`;
};
module.exports = {
'GET /hello/:name': fn_hello
};
现在,我们修改app.js
,让它自动扫描controllers
目录,找到所有 js 文件,导入,然后注册每个 URL:
// 先导入fs模块,然后用readdirSync列出文件
// 这里可以用sync是因为启动时只运行一次,不存在性能问题:
var files = fs.readdirSync(__dirname + '/controllers');
// 过滤出.js文件:
var js_files = files.filter((f)=>{
return f.endsWith('.js');
});
// 处理每个js文件:
for (var f of js_files) {
console.log(`process controller: ${f}...`);
// 导入js文件:
let mapping = require(__dirname + '/controllers/' + f);
for (var url in mapping) {
if (url.startsWith('GET ')) {
// 如果url类似"GET xxx":
var path = url.substring(4);
router.get(path, mapping[url]);
console.log(`register URL mapping: GET ${path}`);
} else if (url.startsWith('POST ')) {
// 如果url类似"POST xxx":
var path = url.substring(5);
router.post(path, mapping[url]);
console.log(`register URL mapping: POST ${path}`);
} else {
// 无效的URL:
console.log(`invalid URL: ${url}`);
}
}
}
如果上面的大段代码看起来还是有点费劲,那就把它拆成更小单元的函数:
function addMapping(router, mapping) {
for (var url in mapping) {
if (url.startsWith('GET ')) {
var path = url.substring(4);
router.get(path, mapping[url]);
console.log(`register URL mapping: GET ${path}`);
} else if (url.startsWith('POST ')) {
var path = url.substring(5);
router.post(path, mapping[url]);
console.log(`register URL mapping: POST ${path}`);
} else {
console.log(`invalid URL: ${url}`);
}
}
}
function addControllers(router) {
var files = fs.readdirSync(__dirname + '/controllers');
var js_files = files.filter((f) => {
return f.endsWith('.js');
});
for (var f of js_files) {
console.log(`process controller: ${f}...`);
let mapping = require(__dirname + '/controllers/' + f);
addMapping(router, mapping);
}
}
addControllers(router);
确保每个函数功能非常简单,一眼能看明白,是代码可维护的关键。
2.4 Controller Middleware
最后,我们把扫描controllers
目录和创建router
的代码从app.js
中提取出来,作为一个简单的middleware
使用,命名为controller.js
:
const fs = require('fs');
function addMapping(router, mapping) {
...
}
function addControllers(router, dir) {
...
}
module.exports = function (dir) {
let
controllers_dir = dir || 'controllers', // 如果不传参数,扫描目录默认为'controllers'
router = require('koa-router')();
addControllers(router, controllers_dir);
return router.routes();
};
这样一来,我们在app.js
的代码又简化了:
...
// 导入controller middleware:
const controller = require('./controller');
...
// 使用middleware:
app.use(controller());
...
经过重新整理后的工程url2-koa
目前具备非常好的模块化,所有处理 URL 的函数按功能组存放在controllers
目录,今后我们也只需要不断往这个目录下加东西就可以了,app.js
保持不变。
原文链接:廖雪峰 - 处理 URL
参考源码: