前言

昨天已经大致介绍了后端的大体框架,今天来说一下如何将这个项目改的较为工程化一些,方便我们去开发。昨天的内容相对简单,只有入口文件和数据库两个最为重要的模块。今天将结合前端一起为大家梳理一下。前端项目就不说了,大家可以去若依官网去下载前后端分离的若依前端框架。

一、项目的结构在这里插入图片描述

以上就是项目的一级目录,展开内容将会在开发过程中详细讲解。

二、路由管理(routes文件夹)

后端开发的主要内容就是给前端提供接口,前端通过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)

这样我们就能统一管理各个模块的路由了。

三、数据库建表(models文件夹)

与路由文件夹一样,数据库的表也根据模块划分,但这里没有入口文件。在这里插入图片描述
这里可以使用一个模板表,即创建任意多个字段,因为表的结构基本一致,等需要真实表的时候直接复制一份,修改字段名字、导出名字即可,节省时间。

四、中间件(middleWares)

根据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文件夹)

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

Logo

快速构建 Web 应用程序

更多推荐