nodejs+koa2+mongodb從零開始搭建koa項目

1、背景

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等模塊,還有部分的中間件集合,該模板對於簡單的後臺項目來講基本夠用了,沒有考慮高併發處理,後期會繼續完善。對於初學者來講,能夠快速的新項目開發,在開始以前先好好看下面的解讀。vue

2、目錄結構

下面的目錄是該模板基礎目錄結構,後面的章節會對每個目錄的配置進行介紹,讓你們在開發中對項目的結構比較清晰,出了問題容易定位。node

├─.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                         // 啓動文件配置
複製代碼

3、搭建過程

一、環境準備

由於node.js v7.6.0開始徹底支持async/await,因此nginx

node.js環境都要7.6.0以上git

node.js環境 版本v7.6以上github

npm 版本3.x以上 快速開始 安裝koa2web

二、初始化package.json

npm initredis

三、安裝koa2

npm install koa算法

執行二、3步驟後文件中會下圖文件,可是這確定不是咱們須要的,那麼接下就來開始咱們的搬磚起圍牆。mongodb

四、新建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中引入的。

五、新建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個參數,分別爲: ctxnext

ctx:  
Koa Context 將 node 的 request 和 response 對象封裝在一個單獨的對象裏面,其爲編寫 web 應用和 API 提供了不少有用的方法。
這些操做在HTTP服務器開發中常用,所以其被添加在上下文這一層,而不是更高層框架中,所以將迫使中間件須要從新實現這些經常使用方法。

next: 
下一個中間件函數,也就是每個中間件若是要往下走必須寫上這個,不然沒法執行。
能夠理解爲前端的vue-Router中的路由守衛中的next(), 執行下一步或者進行傳參。
複製代碼

該文件中須要引入其餘中間件,能夠先引入相關的中間件,後面會一一講解,若是出現報錯,先註釋掉

六、middlewares文件

在這個項目主要用到了幾個中間件,一個是logger.jsresponse.jsjwt.js等其餘中間件。咱們在這個目錄中新建對應中間件後,記得再app.js中進行引入,否在沒法生效。記得引入順序,可參考上面代碼。先講一下中間件

中間件的執行很像一個洋蔥,但並非一層一層的執行,而是以next爲分界,先執行本層中next之前的部分,當下一層中間件執行完後,再執行本層next之後的部分。

一個洋蔥結構,從上往下一層一層進來,再從下往上一層一層回去,是否是有點感受了。

一、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
}

複製代碼
二、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
}

複製代碼

代碼的後面會暴露出responseHandlererrorHandler,responseHandler正確響應,咱們在業務中,只須要對ctx.result進行寫入便可。這個中間件能夠放在全部中間件的最後面,這樣能夠保證前面中間件都須要通過它,再返回前端。errorHandler錯誤響應,這個主要是用來進行出錯或者異常的捕獲,能夠返回響應給前端,要不前端會出現一直padding的狀態直到超時。

三、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-jwtjsonwebtoken進行封裝的,JWT咱們用來生成token,用來判斷用戶的惟一性,每次登陸後返回前端,前端每個須要鑑權的api都須要進行token驗證,咱們利用了koa-jwt進行token的生成,可是怎樣才能在每個接口中獲取到token解析後的用戶呢。這個中間件就起到很大的關鍵做用。會結合在須要鑑權的router中,驗證經過後保存信息到ctx中,能夠供全局使用,完成了這個中間件後,怎樣引用,咱們在後面有進行說明。

記得安裝koa-jwtjsonwebtoken這兩個插件,如下是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()
}

複製代碼
四、cors.js文件

在先後端接口請求中,因爲瀏覽器的限制,會出現跨域的狀況。經常使用的跨域方案有:

一、JSONP跨域

二、nginx反向代理

三、服務器端修改heade

四、document.domain

五、window.name

六、postMessage

七、後臺配置運行跨域

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更多的安全配置,你們能夠參考這位大神的博客,cnodejs.org/topic/5a437…

這個直接在咱們app.js進行引用便可

const helmet = require("koa-helmet")

// Helmet
app.use(helmet())

複製代碼
其餘中間件

koa的中間件能夠說不少大神給咱們作好了輪子,咱們直接能夠拿來用就行,例如:bodyParserkoa-session、將將中間件轉換成koa2可使用的中間件koa-convert、EJS模板使用koa-ejs,你們根據本身須要進行引用,因爲是基礎模板,暫時沒有加上過多中間件,減小體積。咱們項目中還有用到koa-bodyparserkoa-static-cache,記得安裝,並在app.js引入

七、lib文件

這個文件夾主要是用來作存放工具類的文件夾,一些全局的工具處理文件能夠放到這邊來,目前這個項目中只有2個文件,新建error.jsmongoDB.js

error.js中主要是在中間件中拋出異常,因爲前面咱們已經加入了異常捕獲的中間件,在中間件操做過程當中,若是有錯誤,咱們能夠直接拋出異常,這個方法就是爲了方便咱們配置所用的。文件中的方法是CodedError方法繼承了Error,ForbiddenErrorInvalidQueryError是繼承了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中會講到。

八、models文件

該項目中是使用mongoosemongoDB對數據庫進行操做,mongoose語法簡單,須要過多的學習成本。按照官方文檔的配置以及api操做,便可對mongoBD進行靈活性存儲。mongoose的配置包括三大部分:connectModelsSchema

connect:用於建立數據庫鏈接及監聽

Schema:Schema主要用於定義MongoDB中集合Collection裏文檔document的結構,能夠理解爲mongoose對錶結構的定義(不只僅能夠定義文檔的結構和屬性,還能夠定義文檔的實例方法、靜態模型方法、複合索引等),每一個schema會映射到mongodb中的一個collection,schema不具有操做數據庫的能力,簡單理解是對字段的定義,操做數據庫必須按照這些字段進行,否在會報錯。

Models:  Model是由Schema編譯而成的假想(fancy)構造器,具備抽象屬性和行爲。Model的每個實例(instance)就是一個document,document能夠保存到數據庫和對數據庫進行操做。簡單說就是model是由schema生成的模型,能夠對數據庫的操做。

在咱們項目中,咱們把它全局集合在models文件中進行配置。index.js文件裏面操做了connectModels這兩個步驟。新建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})

複製代碼

九、PM2配置

PM2是能夠用於生產環境的Nodejs的進程管理工具,而且它內置一個負載均衡。它不只能夠保證服務不會中斷一直在線,而且提供0秒reload功能,還有其餘一系列進程管理、監控功能。而且使用起來很是簡單。pm2的官方文檔已經進行詳細的配置說明,在這裏就不進行一一簡述,主要講的時個人koa項目怎樣配合PM2進行相關管理或者說部署。PM2經常使用命令須要用的時候能夠進行查看,不必去背,用多就熟悉了。也能夠結合在package.json裏面,用自定義命令運行。咱們在package.jsonscript配置和初始化文件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,更多的配置若是有興趣的小夥伴能夠查看官方文檔的文檔說明。

十、路由配置

新建router文件,該目錄下存放路由基本配置,新建privatepublic兩個文件,安裝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文件中。這若是咱們接口一多,不會顯示得特別混亂。下面咱們就來說這個文件。

十一、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]

十一、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


複製代碼

十二、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"
  }
}

複製代碼

3、其餘

github地址:

github.com/bayi-lzp/ko… (star! star!star!)

技術棧:

koa二、mongoose

近期會更新:

typescript、redis、docker

相關文章
相關標籤/搜索