最近使用koa搭建了個web服務器,包括Session的處理、路由的設計、項目結構的設計、錯誤的處理、數據庫的讀寫等,用起來特別的爽,在這作個總結,供你們參考,有什麼不對的,歡迎討論。javascript
-config.js
-package.json
-scripts/,用來存放初始腳本文件
-logs/,用來存放日誌
-static/,用來存放前端文件
-server/,用來存放後端的代碼複製代碼
server/前端
├── app.js//主文件,建立server,並使用對應的中間件
├── controller//響應路由,調用services中對應的模塊,返回結果
│ ├── home.js
│ ├── strategy.js
│ └── user.js
├── models//操做數據庫
│ ├── strategy.js
│ └── user.js
├── routers//路由的定義目錄
│ ├── home.js
│ ├── routers.js
│ ├── strategy.js
│ └── user.js
├── services//調用models,返回正確的處理;之因此要增長這麼一個在controller和models之間的模塊,主要仍是考慮到services的抽象
│ ├── strategy.js
│ └── user.js
└── utils//公共的模塊,好比日誌,數據庫的讀寫
├── datetime.js
├── db.js
├── log.js
└── redis.js複製代碼
使用到的中間件版本java
"bcrypt": "^3.0.6",
"ioredis": "^4.9.5",
"koa": "^2.7.0",
"koa-bodyparser": "^4.2.1",
"koa-redis": "^4.0.0",
"koa-router": "^7.4.0",
"koa-session-minimal": "^3.0.4",
"koa-mysql-session": "^0.0.2",
"koa-static": "^5.0.0",
"log4js": "^4.3.1",
"mysql": "^2.17.1",
"superagent": "^5.0.6"
複製代碼
app.jsmysql
const Koa = require('koa');
const app = new Koa();
const static = require('koa-static');
const bodyParser = require('koa-bodyparser');
const routers = require('./routers/routers');
const config = require('../config');
const session = require('koa-session-minimal');
const redisStore = require('koa-redis')
const redis = require('./utils/redis')
const log = require('./utils/log')
const logger = async (ctx, next)=>{
log.info(`[uid: ${ctx.session.uid}] ${ctx.request.method}, ${ctx.request.url}`);
await next();
}
const handler = async (ctx, next)=>{
try {
await next();
} catch (error) {
ctx.response.status = error.statusCode || error.status || 500;
ctx.response.body = {
message: error.message
};
}
}
// 配置存儲session信息的redis
const store = new redisStore({
client: redis
});
app.use(bodyParser());
app.use(session({
key: 'SESSION_ID',
store: store,
cookie: {// 存放sessionId的cookie配置
maxAge: 24*3600*1000, // cookie有效時長
expires: '', // cookie失效時間
path: '/', // 寫cookie所在的路徑
domain: config.domain, // 寫cookie所在的域名
httpOnly: '', // 是否只用於http請求中獲取
overwrite: '', // 是否容許重寫
secure: '',
sameSite: '',
signed: '',
}
}));
app
.use(logger)//處理log
.use(handler)//處理出錯信息
.use(static(config.staticPath));//配置靜態資源路徑
app
.use(routers.routes())
.use(routers.allowedMethods());//註冊路由
app.listen(config.port, ()=>{
log.info('server is running on port:'+config.port);
});
複製代碼
因爲http協議不能記住是哪一個用戶在使用該協議,因此須要另外的機制來輔助;Session指的是服務器端用來記住哪一個user在使用的機制,固然也須要前端來配合的(通常都是將Session的信息,好比session_id寫入cookie中),http協議會帶着cookie信息,而後服務器端經過該session_id從mysql或redis中找到對應的uid,這樣就知道是誰在使用了。web
上面的處理機制說着複雜,咱們用着其實很簡單,koa-session-minimal中間件已經幫咱們處理了,固然咱們須要告訴它應該把session信息存在哪裏,能夠是mysql或者redis;redis
const session = require('koa-session-minimal');
const redisStore = require('koa-redis')
const redis = require('./utils/redis')
// 配置存儲session信息的redis
const store = new redisStore({
client: redis
});
app.use(session({
key: 'SESSION_ID',
store: store,
cookie: {// 存放sessionId的cookie配置
maxAge: 24*3600*1000, // cookie有效時長
expires: '', // cookie失效時間
path: '/', // 寫cookie所在的路徑
domain: config.domain, // 寫cookie所在的域名
httpOnly: '', // 是否只用於http請求中獲取
overwrite: '', // 是否容許重寫
secure: '',
sameSite: '',
signed: '',
}
}));
複製代碼
./utils/redis.jssql
const config = require('../../config')
const redisConfig = config.redis
const Redis = require('ioredis')
let redis = new Redis({
host: redisConfig.HOST,
port: redisConfig.PORT,
password: redisConfig.PASSWORD,
family: redisConfig.FAMILY,
db: redisConfig.DB,
ttl: redisConfig.TTL//設置過時時間,單位是秒
})
module.exports = redis
複製代碼
這種方法有個問題,當不少用戶不斷登陸的時候,會在mysql上遺留不少的過時沒用的session信息,這個就須要清理,比較麻煩。但redis中就能夠直接設置個過時時間,很方便,因此我用的是redis來存儲數據庫
const session = require('koa-session-minimal');
const MysqlSession = require('koa-mysql-session');
// 配置存儲session信息的mysql
const store = new MysqlSession({
user: config.database.USERNAME,
password: config.database.PASSWORD,
database: config.database.DATABASE,
host: config.database.HOST,
});
app.use(session({
key: 'SESSION_ID',
store: store,
cookie: {// 存放sessionId的cookie配置
maxAge: 24*3600*1000, // cookie有效時長
expires: '', // cookie失效時間
path: '/', // 寫cookie所在的路徑
domain: 'localhost', // 寫cookie所在的域名
httpOnly: '', // 是否只用於http請求中獲取
overwrite: '', // 是否容許重寫
secure: '',
sameSite: '',
signed: '',
}
}));
複製代碼
koa支持多路由的註冊與響應,很方便根據模塊來設計api與處理json
app.js後端
const routers = require('./routers/routers');
app
.use(routers.routes()).use(routers.allowedMethods());複製代碼
routers/routers.js,將多個模塊中的路由整合在這裏
const Router = require('koa-router');
const home = require('./home');
const user = require('./user');
const strategy = require('./strategy');
const router = new Router();
router.use(home.routes(), home.allowedMethods());
router.use(user.routes(), user.allowedMethods());
router.use(strategy.routes(), strategy.allowedMethods());
module.exports = router;
複製代碼
routers/user.js,用戶相關的路由
const Router = require('koa-router');
const router = new Router();
const userCtl = require('../controller/user');
router.get('/user/list', userCtl.getUserList);
router.get('/user/info', userCtl.getUserInfo);
router.post('/user/signin', userCtl.userSignin);
router.post('/user/signup', userCtl.userSignup);
module.exports = router;
複製代碼
路由註冊好後,就應該響應路由,返回結果,對應到項目中,
routers/user.js --> controller/user.js --> services/user.js --> models/user.js --> utils/db.js複製代碼
async、await的使用,使得寫異步代碼就像是寫同步代碼同樣,邏輯上很是清晰,能夠很好的解決回調地獄的狀況。
項目中的出錯的處理,都是在controller中,經過try/catch來捕獲;邏輯上感受很乾淨,不知道實際工程中會不會有什麼問題,有大量實戰經驗的童鞋能夠說說哦。
controller中處理api相關的邏輯;
項目中使用bcrypt進行密碼的加密與對比;
const userService = require('../services/user')
const bcrypt = require('bcrypt')
const log = require('../utils/log')
const userSignin = async (ctx)=>{
let result = {
success: false,
message: '',
data: null,
}, message = ''
if (ctx.session.uid) {
message = 'aleady login.'
result.data = {
uid: ctx.session.uid
}
} else {
try {
let formData = ctx.request.body
let res = await userService.signin(formData)
if (res) {
if (bcrypt.compareSync(formData.password, res.password)) {
ctx.session = {uid: res.id}
result.success = true
result.data = {uid: res.id, name: res.name}
} else {
message = 'phone or password error.'
}
} else {
message = 'no such user.'
}
} catch (error) {
message = 'login failed'
log.error(message+', '+error)
}
}
result.message = message
ctx.body = result
}
module.exports = {
getUserList,
getUserInfo,
userSignin,
userSignup
}複製代碼
由於service能夠是多種多樣的,因此須要用services模塊來封裝一層;
services中,應該考慮到從數據庫中獲取到的各類狀況,好比經過用戶的手機獲取用戶信息,可能獲取失敗、可能獲取不到、可能獲取成功,好比下面的處理。
const userModel = require('../models/user')
const signin = async (formData)=>{
return new Promise((resolve, reject)=>{
userModel.getUserByPhone(formData.phone)
.then(res=>{
if (Array.isArray(res) && res.length> 0) {
resolve(res[0])
} else {
resolve(null)
}
}, err=>{
reject(err)
})
})
}
module.exports = {
getUserList,
getUserInfoById,
signin,
checkIsUserAdmin,
modifyUser
}
複製代碼
models也是相似,也是爲了封裝對應模塊的數據庫操做
const db = require('../utils/db')
const getUserList = async ()=> {
let keys = ['id', 'phone', 'level', 'avatar', 'login_count', 'create_time', 'last_login_time']
return await db.selectKeys('user_info', keys)
}
const getUserInfoById = async (uid)=> {
let keys = ['id', 'phone', 'level', 'avatar', 'login_count', 'create_time', 'last_login_time']
return await db.selectKeysById('user_info', keys, uid)
}
const getUserByPhone = async (phone)=> {
let _sql = "select * from user_info where phone="+phone+" limit 1"
return await db.query(_sql)
}
const modifyUser = async (user)=> {
return await db.updateData('user_info', user, user.id)
}
module.exports = {
getUserList,
getUserInfoById,
getUserByPhone,
modifyUser
}
複製代碼
mysql的操做模塊,對外提供一些接口給其餘模塊使用
const config = require('./../../config')
const dbConfig = config.database
const mysql = require('mysql')
const pool = mysql.createPool({
host: dbConfig.HOST,
user: dbConfig.USERNAME,
password: dbConfig.PASSWORD,
database: dbConfig.DATABASE
})
let query = (sql, values)=>{
return new Promise((resolve, reject)=>{
pool.getConnection((err, connection)=>{
if (err) {
resolve(err)
} else {
connection.query(sql, values, (err, rows)=>{
if (err) {
reject(err)
} else {
resolve(rows)
}
connection.release()
})
}
})
})
}
let createTable = sql => {
return query(sql, [])
}
let selectAll = (table)=>{
let _sql = "select * from ??"
return query(_sql, [table])
}
let selectAllById = (table, id)=>{
let _sql = "select * from ?? where id = ?"
return query(_sql, [table, id])
}
let selectKeys = (table, keys)=>{
let _sql = "select ?? from ??"
return query(_sql, [keys, table])
}
module.exports = {
query,
createTable,
selectAll,
selectAllById,
selectKeys,
selectKeysById,
selectKeysByKey,
insertData,
insertBatchData,
updateData,
deleteDataById
}複製代碼
使用log4js來對日誌進行歸檔分類
const config = require('../../config')
const debug = config.debug
const logPath = config.logPath
const log4js = require('log4js')
const getCfg = ()=>{
var cfg = {}
if (debug) {
cfg.type = 'console'
} else {
cfg.type = 'file'
cfg.filename = logPath+'/server-'+new Date().toLocaleDateString()+'.log'
}
return cfg
}
log4js.configure({
appenders: {
access: getCfg()
},
categories: {
default: {appenders: ['access'], level: 'info'}
}
})
module.exports = log4js.getLogger('access')
複製代碼