4.3.2 中间件的原理
中间件就是一个函数,它能够访问请求对象 (req)、响应对象 (res) 以及应用程序的请求/响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next 的变量来表示。
中间件中可以执行任意代码,但主要执行以下任务:
对请求和响应对象进行更改。
结束请求/响应循环。
调用堆栈中的下一个中间件函数。
如果当前中间件函数没有结束请求/响应循环,那么它必须调用 next(),以将控制权传递给下一个中间件函数。否则,请求将保持挂起状态。
4.3.3 中间件的使用方法
使用 app.use() 和 app.[METHOD]() 函数将应用层中间件绑定到应用程序对象的实例,其中 METHOD 是中间件函数处理的请求的小写 HTTP 方法(例如 get、put 或 post)。
var app = express();
app.use(function (req, res, next) {
console.log('Time:', Date.now());
next();
});
// 在 /user/:id 路径中为任何类型的 HTTP 请求执行此中间件函数
// 中间件可以传递多个,用于顺序调用。
app.use('/user/:id', function (req, res, next) {
console.log('Request Type:', req.method);
next();
}, function (req, res, next) {
console.log('Request Type:', req.method);
next();
});
// 在/user/:id 路径中为 GET 请求执行此中间件函数
app.get('/user/:id', function (req, res, next) {
res.send('USER');
//此时请求已经完成
});
// 要跳过路由器中间件堆栈中剩余的中间件函数,请调用 next('route') 将控制权传递给下一个路由。
// 注:next('route') 仅在使用 app.METHOD() 或 router.METHOD() 函数装入的中间件函数中有效。
app.get('/user/:id', function (req, res, next) {
// if the user ID is 0, skip to the next route
if (req.params.id == 0) next('route');
// otherwise pass the control to the next middleware function in this stack
else next(); //
}, function (req, res, next) {
// render a regular page
res.render('regular');
});
// if the user ID is 0,this route will execute.
app.get('/user/:id', function (req, res, next) {
res.render('special');
});
注意点:
Express 使用 path-to-regexp 来匹配路由路径。匹配规则:
/abc/:id:匹配/abc/1,/abc/xf,/abc/,其中:id表示任意参数,但它必须是子路径。
/abc?d:匹配abcd或者abd,其中c?表示c可有可无。
/ab+cd:匹配abcd,abbbcd,其中b+表示可以任意一个或多个。
/ab\*cd:匹配abcd,abicd,其中\ *表示任意值。
path中也可以存放数组,表示匹配每一项。
app.all():用于为express或者路由增加中间件,"/"会匹配多种地址或者子地址。
app.all():用于匹配express或者路由任意请求,"/"只会匹配精确地址。
res.send():电风扇
会自动为内容设置Content-Type,如果传递文本,则设置text/html。
res.send() 只能调用一次,调用完毕会自动调用res.end(),如果想多次输出,可以使用res.write。
4.3.4 常用中间件
1. 路由器层中间件
路由器层中间件的工作方式与应用层中间件基本相同。
// index.js
const express = require('express')
const router = require('./router')
const app = express()
app.use(router)
app.listen(3000, () => {
console.log('服务器创建完毕,监听3000端口')
})
//router.js
const express = require('express')
const router = express.Router()
router.all('/', (req, res, next) => {
res.send('你坏')
next()
})
router.all('/user/:id', (req, res, next) => {
if (req.params.id == 0) {
next('route')
} else if (req.params.id == 1) {
res.send('我是老大')
} else {
res.set({
'Content-Type': 'text/html',
})
res.write('我是明明')
res.write('abcd')
res.end()
next()
}
}, (req, res, next) => {
console.log('下一个中间件')
})
router.use('/user/:id', (req, res, next) => {
res.send('超级管理员')
})
module.exports = router
2. body和querystring的转换
req中接收到的body默认是一个字符串,我们在一般倾向于把它转换成一个json对象来使用。
使用第三方中间件body-parser可以为req做转换,bodyParser除了可以将body转换成json外,还可以转换成其他格式。
bodyParser.json():只有请求的Header的Content-Type包含text/json,并且body确实是json数据才会转换成功,否则得到的req.body是空对象。
const express = require('express')
const bodyParser = require('body-parser')
const router = express.Router()
router.all('/body-parser/true', bodyParser.json(), (req, res) => {
res.send(req.body)
})
router.all('/body-parser/false', (req, res) => {
res.send(req.body)
})
module.exports = router
express 4.x默认的req.query就是一个对象,因此不需要对查询字符串进行转换。
3. 静态资源中间件
express提供了express.static()作为静态资源中间件使用,只需要传递一个静态资源路径即可。
在项目目录中加入一个public文件夹,放入一个nodejs.jpg文件。
编写如下代码。
app.use(express.static(path.resolve(__dirname, './public')))
在浏览器直接输入网站根路径+文件名即可访问文件。
4.4 Express 脚手架
使用express-generator可以快速搭建一个Express项目,并能自动下载依赖,可以省去很多步骤。
使用npx一键安装
npx express-generator --ejs <项目名称>
npx命令
npx是npm从5.2版开始增加了的命令,主要用于快速调用node_modules/.bin/ 的命令,如:
以前:$ node-modules/.bin/mocha --version
现在:$ npx mocha --version
同时还能避免全局安装模块而直接使用这种全局模块,如:create-react-app模块
以前:
1.先安装:npm install -g create-react-app;
2.在使用:create-react-app 工程名;
现在一步搞定:
npx create-react-app 工程名
express-generator 支持的可选参数
Options:
-h, --help 输出使用方法
--version 输出版本号
-e, --ejs 添加对 ejs 模板引擎的支持
--hbs 添加对 handlebars 模板引擎的支持
--pug 添加对 pug 模板引擎的支持
-H, --hogan 添加对 hogan.js 模板引擎的支持
--no-view 创建不带视图引擎的项目
-v, --view
-c, --css
--git 添加 .gitignore
-f, --force 强制在非空目录下创建
模板引擎
Express模板引擎(Template Engine), 是用来解析对应类型模板文件然后动态生成由数据和静态页面组成的HTML的一个工具,也就是可以编译成HTML文件的一种预编译文件。
Express中最常用的两个模板工具是ejs和jade(已经改名为pug,不过习惯上还是叫jade),ejs是非常接近html语法的模板,而jade模板是非常简洁的语法,和HTML语法差异较大,jade模板在这里可以学习。
简要学习ejs模板语法:官方文档
<%# 渲染变量 %>
<%= title %>
Welcome to <%= title %>
<% var a = 10, b = 11; %>
<% if(a > b){ %>
<% } %>
- <%= user.name %>
<%# 只执行代码,不渲染内容 %>
<% users.forEach(function(user){ %>
<%# 渲染变量 %>
<% }) %>
标签含义:
```markdown
`<%` '脚本' 标签,用于流程控制,无输出。
`<%_` 删除其前面的空格符
`<%=` 输出数据到模板(输出是转义 HTML 标签)
`<%-` 输出非转义的数据到模板
`<%#` 注释标签,不执行、不输出内容
`<%%` 输出字符串 '<%'
`%>` 一般结束标签
`-%>` 删除紧随其后的换行符
`_%>` 将结束标签后面的空格符删除
模板引擎和索取模板的目录,需要通过以下代码指定:
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
模板是在路由中通过res.render渲染并输出的:
router.get('/', function (req, res, next) {
res.render('index', { title: 'Express', users: [{ name: '花花' }, { name: '世界' }] });
});
五. 使用MongoDB数据库
到目前为止,我们开发的API接口都是生成的假数据。如果有这样一个系统,需要使用Express+Bootstrap开发一套用户的管理系统,所有用户的数据通过上传来提供,并且需要把数据保存。那么这种情形下,数据库就成了必不可少的工具。
5.1 什么是数据库
数据库指的是以一定方式储存在一起、能为多个用户共享、具有尽可能小的冗余度、与应用程序彼此独立的数据集合,用户可以对文件中的数据运行新增、截取、更新、删除等操作。
数据库软件应称为DBMS(数据库管理系统)。数据库是通过 DBMS 创建和操纵的容器。数据库可以是保存在硬件设备上的文件,但也可以不是。在很大程度上说,数据库究竟是文件还是别的什么东西并不重要,因为你并不直接访问数据库;你使用的是DBMS,它替你访问数据库。
5.2 数据库的分类
按照逻辑划分,数据库分为关系型和非关系型数据库。
关系型数据库
关系型数据库是创建在关系模型基础上的数据库,它是数据存储的传统标准。标准数据查询语言SQL就是一种基于关系数据库的语言,这种语言执行对关系数据库中数据的检索和操作。
关系数据库使用表存储数据,在使用之前需要定义数据结构,而且具有数据完整性约束。
SQL是精确的。它最适合于具有精确标准的定义明确的项目。典型的使用场景是在线商店和银行系统。
常用的关系数据库:
MySQL
PostgreSQL
SQL Server
Oracle
非关系型数据库(NoSQL)
非关系型数据库是基于键值对的,可以想象成表中的主键和值的对应关系,而且不需要经过SQL层的解析,所以性能非常高,而且易于扩展。
菲关系型数据库使用类JSON的文档存储数据,格式不限定,不会对数据进行严格验证。
NoSQL是多变的。它最适合于具有不确定需求的数据。典型的使用场景是社交网络,客户管理和网络分析系统。
Redis
MongoDB
关系型和非关系型数据库无分谁优谁劣,他们有各自应用的场景,而且很多情况还会结合使用。
5.3 MongoDB数据库
MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。
MongoDB并非纯粹的非关系型数据库, 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。
因为MongoDB是基于JSON的查询和存储,它最接近JavaScript,而且性能很好,因此我们选用他来作为Express的数据库使用。
5.3.1 安装MongoDB
Windows、Mac安装官网https://www.mongodb.com/download-center/enterprise
数据库安装过程记得选择顺带安装TLS/SSL工具。
5.3.2 准备工作
安装完毕后,我们需要创建一个用来存储数据的目录。
windows用户需要手动在C:\盘目录创建一个data目录,然后data内部创建一个db目录。
Mac用户需要在root根目录下创建一个data目录,然后data内部创建一个db目录。
启动数据库:
mongod
启动成功后,保留这个终端窗口,开启一个新的窗口,输入mongo进入mongo shell模式。
mongo
Windows用户如果无法使用命令,需要设置环境变量指向MongoDB安装目录的bin目录。
5.3.3 MongoDB中的概念
MongoDB是非关系型数据库,它其中有些术语相对于关系型数据库是不一样的。
MongoDB术语对应的SQL术语解释/说明
databasedatabase数据库
collectiontable数据库表/集合
documentrow数据记录行/文档
fieldcolumn数据字段/域
indexindex索引
table joins表连接,MongoDB不支持
primary keyprimary key主键,MongoDB自动将_id字段设置为主键
数据库(Database)
一个mongodb中可以建立多个数据库。
MongoDB的默认数据库为"db",该数据库存储在data目录中。
MongoDB的单个实例可以容纳多个独立的数据库,每一个都有自己的集合和权限,不同的数据库也放置在不同的文件中。
show dbs 命令可以显示所有数据的列表。
> show dbs
执行db命令可以显示当前数据库对象或集合。
> db
运行use命令,可以连接到一个指定的数据库。
> use local
文档(Document)
文档是一组键值(key-value)对。MongoDB 的文档内部的字段和数据类型并不是强制性的,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。
需要注意的是:
文档中的键/值对是有序的。
文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)。
MongoDB区分类型和大小写。
MongoDB的文档不能有重复的键。
db.col.findOne()
集合(collection)
多个文档可以构成一个集合。
集合支持capped特性,该特性会创建一个固定大小的集合,这样collection 的数据存储空间值是提前分配的。
db.createCollection("mycoll", {capped:true, size:100000})
数据类型
ObjectId
MongoDB支持很多种数据类型,但有一种类型很重要也很特殊,就是ObjectId类型。
ObjectId 类似主键,可以很快的去生成和排序,它由12 bytes组成。
MongoDB 中存储的文档必须有一个 _id 键,这个键的值默认是个 ObjectId 对象。
由于 ObjectId 中保存了创建的时间戳,所以你不需要为你的文档保存时间戳字段,你可以通过getTimestamp 函数来获取文档的创建时间:
objectId.getTimestamp()
字符串
字符串默认UTF-8编码。
日期
表示当前距离 1970年1月1日 的毫秒数。日期类型是有符号的, 负数表示 1970 年之前的日期。
5.3.4 shell操作数据库
MongoDB 中默认的数据库为 test,如果你没有创建新的数据库,集合将存放在 test 数据库中。
创建数据库
> use runoob
switched to db runoob
> db
runoob
>
查看数据库
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
>
刚刚创建的数据库没有展示出来,因为它还没有添加数据。
> db.runoob.insert({"name":"菜鸟教程"})
WriteResult({ "nInserted" : 1 })
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
runoob 0.000GB
删除数据库
> use runoob
switched to db runoob
> db.dropDatabase()
{ "dropped" : "runoob", "ok" : 1 }
创建集合
> db.createCollection("users")
{ "ok" : 1 }
>
查看集合
> show collections
users
system.indexes
删除集合
> db.users.drop()
集合中插入文档
>db.users.insert({"name":"小明"})
集合中更新文档
更新的命令比较复杂:
db.collection.update(
{
upsert:
multi:
writeConcern:
}
)
参数说明:
query : update的查询条件,类似sql update查询内where后面的。
update : update的对象和一些更新的操作符(如$,$inc...)等,也可以理解为sql update查询内set后面的
upsert : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
writeConcern :可选,抛出异常的级别。
> db.users.update({'name':'小明'},{$set:{'name':'小花'}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }) # 输出信息
删除文档
db.collection.remove(
{
justOne:
writeConcern:
}
)
参数说明:
query :(可选)删除的文档的条件。
justOne : (可选)如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档。
writeConcern :(可选)抛出异常的级别。
> db.col.remove({'title':'MongoDB 教程'})
WriteResult({ "nRemoved" : 2 })
查询文档
db.collection.find(query, projection)
query :可选,使用查询操作符指定查询条件
projection :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。
> db.col.find().pretty()
{
"_id" : ObjectId("56063f17ade2f21f36b03133"),
"title" : "MongoDB 教程",
"description" : "MongoDB 是一个 Nosql 数据库",
"by" : "菜鸟教程",
"url" : "http://www.runoob.com",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 100
}
此外findOne可以只查询一个
上面的查询是 基于AND条件,也可以基于OR条件查询
>db.col.find({$or:[{"by":"菜鸟教程"},{"title": "MongoDB 教程"}]}).pretty()
{
"_id" : ObjectId("56063f17ade2f21f36b03133"),
"title" : "MongoDB 教程",
"description" : "MongoDB 是一个 Nosql 数据库",
"by" : "菜鸟教程",
"url" : "http://www.runoob.com",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 100
}
>
此外还可以基于大于、小于的查询
> db.col.find({"by":"小明", "likes": {$gt:50}, $or: [{"by": "菜鸟教程"},{"title": "MongoDB 教程"}]}).pretty()
{
"_id" : ObjectId("56063f17ade2f21f36b03133"),
"title" : "MongoDB 教程",
"description" : "MongoDB 是一个 Nosql 数据库",
"by" : "菜鸟教程",
"url" : "http://www.runoob.com",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 100
}
操作格式范例RDBMS中的类似语句
等于{:}db.col.find({"by":"菜鸟教程"}).pretty()where by = '菜鸟教程'
小于{:{$lt:}}db.col.find({"likes":{$lt:50}}).pretty()where likes < 50
小于或等于{:{$lte:}}db.col.find({"likes":{$lte:50}}).pretty()where likes <= 50
大于{:{$gt:}}db.col.find({"likes":{$gt:50}}).pretty()where likes > 50
大于或等于{:{$gte:}}db.col.find({"likes":{$gte:50}}).pretty()where likes >= 50
不等于{:{$ne:}}db.col.find({"likes":{$ne:50}}).pretty()where likes != 50
limit方法
如果你需要在MongoDB中读取指定数量的数据记录,可以使用MongoDB的Limit方法,limit()方法接受一个数字参数,该参数指定从MongoDB中读取的记录条数。
> db.col.find({},{"title":1,_id:0}).limit(2)
{ "title" : "PHP 教程" }
{ "title" : "Java 教程" }
我们除了可以使用limit()方法来读取指定数量的数据外,还可以使用skip()方法来跳过指定数量的数据,skip方法同样接受一个数字参数作为跳过的记录条数。
以下实例只会显示第二条文档数据
>db.col.find({},{"title":1,_id:0}).limit(1).skip(1)
{ "title" : "Java 教程" }
>
排序
在 MongoDB 中使用 sort() 方法对数据进行排序,sort() 方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而 -1 是用于降序排列。
>db.col.find({},{"title":1,_id:0}).sort({"likes":-1})
{ "title" : "PHP 教程" }
{ "title" : "Java 教程" }
{ "title" : "MongoDB 教程" }
>
索引
索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。
为col集合建立索引,基于title
>db.col.createIndex({"title":1})
>
聚合
MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。
> db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : 1}}}])
{
"result" : [
{
"_id" : "runoob.com",
"num_tutorial" : 2
},
{
"_id" : "Neo4j",
"num_tutorial" : 1
}
],
"ok" : 1
}
>
表达式描述实例
$sum计算总和。db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : "$likes"}}}])
$avg计算平均值db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$avg : "$likes"}}}])
$min获取集合中所有文档对应值得最小值。db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$min : "$likes"}}}])
$max获取集合中所有文档对应值得最大值。db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$max : "$likes"}}}])
$push在结果文档中插入值到一个数组中。db.mycol.aggregate([{$group : {_id : "$by_user", url : {$push: "$url"}}}])
$addToSet在结果文档中插入值到一个数组中,但不创建副本。db.mycol.aggregate([{$group : {_id : "$by_user", url : {$addToSet : "$url"}}}])
$first根据资源文档的排序获取第一个文档数据。db.mycol.aggregate([{$group : {_id : "$by_user", first_url : {$first : "$url"}}}])
$last根据资源文档的排序获取最后一个文档数据db.mycol.aggregate([{$group : {_id : "$by_user", last_url : {$last : "$url"}}}])
以上就是MongoDB的基本使用方式,我们在联系过程中发现,mongoDB的shell非常难用,在这里推荐使用GUI工具来管理数据库:Robo3T来管理数据库。