第五章:node学习之中间件

node学习之中间件


前言

本系列文章是通过学习Mosh的视频node教程全方位Node开发-Mosh而整理的笔记,该教程是英文的,有中文字幕,感谢marking1212提供中文字幕翻译


上个章节我们学习了如何使用Exoress创建RESTful服务,这个章节我们将了解Express更多深层的知识,我们会特别关注中间件,配置,查虫,模板引擎等等

一、中间件

Express中的一个核心概念就是中间件或者中间函数,一个中间函数,技术上说就是得到一个请求对象,要么反馈客户端,要么传递给另一个中间函数,你已经见过两个中间函数了,一个是路由句柄函数,

 (req, res) => {
  res.send(courses);
}

在Express中,所有路由句柄函数都是中间函数,因为他需要传入一个请求对象,并且在这里想客户端返回数据,它终结了请求反馈闭环,这就是中间函数的一个例子.
第二个中间函数是

app.use(express.json()); //req.body

当我们调用express.json方法时,这个函数返回一个函数对象:一个中间函数,这个函数的作用就是读取和请求,如果请求体是一个JSON格式对象,它就会格式化这个JSON对象并以此设置req.body属性,这就是运行时发生的事情.
在这里插入图片描述

当服务器收到一个请求,请求就进入一个管道,我们将这个管道称为请求处理管道,管道之中有一个或多个中间函数,每个函数要么根据请求向客户端返回数据,要么将控制权交给其他的中间函数.
在之前的例子中,请求处理管道中有两个中间函数,第一个中间函数是将请求转换为一个JSON格式对象,这个情况下并没有终结请求反馈闭环,这样它就将控制器交给下一个中间函数,这里就是路由句柄,在路由句柄中,req.body属性已经设置好,这样就可以进行一些操作了,然后向客户端发送反馈来终结请求反馈闭环
Express有一些内建中间函数,你同样也可以在管道中添加自定义的中间函数,每个服务器获得的请求都会转到中间函数,使用自定义的中间函数,我们可以创建横切关注点(Crossutting Concerns) 比如我们可以实现登录,验证,认证等功能,所以Express总体来说就是一堆中间函数

二、创建自定义中间件

现在我们来看看如何创建自定义中间件,我们通过调用use方法在请求处理管道中安插一个中间件

app.use(function (req, res, next) {
  console.log('Logging...');
  next();
});

它有3个参数mrequest,response和next,next表示管道中下一个中间件的引用,这样就能更简单的调用中间件,这里就打印一下,打印’登陆中“,我们假设这个中间件处理的是登录功能,处理了登录,我们就调用next来讲控制权交给下一个中间件,如果你没有这么做,你就没有闭合请求反馈闭环,我们的请求就会无限期的挂起.
我们可以在创建另一个授权中间件:

app.use(function (req, res, next) {
  console.log('Authenticating...');
  next();
});

需要引起注意的是,中间件是按顺序调用的,首先是登录函数的调用,然后是授权函数的调用,然后就是另一个路由函数了.
为了让代码更简明,当我们创建中间件的时候,我们不用将所有代码都写在index文件或者模块当中,我们要将每个中间件放在各自独立的文件中.
我们创建logger.js文件把放在其中

function log(req, res, next) {
  console.log('Logging...');
  next();
}
app.use(express.json());

现在你知道这一行是什么意思了吧,当我们调用express.json,它返回了一个需要3个参数的函数,分别是request,response和next,它格式化了请求体,如果是一个JSON对象它就设置好req.body属性,然后就会把控制权交给下一个中间件
这就是我们在独立模块中定义自定义中间件的方法,先导入然后通过app.use方法调用

三.Express内建中间件

Express有很多内建的中间件,你已经很熟悉JSON中间件了,还有另一个类似的中间件,那就是urlencode
这个函数的作用是读取通过urlencode传递的数据,它的请求体格式都是key=value的键值对格式,这是一种传统的格式,现在不常用了

app.use(express.urlencode()); //key=value&key=value

如果是一个HTML表单,里面的输入域需要在服务器创建信息,它的请求体就是这个样子,这就是可以读取请求体中urlencode格式的数据中间件,这个中间件读取这种数据格式,并设置好req.body属性

app.use(express.urlencode({extended:true)); 

使用这个中间件,会出现一个警告,告诉我们应该传入一个对象,这样我们就可以通过urlencode格式传递数组或者复杂的表单数据.

最后一个Express内建中间件是static,我们用它来向外提供静态内容

app.use(express.static('public'));

我们调用express的static方法,这里需要提供一个静态内容的文件夹,我们传入public,我们可以将所有静态内容,比如css,图片到这个文件夹中

四.第三方中间件

访问expressjs.com,最顶端的资源菜单,可以找到中间件菜单,这里有所有你可以在应用中使用的第三方中间件,这并不意味着你要全部用到,因为每个中间件对你应用的运行都会带来影响,如果你不需要中间件的功能,就不要使用,否则只会降低你Express的执行效率,所有花点时间好好研究一下你的应用需要什么中间件,

const express = require("express");
const helmet = require("helmet");

const app = express();

app.use(helmet());

这个例子中为了最佳实践要用到的是Helmet,它可以帮助你通过设置http头部来加强安全性,基本上我们需要做的只是加载Helmet模块,得到一个函数,我们调用就可以得到中间件,然后我们就use它,这就是全部需要做的了.
如果你要深入了解Helmet是怎么运作的,你就需要深入阅读文档了

另一个有用的中间件是Morgon,我们使用Morgan来进行HTTP请求的日志记录,文件最顶端导入morgan模块

const morgan = require('morgan');

Morgan是一个函数,这里注意到它需要提供字符串参数format, 这里使用最简单的格式,tiny

app.use(morgan('tiny'));

有了Morgan,每次服务器收到的HTTP请求都会被日志记录,这里我们向课程终端简单发送一个get请求

GET /api/courses 200 79 - 3.341 ms

Morgan记录了请求的信息,这是tiny格式,非常简单的内容,首先是向这个终端发送了get请求,状态码是代表成功的200,然后就是反馈这个请求所花费的时间,这是tiny格式,如果你需要更多的细节就使用其他格式.
Morgan默认请求是在控制台记录日志,你可以设置它将日志写在日志文件中,同样记住如果你打开这个功能,对你的运行效率就会有影响,你可能不会在开发环境使用这个功能,或者你只会在特定的场合才开启这个功能.
你可以有一个配置文件,在生产环境中的某些特定场景下,你可以短时间开启这个功能然后就关闭.

五.环境

在更加复杂的应用中,我们需要知道应用运行在什么环境中,到底是开发环境,还是生产环境,也许你想依照环境类型决定是否开关某个功能,例如,我们只打算在开发环境中开启对HTTP请求的日志记录,而非生产环境,我们该如何做呢?

app.use(morgan('tiny'));

之前我们介绍过process对象,它是node的全局对象,通过它可以访问当前的进程,这个对象有一个env属性,它提供我们环境变量的值,有一个标准的环境变量是NODE_ENV,这个值返回的是当前node所在环境的值,如果没有设置,他会返回未定义

process.env.NODE_ENV;

同样,我们可以在外部设置它,可以设置为开发,转场或者生产
现在我们来打印它:

consolo.log(`NODE_ENV: ${process.env.NODE_ENV}`;

还有另一种获得当前环境变量的方法,它是app对象的一个方法,app对象中有个方法get,它可以获得当前系统的多个设定值,其中一个设定值就是env

app.get('env')

这个方法内部就是调用了NODE_ENV的值,但是如果这个值未定义,这个方法默认返回开发环境的值.
这两种方法用哪个全看你的喜好。
我们来看这个例子:

if (app.get('env') === 'development') {
  app.use(morgan('tiny'));
  console.log('Morgan enabled...');
}

运行后控制台会显示

Morgan enabled...
Listening on port 3000...

因为这是开发环境,所以看到了提示信息,现在我们把环境设置为生产环境

set NODE_ENV=production

再运行一次,提示语将不会出现.

六.配置

上节我们学习了如何检测当下的环境变量,一个同样的问题出现了,就是我们如何保存应用的配置数据,并且在不同环境复写对应配置,比如在测试环境,你可以需要使用一个不同的数据库或者邮件服务器,所以这节我们来学习如何保存应用的配置,并在不同的环境下复写它
有非常多实现应用配置管理的包,最受欢迎的就是RC了,还有一个更好更易用的管理功能包config ,回到控制台,我们安装config包

npm i config

回到项目组,创建一个config文件夹,文件夹里先创建一个默认配置文件,deault.json
我们可以用一个JSON对象来保存配置信息,我们可以添加配置

{
    "name":"My Express App"
}

然后我们创建另一个文件 development.json ,这个文件中可以保存特定给开发环境的配置信息,这里的配置会复写default文件中的对应配置,也可以添加附加的配置,

 "name":"My Express App -Devalopment"

同样可以添加一个新的配置,这个配置也可以是一个复杂的对象

  "mail":{
        "host":"dev-mail-server"
    }

同样的,我们也可以创建production.json文件,保存对应的配置.
利用config模块我们可以轻松是设置不同环境的配置,但是,不能将应用的机密信息保存在这里,例如:你不能将数据库密码或者邮件服务器密码存在这里,因为当你在仓库查看源代码时,有权查看源代码的所有人都能看到这些机密信息.
当我们处理这些机密的时候,我们应该保存在环境变量中,让我们来看看怎么做
我们来设置一个保存邮件服务器密码的环境变量,

 set app_password=1234

在开发环境中,我们手动设置这些变量,在生产环境中,很可能有个操作面板来操作这些变量,我们将所有的密码和机密保存在环境变量中,然后用config模块读取它们,
我们在config文件夹下,创建一个

custom-environment-variables.json

文件,这个文件中映射环境变量和应用配置的关系,这里我们要映射我们刚刚设置的环境变量的值,就是app_password,

"password":"app_password"

在custom-environment-variales文件中只有映射关系,这里只有password到app_password的映射关系.
利用config对象可以非常方便的得到配置信息,信息源可以是一个JSON文件,也可以是环境变量,甚至是命令行的值.

七.调试

console.log语句作为JavaScript开发者用的最多,在调试的时候总是用它,有时的问题是当调试完成,我们就会删掉或者注释掉它,有时候又需要调试了,我们又回头来改,这种操作非常繁琐,更好的在控制台调试的方法是使用node的debug模块,
使用debug模块就是用debug函数来替换所有的打印命令,我们可以用一个环境变量来控制是否开启调试状态,我们就不用回到代码中修改源代码,我们不需要删除打印命令或者调试命令,我们不需要注释掉它们,可以通过在外部修改环境变量实现,更重要的是,你可以控制调试的层级
也许有时你在调试数据库,你就只想看到数据库相关的调试信息,同样,你不需要回到源码中重复修改代码.接下来我们安装debug模块

npm i debug

导入debug模块

require('debug');

这里的require返回的是一个函数,我们可以调试这个函数并给它参数,这里给它的是一个用于调试的专用命名空间,比如我们可以定义一个用于调试的命名空间app:startup

require('debug')("app:startup")

我们就得到另一个函数,以这个命名空间来显示提示信息,我们就将这个函数命名为startupDebugger

const startupDebugger = require('debug')("app:startup")

可能我们需要另一个用于调试数据库的命名空间,同样我们加载debug模块,我们给返回的函数一个参数命名空间app:db ,我们将它保存在dbDebugger常量中

const dbDebugger = require('debug')('app:db');

然后我们将

console.log('Morgan enabled...');

替换为

startupDebugger('Morgan enabled...')

可能在后面会做一些数据库操作的逻辑,这里你就需要点调试的信息,我们就可以调用另一个dbDebugger函数
现在我们回到控制台,来配置我们需要什么类型的调试信息 ,我们设置一个环境变量DEBUG,将它设置为app:startup ,这样的意思是我们只能看到app:startup命名空间的调试信息
如果现在运行应用,看得到app:startup的提示信息

app:startup Morgan enabled... +0ms

下次启动应用的时候也许不想看到任何调试信息,我们可以将这个环境变量设为空值

set DEBUG=

重启应用,就看不到任何的提示信息了.
也许你现在先查看多个命名空间的调试信息,我们可以将环境变量设置为

set  DEBUG=app:startup,app:db

这样我们就能看到这两个命名空间的调试信息,当你要查看所有命名空间的调试信息时,我们可以写成星号通配符,

set DEBUG=app:*

这样就可以显示所有的信息了.
还有一种设置命名空间的快捷方式,我们不用专门使用set命令来设置环境变量,我们可以在启动应用的同时设置环境变量,比如,我现在只想看app:db命名空间的调试信息,我们可以这样写

DEBUG=app nodemon index.js

这就是设置环境变量的快捷方式,设置的同时启动应用.
debug函数给了我们非常多的功能来控制需要查看什么类型的信息,尽量多使用debug模块替代console.log语句.

八.模板引擎

至今为止的反馈都是JSON对象,有时候你可能需要返回HTML标记语言到客户端,这你就要用到模板引擎了.
Express可以使用的模板引擎有很多,最有名的就是Pug ,还有Mustache和EJS ,每个引擎都有各自的语法来创建动态HTML标记反馈信息,这节我们使用Pug来创建一个动态的HTML反馈包,首先要安装Pug

npm i pug

回到index模块,我们需要这样设置:

app.set('view engine', 'pug');

当我们这样设置后,Express会在内部自己导入pug而不用我们手动导入.
还有另一个设置不是必须的,是可选的,只有你需要变更模板的路径时才会使用

app.set('view', './views');

现在我们创建一个新的views文件夹,在文件夹中我们设置一个新的文件 index.pug 我们可以设置这样的语法

html 
  head   
    title= title 
  body   
    h1= message

可见pug比起传统的HTML标记更加简洁,没有开始和结束标签,现在我们在index模块将其中的变量转换为动态的

app.get('/', (req, res) => {
  res.render('index', { title: 'My Express App', Message: 'Hello' });
});

这时我们运行应用,在网页输入localhost:3000,查看源码

<html> <head>  <title>My Express App</title></head><body>  <h1></h1></body></html>

这里已经将pug模板转为了标准HTML语言了,所以你想反馈HTML标签给客户端,你可以使用多种多样的视图引擎,比如pug,EJS等等 ,但是如果是创建RESTful的后端,我们不需要什么视图引擎或者模板引擎

九.数据库集成

这里只想指明有非常多的数据库可以和Express协同工作,如MySQL ,Oracle,SQL Server ,Mongodb等等,你可以打开Express文档查看更多的数据库

十.验证

Express是一个轻量级的框架,它没有所谓验证的概念
,后面会介绍如何让你的接口终端更加安全.

十一.构建结构性路由

const debug = require('debug')('app:startup');
const config = require('config');
const morgan = require('morgan');
const helmet = require('helmet');
const Joi = require('joi');
const logger = require('./logger');
const express = require('express');
const app = express();

app.set('view engine', 'pug');
app.set('views', './views');

app.use(express.json());
app.use(express.urlencoded({ extended: true })); //key=value&key=value
app.use(express.static('public'));
app.use(helmet());

console.log('Mail Server:' + config.get('mail.host'));
// console.log('Mail Password:' + config.get('mail.password'));

if (app.get('env') === 'development') {
  app.use(morgan('tiny'));
  debug('Morgan enabled...');
}

app.use(logger);

app.use(function (req, res, next) {
  console.log('Authenticating...');
  next();
});

const courses = [
  { id: 1, name: 'course1' },
  { id: 2, name: 'course2' },
  { id: 3, name: 'course3' },
];

app.get('/', (req, res) => {
  res.render('index', { title: 'My Express App', Message: 'Hello' });
});

app.get('/api/courses', (req, res) => {
  res.send(courses);
});

app.post('/api/courses', (req, res) => {
  const { error } = validateCourse(req.body);
  if (error) return res.status(400).send(result.error.details[0].message);

  const course = {
    id: courses.lenth + 1,
    name: req.body.name,
  };
  courses.push(course);
  res.send(course);
});

app.put('/api/courses/:id', (req, res) => {
  const course = courses.find((c) => c.id === parseInt(req.params.id));
  if (!course)
    return res.status(404).send('The course with the given ID was not found.');

  const result = validateCourse(req.body);
  const { error } = validateCourse(req.body);

  if (error) return res.status(400).send(result.error.details[0].message);

  course.name = req.body.name;
  res.send(course);
});

app.get('/api/courses/:id', (req, res) => {
  const course = courses.find((c) => c.id === parseInt(req.params.id));
  if (!course)
    return res.status(404).send('The course with the given ID wan not found.');
  res.send(course);
});

app.delete('/api/courses/:id', (req, res) => {
  const course = courses.find((c) => c.id === parseInt(req.params.id));
  if (!course)
    return res.status(404).send('The course with the given ID wan not found.');
  const index = courses.indexOf(course);
  courses.splice(index, 1);
  res.send(course);
});

const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Listening on port ${port}...`));

function validateCourse(course) {
  const schema = {
    name: Joi.string().min(3).required(),
  };
  return Joi.validate(course, schema);
}

这是现在的index模块,在现实开发中你肯定不会将所有东西放到index.js中,这节我们就看看如何结构化应用:
第一件事:就是清理所有涉及courses的接口,并将它们放到一个独立的文件中,换句话说,每个独立api终端的逻辑代码,我们都要转为一个独立的文件或者模块,所有的操作courses的路由都要丢到一个courses.js文件中.

const express = require('express');
const router = express.Router();

const courses = [
  { id: 1, name: 'course1' },
  { id: 2, name: 'course2' },
  { id: 3, name: 'course3' },
];

router.get('', (req, res) => {
  res.send(courses);
});

router.post('', (req, res) => {
  const { error } = validateCourse(req.body);
  if (error) return res.status(400).send(result.error.details[0].message);

  const course = {
    id: courses.lenth + 1,
    name: req.body.name,
  };
  courses.push(course);
  res.send(course);
});

router.put('/:id', (req, res) => {
  const course = courses.find((c) => c.id === parseInt(req.params.id));
  if (!course)
    return res.status(404).send('The course with the given ID was not found.');

  const result = validateCourse(req.body);
  const { error } = validateCourse(req.body);

  if (error) return res.status(400).send(result.error.details[0].message);

  course.name = req.body.name;
  res.send(course);
});

router.get('/:id', (req, res) => {
  const course = courses.find((c) => c.id === parseInt(req.params.id));
  if (!course)
    return res.status(404).send('The course with the given ID wan not found.');
  res.send(course);
});

router.delete('/:id', (req, res) => {
  const course = courses.find((c) => c.id === parseInt(req.params.id));
  if (!course)
    return res.status(404).send('The course with the given ID wan not found.');
  const index = courses.indexOf(course);
  courses.splice(index, 1);
  res.send(course);
});

function validateCourse(course) {
  const schema = {
    name: Joi.string().min(3).required(),
  };
  return Joi.validate(course, schema);
}
module.exports = router;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值