昨天已经大致介绍了后端的大体框架,今天来说一下如何将这个项目改的较为工程化一些,方便我们去开发。昨天的内容相对简单,只有入口文件和数据库两个最为重要的模块。今天将结合前端一起为大家梳理一下。前端项目就不说了,大家可以去若依官网去下载前后端分离的若依前端框架。
以上就是项目的一级目录,展开内容将会在开发过程中详细讲解。
后端开发的主要内容就是给前端提供接口,前端通过axios发送网络请求,后端接到请求后操作数据库,返回前端需要的数据。同时,前端在请求前后应当进行拦截操作,发送请求前校验认证信息,并且对响应数据进行拦截处理,捕获错误信息等。后端也要对请求进行错误处理,前端再进行错误操作时应及时将错误信息返回到前端,提高前后端交互的效率。下面将着重介绍一下项目中后端的路由处理。
开发时所有的接口文件都放在routes文件夹中,它的目录结构如下图:
我们可以对项目中的模块进行区分,比如在系统模块中(system)存放了整个系统中的所有模块,包括菜单管理、用户管理、角色管理等等。
因为express中间件技术的存在,我们可以使用一个通用的增删改查接口,这样的话所有的接口只需要走通用接口即可,就不需要这么多接口文件。但考虑到项目后期的需求变更以及每个接口的需求可能不一致,和每个接口都需要进行权限校验,这里就没有使用通用接口。
以上期登录接口为例,我们使用了commonjs语法将整个接口文件暴露了出去,并且在server.js里导入并使用,这里的入口文件index.js就代替了这个工作,避免过多的接口导入出现在server.js里面。
module.exports = app => {
// 登录接口,这里简写
const assert = require('http-assert')
const jwt = require('jsonwebtoken')
const User = require('../models/User')
// 举例
app.post('/login', async (req, res) => {
// -----代码块
})
// 举例
app.get('/user', async (req, res) => {
// -----代码块
})
}
在index.js中是这样的:
module.exports = app => {
// 其他模块
require('./other/other')(app)
// system
require('./system/login')(app)
// 错误处理
app.use(async (err, req, res, next) => {
res.status(err.statusCode || 500).send({
message: err.message,
code: err.statusCode || 500
})
})
}
在这里我们不仅完成了统一的错误处理操作,并且注册了所有模块的路由,最后只需要在server.js里引入这个index.js文件即可。以后新的路由模块必须要在这里注册。
// server.js
require('./routes')(app)
这样我们就能统一管理各个模块的路由了。
与路由文件夹一样,数据库的表也根据模块划分,但这里没有入口文件。
这里可以使用一个模板表,即创建任意多个字段,因为表的结构基本一致,等需要真实表的时候直接复制一份,修改字段名字、导出名字即可,节省时间。
根据express工作的机制,每一个接口如果加入了中间件,那么一定会在中间件执行之后,才开始执行接口内容。这里以前端的请求拦截为例。
前端在发送请求时会有一个请求拦截,代码如下:
// request拦截器
request.interceptors.request.use(config => {
if (getToken()) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config
}, error => {
Promise.reject(error)
})
在每个请求发送之前,会在请求头里添加一个token认证信息,后端则需要校验这个请求有没有这个token(一般来说都是传token,在前端登录时后端会返回这个token),并且要校验这个token是不是上次登录时发出的token,避免出现token被私自更改的情况。
基本上除了登录之外的绝大多数接口都需要进行校验,那岂不是每次请求都要校验?每次校验都要写一遍校验的方法?这可太累了,我不干了,删库跑路…
这是就要靠我们的中间件发挥作用了,在每个需要校验的接口前加上这个中间件就行了!这里我们将这个中间件封装成一个公用方法,并暴露出去:
module.exports = permission => {
const jwt = require('jsonwebtoken');
const assert = require('http-assert');
const User = require('../models/system/User')
const Role = require('../models/system/Role')
const Menu = require('../models/system/Menu')
return async (req, res, next) => {
// 获取前端传过来的请求头里的认证信息,处理后转为token,并验证token
const token = String(req.headers.authorization || '').split(' ').pop();
assert(token, 401, '系统检测到您还没有登录,请先登录!');
// 服务端通过给出的secret验证生成的token,将之前传入的id解密出来,进行验证
const { id } = jwt.verify(token, req.app.get('SECRET'));
assert(id, 401, '系统检测到您还没有登录,请先登录!');
// 根据id查找数据库,验证用户是否存在
req.user = await User.findById(id);
assert(req.user, 401, '系统检测到您还没有登录,请先登录!');
/**
* 判断该用户是否拥有操作权限
* '*:*:*'表示全部权限
* permission == '*:*:*'表示该请求不需要权限
* req.user.permissions.includes('*:*:*')用于验证该用户是否拥有全部权限
* req.user.permissions.includes(permission)用于验证当前用户是否具有permission权限
* */
if (req.user.userName === 'admin') {
req.user.permissions = ['*:*:*']
} else {
const permissions = await Role.find({_id: {$in: req.user.roles}}).distinct('permissions')
req.user.permissions = await Menu.find({_id: {$in: permissions}}).distinct('permission')
}
const hasPerm = permission === '*:*:*' || req.user.permissions.includes('*:*:*') || req.user.permissions.includes(permission)
assert(hasPerm, 401, '没有相关操作权限,请联系管理员。')
await next();
}
}
这里我们不仅校验了token信息,而且还根据token查找到前端发起请求的用户,并进行了权限校验。这里assert上期已经说了,如果没有通过校验,请求就会终止,并且会将状态码和错误信息经过前面的错误处理方案返回给前端,前端只需要在响应拦截其中进行拦截,并弹框展示即可。
const auth = require('../../middleWares/auth')
app.get('/profile', auth('system:user:profile'), async(req, res) => {
// --------代码块
})
比如用户在查询个人资料的时候,需要[‘system:user:profile’]这个权限,如果没有这个权限,上面的权限校验就无法通过,请求便会终止。如果没有token,那么请求都不会走到这一步。
assert(hasPerm, 401, '没有相关操作权限,请联系管理员。')
前端可以通过相应拦截器提示错误信息,这里我是根据错误信息判断,如果后端返回了错误信息,说明此次请求失败:
request.interceptors.response.use(res => {
return res.data
}, err => {
if (err.response.data.message) {
Vue.prototype.$notify.error({
title: '错误',
message: err.response.data.message
})
}
return Promise.reject(err)
})
比如用户在登录校验不通过、权限校验不通过时,前端都会提示错误信息:
对于后面没有权限的情况,前端应该对涉及权限操作的按钮进行隐藏,既然没有权限,那就看不到它,避免产生不良交互。一般按钮级别的权限前端往往使用自定义指令,登录时拿到所有权限,然后跟当前按钮权限进行比对,有权限才显示按钮。后端再加一层校验,保证不会出现越权操作。附上自定义指令(来自若依管理系统源码)
import store from '@/store'
export default {
inserted(el, binding, vnode) {
const { value } = binding
const all_permission = "*:*:*";
const permissions = store.getters && store.getters.permissions
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value
const hasPermissions = permissions.some(permission => {
return all_permission === permission || permissionFlag.includes(permission)
})
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`请设置操作权限标签值`)
}
}
}
<el-button size="mini" type="text" icon="el-icon-edit" v-has-permi="['menu:menu:update']">修改el-button>
uploads文件夹用于保存前端上传的文件资源,这里使用一个插件叫multer,可以通过npm安装后进行使用。一般来说,最好对前端上传的资源进行区分一下,避免各种格式的文件存放到一起。这里采取的方式还是根据模块划分:比如单独创建一个文件夹用于存放用户头像,单独创建一个文件夹存放各种业务单据的附件等等。以上传头像为例:
//设置保存规则
const storage = multer.diskStorage({
//destination:字段设置上传路径,可以为函数
destination: __dirname + '../../../uploads/avatar',
//filename:设置文件保存的文件名
filename: function(req, file, cb) {
cb(null, dayjs(new Date()).format('YYYYMMDDHHmmss') + '-' + file.originalname);
}
})
这里设置上传时会自动将头像文件存放在根目录uploads/avatar文件夹下,并且将文件名前面拼上上传日期后进行保存,名称可以自定义设置。
const avatar = multer({ storage })
app.post('/upload/avatar', auth('*:*:*'), avatar.single('file'), async (req, res) => {
const file = req.file;
file.url = `/uploads/avatar/${file.filename}`
res.send({
code: '200',
message: '上传成功',
file
});
})
前端上传头像时调用/upload/avatar接口,后端则拿到头像的完整路径返回前端进行回显,但还差一个操作,前端只拿到图片路径是不够的,这一步的逻辑是前端拿到图片路径后,回到服务器中去访问这张图片,所以,与public一样作为静态资源,将uploads文件夹交给服务器进行托管,这样前端才能访问到:
app.use('/uploads', express.static(__dirname + '/uploads'))
当放到服务器下托管时是能看到图片的,但没有托管时,就没有图片了:
至于utils、plugins、public就不在细说了,有什么不明白的可以留言讨论或者私信我,期待与你们一起交流学习!整个项目架构比较简单,不是特别复杂,肯定和更专业的项目比不了,但逻辑和思想大差不差,能够帮助我们更好地去理解前后端的交互过程就好,另外当自己写项目的时候也可以搭一个简单的服务器,就不会出现没有接口的问题了!
前端代码仓库:https://gitee.com/likeaskingwhy/base-vue-project.git
后端代码仓库:https://gitee.com/likeaskingwhy/base-server-project.git