Node.js<十七>——项目实战-项目架构和注册

694 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

项目架构

  1. .env

我们的环境变量一般都会放置到.env文件中,比如说端口号,数据库的一些信息等等;而且这个东西在git管理中应该要把它忽略掉的,因为这里面一般存储的都有隐私信息,不应该放置到git仓库上面

APP_PORT = 3000

MYSQL_HOST = localhost
MYSQL_DATABASE = coderhub
MYSQL_USER = root
MYSQL_PASSWORD = 13413090799dsz
MYSQL_CONNECTIONlIMIT = 10
  1. main.js文件为项目主入口
  2. app文件夹里面表示的是一些全局相关的东西,比如说app的配置信息和错误处理
  3. controller文件夹称之为‘控制器’,其主要作用就是将路由对应的中间件函数的逻辑剥离出来
  4. service文件夹放的是一些与数据库操作相关的文件
  5. router文件夹表示的是一些与路由相关的文件
  6. utils文件夹里面放置的都是一些工具函数文件
  7. constants文件夹里面放置的项目中的一些常量,比如说请求错误要返回的信息等等
  8. middleware文件夹放置的就是一些要插入到路由中间件前面的中间件,以用户注册功能为例,在真正执行将用户信息插入到数据库的中间件之前必须要先执行一个校验用户传递的参数有没有问题的中间件,这一个中间件我们一般是把它放置到middleware文件夹中

注册功能

  1. main.js

因为对app的操作会随着我们项目的进展越来越多,比如使用app.use添加中间件、使用路由等等,所以我们应该把大部分有关app的操作剥离到其它文件中去,比如app/index.js这个文件

这里想表达的意思就是main.js作为我们整个程序的入口应该是越简单越好的,复杂的操作逻辑应该抽到其它的文件里面才对

// 引入app
const app = require('./app/index')
// 执行全局配置信息的文件
const { APP_PORT } = require('./app/config')

app.listen(APP_PORT, () => {
  console.log('服务器启动成功!');
})
  1. .env

有一些特殊的信息比如说端口号、数据库的信息(端口号、账号、密码等等),后续可能会更改且涉及到隐私的东西也需要单独抽出来,.env就是用来存放这些信息的文件

// 改文件存储的都是一些后面可能会更换且隐私的配置信息
APP_PORT = 3000

MYSQL_HOST = localhost
MYSQL_DATABASE = coderhub
MYSQL_USER = root
MYSQL_PASSWORD = 13413090799dsz
MYSQL_CONNECTIONlIMIT = 10
  1. app/config.js

这个文件存储的是一些全局配置信息,所以是放置在app文件夹中的,刚刚也说道了配置信息存储到了.env文件中,那么我们要如何读取到在.env中存储的信息呢?

这时候可以使用dotenv这个库,调用dotenv.config函数之后就可以将.env中的变量以属性的形式放到process.env中了

const dotenv = require('dotenv')

// 将.env中的变量以属性的形式放到process.env中
dotenv.config() 

module.exports = {
  APP_PORT,
  MYSQL_HOST,
  MYSQL_DATABASE,
  MYSQL_USER,
  MYSQL_PASSWORD,
  MYSQL_CONNECTIONlIMIT
} = process.env
  1. router/user.router.js

全部功能模块的逻辑都写到app/index.js中的话,代码逻辑会非常复杂且可维护性非常低,但我们可以借助koa-router这个库来创建路由,将不同功能模块以路由的形式分离开来

不过一个功能模块中的逻辑依然复杂,而且其主要逻辑是在中间件里面,所以为了让每个路由文件中的代码看起来简洁一些,我们需要把逻辑复杂的中间件函数也抽离出去,执行验证操作的中间件可以放到middware文件夹中,执行真正操作的中间件可以放到controller文件夹中

const Router = require('koa-router')
// 执行注册操作判断客户端传入信息有没有问题且用户名有无注册过的中间件
const { verityUser } = require('../middleware/user.middleware')
// 用于在数据库中增加用户的中间件
const {
  create
} = require('../controller/user.controller')

// 注册路由并指定前缀
const userRouter = new Router({ prefix: '/users' })

// 注册中间件,先验证再执行注册操作
userRouter.post('/', verityUser, create)

module.exports = userRouter
  1. controller/user.controller.js

这个文件就是用来定义路由中对应中间件函数的,我们这里使用一个类来进行管理,将这些函数都添加到这个类的原型链对象prototype中去,这样一来,我们只需要导出由这个类构造出来的实例,外面的文件就可以调用类上所有的函数了

// 用来操作数据库的对象
const service = require('../service/user.service')

class UserController {
  async create(ctx, next) {
    // 1. 获取客户端传递过来的数据
    const userInfo = ctx.request.body
    // 2. 去数据库中创建一条新的数据
    const results = await service.create(userInfo)
    // 3. 返回响应结果 
    ctx.body = results
  }
}

// 将UserController这个实例返回出去,这样外部文件就可以通过该实例取出对应的函数了
module.exports = new UserController()
  1. app/database.js

在介绍service/user.service.js文件之前,我们还需要先将数据库连接上才行。我们采用的是mysql2这个库去和数据库进行连接,还利用建立连接池的方法提高客户端请求的性能,让连接在一定数量之内不会被销毁,利于下次使用。

const mysql2 = require('mysql2')

const {
  MYSQL_HOST,
  MYSQL_DATABASE,
  MYSQL_USER,
  MYSQL_PASSWORD,
  MYSQL_CONNECTIONlIMIT
} = require('./config')

// 创建连接池
const connections = mysql2.createPool({
  host: MYSQL_HOST,
  database: MYSQL_DATABASE,
  user: MYSQL_USER,
  password: MYSQL_PASSWORD,
  connectionLimit: MYSQL_CONNECTIONlIMIT
})

// 检验数据库连接是成功
connections.getConnection((err, conn) => {
  conn.connect(err => {
    if (err) {
      console.log('连接失败:', err);
    } else {
      console.log('数据库连接成功');
    }
  })
})

// 因为后续我们想要通过Promise进行操作
module.exports = connections.promise()
  1. service/user.service.js

这个文件是负责进行对数据库进行操作的,比如注册功能需要在用户表中添加一条新数据,添加数据之前还要检查一下数据表中用户名有没有重复等等

一般来说,书写sql语句都会使用预编译语句,然后通过之前建立好的连接去数据库中查找数据。依然是使用类的方式来管理对应的函数,最终将由该类创建出来的实例返回出去,外面的文件就可以使用这个类原型上的所有函数了

const connection = require('../app/database')

class UserService {
  // 根据用户传递过来的用户名和密码在数据库中添加用户
  async create({ username, password }) {
    try {
      const statement = `INSERT INTO users (name, password) VALUES (?, ?);`
      const results = await connection.execute(statement, [username, password])
      return results[0]
    } catch (err) {
      console.log(err);
    }
  }

  // 根据用户名在数据库中查找数据,将查找到的结果返回出去
  async getUserName(username) {
    try {
      const statement = `SELECT * FROM users WHERE name = ?;`
      const results = await connection.execute(statement, [username])
      return results[0]
    } catch (err) {
      console.log(err);
    }
  }
}

module.exports = new UserService()
  1. middleware/user.middleware.js

这里面存放的一般包括检验等操作的中间件,比如这里处理的就是判断用户所传递的信息准不准确,客户端传递过来的用户名有没有被注册过等操作

const {
  NAME_OR_PWD_IS_REQUIRED,
  USER_ALREADY_EXISTS
} = require('../constants/err-types')
const { getUserName } = require('../service/user.service')

// 注册前对客户端传递信息的校验操作
const verityUser = async (ctx, next) => {
  // 1. 获取用户名和密码
  const { username, password } = ctx.request.body
  // 2. 判断用户名或者密码不为空 
  if (!username || !password) {
    // 制造对应的Error对象
    const error = new Error(NAME_OR_PWD_IS_REQUIRED)
    // 使用上下文ctx上面的app对象触发错误事件
    return ctx.app.emit('error', error, ctx)
  }
  // 3. 判断这次注册的用户名是否有被注册过
  const res = await getUserName(username)
  if (res.length) {
    const error = new Error(USER_ALREADY_EXISTS)
    return ctx.app.emit('error', error, ctx)
  }
  await next()
}

module.exports = {
  verityUser
}
  1. constants/err-type.js

由于整个业务会变得比较庞大,我们项目中会有一些常量用于判断等操作,所以我们可以统一把他们放到一个文件中管理,比如说请求错误要返回的信息等等

const NAME_OR_PWD_IS_REQUIRED = 'NAME_OR_PWD_IS_REQUIRED'
const USER_ALREADY_EXISTS = 'USER_ALREADY_EXISTS'

module.exports = {
  NAME_OR_PWD_IS_REQUIRED,
  USER_ALREADY_EXISTS
}
  1. app/err-handler.js

因为在很多场景都有可能出错并需要将错误信息要返回给客户端,所以我们为了代码的整洁和可维护性,把有关错误的操作集中到一个函数中管理,出错时通过发射错误事件将错误信息传递出去,然后再通过app监听错误事件根据传递过来的错误信息来判断状态码,最终决定响应什么错误信息给客户端

const {
  NAME_OR_PWD_IS_REQUIRED,
  USER_ALREADY_EXISTS
} = require('../constants/err-types')

const errorHandler = (error, ctx) => {
  let status, message
  switch (error.message) {
    case NAME_OR_PWD_IS_REQUIRED:
      status = 400 // Bad Request
      message = '用户名或密码不能为空'
      break
    case USER_ALREADY_EXISTS:
      status = 409 // Conflict
      message = '用户名已经存在'
      break
    default:
      status = 404
      message = 'not found'
  }

  ctx.status = status
  ctx.body = message
}

module.exports = errorHandler
  1. app/index.js

这是包含了主要app操作的文件,比如设置全局中间件、使用koa-bodyparser解析客户端传递过来的参数、将我们的路由注册为中间件、执行连接数据库的文件、监听错误事件等等

const Koa = require('koa')
const bodyParser = require('koa-bodyparser')

const userRouter = require('../router/user.router')
// 执行连接数据库的文件
require('./database')
const errorHandler = require('./err-handler')

const app = new Koa()

// 解析客户端传递过来的参数
app.use(bodyParser())

// 为路由注册中间件
app.use(userRouter.routes())
// 如果某种请求方式服务器没有或者不支持,将会自动返回帮助我们返回请求错误信息
app.use(userRouter.allowedMethods())
// 监听错误事件
app.on('error', errorHandler)

module.exports = app

到此为止,我们的注册功能就已经初步实现了