使用Koa2從零開始實現一個具備基本功能的後端服務器的過程與思路

快速創建一個服務器

安裝koa

<code>npm install koa -S/<code>

基本配置

<code>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 }端口`);
});/<code>

測試

就這樣一個node.js服務器就啟動起來了,

使用Koa2從零開始實現一個具備基本功能的後端服務器的過程與思路


使用postman測試一下

使用Koa2從零開始實現一個具備基本功能的後端服務器的過程與思路

路由中間件

思路:

  • 使用koa-router中間件處理路由;
  • 如果把所有的路由寫在一起,將會非常擁擠,不利於後期維護,所以為每個業務模塊配置模塊子路由;
  • 然後把所有的模塊子路由彙總到./src/roters/index.js;
  • 再在入口文件require('./routers')。

路由中間件目錄

<code>└── src # 源代碼目錄
└── routers # 路由目錄
└── router # 子路由目錄
├── usersRouter.js # 用戶模塊子路由
├── ... # 更多的模塊子路由
├── index.js # 路由入口文件/<code>

安裝koa-router

<code>npm install koa-router -S/<code>

模塊子路由設計

<code>const Router = require('koa-router');
// 導入控制層
const usersController = require('../../controllers/usersController');


let usersRouter = new Router();

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

module.exports = usersRouter;/<code>

模塊子路由彙總

<code>const Router = require('koa-router');

let Routers = new Router();

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

Routers.use(usersRouter.routes());

module.exports = Routers;/<code>

使用路由中間件

<code>// 使用路由中間件
const Routers = require('./routers');
app.use(Routers.routes()).use(Routers.allowedMethods());/<code>

接口測試

使用postman測試接口localhost:5000/users/login

使用Koa2從零開始實現一個具備基本功能的後端服務器的過程與思路

數據庫連接封裝

思路:

  • 後端與數據庫的交互是非常頻繁的,如果是一個接一個地創建和管理連接,將會非常麻煩;
  • 所以使用連接池的方式,封裝一個連接池模塊;
  • 對連接進行集中的管理(取出連接,釋放連接);
  • 執行查詢使用的是connection.query(),對connection.query()進行二次封裝,統一處理異常;
  • 向外導出一個db.query()對象,使用的時候,只需要傳入sql語句、查詢參數即可,例如:
<code>db.query('select * from users where userName = ? and password = ?', ['userName', 'password'])/<code>

安裝mysql依賴包

<code>npm install mysql -S/<code>

配置連接選項

在config.js添加如下代碼,然後在db.js引入

<code>// 數據庫連接設置
dbConfig: {

connectionLimit: 10,
host: 'localhost',
user: 'root',
password: '',
database: 'storeDB'
}/<code>

連接池封裝

創建"./src/models/db.js"

<code>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;/<code>

更多的信息請參考mysql文檔。

請求體數據處理

思路:

  • 使用koa-body中間件,可以很方便的處理請求體的數據,例如
<code>let { userName, password } = ctx.request.body;/<code>

安裝koa-body中間件

<code>npm install koa-body -S/<code>

使用koa-body中間件

在config.js配置上傳文件路徑

<code>uploadDir: path.join(__dirname, path.resolve('../public/')), // 上傳文件路徑/<code>

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

<code>const KoaBody = require('koa-body');
let { uploadDir } = require('./config');/<code>
<code>// 處理請求體數據
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);
}

}
}));/<code>

異常處理

思路:

  • 程序在執行的過程中難免會出現異常;
  • 如果因為一個異常服務器就掛掉,那會大大增加服務器的維護成本,而且體驗極差;
  • 所以在中間件的執行前進行一次異常處理。

在app.js添加如下代碼

<code>// 異常處理中間件
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
console.log(error);
ctx.body = {
code: '500',
msg: '服務器未知錯誤'
}
}
});/<code>

靜態資源服務器

思路:

  • 前端需要大量的靜態資源,後端不可能為每條靜態資源的請求都寫一份代碼;
  • koa-static可以非常方便的實現一個靜態資源服務器;
  • 只需要創建一個文件夾統一放靜態資源,例如./public;
  • 那麼就可以通過http://localhost:5000/public/文件夾/文件名直接訪問。

安裝koa-static中間件

<code>npm install koa-static -S/<code>

使用koa-static中間件

在config.js配置靜態資源路徑

<code>staticDir: path.resolve('../public'), // 靜態資源路徑/<code>

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

<code>const KoaStatic = require('koa-static');
let { staticDir } = require('./config');/<code>
<code>// 為靜態資源請求重寫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));/<code>

接口測試

使用瀏覽器測試接口http://localhost:5000/public/imgs/a.png

使用Koa2從零開始實現一個具備基本功能的後端服務器的過程與思路

session實現

思路:

  • 使用koa-session中間件實現session的操作;
  • 用於登錄狀態的管理;
  • 本例子使用內存存儲的方案,適用於session數據量小的場景;
  • 如果session數據量大,建議使用外部存儲介質存放session數據 。

安裝koa-session中間件

<code>npm install koa-session -S/<code>

使用koa-session中間件

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

<code>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;/<code>

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

<code>const Session = require('koa-session');
// session
const CONFIG = require('./middleware/session');
app.keys = ['session app keys'];
app.use(Session(CONFIG, app));/<code>

登錄攔截器

思路:

  • 系統會有一些模塊需要用戶登錄後才能使用的;
  • 接口設計是,需要登錄的模塊api均以/user/開頭;
  • 那麼只需要在全局路由執行前判斷api是否以/user/;
  • 如果是,則判斷是否登錄,登錄了就放行,否則攔截,直接返回錯誤信息;
  • 如果不是,直接放行。

在"./src/middleware/isLogin.js",創建一個驗證是否登錄的函數

<code>module.exports = async (ctx, next) => {
if (ctx.url.startsWith('/user/')) {
if (!ctx.session.user) {
ctx.body = {
code: '401',
msg: '用戶沒有登錄,請登錄後再操作'

}
return;
}
}
await next();
};/<code>

在app.js使用登錄攔截器

<code>// 判斷是否登錄
const isLogin = require('./middleware/isLogin');
app.use(isLogin);/<code>

分層設計

思路:

  • 路由負責流量分發;
  • 控制層負責業務邏輯處理,及返回接口json數據;
  • 數據持久層負責數據庫操作;
  • 下面以用戶模塊的登錄、註冊、用戶名查找接口的實現為例說明。

目錄結構

<code>└── src # 源代碼目錄
└── routers # 路由目錄
└── router # 子路由目錄
├── usersRouter.js # 用戶模塊子路由
├── ... # 更多的模塊子路由
├── index.js # 路由入口文件

└── controllers # 控制層目錄
├── usersController.js # 用戶模塊控制層
├── ... # 更多的模塊控制層
└── models # 數據持久層目錄
└── dao # 模塊數據持久層目錄
├── usersDao.js # 用戶模塊數據持久層
├── ... # 更多的模塊數據持久層
├── db.js # 數據庫連接函數
├── app.js # 入口文件/<code>

用戶模塊接口實現

接口文檔

數據庫設計

<code>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
);/<code>

路由設計

<code>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;/<code>

控制層設計

<code>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);
}
}
};/<code>

數據持久層設計

<code>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]);
}
}/<code>

校驗用戶信息規則函數

<code>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;
}
}/<code>

測試

登錄測試

使用Koa2從零開始實現一個具備基本功能的後端服務器的過程與思路


註冊測試

使用Koa2從零開始實現一個具備基本功能的後端服務器的過程與思路


查找用戶名測試

使用Koa2從零開始實現一個具備基本功能的後端服務器的過程與思路

結語

  • 一個node.js(Koa)後端服務器快速啟動模板到這裡已經搭建好了;
  • 需要使用的時候只需要分模塊的添加一些接口並實現,就可以快速的構建起來一個後端服務器;
  • 後面還打算加一個文件上傳(續傳)模塊;
  • 項目源代碼倉庫:koa2-start-basic,如果你覺得還不錯,可以到Github點Star支持一下哦;
  • 筆者還在不斷的學習中,如果有表述錯誤或設計錯誤,歡迎提意見。
  • 感謝你的閱讀!


作者:hai_27
鏈接:https://juejin.im/post/5e6de50f6fb9a07cae137bf5


分享到:


相關文章: