本文是博主總結了以前的本身在作的不少個項目的一些知識點,固然我在這裏不會過多的講解業務的流程,而是創建一個小demon,旨在幫助你們去更加高效 更加便捷的生成本身的node後臺接口項目,本文底部提供了一個 藍圖,歡迎你們下載,start,實際上,這樣的一套思路打下來,基本上就已經創建手擼了一個nodejs框架出來了。大多數框架基本上都是這樣構建出來的,底層的Node 第二層的KOA 或者express,第三層就是各類第三方包的加持。前端
注意:本文略長,我分了兩個章節node
本文寫了一個功能比較齊全的博客後臺管理系統,用來演示這些工具的使用,源代碼已經分章節的放在了github之中,連接在文章底部laravel
歡迎各位大牛指教,若有不足望諒解,這裏只是提供了一個從express過渡到其它框架的文章,實際上,這篇文章所介紹的工具,也僅僅是工具啦,若是是真實開發項目,咱們可能更加青睞於選擇一個成熟穩定的框架,好比AdonisJS(Node版的laravel) ,NestJS(Node版的spring),EggJS.....,我更推薦NestJS,博主後期會出一些Nest教學博文,歡迎關注git
至於選擇Nest緣由以下程序員
不少時候你們作爲 高技術人才(程序猿單身狗),最忌諱的事情就是什麼都是還不清楚的狀況下就去,吧唧的敲代碼,就從我的的經驗來談,思路這種東西真的很是很是的重要github
通常來說,咱們能夠從兩個角度來看架構的設計,一個是數據,一個http報文(res,req)web
第一步,咱們拿到一個需求,要作的第一件的事情就是分析數據創建模型
第二步,仔細的分析數據的扭轉(以下這裏假設了這樣的一種)spring
用戶點點擊文章的時候,咱們能進行數據的聯合查詢,而且把查詢的數據返回給回去mongodb
開發後臺的時候,對於一個有追求的工程師來講,兩者的完美結合纔是咱們不變的追求,數據庫
更快,更高效,更穩定
咱們嚴格約定:Aritcle (庫) => (對應的接口)articles
咱們這裏有一些約定是必需要遵照的,我認爲在工做中,若是遵照這些規範,能夠方便後續的各類業務的操做
嚴格要求數據庫是單數並且首字母的大寫形式
嚴格要求請求的api接口是小寫的複數形式
Aritcle (庫) => (對應的接口)articles
好了,有了前面的約定還有理論,如今咱們來實操
以上咱們詳細的說明了各個數據之間的關聯操做
具體的代碼實現,這裏講解了如何在mongoose中進行多表(集合)關聯
const mongoose = require('mongoose') const schema = new mongoose.Schema({ name:{type:String}, thumbnails:{type:String}, url:{type:String} }) module.exports = mongoose.model('Ad',schema)
如下的代碼大多都是大同小異,咱們只列出來Schema規則
const schema = new mongoose.Schema({ username:{type:String}, passowrd:{type:String} })
const schema = new mongoose.Schema({ title:{type:String}, thumbnails:{type:String}, body:{type:String}, hot:{type:Number}, // 建立時間與更新時間 createTime: { type: Date, default: Date.now }, updateTime: { type: Date, default: Date.now } // 一篇文章可能同屬於多個分類之下 category:[{type:mongoose.SchemaTypes.ObjectId,ref:'Category'}], },{ versionKey: false,//這個是表示是否自動的生成__v默認的ture表示生成 // 這個就能作到自動管理時間了,很是的方面 timestamps: { createdAt: 'createTime', updatedAt: 'updateTime' } })
const schema = new mongoose.Schema({ iamge:{type:String}, name:{type:String}, body:{type:String}, })
const schema = new mongoose.Schema({ title:{type:String}, thumbanils:{type:String}, //父分類,一篇文章,咱們假設一個文章能有一個父分類,一個欄目(書籍) parent:{type:mongoose.SchemaTypes.ObjectId,ref:'Category'}, book:{type:mongoose.SchemaTypes.ObjectId,ref:'Book'} })
const schema = new mongoose.Schema({ body:{type:String}, isPublic:{type:Boolean} })
他們的模型在這個文件夾下
咱們所有使用REST風格接口
REST全稱是Representational State Transfer,中文意思是表述(編者注:一般譯爲表徵)性狀態轉移
大白話說就是一種API接口編寫的規範,固然了這裏不詳細的展開敘述,咱們來看看有用的
下面的代碼就用到了一些經常使用的RES風格
請不要關注具體的業務邏輯,咱們的總店是請求的接口的編寫
// 單一個的post不帶參數就是表示----> 增 (往資源裏面增長些什麼) router.post('/api/articles', async(req, res) => { const model = await Article.create(req.body) // console.log(Article); res.send(model) }) // 單一個get不帶參數表示-------> 查 (把資源裏的都查出來) router.get('/api/articles', async(req, res) => { const queryOptions = {} if (Article.modelName === 'Category') { queryOptions.populate = 'parent' } const items = await Article.find().setOptions(queryOptions).limit(10) res.send(items) }) //get帶參數表示-------> 指定條件的查 router.get('/api/articles/:id', async(req, res) => { //咱們的req.orane裏面就又東 console.log(req.params.id); const items = await Article.findById(req.params.id) res.send(items) }) // put帶參數表示-------> 更新某個指定的資源數據 router.put('/api/articles/:id', async(req, res) => { const items = await Article.findByIdAndUpdate(req.params.id, req.body) res.send(items) }) // deldete帶參數表示------> 刪除指定的資源數據 router.delete('/api/articles/:id', async(req, res) => { await Article.findByIdAndDelete(req.params.id, req.body) res.send({ sucees: true }) })
咱們約定,返回信息的格式res.status(200).send({ message: '刪除成功' })
咱們都知道,再有些狀況下,咱們的獲得的一些結果是差不太多的,有時候,咱們但願獲得一些格式上統一的數據,這樣就能大大的簡化前端的操做。作爲一名優秀的有節操的後臺程序員,咱們應該與前端約定一些數據的統一返回格式,這樣就能大大的加快,大大的簡化項目的開發
好比我習慣把一些操做的數據統一一個格式發出去
注意:我指的統一,是指沒有實際的數據庫訊息返回的時候,若是有數據,就老老實實返回對應的數據就行了
咱們返回這樣的數據
res.status(200).send({ message: '刪除成功' })
// 程序設計的一個概念:中斷條件 if (!user) { return res.status(400).send({ message: '刪除失敗' }) }
if (!user) { return res.status(400).send({ message: '用戶不存在' }) }
以上res.status(400).send({ message: '用戶不存在' })就是咱們的約定
中間件約定方案:咱們約定一個規則去搭建咱們的中間件
假設咱們有一個訪問文章詳情的接口,獲取的這個數據,須要有文章詳情body,文章的tabs,上一篇 下一篇是否存在(也就是判斷數據庫中,文章以前是否還有文章)
// 文章詳情頁,不要關注具體的業務,我這裏想表達的是。若是是多箇中間件,咱們就用【】括起來,並且咱們嚴格要求全部中間件處理以後若是有接口都必須放在req上,這樣咱們後續就能夠很是方便的拿中間件處理的數據了,req對象,再整個node中,還有一個角色(第三方),能夠用來作數據的扭轉的工具 articleApp.get('/:id', [article.getArticleById, article.getTabs, article.getPrev, article.getNext, category.getList, auth.getUser], (req, res) => { let { article, categories, tabs, prev, next, user } = req res.send( { res:{ // 若是key和value同樣咱們能夠忽略掉 article:article, categories:categories, tabs, prev, next, user } } ) })
咱們程序執行的時候,可能回報錯,可是咱們但願給用戶友好的提示,而不是直接給除報錯信息,那麼咱們能夠這樣的來作,定義一個統一的錯誤處理中間件
注意啊,因爲是總體的錯誤處理中間件,因而咱們把整個東西放在main中的app下就行了全局的use一下,捕獲全局的錯誤
// 錯誤處理中間件,統一的處理咱們http-assart拋出的錯誤 app.use(async (err,req,res,next)=>{ // 具體的捕獲到信息是err中,再服務器爲了排查錯誤,咱們打印出來 consel.log(err) res.status(500).send({ message:'服務器除問題了~~~請等待修復' }) })
以上就是咱們的第一部分的所有內容
至此咱們項目的文件夾以下
這裏介紹了一個很是好用的接口測試工具RESTClinet
/.http
@uri = http://127.0.0.1:3333/api ### 接口測試 GET {{uri}}/test ### 獲取JSON數據 GET {{uri}}/getjson ### 後去六位數驗證碼 GET {{uri}}/getcode ###### 正式的對數據庫操做 ######### ### 驗證用戶是否存在 GET {{uri}}/validataName/bmlaoli ### 增:====> 實現用戶註冊 POST {{uri}}/doRegister Content-Type: application/json { "name":"123123", "gender":"男", "isDelete":"true" } ### 刪:====> 根據id進行數據庫的某一項刪除 DELETE {{uri}}/deletes/9 ### 改:====> 根據id修改某個數據的具體的值 PATCH {{uri}}/changedata/7 Content-Type: application/json { "name":"李仕增", "gender":"男", "isDelete":"true" } ### 查: =====> 獲取最真實的數據 GET {{uri}}/getalldata ### 生成指定的表裏面的項 GET {{uri}}/createTable
cros模塊的使用
咱們使用一個cros,
const cors = require('cors') app.use(cors())
express就行了
咱們使用一個express就能解決了
// 文件上傳的文件夾模塊配置,同時也是靜態資源的處理, app.use('/uploads', express.static(__dirname + '/uploads')) //靜態路由
對於post的解決方案很是的簡單,咱們只須要使用express爲咱們提供的一些工具就行了
// 如下兩個專門用來處理application/x-www-form-urlencoded,application/json格式的post請求 app.uer(express.urlencoded({extended:true})) app.use(express.json())
講解要點:model操做,connet’,popuerlate查詢語句
這裏咱們使用的MongoDB數據庫。咱們只須要創建模型以後拿到數據表(集合)的操做模型就能夠了,模型咱們以前是已經定義過的,很是的簡單,咱們只須要創建連接,而且拿來操做就行了
/plugin/db.js
module.exports = app => { // 使用app有一個好處就是這些項咱們都是能夠配置的,這個app實際上你寫成option也沒問題 const mongoose = require("mongoose") mongoose.connect('mongodb://127.0.0.1:27017/Commet-Tools', { useNewUrlParser: true, useUnifiedTopology: true }) }
/index.js
require('./plugin/db')(app)
router.post('/api/articles', async(req, res) => { const model = await req.Model.create(req.body) // console.log(req.Model); res.send(model) })
這裏咱們主要使用
咱們看看咱們目前的項目目錄結構,再看看咱們的CRUD業務邏輯代碼
const express = require('express') const app = express() // POST解決方案 app.uer(express.urlencoded({extended:true})) app.use(express.json()) require('./plugin/db')(app) require('./route/admin/index')(app) app.listen(3000,()=>{ console.log('http://localhost:3000'); })
// 單一個的post不帶參數就是表示----> 增 (往資源裏面增長些什麼) router.post('/api/articles', async(req, res) => { const model = await Article.create(req.body) // console.log(Article); res.send(model) }) // 單一個get不帶參數表示-------> 查 (把資源裏的都查出來) router.get('/api/articles', async(req, res) => { const queryOptions = {} if (Article.modelName === 'Category') { queryOptions.populate = 'parent' } const items = await Article.find().setOptions(queryOptions).limit(10) res.send(items) }) //get帶參數表示-------> 指定條件的查 router.get('/api/articles/:id', async(req, res) => { //咱們的req.orane裏面就又東 console.log(req.params.id); const items = await Article.findById(req.params.id) res.send(items) }) // put帶參數表示-------> 更新某個指定的資源數據 router.put('/api/articles/:id', async(req, res) => { const items = await Article.findByIdAndUpdate(req.params.id, req.body) res.send(items) }) // deldete帶參數表示------> 刪除指定的資源數據 router.delete('/api/articles/:id', async(req, res) => { await Article.findByIdAndDelete(req.params.id, req.body) res.send({ sucees: true }) }) // 使用router 這一步必定不能少 app.use('/api',router)
REST測試文件以下
@uri = http://localhost:3001/api ### 測試 GET {{uri}}/test ### 增 POST {{uri}}/articles Content-Type: application/json { "title":"測試標題3", "thumbnails":"http://www.mongoing.com/wp-content/uploads/2016/01/MongoDB-%E6%A8%A1%E5%BC%8F%E8%AE%BE%E8%AE%A1%E8%BF%9B%E9%98%B6%E6%A1%88%E4%BE%8B_%E9%A1%B5%E9%9D%A2_35.png", "body":"<h1>這是咱們的測試內容/h1>", "hot":522 } ### 刪 DELETE {{uri}}/articles/5eca1161017fa61840905206 ### 改,僅僅是更改一部分, PUT {{uri}}/articles/5eca1161017fa61840905206 Content-Type: application/json { "category":"" "title":"測試標題2", "body":"<h1>這是咱們的測試內容/h1>", "hot":522 } ### 查 GET {{uri}}/articles ### 指定的查 GET {{uri}}/articles/5eca1161017fa61840905206
inflection
咱們發現,若是是這裏只是指定的一個資源(表-集合)的CRUD,若是說咱們有不少的資源,那麼咱們是不太可能一個一個去複製這些CRUD代碼,所以,咱們想的事情是封裝,封裝成統一的CRUD接口
咱們的思路很是的清晰也很是的簡單,在請求地址中,把資源獲取出來,而後去查對應的資源模塊就行了,這裏咱們須要來回顧一下,咱們以前的接口API規則還有資源命名的規則,articles====> Article,因此,這個命名規則在這裏就用得上了,咱們須要使用一個模塊來處理大小寫首字母的轉化,還有單數複數的轉換inflection
// 咱們但願中間件能夠配置,這樣咱們就能夠高階函數 module.exports = Option=>{ return async(req, res, next) => { const inflection = require('inflection') //轉化成單數大寫的字符串形式 let moldeName = inflection.classify(req.params.resource) console.log(moldeName); //categorys ===> Category //注意這裏的關聯查詢populate方法,裏面放的就是一個要被關聯的字段 req.Model = require(`../model/${moldeName}`) req.modelNmae = moldeName next() } }
/router/admin/index.js
app.use('/api/rest/:resource', resourceMiddelWeare(), router)
// 單一個的post不帶參數就是表示----> 增 (往資源裏面增長些什麼) router.post('/', async(req, res) => { const model = await req.Model.create(req.body) res.send(model) }) // 單一個get不帶參數表示-------> 查 (把資源裏的都查出來) router.get('/', async(req, res) => { const queryOptions = {} if (req.modelName === 'Category') { queryOptions.populate = 'parent' } const items = await req.Model.find().setOptions(queryOptions).limit(10) res.send(items) }) //get帶參數表示-------> 指定條件的查 router.get('/:id', async(req, res) => { //咱們的req.orane裏面就又東 console.log(req.params.id); const items = await req.Model.findById(req.params.id) res.send(items) }) // put帶參數表示-------> 更新某個指定的資源數據 router.put('/:id', async(req, res) => { const items = await req.Model.findByIdAndUpdate(req.params.id, req.body) res.send(items) }) // deldete帶參數表示------> 刪除指定的資源數據 router.delete('/:id', async(req, res) => { await req.Model.findByIdAndDelete(req.params.id, req.body) res.send({ sucees: true }) })
以上就是咱們的一個通用的CRUD接口的編寫方式了