一、背景
Koa 是一個新的 web 框架,由 Express 幕后的原班人馬打造, 致力于成為 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。 通過利用 async 函數,Koa 幫你丟棄回調函數,并有力地增強錯誤處理。 Koa 并沒有捆綁任何中間件, 而是提供了一套優雅的方法,幫助您快速而愉快地編寫服務端應用程序。
結合當前的node比較火的三大框架,Express、Koa、egg。筆者之前用的Express,后面發現回調把我搞死了,實在太無奈了。終于有一天去嘗試了Koa來進行開發,發現實在太舒服了。ES6語法支持很多,同步模式也很到位,但是在學習koa的過程中,發現基本的基礎知識都了解了,也按照官方的文檔走了一遍,但發現好像無從下手。感覺開發過程中,分層不太明顯,業務邏輯簡單還好,一多麻煩就來了。查看了資料后,有一個koa的腳手架叫做koa-generator
,馬上嘗試后發現不是我想要的模板。看來github已經有2年沒有維護了,koa2一些新特性也沒有加上,感覺有點快落伍了。于是結合其他人的模式,也避免后面自己過多的重復造輪子。編寫一個Koa項目的初始模板。主要遵循MVC模式,這個模板主要的功能集成了Logger、Router、JWT、Mongoose、PM2等模塊,還有部分的中間件集合,該模板對于簡單的后臺項目來說基本夠用了,沒有考慮高并發處理,后期會繼續完善。對于初學者來說,可以快速的新項目開發,在開始之前先好好看下面的解讀。
二、目錄結構
下面的目錄是該模板基礎目錄結構,后面的章節會對每一個目錄的配置進行介紹,讓大家在開發中對項目的結構比較清晰,出了問題容易定位。
├─.gitignore // 忽略文件配置
├─app.js // 應用入口
├─config.js // 公共配置文件
├─ecosystem.config.js // pm2配置文件
├─package.json // 依賴文件配置
├─README.md // README.md文檔
├─routes // 路由
| ├─private.js // 校驗接口
| └public.js // 公開接口
├─models // 數據庫配置及模型
| ├─index.js // 數據庫配置
| └user.js // 用戶的schema文件
├─middlewares // 中間件
| ├─cors.js // 跨域中間件
| ├─jwt.js // jwt中間件
| ├─logger.js // 日志打印中間件
| └response.js // 響應及異常處理中間件
├─logs // 日志目錄
| ├─koa-template.log
| └koa-template.log-2019-05-28
├─lib // 工具庫
| ├─error.js // 異常處理
| └mongoDB.js // mongoDB配置
├─controllers // 操作業務邏輯
| ├─index.js // 配置
| ├─login.js // 登錄
| └test.js // 測試
├─services // 操作數據庫
| ├─index.js // 配置
| ├─user.js // 用戶
├─bin // 啟動目錄
| └www // 啟動文件配置
三、搭建過程
1、環境準備
因為node.js v7.6.0開始完全支持async/await
,所以
node.js環境都要7.6.0以上
node.js環境 版本v7.6以上
npm 版本3.x以上
快速開始
安裝koa2
2、初始化package.json
npm init
3、安裝koa2
npm install koa
執行2、3步驟后文件中會下圖文件,但是這肯定不是我們需要的,那么接下就來開始我們的搬磚起圍墻。
4、新建bin文件
新建bin文件,這個目錄中新建www文件,因為我們后端的項目基本上是在Linux上進行運行的,其實我們不必去擔心文件的后綴是什么,只需知道該文件是可執行文件還是不可執行文件就行了。這個文件有什么用呢?其實我們這個文件是用來部署的時候可以啟動我們一整個后端程序,也就是我們前端中的集成的運行環境。我們的運行、關閉、重啟都在這文件進行即可。基本代碼如下:
#!/usr/bin/env node
/**
* Module dependencies.
*/
const app = require('../app')
const http = require('http')
const config = require('../config')
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || config.port)
// app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app.callback())
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port)
server.on('error', onError)
server.on('listening', onListening)
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
const port = parseInt(val, 10)
if (isNaN(port)) {
// named pipe
return val
}
if (port >= 0) {
// port number
return port
}
return false
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error
}
const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges')
process.exit(1)
break
case 'EADDRINUSE':
console.error(bind + ' is already in use')
process.exit(1)
break
default:
throw error
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
const addr = server.address()
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port
console.log('Listening on ' + bind)
}
相信用過koa-generator對這個代碼斌并不陌生,這其實就是他里面的代碼,express項目的www文件也基本差不多。還是希望大家可以把這里面的代碼過一遍,它的基本思路就是利用了node.js中的http模塊,讓http暴露你的端口并進行監聽,這個端口是在配置文件config.js中引入的。
5、新建app.js
新建文件app.js,先簡單的看一下代碼
'use strict'
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')()
const staticCache = require('koa-static-cache')
const config = require('./config')
const publicRouter = require('./routes/public')
const privateRouter = require('./routes/private')
const { loggerMiddleware } = require('./middlewares/logger')
const { errorHandler, responseHandler } = require('./middlewares/response')
const app = new Koa()
// Logger
app.use(loggerMiddleware)
// Error Handler
app.use(errorHandler)
// Global Middlewares
app.use(bodyParser)
app.use(staticCache(config.publicDir))
// Routes
app.use(publicRouter.routes(), publicRouter.allowedMethods())
app.use(privateRouter.routes(), privateRouter.allowedMethods())
// Response
app.use(responseHandler)
module.exports = app
這個文件中,我們可以看到較多的中間件,中間件的執行順序是從外到內,再從內到外,也就是洋蔥模式。如果還不大了解中間的小伙伴可以去查找相關資料。中間件的執行過程是依靠app.use()
進行傳遞的,你可以簡單的理解為自己編寫的函數,依次去執行即可。每一個中間件會在app調用是傳入2個參數,分別為: ctx
和next
ctx:
Koa Context 將 node 的 request 和 response 對象封裝在一個單獨的對象里面,其為編寫 web 應用和 API 提供了很多有用的方法。
這些操作在HTTP服務器開發中經常使用,因此其被添加在上下文這一層,而不是更高層框架中,因此將迫使中間件需要重新實現這些常用方法。
next:
下一個中間件函數,也就是每一個中間件如果要往下走必須寫上這個,否則無法執行。
可以理解為前端的vue-Router中的路由守衛中的next(), 執行下一步或者進行傳參。
該文件中需要引入其他中間件,可以先引入相關的中間件,后面會一一講解,如果出現報錯,先注釋掉
6、middlewares文件
在這個項目主要用到了幾個中間件,一個是logger.js
、response.js
和 jwt.js
等其他中間件。我們在這個目錄中新建對應中間件后,記得再app.js中進行引入,否在無法生效。記得引入順序,可參考上面代碼。先講一下中間件
中間件的執行很像一個洋蔥,但并不是一層一層的執行,而是以next為分界,先執行本層中next以前的部分,當下一層中間件執行完后,再執行本層next以后的部分。
一個洋蔥結構,從上往下一層一層進來,再從下往上一層一層回去,是不是有點感覺了。
1、logger.js
大家可以想一下,如果我們項目在開發中,或者上線了,我們要看我們執行的日志或者請求的參數以及報錯等信息,如果沒有再每一個請求中體現出來,那么遇到問題我們會很難定位到是前端的問題還是后端。而logger這個中間件就是用來對這些情況進行處理的,原來的koa模板中,只是簡單的進行log的打印而已,這個中間件是用了log4js模塊進行封裝的。詳細使用方法查看官方文檔,這個中間件會在控制臺或者日志中打印出固定的格式,http請求方法、返回狀態、請求url、IP地址、請求時間等,而且我們也可以很好的利用log4js中的配置,來打印出自定義的日志。可以代替console.log()
使用,在使用這個中間件的時候,必須放在第一個中間件,才能保證所以的請求及操作會先經過logger進行記錄再到下一個中間件。
安裝插件:npm i log4js
, 這個文件中也需要引入fs
, path
,config.js
文件,log4的相關配置大家可以去官網進行查看,這里主要是要拿到每一個請求的請求參數方法類型等,可以根據自己需要進行添加。已經再代碼中進行注釋,可以邊看代碼邊理解。
其代碼如下:
'use strict'
const fs = require('fs')
const path = require('path')
const log4js = require('log4js')
const config = require('../config')
// 這個是判斷是否有logs目錄,沒有就新建,用來存放日志
const logsDir = path.parse(config.logPath).dir
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir)
}
// 配置log4.js
log4js.configure({
appenders: {
console: { type: 'console' },
dateFile: { type: 'dateFile', filename: config.logPath, pattern: '-yyyy-MM-dd' }
},
categories: {
default: {
appenders: ['console', 'dateFile'],
level: 'info'
}
}
})
const logger = log4js.getLogger('[Default]')
// logger中間件
const loggerMiddleware = async (ctx, next) => {
// 請求開始時間
const start = new Date()
await next()
// 結束時間
const ms = new Date() - start
// 打印出請求相關參數
const remoteAddress = ctx.headers['x-forwarded-for'] || ctx.ip || ctx.ips ||
(ctx.socket && (ctx.socket.remoteAddress || (ctx.socket.socket && ctx.socket.socket.remoteAddress)))
let logText = `${ctx.method} ${ctx.status} ${ctx.url} 請求參數: ${JSON.stringify(ctx.request.body)} 響應參數: ${JSON.stringify(ctx.body)} - ${remoteAddress} - ${ms}ms`
logger.info(logText)
}
module.exports = {
logger,
loggerMiddleware
}
2、response.js
新建response.js
這個中間件主要是用來對返回前端的響應進行處理,基礎的koa模板中,我們可以用 ctx.body
進行返回前端,但是發現有些東西經常重復寫,還不如提出來進行封裝,而且還不用擔心返回的格式會不一致。
先看看代碼:
'use strict'
const { logger } = require('./logger')
// 這個middleware用于將ctx.result中的內容最終回傳給客戶端
const responseHandler = (ctx) => {
if (ctx.result !== undefined) {
ctx.type = 'json'
ctx.body = {
code: 200,
msg: ctx.msg || '',
data: ctx.result
}
}
}
// 這個middleware處理在其它middleware中出現的異常,我們在next()后面進行異常捕獲,出現異常直接進入這個中間件進行處理
const errorHandler = (ctx, next) => {
return next().catch(err => {
if (err.code == null) {
logger.error(err.stack)
}
ctx.body = {
code: err.code || -1,
data: null,
msg: err.message.trim()
}
// 保證返回狀態是 200
ctx.status = 200
return Promise.resolve()
})
}
module.exports = {
responseHandler,
errorHandler
}
代碼的后面會暴露出responseHandler
和errorHandler
,responseHandler
正確響應,我們在業務中,只需要對ctx.result
進行寫入即可。這個中間件可以放在所有中間件的最后面,這樣可以保證前面中間件都需要經過它,再返回前端。errorHandler
錯誤響應,這個主要是用來進行出錯或者異常的捕獲,可以返回響應給前端,要不前端會出現一直padding的狀態直到超時。
3、jwt.js
新建文件jwt.js, 大家先了解一下JWT是什么,流程是怎樣的
JWT是什么:
JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息
JSON Web Token的結構是什么樣的
Header
header典型的由兩部分組成:token的類型(“JWT”)和算法名稱(比如:HMAC SHA256或者RSA等等)。
payload
JWT的第二部分是payload,它包含聲明(要求)。聲明是關于實體(通常是用戶)和其他數據的聲明。
Signature
為了得到簽名部分,你必須有編碼過的header、編碼過的payload、一個秘鑰,簽名算法是header中指定的那個,然對它們簽名即可
JWT流程:
用戶使用用戶名密碼來請求服務器
服務器進行驗證用戶的信息
服務器通過驗證發送給用戶一個token
客戶端存儲token,并在每次請求時附送上這個token值
服務端驗證token值,并返回數據
了解完這個JWT后,在我們項目中,用了其實這個中間件是對koa-jwt
和jsonwebtoken
進行封裝的,JWT我們用來生成token,用來判斷用戶的唯一性,每次登錄后返回前端,前端每一個需要鑒權的api都需要進行token驗證,我們利用了koa-jwt
進行token的生成,但是怎樣才能在每一個接口中獲取到token解析后的用戶呢。這個中間件就起到很大的關鍵作用。會結合在需要鑒權的router
中,驗證通過后保存信息到ctx中,可以供全局使用,完成了這個中間件后,怎樣引用,我們在后面有進行說明。
記得安裝koa-jwt
和jsonwebtoken
這兩個插件,以下是jwt.js代碼:
'use strict'
const koaJwt = require('koa-jwt')
const jwt = require('jsonwebtoken')
const config = require('../config')
const jwtMiddleware = koaJwt({ secret: config.secret })
module.exports = function (ctx, next) {
// 將 token 中的數據解密后存到 ctx 中
try {
if (typeof ctx.request.headers.authorization === 'string') {
const token = ctx.request.headers.authorization.slice(7)
ctx.jwtData = jwt.verify(token, config.secret)
} else {
throw {code: 401, message: 'no authorization'}
}
} catch (err) {
throw {code: 401, message: err.message}
}
next()
}
4、cors.js文件
在前后端接口請求中,由于瀏覽器的限制,會出現跨域的情況。常用的跨域方案有:
1、JSONP跨域
2、nginx反向代理
3、服務器端修改heade
4、document.domain
5、window.name
6、postMessage
7、后臺配置運行跨域
koa中如何設置跨域
先看看koa中如何設置跨域,cors具體的實現過程,具體的詳細介紹,已經在代碼中進行注釋了。先看一下原生的配置,后面直接使用中間件即可,不過還是需要了解一下具體實現方式,萬一出了問題,能快熟的排查。
app.use(async (ctx, next) => {
// 允許來自所有域名請求
ctx.set("Access-Control-Allow-Origin", "*");
// 這樣就能只允許 http://localhost:8080 這個域名的請求了
// ctx.set("Access-Control-Allow-Origin", "http://localhost:8080");
// 設置所允許的HTTP請求方法
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE");
// 字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段.
ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type");
// 服務器收到請求以后,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,確認允許跨源請求,就可以做出回應。
// Content-Type表示具體請求中的媒體類型信息
ctx.set("Content-Type", "application/json;charset=utf-8");
// 該字段可選。它的值是一個布爾值,表示是否允許發送Cookie。默認情況下,Cookie不包括在CORS請求之中。
// 當設置成允許請求攜帶cookie時,需要保證"Access-Control-Allow-Origin"是服務器有的域名,而不能是"*";
ctx.set("Access-Control-Allow-Credentials", true);
// 該字段可選,用來指定本次預檢請求的有效期,單位為秒。
// 當請求方法是PUT或DELETE等特殊方法或者Content-Type字段的類型是application/json時,服務器會提前發送一次請求進行驗證
// 下面的的設置只本次驗證的有效時間,即在該時間段內服務端可以不用進行驗證
ctx.set("Access-Control-Max-Age", 300);
/*
CORS請求時,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本字段:
Cache-Control、
Content-Language、
Content-Type、
Expires、
Last-Modified、
Pragma。
*/
// 需要獲取其他字段時,使用Access-Control-Expose-Headers,
// getResponseHeader('myData')可以返回我們所需的值
//https://www.rails365.net/articles/cors-jin-jie-expose-headers-wu
ctx.set("Access-Control-Expose-Headers", "myData");
await next();
})
相對用得較多是的大神封裝好得koa-cors中間件,可以自行查看npm上得文檔,在這個項目中用的就是koa-cors的中間件,基本的配置寫在cors.js里面了,再通過中間件進行引用。注意要寫在router前面,避免在沒有進行跨域配置前就去請求接口。
app.js中的引用,記得安裝引入koa-cors
// cors
app.use(cors(corsHandler))
'use strict'
const corsHandler = {
origin: function (ctx) {
if (ctx.url === '/test') {
// 這里可以配置不運行跨域的接口地址
return false;
}
return '*';
},
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
maxAge: 5,
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}
module.exports = {
corsHandler
}
koa-helmet 中間件
koa-helmet 可以幫助你的 app 抵御一些比較常見的安全 web 安全隱患,它其實是將 9 個安全中間件集中到了一起,做了合并,大部分都是對于 http header 的操作,下圖為默認開啟的功能。
在項目中使用先安裝該中間件, npm i koa-helmet --save
,該項目中直接引用默認配置即可,如果有需要,可以看官方文檔自己進行配置。關于koa更多的安全配置,大家可以參考這位大神的博客,https://cnodejs.org/topic/5a437fc18230827a18293afa
這個直接在我們app.js進行引用即可
const helmet = require("koa-helmet")
// Helmet
app.use(helmet())
其他中間件
koa的中間件可以說很多大神給我們做好了輪子,我們直接可以拿來用就行,例如:bodyParser
、koa-session
、將將中間件轉換成koa2可以使用的中間件koa-convert
、EJS模板使用koa-ejs
,大家根據自己需要進行引用,由于是基礎模板,暫時沒有加上過多中間件,減少體積。我們項目中還有用到koa-bodyparser
、koa-static-cache
,記得安裝,并在app.js引入
7、lib文件
這個文件夾主要是用來做存放工具類的文件夾,一些全局的工具處理文件可以放到這邊來,目前這個項目中只有2個文件,新建error.js
和mongoDB.js
error.js
中主要是在中間件中拋出異常,由于前面我們已經加入了異常捕獲的中間件,在中間件操作過程中,如果有錯誤,我們可以直接拋出異常,這個方法就是為了方便我們配置所用的。文件中的方法是CodedError
方法繼承了Error,ForbiddenError
和InvalidQueryError
是繼承了CodedError
,記得在使用的時候得實例化一下該構造函數。如果小伙伴對ES6的繼承還不熟悉,可以先看一下文檔再來看該工具類。
'use strict'
class CodedError extends Error {
constructor (message = '未知錯誤', code = -1) {
super(message)
this.code = code
}
}
module.exports = {
CodedError,
/**
* 拒絕訪問構造函數
*/
ForbiddenError: class ForbiddenError extends CodedError {
constructor (message = '拒絕訪問') {
super(message, 403)
}
},
/**
* 無效的參數構造函數
*/
InvalidQueryError: class InvalidQueryError extends CodedError {
constructor (message = '無效的參數') {
super(message, 400)
}
}
}
mongoDB.js
文件是對mongoDB的鏈接配置,后續在models中會講到。
8、models文件
該項目中是使用mongoose
對mongoDB
對數據庫進行操作,mongoose
語法簡單,需要過多的學習成本。按照官方文檔的配置以及api操作,即可對mongoBD
進行靈活性存儲。mongoose
的配置包括三大部分:connect
、Models
和Schema
connect
:用于創建數據庫連接及監聽
Schema
:Schema主要用于定義MongoDB中集合Collection里文檔document的結構,可以理解為mongoose對表結構的定義(不僅僅可以定義文檔的結構和屬性,還可以定義文檔的實例方法、靜態模型方法、復合索引等),每個schema會映射到mongodb中的一個collection,schema不具備操作數據庫的能力,簡單理解是對字段的定義,操作數據庫必須按照這些字段進行,否在會報錯。
Models
: Model是由Schema編譯而成的假想(fancy)構造器,具有抽象屬性和行為。Model的每一個實例(instance)就是一個document,document可以保存到數據庫和對數據庫進行操作。簡單說就是model是由schema生成的模型,可以對數據庫的操作。
在我們項目中,我們把它全局集合在models文件中進行配置。index.js
文件里面操作了connect
、Models
這兩個步驟。新建index.js,安裝mongoose
,引入相關文件,復制以下代碼:
const fs = require('fs');
const path = require('path');
const mongoose = require('mongoose'); //引用mongoose模塊
const config = require('../config')
const { logger } = require('../middlewares/logger')
let url = "mongodb://" + config.mongoDB.host + ":" + config.mongoDB.port + "/" + config.mongoDB.database;
var mongo = mongoose.createConnection(url); //創建一個數據庫連接
let db = {
mongoose: mongoose,
mongo: mongo,
models: {}
};
// 錯誤
mongo.on('error', function (err) {
logger.error(new Error(err));
});
// 開啟
mongo.once('open', function () {
logger.info("mongo is opened");
});
// 整合models文件下的其他js文件
fs.readdirSync(__dirname)
.filter(function (file) {
return (file.indexOf(".") !== 0) && (file !== "index.js");
}).forEach(function (file) {
var modelFile = require(path.join(__dirname, file));
var schema = new mongoose.Schema(modelFile.schema);
db.models[modelFile.name] = mongo.model(modelFile.name, schema, modelFile.name);
});
// 根據name選擇model
db.getModel = function (name) {
return this.models[name];
};
module.exports = db;
代碼中的鏈接部分一看基本就明白了,可是models
部分怎么看不出所以然。其實是模塊化開發的一部分,這里是為了整合models文件下的其他js文件,方便開發者使用,不用每寫一個文件就要進行引入和導出。
初始情況下,models
引入只需mongoose.model('名稱', schema);
并將其暴露出去,即可對數據庫進行操作。
fs.readdirSync(__dirname)
.filter(function (file) {
return (file.indexOf(".") !== 0) && (file !== "index.js");
}).forEach(function (file) {
var modelFile = require(path.join(__dirname, file));
var schema = new mongoose.Schema(modelFile.schema);
db.models[modelFile.name] = mongo.model(modelFile.name, schema, modelFile.name);
});
在這個文件內,我們做了這樣一件事:讀取 models 目錄下所有文件名不為 index.js 且以 .js 為后綴名的文件,使用 require 進行引用,并將其整合為一個 schema對象后再引入到models并且暴露出去給操作數據庫。這樣子做的好處是,在項目越來越龐大以后,如果我們需要添加新的 schema ,只需直接在 models 目錄下新建 .js 文件即可,則不用再進行引入的關系操作
由于有了上一步的操作,我們后面直接新增一個schema的配置文件即可。index.js會自動的引入并暴露出model
'use strict'
module.exports = {
name: "user",
schema: {
uuid: String, // UUID
userName: String, // 用戶名
password: String, // 密碼
}
};
我們使用時可以這樣操作,
const User = require('../models/index').getModel('user')
const user = await User.findOne({userName: userName})
9、PM2配置
PM2是可以用于生產環境的Nodejs的進程管理工具,并且它內置一個負載均衡。它不僅可以保證服務不會中斷一直在線,并且提供0秒reload功能,還有其他一系列進程管理、監控功能。并且使用起來非常簡單。pm2的官方文檔已經進行詳細的配置說明,在這里就不進行一一簡述,主要講的時我的koa項目怎樣配合PM2進行相關管理或者說部署。PM2常用命令需要用的時候可以進行查看,沒必要去背,用多就熟悉了。也可以結合在package.json里面,用自定義命令運行。我們在package.json
的script
配置和初始化文件ecosystem.config.js
進行了多環境運行的配置,我們可以根據需要進行切換環境。
package.json
文件添加如下:
"scripts": {
"start": "node ./bin/www",
"dev": "pm2 start ecosystem.config.js --env dev",
"test": "pm2 start ecosystem.config.js --env test",
"pro": "pm2 start ecosystem.config.js --env pro",
"logs": "pm2 logs",
"stop": "pm2 stop ecosystem.config.js"
},
其中的
npm run start: 直接跑www文件,可用于調試
npm run dev: 開發環境
npm run test:測試環境
npm run pro:生產環境
npm run logs: 查看pm2的日志
npm run stop: 停止pm2服務
新增ecosystem.config.js
文件:
module.exports = {
apps : [{
name: 'API',
script: './bin/www',
// Options reference: https://pm2.io/doc/en/runtime/reference/ecosystem-file/
args: 'one two',
instances: 1,
autorestart: true,
watch: true,
ignore_watch: [ // 不用監聽的文件
'node_modules',
'logs'
],
max_memory_restart: '1G',
env_pro: {
"NODE_ENV": "production",
"REMOTE_ADDR": ""
},
env_dev: {
"NODE_ENV": "development",
"REMOTE_ADDR": ""
},
env_test: {
"NODE_ENV": "test",
"REMOTE_ADDR": ""
}
}]
};
這個文件主要是對pm2的基本配置,不用每次都進行配置,直接在文件進行改動即可。我們需要關注的是可以在env
中,增加我們需要的環境及變量即可,文件中的watch
屬性是可以配置監聽文件改動后,自動重啟項目,比較好用。如果想忽略某一個文件夾的變動可以ignore_watch
,更多的配置如果有興趣的小伙伴可以查看官方文檔的文檔說明。
10、路由配置
新建router文件,該目錄下存放路由基本配置,新建private
和public
兩個文件,安裝router
,在app.js引入路由后,
const publicRouter = require('./routes/public')
const privateRouter = require('./routes/private')
// Routes
app.use(publicRouter.routes(), publicRouter.allowedMethods())
app.use(privateRouter.routes(), privateRouter.allowedMethods())
每一個路由都必須暴露出去,這樣在app.js文件中使用該中間件。publicRouter.allowedMethods()
根據ctx.status
設置response
響應頭
private
:該文件下的路由是需要通過jwt驗證的,才能進行訪問。前面我們做了jwt的中間件,我們直接引入即可
router.use(jwtMiddleware)
記得要放在請求路由的前面,才能保證每次都經過它。我們對其前綴做了處理,router.prefix('/api')
在每一個請求的時候都需要帶上這個前綴,抽出來也是為了服務目錄的改變,可以直接更改即可,做了全局的操作
'use strict'
const Router = require('koa-router')
const controllers = require('../controllers')
const jwtMiddleware = require('../middlewares/jwt')
const router = new Router()
router.prefix('/api')
router.use(jwtMiddleware)
router.get('/test', controllers.test.test)
module.exports = router
public
:該文件與上面相反,主要用來不進行登錄的校驗,也就是我們常用的登錄、注冊等不需要驗證的接口。
'use strict'
const Router = require('koa-router')
const controllers = require('../controllers')
const router = new Router()
router.prefix('/api')
router.post('/login', controllers.login.login)
module.exports = router
為什么我們沒在這里處理業務邏輯呢?其實這里是遵循了MVC的思想,進行了分離。把數據庫的操作放到了controllers文件中。這如果我們接口一多,不會顯示得特別混亂。下面我們就來講這個文件。
11、controllers文件
為了讓整個項目更為模塊化,該目錄下主要是處理對應的路由的回調函數,一般我們不會在router文件中去業務邏輯操作等步驟,這里采用 routes 和 controller 分開,在方便代碼的查看同時,也方便代碼的維護和開發。
在controller新建index.js文件:
該文件與models中的index.js文件中的集合該目錄下的文件類似,這里是將其他文件導出統一到index暴露出去。
'use strict'
const fs = require('fs')
const files = fs.readdirSync(__dirname).filter(file => file !== 'index.js')
const controllers = {}
for (const file of files) {
if (file.toLowerCase().endsWith('js')) {
const controller = require(`./${file}`)
controllers[`${file.replace(/\.js/, '')}`] = controller
}
}
module.exports = controllers
其他文件的編寫可以按照下面基本框架進行,在這里會用到前面封裝好的業務,例如操作、響應、jwt等操作。大家可以認真看以下代碼分析一下。
新建user.js文件,這個文件就是我們處理業務的,我們可以按需添加
'use strict'
const jwt = require('jsonwebtoken')
const config = require('../config')
const userServices = require('../services').user
const { InvalidQueryError } = require('../lib/error')
const login = {}
login.login = async (ctx, next) => {
console.log(userServices)
const {userName, password} = ctx.request.body
if (!userName || !password) {
throw new InvalidQueryError()
}
const user = await userServices.login({
userName: userName,
password: password
})
if (!user) {
ctx.result = ''
ctx.msg = '用戶不存在'
} else {
ctx.result = jwt.sign({
data: user._id,
// 設置 token 過期時間
exp: Math.floor(Date.now() / 1000) + (60 * 60), // 60 seconds * 60 minutes = 1 hour
}, config.secret)
}
return next()
}
module.exports = login
注:請求 Header 參數中添加 Authorization
屬性,value
格式為:Bearer [token]
11、services文件
新增services文件,這個文件主要是用來處理數據庫以及服務等邏輯,我們進行了抽離,在該文件夾下面新建index.js,該文件內容還是與controllers文件中的用法一致,大家可參考上面的說明,只是部分字段需要改動,詳細看下面代碼:
'use strict'
const fs = require('fs')
const files = fs.readdirSync(__dirname).filter(file => file !== 'index.js')
const services = {}
for (const file of files) {
if (file.toLowerCase().endsWith('js')) {
const service = require(`./${file}`)
services[`${file.replace(/\.js/, '')}`] = service
}
}
module.exports = services
如果需要新建其他模塊操作,可以在該新建例如:user.js文件,目前該文件是對數據庫user的集合操作,示例如下:
const User = require('../models/index').getModel('user')
const user = {
/**
* @Description: 登錄
* @date 2019/5/30
* @params: { Object } userData
* @return: { Object | null }
*/
async login (userData) {
return await User.findOne(userData)
}
}
module.exports = user
12、config.js文件
根目錄新建該文件主要用來存放全局的配置,如果一個項目中沒有全局的配置,那么一個地方改動牽動的其他地方很多,這樣很不利于工作效率,在開發過程中,我們一般會把常用的都放在這個文件,例如:數據庫參數,端口,密鑰,全局變量等。看自己的需求適當的更改。該文件將變量進行了暴露,引用時進行require即可。
'use strict'
const path = require('path')
module.exports = {
port: '3001',
secret: 'secret',
publicDir: path.resolve(__dirname, './public'),
logPath: path.resolve(__dirname, './logs/koa-template.log'),
mongoDB: {
database: 'mall',
username: 'root',
password: 'root',
host: '127.0.0.1',
port: 27017
}
}
package.json文件
每個Nodejs項目的根目錄下面,一般都會有一個package.json文件。該文件可以由npm init生成,我們再開始已經操作了,定義了項目所需要的各種模塊,以及項目的配置信息(比如名稱、版本、許可證等元數據)。
package.json文件內部就是一個JSON對象,該對象的每一個成員就是當前項目的一項設置。我們也可在里面配置我們的npm run XXX
的命令,大家可以根據需求進行配置。這是這項目需要用到的package.json文件。看是否與你的跟該文件一樣。
{
"name": "koa-template",
"version": "0.1.0",
"author": "bayi",
"private": true,
"scripts": {
"start": "node ./bin/www",
"dev": "pm2 start ecosystem.config.js --env dev",
"test": "pm2 start ecosystem.config.js --env test",
"pro": "pm2 start ecosystem.config.js --env pro",
"logs": "pm2 logs",
"stop": "pm2 stop ecosystem.config.js"
},
"dependencies": {
"koa": "^2.6.2",
"koa-bodyparser": "^4.2.1",
"koa-helmet": "^4.1.0",
"koa-jwt": "^3.5.1",
"koa-router": "^7.4.0",
"koa-static-cache": "^5.1.2",
"koa2-cors": "^2.0.6",
"log4js": "^3.0.6",
"mongoose": "^5.5.5"
}
}
三、其他
github地址:
https://github.com/bayi-lzp/koa-template (star! star!star!)
技術棧:
koa2、mongoose
近期會更新:
typescript、redis、docker