
MongoDB 是一个基于分布式文件存储的数据库,官方地址 https://www.mongodb.com/
数据库(DataBase)是按照数据结构来组织、存储和管理数据的 应用程序。
没错,数据库就是一个应用程序。是不是和我所理解的应用程序差别很大,我所理解的应用程序是有界面、严重依赖鼠标、键盘只是用于输入必要信息、完全不用输入代码的程序。
像数据库这样的应用程序,其实我也做过,vue、html、react甚至是编程语言的任意第三方库,都相当于是应用程序,只不过数据库的抽象程度非常高,不容易掌握。
数据库的主要作用就是 管理数据 ,对数据进行 增(c)、删(d)、改(u)、查(r)
相比于纯文件管理数据(纯文本管理数据,最起码有IO瓶颈、IO冲突等问题,像pandas这样的数据处理库,是没有办法解决的),数据库管理数据有如下特点:
数据库其实是一个应用程序,原来我是不知道的。我对数据库只有模糊的印象,别人说要用,我就用,因为数据库太难学,所以一直没用。
为什么要使用数据库?因为数据库可以帮助我解决很多问题,不管是开发还是部署,都已经有了成熟的方案,照着做就行了。而且数据库里面有各种数据结构,所以响应速度非常快。我只需要知道怎么使用“数据库”这种软件就行了。
操作语法与 JavaScript 类似,容易上手,学习成本低。
Mongodb 中有三个重要概念需要掌握
什么是“数据库服务”?
MongoDB就是一个数据库服务,例如在本机的MongoDB中,可以创建多个数据库。

JSON 文件示例:
xxxxxxxxxx451{2 "accounts": [3 {4 "id": "3-YLju5f3",5 "title": "买电脑",6 "time": "2023-02-08",7 "type": "-1",8 "account": "5500",9 "remarks": "为了上网课"10 },11 {12 "id": "3-YLju5f4",13 "title": "请女朋友吃饭",14 "time": "2023-02-08",15 "type": "-1",16 "account": "214",17 "remarks": "情人节聚餐"18 },19 {20 "id": "mRQiD4s3K",21 "title": "发工资",22 "time": "2023-02-19",23 "type": "1",24 "account": "4396",25 "remarks": "终于发工资啦!~~"26 }27 ],28 "users":[29 {30 "id": 1,31 "name": "zhangsan",32 "age": 1833 },34 {35 "id": 2,36 "name": "lisi",37 "age": 2038 },39 {40 "id": 3,41 "name": "wangwu",42 "age": 2243 }44 ]45}
大家可以通过 JSON 文件来理解 Mongodb 中的概念
JSON 文件 好比是一个 数据库 ,一个 Mongodb 服务下可以有 N 个数据库一级属性的数组值 好比是 集合文档字段一般情况下
- 一个项目使用一个数据库
- 一个集合会存储同一种类型的数据
下载地址: https://www.mongodb.com/try/download/community
建议选择 zip 类型, 通用性更强。不过我是下载的msi,直接安装即可,下面的配置步骤都是针对zip类型安装来的。
配置步骤如下:
1> 将压缩包移动到 C:\Program Files 下,然后解压
2> 创建 C:\data\db 目录,mongodb 会将数据默认保存在这个文件夹
3> 以 mongodb 中 bin 目录作为工作目录,启动命令行
4> 运行命令 mongod
看到最后的 waiting for connections 则表明服务已经启动成功

然后可以使用 mongo 命令连接本机的 mongodb 服务,里面可以输入mongo的命令。

注意:
- 为了方便后续方便使用 mongod 命令,可以将 bin 目录配置到环境变量 Path 中
千万不要选中服务端窗口的内容,选中会停止服务,可以敲回车取消选中- 如果想要在命令行中使用MongoDB,需要先启动服务
mongod,然后另外开一个命令行,输入mongo来连接MongoDB的服务。

命令行交互一般是学习数据库的第一步,不过这些命令在后续用的比较少,所以大家了解即可。
xxxxxxxxxx11show dbsxxxxxxxxxx11use 数据库名xxxxxxxxxx11dbxxxxxxxxxx21use 库名2db.dropDatabase()
xxxxxxxxxx11db.createCollection('集合名称')xxxxxxxxxx11show collectionsxxxxxxxxxx11db.集合名.drop()xxxxxxxxxx11db.集合名.renameColletion('newName')
xxxxxxxxxx41db.集合名.insert(文档对象)23// 例子4db.users.insert({name:'张三',age:18})xxxxxxxxxx41db.集合名.find(查询条件)23// 例子4db.users.find({name:'张三'})_id 是 mongodb 自动生成的唯一编号,用来唯一标识文档
xxxxxxxxxx41db.集合名.update(查询条件,新的文档)23// 例子4db.users.update({name:'张三'},{$set:{age:19}})xxxxxxxxxx41db.集合名.remove(查询条件)23// 例子4db.users.remove({name:'张三'})
有些情况下,可以做伪删除,帮助用户如果需要数据的时候,可以恢复。比如说在一个数据对象里面,添加is_deleted属性,如果删除,则设置为true,这样在返回数据结果的时候,加上这个条件即可。
记住使用场景对我来说非常有用,因为我的项目经验有限,什么时候需要使用MongoDB,什么时候使用关系型数据库,这些都还不知道,所以我把使用场景记下来,等遇到的时候,想起来就行了。
ORM模型,现在看来只适合于关系型数据库,不管是sequelize还是prisma,在里面我都没有看到引入MongoDB或者其它的No SQL。另外我看了一下MongoDB的文档,里面的查询语言本身和ORM模型的语言很类似,所以就没有人来编写它的ORM模型。(2023-11-15记录:这个理解是错误的,因为MongoDB最好的ORM模型就是mongoose,谁说没有的)
关系型数据库和No SQL没有谁更重要的问题,而是谁更适合某些场景,在下面这些场景下,使用No SQL是最好的选择。
MongoDB 主要应用于以下场景:
- 内容管理:MongoDB 可以用于存储网站的文章、博客、图片、视频等内容。
- 社交网络:MongoDB 可以用于存储社交网络用户的信息、帖子、评论等数据。
- 物联网:MongoDB 可以用于存储物联网设备的数据,如传感器数据、位置信息等。
- 游戏:MongoDB 可以用于存储游戏用户的数据,如角色信息、装备信息等。
- 大数据分析:MongoDB 可以用于存储大数据,并提供灵活的查询和分析能力。
以下是一些具体的例子:
- 亚马逊的电子商务平台 Amazon.com 使用 MongoDB 存储商品信息、订单信息、用户信息等。
- Facebook 使用 MongoDB 存储用户信息、帖子信息、评论信息等。
- Uber 使用 MongoDB 存储司机信息、乘客信息、订单信息等。
- 腾讯游戏使用 MongoDB 存储游戏用户信息、游戏数据等。
- 阿里巴巴使用 MongoDB 存储电商交易数据、物流数据等。
以下是MongoDB的主要应用场景以及一些示例:
内容管理系统(CMS):
- 示例: 一个新闻网站可以使用MongoDB来存储文章、图片、视频和评论等内容,每个内容项都可以表示为一个文档。这样的CMS系统可以轻松地扩展以处理大量的新闻和媒体内容。
社交媒体应用程序:
- 示例: 社交媒体平台可以使用MongoDB存储用户资料、帖子、评论、关注关系等数据。由于MongoDB支持嵌套文档,可以将用户的活动流存储在其个人资料文档中,使实时推送和社交互动更高效。
实时分析和大数据处理:
- 示例: 一家电子商务公司可以使用MongoDB来存储销售数据、用户行为和产品信息。通过MongoDB的聚合功能,可以实时分析销售趋势、用户偏好和库存情况,以做出及时的经营决策。
物联网(IoT)和传感器数据:
- 示例: IoT设备可以生成大量数据,如温度、湿度、位置等。MongoDB可以用于存储和查询这些数据,以监控设备状态、执行预测维护和生成实时报告。
实时应用程序:
- 示例: 在线游戏和聊天应用程序需要快速响应用户的操作和实时更新。MongoDB可以用于存储用户配置、游戏状态和聊天消息,以实现实时互动。
日志和事件存储:
- 示例: 大型服务器集群生成大量的日志和事件数据。MongoDB可以用于存储和检索这些数据,以进行系统监控、性能分析和故障排除。
地理空间数据:
- 示例: 地图应用程序和位置服务可以使用MongoDB来存储地理空间数据,如地点、地区边界和路线。这些数据可以用于位置搜索、导航和地理信息分析。
Mongoose 是一个对象文档模型库,官网 http://www.mongoosejs.net/,前面是中文网,不忍直视。还是直接看英文网:https://mongoosejs.com/。

方便使用代码操作 mongodb 数据库。
其实一直搞不懂为什么SQL有自己的语言,在项目中使用的时候却要使用ORM模型。
原因其实很简单,我如何才能针对不同的需求简洁、快速地写出不同的SQL语句呢?我如果想要使用原生SQL语句做到,还要在模板字符串里面写SQL语句,SQL语句复杂的时候有多长你知道吗?一个简单的关联查询就会很长了,更不要说复杂的增删改查了。模板字符串不方便的,是真不方便,里面预留出的
${}真的能够应付不同的需求吗?我看未必,所以就每一个数据库,都会有很多ORM模型设计出来,使用ORM真的很方便。
创建一个文件夹,使用npm init -y来初始化包,在里面安装mongoose。
看到下面的内容,可能会感觉有些奇怪,mongoose是在单个js文件里面使用的啊,为什么要初始化一个包呢?
因为要用到mongoose,需要导入才能使用。
npm i mongoose

x1// 1、 安装mongoose2// 2、 导入mongoose3const mongoose = require("mongoose");45// 3、连接MongoDB服务。注意:连接的是具体的数据库,如果此数据库不存在,则创建。6mongoose.connect("mongodb://127.0.0.1:27017/bilibili");78// 4、设置回调9// 设置连接成功的回调10mongoose.connection.on("open", () => {11 console.log("连接成功!");12});13// 设置连接失败的回调14mongoose.connection.on("error", () => {15 console.log("连接失败!");16});17// 设置连接关闭的回调18mongoose.connection.on("close", () => {19 console.log("连接关闭!");20});测试之前,一定要把MongoDB服务启动,命令行中输入mongod启动。执行这个文件,看效果:

在上述代码最后加上这段代码,演示一下连接关闭的效果:
xxxxxxxxxx41// 关闭mongodb连接2setTimeout(() => {3 mongoose.disconnect();4}, 2000);
注意:一般在项目中,是不需要关闭连接的,因为项目上线后就是为了不断使用的。
在mongodb@5.x版本中,官方推荐在绑定open方法时,使用once来绑定。http://www.mongoosejs.net/docs/index.html

xxxxxxxxxx41// 设置连接成功的回调2mongoose.connection.once("open", () => {3 console.log("连接成功!");4});为什么要改成使用once来绑定open事件呢?因为once表示open事件里面的回调函数只执行一次,而on表示open事件里面的回调函数每次触发都会执行。假如说mongodb连接掉线了,那么mongoose会尝试重新连接,重新连接上之后,使用on绑定的事件都会执行一次,而once在之前已经执行过了,此时就不会再次执行了。
那么open事件的回调函数里面会放一些只需要执行一次的代码,在后面的学习中可以看到。
注意:
在学习的时候,由于MongoDB是5.x版本,所以相应的mongoose也是5.x版本。现在MongoDB最新版是8.x版本,所以mongoose也是8.x版本。
定义文档结构的时候,我一直都没有使用
new关键字,而是直接定义,项目并没有报错:xxxxxxxxxx51let BookSchema = mongoose.Schema({2name: String,3author: String,4price: Number,5});现在看了官方文档之后,发现定义文档结构的时候,都使用了
new关键字,所以以后一定要加上这个关键字,避免意想不到的bug。下面的笔记中我看到的位置都会加上
new关键字,但是难免有没有改到的地方,那么看到了就要注意。

xxxxxxxxxx691// 2、 导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 console.log("连接成功!");11 // 5、创建文档的结构对象。schema英文意思是:A schema is an outline of a plan or theory。可以翻译为模式、图式。这里翻译为结构。12 // 作用:设置集合(collections)中的文档(document)的属性以及属性值的类型。13 let BookSchema = new mongoose.Schema({14 name: String,15 author: String,16 price: Number,17 });1819 // 6、创建模型对象20 // 模型对象的作用:对文档操作的封装对象。意味着对文档的任何操作(CRUD),可以通过模型对象来完成。我有一个疑问:为什么是通过操作文档模型来增删改查呢?而不是通过操作集合来实现呢?这个暂时不知道,也许等我设计一个ORM时就清楚这么做的好处了。21 // 2023-11-20 也许刚开始我是真的不懂,本来就是操作模型对象(相当于集合)来增删改查,文档的结构对象只是定义模型对象的一个参数而已,定义出来就好了。22 // 第一个参数是集合的名称(如果没有就创建),第二个参数是文档的结构对象。23 let BookModel = mongoose.model("books", BookSchema);2425 // 7、调用模型对象,进行具体操作,比如说:新增、修改、删除、查询26 // 新增。模型对象的create方法接收两个参数,第一个参数是数据对象,第二个参数是回调函数。27 28 // mongoose@5.x版本用法29 // BookModel.create(30 // {31 // name: "西游记",32 // author: "吴承恩",33 // price: 19.9,34 // },35 // (err, data) => {36 // if (err) {37 // console.log(err);38 // return;39 // }40 // // data是插入成功之后的文档对象41 // // 如果没有出错,则输出插入后的文档对象。这一步一般在实际项目中不需要,因为实际项目中需要返回的是code编码给前端。42 // console.log(data);43 // }44 // );4546 // mongoose@7.5.3用法,直接使用链式调用。47 BookModel.create({48 name: "西游记",49 author: "吴承恩",50 price: 19.9,51 }).then((err, data) => {52 if (err) {53 console.log(err);54 return;55 }56 // data是插入成功之后的文档对象57 // 如果没有出错,则输出插入后的文档对象。这一步一般在实际项目中不需要,因为实际项目中需要返回的是code编码给前端。58 console.log(data);59 });60});61// 设置连接失败的回调62mongoose.connection.on("error", () => {63 console.log("连接失败!");64});65// 设置连接关闭的回调66mongoose.connection.on("close", () => {67 console.log("连接关闭!");68});69执行查看效果:

这里的_id是mongodb为我们生成的一个唯一编号。__v是mongoose为我们生成的一个版本编号,一般用不到。
查询数据库,也可以看到数据插入成功了。

文档结构可选的常用字段类型列表
| 类型 | 描述 |
|---|---|
| String | 字符串 |
| Number | 数字 |
| Boolean | 布尔值 |
| Array | 数组,也可以使用 [] 来表示 |
| Date | 日期 |
| Buffer | Buffer对象 |
| Mixed | 任意类型,实际使用时需要写完整的mongoose.Schema.Types.Mixed指定 |
| ObjectId | 对象ID,实际使用时需要写完整的mongoose.Schema.Types.ObjectId指定。常用作外键。 |
| Decimal128 | 高精度数字,实际使用时需要写完整的mongoose.Schema.Types.Decimal128指定 |
Mongoose 有一些内建验证器,可以对字段值进行验证。在创建文档的结构对象时,除了指定属性值类型外,还可以进行验证。
xxxxxxxxxx91let BookSchema = new mongoose.Schema({2 // name: String,3 name: {4 type: String,5 required: true,// 设置必填项6 },7 author: String,8 price: Number,9 });xxxxxxxxxx41title: {2 type: String,3 required: true // 设置必填项4},xxxxxxxxxx41author: {2 type: String,3 default: '匿名' //默认值4},xxxxxxxxxx41gender: {2 type: String,3 enum: ['男','女'] //设置的值必须是数组中的值4},xxxxxxxxxx41username: {2 type: String,3 unique: true4},unique 需要
重建集合才能有效果。什么叫“重建”?重建就是先删除集合再创建集合,重新创建。永远不要相信用户的输入,字段值验证是非常必须的。
为了保证数据的完整性,可以先将bilibili数据库删除掉,执行下面的代码。
xxxxxxxxxx41# 打开mongo命令行23use bilibili4db.dropDatabase()
数据库的基本操作包括四个,增加(create),删除(delete),修改(update),查(read)。为了方便学习,先将老师提供的data.js执行一下,插入一些数据。
xxxxxxxxxx1731// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 7、新增22 BookModel.insertMany([23 {24 name: "西游记",25 author: "吴承恩",26 price: 19.9,27 is_hot: true,28 },29 {30 name: "红楼梦",31 author: "曹雪芹",32 price: 29.9,33 is_hot: true,34 },35 {36 name: "三国演义",37 author: "罗贯中",38 price: 25.9,39 is_hot: true,40 },41 {42 name: "水浒传",43 author: "施耐庵",44 price: 20.9,45 is_hot: true,46 },47 {48 name: "活着",49 author: "余华",50 price: 19.9,51 is_hot: true,52 },53 {54 name: "狂飙",55 author: "徐纪周",56 price: 68,57 is_hot: true,58 },59 {60 name: "大魏能臣",61 author: "黑男爵",62 price: 9.9,63 is_hot: false,64 },65 {66 name: "知北游",67 author: "洛水",68 price: 59,69 is_hot: false,70 },71 {72 name: "道君",73 author: "跃千愁",74 price: 59,75 is_hot: false,76 },77 {78 name: "七煞碑",79 author: "游泳的猫",80 price: 29,81 is_hot: false,82 },83 {84 name: "独游",85 author: "酒精过敏",86 price: 15,87 is_hot: false,88 },89 {90 name: "大泼猴",91 author: "甲鱼不是龟",92 price: 26,93 is_hot: false,94 },95 {96 name: "黑暗王者",97 author: "古羲",98 price: 39,99 is_hot: false,100 },101 {102 name: "不二大道",103 author: "文刀手予",104 price: 89,105 is_hot: false,106 },107 {108 name: "大泼猴",109 author: "甲鱼不是龟",110 price: 59,111 is_hot: false,112 },113 {114 name: "长安的荔枝",115 author: "马伯庸",116 price: 45,117 is_hot: true,118 },119 {120 name: "命运",121 author: "蔡崇达",122 price: 59.8,123 is_hot: true,124 },125 {126 name: "如雪如山",127 author: "张天翼",128 price: 58,129 is_hot: true,130 },131 {132 name: "三体",133 author: "刘慈欣",134 price: 23,135 is_hot: true,136 },137 {138 name: "秋园",139 author: "杨本芬",140 price: 38,141 is_hot: true,142 },143 {144 name: "百年孤独",145 author: "范晔",146 price: 39.5,147 is_hot: true,148 },149 {150 name: "在细雨中呼喊",151 author: "余华",152 price: 25,153 is_hot: true,154 },155 ]).then((data) => {156 console.log(data);157 }).catch(err => {158 if (err) {159 console.log(err);160 return;161 }162 })163});164165// 设置连接失败的回调166mongoose.connection.on("error", () => {167 console.log("error");168});169170// 设置连接关闭的回调171mongoose.connection.on("close", () => {172 console.log("close");173});注意:mongoose.model()会使用集合名称的复数,在数据库中创建集合。上面的代码中,使用了"novel",实际在数据库中,就是novels。
注意:因为在mongoose@7.5.x版本中,一些方法不再有第二个参数,而是使用then的链式调用,所以一般我都写的是@7.5.x版本的方法,@5.x版本的方法不再给出示例了,其实也很简单,就是把@5.x版本中的第二个参数放到then中即可。
但是
then里面的参数到底是(err,data)=>{}还是(data)=>{},还是要看官方文档,也可以自己试一下。我看了一下,应该是这样写是不行的:
xxxxxxxxxx71.then((err,data) => {2if(err){3throw err;4return;5}6console.log(data);7})这样会报错没有办法获取到
throw err的err值,所以必须写一个catch。但是写了catch还是会报错,所以一般我写的时候,可以先这样写:
xxxxxxxxxx71.then((err,data) => {2if(err){3console.log(err);4return;5}6console.log(data);7})如果没有反应,输出不了data,可以改成这样:
xxxxxxxxxx61.then(data => {2console.log(data);3})4.catch(err => {5console.log(err);6})如果还是不行,就直接看文档。
与mongoose的版本还是有关系的,如果不知道,还是看文档。
插入一条
create()方法。
xxxxxxxxxx91SongModel.create({2 title:'给我一首歌的时间',3 author: 'Jay'4}).then(function(err, data){5 //错误6 console.log(err);7 //插入后的数据对象8 console.log(data);9})批量插入
insertMany()方法。
xxxxxxxxxx371//1.引入mongoose2const mongoose = require('mongoose');34//2.链接mongodb数据库 connect 连接5mongoose.connect('mongodb://127.0.0.1:27017/project');67//3.设置连接的回调8mongoose.connection.on('open',()=>{9//4.声明文档结构10const PhoneSchema = new mongoose.Schema({11 brand:String,12 color:String,13 price:Number,14 tags:Array15 })1617 //6.创建模型对象18 const PhoneModel = mongoose.model('phone',PhoneSchema);19 20 PhoneModel.insertMany([21 {22 brand:'华为',23 color:'灰色',24 price:2399,25 tags:['电量大','屏幕大','信号好']26 },27 {28 brand:'小米',29 color:'白色',30 price:2099,31 tags:['电量大','屏幕大','信号好']32 }33 ]).then((err,data)=>{34 if(err) throw err;35 console.log('写入成功');36 })37})1、删除一条数据
xxxxxxxxxx511// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 删除。deleteOne方法有两个参数,第一个参数是条件,第二个参数是回调函数。2223 // mongoose@5.x版本24 // BookModel.deleteOne({ _id: "651792320e77f77eb6f3c909" }, (err, data) => {25 // if(err){26 // console.log(err);27 // return;28 // }29 // console.log(data);30 // });3132 // mongoose@7.5.3版本33 BookModel.deleteOne({ _id: "651792320e77f77eb6f3c909" }).then((err, data) => {34 if (err) {35 console.log(err);36 return;37 }38 console.log(data);39 })40});4142// 设置连接失败的回调43mongoose.connection.on("error", () => {44 console.log("error");45});4647// 设置连接关闭的回调48mongoose.connection.on("close", () => {49 console.log("close");50});51
2、批量删除
xxxxxxxxxx401// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 批量删除22 BookModel.deleteMany({is_hot:false}).then((err,data) => {23 if(err){24 console.log(err);25 return;26 }27 console.log(data);28 })29});3031// 设置连接失败的回调32mongoose.connection.on("error", () => {33 console.log("error");34});3536// 设置连接关闭的回调37mongoose.connection.on("close", () => {38 console.log("close");39});40
1、更新一条数据
xxxxxxxxxx391// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 更新一条数据。第一个参数是查找条件,第二个参数是需要修改的值。22 BookModel.updateOne({ name: "红楼梦" }, { price: 9.9 }).then((err, data) => {23 if (err) {24 console.log(err);25 return;26 }27 console.log(data);28 });29});3031// 设置连接失败的回调32mongoose.connection.on("error", () => {33 console.log("error");34});3536// 设置连接关闭的回调37mongoose.connection.on("close", () => {38 console.log("close");39});

2、批量更新数据
xxxxxxxxxx401// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 批量更新22 BookModel.updateMany({ author: "余华" }, { is_hot: true }).then((err, data) => {23 if (err) {24 console.log(err);25 return;26 }27 console.log(data);28 });29});3031// 设置连接失败的回调32mongoose.connection.on("error", () => {33 console.log("error");34});3536// 设置连接关闭的回调37mongoose.connection.on("close", () => {38 console.log("close");39});40
1、查询一条数据
findOne()方法:
xxxxxxxxxx391// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 读取一条数据22 BookModel.findOne({ name: "狂飙" }).then((err, data) => {23 if (err) {24 console.log(err);25 return;26 }27 console.log(data);28 });29});3031// 设置连接失败的回调32mongoose.connection.on("error", () => {33 console.log("error");34});3536// 设置连接关闭的回调37mongoose.connection.on("close", () => {38 console.log("close");39});
findById()方法:
xxxxxxxxxx391// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 根据id获取单条数据22 BookModel.findById("651792320e77f77eb6f3c90e").then((err, data) => {23 if (err) {24 console.log(err);25 return;26 }27 console.log(data);28 });29});3031// 设置连接失败的回调32mongoose.connection.on("error", () => {33 console.log("error");34});3536// 设置连接关闭的回调37mongoose.connection.on("close", () => {38 console.log("close");39});2、批量查询数据
不加条件查询,获取所有数据:
xxxxxxxxxx401// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 批量获取数据22 // 不加条件,获取所有数据23 BookModel.find().then((err, data) => {24 if (err) {25 console.log(err);26 return;27 }28 console.log(data);29 });30});3132// 设置连接失败的回调33mongoose.connection.on("error", () => {34 console.log("error");35});3637// 设置连接关闭的回调38mongoose.connection.on("close", () => {39 console.log("close");40});加条件查询:
xxxxxxxxxx391// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 批量获取数据22 BookModel.find({ author: "余华" }).then((err, data) => {23 if (err) {24 console.log(err);25 return;26 }27 console.log(data);28 });29});3031// 设置连接失败的回调32mongoose.connection.on("error", () => {33 console.log("error");34});3536// 设置连接关闭的回调37mongoose.connection.on("close", () => {38 console.log("close");39});
注意:虽然下面的例子大部分是MongoDB原生语言的例子,但mongoose使用的时候,条件部分是一样的。我会给出部分例子,照着写就行了。
需要留意条件的写法,条件可以看成是一种值,采用对象的方式来写,条件为key,具体的范围为值。
在 mongodb 不能使用 > < >= <= !== 等运算符,需要使用替代符号
> 使用 $gt< 使用 $lt>= 使用 $gte<= 使用 $lte!== 使用 $nexxxxxxxxxx11db.students.find({id:{$gt:3}}); // id号比3大的所有记录xxxxxxxxxx391// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 找出价格小与20元的书22 BookModel.find({ price: { $lt: 20 } }).then((err, data) => {23 if (err) {24 console.log(err);25 return;26 }27 console.log(data);28 });29});3031// 设置连接失败的回调32mongoose.connection.on("error", () => {33 console.log("error");34});3536// 设置连接关闭的回调37mongoose.connection.on("close", () => {38 console.log("close");39});效果:

注意括号的写法。
$or 逻辑或的情况
xxxxxxxxxx11db.students.fond({$or:[{age:18},{age:24}]});xxxxxxxxxx391// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 找出曹雪芹或者余华的书22 BookModel.find({ $or: [{ author: "余华" }, { author: "曹雪芹" }] }).then((err, data) => {23 if (err) {24 console.log(err);25 return;26 }27 console.log(data);28 });29});3031// 设置连接失败的回调32mongoose.connection.on("error", () => {33 console.log("error");34});3536// 设置连接关闭的回调37mongoose.connection.on("close", () => {38 console.log("close");39});
$and 逻辑与的情况
xxxxxxxxxx11db.students.find({$and:[{age:{$lt:20}},{age:{$gt:15}}]});xxxxxxxxxx391// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 价格大于40且小于60的书22 BookModel.find({ $and: [{ price: { $gt: 40 } }, { price: { $lt: 60 } }] }).then((err, data) => {23 if (err) {24 console.log(err);25 return;26 }27 console.log(data);28 });29});3031// 设置连接失败的回调32mongoose.connection.on("error", () => {33 console.log("error");34});3536// 设置连接关闭的回调37mongoose.connection.on("close", () => {38 console.log("close");39});
条件中可以直接使用 JS 的正则语法,通过正则可以进行模糊查询。
xxxxxxxxxx11db.students.find({name:/imissyou/});xxxxxxxxxx391// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 正则表达式,搜索书籍名称中带有 三 的图书22 BookModel.find({ name: new RegExp("三") }).then((err, data) => {23 if (err) {24 console.log(err);25 return;26 }27 console.log(data);28 });29});3031// 设置连接失败的回调32mongoose.connection.on("error", () => {33 console.log("error");34});3536// 设置连接关闭的回调37mongoose.connection.on("close", () => {38 console.log("close");39});
MongoDB版本问题,参考:https://juejin.cn/post/7225240369524670519
只读取某些字段,这样返回的速度会快很多。
xxxxxxxxxx71// 0:不要的字段2// 1:要的字段3SongModel.find().select({_id:0,title:1}).exec(function(err,data){4 if(err) throw err;5 console.log(data);6 mongoose.connection.close();7});xxxxxxxxxx561// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 设置字段,只读取书籍名称和作者22 // mongodb@5.x版本23 // BookModel.find()24 // .select({ name: 1, author: 1 })25 // .exec((err, data) => {26 // if (err) {27 // console.log(err);28 // return;29 // }30 // console.log(data);31 // });3233 // mongodb@7.x版本34 BookModel.find()35 .select({ name: 1, author: 1 })36 .then((data) => {37 console.log(data);38 })39 .catch(err => {40 if (err) {41 console.log(err);42 return;43 }44 })45});4647// 设置连接失败的回调48mongoose.connection.on("error", () => {49 console.log("error");50});5152// 设置连接关闭的回调53mongoose.connection.on("close", () => {54 console.log("close");55});56效果:

xxxxxxxxxx81//sort 排序2//1:升序3//-1:倒序4SongModel.find().sort({hot:1}).exec(function(err,data){5 if(err) throw err;6 console.log(data);7 mongoose.connection.close();8});xxxxxxxxxx421// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 按照价格升序排序22 BookModel.find()23 .select({ author: 1, name: 1, price: 1, _id: 0 })24 .sort({ price: 1 })25 .then((err, data) => {26 if (err) {27 console.log(err);28 return;29 }30 console.log(data);31 });32});3334// 设置连接失败的回调35mongoose.connection.on("error", () => {36 console.log("error");37});3839// 设置连接关闭的回调40mongoose.connection.on("close", () => {41 console.log("close");42});效果:

xxxxxxxxxx61//skip 跳过多少个,如果是0可以省略不写skip方法。 limit 限定取多少个值。两个参数非常好用,不用考虑索引值从0开始还是从1开始,正常思维取值即可。2SongModel.find().skip(10).limit(10).exec(function(err,data){3 if(err) throw err;4 console.log(data);5 mongoose.connection.close();6});xxxxxxxxxx441// 2、导入mongoose2const mongoose = require("mongoose");34// 3、连接MongoDB服务5mongoose.connect("mongodb://127.0.0.1:27017/bilibili");67// 4、设置回调8// 设置连接成功的回调9mongoose.connection.once("open", () => {10 // 5、创建文档的结构对象11 let BookSchema = new mongoose.Schema({12 name: String,13 author: String,14 price: Number,15 is_hot: Boolean,16 });1718 // 6、创建模型对象19 let BookModel = mongoose.model("novel", BookSchema);2021 // 取出价格最高的三本书22 BookModel.find()23 .select({ name: 1, author: 1, price: 1, _id: 0 })24 .sort({ price: -1 })25 .skip(0)26 .limit(3)27 .then((err, data) => {28 if (err) {29 console.log(err);30 return;31 }32 console.log(data);33 });34});3536// 设置连接失败的回调37mongoose.connection.on("error", () => {38 console.log("error");39});4041// 设置连接关闭的回调42mongoose.connection.on("close", () => {43 console.log("close");44});

1、将mongoose操作基本代码提取到一个单独的文件中。
难点在于mongoose绑定的open和error事件,里面的回调函数该怎么做?答案:使用函数参数,将这些回调函数传递过来。
在项目package.json同级创建db文件夹,创建db.js文件。
xxxxxxxxxx311// db/db.js23/**4 *5 * @param {*} success 数据库连接成功的回调6 * @param {*} error 数据库连接失败的回调7 */8module.exports = function (success, error) {9 // 1、安装mongoose10 // 2、导入mongoose11 const mongoose = require("mongoose");1213 // 3、连接MongoDB服务14 mongoose.connect("mongodb://127.0.0.1:27017/bilibili");1516 // 4、设置回调17 // 设置连接成功的回调18 mongoose.connection.once("open", () => {19 // console.log("连接成功");20 success();21 });22 // 设置连接失败的回调23 mongoose.connection.on("error", () => {24 // console.log("连接失败");25 error();26 });27 // 设置连接关闭的回调28 mongoose.connection.on("close", () => {29 console.log("连接关闭");30 });31};2、在index.js文件中,引入db/db.js文件,传入两个函数参数。重点在于第一个参数函数,里面可以放一些数据库操作的代码。
xxxxxxxxxx381// 导入 db 文件2const db = require("./db/db");34// 导入mongoose5const mongoose = require("mongoose");67// 调用函数8db(9 () => {10 // 数据库操作11 // 5、创建文档的结构对象。12 let BookSchema = new mongoose.Schema({13 name: {14 type: String,15 required: true,16 },17 author: String,18 price: Number,19 });2021 // 6、创建模型对象22 let BookModel = mongoose.model("books", BookSchema);2324 // 7、调用模型对象,进行具体操作,比如说:新增、修改、删除、查询25 BookModel.create({26 name: "万历十五年",27 author: "黄仁宇",28 price: 19.9,29 }).then((err, data) => {30 if (err) {31 console.log(err);32 return;33 }34 console.log(data);35 });36 },37 () => {}38);效果:

3、提取结构对象和模型对象
新建一个models文件夹,里面创建BookModel.js文件(每个模型对象都可以对应一个文件)。将原本在db的第一个参数函数里面创建模型对象的过程,提取出来,放到这个文件里面,并暴露。
xxxxxxxxxx201// models/BookModel.js23// 导入mongoose4const mongoose = require("mongoose");56// 创建文档的结构对象。7let BookSchema = new mongoose.Schema({8 name: {9 type: String,10 required: true,11 },12 author: String,13 price: Number,14});1516// 创建模型对象17let BookModel = mongoose.model("books", BookSchema);1819// 暴露模型对象20module.exports = BookModel;在index.js中导入BookModel,并使用:
xxxxxxxxxx271// 导入 db 文件2const db = require("./db/db");34// 导入mongoose5const mongoose = require("mongoose");67// 导入BookModel8const BookModel = require("./models/BookModel")910// 调用函数11db(12 () => {13 // 7、调用模型对象,进行具体操作,比如说:新增、修改、删除、查询14 BookModel.create({15 name: "中国大历史",16 author: "黄仁宇",17 price: 29.9,18 }).then((err, data) => {19 if (err) {20 console.log(err);21 return;22 }23 console.log(data);24 });25 },26 () => {}27);效果:

4、为db.js的第二个参数设置默认值
因为第二个参数只是显示error,大多数情况下在调用db的时候,可以省略不写,此时就可以赋默认值,默认值为一个函数。
xxxxxxxxxx361// db/db.js23/**4 *5 * @param {*} success 数据库连接成功的回调6 * @param {*} error 数据库连接失败的回调7 */8module.exports = function (9 success,10 error = () => {11 console.log("连接失败");12 }13) {14 // 1、安装mongoose15 // 2、导入mongoose16 const mongoose = require("mongoose");1718 // 3、连接MongoDB服务19 mongoose.connect("mongodb://127.0.0.1:27018/bilibili");2021 // 4、设置回调22 // 设置连接成功的回调23 mongoose.connection.once("open", () => {24 // console.log("连接成功");25 success();26 });27 // 设置连接失败的回调28 mongoose.connection.on("error", () => {29 // console.log("连接失败");30 error();31 });32 // 设置连接关闭的回调33 mongoose.connection.on("close", () => {34 console.log("连接关闭");35 });36};将连接端口号改为27018,同时去掉db调用时的第二个参数,测试看效果:

5、将连接MongoDB的一些参数,提取出来,放到配置文件中。
创建config文件夹,config.js文件。
xxxxxxxxxx71// config/config.js23module.exports = {4 DBHOST: "127.0.0.1",5 DBPORT: "27017",6 DBNAME: "bilibili",7};在db/db.js中导入配置项。
xxxxxxxxxx381// db/db.js23/**4 *5 * @param {*} success 数据库连接成功的回调6 * @param {*} error 数据库连接失败的回调7 */8module.exports = function (9 success,10 error = () => {11 console.log("连接失败");12 }13) {14 // 1、安装mongoose15 // 2、导入mongoose16 const mongoose = require("mongoose");1718 // 导入配置项19 const { DBNAME, DBPORT, DBHOST } = require("../config/config")20 // 3、连接MongoDB服务21 mongoose.connect(`mongodb://${DBHOST}:${DBPORT}/${DBNAME}`);2223 // 4、设置回调24 // 设置连接成功的回调25 mongoose.connection.once("open", () => {26 // console.log("连接成功");27 success();28 });29 // 设置连接失败的回调30 mongoose.connection.on("error", () => {31 // console.log("连接失败");32 error();33 });34 // 设置连接关闭的回调35 mongoose.connection.on("close", () => {36 console.log("连接关闭");37 });38};其实,把mongoose的基本流程和定义模型对象的流程搞清楚,上面的流程就都搞清楚了。
疑问:如果有多个模型对象,是不是都要引入到index.js文件里面,在第一个函数参数里面进行操作?这样代码还是很复杂,该怎么模块化呢?
老师举了一个例子,就是新增MovieModel这个模型对象,但是使用的时候,新建了一个js文件,里面导入了db,在db的第一个参数函数中操作MovieModel。说明index.js不是操作数据库的唯一入口文件,而是每个Model对应一个操作文件,该执行哪个文件就执行哪个文件。具体怎么操作,看后面会不会讲到,如果没有讲到,就找别的视频来看。
从下面的案例中可以看到,express项目中,第一步做好之后,就可以在具体的router文件里面进行model的操作了,不需要统一在index.js中引入并使用,这是怎么实现的?暂时不管。
我们可以使用图形化的管理工具来对 Mongodb 进行交互,这里演示两个图形化工具
Robo 3T 免费 https://github.com/Studio3T/robomongo/releases
Navicat 收费 https://www.navicat.com.cn/
为了简便起见,将之前的accounts项目拿过来,跑起来。做的修改只是将里面的lowdb改为mongodb。
1、在项目中创建config.js,db.js,并编写代码,创建models文件夹。

2、在项目的bin/www文件中,引入db,将原文件里面的所有代码放入到db的第一个函数里面,原文件的代码不需要任何修改(当然,第一行的代码\#!/usr/bin/env node不需要动)。这一步的目的是:先连接数据库,再启动服务。

红框里面的代码是www文件中原本的代码,不做任何改变,只放进db的第一个回调函数里面即可。
3、创建模型文件
在models文件夹中创建AccountModel.js,编写代码:
xxxxxxxxxx261// 导入mongoose2const mongoose = require("mongoose");34// 创建文档的结构对象5let AccountSchema = new mongoose.Schema({6 title: {7 type: String,8 required: true,9 },10 time: Date,11 type: {12 type: Number,13 default: -1,14 },15 account: {16 type: Number,17 required: true,18 },19 remark: String,20});2122// 创建模型对象。对文档操作的封装对象23let AccountModel = mongoose.model("accounts", AccountSchema);2425// 暴露模型对象26module.exports = AccountModel;注意:编写好了模型文件之后,就可以直接操作了,不需要预先创建accounts这个集合,为什么呢?因为有一个重要点:如果没有即创建,这就是ORM模型带来的好处。这一点顾虑打消掉很重要啊。
4、插入数据库
之前是将添加数据的路由放在了routes/index.js中,只需要改写里面的存取代码即可。
先看一下提交表格的数据:

可以看到time不是Date类型,需要使用moment库来帮忙转成Date类型。
xxxxxxxxxx321// routes/index.js23var express = require("express");4const AccountModel = require("../models/AccountModel");5var router = express.Router();6// 导入moment7const moment = require("moment");89// 新增记账记录10router.post("/account", (req, res, next) => {11 // 为什么可以直接使用req.body,不应该先引入body-parser吗?其实在app.js里面已经引入了。12 // console.log(req.body);1314 AccountModel.create({15 req.body,16 // 修改 time 属性的值。这里用到了ES6的语法,在一个对象里面,如果出现同名属性,则会被后面的属性值覆盖。这种用法很简答,一定要会用。17 time: moment(req.body.time).toDate(),18 })19 .then((data) => {20 res.render("success", { msg: "添加成功", url: "/account" });21 })22 .catch((err) => {23 if (err) {24 res.status(500).send("添加失败");25 return;26 }27 });28});2930// 其余的路由,如果没有改变,我就先不展示了3132module.exports = router;测试一下127.0.0.1:3000/account/create,插入成功,查看数据库:

其实在这里我有一个疑问:文档结构对象里面规定了type和account的类型为Number,但是实际传递给后端的是String类型(我输出查看了确实是String类型),为什么不会报错呢?而且即使传递过去的是String类型,数据库保存的还是Number类型,这又是为什么呢?
这个疑问暂时不知道,等有时间一定要搜索一下。
5、读取数据库
读取数据库很简单,还是在路由里面进行处理:
xxxxxxxxxx231// routes/index.js23var express = require("express");4const AccountModel = require("../models/AccountModel");5var router = express.Router();6// 导入moment7const moment = require("moment");89// 记账本的列表10router.get("/account", function (req, res, next) {11 AccountModel.find()12 .then((data) => {13 res.render("list", { accounts: data, moment: moment });// 将moment传递给ejs页面,处理时间14 })15 .catch((err) => {16 if (err) {17 res.status(500).send("读取失败");18 return;19 }20 });21});2223module.exports = router;ejs文件里面使用moment,更改时间显示格式:
xxxxxxxxxx5212<html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta http-equiv="X-UA-Compatible" content="IE=edge" />6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />7 <title>Document</title>8 <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet" />9 <style>10 label {11 font-weight: normal;12 }13 .panel-body .glyphicon-remove {14 display: none;15 }16 .panel-body:hover .glyphicon-remove {17 display: inline-block;18 }19 </style>20 </head>21 <body>22 <div class="container">23 <div class="row">24 <div class="col-xs-12 col-lg-8 col-lg-offset-2">25 <h2>记账本</h2>26 <hr />27 <div class="accounts">28 <% accounts.forEach(item => { %>29 <div class="panel <%= item.type === -1 ? 'panel-warning' : 'panel-success' %>">30 <div class="panel-heading"><%= moment(item.time).format("YYYY-MM-DD") %></div>31 <div class="panel-body">32 <div class="col-xs-6"><%= item.remark %></div>33 <div class="col-xs-2 text-center">34 <span class="label <%= item.type === -1 ? 'label-warning' : 'label-success' %>"><%= item.type === -1 ? '支出' : '收入' %></span>35 </div>36 <div class="col-xs-2 text-right"><%= item.account %> 元</div>37 <div class="col-xs-2 text-right">3839 <a href="/account/<%= item.id %>">40 <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>41 </a>4243 </div>44 </div>45 </div>46 <% }) %>47 </div>48 </div>49 </div>50 </div>51 </body>52</html>显示效果:

6、删除文档
根据id来删除,mongodb里面保存的是_id,所以在涉及到id的地方,要注意。
xxxxxxxxxx311// routes/index.js23var express = require("express");4const AccountModel = require("../models/AccountModel");5var router = express.Router();6// 导入moment7const moment = require("moment");89// 删除记录10router.get("/account/del/:id", (req, res, next) => {11 // 获取params的id参数12 let id = req.params.id;13 // 删除14 AccountModel.deleteOne({ _id: id })15 .then((data) => {16 res.render("success", { msg: "删除成功", url: "/account" });17 })18 .catch((err) => {19 if(err){20 res.status(500).send("删除失败")21 return;22 }23 });24});2526// 记账本添加记录27router.get("/account/create", (req, res, next) => {28 res.render("create");29});3031module.exports = router;7、优化
为了防止用户误删,先弹出弹窗给用户确认。在列表页添加“新增”按钮,方便用户新增。
xxxxxxxxxx681<!-- views/list.ejs -->23<html lang="en">4 <head>5 <meta charset="UTF-8" />6 <meta http-equiv="X-UA-Compatible" content="IE=edge" />7 <meta name="viewport" content="width=device-width, initial-scale=1.0" />8 <title>Document</title>9 <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet" />10 <style>11 label {12 font-weight: normal;13 }14 .panel-body .glyphicon-remove {15 display: none;16 }17 .panel-body:hover .glyphicon-remove {18 display: inline-block;19 }20 </style>21 </head>22 <body>23 <div class="container">24 <div class="row">25 <div class="col-xs-12 col-lg-8 col-lg-offset-2">26 <div class="row">27 <h2 class="col-xs-6">记账本</h2>28 <h2 class="col-xs-6 text-right"><a href="/account/create" class="btn btn-primary">添加账单</a></h2>29 </div>30 <hr />31 <div class="accounts">32 <% accounts.forEach(item => { %>33 <div class="panel <%= item.type === -1 ? 'panel-warning' : 'panel-success' %>">34 <div class="panel-heading"><%= moment(item.time).format("YYYY-MM-DD") %></div>35 <div class="panel-body">36 <div class="col-xs-6"><%= item.remark %></div>37 <div class="col-xs-2 text-center">38 <span class="label <%= item.type === -1 ? 'label-warning' : 'label-success' %>"><%= item.type === -1 ? '支出' : '收入' %></span>39 </div>40 <div class="col-xs-2 text-right"><%= item.account %> 元</div>41 <div class="col-xs-2 text-right">42 <a class="addBtn" href="/account/del/<%= item._id %>">43 <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>44 </a>45 </div>46 </div>47 </div>48 <% }) %>49 </div>50 </div>51 </div>52 </div>53 </body>54 <script>55 let btns = document.querySelectorAll(".addBtn");5657 btns.forEach((item) => {58 item.addEventListener("click", function (e) {59 if (confirm("您确定要删除该文档吗?")) {60 return true;61 } else {62 // 阻止默认行为。即不能跳转了。63 e.preventDefault();64 }65 });66 });67 </script>68</html>显示效果:


案例小结:
里面的模型对象的方法then用法和5.x小节学习的用法不一样了,之前在then里面还可以写err,data参数,但是现在只有一个参数了,要在catch里面写err参数才行。(可能与mongoose版本有关)
接口是 前后端通信的桥梁
简单理解:一个接口就是 服务中的一个路由规则 ,根据请求响应结果
接口的英文单词是 API (Application Program Interface),所以有时也称之为 API 接口
这里的接口指的是『数据接口』, 与编程语言(Java,Go 等)中的接口语法不同
实现 前后端通信

大多数接口都是由 后端工程师开发的, 开发语言不限
一般情况下接口都是由 前端工程师 调用的,但有时 后端工程师也会调用接口 ,比如短信接口,支付接口等
一个接口一般由如下几个部分组成
一个接口示例 https://www.free-api.com/doc/325

RESTful API 是一种特殊风格的接口,主要特点有如下几个:
资源 ,路径中不能有 动词 ,例如 create , delete , update 等这些都不能有HTTP 请求方法 对应。比如说想要新增,就要使用POST请求方法。HTTP 响应状态码 对应。比如说找不到结果,就要返回404,;找到了结果,就要返回200。规则示例:
| 操作 | 请求类型 | URL | 返回 |
|---|---|---|---|
| 新增歌曲 | POST | /song | 返回新生成的歌曲信息 |
| 删除歌曲 | DELETE | /song/10 | 返回一个空文档 |
| 修改歌曲 | PUT | /song/10 | 返回更新后的歌曲信息 |
| 修改歌曲 | PATCH | /song/10 | 返回更新后的歌曲信息 |
| 获取所有歌曲 | GET | /song | 返回歌曲列表数组 |
| 获取单个歌曲 | GET | /song/10 | 返回单个歌曲信息 |
扩展阅读:https://www.ruanyifeng.com/blog/2014/05/restful_api.html
json-server 本身是一个 JS 编写的工具包,可以快速搭建 RESTful API 服务。
注意:json-server只是快速搭建了restful api服务,并不是把代码都给写出来了。只是可以很方便的调用一些接口,给前端使用,可以说是临时使用的好方法。
官方地址: https://github.com/typicode/json-server
操作步骤:
json-serverxxxxxxxxxx11npm i -g json-serverxxxxxxxxxx71{2 "song": [3 { "id": 1, "name": "干杯", "singer": "五月天" },4 { "id": 2, "name": "当", "singer": "动力火车" },5 { "id": 3, "name": "不能说的秘密", "singer": "周杰伦" }6 ]7}以 JSON 文件所在文件夹作为工作目录 ,执行命令:json-server --watch db.json。默认监听端口为 3000。
注意看Resources,里面只有一个资源http://localhost:3000/song,如果db.json里面添加了集合的话,资源会增加。

测试GET请求:


在浏览器中可以测试get请求,但是post/put/delete等请求,使用APIPOST等工具来测试更方便。在下一小节会讲到这些工具的使用。
介绍几个接口测试工具
1、基本使用


2、设置默认环境



3、添加新增接口

4、删除接口

5、更新接口

6、apipost的公共参数


7、apipost的文档功能


既然apipost这么好,是不是写接口都在apipost里面写就行了呢?
肯定不是啊,上面使用apipost有一个前提,就是使用了json-server,帮助我们构造了restful api,这样编写文档的时候可以很轻松。但是一般情况下,接口都是先写好再来测试的,接口里面具体执行的代码肯定是有点复杂的,要考虑很多问题。那么文档功能怎么加呢?是加在代码里面还是在apipost里面生成呢?我觉得还是在代码里面加比较简单,apipost与代码的结合能力我还没有体会到,除非apipost可以添加到代码里面去,然后代码执行之后,在apipost里面生成测试接口的包、生成文档,否则还是很麻烦。
可以找一下apipost有没有这样的功能,有的话就完美了。
其实面对接口,刚开始我还不知道该怎么写?其实还是routes,只不过之前的案例中访问routes,得到的是ejs页面,访问api,得到的是数据。这一点要搞清楚。
这个案例就是将接口写出来,用apifox测试即可。
将接口改为api接口,纯粹提供数据服务,具体的操作交给前端来做。
1、在routes里面新建api文件夹,创建account.js,将routes/index.js里面的代码复制过来。并在app.js中导入account.js。
xxxxxxxxxx601// routes/api/account.js23var express = require("express");4const AccountModel = require("../../models/AccountModel");5var router = express.Router();6// 导入moment7const moment = require("moment");89// 记账本的列表10router.get("/account", function (req, res, next) {11 AccountModel.find()12 .then((data) => {13 res.render("list", { accounts: data, moment: moment });14 })15 .catch((err) => {16 if (err) {17 res.status(500).send("读取失败");18 }19 });20});2122// 新增记账记录23router.post("/account", (req, res, next) => {24 AccountModel.create({25 req.body,26 // 修改 time 属性的值。这里用到了ES6的语法,在一个对象里面,如果出现同名属性,则会被后面的属性值覆盖。这种用法很简答,一定要会用。27 time: moment(req.body.time).toDate(),28 })29 .then((data) => {30 res.render("success", { msg: "添加成功", url: "/account" });31 })32 .catch((err) => {33 if (err) {34 res.status(500).send("添加失败");35 }36 });37});3839// 删除记录40router.get("/account/del/:id", (req, res, next) => {41 // 获取params的id参数42 let id = req.params.id;43 // 删除44 AccountModel.deleteOne({ _id: id })45 .then((data) => {46 res.render("success", { msg: "删除成功", url: "/account" });47 })48 .catch((err) => {49 if(err){50 res.status(500).send("删除失败")51 }52 });53});5455// 记账本添加记录56router.get("/account/create", (req, res, next) => {57 res.render("create");58});5960module.exports = router;
为api接口的路由地址添加了前缀api,测试OK:

下面就可以更改相应的接口了,更改也很简单,只需要将返回结果改写一下即可。说明一下:这里只是写接口,在apipost里面进行测试,并没有做页面的渲染,因为这不是重点。
2、获取账单接口
xxxxxxxxxx351// routes/api/account.js23var express = require("express");4const AccountModel = require("../../models/AccountModel");5var router = express.Router();6// 导入moment7const moment = require("moment");89// 记账本的列表10router.get("/account", function (req, res, next) {11 AccountModel.find()12 .then((data) => {13 res.json({14 // 响应的编号。可以写0000或000000,也可以写20000。因为银联返回的结果是0000.所以建议写000015 code: "0000",16 // 响应的信息17 msg: "读取成功",18 // 响应的数据19 data: data,20 });21 })22 .catch((err) => {23 if (err) {24 // 读取失败不需要返回statusCode吗?不用返回,因为浏览器会帮助我们显示具体的状态码。25 res.json({26 // 读取失败的code值,都有相应规定的,按照规定来做即可。27 code: "1001",28 msg: "读取失败",29 data: null,30 });31 }32 });33});3435module.exports = router;测试:

为了测试获取失败的场景,我在任务管理器中将MongoDB的服务停了:


3、创建账单接口
xxxxxxxxxx341// routes/api/account.js23var express = require("express");4const AccountModel = require("../../models/AccountModel");5var router = express.Router();6// 导入moment7const moment = require("moment");89// 新增记账记录10router.post("/account", (req, res, next) => {11 AccountModel.create({12 req.body,13 // 修改 time 属性的值。这里用到了ES6的语法,在一个对象里面,如果出现同名属性,则会被后面的属性值覆盖。这种用法很简答,一定要会用。14 time: moment(req.body.time).toDate(),15 })16 .then((data) => {17 res.json({18 code: "0000",19 msg: "新增成功",20 data: data,21 });22 })23 .catch((err) => {24 if (err) {25 res.json({26 code: "1002",27 msg: "新增失败",28 data: null,29 });30 }31 });32});3334module.exports = router;测试:


4、删除账单接口
xxxxxxxxxx331// routes/api/account.js23var express = require("express");4const AccountModel = require("../../models/AccountModel");5var router = express.Router();6// 导入moment7const moment = require("moment");89// 删除记录10router.delete("/account/:id", (req, res, next) => {11 // 获取params的id参数12 let id = req.params.id;13 // 删除14 AccountModel.deleteOne({ _id: id })15 .then((data) => {16 res.json({17 code: "0000",18 msg: "删除成功",19 data: {},20 });21 })22 .catch((err) => {23 if (err) {24 res.json({25 code: "1003",26 msg: "删除失败",27 data: null,28 });29 }30 });31});3233module.exports = router;测试:

5、获取单条账单接口
xxxxxxxxxx321// routes/api/account.js23var express = require("express");4const AccountModel = require("../../models/AccountModel");5var router = express.Router();6// 导入moment7const moment = require("moment");89// 获取单条数据10router.get("/account/:id", (req, res, next) => {11 // 获取params的id参数12 let id = req.params.id;13 AccountModel.findById(id)14 .then((data) => {15 res.json({16 code: "0000",17 msg: "查询成功",18 data: data,19 });20 })21 .catch((err) => {22 if (err) {23 res.json({24 code: "1004",25 msg: "查询失败",26 data: null,27 });28 }29 });30});3132module.exports = router;测试:

6、更新账单接口
xxxxxxxxxx321// routes/api/account.js23var express = require("express");4const AccountModel = require("../../models/AccountModel");5var router = express.Router();6// 导入moment7const moment = require("moment");89// 更新账单10router.patch("/account/:id", (req, res, next) => {11 // 获取params的id参数12 let id = req.params.id;13 AccountModel.updateOne({ _id: id }, { req.body, time: moment(req.body.time).toDate() })14 .then((data) => {15 res.json({16 code: "0000",17 msg: "更新成功",18 data: data,19 });20 })21 .catch((err) => {22 if (err) {23 res.json({24 code: "1005",25 msg: "更新失败",26 data: null,27 });28 }29 });30});3132module.exports = router;测试:

所谓会话控制就是 对会话进行控制。什么是“会话”?就是客户端通过http协议与服务端进行交流沟通。
HTTP 是一种无状态的协议,它没有办法区分多次的请求是否来自于同一个客户端, 无法区分用户。
而产品中又大量存在的这样的需求,所以我们需要通过 会话控制 来解决该问题。
常见的会话控制技术有三种:
cookie 是 HTTP 服务器发送到用户浏览器并保存在本地的一小块数据。
cookie 是保存在浏览器端的一小块数据。
cookie 是按照域名划分保存的。
简单示例:
| 域名 | cookie |
|---|---|
| www.baidu.com | a=100;b=200 |
| www.bilibili.com | xid=1020abce121;hm=112411213 |
| jd.com | x=100;ocw=12414cce |
浏览器向服务器发送请求时,会自动将 当前域名下 可用的 cookie 设置在请求头中,然后传递给服务器。这个请求头的名字也叫 cookie ,所以将 cookie 理解为一个 HTTP 的请求头也是可以的。
从上面的信息就可以知道下面的cookie运行流程了。之前关于cookie我只知道可以用来存取,比如说不方便存放在vuex中的数据可以放在cookie中,但这些都是浏览器端cookie。这里讲的是服务端和浏览器通信的cookie,两者还是不一样的。
1、浏览器端向服务端传递信息。
2、服务端接受到浏览器端的信息,校验通过之后,下发给浏览器端。
3、后续浏览器端发送请求时,会自动带上cookie。服务端就可以根据cookie来做后续处理。
那么真正要做的,就是前两步。第一步可以说是正常登录流程都会做的,那么重点就在第2步了。而第3步,就可以设置一个中间件,这样大部分接口都可以很方便处理cookie了。
浏览器端填写账号和密码校验身份,服务端校验通过后下发 cookie给浏览器端。

有了 cookie 之后,后续浏览器端向服务器发送请求时,就会自动携带 cookie。服务端就可以根据这个cookie来进行后续接口的校验。

浏览器操作 cookie 的操作,使用相对较少,大家了解即可
1、禁用所有 cookie


2、删除 cookie


3、查看 cookie
chrome浏览器中已经不允许查看cookie了,但只是不允许直接看,通过F12调出开发者工具还是可以看到的。

express 中可以使用 cookie-parser 进行处理cookie。使用express基本框架来做,不使用express-generator,为什么?因为express-generator会把东西全部搭建好。
1、搭建express基本框架,安装cookie-parser,npm i cookie-parser。
xxxxxxxxxx131// 导入express2const express = require("express");34// 创建应用对象5const app = express();67// 创建路由规则8app.get("/", (req, res) => {9 res.send("home");10});1112// 启动服务13app.listen(3000, () => {});2、 设置cookie
xxxxxxxxxx161// 导入express2const express = require("express");34// 创建应用对象5const app = express();67// 创建路由规则8app.get("/set-cookie", (req, res) => {9 // name是设置的cookie属性,zhangsan是属性值。zhangsan严格来说是要从浏览器端传递过来的,这里只是说明cookie的用法,所以简略一些。10 // res.cookie("name", "zhangsan");// 这种方式设置的cookie,在浏览器关闭的时候,会被销毁。11 res.cookie("name", "zhangsan", { maxAge: 20 * 1000 }); // 通过设置maxAge属性,可以设置cookie的生命周期长度,单位是ms。即使浏览器关闭之后,如果在存活时间之内,也会保存。12 res.send("home");13});1415// 启动服务16app.listen(3000, () => {});
3、删除cookie
xxxxxxxxxx251// 导入express2const express = require("express");34// 创建应用对象5const app = express();67// 创建路由规则8app.get("/set-cookie", (req, res) => {9 // name是设置的cookie属性,zhangsan是属性值。zhangsan严格来说是要从浏览器端传递过来的,这里只是说明cookie的用法,所以简略一些。10 // res.cookie("name", "zhangsan");// 这种方式设置的cookie,在浏览器关闭的时候,会被销毁。11 res.cookie("name", "zhangsan", { maxAge: 20 * 1000 }); // 通过设置maxAge属性,可以设置cookie的生命周期长度,单位是ms。即使浏览器关闭之后,如果在存活时间之内,也会保存。12 // 可以设置很多cookie13 res.cookie("theme", "blue");14 res.send("home");15});1617// 删除cookie18app.get("/remove-cookie", (req, res) => {19 res.clearCookie("name");20 res.send("删除成功");21});2223// 启动服务24app.listen(3000, () => {});25

再访问别的路由地址:

4、读取cookie
xxxxxxxxxx341// 导入express2const express = require("express");3// 导入cookie-parser4const cookieParser = require("cookie-parser");56// 创建应用对象7const app = express();8// 使用cookie-parser9app.use(cookieParser());1011// 创建路由规则12app.get("/set-cookie", (req, res) => {13 // name是设置的cookie属性,zhangsan是属性值。zhangsan严格来说是要从浏览器端传递过来的,这里只是说明cookie的用法,所以简略一些。14 // res.cookie("name", "zhangsan");// 这种方式设置的cookie,在浏览器关闭的时候,会被销毁。15 res.cookie("name", "zhangsan", { maxAge: 60 * 1000 }); // 通过设置maxAge属性,可以设置cookie的生命周期长度,单位是ms。即使浏览器关闭之后,如果在存活时间之内,也会保存。16 // 可以设置很多cookie17 res.cookie("theme", "blue");18 res.send("home");19});2021// 删除cookie22app.get("/remove-cookie", (req, res) => {23 res.clearCookie("name");24 res.send("删除成功");25});2627// 获取cookie28app.get("/get-cookie", (req, res) => {29 console.log(req.cookies);30 res.send(`欢迎您,${req.cookies.name}`);31});3233// 启动服务34app.listen(3000, () => {});

不同浏览器中的 cookie 是相互独立的,不共享。
session 是保存在 服务器端的一块数据 ,保存当前访问用户的相关信息。
实现会话控制,可以识别用户的身份,快速获取当前用户的相关信息。
浏览器端填写账号和密码校验身份,服务端校验通过后创建 session 信息 ,然后将 session_id 的值通过响应头返回给浏览器。

浏览器有了 cookie,下次发送请求时会自动携带 cookie,服务器通过 cookie 中的 session_id 的值确定用户的身份。

express 中可以使用 express-session 对 session 进行操作。使用express的基本代码结构,一步一步实现。
1、编写expres基本结构代码。安装express-session和connect-mongo,connect-mongo用于连接MongoDB数据库,这里是要把session存储到数据库中。
xxxxxxxxxx131// 导入express2const express = require("express");34// 创建应用对象5const app = express();67// 配置路由规则8app.get("/", (req, res) => {9 res.send("home");10});1112// 启动服务13app.listen(3000);2、express-session安装与配置
xxxxxxxxxx331// 导入express2const express = require("express");3// 导入express-session,connect-mongo4const session = require("express-session");5const MongoStore = require("connect-mongo");67// 创建应用对象8const app = express();910// 设置session中间件11app.use(12 session({13 name: "sid", //设置cookie的name,默认值是:connect.sid14 secret: "atguigu", //参与加密的字符串(又称签名)15 saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id16 resave: true, //是否在每次请求时重新保存session。因为session是有生命周期的,所以在session过期之后,用户需要重新登录,但是如果用户其实一直都在使用,那么可以设置resave为true,每次都重新保存session,延长session的实际生命周期,这样用户的体验会比较好,不用一直需要登录。17 store: MongoStore.create({18 mongoUrl: "mongodb://127.0.0.1:27017/project", //数据库的连接配置19 }),20 cookie: {21 httpOnly: true, // 开启后前端无法通过 JS 操作22 maxAge: 1000 * 300, // 这一条 是控制 sessionID 的过期时间的!!!单位ms。23 },24 })25);2627// 配置路由规则28app.get("/", (req, res) => {29 res.send("home");30});3132// 启动服务33app.listen(3000);将服务启动后,project数据库中就会出现sessions集合。

3、express中session的操作
创建session
xxxxxxxxxx451// 导入express2const express = require("express");3// 导入express-session,connect-mongo4const session = require("express-session");5const MongoStore = require("connect-mongo");67// 创建应用对象8const app = express();910// 设置session中间件11app.use(12 session({13 name: "sid", //设置cookie的name,默认值是:connect.sid14 secret: "atguigu", //参与加密的字符串(又称签名)15 saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id16 resave: true, //是否在每次请求时重新保存session。因为session是有生命周期的,所以在session过期之后,用户需要重新登录,但是如果用户其实一直都在使用,那么可以设置resave为true,每次都重新保存session,延长session的实际生命周期,这样用户的体验会比较好,不用一直需要登录。17 store: MongoStore.create({18 mongoUrl: "mongodb://127.0.0.1:27017/project", //数据库的连接配置19 }),20 cookie: {21 httpOnly: true, // 开启后前端无法通过 JS 操作22 maxAge: 1000 * 300, // 这一条 是控制 sessionID 的过期时间的!!!单位ms。23 },24 })25);2627// 配置路由规则28app.get("/", (req, res) => {29 res.send("home");30});3132// 登录33app.get("/login", (req, res) => {34 // 需求:用户名和密码都是admin,才创建session。这一步在实际项目中其实可以这样做,获取到username和password,然后在数据库中查找有没有,有就登录成功。35 const { username, password } = req.query;36 if (username === "admin" && password === "admin") {37 req.session.username = "admin";// 可以创建多个session,这里只创建了一个。38 res.send("登录成功");39 } else {40 res.send("登录失败");41 }42});4344// 启动服务45app.listen(3000);


获取session
xxxxxxxxxx551// 导入express2const express = require("express");3// 导入express-session,connect-mongo4const session = require("express-session");5const MongoStore = require("connect-mongo");67// 创建应用对象8const app = express();910// 设置session中间件11app.use(12 session({13 name: "sid", //设置cookie的name,默认值是:connect.sid14 secret: "atguigu", //参与加密的字符串(又称签名)15 saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id16 resave: true, //是否在每次请求时重新保存session。因为session是有生命周期的,所以在session过期之后,用户需要重新登录,但是如果用户其实一直都在使用,那么可以设置resave为true,每次都重新保存session,延长session的实际生命周期,这样用户的体验会比较好,不用一直需要登录。17 store: MongoStore.create({18 mongoUrl: "mongodb://127.0.0.1:27017/project", //数据库的连接配置19 }),20 cookie: {21 httpOnly: true, // 开启后前端无法通过 JS 操作22 maxAge: 1000 * 300, // 这一条 是控制 sessionID 的过期时间的!!!单位ms。23 },24 })25);2627// 配置路由规则28app.get("/", (req, res) => {29 res.send("home");30});3132// 登录33app.get("/login", (req, res) => {34 // 需求:用户名和密码都是admin,才创建session。这一步在实际项目中其实可以这样做,获取到username和password,然后在数据库中查找有没有,有就登录成功。35 const { username, password } = req.query;36 if (username === "admin" && password === "admin") {37 req.session.username = "admin"; // 可以创建多个session,这里只创建了一个。38 res.send("登录成功");39 } else {40 res.send("登录失败");41 }42});4344// session的读取。这里有一个需求,如果传递过来的session有username,那么就可以返回数据;否则就不返回数据。45app.get("/cart", (req, res) => {46 // 检测session,是否存在用户数据。为什么可以直接使用req.session.username,而不是根据cookie里面的信息,从数据库中取出session,然后比对?这是因为express-session这个库已经帮助我们处理好了session的相关操作,而且这个库被注册为了全局中间件,我们直接使用这个库的方法即可。当然具体用法还是要看文档。47 if (req.session.username) {48 res.send(`购物车页面,欢迎您 ${req.session.username}`);49 } else {50 res.send("您还没有登录~");51 }52});5354// 启动服务55app.listen(3000);
销毁session
xxxxxxxxxx631// 导入express2const express = require("express");3// 导入express-session,connect-mongo4const session = require("express-session");5const MongoStore = require("connect-mongo");67// 创建应用对象8const app = express();910// 设置session中间件11app.use(12 session({13 name: "sid", //设置cookie的name,默认值是:connect.sid14 secret: "atguigu", //参与加密的字符串(又称签名)15 saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id16 resave: true, //是否在每次请求时重新保存session。因为session是有生命周期的,所以在session过期之后,用户需要重新登录,但是如果用户其实一直都在使用,那么可以设置resave为true,每次都重新保存session,延长session的实际生命周期,这样用户的体验会比较好,不用一直需要登录。17 store: MongoStore.create({18 mongoUrl: "mongodb://127.0.0.1:27017/project", //数据库的连接配置19 }),20 cookie: {21 httpOnly: true, // 开启后前端无法通过 JS 操作22 maxAge: 1000 * 300, // 这一条 是控制 sessionID 的过期时间的!!!单位ms。23 },24 })25);2627// 配置路由规则28app.get("/", (req, res) => {29 res.send("home");30});3132// 登录33app.get("/login", (req, res) => {34 // 需求:用户名和密码都是admin,才创建session。这一步在实际项目中其实可以这样做,获取到username和password,然后在数据库中查找有没有,有就登录成功。35 const { username, password } = req.query;36 if (username === "admin" && password === "admin") {37 req.session.username = "admin"; // 可以创建多个session,这里只创建了一个。38 res.send("登录成功");39 } else {40 res.send("登录失败");41 }42});4344// session的读取。这里有一个需求,如果传递过来的session有username,那么就可以返回数据;否则就不返回数据。45app.get("/cart", (req, res) => {46 // 检测session,是否存在用户数据。为什么可以直接使用req.session.username,而不是根据cookie里面的信息,从数据库中取出session,然后比对?这是因为express-session这个库已经帮助我们处理好了session的相关操作,而且这个库被注册为了全局中间件,我们直接使用这个库的方法即可。47 if (req.session.username) {48 res.send(`购物车页面,欢迎您 ${req.session.username}`);49 } else {50 res.send("您还没有登录~");51 }52});5354// session的销毁55app.get("/logout", (req, res) => {56 // 不仅仅是浏览器端的session会失效,并且mongo数据库中存储的session也会被销毁。57 req.session.destroy(() => {58 res.send("退出成功~");59 });60});6162// 启动服务63app.listen(3000);
还是拿上一个案例来做完善,这里完善的只是session相关的部分,别的内容先不管。
在views下创建reg.ejs文件,用作注册页面。文件老师已经提供了。
xxxxxxxxxx371<!-- views/auth/reg.ejs -->234<html lang="en">56<head>7 <meta charset="UTF-8" />8 <meta http-equiv="X-UA-Compatible" content="IE=edge" />9 <meta name="viewport" content="width=device-width, initial-scale=1.0" />10 <title>注册</title>11 <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet" />12</head>1314<body>15 <div class="container">16 <div class="row">17 <div class="col-xs-12 col-md-8 col-md-offset-2 col-lg-4 col-lg-offset-4">18 <h2>注册</h2>19 <hr />20 <form method="post" action="/reg">21 <div class="form-group">22 <label for="item">用户名</label>23 <input name="username" type="text" class="form-control" id="item" />24 </div>25 <div class="form-group">26 <label for="time">密码</label>27 <input name="password" type="password" class="form-control" id="time" />28 </div>29 <hr>30 <button type="submit" class="btn btn-primary btn-block">注册</button>31 </form>32 </div>33 </div>34 </div>35</body>3637</html>在routes里面创建auth.js,用作注册相关路由:
xxxxxxxxxx111// routes/auth.js23var express = require("express");4var router = express.Router();56// 注册7router.get("/reg", (req, res) => {8 res.render("auth/reg");9});1011module.exports = router;在app.js中导入auth路由文件:

运行并访问:

创建UserModel.js,保存注册用户的相关信息:
xxxxxxxxxx221// models/UserModel.js23// 导入mongoose4const mongoose = require("mongoose");56// 创建文档的结构对象7let UserSchema = new mongoose.Schema({8 username: {9 type: String,10 required: true,11 },12 password: {13 type: String,14 required: true,15 },16});1718// 创建模型对象19let UserModel = mongoose.model("users", UserSchema);2021// 暴露模型对象22module.exports = UserModel;编写注册路由:
xxxxxxxxxx301// routes/auth.js23var express = require("express");4var router = express.Router();5// 导入model6const UserModel = require("../models/UserModel");78// 注册9router.get("/reg", (req, res) => {10 res.render("auth/reg");11});1213// 注册用户14router.post("/reg", (req, res) => {15 // console.log(req.body);1617 UserModel.create({ req.body })18 .then((data) => {19 if (data) {20 res.render("success", { msg: "注册成功", url: "/login" });21 }22 })23 .catch((err) => {24 if (err) {25 res.status(500).send("注册失败,请稍后再试~");26 }27 });28});2930module.exports = router;效果:


因为用户的密码很重要,所以这里用到了md5来加密用户的密码再保存。但md5是单向加密,即加密后不能解密,那用户登录的时候怎么看用户输入的密码是否正确呢?可以在登录时使用md5加密账户密码,然后比较数据库中存储的加密后的密码是否一致。安装:npm i md5
xxxxxxxxxx321// routes/auth.js23var express = require("express");4var router = express.Router();5// 导入model6const UserModel = require("../models/UserModel");7// 导入md58const md5 = require("md5");910// 注册11router.get("/reg", (req, res) => {12 res.render("auth/reg");13});1415// 注册用户16router.post("/reg", (req, res) => {17 // console.log(req.body);1819 UserModel.create({ req.body, password: md5(req.body.password) })20 .then((data) => {21 if (data) {22 res.render("success", { msg: "注册成功", url: "/login" });23 }24 })25 .catch((err) => {26 if (err) {27 res.status(500).send("注册失败,请稍后再试~");28 }29 });30});3132module.exports = router;
创建登录界面:
xxxxxxxxxx371<!-- views/auth/login.ejs -->234<html lang="en">56<head>7 <meta charset="UTF-8" />8 <meta http-equiv="X-UA-Compatible" content="IE=edge" />9 <meta name="viewport" content="width=device-width, initial-scale=1.0" />10 <title>登录</title>11 <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet" />12</head>1314<body>15 <div class="container">16 <div class="row">17 <div class="col-xs-12 col-md-8 col-md-offset-2 col-lg-4 col-lg-offset-4">18 <h2>登录</h2>19 <hr />20 <form method="post" action="/login">21 <div class="form-group">22 <label for="item">用户名</label>23 <input name="username" type="text" class="form-control" id="item" />24 </div>25 <div class="form-group">26 <label for="time">密码</label>27 <input name="password" type="password" class="form-control" id="time" />28 </div>29 <hr>30 <button type="submit" class="btn btn-primary btn-block">登录</button>31 </form>32 </div>33 </div>34 </div>35</body>3637</html>编写路由规则:
xxxxxxxxxx341// routes/auth.js23var express = require("express");4var router = express.Router();5// 导入model6const UserModel = require("../models/UserModel");7// 导入md58const md5 = require("md5");910// 登录页面11router.get("/login", (req, res) => {12 res.render("auth/login");13});1415// 用户登录16router.post("/login", (req, res) => {17 // 获取用户名和密码18 const { username, password } = req.body;19 // 查询数据库20 UserModel.findOne({ username: username, password: md5(password) })21 .then((data) => {22 if (!data || (Array.isArray(data) && data.length === 0)) {23 res.status(500).send("用户名或密码错误");24 }25 res.render("success", { msg: "登录成功", url: "/account" });26 })27 .catch((err) => {28 if (err) {29 res.status(500).send("登录失败,请重试");30 }31 });32});3334module.exports = router;
在上一小节中,其实忘记做了一件事情,就是写session,这样跳转到account页面之后,有session才能获取到数据。(更进一步的,需要session才能获取到用户对应的数据,这一步这个案例没做)
项目中安装npm i express-session connect-mongo,按照之前案例的写法,将这两个中间件用起来。因为session涉及到大部分接口,所以必须在app.js里面进行配置。

写入session:
xxxxxxxxxx371// routes/auth.js23var express = require("express");4var router = express.Router();5// 导入model6const UserModel = require("../models/UserModel");7// 导入md58const md5 = require("md5");910// 登录页面11router.get("/login", (req, res) => {12 res.render("auth/login");13});1415// 用户登录16router.post("/login", (req, res) => {17 // 获取用户名和密码18 const { username, password } = req.body;19 // 查询数据库20 UserModel.findOne({ username: username, password: md5(password) })21 .then((data) => {22 if (!data) {23 res.status(500).send("用户名或密码错误");24 }25 // 写入session26 req.session.username = username;27 req.session._id = data._id;// 这个_id在查找用户的时候很有用。28 res.render("success", { msg: "登录成功", url: "/account" });29 })30 .catch((err) => {31 if (err) {32 res.status(500).send("登录失败,请重试");33 }34 });35});3637module.exports = router;
需求:检测用户是否登录,只有登录后才能查看列表。
因为登录检测大部分接口都需要用到,所以做成一个中间件。创建middlewares文件夹,里面创建具体的中间件。
xxxxxxxxxx111// middlewares/chekcLoginMiddleware.js23// 检测用户是否登录的中间件4module.exports = (req, res, next) => {5 // 这里的req.session.username为什么可以直接获取?这是express-session库帮助我们能够这样做。6 if (!req.session.username) {7 return res.redirect("/login");8 }9 next();10};11在需要用到中间件的地方,使用:
xxxxxxxxxx11// routes/index.js
打开一个没有session的浏览器窗口,查看效果,可以看到一输入/account之后,就立即跳转到登录界面了。

在list.ejs里面添加一个退出登录的按钮:

编写路由规则:
xxxxxxxxxx181// routes/auth.js23var express = require("express");4var router = express.Router();5// 导入model6const UserModel = require("../models/UserModel");7// 导入md58const md5 = require("md5");910// 退出登录11router.post("/logout", (req, res) => {12 // 销毁session13 req.session.destroy(() => {14 res.render("success", { msg: "退出成功", url: "/login" });15 });16});1718module.exports = router;查看效果:

用live-server打开attack.html,这样我的account账号就退出登录了。

并且从attack.html发送请求的时候,会带上cookie。这是由于logout方法是GET造成的,需要将这个方法改为POST,就能解决问题。
将list.ejs里面的退出元素改为form表单:

将退出的路由方法改为post:

首页路由配置:
xxxxxxxxxx71// routes/index.js23// 首页4router.get("/", (req, res) => {5 // 直接重定向到account6 res.redirect("/account");7});404页面配置。app.js已经有了404相关代码,只需要进行修改即可:


cookie 和 session 的区别主要有如下几点:
相对 较好4K ,且单个域名下的存储数量也有限制token 是服务端生成并返回给 HTTP 客户端的一串加密字符串,token中保存着 用户信息。


实现会话控制,可以识别用户的身份,主要用于移动端 APP(安卓、IOS、小程序、H5等等)。
浏览器填写账号和密码校验身份,服务端校验通过后响应 token,token 一般是在响应体中返回给客户端的。

浏览器后续发送请求时,需要手动将 token 添加在请求报文中,一般是放在请求头中。

服务端压力更小
相对更安全
扩展性更强
JWT(JSON Web Token )是目前最流行的跨域认证解决方案,可用于基于 token 的身份验证。
JWT 使 token 的生成与校验更规范。
我们可以使用 jsonwebtoken 包 来操作 token。
xxxxxxxxxx191// 导入 jsonwebtokan2const jwt = require('jsonwebtoken');34// 创建 token5// jwt.sign(数据, 加密字符串, 配置对象)6let token = jwt.sign({7 username: 'zhangsan'8}, 'atguigu', {9 expiresIn: 60 //单位是 秒10})1112// 校验 token13jwt.verify(token, 'atguigu', (err, data) => {14 if(err){15 console.log('校验失败~~');16 return17 }18 console.log(data);19})查看一下token是什么:

查看一下校验结果:

扩展阅读:https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
从上面的例子可以看到,token的操作非常简单。
接受浏览器端传过来的参数→创建token→设置中间件,在需要的地方校验token。
同时校验token得到的信息,可以从数据库中找到相应的数据,这样就可以返回给不同的用户以不同的结果了。
需求:对api接口进行限制,加上token。就在上一个案例中完善,安装npm i jsonwebtoken。
在routes/api里面创建auth.js,添加api登录的接口:
xxxxxxxxxx541// routes/api/auth.js23var express = require("express");4var router = express.Router();5// 导入model6const UserModel = require("../../models/UserModel");7// 导入md58const md5 = require("md5");9// 导入jsonwebtoken10const jwt = require("jsonwebtoken");1112// 用户登录13router.post("/login", (req, res) => {14 // 获取用户名和密码15 const { username, password } = req.body;16 // 查询数据库17 UserModel.findOne({ username: username, password: md5(password) })18 .then((data) => {19 if (!data) {20 return res.json({21 code: "2001",22 msg: "用户名或密码错误",23 data: null,24 });25 }26 // 生成token27 let token = jwt.sign(28 {29 username: data.username,30 _id: data._id,31 },32 "atguigu",33 {34 expiresIn: 60 * 60 * 24 * 7,35 }36 );37 res.json({38 code: "0000",39 msg: "登录成功",40 data: token,41 });42 })43 .catch((err) => {44 if (err) {45 res.json({46 code: "2002",47 msg: "登录失败,请重试!",48 data: null,49 });50 }51 });52});5354module.exports = router;在app.js中导入并使用这个路由文件:

使用apipost来测试接口,有种熟悉的感觉了:


服务端将token传递给浏览器端之后,浏览器端需要在后续的请求中,在请求头上加上token,这样在服务端会校验请求头里面的token是否正确。
为了能在大部分api接口里面校验token,需要创建一个中间件,api接口直接使用中间件即可。(注意这个中间件不能是全局中间件,因为有一些接口是不能使用token校验的,比如说注册、登录的接口)
xxxxxxxxxx281// middlewares/checkTokenMiddleware.js23const jwt = require("jsonwebtoken")45module.exports = (req, res, next) => {6 // 获取token。因为用户登录后,都会在接口的请求头中携带token,所以这里可以从req中获取到。7 let token = req.get("token")8 // 判断是否有token9 if(!token){10 return res.json({11 code:"2003",12 msg:"token 缺失",13 data:null14 })15 }16 // 校验token17 jwt.verify(token,"atguigu",(err,data)=>{18 if(err){19 return res.json({20 code:"2004",21 msg:"token校验失败",22 data:null23 })24 }25 // 如果 token 校验成功。26 next();27 })28};在需要使用中间件的地方,引入:

测试:


将jwt用到的密匙放在配置文件中,用到的地方就导入使用:
xxxxxxxxxx81// config/config.js23module.exports = {4 DBHOST: "127.0.0.1",5 DBPORT: "27017",6 DBNAME: "bilibili",7 SECRET: "atguigu",8};在token校验OK之后,在req上挂载用户信息,这样在使用此中间件的路由规则里面,就可以获取到用户信息,根据用户信息可以获取相应的数据:
xxxxxxxxxx331// middleware/checkTokenMiddleware.js23const jwt = require("jsonwebtoken");4// 导入配置项5const { SECRET } = require("../config/config");67module.exports = (req, res, next) => {8 // 获取token9 let token = req.get("token");10 // 判断是否有token11 if (!token) {12 return res.json({13 code: "2003",14 msg: "token 缺失",15 data: null,16 });17 }18 // 校验token19 jwt.verify(token, SECRET, (err, data) => {20 if (err) {21 return res.json({22 code: "2004",23 msg: "token校验失败",24 data: null,25 });26 }27 // 保存用户信息。为什么这里的data有用户信息?从之前单独的jsonwebtoken案例就可以看出来,jwt.verify校验是会解析出原始保存的信息的。28 // 这样在使用中间件的路由规则里面,就可以获取到用户信息,根据用户信息来获取相应的数据。29 req.user = data;30 // 如果 token 校验成功31 next();32 });33};xxxxxxxxxx191// routes/api/account.js23var express = require("express");4const AccountModel = require("../../models/AccountModel");5var router = express.Router();6// 导入moment7const moment = require("moment");8// 导入token校验中间件9const checkTokenMiddleware = require("../../middlewares/checkTokenMiddleware");1011// 记账本的列表12router.get("/account", checkTokenMiddleware, function (req, res, next) {13 14 // 打印出req.user看一看15 console.log(req.user);// 实现的原理和req.body、req.session的原理是一样的,都是通过中间件处理之后挂载上去的。16 17});1819module.exports = router;需求:将127.0.0.1的本地域名配置为www.baidu.com,这样就可以通过www.baidu.com:3000来访问此项目了(注意要清除缓存,不想清除可以打开浏览器的无痕窗口来查看)。



所谓本地域名就是 只能在本机使用的域名 ,一般在开发阶段使用。
编辑文件 C:\Windows\System32\drivers\etc\hosts
xxxxxxxxxx11127.0.0.1 www.baidu.com如果修改失败, 可以修改该文件的权限

在地址栏输入 域名 之后,浏览器会先进行 DNS(Domain Name System) 查询,获取该域名对应的 IP 地址。请求会发送到 DNS 服务器,可以 根据域名返回 IP 地址。
可以通过 ipconfig /all 查看本机的 DNS 服务器。
hosts 文件也可以设置域名与 IP 的映射关系,在发送请求前,可以通过该文件获取域名的 IP 地址。
这个不用讲了。/public/upload文件夹应该加入到.gitignore里面,因为项目上线之后,所有的数据和数据库都要清空,这个文件夹专门是附件的文件夹,所以不用上传。

打开远程桌面连接:



在服务器中安装项目相关的软件,git、nodejs、vscode、数据库等等。
在GitHub或Gitee里面克隆项目代码到服务器,将项目启动。
我还以为nodejs-express项目会进行打包操作,哪知道老师只是使用npm start将项目启动起来了(注意package.json里面的命令不要使用nodemon,而要使用node,因为nodemon是调试用的)。有没有打包呢?放到更专业的服务器上?以后找一下答案。





https本意是http + SSL(secure sockets layer 安全套接层)。https可以加密http报文,所以大家也可以理解为是安全的http。
在服务器中操作:
1、下载工具:https://dl.eff.org/certbot-beta-installer-win_amd64.exe
2、安装工具
3、管理员运行命令certbot certonly --standalone

4、代码配置如下:
xxxxxxxxxx131const https = require("https")2https3.createServer(4 {5 key: fs.readFileSync("/etc/letsencrypt/path/to/key.pem"),6 cert: fs.readFileSync("/etc/letsencrypt/path/to/cert.pem"),7 ca: fs.readFileSync("/etc/letsencrypt/path/to/chain.pem")8 },9 app10)11.listen(443, () => {12 console.log("listening...")13})将项目的bin/www里面用http启动的服务,换成用https启动的服务,并将加密工具的地址换成安装的实际地址:


使用https启动服务,需要换成默认的443端口。
如果想要http和https协议都可以访问网站,可以在bin/www里面同时用http和https启动项目,具体可以看老师的讲解。
5、证书更新
证书有效期为三个月,可以通过下面的命令更新。
xxxxxxxxxx51## 一般更新(如果证书有效期小于1个月,可以用这个命令)2certbot renew34## 强制更新(如果证书有效期大于1个月,可以用这个命令)5certbot --force-renewal