Fork me on GitHub
余鸢

使用Express的Web Apps

Express是一个极小而灵活的Node.js Web应用程序框架,为构建Web应用程序提供了一套强大的功能。

Express的官方网站是expressjs.com。 源可以在GitHub上找到。

入门

您将首先需要创建一个目录,在您的shell中访问它,并通过运行npm install express -save,使用npm安装Express

创建一个文件并将其命名为app.js,并添加以下代码,该代码创建一个新的Express服务器,并使用app.get方法向其添加一个端点(/ ping):

1
2
3
4
5
6
7
8
9
const express = require('express');
const app = express();
app.get('/ping', (request, response) => {
response.send('pong');
});
app.listen(8080, 'localhost');

要运行您的脚本,请在您的shell中使用以下命令:

1
> node app.js

您的应用程序将接受本地主机端口8080上的连接。如果省略了app.listen的hostname参数,则服务器将接受计算机的IP地址以及localhost上的连接。 如果端口值为0,则操作系统将分配可用端口。

一旦您的脚本运行,您可以在shell中测试它,以确认您从服务器获得预期响应“pong”:

1
2
> curl http://localhost:8080/ping
pong

您也可以打开一个Web浏览器,浏览到url http://localhost:8080/ping来查看输出

Basic routing

首先创建一个快速应用程序:

1
2
const express = require('express');
const app = express();

那么你可以定义这样的routes:

1
app.get('/someUri', function (req, res, next) {})

该结构适用于所有HTTP方法,并且期望path作为第一个参数,并且该路径的处理程序接收请求和响应对象。 所以,对于基本的HTTP方法,这些是routes

1
2
3
4
5
6
7
8
9
10
11
// GET www.domain.com/myPath
app.get('/myPath', function (req, res, next) {})
// POST www.domain.com/myPath
app.post('/myPath', function (req, res, next) {})
// PUT www.domain.com/myPath
app.put('/myPath', function (req, res, next) {})
// DELETE www.domain.com/myPath
app.delete('/myPath', function (req, res, next) {})

您可以在此处查看支持的verbs 的完整列表。 如果要为路由和所有HTTP方法定义相同的行为,可以使用:

1
app.all('/myPath', function (req, res, next) {})

or

1
app.use('/myPath', function (req, res, next) {})

or

1
2
3
app.use('*', function (req, res, next) {})
// * wildcard will route for all paths

您可以链接单个路径的路由定义

1
2
3
4
app.route('/myPath')
.get(function (req, res, next) {})
.post(function (req, res, next) {})
.put(function (req, res, next) {})

您还可以向任何HTTP方法添加功能。 它们将在最后回调之前运行,并将参数(req,res,next)作为参数。

1
2
// GET www.domain.com/myPath
app.get('/myPath', myFunction, function (req, res, next) {})

您的最终回调可以存储在外部文件中,以避免将过多的代码放在一个文件中:

1
2
// other.js
exports.doSomething = function(req, res, next) {/* do some stuff */};

然后在包含您的routes的文件中:

1
2
const other = require('./other.js');
app.get('/someUri', myFunction, other.doSomething);

这将使您的代码更整洁。

Modular express application

使快速Web应用模块化使用路由器工厂:

Module:

1
2
3
4
5
6
7
8
9
10
11
12
// greet.js
const express = require('express');
module.exports = function(options = {}) { // Router factory
const router = express.Router();
router.get('/greet', (req, res, next) => {
res.end(options.greeting);
});
return router;
};

Application:

1
2
3
4
5
6
7
// app.js
const express = require('express');
const greetMiddleware = require('./greet.js');
express()
.use('/api/v1/', greetMiddleware({ greeting:'Hello world' }))
.listen(8080);

这将使您的应用程序模块化,可定制,您的代码可重用。

当访问http://<hostname>:8080/api/v1/greet输出将是Hello world

比较复杂的例子

具有显示中间件工厂优势的服务的示例。

Module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// greet.js
const express = require('express');
module.exports = function(options = {}) { // Router factory
const router = express.Router();
// Get controller
const {service} = options;
router.get('/greet', (req, res, next) => {
res.end(
service.createGreeting(req.query.name || 'Stranger')
);
});
return router;
};

Application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app.js
const express = require('express');
const greetMiddleware = require('./greet.js');
class GreetingService {
constructor(greeting || 'Hello, ') {
this.greeting = greeting;
}
createGreeting(name) {
return `${this.message}, ${name}!`;
}
}
express()
.use('/api/v1/service1', greetMiddleware({
service: new GreetingService('Hello'),
}))
.use('/api/v1/service2', greetMiddleware({
service: new GreetingService('Hi'),
}))
.listen(8080);

当访问http://<hostname>:8080/api/v1/service1/greet?name=World时,输出将是Hello,World并访问http://<hostname>:8080/api/v1/service2/greet?name=World的输出将是Hello,World

使用模板引擎

使用模板引擎

以下代码将将Jade设置为模板引擎。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require('express'); //Imports the express module
const app = express(); //Creates an instance of the express module
const PORT = 3000; //Randomly chosen port
app.set('view engine','jade'); //Sets jade as the View Engine / Template Engine
app.set('views','src/views'); //Sets the directory where all the views (.jade files) are stored.
//Creates a Root Route
app.get('/',function(req, res){
res.render('index'); //将index.jade文件渲染为html并作为响应返回。 渲染功能可选地将数据传递给视图。
});
//启动具有回调的Express服务器
app.listen(PORT, function(err) {
if (!err) {
console.log('Server is running at port', PORT);
} else {
console.log(JSON.stringify(err));
}
});

类似地,也可以使用其他模板引擎,例如Handlebars(hbs) 或ejs。 记得要安装npm install模板引擎。 对于Handlebars,我们使用hbs包,对于Jade,我们有一个 jade包,对于EJS,我们有一个ejs包。 PS:Jade最近更名为pug

EJS模板示例

使用EJS(像其他快速模板),您可以运行服务器代码并从您的HTML访问您的服务器变量。
在EJS中,使用“<%”作为开始标签,将“%>”作为结束标签,可以使用<%=var_name%>访问作为渲染参数传递的变量。

例如,如果您的服务器代码中有supplies array

你可以循环使用它

1
2
3
4
5
6
7
8
9
<h1><%= title %></h1>
<ul>
<% for(var i=0; i<supplies.length; i++) { %>
<li>
<a href='supplies/<%= supplies[i] %>'>
<%= supplies[i] %>
</a>
</li>
<% } %>

从示例中可以看出,每当您在服务器端代码和HTML之间切换时,都需要关闭当前的EJS标签,并在之后打开一个新的EJS标签,因此我们希望在for命令中创建li,因此我们需要关闭EJS标签 在最后,为大括号创建新标签
另一个例子
如果要将输入的默认版本从服务器端变为一个变量,我们使用<%=

例如:

1
2
Message:<br>
<input type="text" value="<%= message %>" name="message" required>

这里从服务器端传递的消息变量将是您的输入的默认值,请注意,如果您没有从服务器端传递消息变量,EJS将抛出异常。 您可以使用res.render('index', {message: message}); 传递参数(对于名为index.ejs的ejs文件)。

在EJS标签中,您还可以使用ifwhile或任何其他JavaScript命令。

提供静态文件

使用Express构建Web服务器时,通常需要提供动态内容和静态文件的组合。

例如,您可能将index.html和script.js作为文件系统中保留的静态文件。

通常使用名为“public”的文件夹具有静态文件。 在这种情况下,文件夹结构可能如下所示:

1
2
3
4
5
6
7
project root
│ server.js
│ package.json
└───public
│ index.html
│ script.js

这是如何配置Express来提供静态文件:

1
2
3
4
var express = require('express');
var app = express();
app.use(express.static('public'));

注意:一旦文件夹被配置,index.html,script.js和“public”文件夹中的所有文件将在根路径中可用(您不能在url中指定/ public /)。 这是因为,express快速查找相对于配置的静态文件夹的文件。 您可以指定虚拟路径前缀,如下所示:

1
app.use('/static', express.static('public'));

将在/ static / prefix下使资源可用。

多个文件夹

可以同时定义多个文件夹:

1
2
3
app.use(express.static('public'));
app.use(express.static('images'));
app.use(express.static('files'));

Express服务器将以定制顺序检查文件夹。 如果具有相同名称的文件,则第一个匹配文件夹中的文件将被提供。

添加中间件

中间件功能是可以访问请求对象(req),响应对象(res)以及应用程序的请求 - 响应周期中的下一个中间件函数的函数。

中间件功能可以执行任何代码,更改res和req对象,结束响应周期并调用下一个中间件。

中间件的很常见的例子是cors模块。 要添加CORS支持,只需安装它,就要求它并放置此行:

1
app.use(cors());

在任何路由器或路由功能之前。

Error Handling

Basic Error Handling

默认情况下,Express将在/views目录中查找“error”视图来呈现。 只需创建“error”视图并将其放在视图目录中来处理错误。 使用错误消息,状态和堆栈跟踪写入错误,例如:

views/error.pug

1
2
3
4
5
html
body
h1= message
h2= error.status
p= error.stack

Advanced Error Handling

在中间件功能堆栈的最后定义您的错误处理中间件功能。 这些有四个参数而不是三个(err,req,res,next),例如:

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 抓取404并转发到错误处理程序
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
//将错误传递到下一个匹配路由。
next(err);
});
// handle error, print stacktrace
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});

您可以定义几个错误处理中间件函数,就像常规中间件功能一样。

Express中的错误处理

在Express中,您可以定义统一的错误处理程序来处理应用程序中发生的错误。 在所有路由和逻辑代码的末尾定义处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var express = require('express');
var app = express();
//GET /names/john
app.get('/names/:name', function(req, res, next){
if (req.params.name == 'john'){
return res.send('Valid Name');
} else{
next(new Error('Not valid name')); //pass to error handler
}
});
//error handler
app.use(function(err, req, res, next){
console.log(err.stack); // e.g., Not valid name
return res.status(500).send('Internal Server Occured');
});
app.listen(3000);

从请求获取信息

从请求的URL获取信息(请注意,req是路由的处理函数中的请求对象)。 考虑这个路由定义/settings/:user_id和这个特定的例子/settings/32135?field=name

1
2
3
4
5
6
7
8
// get the full path
req.originalUrl // => /settings/32135?field=name
// get the user_id param
req.params.user_id // => 32135
// get the query value of the field
req.query.field // => 'name'

您也可以获取请求的标题,像这样

1
2
req.get('Content-Type')
// "text/plain"

为了简化获取其他信息,您可以使用中间件。 例如,要获取请求的身体信息,您可以使用 body-parser 中间件,这将将原始请求体转换为可用格式。

1
2
3
4
5
var app = require('express')();
var bodyParser = require('body-parser');
app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded

现在假设这样的请求

1
2
3
4
PUT /settings/32135
{
"name": "Peter"
}

您可以访问这样发布的名称

1
2
req.body.name
// "Peter"

以类似的方式,您可以从请求访问Cookie,您还需要一个中间件,如 cookie-parser

1
req.cookies.name

Hook:如何在任何req之前和之后执行代码

app.use()和中间件可以用于“before”,并且 closefinish事件的组合可以用于“after”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.use(function (req, res, next) {
function afterResponse() {
res.removeListener('finish', afterResponse);
res.removeListener('close', afterResponse);
// actions after response
}
res.on('finish', afterResponse);
res.on('close', afterResponse);
// action before request
// eventually calling `next()`
next();
});
...
app.use(app.router);

这个例子就是logger中间件,默认情况下它将追加到日志中。

只要确保这“中间件”是用在app.router为顺序无关紧要。

具有ExpressJS的JSON API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var express = require('express');
var cors = require('cors'); // Use cors module for enable Cross-origin resource sharing
var app = express();
app.use(cors()); // for all routes
var port = process.env.PORT || 8080;
app.get('/', function(req, res) {
var info = {
'string_value': 'StackOverflow',
'number_value': 8476
}
res.json(info);
// or
/* res.send(JSON.stringify({
string_value: 'StackOverflow',
number_value: 8476
})) */
//you can add a status code to the json response
/* res.status(200).json(info) */
})
app.listen(port, function() {
console.log('Node.js listening on port ' + port)
})

http://localhost:8080/ output对象上

1
2
3
4
{
string_value: "StackOverflow",
number_value: 8476
}

在Express定制中间件

在Express中,您可以定义可用于检查请求或设置某些标题的中间件。

1
app.use(function(req, res, next){ }); // signature

以下代码将user添加到请求对象,并将控件传递给下一个匹配路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var express = require('express');
var app = express();
//each request will pass through it
app.use(function(req, res, next){
req.user = 'testuser';
next(); // it will pass the control to next matching route
});
app.get('/', function(req, res){
var user = req.user;
console.log(user); // testuser
return res.send(user);
});
app.listen(3000);

Named routes in Django-style

一个大问题是Express开箱即用不支持有valuable的命名路由。 解决方案是安装支持的第三方软件包,例如 express-reverse

1
npm install express-reverse

将其插入到您的项目中:

1
2
var app = require('express')();
require('express-reverse')(app);

然后像这样使用它:

1
2
3
app.get('test', '/hello', function(req, res) {
res.end('hello');
});

这种方法的缺点是您不能使用高级路由器使用中所示的路由Express模块。 解决方法是将您的app作为参数传递给您的路由器工厂:

1
require('./middlewares/routing')(app);

像这样使用:

1
2
3
4
5
module.exports = (app) => {
app.get('test', '/hello', function(req, res) {
res.end('hello');
});
};

您可以从现在开始,如何定义函数将其与指定的自定义命名空间合并并指向适当的控制器。

使用cookie-parser设置cookie

以下是使用 cookie-parser模块设置和读取Cookie的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var express = require('express');
var cookieParser = require('cookie-parser'); // module for parsing cookies
var app = express();
app.use(cookieParser());
app.get('/setcookie', function(req, res){
// setting cookies
res.cookie('username', 'john doe', { maxAge: 900000, httpOnly: true });
return res.send('Cookie has been set');
});
app.get('/getcookie', function(req, res) {
var username = req.cookies['username'];
if (username) {
return res.send(username);
}
return res.send('No cookie found');
});
app.listen(3000);

Error handling

基本文档可以在这里找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
app.get('/path/:id(\\d+)', function (req, res, next) { // please note: "next" is passed
if (req.params.id == 0) // validate param
return next(new Error('Id is 0')); // go to first Error handler, see below
// Catch error on sync operation
var data;
try {
data = JSON.parse('/file.json');
} catch (err) {
return next(err);
}
// If some critical error then stop application
if (!data)
throw new Error('Smth wrong');
// If you need send extra info to Error handler
// then send custom error (see Appendix B)
if (smth)
next(new MyError('smth wrong', arg1, arg2))
// Finish request by res.render or res.end
res.status(200).end('OK');
});
// Be sure: order of app.use have matter
// Error handler
app.use(function(err, req, res, next)) {
if (smth-check, e.g. req.url != 'POST')
return next(err); // go-to Error handler 2.
console.log(req.url, err.message);
if (req.xhr) // if req via ajax then send json else render error-page
res.json(err);
else
res.render('error.html', {error: err.message});
});
// Error handler 2
app.use(function(err, req, res, next)) {
// do smth here e.g. check that error is MyError
if (err instanceof MyError) {
console.log(err.message, err.arg1, err.arg2);
}
...
res.end();
});

Appendix A

1
2
3
4
5
6
// "In Express, 404 responses are not the result of an error,
// so the error-handler middleware will not capture them."
// You can change it.
app.use(function(req, res, next) {
next(new Error(404));
});

Appendix B

1
2
3
4
5
6
7
8
9
10
11
// How to define custom error
var util = require('util');
...
function MyError(message, arg1, arg2) {
this.message = message;
this.arg1 = arg1;
this.arg2 = arg2;
Error.captureStackTrace(this, MyError);
}
util.inherits(MyError, Error);
MyError.prototype.name = 'MyError';

处理POST请求

就像使用app.get方法处理Express中的get请求一样,您可以使用app.post方法来处理帖子请求。

但在您可以处理POST请求之前,您将需要使用body-parser中间件。 它只是解析POSTPUTDELETE和其他请求的正文。

Body-Parser中间件解析请求的正文,并将其转换为req.body中可用的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var bodyParser = require('body-parser');
const express = require('express');
const app = express();
// Parses the body for POST, PUT, DELETE, etc.
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.post('/post-data-here', function(req, res, next){
console.log(req.body); // req.body contains the parsed body of the request.
});
app.listen(8080, 'localhost');

Hello World

在这里,我们使用Express创建一个基本的hello world服务器。路线:

  • ‘/‘
  • ‘/wiki’

rest 时会给出“404”,即找不到页面。

1
2
3
4
5
6
7
8
9
10
'use strict';
const port = process.env.PORT || 3000;
var app = require('express')();
app.listen(port);
app.get('/',(req,res)=>res.send('HelloWorld!'));
app.get('/wiki',(req,res)=>res.send('This is wiki page.'));
app.use((req,res)=>res.send('404-PageNotFound'));

注意:我们将404路由作为最后一条路由,作为快速堆叠路由,并按顺序对每个请求进行处理。

使用中间件和next回调

Express将next回调传递给每个路由处理程序和中间件功能,可用于跨多个处理程序的单个路由断开逻辑。 使用no参数调用next()可以指示express继续下一个匹配的中间件或路由处理程序。 使用错误调用next(err)将触发任何错误处理程序中间件。 调用next('route')将绕过当前路由上的任何后续中间件,并跳转到下一个匹配路由。 这允许域逻辑被分离成可重复使用的组件,这些组件是独立的,更简单的测试,更易于维护和更改。

多个匹配路由

/api/foo/api/bar的请求将运行初始处理程序来查找成员,然后将控制权传递给每个路由的实际处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.get('/api', function(req, res, next) {
// Both /api/foo and /api/bar will run this
lookupMember(function(err, member) {
if (err) return next(err);
req.member = member;
next();
});
});
app.get('/api/foo', function(req, res, next) {
// Only /api/foo will run this
doSomethingWithMember(req.member);
});
app.get('/api/bar', function(req, res, next) {
// Only /api/bar will run this
doSomethingDifferentWithMember(req.member);
});

Error handler

错误处理程序是具有签名function(err, req, res, next)的中间件。 它们可以按路径设置(例如app.get('/foo', function(err, req, res, next)),但通常,呈现错误页面的单个错误处理程序就足够了。

1
2
3
4
5
6
7
8
9
10
11
12
13
app.get('/foo', function(req, res, next) {
doSomethingAsync(function(err, data) {
if (err) return next(err);
renderPage(data);
});
});
// In the case that doSomethingAsync return an error, this special
// error handler middleware will be called with the error as the
// first parameter.
app.use(function(err, req, res, next) {
renderErrorPage(err);
});

中间件

上述每个功能实际上是一个中间件功能,只要请求与定义的路由匹配,就可以运行,但是可以在单个路由上定义任意数量的中间件功能。 这样就可以将中间件定义在单独的文件和常见逻辑中,以便跨多个路由重用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
app.get('/bananas', function(req, res, next) {
getMember(function(err, member) {
if (err) return next(err);
// If there's no member, don't try to look
// up data. Just go render the page now.
if (!member) return next('route');
// Otherwise, call the next middleware and fetch
// the member's data.
req.member = member;
next();
});
}, function(req, res, next) {
getMemberData(req.member, function(err, data) {
if (err) return next(err);
// If this member has no data, don't bother
// parsing it. Just go render the page now.
if (!data) return next('route');
// Otherwise, call the next middleware and parse
// the member's data. THEN render the page.
req.member.data = data;
next();
});
}, function(req, res, next) {
req.member.parsedData = parseMemberData(req.member.data);
next();
});
app.get('/bananas', function(req, res, next) {
renderBananas(req.member);
});

在这个例子中,每一个中间件函数都可以是它自己的文件,也可以是文件中的其他变量,这样它可以在其他的路由中被重用。