【愣錘筆記】中高級前端進階之極速通關koa2全棧核心內容(圖文並茂)

隨着Node.js的橫空處世,原本目的是解決部分後端的問題,誰知道卻無心間給前端開發帶來了一場顛覆性的革命,今後前端拉開了現代化開發的序幕。現在,做爲前端開發,不管是想進階或是拓寬我的知識邊界,node.js早已經是前端必須掌握的了。拿下node.js,你還在猶豫什麼?javascript

Koa.js是基於node.js的一個開發框架,小巧靈活,對於一些中小型項目開發仍是比較友好的。Koa上手簡單,所以成爲了避免少小夥伴上手node開發的選擇之一。本文主要從如下幾個方面講解koa後端開發最核心的部份內容,讓人人都是全棧小能手:css

  • 安裝Koa2與啓動一個hello world服務
  • Koa的接口開發
  • 什麼是REST API?
  • 在Koa開發REST風格的API
  • 使用PostMan測試咱們的接口
  • 安裝MongoDb數據庫
  • MongoDb可視化工具的使用
  • 使用mongoose操做MongoDb數據庫
  • 對前端提交的密碼進行加密與解密
  • 在Koa中使用JWT作登陸鑑權
  • 過濾用戶提交的數據,防止XSS攻擊
  • 使用mocha進行同步、異步和接口的單元測試
  • 單元測試覆蓋率

安裝Koa2與啓動一個hello world服務

開發node後端須要安裝node.js環境和npm包管理工具,這塊就很少說了,相信如今的前端小夥伴基本都會的。首先建立一個文件文件夾做爲咱們的項目目錄:html

// 終端。建立koa-test並進入該目錄
mkdir koa-test
cd koa-test
複製代碼

緊接着,像咱們平時作前端項目同樣安裝koa庫:前端

// 終端:安裝koa2
cnpm i koa -S
複製代碼

根目錄下建立app.js做爲咱們程序的入口,就像咱們vue項目中的main.js:vue

// 引入Koa庫並初始化
const Koa = require('koa');
const app = new Koa();

// 啓動服務
// 監聽3000端口,就像vue中默認的是監聽8080同樣
// 固然了你也能夠監聽其餘端口
const appService = app.listen(3000, () => {
  console.log('[Koa]Server is starting at localhost:3000');
});

// 導出服務(是爲了供單測使用)
// 即便這裏不導出也正常能夠跑項目,後面會講解這裏爲何須要導出
module.exports = appService;
複製代碼

最後在終端啓動咱們的程序服務:java

// node是node.js的命令
node app.js
複製代碼

以下圖,在瀏覽器輸入localhost:3000,就能夠看咱們開啓的服務了:node

node server start success

koa路由(接口編寫)

關於接口,相信你們都不陌生。做爲前端開發,天天都會和後臺人員提供的接口打交道。下面,咱們看如何在Koa開發的服務中,開發供前端使用的接口吧!git

  • 開發接口,其實就是在Koa中寫路由,須要用到koa-router這個庫,就比如前端vue開發中的vue-router也是前端的路由同樣:
// 安裝路由庫
// -S是--save的簡寫,表示在生成環境中使用
// -D是--save-dev的簡寫,表示在開發環境中使用
cnpm i koa-router -S

// 對於前端的參數,咱們是須要獲取使用的
// get提交的參數咱們能夠輕鬆得到,
// 可是post的數據,咱們須要解析才能使用
// 所以須要安裝koa-bodyparser庫來處理post的數據
// 終端執行:
cnpm i koa-bodyparser -S
複製代碼
  • 掛載koa-bodyparser和api路由

出於標準,咱們須要將api相關的內容獨立出來。就像咱們vue項目開發中的src下也會分components、pages、api、assets等。這裏咱們在根目錄下建立api文件夾,用來存放咱們的路由文件,如圖:github

api

koa-test/api/index.js是咱們api模塊的出口,modules文件用來存放全部的API模塊,例如這裏有user相關接口都在user.js中等。具體的內容會放在後面細說。web

下面咱們看如何在app.js中掛載路由和其餘中間件:

// koa-test/app.js

// 引入koa-bodyparser用於解析post數據
const bodyParser = require('koa-bodyparser');

// 引入根目錄下的api路由
// 即把koa-test/api/index.js暴露出來的路由引入進來
const router = require('./api')

// app.js中掛載koa-bodyparser
// 注意:在路由掛載前先掛載 koa-bodyparser
app.use(bodyParser());

// 掛載路由
// 服務啓動後能夠在瀏覽器輸入localhost:3000看到提示
app.use(async ctx => ctx.body = '服務啓動成功');
app.use(router.routes());
app.use(router.allowedMethods());

// 省略上面的其餘代碼
複製代碼
  • 編寫api的index.js
// 引入koa-router
const Router = require('koa-router');

// 引入modules文件夾下的路由模塊
const articleRouter = require('./modules/articles');

// 實例化Router中間件
const router = new Router();

// 註冊路由
// 注意該路由模塊文件在註冊時增長了'/articles前綴
// 即該模塊下全部的接口地址都會以/articles做爲前綴
router.use('/articles', articleRouter.routes(), articleRouter.allowedMethods())

// 將註冊後的路由導出
// 供app.js中的koa掛載
module.exports = router;
複製代碼
  • 編寫具體的路由文件,eg:articles.js文件:
// 仍是須要先導入koa-router
const Router = require('koa-router');
// 實例化router
const router = new Router();

// 註冊get方法
// 能夠經過ctx.query獲取parse後的參數
// 或者經過ctx.queryString獲取序列化後的參數
router.get('/list', (ctx, next) => {
  ctx.body = {
        code: 200,
        data: [
            {
                id: 1,
                name: '小明',
                sex: 0,
                age: 22
            }
        ],
        message: 'ok'
    };
});

// 註冊post方法
// app.js中掛載koa-bodyparse中間件後,
// 能夠經過ctx.request.body獲取post參數
// eg:這裏的data就是前端post時提交的數據
router.post('/update', (ctx, next) => {
    let data = ctx.request.body
    ctx.body = {
        code: 200,
        data,
        message: 'ok'
    };
});

// 將該模塊的路由(api接口)暴露出去
// 供api/index.js路由註冊
module.exports = router;
複製代碼

這裏的ctx.body,就是返回給前端的json數據。

基本的路由編寫就到這了,固然了,實際業務開發中還會涉及到put、delete類型等等的接口。基本寫法都大同小異,這裏附上koa-router的官網文檔地址,查看更多的路由編寫細節把。

什麼是REST API?

引用網上的定義就是:

REST 指的是一組架構約束條件和原則。 知足這些約束條件和原則的應用程序或設計就是 RESTful

下面看如何定義rest風格的api接口:

// 獲取操做使用get:
// 例如:獲取所有文章
get /api/articles
// 帶搜索條件帶獲取文章(例如頁數、每頁條數、文章類型等等)
get /api/articles?page=1=pageSize=50&type=1
// 獲取id爲12345帶單條文章
get /api/articles/12345

// 資源分類,
// eg:獲取id爲12345的文章的評論
get /api/articles/12345/comments
// 獲取id爲12345的文章的帶搜索條件的評論
get /api/articles/12345/comments?page=1&pageSize=50

// 提交數據使用post類型:
// 建立文章
post /api/articles

// 更新數據使用put類型:
// 例如:更新id爲12345的文章內容
put /api/articles/12345

// 刪除id爲12345的文章
delete /api/articles/12345
複製代碼

REST風格的API編寫能夠參考廖雪峯大大的這篇 編寫REST API 文章

在Koa開發REST風格的API

在Koa中開發REST風格的API也很簡單,koa-router爲咱們的ctx對象提供了params對象,能夠獲取REST風格的API中的參數。很像vue-router中的動態路由有木有?

router.get('/user/:userId', async ctx => {
    // 獲取動態路由的參數
    // 經過koa-router提供的ctx.params對象獲取
    const id = ctx.params.userId
    // 省略其餘代碼
}
複製代碼

使用PostMan測試咱們的接口

咱們在開發接口過程當中,確定須要測試咱們寫的接口正不正確,有木有按照預期返回結果。那麼怎麼訪問咱們的接口查看是否正確呢?最簡單的確定有那麼一款工具完,咱們直接在上面操做就行了,呢~~以下,能夠安裝postman使用:

postman

這樣的話,咱們能夠建立接口來訪問咱們寫的接口服務,還能夠攜帶各類參數,調試起來仍是很是方便的,這個就很少說了,網上搜postman下載就能夠了,免費開源的。

安裝MongoDb數據庫

OK,上面說完了編寫接口,對應的確定須要咱們操做數據庫,而後給前端返回數據,例如基本的增刪改查呀。下面先看基本mongodb數據的安裝吧,這裏以Mac OS爲列:

這裏介紹的是在終端用curl的方式安裝的(顯示騷騷的~~),其實直接到官網下載mongo的安裝包也是同樣的:

  • (很騷氣滴)安裝mongoDb
# 進入 /usr/local
cd /usr/local

# 下載
sudo curl -O https://fastdl.mongodb.org/osx/mongodb-osx-ssl-x86_64-4.0.9.tgz

# 解壓
sudo tar -zxvf mongodb-osx-ssl-x86_64-4.0.9.tgz

# 重命名爲 mongodb 目錄
sudo mv mongodb-osx-x86_64-4.0.9/ mongodb

// 添加環境變量
export PATH=/usr/local/mongodb/bin:$PATH

// 新建一個數據庫存儲目錄
sudo mkdir -p /data/db

// 啓動mongod
sudo mongod
複製代碼

mongodb的安裝過程很簡單,就不贅述了,更多的安裝方法能夠參考 mongoDb 安裝參考地址

MongoDb可視化工具的使用

mongodb的可視化工具,我這裏推薦的是Studio 3T,能夠很方便的鏈接數據庫,查看數據庫的內容,或者操做數據庫等。

最後這裏附上Robo3的下載安裝地址,安裝很簡單,就像裝個qq同樣,不贅述了。

mongoose操做MongoDb數據庫

在node中,咱們基本上是使用mongoose鏈接、操做數據庫。首先,咱們須要安裝mongoose:

// 安裝mongoose
cnpm i mongoose -S
複製代碼

然後,在文件根目錄下新建databse文件夾,用來專門放置鏈接和操做數據相關的文件:

database/index.js中,咱們用來寫鏈接數據庫的方法,最後將其導出供app.js中鏈接使用:

// 引入mongoose庫
const mongoose = require('mongoose');

// 定義數據庫地址的常量
// 更標準的能夠新建一個數據配置文件,
// 用來專門存放數據相關的配置,好比帳號密碼等等
const DB_ADDRESS = 'mongodb://localhost/koa-test';

mongoose.Promise = global.Promise;
mongoose.set('useCreateIndex', true);

// 簡單封裝log
const log = console.log.bind(console);

// 定義鏈接函數
const connect = () => {
  // 重連次數
  let connectTimes = 0;
  // 設置最大重連次數
  const MAX_CONNECT_TIMES = 3;

  // 斷線重連
  const reconnectDB = (resolve, reject) => {
    if (connectTimes < MAX_CONNECT_TIMES) {
      connectTimes++;
      mongoose.connect(DB_ADDRESS, connectConfig);
    } else {
      log('[mongodb] database connect fail!');
      reject();
    }
  }

  // 鏈接數據庫
  mongoose.connect(DB_ADDRESS, connectConfig);

  return new Promise((resolve, reject) => {
    // 監聽數據庫斷開,從新鏈接
    mongoose.connection.on('disconnected', () => {
      reconnectDB(reject);
    });
    // 監聽數據庫鏈接出錯,從新鏈接
    mongoose.connection.on('error', err => {
      log(err);
      reconnectDB(reject);
    });
    // 監聽鏈接成功
    mongoose.connection.on('open', () => {
      log('[mongodb server] database connect success!');
      resolve();
    });
  });
};

// 暴露出去
exports.connect = connect;

// 還須要引入schema,在下面演示
// ……
複製代碼

這裏主要的做用就是:

(1)經過mongoose.connect()方法鏈接數據庫;  

(2)監聽disconnected和error事件,進行數據庫重連,而且最多重連三次;

(3)返回promise來告知鏈接成功與否
複製代碼
  • 引入全部的schema

經過以前的文件夾截圖能夠看出,咱們建立了schema文件夾,是用來存放全部數據庫建模相關的內容,其實就是經過schema來對數據庫進行操做的。下面看下如何導入咱們全部的schema下的文件的(雖然一個個引入也是能夠的,可是咱們是有追求的程序猿~~):

在databse/index.js:

// 引入glob
const glob = require('glob');

// 引入弄的的path方法
// 能夠讀取、解析、拼接路徑等等
const path = require('path');

// 暴露一個initSchemas方法
// 用於導入database/schema文件夾下全部schema
exports.initSchemas = () => {
    // 經過glob讀取schema文件夾下內容
    glob.sync(path.resolve(__dirname, './schema/', '**/*.js')).forEach(require);
}
複製代碼

不清楚glob用法的,這裏附上glob文檔地址

更多mongoose的內容能夠查看mongoose中文文檔地址

  • 簡單說下path模塊經常使用的方法:

path是node提供的一個模塊,主要用來處理和路徑相關的內容:

// path使用前,仍是須要先導入
const path = require('path');

// join方法能夠將全部參數鏈接起來,返回一個路徑
path.join() 
// eg:
path.join('a', 'b', 'c', 'd'); // a/b/c/d
path.join(__dirname, '/a', '//b', '///c', 'd'); // /Users/yoreirei/Documents/demo/node-demo/a/b/c/d
path.join(__dirname, 'a', 'b', '../c', 'd'); // /Users/yoreirei/Documents/demo/node-demo/a/c/d
path.join(__dirname, 'a', './b', './c', './d'); // /Users/yoreirei/Documents/demo/node-demo/a/b/c/d

// parse方法將路徑解析爲一個路徑對象
path.parse()
// eg:
path.parse(path1) // { root: '', dir: 'a/b/c', base: 'd', ext: '', name: 'd' }
path.parse(path2) // { root: '/', dir: '/Users/yoreirei/Documents/demo/node-demo/a/b/c', base: 'd', ext: '', name: 'd' }

// format方法將路徑對象轉換成路徑地址
path.format(parse1) // a/b/c/d
複製代碼

注意:__dirname獲取的是當前文件模塊所在的絕對路徑。這個前端小夥伴在vue-cli的entry應該看到過,很熟悉吧。

拓展來一下,OK,咱們繼續schema建模。

  • Schema建模,經過schema操做數據庫

// database/schema/User.js

// 引入mongoose
const mongoose = require('mongoose');
// 獲取mongoose.Schema方法用於建模
const { Schema } = mongoose;

// 生成id
let ObjectId = Schema.Types.ObjectId;

// 建立用戶的schema
// 例如建立一個包含用戶名、密碼、建立時間、
// 最後登陸時間、點贊內容、收藏內容的schema
const userSchema = new Schema({
  UserId: ObjectId,
  // 咱們能夠定義每一個字段的類型,例如String、Number、Array等等
  // 能夠定義該字段的值是否惟一,若是設置了惟一,
  // 那麼後續插入相同的值時就會報錯
  userName: {
    unique: true,
    type: String
  },
  password: String,
  likes: {
    type: Array,
    default: []
  },
  collect: {
    type: Array,
    default: []
  }
}, {
    // 加入該配置項,會自動生成建立時間
    // 在文檔更新時,也會自動更新時間
    timestamps: {
        createdAt: 'createdAt',
        updatedAt: 'updatedAt'
    }
});

// 最後,使用mongoose發佈模型
mongoose.model('User', userSchema);
複製代碼

使用schema建模就是這麼簡單,小夥伴能夠本身擴展建立其餘schema。這個操做其實就相似於其餘數據中的建表。

對前端提交的密碼進行加密與解密

基本上咱們的服務中都會涉及到用戶的註冊和登陸等等。而對於用戶註冊的密碼,咱們是不會明文保存的,這樣是不安全的。通常的作法都是對明文密碼進行加密後存儲,而用戶登陸時再對用戶的密碼加密後後和數據庫中加密過的密碼進行比對,看是否正確。而前端常見的也能夠在用戶提交時進行md5等方式的加密提交。

關於加密,咱們可使用bcript對密碼的加密與解密。

密碼

  • 加密

咱們須要對用戶註冊時的密碼進行加密,使其不可逆。首先,咱們須要安裝bcript庫:

// 安裝bcript
cnpm i bcript -S
複製代碼

database/schema/User.js

// 引入bcript
const bcrypt = require('bcrypt');

// 定義bcrip加密時的配置常量
const SALT_ROUNDS = 10;

// 每次保存時進行密碼加密
// 注意此處pre的第二個參數,不能是箭頭函數,否則拿不到this
userSchema.pre('save', function(next) {
  bcrypt.genSalt(SALT_ROUNDS, (err, salt) => {
    if (err) return next(err);
    bcrypt.hash(this.password, salt, (err, hash) => {
      if (err) return next(err);
      // 將用戶提交的密碼替換成加密後的hash
      this.password = hash;
      next();
    });
  });
});
複製代碼

注意:咱們這裏的加密作法時,在用戶建模的時候,監聽save事件,即用戶每次存儲數據的時候,都會執行咱們定義的回調,而咱們就在回調的函數中進行加密的操做。

  • 解密

驗證用戶登陸的密碼時,咱們須要拿到用戶的密碼而後經過bcript驗證是否和加密後的數據同樣。

database/schema/User.js:

// 定義userSchema的實例方法
// 解密user password
// 注意mehtod要加s
userSchema.methods = {
    // 定義一個對比密碼是否正確的方法
    // userPassword用戶提交的密碼
    // passwordHash數據庫查出來的加過密的密碼
    comparePassword (userPassword, passwordHash) {
        return new Promise((resolve, reject) => {
            bcrypt.compare(userPassword, passwordHash, (err, res) => {
                // 驗證完成
                // res值爲false|true,表示密碼不一樣/相同
                if (!err) return resolve(res);
                // 驗證出錯
                return reject(err);
            });
        });
    }
}
複製代碼

注意:咱們這裏的作法是給schema增長一個實例方法,那麼咱們(例如編寫登陸接口,那麼用戶的密碼後)經過調用schema的實例去比對密碼是否正確。

  • 簡單演示一下login接口(去掉了參數驗證和JWT)
/** * 用戶登陸 * @param { String } userName 用戶名 * @param { String } password 密碼 */
router.post('/login', async ctx => {
  // 前提引入mongoose
  // 獲取User集合(相似於其餘數據的表)
  const userModal = mongoose.model('User');
  // 集合的實例
  const userInstance = new userModal();
  // 定義查詢參數
  const query = { userName: data.userName };
  // 先查找用戶是否存在
  await userModal.findOne(query).exec()
    .then(async res => {
      // 用戶存在,拿到用戶數據
      // 調用集合的實例方法,比對密碼是否正確
      // then回調錶示驗證操做完成
      // 經過返回的參數isMatch(true/false)表示驗證是否正確
      await userInstance.comparePassword(data.password, res.password).then((isMatch) => {
        // 驗證密碼是否正確
        if (isMatch) {
            // 此處省略token生成,會在後面講解
            // *****
            return ctx.body = {
                code: 200,
                message: 'ok'
            };
        }
        return ctx.body = {
          code: 400,
          message: '帳號密碼錯誤'
        };
      }).catch(() => {
        return ctx.body = {
          code: 500,
          message: error.message
        };
      })
    // 用戶不存在,直接提示
    }).catch(() => {
      return ctx.body = {
        code: 400,
        data: null,
        message: '當前用戶不存在'
      };
    });
});
複製代碼

關於bcript的內容能夠參考 bcript的npm文檔地址

其實關於登陸這一塊,咱們是須要作登陸鑑權的,好比是否過時等等,再複雜一些還會有redis持久化等等。這裏省略了JWT登陸鑑權,下面會介紹。

在Koa中使用JWT作登陸鑑權

jwt是經常使用的用戶登陸鑑權方式:

(1)前端經過登陸接口拿到token,存到本地,前端在後續的增刪改查的時候會在請求頭攜帶token。

(2)後端會根據請求時攜帶的authorization(即用戶token),判斷用戶是否登陸過時(統一攔截),登陸過時則返回401,或者判斷當前用戶是否有權限進行此操做。

在koa2中使用jwt,要提到兩個中間件:

(1)jsonwebtoken 生成和解析token

(2)koa-jwt 攔截(所有/部分)用戶請求並驗證token

  • 生成token

首先安裝jsonwebtoken:

// 安裝jsonwebtoken
cnpm i jsonwebtoken -S
複製代碼

api/modules/user.js中

const { createToken } = require('../../utils/account');
// 根據上面的登陸接口,在用戶帳號密碼查詢正確後
// 生成token返回給前端,createToken方法日後看
const token = createToken(res)
return ctx.body = {
    code: 200,
    data: token,
    message: 'ok'
};
複製代碼

根目錄下新建utils文件夾,

utils/account.js

// 引入jsonwebtoken
const JWT = require('jsonwebtoken');

// 自定義生成token的密鑰(隨意定義的字符串)
// 就其安全性而言,不能暴露給前端,否則就能夠隨意拿到token
const JWT_SECRET = 'system-user-token';

// 生成JWT Token
// 同時能夠設置過時時間
exports.createToken = (config = {}, expiresIn = '7 days') => {
  const { userName, _id } = config;
  const options = { userName, _id };
  const custom = { expiresIn };
  // 經過配置參數,而後調用JWT.sign方法就會生成token
  return JWT.sign(options, JWT_SECRET, custom);
};

// 暴露出密鑰
// 這裏將密鑰暴露出去是爲了後面驗證的時候會用到
// 爲了統一,不用到處寫'system-user-token'這個字符串而已
exports.JWT_SECRET = JWT_SECRET;
複製代碼
  • 請求攔截驗證token

如今完成了登陸接口生成token問題,那麼在用戶請求的時候,咱們還須要攔截用戶請求並驗證是否過時。下固然就是koa-jwt上場了:

首先安裝:

// 安裝koa-jwt
cnpm i koa-jwt -S
複製代碼

app.js中作統一攔截,並設置不須要token的接口(例如登陸、註冊等接口):

// 引入jwt
const jwt = require('koa-jwt');
// 拿到咱們的密鑰字符串
const { JWT_SECRET } = require('./utils/account');

// jwt驗證錯誤的處理
// jwt會對驗證不經過的路由返回401狀態碼
// 咱們經過koa攔截錯誤,並對狀態碼爲401的返回無權限的提示
// 注意:須要放在jwt中間件掛載以前
app.use(function(ctx, next){
  return next().catch((err) => {
    if (401 == err.status) {
      ctx.status = 401;
      ctx.body = {
        code: 401,
        message: '暫無權限'
      };
    } else {
      throw err;
    }
  });
});

// 掛載jwt中間件
// secret參數是用於驗證的密鑰
// unless方法,設置不須要token的接口
app.use(
    jwt({ secret: JWT_SECRET }).unless({
        path: [
            '/login',
            '/register'
        ]
    })
);
複製代碼

其實,koa-jwt是封裝了koa-lessjsonwebtoken這兩個中間件,。

koa-less的做用是隻有在koa-less參數不匹配的時候才執行前面的中間件。 jsonwebtoken就是上面咱們用的用於生成和解析token的。

看到這,小夥伴是否是想說:

wtf

  • 解析token

在不少時候咱們須要拿到前端攜帶的token,從token中獲取用戶相關的信息,而後再作某些事情。例如,拿到用戶的token後,根據token解析出用戶的id,而後根據id查詢用戶存在後再執行某些操做。

api/article.js接口文件,這裏的發佈文章接口,咱們在拿到用戶提交的數據時,根據header中的authorization讀取到用戶的信息:

// 省略了部分代碼
const Router = require('koa-router');
const mongoose = require('mongoose');
// 先導入咱們封裝的兩個方法
const { decodeToken, parseAuth } = require('../../utils/account');

// 定義文章發佈接口
router.post('/article', async ctx => {
  let data = ctx.request.body;

  // 省略參數驗證部分
  // ****
  
  // 解析出用戶token
  const authorization = parseAuth(ctx);
  // 根據token解析出token中的用戶_id
  const tokenDecoded = decodeToken(authorization);
  const { _id } = tokenDecoded;
  
  const userModal = mongoose.model('User');
  // 先查詢用戶是否存在,拿到用戶信息
  await userModal.findById(_id).exec()
    // 在用戶查到後,再進行建立文章的操做
    .then(async res => {
      data.author = res.userName
      const articleModal = mongoose.model('Article');
      const newArticle = articleModal(data);
      // 存儲文章數據
      await newArticle.save()
        .then(() => {
          return ctx.body = {
            code: 200,
            message: '保存成功'
          };
        }).catch(error => {
          return ctx.body = {
            code: 500,
            message: error.message || '保存失敗'
          };
        });
    })
    .catch(err => {
      return ctx.body = {
        code: 500,
        message: err.message || '用戶不存在'
      }
    })
});

module.exports = router;
複製代碼

utils/account.js:

const JWT = require('jsonwebtoken');

// 從ctx中解析authorization
exports.parseAuth = ctx => {
  if (!ctx || !ctx.header.authorization) return null;
  const parts = ctx.header.authorization.split(' ');
  if (parts.length < 2) return null;
  return parts[1];
}

// 解析JWT Token
exports.decodeToken = (token) => {
  return JWT.decode(token);
};
複製代碼

注意: (1)主要思路就是,咱們經過用戶請求的header中攜帶的authorization,解析出用戶的_id,而後經過_id去數據庫查出用戶對應的信息(項目大的話,這裏會使用例如redis的緩存技術去讀取用戶的信息,而不是每次直接操做數據庫),而後再建立文章。

(2)解析authorization的方法很簡單,即便直接調用jsonwebtoken這個庫的decode方法便可,相關介紹官網都有說明。

(3)關於怎麼拿到authorization這裏要說明一點,其實咱們拿到的是下面這種數據,它其實包含了兩部分,因此咱們須要解析出咱們想要的內容(即空格後面的內容):

authorization

解析方法也很簡單,就是根據空格分割,取後面那部分。

過濾用戶數據,防止XSS攻擊

上面提到了發佈用戶數據,那麼對於用戶提交的數據,若是不作過濾,用戶極可能傳一些<script>alert('動態注入js')</script>的惡意代碼。

針對這種狀況,咱們能夠對用戶的內容進行過濾,對一些關鍵字符進行轉譯。

// 安裝xss庫
cnpm i xss -S

// 使用,在咱們存文章數據的schema中進行處理
// 在每次存的時候進行過濾
// schema/Article.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const xss = require('xss');

const articleSchema = new Schema({
    // 省略部份內容
});

// 每次保存時進行密碼加密
// 注意此處pre的第二個參數,不能是箭頭函數,否則拿不到this
articleSchema.pre('save', function(next) {
    // 對標題和內容進行xss過濾
    this.title = xss(this.title);
    this.content = xss(this.content);
    next();
});

mongoose.model('Article', articleSchema);
複製代碼

以下圖,能夠看到過濾先後的數據庫數據對比:

對比

更多關於xss的內容可查看xss 中文文檔

注意:若是是mySql等關係型數據庫,咱們還會處理防止sql的注入等操做,關於這塊,有興趣的小夥伴能夠自行研究研究。

使用mocha進行同步、異步和接口的單元測試

單元測試常見的風格有:行爲驅動開發(BDD),測試驅動開發(TDD)。那麼二者有什麼區別呢?

(1)BDD關注的是整個系統的最終實現是否和用戶指望一致。

(2)TDD關注的是取得快速反饋,使全部功能都是可用的。

  • 使用Mocha進行單元測試

首先安裝:

// 安裝
cnpm i mocha -D
複製代碼

配置npm測試命令:

// 配置npm script命令
"scripts": {
    "test": "mocha"
}
複製代碼

新建text/test.js,寫測試代碼:

// 引入node的斷言庫
// 其實就像是一個工具庫
const assert = require('assert');
// 引入待測試文件
const lib = require('./index');

// 測試iterate這個函數的功能
// describe定義測試的描述
// it定義打印的內容
describe('Math', () => {
  describe('#iterate', () => {
    it('should return 10', () => {
      // 經過斷言判斷是否經過測試
      assert.equal('10', lib.iterate(5, 5));
    });
    it('should return 0', () => {
      assert.equal('0', lib.iterate());
    });
    it('should return 10', () => {
      assert.equal('10', lib.iterate(1, 1));
    });
  });
});

// test/index.js
const iterate = (...arg) => {
  if (!arg.length) return 0;
  return arg.reduce((val, cur) => val + cur);
};

module.exports = {
  iterate
}

// 終端運行
// 會自動查找test文件夾下全部的測試文件並執行測試
npm test
複製代碼

能夠看出,當結果不符合預期時會測試不經過:

test result

更多內容能夠參考 mocha 文檔地址

  • 使用should斷言庫

上面的斷言庫咱們使用的是node提供的asset斷言模塊,其實我還有不少其餘選擇,例如should.js和chai等等,由於這些庫提供了比asset更爲豐富的供。

這裏演示一些should這個斷言庫:

// 安裝should
cnpm i should -D

// 引入
const should = require('should');

// 使用(和上面node的相似)
require('should');
const lib = require('./index');

/** * 單元測試 */
describe('Math', () => {
    describe('#iterate', () => {
        it('should return 10', () => {
            lib.iterate(5, 5).should.be.equal(10);
        });
        it('should return 0', () => {
            lib.iterate().should.be.equal(0);
        });
        it('should return 10', () => {
            lib.iterate(1, 1).should.be.equal(10);
        });
    });
});
複製代碼

should斷言庫的內容比node的assert斷言功能更豐富。斷言效果以下:

should result

更多內容能夠查看 shuold文檔地址

到目前來看,chai其實更爲流行一些,chai同時提供了TDD和BDD風格的用法。有興趣的小夥伴能夠翻閱其文檔學習查看,基本用法也都大同小異吧。

  • 使用mocha異步測試

以上演示都是一些同步測試,那麼mocha對異步的測試該怎麼作呢?其實很簡單,只須要手動調用一個回調函數:

// 待測試文件
// 模擬定義普通的一個異步函數
const asyncFunc = (cb) => {
    setTimeout(() => {
        console.log('async init after 1000')
        cb()
    }, 1000)
}

// 測試代碼
// PS:省略導入導出的操做了

// 測試普通異步函數
describe('Async', () => {
    describe('#asyncFunc', () => {
        it('should an async function', done => {
            // eg:最關鍵的是在異步調用完成後,
            // 須要手動調用回調函數done,
            // 來告訴mocha異步調用完成
            asyncTest.asyncFunc(done)
        })
    })
});
複製代碼

效果以下:

異步測試

  • 對於須要異步調用的狀況
// async異步函數的測試
// 模擬一個異步函數:
const getInfo = async (bool) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // 若是參數是true,則成功返回
            // 不然返回失敗
            if (bool) return resolve('success');
            return reject('fail');
        }, 1000);
    });
};

// 測試
describe('Async', () => {
    describe('#getInfo', () => {
        it('should return success', done => {
            // 仍是須要手動調用回調函數
            // async函數須要寫在內部
            (async function () {
                try {
                    // 等待異步完成後,手動調用done()
                    await asyncTest.getInfo(true);
                    done();
                } catch (error) {
                    done(error)
                }
            })();
        });
    });
})
複製代碼

效果以下:

test success

若是 await asyncTest.getInfo(true);參數傳入false,模擬測試不經過的效果,會看到下這個樣子:

test error

  • 更簡單的寫法,能夠直接將it的回調第二個參數寫成異步函數:
// 效果也是同樣的
describe('#getInfo', () => {
    it('should return success', async () => {
      await asyncTest.getInfo(false);
    });
});
複製代碼
  • 接口的單元測試

首先要安裝supertest這個庫

// 安裝
cnpm i supertest -D
複製代碼

測試內容:

// 導入咱們的服務
// 前提是在app.js文件中,將啓動後的服務導出
//eg: moudle.exports = app.listen(3000)
const app = require('../app');
// 導入用於接口的測試的supertest
const request = require('supertest');

describe('GET /', () => {
  it('should return status with 200', (done) => {
    // 測試
    // get是測試get請求
    // expect是指望的內容
    request(app)
        .get('/')
        .expect(200)
        .end((err, res) => {
            // 在end中獲得接口的內容
            // 而後根據狀況手動調用done
            if (err) return done(err);
            done();
        });
    });
});
複製代碼

測試結果經過,以下圖:

api test success

// 文章詳情接口須要authorization
// 咱們的測試用例但願返回200狀態碼,可是返回了401
// 因此當前測試不經過
describe('GET /artiles/:id', () => {
    it('should an article info', (done) => {
        request(app)
        .get('/api/articles/5d2edc370fddf68b438b6b53')
        .expect(200, done);
    })
})
複製代碼

fail result

更多內容能夠參考 supertest文檔地址

單元測試覆蓋率

關於單元測試覆蓋率,簡單提一下,能夠測試出咱們的測試代碼的覆蓋狀況是怎樣的。

  • 安裝
// 首先安裝istanbul這個庫
cnpm i istanbul -D
複製代碼
  • 使用
// 在test文件夾下,打開終端執行如下命令
 // 會測試index.js文件的代碼測試覆蓋狀況
 istanbul cover index.js
複製代碼

如圖所示,咱們能夠看到8塊代碼覆蓋了3個,2個分支可是一個都沒有覆蓋到,0個函數,6行代碼覆蓋了個,以及對應的覆蓋率是多少。

istanbul cover result

同時,在test文件夾下能夠看到生成了coverage文件夾,裏面保存了覆蓋率結果,能夠點擊index.html查看結果。

index.html能夠看的更直觀:

更多的內容參考istanbul文檔吧。

好了,關於Koa2與mongodb的內容,就到這了,二期初步打算擴展如下關於Koa的內容:

  • 拓展:Koa安裝mySql數據庫
  • 擴展:Koa中mySql的基本操做(增刪改查),作個curl仔
  • 擴展:mySql數據庫的可視化工具

參考文獻


示例代碼同步在github,歡迎訪問!

百尺竿頭、日進一步 我是愣錘,歡迎交流與分享

相關文章
相關標籤/搜索