使用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日

相關文章
相關標籤/搜索