Node + MongoDB 通用 CRUD 介面實現筆記
釋出時間:2020-07-15 11:46:01
原發布地:
文章採用 CC BY-NC-SA 4.0 許可,請註明出處;商業轉載請聯絡作者授權。
目錄
近期在補習 + 的配合使用,最初目標是實現一個介面用於 CRUD 分類資料。專案繼續發展的時候發現除去分類資料以外,其他資料也需要用到幾乎一模一樣的介面。在路由中重複定義一大堆只有名字不同的介面對於學習用的小專案來說實在是有點沒有必要,因此嘗試學習了通用 CRUD 介面的實現。
原版定義
以分類為例, 模型定義,每個分類有自身的名稱,以及關聯的父級分類的 ObjectId:
const mongoose = require('mongoose'); const schema = new mongoose.Schema({ name: { type: String }, parent: { type: mongoose.SchemaTypes.ObjectId, ref: 'Category' }, // 型別為ID,關聯這個模型本身 }); module.exports = mongoose.model('Category', schema);
原介面:
const express = require('express'); const router = express.Router(); const Category = require('../../model/Category'); module.exports = (app) => { router.post('/categories', async (req, res) => { const model = await Category.create(req.body); res.send(model); }); router.put('/categories/:id', async (req, res) => { const model = await Category.findByIdAndUpdate(req.params.id, req.body); res.send(model); }); router.get('/categories', async (req, res) => { const items = await Category.find().populate('parent').limit(10); // populate 根據 parent 記憶體的 ID 同時查詢出 parent 作為物件返回 res.send(items); }); router.get('/categories/:id', async (req, res) => { const model = await Category.findById(req.params.id); res.send(model); }); router.delete('/categories/:id', async (req, res) => { const model = await Category.findByIdAndDelete(req.params.id); res.send(model); }); app.use('/admin/api', router); };
引入 Category
模型,針對定義在 /admin/api/categories
下的不同路徑和 HTTP 方法實現 CRUD。
目標
以類似 RESTful 風格實現:訪問 /api/categories
時使用 Category
模型進行操作;相對應訪問 /api/types
時使用 Type 模型進行操作;其他類推。
實現
基礎實現
首先為了與其他普通介面相區分避免誤操作對 URL 進行修改,將 app.use('/admin/api', router);
改為 app.use('/admin/api/rest/:resource', router);
;此處的 :resource
即為請求型別 (比如上文的 categories
其次為了在路由內可訪問自身 req.params.resource
(即獲取 :resource
引數) 修改 Router 建構函式選項:
const router = express.Router({ mergeParams: true });
以建立分類的路由為例,首先對路徑進行修改:
router.post('/', async (req, res) => {
const model = await Category.create(req.body);
res.send(model);
});
在這裡我們不能直接使用引入的 Category 模型,而是要根據 URL 動態獲取。通過 req.params.resource
既可以獲取到請求型別,此處的例子獲取到的就是訪問 /admin/api/rest/categories
對應的 categories
。
通過 這個包提供的 classify
方法將 categories
轉換為對應的模型名 Category
,進行動態的模型引入:
router.post('/', async (req, res) => {
const parseModelName = require('inflection').classify; // 定義 parseModelName 方法
const modelName = parseModelName(req.params.resource); // 小寫複數轉首字母大寫單數類名
const Model = require(`../../model/${modelName}`); // 動態引入對應的 Mongoose 模型
const model = await Model.create(req.body); // 建立資料
res.send(model);
});
中介軟體
通過以上的修改以及可以基本實現動態 CRUD 了,但是將動態引入模型的程式碼在每段路由設定路面複製一份顯然是很麻煩的,也不利於以後的修改,因此嘗試通過中介軟體引入模型。
首先依舊是通過 這個包提供的 classify
方法定義一個 parseModelName
方法:
const parseModelName = require('inflection').classify;
建立一個類名轉換中介軟體:
async function modelNameMiddleware(req, res, next) {
const modelName = parseModelName(req.params.resource); // 獲取模型名稱
req.Model = require(`../../model/${modelName}`); // 掛載 require 的 model 使其能在下一步中以 req.Model 直接使用
next();
}
注意這裡將動態引入的模型掛載到 req.Model
,這樣在下一個中介軟體 (也就是路由) 中就可以直接通過 req.Model
使用了。
最後修改路由定義:
router.post('/', async (req, res) => {
const model = await req.Model.create(req.body); // 直接通過 req.Model 使用模型
res.send(model);
});
app.use('/admin/api/rest/:resource', modelNameMiddleware, router); // 使用定義的中介軟體
完整程式碼
const express = require('express');
const router = express.Router({ mergeParams: true });
// mergeParams 使路由內可訪問自身 req.params.resource
// 小寫複數形式轉為單數大寫類名形式
const parseModelName = require('inflection').classify;
// 獲取類名轉換中介軟體
async function modelNameMiddleware(req, res, next) {
const modelName = parseModelName(req.params.resource);
// 掛載 require 的 model 使其能在下一步中以 req.Model 直接使用
req.Model = require(`../../model/${modelName}`);
next();
}
module.exports = (app) => {
router.post('/', async (req, res) => {
const model = await req.Model.create(req.body);
res.send(model);
});
router.put('/:id', async (req, res) => {
const model = await req.Model.findByIdAndUpdate(req.params.id, req.body);
res.send(model);
});
router.get('/', async (req, res) => {
const items = await req.Model.find().populate('parent').limit(10);
// populate 根據 parent 記憶體的 ID 同時查詢出 parent 作為物件返回
res.send(items);
});
router.get('/:id', async (req, res) => {
const model = await req.Model.findById(req.params.id);
res.send(model);
});
router.delete('/:id', async (req, res) => {
const model = await req.Model.findByIdAndDelete(req.params.id);
res.send(model);
});
app.use('/admin/api/rest/:resource', modelNameMiddleware, router);
};