Express4入门指南

Category:
发表:
大纲
  1. 1. 安装和基本示例
  2. 2. 工作原理
  3. 3. 中间件机制
    1. 3.1. 中间件的分类
  4. 4. Router机制
    1. 4.1. 模式匹配和参数
    2. 4.2. 路由回调
    3. 4.3. app.router()
  5. 5. 模板引擎

Express是基于Nodejs以及一系列Nodejs第三方package的一款Web开发框架。

Express经历过2.x,3.x以及最新的4.x版本。Express各个版本的差异还是比较大的。现在2.x版本官方已经不再维护(deprecated),3.x版本虽然可以使用,但是官方建议升级到4.x版本。如果你之前使用的express 3.x版本,官方也有给出迁移到4.x的指南

本文围绕express的Routing和Middleware机制,作一些讨论。主要参考express官网的guide。

安装和基本示例

我们首先从最基本的讲起。首先我们得有一个nodejs项目,

> mkdir test & cd test
> npm init

我一般习惯使用npm init来创建一个nodejs项目。因为它可以提供一个交互式的命令行来生成最基本的package.json文件。

在创建好package.json文件之后,接下来我们就需要安装express了,

> npm install express --save

我们使用--save选项自动更新package.jsondependencies字段。在express安装结束之后,package.json就变成了下面这样了,

{
  "name": "test",
  "version": "1.0.0",
  "description": "kick off express4",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "nodejs"
  ],
  "author": "gejiawen",
  "license": "MIT",
  "dependencies": {
    "express": "^4.13.3"
  }
}

根据你系统的差异或者网络因素,你可能需要使用sudo npm install express --save,或者是sudo cnpm install express --save

使用npm安装包的时候,除了--save选项,我们还可以使用--save-dev。两者在安装完包之后,都可以自动更新package.json文件,不同的是,前者更新的是dependencies字段,后者更新的是devDependencies字段。而dependenciesdevDependencies的区别可以简单的理解成,前者是生产环境所需要的依赖,而后者是开发环境所需要的依赖。

在安装完毕express之后,我们需要新建一个启动文件,就是package.jsonmain字段指定的文件。当然你说我能不能新建一个文件叫别的什么名字,那当然也是没问题的。

> touch index.js

index.js文件的内容如下,

var express = require('express');
var app = express();

app.get('/', function (req, res) {
    res.send('Hello world!');
});

app.listen(3000, function () {
    console.log('app is listening at localhost:3000');
});

最后再启动这个文件,

> node index.js

至此,我们使用express开发的一个最最简单的web程序就完成了。可以我浏览器输入localhost:3000,看看效果吧。

工作原理

经过上面的简单示例,看起来使用express做web真的非常简单。为什么可以做到这么简单呢?这是跟express的设计理念有关系的。

整个express应用可以简单的抽象成一系列的路由和中间件。当然这些路由和中间件之间存在着调用和先后关系。

那么,路由和中间件具体又是指的是什么呢?

简单来说,express中的路由可以看成一种path watcher,比如上面示例中的代码,

app.get('/', function (req, res) {
    res.send('Hello world!');
});

这段代码中,express app将监控/路径,来自客户端所有对/路径的请求,都使用后面的回调函数处理。

而这里的处理请求的回调函数其实就是所谓的中间件。

所以,中间件其实就是处理HTTP请求的回调函数,一般来说它会有3个参数,分别是reqresnext,分别表示请求对象,响应对象以及next回调。next回调的作用是将控制权传递给下一个中间件。

如果一个中间件是专门用于错误处理的,它将会有4个参数,分别是errreqresnext。其中第一个参数表示错误对象。

中间件的基本执行过程是这样的,在中间件内部对req和res对象进行处理加工,执行相关逻辑,然后根据业务决定是否执行next()函数,传递到下一个中间件

除此之外,一般我们建议一个中间件只专注做一件事,也就是说中间件的职责尽量单一,这样利于维护和协调中间件。

中间件机制

下面我们来详细的说一说express的中间件机制。

其实所谓的中间件就是一个函数回调。express中采用use来注册中间件。比如,

var expresss = require('express');
var app = express();

app.use(function (req, res, next) {
    console.log('Time: ', new Date());
    next();
});

// more code...

如上例所示,这个中间件是整个app的第一个中间件,当有请求过来时,它将会被第一个调用,在console中打印出时间信息。执行完毕之后,通过next()将执行权传递给后面的中间件。

此外,我们可以再中间件内容做一些额外的事情,比如对请求路径的判断,

app.use(function(req, res, next) {
    if (request.url === "/") {
        response.writeHead(200, { "Content-Type": "text/plain" });
        response.end("Welcome to the homepage!\n");
    } else {
        next();
    }
});

app.use(function(req, res, next) {
    if (req.url === "/about") {
        res.writeHead(200, { "Content-Type": "text/plain" });
        res.end("about page!\n");
    } else {
        next();
    }
});

app.use(function(req, res, next) {
    res.writeHead(404, { "Content-Type": "text/plain" });
    res.end("404 error!\n");
})

上面的代码表示,

  • 如果请求的是/路径,返回给客户端Welcome to the homepage!。如果是其他路径,就调用下一个中间件。
  • 第二个中间件中,判断路径是否是/about,如果是,将返回给客户端about page!。如果不是,调用下一个中间件。
  • 当执行权传递到第三个中间件时,那么客户端请求的路径不是/也不是/about,此时我们将给出一个404 error!的提示,并同时终止中间件的继续调用。

从上面的示例代码,我们足以管中窥豹,领略一下express中间件机制,以及它们的调用策略。简单来说就是,express按照顺序执行中间件,每个中间件做好自己的任务并决定是否调用下一个中间件

中间件的分类

express的官网上对中间件作了个分类,包含如下几种,

应用级别的中间件其实就是指挂在app上的中间件。通常我们除了可以通过app.use来挂载中间件之外,express还提供了app.METHOD这种方式。这里的METHOD指的是http的方法。比如

  • app.get
  • app.post
  • app.put

除此之外,还有一个特别的动词我们也可以使用,就是app.all(),它表示所有的请求都会通过这个中间件。看下面的示例代码,

app.all("*", function(request, response, next) {
    response.writeHead(200, { "Content-Type": "text/plain" });
    next();
});

app.get("/", function(request, response) {
    response.end("Welcome to the homepage!");
});

app.get("/about", function(request, response) {
    response.end("Welcome to the about page!");
});

app.get("*", function(request, response) {
    response.end("404!");
});

所有的请求都将会执行第一个中间,用户设置http响应的头信息。如果请求的是/或者/about,则执行相应的中间件。这里注意,第二个和第三个中间件都没有调用next(),所以他们执行完毕之后,并不会调用后面的中间件。如果请求的路径不是/或者/about,那么第二个和第三个中间件都将不会被调用,第四个中间件将会被调用。

所以说,express应用中中间件的顺序是很重要的,它将影响中间件的执行顺序。

Router机制

前面我们知道,可以通过app.use或者是app.METHOD('/')来配置路由。

在Express4中,路由成了一个单独的组件,express提供了一个新的对象express.Router,可以通过它来更好的配置路由。我们先来看一个示例,

// birds.js
var express = require('express');
var router = express.Router();

router.use(function (req, res, next) {
    console.log('Time: ', new Date());
    next();
});

router.get('/', function (req, res) {
    res.send('Birds home page');
});

router.get('/about', function (req, res) {
    res.send('Birds about page');
});

module.exports = router;
// index.js
var express = require('express');
var birds = require('./birds');

var app = express();

app.get('/', function (req, res) {
    res.send('app home page');
});

app.use('/birds', birds);

app.listen(3000, function () {
    console.log('server starting...');
});

通过这个简单的例子,我们可以独立封装一组路由成为一个路由中间件,在主文件中可以根据需要挂在到不同的路径上,如app.use('/birds', birds)。这样一来,路由中间件中对路由的配置最终会被解析成如下,

  • /birds/
  • /birds/about/

我们使用express进行web开发的时候,一般也是推荐建立一个routes文件夹,将所有页面的路径配置都抽象成一个路由中间件,然后在主文件中挂载。

模式匹配和参数

使用进行路由配置的时候,我们不仅仅可以使用字符常量,还可以使用字符串的模式匹配,如下,

// will match acd and abcd
app.get('/ab?cd', function(req, res) {
    res.send('ab?cd');
});

// will match abcd, abbcd, abbbcd, and so on
app.get('/ab+cd', function(req, res) {
    res.send('ab+cd');
});

// will match abcd, abxcd, abRABDOMcd, ab123cd, and so on
app.get('/ab*cd', function(req, res) {
    res.send('ab*cd');
});

// will match /abe and /abcde
app.get('/ab(cd)?e', function(req, res) {
    res.send('ab(cd)?e');
});

// will match anything with an a in the route name:
app.get(/a/, function(req, res) {
    res.send('/a/');
});

// will match butterfly, dragonfly; but not butterflyman, dragonfly man, and so on
app.get(/.*fly$/, function(req, res) {
    res.send('/.*fly$/');
});

如你所见,路由的模式匹配其实很简单。你可以使用正则,让你的路由回调更加随心所欲的匹配不同的路径。

路由参数的意思就是你可以在路由配置时定义好路径中带有的参数,比如

app.get('/book/:bookId', function (req, res, next) {
    console.log(req.params);
});

这样你就可以在回调中,处理req中的params对象。

路由回调

有时候我们可能会有这样一个需求,我需要在路由回调中进行一些预处理。比如下面这个例子

var express = require('express');
var app = express();

app.get('/user/:id', function (req, res, next) {
    if (req.params.id === 0) {
        next('route');
    } else {
        next();
    }
}, function (req, res, next) {
    res.send('regular');
});

app.get('/user/:id', function (req, res, next) {
    res.send('special');
});

app.listen(3000);

上面的示例代码,有两点需要注意,

1.我们可以在路由配置的回调中,添加多个回调函数。如果有多个回调,我们可以有两种形式,如下,

app.get('/', function() {}, function() {});

app.get('/', [callback1, callback2, callback3]);

2.同一个路由配置的多个回调函数在执行时可以被跳过。

上面代码中的next('route')表示跳过当前路由中间件中剩下的路由回调,执行下一个中间件。而next()表示执行当前中间件的下一个路由回调。所以,当我请求/user/100时,返回的是regular。当我请求/user/0时,返回的是special。

app.router()

在express4中我们可以使用app.router对同一个路径做不同的请求方法配置,如下,

app.route('/book')
    .get(function(req, res) {
        res.send('Get a random book');
    })
    .post(function(req, res) {
        res.send('Add a book');
    })
    .put(function(req, res) {
        res.send('Update the book');
    });

其实这种方式在路由中间件中也是可以用的。针对特定的路由定制场景是特别适用的,可以更好的模块化、节省一些冗余的代码。

模板引擎

之前我们的示例代码中,都是简单是使用res.send给客户端发送简单的文本。如果我们想要客户端渲染一个自定义的页面,那该怎么做呢?

这就需要用到模板了。严格来说,express使用的模板属于服务端模板,因为它是在服务端解析完毕,发送给客户端进行渲染的。express支持很多模板引擎,基本上将市面上常见模板引擎一网打尽了。express默认的模板引擎是jade,一款非常优雅的模板引擎。后面博主会有文章介绍jade。

言归正传,我们在express中如何使用模板引擎来加载模板呢?其实很简单,三步走即可,

  1. 设置好模板的静态路径
  2. 设置好渲染的模板引擎
  3. 在需要的地方渲染你的模板

具体看下面的示例代码,

// 省略无关代码

// 设置模板文件夹的路径
app.set('views', path.join(__dirname, 'views'));
// 设置模板文件的后缀名
app.set('view engine', 'jade');

app.get('/', function (req, res){
    res.render('index');
});
app.get('/about', function(req, res) {
    res.render('about');
});
app.get('/article', function(req, res) {
    res.render('article');
});

// 省略无关代码

我们在views目录下有3个模板,分别为views/index.jadeviews/about.jadeviews/article.jade,当请求不同的路径时,我们会发送不同的模板给前端渲染。

如果你使用的是别的模板引擎,请参阅模板引擎针对express的相关说明。

本文由于篇幅原因,不会关注express的方方面面,更多的内容请参考官网的API Reference