一、前言
本文目標
本文是博主總結了之前的自己在做的很多個項目的一些知識點,當然我在這里不會過多的講解業務的流程,而是建立一個小demon,旨在幫助大家去更加高效 更加便捷的生成自己的node后台接口項目,本文底部提供了一個 藍圖,歡迎大家下載,start,實際上,這樣的一套思路打下來,基本上就已經建立手擼了一個nodejs框架出來了。大多數框架基本上都是這樣構建出來的,底層的Node 第二層的KOA 或者express,第三層就是各種第三方包的加持。
注意:本文略長,我分了兩個章節
本文寫了一個功能比較齊全的博客后台管理系統,用來演示這些工具的使用,源代碼已經分章節的放在了github之中,鏈接在文章底部
望周知
歡迎各位大牛指教,如有不足望諒解,這里只是提供了一個從express過渡到其它框架的文章,實際上,這篇文章所介紹的工具,也僅僅是工具啦,如果是真實開發項目,我們可能更加青睞於選擇一個成熟穩定的框架,比如AdonisJS(Node版的laravel) ,NestJS(Node版的spring),EggJS.....,我更推薦NestJS,博主后期會出一些Nest教學博文,歡迎關注
至於選擇Nest原因如下
二、特別提示
整體的架構思路
- 忌諱
很多時候大家做為 高技術人才(程序猿單身狗),最忌諱的事情就是什么都是還不清楚的情況下就去,吧唧的敲代碼,就從個人的經驗來談,思路這種東西真的非常非常的重要
- 從更高的層次來看架構的設計
一般來講,我們可以從兩個角度來看架構的設計,一個是數據,一個http報文(res,req)
- 數據
我們看看如果從數據的扭轉角度,也就是說,我們站在數據的角度,看看整體的web架構應該如何做才是相對比較合理的.
第一步,我們拿到一個需求,要做的第一件的事情就是分析數據建立模型
第二步,仔細的分析數據的扭轉(如下這里假設了這樣的一種)
用戶點點擊文章的時候,我們能進行數據的聯合查詢,並且把查詢的數據返回給回去
- 報文
從報文的角度,看整體的架構,這里實際上也非常的簡單,就是看看我們的報文到底經過了什么加工到底得到了什么樣的數據,看看req,res經歷了什么,就可以很好的把握 整個的后台的API設計架構,
- 結合
開發后台的時候,對於一個有追求的工程師來說,二者的完美結合才是我們不變的追求,
更快,更高效,更穩定
數據庫建模約定
我們嚴格約定:Aritcle (庫) => (對應的接口)articles
我們這里有一些約定是必須要遵守的,我認為在工作中,如果遵守這些規范,可以方便后續的各種業務的操作
約定
- 約定1
嚴格要求數據庫是單數而且首字母的大寫形式
- 約定2
嚴格要求請求的api接口是小寫的復數形式
- 比如
Aritcle (庫) => (對應的接口)articles
實操
好了,有了前面的約定還有理論,現在我們來實操
- 模型
需求:我希望建立一個博客網站,博客網站目前有如下的數據,他們的數據模型圖如下(為了方便我們使用Native的模型設計,但是實際上我們這里還是使用MongoDB數據庫)
以上我們詳細的說明了各個數據之間的關聯操作
- 代碼實現
工程目錄如下
具體的代碼實現,這里講解了如何在mongoose中進行多表(集合)關聯
- 廣告模型
/model/Ad.js
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規則
- 管理員模型
/mode/AdminUser.js
const schema = new mongoose.Schema({
username:{type:String},
passowrd:{type:String}
})
- 文章模型
/mode/Article.js
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' }
})
- 欄目模型
/mode/Book.js
const schema = new mongoose.Schema({
iamge:{type:String},
name:{type:String},
body:{type:String},
})
- 分類模型
/mode/Category.js
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'}
})
- 評論模型
/mode/Comment.js
const schema = new mongoose.Schema({
body:{type:String},
isPublic:{type:Boolean}
})
他們的模型在這個文件夾下
REST風格約定
我們全部使用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
})
})
message風格約定方案
我們約定,返回信息的格式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:'服務器除問題了~~~請等待修復'
})
})
以上就是我們的第一部分的全部內容
至此我們項目的文件夾如下
一款非常好用的REST測試插件
這里介紹了一個非常好用的接口測試工具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請求處理方案
對於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)
- 假設有一個接口要求查詢數據那么可以這樣,使用mongoose的ORM方法
router.post('/api/articles', async(req, res) => {
const model = await req.Model.create(req.body)
// console.log(req.Model);
res.send(model)
})
CRUD解決方案
CRUD業務邏輯
這里我們主要使用
我們看看我們目前的項目目錄結構,再看看我們的CRUD業務邏輯代碼
- 入口
/index.js
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');
})
- 子路由CRUD接口邏輯所在
/router/admin/index.js
// 單一個的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
- 我們抽離一個中間件,放在要通用的CRUD資源請求中
/middleware/resouce.js
// 我們希望中間件可以配置,這樣我們就可以高階函數
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)
- 在其他的資源中把固定寫死的資源表,替換成一個動態的表
/router/admin/index.js
// 單一個的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接口的編寫方式了