1.0 前言
我们在实际的开发过程中,肯定会对项目目录结构,开发规范做一些约定的设计,这样项目看起来会比较舒服,别人参与进来的时候,也会比较清晰,而且项目越是复杂,项目目录规范确定下来,清晰,程序才会健壮。以下是笔者在结合实际开发和社区实践梳理出来。
2.0 目录结构
project└── app├── config # 环境配置相关│ ├── base.env.js│ ├── dev.env.js│ ├── prod.env.js│ └── qa.env.js│ └── index.js├── db # 数据库配置相关│ └── mysql.js├── log4j # 日志配置相关│ └── logger.js├── controllers # 操作层 执行服务端模板渲染,json接口返回数据,页面跳转│ ├── admin.js│ ├── index.js│ ├── user.js│ └── work.js├── services # 业务层 实现数据层model到操作层controller的耦合封装│ └── user.js├── models # 数据模型层 执行数据操作│ └── user.js├── routers # 路由层 控制路由│ └── index.js|———————page # 页面跳转│ └── index.js|———————api # ajax api 相关│ └── v1.js└── bin└── www # 项目启动相关└── views # 服务端模板代码├── index.ejs└── work.ejs└── static # 静态资源相关└── app.js└── package.json
3.0 MVC 结构设计
在后端代码目录结构中,我们常常采用的是 MVC 结构设计
- Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
 - View(视图) - 视图代表模型包含的数据的可视化。
 - Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。
 
把项目分为这样的 3 层,好处就是目录清晰,试想,随着项目代码越来越多,如果没有做好目录划分,代码可维护性是特别差的,接下来我们把我们之前的用户管理改造成符合 MVC 设计的结构。
那现在有一个问题出现了,假设我们有一个业务场景是注册场景,需要先检测手机号,微信号是否已经注册了,注册了的话,假设已经注册了,需要踢出登录用户,然后走注册逻辑,注册完成后,需要缓存用户信息,创建页面调用 api 的 token,如果把这些代码都写到 Controller 层,这个代码会非常多。
所以我们一般都是在 Controller 和 View 层之间,再创建一个 Service 层,专门负责处理这些业务逻辑,保持Controller 的整洁,Controller 层做参数的校验,组合多个 Service 层。而且由于我们现在都是进行的前后端分离开发,用 Ajax 交互,这样 View 层 实际用的非常少了,后端主要是暴露 API 给前端调用,所以模型稍作改动。 
如上,我们就变成了这种设计,Sevice 层 来通过 Model 操作数据库,组装 Controller 层需要的数据,然后Controller 返回给页面,总的来说就是我们不要直接在 Controller 操作数据库,把具体的业务逻辑放到 Service 层里面去。
接下来我们把之前的用户操作 改造下
步骤一:创建 Model 层
Model 层我们已经创建好了,就是 app/models 下面的 user.js。
const { Sequelize, DataTypes, Model } = require('sequelize')const { getConnection } = require('../db/mysql')class User extends Model {}User.init({id: {type: DataTypes.INTEGER({ length: 11, unsigned: true }),autoIncrement: true,primaryKey: true,allowNull: false,comment: 'id',},name: {type: DataTypes.STRING,allowNull: false,comment: '名称',},email: {type: DataTypes.STRING,allowNull: false,comment: '邮箱',},imgUrl: {type: DataTypes.STRING,allowNull: false,comment: '图片url',},}, {sequelize: getConnection(),modelName: 'User',tableName: 'user',underscored: true, // 转下划线comment: '用户表',freezeTableName: true,timestamps: true,})module.exports = User
步骤二:创建 Service 层
// app/services/user.jsconst User = require('../models/user')async function findAll() {return User.findAll()}async function findById(id) {return User.findByPk(id)}async function add(name, email, imgUrl) {return User.create({ name, email, imgUrl })}async function remove(id) {const data = await User.destroy(id)return data}module.exports = {findAll,findById,add,remove}
步骤二:创建 Controller 层
const {findAll,findById,add,remove} = require('../services/user')async function list(ctx) {const data = await findAll()ctx.body = {data: data,success: true}}async function detail(ctx) {const id = ctx.params.idif(!id) {ctx.body = {messge: "参数ID不能为空",success: true}return}const data = await findById(id)ctx.body = {data: data[0],success: true}}async function add(ctx) {const { path } = ctx.request.files.fileconst { name, email } = ctx.request.body // 获取 request body 字段const imgUrl = path.split("/static")[1]if(!name || !email || !imgUrl) {ctx.body = {messge: "参数错误",success: true}return}const data = await add({ name, email, imgUrl })ctx.body = {data: data,success: true,}}async function remove(params) {const id = ctx.params.idconst data = await remove(id)ctx.body = {data: data[0],success: true}}module.exports = {detail,list,add,remove}
在 Controller 层我们做参数校验 ,组合 Service 层。这样后,在前端页面我们只需要用 Ajax 调用对应的 api 就可以完成整个流程了。
假设没有用前后端分离,使用的是模版引擎,那就对应的 view 层就放在 app/views 目录下。
4.0 静态资源目录
一般我们在 static 目录下放置静态资源相关的,对应 js,图片都单独放到对应目录下面去。
这样归类就清晰了,以后就知道去哪找对应的资源。
5.0 环境配置相关
开发,测试,生产,不同环境有不同的配置文件,这里我们就需要不同的配置文件,这些信息我们放到 config 目录下面去。
project└── app├── config # 环境配置相关│ ├── base.env.js│ ├── dev.env.js│ ├── prod.env.js│ └── qa.env.js│ └── index.js
6.0 bin 目录
bin 是放置启动项目相关。
7.0 小结
这节我们学习了 基于 MVC 的结构改造后的,后端项目接口设计,以及各个目录一般放置什么配置文件的使用,使用约定好的配置,可以大大降低 代码耦合,而且结构清晰也是构建大型应用的基石,大家可以对着 Demo 本地跑起来看看,Demo 地址
