使用Koa2从零开始实现一个具有基本功能的后端服务器的过程与思路分析

前言

使用Koa2实现了一个node.js后端服务器快速启动模板(即具有后端服务器的基本功能),使用了路由、数据库链接、请求体处理、异常处理、静态资源请求处理、session、登陆拦截器等中间件,基本实现了一个node.js后端服务器的基本功能。并设计实现了用户模块的登陆、注册、查找用户名接口。javascript

以前发了篇专栏 基于Vuex实现小米商城购物车,有同窗好奇问我接口数据怎么来?昨晚我忽然想到,能够从那个后端服务器把关键部分抽离出来实现一个后端服务器快速启动模板,须要使用的时候只须要分模块的添加一些接口并实现,就能够快速的构建起来一个后端服务器。前端

快速建立一个服务器

安装koa

npm install koa -S
复制代码

基本配置

const Koa = require('koa');

let { Port } = require('./config');

let app = new Koa();

// response
app.use(ctx => {
  ctx.body = 'Hello Koa';
});

// 监听服务器启动端口
app.listen(Port, () => {
  console.log(`服务器启动在${ Port }端口`);
});
复制代码

测试

就这样一个node.js服务器就启动起来了,java

使用postman测试一下node

路由中间件

思路:mysql

  • 使用koa-router中间件处理路由;
  • 若是把全部的路由写在一块儿,将会很是拥挤,不利于后期维护,因此为每一个业务模块配置模块子路由;
  • 而后把全部的模块子路由汇总到./src/roters/index.js
  • 再在入口文件require('./routers')

路由中间件目录

└── src # 源代码目录
    └── routers # 路由目录
        └── router # 子路由目录
            ├── usersRouter.js # 用户模块子路由
            ├── ... # 更多的模块子路由
        ├── index.js # 路由入口文件
复制代码

安装koa-router

npm install koa-router -S
复制代码

模块子路由设计

const Router = require('koa-router');
// 导入控制层
const usersController = require('../../controllers/usersController');

let usersRouter = new Router();

usersRouter
  .post('/users/login', usersController.Login)

module.exports = usersRouter;
复制代码

模块子路由汇总

const Router = require('koa-router');

let Routers = new Router();

const usersRouter = require('./router/usersRouter');

Routers.use(usersRouter.routes());

module.exports = Routers;
复制代码

使用路由中间件

// 使用路由中间件
const Routers = require('./routers');
app.use(Routers.routes()).use(Routers.allowedMethods());
复制代码

接口测试

使用postman测试接口localhost:5000/users/logingit

数据库链接封装

思路:github

  • 后端与数据库的交互是很是频繁的,若是是一个接一个地建立和管理链接,将会很是麻烦;
  • 因此使用链接池的方式,封装一个链接池模块;
  • 对链接进行集中的管理(取出链接,释放链接);
  • 执行查询使用的是connection.query(),对connection.query()进行二次封装,统一处理异常;
  • 向外导出一个db.query()对象,使用的时候,只须要传入sql语句、查询参数便可,例如:
db.query('select * from users where userName = ? and password = ?', ['userName', 'password'])
复制代码

安装mysql依赖包

npm install mysql -S
复制代码

配置链接选项

在config.js添加以下代码,而后在db.js引入sql

// 数据库链接设置
dbConfig: {
  connectionLimit: 10,
  host: 'localhost',
  user: 'root',
  password: '',
  database: 'storeDB'
}
复制代码

链接池封装

建立"./src/models/db.js"数据库

var mysql = require('mysql');
const { dbConfig } = require('../config.js');
var pool = mysql.createPool(dbConfig);

var db = {};

db.query = function (sql, params) {

  return new Promise((resolve, reject) => {
    // 取出链接
    pool.getConnection(function (err, connection) {

      if (err) {
        reject(err);
        return;
      }
    
      connection.query(sql, params, function (error, results, fields) {
        console.log(`${ sql }=>${ params }`);
        // 释放链接
        connection.release();
        if (error) {
          reject(error);
          return;
        }
        resolve(results);
      });
    
    });
  });
}
// 导出对象
module.exports = db;
复制代码

更多的信息请参考mysql文档npm

请求体数据处理

思路:

  • 使用koa-body中间件,能够很方便的处理请求体的数据,例如
let { userName, password } = ctx.request.body;
复制代码

安装koa-body中间件

npm install koa-body -S
复制代码

使用koa-body中间件

在config.js配置上传文件路径

uploadDir: path.join(__dirname, path.resolve('../public/')), // 上传文件路径
复制代码

在app.js使用koa-body中间件

const KoaBody = require('koa-body');
let { uploadDir } = require('./config');
复制代码
// 处理请求体数据
app.use(KoaBody({
  multipart: true,
  // parsedMethods默认是['POST', 'PUT', 'PATCH']
  parsedMethods: ['POST', 'PUT', 'PATCH', 'GET', 'HEAD', 'DELETE'],
  formidable: {
    uploadDir: uploadDir, // 设置文件上传目录
    keepExtensions: true, // 保持文件的后缀
    maxFieldsSize: 2 * 1024 * 1024, // 文件上传大小限制
    onFileBegin: (name, file) => { // 文件上传前的设置
      // console.log(`name: ${name}`);
      // console.log(file);
    }
  }
}));
复制代码

异常处理

思路:

  • 程序在执行的过程当中不免会出现异常;
  • 若是由于一个异常服务器就挂掉,那会大大增长服务器的维护成本,并且体验极差;
  • 因此在中间件的执行前进行一次异常处理。

在app.js添加以下代码

// 异常处理中间件
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    console.log(error);
    ctx.body = {
      code: '500',
      msg: '服务器未知错误'
    }
  }
});
复制代码

静态资源服务器

思路:

  • 前端须要大量的静态资源,后端不可能为每条静态资源的请求都写一份代码;
  • koa-static能够很是方便的实现一个静态资源服务器;
  • 只须要建立一个文件夹统一放静态资源,例如./public
  • 那么就能够经过http://localhost:5000/public/文件夹/文件名直接访问。

安装koa-static中间件

npm install koa-static -S
复制代码

使用koa-static中间件

在config.js配置静态资源路径

staticDir: path.resolve('../public'), // 静态资源路径
复制代码

在app.js使用koa-static中间件

const KoaStatic = require('koa-static');
let { staticDir } = require('./config');
复制代码
// 为静态资源请求重写url
app.use(async (ctx, next) => {
  if (ctx.url.startsWith('/public')) {
    ctx.url = ctx.url.replace('/public', '');
  }
  await next();
});
// 使用koa-static处理静态资源
app.use(KoaStatic(staticDir));
复制代码

接口测试

使用浏览器测试接口http://localhost:5000/public/imgs/a.png

session实现

思路:

  • 使用koa-session中间件实现session的操做;
  • 用于登陆状态的管理;
  • 本例子使用内存存储的方案,适用于session数据量小的场景;
  • 若是session数据量大,建议使用外部存储介质存放session数据 。

安装koa-session中间件

npm install koa-session -S
复制代码

使用koa-session中间件

建立"./src/middleware/session.js"

let store = {
  storage: {},
  set (key, session) {
    this.storage[key] = session;
  },
  get (key) {
    return this.storage[key];
  },
  destroy (key) {
    delete this.storage[key];
  }
}
let CONFIG = {
  key: 'koa:session',
  maxAge: 86400000,
  autoCommit: true, // 自动提交标头(默认为true)
  overwrite: true, // 是否能够覆盖(默认为true
  httpOnly: true, // httpOnly与否(默认为true)
  signed: true, // 是否签名(默认为true)
  rolling: false, // 强制在每一个响应上设置会话标识符cookie。到期重置为原始的maxAge,重置到期倒数
  renew: false, // 在会话即将到期时更新会话,所以咱们始终可使用户保持登陆状态。(默认为false)
  sameSite: null, // 会话cookie sameSite选项
  store // session池
}

module.exports = CONFIG;
复制代码

在app.js使用koa-session中间件

const Session = require('koa-session');
// session
const CONFIG = require('./middleware/session');
app.keys = ['session app keys'];
app.use(Session(CONFIG, app));
复制代码

登陆拦截器

思路:

  • 系统会有一些模块须要用户登陆后才能使用的;
  • 接口设计是,须要登陆的模块api均以/user/开头;
  • 那么只须要在全局路由执行前判断api是否以/user/;
  • 若是是,则判断是否登陆,登陆了就放行,不然拦截,直接返回错误信息;
  • 若是不是,直接放行。

在"./src/middleware/isLogin.js",建立一个验证是否登陆的函数

module.exports = async (ctx, next) => {
  if (ctx.url.startsWith('/user/')) {
    if (!ctx.session.user) {
      ctx.body = {
        code: '401',
        msg: '用户没有登陆,请登陆后再操做'
      }
      return;
    }
  }
  await next();
};
复制代码

在app.js使用登陆拦截器

// 判断是否登陆
const isLogin = require('./middleware/isLogin');
app.use(isLogin);
复制代码

分层设计

思路:

  • 路由负责流量分发;
  • 控制层负责业务逻辑处理,及返回接口json数据;
  • 数据持久层负责数据库操做;
  • 下面以用户模块的登陆、注册、用户名查找接口的实现为例说明。

目录结构

└── src # 源代码目录
    └── routers # 路由目录
        └── router # 子路由目录
            ├── usersRouter.js # 用户模块子路由
            ├── ... # 更多的模块子路由
        ├── index.js # 路由入口文件
    └── controllers # 控制层目录
        ├── usersController.js # 用户模块控制层
        ├── ... # 更多的模块控制层
    └── models # 数据持久层目录
        └── dao # 模块数据持久层目录
            ├── usersDao.js # 用户模块数据持久层
            ├── ... # 更多的模块数据持久层
        ├── db.js # 数据库链接函数
    ├── app.js # 入口文件
复制代码

用户模块接口实现

接口文档

数据库设计

create database storeDB;
use storeDB;
create table users(
  user_id int primary key auto_increment,
  userName char (20) not null unique,
  password char (20) not null,
  userPhoneNumber char(11) null
);
复制代码

路由设计

const Router = require('koa-router');
// 导入控制层
const usersController = require('../../controllers/usersController');

let usersRouter = new Router();

usersRouter
  .post('/users/login', usersController.Login)
  .post('/users/findUserName', usersController.FindUserName)
  .post('/users/register', usersController.Register)

module.exports = usersRouter;
复制代码

控制层设计

const userDao = require('../models/dao/usersDao');
const { checkUserInfo, checkUserName } = require('../middleware/checkUserInfo');

module.exports = {
  /** * 用户登陆 * @param {Object} ctx */
  Login: async ctx => {

    let { userName, password } = ctx.request.body;

    // 校验用户信息是否符合规则
    if (!checkUserInfo(ctx, userName, password)) {
      return;
    }

    // 链接数据库根据用户名和密码查询用户信息
    let user = await userDao.Login(userName, password);
    // 结果集长度为0则表明没有该用户
    if (user.length === 0) {
      ctx.body = {
        code: '004',
        msg: '用户名或密码错误'
      }
      return;
    }

    // 数据库设置用户名惟一
    // 结果集长度为1则表明存在该用户
    if (user.length === 1) {

      const loginUser = {
        user_id: user[0].user_id,
        userName: user[0].userName
      };
      // 保存用户信息到session
      ctx.session.user = loginUser;

      ctx.body = {
        code: '001',
        user: loginUser,
        msg: '登陆成功'
      }
      return;
    }

    //数据库设置用户名惟一
    //若存在user.length != 1 || user.length!=0
    //返回未知错误
    //正常不会出现
    ctx.body = {
      code: '500',
      msg: '未知错误'
    }
  },
  /** * 查询是否存在某个用户名,用于注册时前端校验 * @param {Object} ctx */
  FindUserName: async ctx => {
    let { userName } = ctx.request.body;

    // 校验用户名是否符合规则
    if (!checkUserName(ctx, userName)) {
      return;
    }
    // 链接数据库根据用户名查询用户信息
    let user = await userDao.FindUserName(userName);
    // 结果集长度为0则表明不存在该用户,能够注册
    if (user.length === 0) {
      ctx.body = {
        code: '001',
        msg: '用户名不存在,能够注册'
      }
      return;
    }

    //数据库设置用户名惟一
    //结果集长度为1则表明存在该用户,不能够注册
    if (user.length === 1) {
      ctx.body = {
        code: '004',
        msg: '用户名已经存在,不能注册'
      }
      return;
    }

    //数据库设置用户名惟一,
    //若存在user.length != 1 || user.length!=0
    //返回未知错误
    //正常不会出现
    ctx.body = {
      code: '500',
      msg: '未知错误'
    }
  },
  Register: async ctx => {
    let { userName, password } = ctx.request.body;

    // 校验用户信息是否符合规则
    if (!checkUserInfo(ctx, userName, password)) {
      return;
    }
    // 链接数据库根据用户名查询用户信息
    // 先判断该用户是否存在
    let user = await userDao.FindUserName(userName);

    if (user.length !== 0) {
      ctx.body = {
        code: '004',
        msg: '用户名已经存在,不能注册'
      }
      return;
    }

    try {
      // 链接数据库插入用户信息
      let registerResult = await userDao.Register(userName, password);
      // 操做所影响的记录行数为1,则表明注册成功
      if (registerResult.affectedRows === 1) {
        ctx.body = {
          code: '001',
          msg: '注册成功'
        }
        return;
      }
      // 不然失败
      ctx.body = {
        code: '500',
        msg: '未知错误,注册失败'
      }
    } catch (error) {
      reject(error);
    }
  }
};
复制代码

数据持久层设计

const db = require('../db.js');

module.exports = {
  // 链接数据库根据用户名和密码查询用户信息
  Login: async (userName, password) => {
    const sql = 'select * from users where userName = ? and password = ?';
    return await db.query(sql, [userName, password]);
  },
  // 链接数据库根据用户名查询用户信息
  FindUserName: async (userName) => {
    const sql = 'select * from users where userName = ?';
    return await db.query(sql, [userName]);
  },
  // 链接数据库插入用户信息
  Register: async (userName, password) => {
    const sql = 'insert into users values(null,?,?,null)';
    return await db.query(sql, [userName, password]);
  }
}
复制代码

校验用户信息规则函数

module.exports = {
  /** * 校验用户信息是否符合规则 * @param {Object} ctx * @param {string} userName * @param {string} password * @return: */
  checkUserInfo: (ctx, userName = '', password = '') => {
    // userName = userName ? userName : '';
    // password = password ? password : '';
    // 判断是否为空
    if (userName.length === 0 || password.length === 0) {
      ctx.body = {
        code: '002',
        msg: '用户名或密码不能为空'
      }
      return false;
    }
    // 用户名校验规则
    const userNameRule = /^[a-zA-Z][a-zA-Z0-9_]{4,15}$/;
    if (!userNameRule.test(userName)) {
      ctx.body = {
        code: '003',
        msg: '用户名不合法(以字母开头,容许5-16字节,容许字母数字下划线)'
      }
      return false;
    }
    // 密码校验规则
    const passwordRule = /^[a-zA-Z]\w{5,17}$/;
    if (!passwordRule.test(password)) {
      ctx.body = {
        code: '003',
        msg: '密码不合法(以字母开头,长度在6~18之间,只能包含字母、数字和下划线)'
      }
      return false;
    }

    return true;
  },
  /** * 校验用户名是否符合规则 * @param {type} * @return: */
  checkUserName: (ctx, userName = '') => {
    // 判断是否为空
    if (userName.length === 0) {
      ctx.body = {
        code: '002',
        msg: '用户名不能为空'
      }
      return false;
    }
    // 用户名校验规则
    const userNameRule = /^[a-zA-Z][a-zA-Z0-9_]{4,15}$/;
    if (!userNameRule.test(userName)) {
      ctx.body = {
        code: '003',
        msg: '用户名不合法(以字母开头,容许5-16字节,容许字母数字下划线)'
      }
      return false;
    }

    return true;
  }
}
复制代码

测试

登陆测试

注册测试

查找用户名测试

结语

  • 一个node.js(Koa)后端服务器快速启动模板到这里已经搭建好了;
  • 须要使用的时候只须要分模块的添加一些接口并实现,就能够快速的构建起来一个后端服务器;
  • 后面还打算加一个文件上传(续传)模块;
  • 项目源代码仓库:koa2-start-basic,若是你以为还不错,能够到Github点Star支持一下哦;
  • 笔者还在不断的学习中,若是有表述错误或设计错误,欢迎提意见。
  • 感谢你的阅读!

笔者:hai-27

2020年3月15日

相关文章
相关标签/搜索