今天开始使用 node + vue3 + ts搭建一个简易资金管理系统的前后端分离项目,因为前后端分离所以会分两个专栏分别讲解前端与后端的实现,后端项目文章讲解可参考:前端链接,我会在前后端的两类专栏的最后一篇文章中会将项目代码开源到我的GithHub上,大家可以自行去进行下载运行,希望本文章对有帮助的朋友们能多多关注本专栏,学习更多前端node知识,然后开篇先简单介绍一下本项目用到的技术栈都有哪几个方面(阅读本文章能够学习到的技术):
node:基于Chrome V8引擎的开源、跨平台的JavaScript运行环境。
express:基于node.js的Web应用框架,帮助开发者快速构建可靠、高效的Web应用程序。
pm2:针对node应用生产环境进程管理工具,可帮助简化应用程序部署运行和监视等工作。
MongoDB:面向文档的NoSQL数据库系统,它可以灵活地存储非结构化数据。
Apifox:易用界面和功能,帮助开发人员和团队更高效地创建、设计和管理 RESTful API。
jwt:用于定义在网络上传输的信息安全可验证和可信任的方式,用于身份验证和授权机制。
目录
express搭建服务器
连接MongoDB数据库
搭建注册接口
搭建登录接口
获取登录信息
添加和获取信息
编辑和删除信息
express搭建服务器
首先我们需要创建一个文件夹用于存放后端项目,然后将文件夹拖到编辑器vscode当中,接下来需要打开终端执行 npm init 初始化package.json文件,相关步骤如下:
接下来开始装载 express 框架,终端执行如下命令:
npm install express
接下来我们在项目文件夹中创建入口文件 server.js 然后开始使用 express 框架:
const express = require("express")
const app = express()
// 设置路由
app.get('/', (req,res) =>{
res.send('hello world')
})
// 设置端口号
const post = process.env.PORT || 8080
app.listen(post, () => {
console.log(`Server is running on port ${post},url: http://127.0.0.1:${post}`)
})
为了方便代码的执行,这里我们采用 pm2 进程管理工具进行使用,关于pm2的安装和具体的使用教程可以参考我之前的文章:深入理解 PM2:Node.js 应用部署和管理利器 ,为了方便简介,这里我们可以直接在package.json文件中设置一下我们的命令,如 下:
pm2进程管理工具是不会随着你将终端关闭而停止运行的,如果是第一次运行项目的话,可以根据我们上面设置的命令执行:npm run start 即可,如果想关闭项目的话,执行 pm2 stop 加项目名称就行了,如果实时查看日志,执行 pm2 log 即可,具体的实操自己体会,这里不再赘述。
接下来我们打开我们本地的 5000 端口,可以看到我们后端运行的项目了:
连接MongoDB数据库
接下来我们在本地创建一个 MongoDB数据库,用来后面后端编写相应接口所需要存放的数据,如果不了解MongoDB的朋友推荐可以参考一下我之前的文章:MongoDB数据库 ,接下来我们需要开始执行 node 连接MongoDB数据库了。
启动MongoDB服务:
点击win键输入cmd,点击以管理员身份运行
执行 net start MongoDB 命令,运行MongoDB数据库服务:
创建MongoDB数据库:
使用 Navicat 图形化管理工具,创建数据库。点击新建连接,选择 MongoDB
配置相应参数,连接直接默认即可,一般情况下,我们连接只需要连接主库查数据,所以选择独立的这个连接方式就可以,填写好常规参数可以点击测试连接是否正常,即可连接!因为是个人测试嘛,使用的是本地的localhost:
出现如下界面说明连接成功,我们直接点击确定即可。
注意:进入到数据库之后,我们删除默认的数据库,重新创建一个名为 node_fund 名称的数据库
连接MongoDB数据库:
接下来我们需要借助vscode工具来连接mongodb数据库,首先终端执行如下命令安装相应的包:
npm install mongoose
安装完成之后,我们在server.js中执行如下命令进行验证是否连接数据库成功:
// 引入 express 服务框架
const express = require("express")
// 引入 mongoose 数据库服务
const mongoose = require("mongoose")
const app = express()
// 连接数据库的 URL
const MongoUrl = 'mongodb://localhost:27017/node_fund'
// 连接数据库
mongoose.connect(MongoUrl).then(() => {
console.log('连接成功')
}).catch((err) => {
console.log('连接失败', err)
})
// 设置路由
app.get('/', (req,res) =>{
res.send('hello world')
})
// 设置端口号
const post = process.env.PORT || 5000
app.listen(post, () => {
console.log(`Server is running on port ${post},url: http://127.0.0.1:${post}`)
})
当我们执行 pm2 log 查看日志的时候可以看到,我们的终端打印了连接成功的字眼:
当我们更改 MongoUrl 的默认url的时候,终端就会打印出相应的连接失败以及相应报错:
搭建注册接口
接下来我们需要开始搭建真正的接口了,首先我们需要在项目根目录下创建相关文件夹编写接口:
在编写接口之前,我们需要先安装 body-parser ,为了方便地在node中处理 POST 请求的请求体数据,从而更轻松地进行数据处理和响应,终端执行如下命令进行安装:
npm install body-parser
安装完成之后,我们还需要在入口文件 server.js 中使用它来解析请求体:
const express = require("express")
const bodyParser = require("body-parser")
const app = express()
// 使用 body-parser 中间件
app.use(bodyParser.urlencoded({ extended: false })) // 解析表单数据
app.use(bodyParser.json()) // 解析 JSON 格式的请求体
插件安装完成之后,我们还需要在项目根目录下创建一个models文件,用于存放从数据库获取数据所需要的数据及其相应的类型,这里需要借助 Schema 属性:
const mongoose = require('mongoose')
// 定义模式,用于指定数据的结构和字段。
const Schema = mongoose.Schema
// 使用Schema变量来定义具体的数据模型
const userSchema = new Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
avatar: {
type: String,
},
identity: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
},
})
/**
* 创建了一个名为users的MongoDB集合,并使用userSchema指定了集合中文档的结构
* 将前一步创建的模型赋值给一个变量User,使其成为我们操作users集合的接口。
*/
module.exports = User = mongoose.model('users', userSchema)
定义完相应的注册登录接口所需类型之后,接下来就需要正式在user.js文件中编写相应的注册接口,代码如下:
// 用户登录 / 注册相关的内容
const express = require('express')
const router = express.Router()
// 引入具体的数据类型
const User = require('../../models/User')
/**
* 注册接口
* POST api/users/register
*/
router.post('/register', (req,res) => {
// 查询数据库中是否拥有邮箱
User.findOne({ email: req.body.email }).then((user) => {
if (user) {
return res.status(400).json({ email: '邮箱已被注册' })
} else {
// 注册新邮箱
const newUser = new User({
name: req.body.name,
email: req.body.email,
avatar,
password: req.body.password,
identity: req.body.identity
})
}
})
})
module.exports = router
因为密码是至关重要的数据,所以这里我们需要对用户的密码进行一个加密,终端执行如下命令:
npm install bcrypt
下载成功之后,导入 bcrypt 然后对数据进行相应的hash加密,修改后的代码如下:
// 用户登录 / 注册相关的内容
const express = require('express')
const router = express.Router()
const bcrypt = require("bcrypt")
// 引入具体的数据类型
const User = require('../../models/User')
/**
* 注册接口
* POST api/users/register
*/
router.post('/register', (req,res) => {
// 查询数据库中是否拥有邮箱
User.findOne({ email: req.body.email }).then((user) => {
if (user) {
return res.status(400).json({ email: '邮箱已被注册' })
} else {
// 注册新邮箱
const newUser = new User({
name: req.body.name,
email: req.body.email,
avatar,
password: req.body.password,
identity: req.body.identity
})
// 进行密码加密
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (err, hash) => {
if (err) throw err
newUser.password = hash
newUser.save().then(user => res.json(user)).catch(err => console.log(err))
})
})
}
})
})
module.exports = router
写完相应代码之后,这里我们还需要将该路由代码在入口文件 server.js 中进行一个引入:
const express = require("express")
const app = express()
// 引入user.js
const users = require('./routes/api/user')
// 使用routes
app.use('/api/users', users)
接下来我们需要借助接口测试工具 Apifox 进行相应的接口测试,如果不了解 Apifox 的朋友,可以参考我之前的文章:Apifox:详细使用教程,带你轻松拿捏 ,为了便于测试,我们先把 avatar 图像参数先删除,先测试一下其他参数。当我们输入相关路径及其参数点击发送之后:
当我们再次点击发送之后,就会出现当前的邮箱已被注册,符合逻辑规律:
接下来我们需要处理 avatar 头像参数,需要将图片数据保存到数据库中,可以使用 Buffer 对象来处理二进制数据,并将其存储为 Buffer 类型的字段。这里我们需要安装如下的这个插件进行处理
npm install multer
然后代码进行如下方式的修改:
// 用户登录 / 注册相关的内容
const express = require('express')
const router = express.Router()
const bcrypt = require("bcrypt")
const multer = require('multer');
const jwt = require("jsonwebtoken")
const passport = require("passport")
// 引入具体的数据类型
const User = require('../../models/User')
// 配置 multer
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'public/images') // 设置图片保存的路径
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
cb(null, file.fieldname + '-' + uniqueSuffix + '.' + file.mimetype.split('/')[1]) // 设置图片的文件名
}
})
const upload = multer({ storage: storage })
/**
* 注册接口
* POST api/users/register
*/
router.post('/register', upload.single('avatar'), (req,res) => {
// 查询数据库中是否拥有邮箱
User.findOne({ email: req.body.email }).then((user) => {
if (user) {
return res.status(400).json('邮箱已被注册')
} else {
const avatarUrl = req.protocol + '://' + req.get('host') + '/images/' + req.file.filename;
// 注册新邮箱
const newUser = new User({
name: req.body.name,
email: req.body.email,
avatar: avatarUrl, // 使用上传的图片的文件名作为 avatar 字段的值
password: req.body.password,
identity: req.body.identity
})
// 进行密码加密
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (err, hash) => {
if (err) throw err
newUser.password = hash
newUser.save().then(user => res.json(user)).catch(err => console.log(err))
})
})
}
})
})
接下来进行Apifox接口测试之后,得到的结果如下:
如果想访问图片资源的话,这里我们还需要在入口文件出将静态资源设置为可访问:
app.use(express.static('public'));
当我们打开 Navicat 可视化管理工具之后,找到我们创建的对应数据,打开集合就能看到我们在测试工具Apifox生成的数据,在数据库当中也呈现出来了:
搭建登录接口
和注册接口一样,原理就是我们拿到用户请求过来的email和password之后,进行数据库的一个查询,如果查询当前数据库没有用户传递过来的email数据,就返回用户不存在,否则的话就开始比较密码进行相应的匹配得到对应的结果:
/**
* 登录接口
* POST api/users/login
*/
router.post('/login', (req,res) => {
const email = req.body.email
const password = req.body.password
// 查询数据库
User.findOne({ email }).then(user => {
if (!user) {
return res.status(404).json({ email: '用户不存在!' })
}
// 密码匹配
bcrypt.compare(password, user.password).then(isMatch => {
if (isMatch) {
res.json({ msg: 'success' })
} else {
return res.status(400).json({ password: '密码错误!' })
}
})
})
})
比如说我们拿一下数据库当中真实存在的数据进行测试一下,得到的结果肯定是 success :
如果说我们随便输入密码或者用户名可能不是success了,举个例子改一下密码:
接下来我们需要给登录成功的 success 返回相应的token,终端执行如下命令按照相应包:
npm install jsonwebtoken
在导入 const jwt = require("jsonwebtoken") 之后,在密码匹配的地方执行jwt设置一个标记,过期时间我们设置了一个小时,然后token的话和前面的字符串进行了一个拼接:
在Apifox接口测试工具进行测试得到的相应结果如下:
获取登录信息
登录的接口写完,我们还需要写一个获取登录信息的接口函数,首先判断我们是否登录成功即需要验证一下我们当前登录的token是否存在且正确,所以这里我们需要借助 passport-jwt等工具进行:
npm install passport-jwt passport
接下来我们需要在入口文件 server.js 中初始化passport,然后将逻辑代码单独抽离出来:
// passport初始化
app.use(passport.initialize())
// 引入passport逻辑功能代码
require("./config/passport")(passport)
在抽离出来的config文件夹下的passport文件,这里我们开始书写真正的token校验:
const JwtStrategy = require("passport-jwt").Strategy
ExtractJwt = require("passport-jwt").ExtractJwt
const mongoose = require("mongoose")
const User = mongoose.model("users")
const opts = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()
opts.secretOrKey = 'secret'
module.exports = passport => {
passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
User.findById(jwt_payload.id).then(user => {
if (user) {
return done(null, user)
}
return done(null, false)
}).catch((err) => {
console.log(err)
})
}))
}
在user.js路由文件夹下,我们开始书写获取登录信息的接口了,这里我们仍然将图片资源进行应该路径的拼接:
/**
* 获取登录信息
* POST api/users/current
*/
router.get('/current', passport.authenticate("jwt", { session: false }), (req,res) => {
res.json({
id: req.user.id,
name: req.user.name,
email: req.user.email,
avatar: req.user.avatar,
identity: req.user.identity
})
})
通过Apifox得到的结果如下:
添加和获取信息
注册和登录的功能写完之后,接下来需要我们开始编写添加和获取信息的接口,这里我们要在api文件夹下再新建一个 profile 文件,以及在模型models文件夹下新建 Profile文件:
创建的两个文件都要在入口文件 server.js 中进行引入:
// 引入profile.js
const profile = require('./routes/api/profile')
// 使用routes
app.use('/api/profile', profile)
接下来我们先编写数据模型文件的代码,具体代码如下:
const mongoose = require('mongoose')
// 定义模式,用于指定数据的结构和字段。
const Schema = mongoose.Schema
// 使用Schema变量来定义具体的数据模型
const ProfileSchema = new Schema({
type: {
type: String,
},
describe: {
type: String,
},
income: {
type: String,
required: true
},
expend: {
type: String,
required: true
},
cash: {
type: String,
required: true
},
remark: {
type: String,
},
date: {
type: Date,
default: Date.now
},
})
/**
* 创建了一个名为users的MongoDB集合,并使用userSchema指定了集合中文档的结构
* 将前一步创建的模型赋值给一个变量User,使其成为我们操作users集合的接口。
*/
module.exports = Profile = mongoose.model('profile', ProfileSchema)
根据上面讲解的编写接口的经验,我们很容易就写出添加和获取信息的即可,如下:
// 用户登录 / 注册相关的内容
const express = require('express')
const router = express.Router()
const passport = require("passport")
// 引入具体的数据类型
const Profile = require('../../models/Profile')
/**
* 创建信息接口
* POST api/profiles/add
*/
router.post("/add", (req, res, next) => {
passport.authenticate("jwt", { session: false }, (err, user, info) => {
// 判断错误情况
if (err) return res.status(500).json({ error: "Internal Server Error" });
if (!user) return res.status(401).json({ error: "Unauthorized" });
const profileFields = {};
if (req.body.type) profileFields.type = req.body.type;
if (req.body.describe) profileFields.describe = req.body.describe;
if (req.body.income) profileFields.income = req.body.income;
if (req.body.expend) profileFields.expend = req.body.expend;
if (req.body.cash) profileFields.cash = req.body.cash;
if (req.body.remark) profileFields.remark = req.body.remark;
new Profile(profileFields).save().then((profile) => {
res.json(profile);
}).catch(err => {
res.status(500).json(err);
})
})(req, res, next);
})
/**
* 获取所有信息
* POST api/profiles
*/
router.get( '/', passport.authenticate('jwt', { session: false }), (req, res) => {
Profile.find().then((profiles) => {
if (!profiles || profiles.length === 0) return res.status(404).json({ error: '数据为空!' })
const profileData = { profiles: profiles }
res.json(profileData)
}).catch((err) => res.status(404).json(err));
}
);
module.exports = router
获取单个信息的接口也很简单,只需要在路径拼接上id,id后面前端传递给我们即可:
/**
* 获取单个信息
* POST api/profiles/:id
*/
router.get('/:id', passport.authenticate('jwt', { session: false }), (req, res) => {
Profile.findOne({ _id: req.params.id }).then((profile) => {
if (!profile) return res.status(404).json({ error: '数据为空!' });
res.json(profile);
}).catch((err) => res.status(404).json(err));
});
通过Apifox测试的结果如下:
编辑和删除信息
编辑信息接口和添加信息接口写法大体一致,如下:
/**
* 编辑信息接口
* POST api/profiles/edit
*/
router.post(
"/edit/:id",
passport.authenticate("jwt", { session: false }),
(req, res) => {
const profileFields = {};
if (req.body.type) profileFields.type = req.body.type;
if (req.body.describe) profileFields.describe = req.body.describe;
if (req.body.income) profileFields.income = req.body.income;
if (req.body.expend) profileFields.expend = req.body.expend;
if (req.body.cash) profileFields.cash = req.body.cash;
if (req.body.remark) profileFields.remark = req.body.remark;
Profile.findOneAndUpdate(
{ _id: req.params.id },
{ $set: profileFields },
{ new: true }
).then(profile => res.json(profile))
}
)
删除信息的接口也很简单:
/**
* 删除信息
* POST api/profiles/:id
*/
router.delete(
'/delete/:id',
passport.authenticate('jwt', { session: false }),
(req, res) => {
Profile.findOneAndDelete({ _id: req.params.id }).then(profile => {
res.json(profile);
}).catch(err => res.status(404).json('删除失败'));
});
表示删除成功!
ok!后端的一些基础代码已经写完了,下一篇将借助这些接口开始前端页面的书写了,等前后端项目全部写完,我再将源码开源出来,敬请期待!!!