NodeJS——大匯總(一)(只需要使用這些東西,就能處理80%以上業務需求,全網最全node解決方案,吐血整理)

一、前言

本文目標

本文是博主總結了之前的自己在做的很多個項目的一些知識點,當然我在這裡不會過多的講解業務的流程,而是建立一個小demon,旨在幫助大家去更加高效 更加便捷的生成自己的node後台接口項目,本文底部提供了一個 藍圖,歡迎大家下載,start,實際上,這樣的一套思路打下來,基本上就已經建立手擼了一個nodejs框架出來了。大多數框架基本上都是這樣構建出來的,底層的Node 第二層的KOA 或者express,第三層就是各種第三方包的加持。

注意:本文略長,我分了兩個章節

本文寫了一個功能比較齊全的博客後台管理系統,用來演示這些工具的使用,源代碼已經分章節的放在了github之中,鏈接在文章底部

望周知

歡迎各位大牛指教,如有不足望諒解,這裡只是提供了一個從express過渡到其它框架的文章,實際上,這篇文章所介紹的工具,也僅僅是工具啦,如果是真實開發項目,我們可能更加青睞於選擇一個成熟穩定的框架,比如AdonisJS(Node版的laravel) ,NestJS(Node版的spring),EggJS…..,我更推薦NestJS,博主後期會出一些Nest教學博文,歡迎關注

至於選擇Nest原因如下

二、特別提示

整體的架構思路

  1. 忌諱

很多時候大家做為 高技術人才(程序猿單身狗),最忌諱的事情就是什麼都是還不清楚的情況下就去,吧唧的敲代碼,就從個人的經驗來談,思路這種東西真的非常非常的重要

  1. 從更高的層次來看架構的設計

一般來講,我們可以從兩個角度來看架構的設計,一個是數據,一個http報文(res,req)

  • 數據
    我們看看如果從數據的扭轉角度,也就是說,我們站在數據的角度,看看整體的web架構應該如何做才是相對比較合理的.

第一步,我們拿到一個需求,要做的第一件的事情就是分析數據建立模型
第二步,仔細的分析數據的扭轉(如下這裡假設了這樣的一種)

用戶點點擊文章的時候,我們能進行數據的聯合查詢,並且把查詢的數據返回給回去

  • 報文
    從報文的角度,看整體的架構,這裡實際上也非常的簡單,就是看看我們的報文到底經過了什麼加工到底得到了什麼樣的數據,看看req,res經歷了什麼,就可以很好的把握 整個的後台的API設計架構,

  1. 結合

開發後台的時候,對於一個有追求的工程師來說,二者的完美結合才是我們不變的追求,

更快,更高效,更穩定

數據庫建模約定

我們嚴格約定:Aritcle (庫) => (對應的接口)articles

我們這裡有一些約定是必須要遵守的,我認為在工作中,如果遵守這些規範,可以方便後續的各種業務的操作

約定

  • 約定1

嚴格要求數據庫是單數而且首字母的大寫形式

  • 約定2

嚴格要求請求的api接口是小寫的複數形式

  • 比如

Aritcle (庫)  => (對應的接口)articles  

實操

好了,有了前面的約定還有理論,現在我們來實操

  1. 模型
    需求:我希望建立一個博客網站,博客網站目前有如下的數據,他們的數據模型圖如下(為了方便我們使用Native的模型設計,但是實際上我們這裡還是使用MongoDB數據庫)

以上我們詳細的說明了各個數據之間的關聯操作

  1. 代碼實現
    工程目錄如下

具體的代碼實現,這裡講解了如何在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: ‘刪除成功’ })

我們都知道,再有些情況下,我們的得到的一些結果是差不太多的,有時候,我們希望得到一些格式上統一的數據,這樣就能大大的簡化前端的操作。做為一名優秀的有節操的後台程序員,我們應該與前端約定一些數據的統一返回格式,這樣就能大大的加快,大大的簡化項目的開發

比如我習慣把一些操作的數據統一一個格式發出去
注意:我指的統一,是指沒有實際的數據庫訊息返回的時候,如果有數據,就老老實實返回對應的數據就好了

  1. 假設我們刪除成功了

我們返回這樣的數據


    res.status(200).send({ message: '刪除成功' })

  1. 假設我們刪除失敗了
    // 程序設計的一個概念:中斷條件
   if (!user) {
        return res.status(400).send({ message: '刪除失敗' })
    }

  1. 假設我們需要權限
  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 =  //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查詢語句

  1. 基礎知識

這裡我們使用的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)
  1. 假設有一個接口要求查詢數據那麼可以這樣,使用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業務邏輯代碼

  1. 入口
    /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('//localhost:3000');
})
  1. 子路由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)

  1. 測試結果

REST測試文件如下

@uri =  //localhost:3001/api

### 測試
GET {{uri}}/test


### 增
POST {{uri}}/articles
Content-Type: application/json

{
    "title":"測試標題3",
    "thumbnails":"//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

  1. 我們抽離一個中間件,放在要通用的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)
  1. 在其他的資源中把固定寫死的資源表,替換成一個動態的表
    /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接口的編寫方式了