koa2开发入门
一.koa2入门
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
这个目录,然后执行命令:
C:\...\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
就可以把所需包以及依赖包一次性全部装好:
C:\...\hello-koa> 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"
}
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:
// 导入koa,和koa 1.x不同,在koa2中,我们导入的是一个class,因此用大写的Koa表示:
const Koa = require('koa'); // 创建一个Koa对象表示web app本身:
const app = new Koa(); 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>';
}); // 在端口3000监听:
app.listen(3000);
console.log('app started at port 3000...');
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
。
二.处理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编写处理函数。
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/hello/koa
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请求了。写一个简单的登录表单:
const Koa = require('koa'); // 注意require('koa-router')返回的是函数:
const router = require('koa-router')();
const bodyParser = require('koa-bodyparser'); 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>
<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>`;
}
}); router.get('/', async (ctx, next) => {
ctx.response.body = '<h1>Index</h1>';
}); // add router middleware:
app.use(bodyParser());
app.use(router.routes()); app.listen(3000);
console.log('app started at port 3000...');
注意到我们用var name = ctx.request.body.name || ''
拿到表单的name
字段,如果该字段不存在,默认值设置为''
。
类似的,put、delete、head请求也可以由router处理。
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);
确保每个函数功能非常简单,一眼能看明白,是代码可维护的关键。
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();
};
完整内容如下:
const fs = require('fs'); 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);
}
} 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()); ...
完整内容如下所示:
// 导入koa,和koa 1.x不同,在koa2中,我们导入的是一个class,因此用大写的Koa表示:
const Koa = require('koa');
const bodyParser = require('koa-bodyparser'); // 创建一个Koa对象表示web app本身:
const app = new Koa(); // 导入controller middleware:
const controller = require('./controller'); // 使用middleware:
app.use(bodyParser());
app.use(controller()); // 在端口3000监听:
app.listen(3000);
console.log('app started at port 3000...');
经过重新整理后的工程url2-koa
目前具备非常好的模块化,所有处理URL的函数按功能组存放在controllers
目录,今后我们也只需要不断往这个目录下加东西就可以了,app.js
保持不变。
三.使用Nunjucks
Nunjucks是什么东东?其实它是一个模板引擎。
那什么是模板引擎?
模板引擎就是基于模板配合数据构造出字符串输出的一个组件。比如下面的函数就是一个模板引擎:
function examResult (data) {
return `${data.name}同学一年级期末考试语文${data.chinese}分,数学${data.math}分,位于年级第${data.ranking}名。`
}
如果我们输入数据如下:
examResult({
name: '小明',
chinese: 78,
math: 87,
ranking: 999
});
该模板引擎把模板字符串里面对应的变量替换以后,就可以得到以下输出:
小明同学一年级期末考试语文78分,数学87分,位于年级第999名。
模板引擎最常见的输出就是输出网页,也就是HTML文本。当然,也可以输出任意格式的文本,比如Text,XML,Markdown等等。
有同学要问了:既然JavaScript的模板字符串可以实现模板功能,那为什么我们还需要另外的模板引擎?
因为JavaScript的模板字符串必须写在JavaScript代码中,要想写出新浪首页这样复杂的页面,是非常困难的。
输出HTML有几个特别重要的问题需要考虑:
a.转义
对特殊字符要转义,避免受到XSS攻击。比如,如果变量name
的值不是小明
,而是小明<script>...</script>
,模板引擎输出的HTML到了浏览器,就会自动执行恶意JavaScript代码。
b.格式化
对不同类型的变量要格式化,比如,货币需要变成12,345.00
这样的格式,日期需要变成2016-01-01
这样的格式。
c.简单逻辑
模板还需要能执行一些简单逻辑,比如,要按条件输出内容,需要if实现如下输出:
{{ name }}同学,
{% if score >= 90 %}
成绩优秀,应该奖励
{% elif score >=60 %}
成绩良好,继续努力
{% else %}
不及格,建议回家打屁股
{% endif %}
所以,我们需要一个功能强大的模板引擎,来完成页面输出的功能。
1.Nunjucks
我们选择Nunjucks作为模板引擎。Nunjucks是Mozilla开发的一个纯JavaScript编写的模板引擎,既可以用在Node环境下,又可以运行在浏览器端。但是,主要还是运行在Node环境下,因为浏览器端有更好的模板解决方案,例如MVVM框架。
如果你使用过Python的模板引擎jinja2,那么使用Nunjucks就非常简单,两者的语法几乎是一模一样的,因为Nunjucks就是用JavaScript重新实现了jinjia2。
从上面的例子我们可以看到,虽然模板引擎内部可能非常复杂,但是使用一个模板引擎是非常简单的,因为本质上我们只需要构造这样一个函数:
function render(view, model) {
// TODO:...
}
其中,view
是模板的名称(又称为视图),因为可能存在多个模板,需要选择其中一个。model
就是数据,在JavaScript中,它就是一个简单的Object。render
函数返回一个字符串,就是模板的输出。
下面我们来使用Nunjucks这个模板引擎来编写几个HTML模板,并且用实际数据来渲染模板并获得最终的HTML输出。
我们创建一个use-nunjucks
的VS Code工程结构如下:
use-nunjucks/
|
+- .vscode/
| |
| +- launch.json <-- VSCode 配置文件
|
+- views/
| |
| +- hello.html <-- HTML模板文件
|
+- app.js <-- 入口js
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的所有依赖包
其中,模板文件存放在views
目录中。
我们先在package.json
中添加nunjucks
的依赖:
"nunjucks": "2.4.2"
注意,模板引擎是可以独立使用的,并不需要依赖koa。用npm install
安装所有依赖包。
紧接着,我们要编写使用Nunjucks的函数render
。怎么写?方法是查看Nunjucks的官方文档,仔细阅读后,在app.js
中编写代码如下:
const nunjucks = require('nunjucks'); function createEnv(path, opts) {
var
autoescape = opts.autoescape === undefined ? true : opts.autoescape,
noCache = opts.noCache || false,
watch = opts.watch || false,
throwOnUndefined = opts.throwOnUndefined || false,
env = new nunjucks.Environment(
new nunjucks.FileSystemLoader('views', {
noCache: noCache,
watch: watch,
}), {
autoescape: autoescape,
throwOnUndefined: throwOnUndefined
});
if (opts.filters) {
for (var f in opts.filters) {
env.addFilter(f, opts.filters[f]);
}
}
return env;
} var env = createEnv('views', {
watch: true,
filters: {
hex: function (n) {
return '0x' + n.toString(16);
}
}
}); var s = env.render('hello.html', { name: '小明' });
console.log(s); var s2 = env.render('hello.html', { name: '<script>alert("小明")</script>' });
console.log(s2); var s3 = env.render('list.html', { fruits: ['apple', 'orange', 'banana'] });
console.log(s3); console.log(env.render('extend.html', {
header: 'Hello',
body: 'bla bla bla...'
}));
变量env
就表示Nunjucks模板引擎对象,它有一个render(view, model)
方法,正好传入view
和model
两个参数,并返回字符串。
创建env
需要的参数可以查看文档获知。我们用autoescape = opts.autoescape && true
这样的代码给每个参数加上默认值,最后使用new nunjucks.FileSystemLoader('views')
创建一个文件系统加载器,从views
目录读取模板。
我们编写一个hello.html
模板文件,放到views
目录下,内容如下:
<h1>Hello {{ name }}</h1>
然后,我们就可以用下面的代码来渲染这个模板:
var s = env.render('hello.html', { name: '小明' });
console.log(s);
获得输出如下:
<h1>Hello 小明</h1>
咋一看,这和使用JavaScript模板字符串没啥区别嘛。不过,试试:
var s = env.render('hello.html', { name: '<script>alert("小明")</script>' });
console.log(s);
获得输出如下:
<h1>Hello <script>alert("小明")</script></h1>
这样就避免了输出恶意脚本。
此外,可以使用Nunjucks提供的功能强大的tag,编写条件判断、循环等功能,例如:
<!-- 循环输出名字 -->
<body>
<h3>Fruits List</h3>
{% for f in fruits %}
<p>{{ f }}</p>
{% endfor %}
</body>
Nunjucks模板引擎最强大的功能在于模板的继承。仔细观察各种网站可以发现,网站的结构实际上是类似的,头部、尾部都是固定格式,只有中间页面部分内容不同。如果每个模板都重复头尾,一旦要修改头部或尾部,那就需要改动所有模板。
更好的方式是使用继承。先定义一个基本的网页框架base.html
:
<html><body>
{% block header %} <h3>Unnamed</h3> {% endblock %}
{% block body %} <div>No body</div> {% endblock %}
{% block footer %} <div>copyright</div> {% endblock %}
</body>
base.html
定义了三个可编辑的块,分别命名为header
、body
和footer
。子模板可以有选择地对块进行重新定义:
{% extends 'base.html' %} {% block header %}<h1>{{ header }}</h1>{% endblock %} {% block body %}<p>{{ body }}</p>{% endblock %}
然后,我们对子模板进行渲染:
console.log(env.render('extend.html', {
header: 'Hello',
body: 'bla bla bla...'
}));
输出HTML如下:
<html><body>
<h1>Hello</h1>
<p>bla bla bla...</p>
<div>copyright</div> <-- footer没有重定义,所以仍使用父模板的内容
</body>
2.性能
最后我们要考虑一下Nunjucks的性能。
对于模板渲染本身来说,速度是非常非常快的,因为就是拼字符串嘛,纯CPU操作。
性能问题主要出现在从文件读取模板内容这一步。这是一个IO操作,在Node.js环境中,我们知道,单线程的JavaScript最不能忍受的就是同步IO,但Nunjucks默认就使用同步IO读取模板文件。
好消息是Nunjucks会缓存已读取的文件内容,也就是说,模板文件最多读取一次,就会放在内存中,后面的请求是不会再次读取文件的,只要我们指定了noCache: false
这个参数。
在开发环境下,可以关闭cache,这样每次重新加载模板,便于实时修改模板。在生产环境下,一定要打开cache,这样就不会有性能问题。
Nunjucks也提供了异步读取的方式,但是这样写起来很麻烦,有简单的写法我们就不会考虑复杂的写法。保持代码简单是可维护性的关键。
四.使用MVC
1.MVC
我们已经可以用koa处理不同的URL,还可以用Nunjucks渲染模板。现在,是时候把这两者结合起来了!
当用户通过浏览器请求一个URL时,koa将调用某个异步函数处理该URL。在这个异步函数内部,我们用一行代码:
ctx.render('home.html', { name: 'Michael' });
通过Nunjucks把数据用指定的模板渲染成HTML,然后输出给浏览器,用户就可以看到渲染后的页面了:
这就是传说中的MVC:Model-View-Controller,中文名“模型-视图-控制器”。
异步函数是C:Controller,Controller负责业务逻辑,比如检查用户名是否存在,取出用户信息等等;
包含变量{{ name }}
的模板就是V:View,View负责显示逻辑,通过简单地替换一些变量,View最终输出的就是用户看到的HTML。
MVC中的Model在哪?Model是用来传给View的,这样View在替换变量的时候,就可以从Model中取出相应的数据。
上面的例子中,Model就是一个JavaScript对象:
{ name: 'Michael' }
下面,我们根据原来的url2-koa
创建工程view-koa
,把koa2、Nunjucks整合起来,然后,把原来直接输出字符串的方式,改为ctx.render(view, model)
的方式。
工程view-koa
结构如下:
view-koa/
|
+- .vscode/
| |
| +- launch.json <-- VSCode 配置文件
|
+- controllers/ <-- Controller
|
+- views/ <-- html模板文件
|
+- static/ <-- 静态资源文件
|
+- controller.js <-- 扫描注册Controller
|
+- app.js <-- 使用koa的js
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的所有依赖包
在package.json
中,我们将要用到的依赖包有:
"koa": "2.0.0",
"koa-bodyparser": "3.2.0",
"koa-router": "7.0.0",
"nunjucks": "2.4.2",
"mime": "1.3.4",
"mz": "2.4.0"
先用npm install
安装依赖包。
然后,我们准备编写以下两个Controller:
2.处理首页 GET /
我们定义一个async函数处理首页URL/
:
async (ctx, next) => {
ctx.render('index.html', {
title: 'Welcome'
});
}
注意到koa并没有在ctx
对象上提供render
方法,这里我们假设应该这么使用,这样,我们在编写Controller的时候,最后一步调用ctx.render(view, model)
就完成了页面输出。
3.处理登录请求 POST /signin
我们再定义一个async函数处理登录请求/signin
:
async (ctx, next) => {
var
email = ctx.request.body.email || '',
password = ctx.request.body.password || '';
if (email === 'admin@example.com' && password === '123456') {
// 登录成功:
ctx.render('signin-ok.html', {
title: 'Sign In OK',
name: 'Mr Node'
});
} else {
// 登录失败:
ctx.render('signin-failed.html', {
title: 'Sign In Failed'
});
}
}
由于登录请求是一个POST,我们就用ctx.request.body.<name>
拿到POST请求的数据,并给一个默认值。
登录成功时我们用signin-ok.html
渲染,登录失败时我们用signin-failed.html
渲染,所以,我们一共需要以下3个View:
- index.html
- signin-ok.html
- signin-failed.html
4.编写View
在编写View的时候,我们实际上是在编写HTML页。为了让页面看起来美观大方,使用一个现成的CSS框架是非常有必要的。我们用Bootstrap这个CSS框架。从首页下载zip包后解压,我们把所有静态资源文件放到/static
目录下:
view-koa/
|
+- static/
|
+- css/ <- 存放bootstrap.css等
|
+- fonts/ <- 存放字体文件
|
+- js/ <- 存放bootstrap.js等
这样我们在编写HTML的时候,可以直接用Bootstrap的CSS,像这样:
<link rel="stylesheet" href="/static/css/bootstrap.css">
现在,在使用MVC之前,第一个问题来了,如何处理静态文件?
我们把所有静态资源文件全部放入/static
目录,目的就是能统一处理静态文件。在koa中,我们需要编写一个middleware,处理以/static/
开头的URL。
5.编写middleware
我们来编写一个处理静态文件的middleware。编写middleware实际上一点也不复杂。我们先创建一个static-files.js
的文件,编写一个能处理静态文件的middleware:
const path = require('path');
const mime = require('mime');
const fs = require('mz/fs'); function staticFiles(url, dir) {
return async (ctx, next) => {
let rpath = ctx.request.path;
console.log(rpath)
console.log(url)
console.log(dir)
if (rpath.startsWith(url)) {
let fp = path.join(dir, rpath.substring(url.length));
console.log(fp)
if (await fs.exists(fp)) {
ctx.response.type = mime.lookup(rpath);
ctx.response.body = await fs.readFile(fp);
} else {
ctx.response.status = 404;
}
} else {
await next();
}
};
} module.exports = staticFiles;
staticFiles
是一个普通函数,它接收两个参数:URL前缀和一个目录,然后返回一个async函数。这个async函数会判断当前的URL是否以指定前缀开头,如果是,就把URL的路径视为文件,并发送文件内容。如果不是,这个async函数就不做任何事情,而是简单地调用await next()
让下一个middleware去处理请求。
我们使用了一个mz
的包,并通过require('mz/fs');
导入。mz
提供的API和Node.js的fs
模块完全相同,但fs
模块使用回调,而mz
封装了fs
对应的函数,并改为Promise。这样,我们就可以非常简单的用await
调用mz
的函数,而不需要任何回调。
所有的第三方包都可以通过npm官网搜索并查看其文档:
最后,这个middleware使用起来也很简单,在app.js
里加一行代码:
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));
注意:也可以去npm搜索能用于koa2的处理静态文件的包并直接使用。
6.集成Nunjucks
集成Nunjucks实际上也是编写一个middleware,这个middleware的作用是给ctx
对象绑定一个render(view, model)
的方法,这样,后面的Controller就可以调用这个方法来渲染模板了。
我们创建一个templating.js
来实现这个middleware:
const nunjucks = require('nunjucks'); function createEnv(path, opts) {
var
autoescape = opts.autoescape === undefined ? true : opts.autoescape,
noCache = opts.noCache || false,
watch = opts.watch || false,
throwOnUndefined = opts.throwOnUndefined || false,
env = new nunjucks.Environment(
new nunjucks.FileSystemLoader(path, {
noCache: noCache,
watch: watch,
}), {
autoescape: autoescape,
throwOnUndefined: throwOnUndefined
});
if (opts.filters) {
for (var f in opts.filters) {
env.addFilter(f, opts.filters[f]);
}
}
return env;
} function templating(path, opts) {
var env = createEnv(path, opts);
return async (ctx, next) => {
ctx.render = function (view, model) {
ctx.response.body = env.render(view, Object.assign({}, ctx.state || {}, model || {}));
ctx.response.type = 'text/html';
};
await next();
};
} module.exports = templating;
注意到createEnv()
函数和前面使用Nunjucks时编写的函数是一模一样的。我们主要关心tempating()
函数,它会返回一个middleware,在这个middleware中,我们只给ctx
“安装”了一个render()
函数,其他什么事情也没干,就继续调用下一个middleware。
使用的时候,我们在app.js
添加如下代码:
const isProduction = process.env.NODE_ENV === 'production'; app.use(templating('views', {
noCache: !isProduction,
watch: !isProduction
}));
这里我们定义了一个常量isProduction
,它判断当前环境是否是production环境。如果是,就使用缓存,如果不是,就关闭缓存。在开发环境下,关闭缓存后,我们修改View,可以直接刷新浏览器看到效果,否则,每次修改都必须重启Node程序,会极大地降低开发效率。
Node.js在全局变量process
中定义了一个环境变量env.NODE_ENV
,为什么要使用该环境变量?因为我们在开发的时候,环境变量应该设置为'development'
,而部署到服务器时,环境变量应该设置为'production'
。在编写代码的时候,要根据当前环境作不同的判断。
注意:生产环境上必须配置环境变量NODE_ENV = 'production'
,而开发环境不需要配置,实际上NODE_ENV
可能是undefined
,所以判断的时候,不要用NODE_ENV === 'development'
。
类似的,我们在使用上面编写的处理静态文件的middleware时,也可以根据环境变量判断:
if (! isProduction) {
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));
}
这是因为在生产环境下,静态文件是由部署在最前面的反向代理服务器(如Nginx)处理的,Node程序不需要处理静态文件。而在开发环境下,我们希望koa能顺带处理静态文件,否则,就必须手动配置一个反向代理服务器,这样会导致开发环境非常复杂。
7.编写View
在编写View的时候,非常有必要先编写一个base.html
作为骨架,其他模板都继承自base.html
,这样,才能大大减少重复工作。
编写HTML不在本教程的讨论范围之内。这里我们参考Bootstrap的官网简单编写了base.html
。
8.运行
一切顺利的话,这个view-koa
工程应该可以顺利运行。运行前,我们再检查一下app.js
里的middleware的顺序:
第一个middleware是记录URL以及页面执行时间:
app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
var
start = new Date().getTime(),
execTime;
await next();
execTime = new Date().getTime() - start;
ctx.response.set('X-Response-Time', `${execTime}ms`);
});
第二个middleware处理静态文件:
if (! isProduction) {
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));
}
第三个middleware解析POST请求:
app.use(bodyParser());
第四个middleware负责给ctx
加上render()
来使用Nunjucks:
app.use(templating('view', {
noCache: !isProduction,
watch: !isProduction
}));
最后一个middleware处理URL路由:
app.use(controller());
现在,在VS Code中运行代码,不出意外的话,在浏览器输入localhost:3000/
,可以看到首页内容:
直接在首页登录,如果输入正确的Email和Password,进入登录成功的页面:
如果输入的Email和Password不正确,进入登录失败的页面:
怎么判断正确的Email和Password?目前我们在signin.js
中是这么判断的:
if (email === 'admin@example.com' && password === '123456') {
...
}
当然,真实的网站会根据用户输入的Email和Password去数据库查询并判断登录是否成功,不过这需要涉及到Node.js环境如何操作数据库,我们后面再讨论。
9.扩展
注意到ctx.render
内部渲染模板时,Model对象并不是传入的model变量,而是:
Object.assign({}, ctx.state || {}, model || {})
这个小技巧是为了扩展。
首先,model || {}
确保了即使传入undefined
,model也会变为默认值{}
。Object.assign()
会把除第一个参数外的其他参数的所有属性复制到第一个参数中。第二个参数是ctx.state || {}
,这个目的是为了能把一些公共的变量放入ctx.state
并传给View。
例如,某个middleware负责检查用户权限,它可以把当前用户放入ctx.state
中:
app.use(async (ctx, next) => {
var user = tryGetUserFromCookie(ctx.request);
if (user) {
ctx.state.user = user;
await next();
} else {
ctx.response.status = 403;
}
});
这样就没有必要在每个Controller的async函数中都把user变量放入model中。
文章来源:廖雪峰的官方网站,所有的示例我都在本机运行过,可以到http://bijian1013.iteye.com/blog/2425085下载。
koa2的官方文档资料详见http://www.koacn.com/#context,https://koa.bootcss.com/#。
koa2开发入门的更多相关文章
- Vue+koa2开发一款全栈小程序(1.课程介绍+2.ES6入门)
1.课程介绍 1.课程概述 1.做什么? Vue+koa2开发一款全栈小程序 2.哪些功能? 个人中心.图书列表.图书详情.图书评论.个人评论列表 3.技术栈 小程序.Vue.js.koa2.koa- ...
- openresty 前端开发入门五之Mysql篇
openresty 前端开发入门五之Mysql篇 这章主要演示怎么通过lua连接mysql,并根据用户输入的name从mysql获取数据,并返回给用户 操作mysql主要用到了lua-resty-my ...
- java WEB开发入门
WEB开发入门 1 进入web JAVASE:标准- standard JAVA桌面程序 GUI SOCKET JAVAEE:企业-浏览器控制 web 2 软件结构 C/S :client ...
- [译]:Xamarin.Android开发入门——Hello,Android Multiscreen深入理解
原文链接:Hello, Android Multiscreen_DeepDive. 译文链接:Xamarin.Android开发入门--Hello,Android Multiscreen深入理解. 本 ...
- [译]:Xamarin.Android开发入门——Hello,Android深入理解
返回索引目录 原文链接:Hello, Android_DeepDive. 译文链接:Xamarin.Android开发入门--Hello,Android深入理解 本部分介绍利用Xamarin开发And ...
- [译]:Xamarin.Android开发入门——Hello,Android快速上手
返回索引目录 原文链接:Hello, Android_Quickstart. 译文链接:Xamarin.Android开发入门--Hello,Android快速上手 本部分介绍利用Xamarin开发A ...
- VR原理讲解及开发入门
本文是作者obuil根据多年心得专门为想要入门的VR开发者所写,由52VR网站提供支持. 1. VR沉浸感和交互作用产生的原理: 在之前,我们观看一个虚拟的创造内容是通过平面显示器的,52VR ...
- Eclipse_luna_J2EE_For_JS+tomcat8.0环境搭建、配置、开发入门
一.所有需要的软件.插件等下载地址 J2SE的官方下载路径:http://www.oracle.com/technetwork/java/javase/downloads/index.html Ecl ...
- OWIN的理解和实践(三) –Middleware开发入门
上篇我们谈了Host和Server的建立,但Host和Server无法产出任何有实际意义的内容,真正的内容来自于加载于Server的Middleware,本篇我们就着重介绍下Middleware的开发 ...
随机推荐
- Mongodb 与 SQL 语句对照表
In addition to the charts that follow, you might want to consider the Frequently Asked Questions sec ...
- 构建NetCore应用框架之实战篇系列
构建NetCore应用框架之实战篇 构建NetCore应用框架之实战篇(一):什么是框架,如何设计一个框架 构建NetCore应用框架之实战篇(二):BitAdminCore框架定位及架构 构建Net ...
- Asp.Net从相对路径获取绝对路径的方法(不需要httpcontext上下文也可)
//如果拿不到当前HttpContext上下文的话可以用该方法取得绝对路径 var filePath = HostingEnvironment.MapPath("需要获取绝对路径 的 相对路 ...
- S11 day 95 RestFramework 之 视图
1. 基于APIVIEW视图序列化 我们会发现两个类有好多重复的代码.所以我们引入了GenericAPIView (继承了APIView) 2.Generic ApiVIew和 ListModel ...
- 769. Max Chunks To Make Sorted
Given an array arr that is a permutation of [0, 1, ..., arr.length - 1], we split the array into som ...
- 微信小程序与vueJs的异同
简而言之,所有的框架都是建立在原生javascript基础之上的,所以对于有一定js基础的同学来说,各种框架都是比较容易入手的,但不同的框架之间又有一定的差别,有时候切换使用时就会掉入坑了. 一.微信 ...
- 记录Kali Linux 安装输入法过程
1.首先设置源,打开终端输入. eafpad /etc/apt/sources.list 清空Sources.list里的内容,设置一个阿里云的源就行了. deb http://mirrors.ali ...
- ubuntu 16.04 docker下安装klee环境
手动构建docker映象: 从github上获取klee源码 git clone https://github.com/klee/klee.git cd klee 使用存储库根目录中dockerfil ...
- JavaScript里的Date 对象属性及对象方法--实现简单的日历
上网搜索"js 日历插件"就会出来各种效果的功能丰富的日历插件,很多都可以下载源码,然后根据各自的需求对源码进行修改就可以直接用了. 但今天讲的不是如何使用这些插件,而是讲如何实现 ...
- 深入理解Java接口和抽象类
对于面向对象编程来说,抽象是它的一大特征之一.在Java中,可以通过两种形式来体现OOP的抽象:接口和抽象类.这两者有太多相似的地方,又有太多不同的地方.很多人在初学的时候会以为它们可以随意互换使用, ...