高仿“饿了么”Vue项目(二)
这一讲我们继续研究高仿“饿了么”项目的后台代码,特别是数据库存储部分。
一、MongoDB的基本概念
MongoDB中有三个重要概念:Database、Collection、Document,分别是数据库、集合和文档。
- Database概念相当于MySQL中的数据库。
- Collection概念相当于MySQL中的表。
- Document概念相当于MySQL中的记录。
如下图所示:
从概念的名称上,不难看出MongoDB有意识地与传统的关系型数据库区别开。一个“表”,在MongoDB中称为“集合”,即一组有关联的数据的集合,但集合中的文档可能格式各不相同。一个“记录”,在MongoDB中称为“文档”。
每一个文档都有一个_id字段,它类似于Oracle数据库的rowid。关于_id字段,有如下说明:
- 在Mongo数据库中,存储在集合中的每个文档都需要一个惟一的_id域作为主键。如果插入操作时遗漏了_id域,Mongo会自动给_id域生成一个ObjectId。
- ObjectId是以其创建时的时间戳的4字节为首的12字节组成的值,规则如下:
B、3字节的机器标识符
C、2字节的进程ID
D、3字节的计数器,由一个随机数开始
二、Mongoose
Mongoose,发音['mɑːŋɡuːs],中文意为猫鼬,它的字形和Mongo很接近。
Mongoose是node中进行MongoDB开发的接口。
Mongoose中有以下几个重要概念:Schema、Model、Document,即架构、模型和文档。
Schema用于定义数据结构,类似于表结构定义。每个Schema(架构)会对应于一个MongoDB中的Collection(集合)。Schema不仅可以定义数据结构,还可以定义方法。
Model是由Schema编译而成的构造器,相当于一个类。Model可以对集合进行增删改查。
Document是由Model创建的实例,是一个对象,它相当于一条记录。
理解这几个概念后,我们就可以开始查看项目的源代码了。
三、项目源代码
项目的目录结构如下:
1. mongodb/db.js
'use strict'; import mongoose from 'mongoose'; import config from 'config-lite'; import chalk from 'chalk'; mongoose.connect(config.url, { useMongoClient: true }); mongoose.Promise = global.Promise; const db = mongoose.connection; db.once('open', () => { console.log( chalk.green('连接数据库成功') ); }) db.on('error', function (error) { console.error( chalk.red('Error in MongoDb connection: ' + error) ); mongoose.disconnect(); }); db.on('close', function () { console.log( chalk.red('数据库断开,重新连接数据库') ); mongoose.connect(config.url, { server: { auto_reconnect: true } }); }); export default db;
从代码中大概可以看出,这个文件的作用是连接数据库。app.js会包含这个文件,所以后台程序一启动,就会去连接MongoDB数据库。
mongoose.connect(uri, options);
mongoose.connect()方法的作用是连接数据库。uri是数据库连接字符串,在配置文件config/default.js中有定义config.url。options是一些选项,选项的细节我们就不管了。
Node中所有的异步操作在完成时都会发送一个事件到事件队列。所有这些产生事件的对象都是events.EventEmitter的实例。
EventEmitter.addListener(event, listener)
为指定事件添加一个监听器。
EventEmitter.on(event, listener)
为指定事件注册一个监听器,接受一个字符串event和一个回调函数。
EventEmitter.once(event, listener)
为指定事件注册一个单次监听器,即监听器最多只会触发一次,触发后立刻解除该监听器。
上面的代码中,为“open”、“close”和“error”事件分别注册了事件监听器。
在项目的代码中,可能会发现定义变量,很多时候都使用了const关键字,例如:
const db = mongoose.connection;
一般情况下,如果该变量只被赋值一次,后面不会再被修改,都建议使用const(常量)。for循环的循环变量可以使用let,原则上如果变量不会被修改,都建议使用const。
其它插件:
config-lite是一个轻量的读取配置文件的模块。config-lite会根据环境变量(NODE_ENV)的不同从当前执行进程目录下的config目录加载不同的配置文件。如果不设置NODE_ENV,则读取默认的default配置文件,如果设置了NODE_ENV,则会合并指定的配置文件和default配置文件作为配置,config-lite支持.js、.json、.node、.yml、.yaml后缀的文件。
chalk,发音[tʃɔk],中文意为粉笔,是一个颜色的插件,可以通过chalk.blue('hello world')为控制台输出的内容提供颜色。
2. models/admin/admin.js
models目录下的文件主要用于创建模型(Model)。前面已经说过了Mongoose中有几个重要概念:Schema、Model、Document。通常先创建Schema,再创建Model,那么其它模块就可以使用new Model()语法创建对象了。
'use strict'; import mongoose from 'mongoose' const Schema = mongoose.Schema; const adminSchema = new Schema({ user_name: String, password: String, id: Number, create_time: String, admin: { type: String, default: '管理员' }, status: Number, avatar: { type: String, default: 'default.jpg' }, city: String, }) adminSchema.index({ id: 1 }); const Admin = mongoose.model('Admin', adminSchema); export default Admin
这里,首先创建了一个adminSchema对象,它是new Schema()。Schema描述了“表”的数据结构。
adminSchema.index({ id: 1 });这句话创建了一个索引,按id升序排列。
之后,根据Schema,生成了Model,Model可以理解为Java中的实体类。
mongoose.model()方法的第一个参数是模型名称,第二个参数是Schema对象。
Mongoose会将集合名称设为模型名称(第一个参数)的小写形式。如果模型名称的最后一个字符是字母,则会变成复数;如果模型名称的最后一个字符是数字,则不变。例如模型名称为"Admin",则集合名称为"admins";如果模型名称为"Model1",则集合名称为"model1"。
这样,通过指定模型名称,就能够与数据库中的集合名称对应起来。
3. controller/admin/admin.js
这个文件是具体的业务操作类。
由于这个类代码较长,我们截取了一些典型的代码。
代码一,查询所有管理员:
class Admin { async getAllAdmin(req, res, next) { const { limit = 20, offset = 0 } = req.query; try { const allAdmin = await AdminModel.find({}, '-_id -password').sort({ id: -1 }).skip(Number(offset)).limit(Number(limit)) res.send({ status: 1, data: allAdmin, }) } catch (err) { console.log('获取超级管理列表失败', err); res.send({ status: 0, type: 'ERROR_GET_ADMIN_LIST', message: '获取超级管理列表失败' }) } } }
这个类中定义的方法都是异步方法,都用async修饰,async是ES7引入的用于异步操作的关键字,await也是。
async放在方法前,用于声明一个方法是异步方法。await放在方法调用前,表示等待异步方法执行完成,并取得异步方法的返回结果。
在上面的代码中,用到了Model的一些方法。
AdminModel.find({}, '-_id -password'),find()方法的作用是在MongoDB中执行查询,参数一是查询条件,是JSON格式的查询条件,参数二是字段过滤,例子中'-_id -password'表示不显示_id字段和password字段。
接下来的几个方法是对查询结果进行再次处理。
sort({ id: -1 }),sort()方法的作用是排序,{ id: -1 }表示按id字段降序排列,-1为降序,1为升序。
skip()和limit()方法常用于分页,skip表示忽略前M条,limit表示只显示N条。
这一系列方法调用形成链式调用。
代码二,注册管理员:
try { const admin = await AdminModel.findOne({ user_name }) if (admin) { console.log('该用户已经存在'); res.send({ status: 0, type: 'USER_HAS_EXIST', message: '该用户已经存在', }) } else { const adminTip = status == 1 ? '管理员' : '超级管理员' const admin_id = await this.getId('admin_id'); const newpassword = this.encryption(password); const newAdmin = { user_name, password: newpassword, id: admin_id, create_time: dtime().format('YYYY-MM-DD'), admin: adminTip, status, } await AdminModel.create(newAdmin) req.session.admin_id = admin_id; res.send({ status: 1, message: '注册管理员成功', }) } } catch (err) { console.log('注册管理员失败', err); res.send({ status: 0, type: 'REGISTER_ADMIN_FAILED', message: '注册管理员失败', }) }
在这段代码中,使用了AdminModel.findOne()来查找并返回一条数据,使用了AdminModel.create(),新增一条数据。
如果你想了解更多关于Mongoose的细节,请查看http://mongoosejs.com/docs/api.html。