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

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

# 一、前言 ## 本文目標 > 本文是博主總結了之前的自己在做的很多個專案的一些知識點,當然我在這裡不會過多的講解業務的流程,而是建立一個小demon,旨在幫助大家去更加高效 更加便捷的生成自己的node後臺介面專案,本文底部提供了一個 藍圖,歡迎大家下載,start,實際上,這樣的一套思路打下來,基本上就已經建立手擼了一個nodejs框架出來了。大多數框架基本上都是這樣構建出來的,底層的Node 第二層的KOA 或者express,第三層就是各種第三方包的加持。 **注意:本文略長,我分了兩個章節** 本文寫了一個功能比較齊全的部落格後臺管理系統,用來演示這些工具的使用,原始碼已經分章節的放在了github之中,連結在文章底部 ## 望周知 > 歡迎各位大牛指教,如有不足望諒解,這裡只是提供了一個從express過渡到其它框架的文章,實際上,這篇文章所介紹的工具,也僅僅是工具啦,如果是真實開發專案,我們可能更加青睞於選擇一個成熟穩定的框架,比如AdonisJS(Node版的laravel) ,NestJS(Node版的spring),EggJS.....,我更推薦NestJS,博主後期會出一些Nest教學博文,歡迎關注 至於選擇Nest原因如下 ![](https://img2020.cnblogs.com/blog/1547034/202005/1547034-20200527210937617-1213529871.png) # 二、特別提示 ## 整體的架構思路 1. 忌諱 > 很多時候大家做為 高技術人才(程式猿單身狗),最忌諱的事情就是什麼都是還不清楚的情況下就去,吧唧的敲程式碼,就從個人的經驗來談,思路這種東西真的非常非常的重要 2. 從更高的層次來看架構的設計 > 一般來講,我們可以從兩個角度來看架構的設計,一個是資料,一個http報文(res,req) - 資料 我們看看如果從資料的扭轉角度,也就是說,我們站在資料的角度,看看整體的web架構應該如何做才是相對比較合理的. 第一步,我們拿到一個需求,要做的第一件的事情就是分析資料建立模型 第二步,仔細的分析資料的扭轉(如下這裡假設了這樣的一種) 使用者點點選文章的時候,我們能進行資料的聯合查詢,並且把查詢的資料返回給回去 ![](https://img2020.cnblogs.com/blog/1547034/202005/1547034-20200527210953796-1353840917.png) - 報文 從報文的角度,看整體的架構,這裡實際上也非常的簡單,就是看看我們的報文到底經過了什麼加工到底得到了什麼樣的資料,看看req,res經歷了什麼,就可以很好的把握 整個的後臺的API設計架構, ![](https://img2020.cnblogs.com/blog/1547034/202005/1547034-20200527211012456-482097519.png) 3. 結合 > 開發後臺的時候,對於一個有追求的工程師來說,二者的完美結合才是我們不變的追求, *更快,更高效,更穩定* ![](https://img2020.cnblogs.com/blog/1547034/202005/1547034-20200527211020555-1924499944.png) ## 資料庫建模約定 **我們嚴格約定:Aritcle (庫) => (對應的介面)articles** > 我們這裡有一些約定是必須要遵守的,我認為在工作中,如果遵守這些規範,可以方便後續的各種業務的操作 ### 約定 - 約定1 > 嚴格要求資料庫是單數而且首字母的大寫形式 - 約定2 > 嚴格要求請求的api介面是小寫的複數形式 - 比如 ```js Aritcle (庫) => (對應的介面)articles ``` ### 實操 > 好了,有了前面的約定還有理論,現在我們來實操 1. 模型 需求:我希望建立一個部落格網站,部落格網站目前有如下的資料,他們的資料模型圖如下(為了方便我們使用Native的模型設計,但是實際上我們這裡還是使用MongoDB資料庫) ![](https://img2020.cnblogs.com/blog/1547034/202005/1547034-20200527211035586-288696807.png) *以上我們詳細的說明了各個資料之間的關聯操作* 2. 程式碼實現 工程目錄如下 ![](https://img2020.cnblogs.com/blog/1547034/202005/1547034-20200527211045132-320087029.png) 具體的程式碼實現,這裡講解了如何在mongoose中進行多表(集合)關聯 - 廣告模型 /model/Ad.js ```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 ```js const schema = new mongoose.Schema({ username:{type:String}, passowrd:{type:String} }) ``` - 文章模型 /mode/Article.js ```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 ```js const schema = new mongoose.Schema({ iamge:{type:String}, name:{type:String}, body:{type:String}, }) ``` - 分類模型 /mode/Category.js ```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 ```js const schema = new mongoose.Schema({ body:{type:String}, isPublic:{type:Boolean} }) ``` *他們的模型在這個資料夾下* ![](https://img2020.cnblogs.com/blog/1547034/202005/1547034-20200527211107114-334594347.png) ## REST風格約定 **我們全部使用REST風格介面** > REST全稱是Representational State Transfer,中文意思是表述(編者注:通常譯為表徵)性狀態轉移 > 大白話說就是一種API介面編寫的規範,當然了這裡不詳細的展開敘述,我們來看看有用的 *下面的程式碼就用到了一些常用的RES風格* > 請不要關注具體的業務邏輯,我們的總店是請求的介面的編寫 ```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 }) }) ``` ## message風格約定方案 **我們約定,返回資訊的格式res.status(200).send({ message: '刪除成功' })** > 我們都知道,再有些情況下,我們的得到的一些結果是差不太多的,有時候,我們希望得到一些格式上統一的資料,這樣就能大大的簡化前端的操作。做為一名優秀的有節操的後臺程式設計師,我們應該與前端約定一些資料的統一返回格式,這樣就能大大的加快,大大的簡化專案的開發 > 比如我習慣把一些操作的資料統一一個格式發出去 *注意:我指的統一,是指沒有實際的資料庫訊息返回的時候,如果有資料,就老老實實返回對應的資料就好了* 1. 假設我們刪除成功了 > 我們返回這樣的資料 ```js res.status(200).send({ message: '刪除成功' }) ``` 2. 假設我們刪除失敗了 ```js // 程式設計的一個概念:中斷條件 if (!user) { return res.status(400).send({ message: '刪除失敗' }) } ``` 3. 假設我們需要許可權 ```js if (!user) { return res.status(400).send({ message: '使用者不存在' }) } ``` *以上res.status(400).send({ message: '使用者不存在' })就是我們的約定* ## 中介軟體約定方案 **中介軟體約定方案:我們約定一個規則去搭建我們的中介軟體** - 假設有這樣的一種情況,我們有一個介面要處理一項非常複雜的業務,使用了非常多的中介軟體,那麼我該如何處理呢, > 假設我們有一個訪問文章詳情的介面,獲取的這個資料,需要有文章詳情body,文章的tabs,上一篇 下一篇是否存在(也就是判斷資料庫中,文章之前是否還有文章) ```js // 文章詳情頁,不要關注具體的業務,我這裡想表達的是。如果是多箇中間件,我們就用【】括起來,而且我們嚴格要求所有中介軟體處理之後如果有介面都必須放在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一下,捕獲全域性的錯誤 ```js // 錯誤處理中介軟體,統一的處理我們http-assart丟擲的錯誤 app.use(async (err,req,res,next)=>{ // 具體的捕獲到資訊是err中,再伺服器為了排查錯誤,我們打印出來 consel.log(err) res.status(500).send({ message:'伺服器除問題了~~~請等待修復' }) }) ``` **以上就是我們的第一部分的全部內容** > 至此我們專案的資料夾如下 ![](https://img2020.cnblogs.com/blog/1547034/202005/1547034-20200527211122162-1544405854.png) ## 一款非常好用的REST測試外掛 > 這裡介紹了一個非常好用的介面測試工具RESTClinet /.http ```js @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, ```js const cors = require('cors') app.use(cors()) ``` ## 靜態資源的解決方案 **express就好了** > 我們使用一個express就能解決了 ```js // 檔案上傳的資料夾模組配置,同時也是靜態資源的處理, app.use('/uploads', express.static(__dirname + '/uploads')) //靜態路由 ``` ## post請求處理方案 > 對於post的解決方案非常的簡單,我們只需要使用express為我們提供的一些工具就好了 ```js // 以下兩個專門用來處理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 ```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 ```js require('./plugin/db')(app) ``` 2. 假設有一個介面要求查詢資料那麼可以這樣,使用mongoose的ORM方法 ```js 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 ```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'); }) ``` 2. 子路由CRUD介面邏輯所在 /router/admin/index.js ```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) ``` 3. 測試結果 ![](https://img2020.cnblogs.com/blog/1547034/202005/1547034-20200527211135442-751993356.png) REST測試檔案如下 ```js @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>", "hot":522 } ### 刪 DELETE {{uri}}/articles/5eca1161017fa61840905206 ### 改,僅僅是更改一部分, PUT {{uri}}/articles/5eca1161017fa61840905206 Content-Type: application/json { "category":"" "title":"測試標題2", "body":"

這是我們的測試內容/h1>", "hot":522 } ### 查 GET {{uri}}/articles ### 指定的查 GET {{uri}}/articles/5eca1161017fa61840905206 ``` ### 通用的抽象封裝 **inflection** > 我們發現,如果是這裡只是指定的一個資源(表-集合)的CRUD,如果說我們有很多的資源,那麼我們是不太可能一個一個去複製這些CRUD程式碼,因此,我們想的事情是封裝,封裝成統一的CRUD介面 > 我們的思路非常的清晰也非常的簡單,在請求地址中,把資源獲取出來,然後去查對應的資源模組就好了,這裡我們需要來回顧一下,我們之前的介面API規則還有資源命名的規則,articles====> Article,所以,這個命名規則在這裡就用得上了,我們需要使用一個模組來處理大小寫首字母的轉化,還有單數複數的轉換inflection 1. 我們抽離一箇中間件,放在要通用的CRUD資源請求中 /middleware/resouce.js ```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 ```js app.use('/api/rest/:resource', resourceMiddelWeare(), router) ``` 2. 在其他的資源中把固定寫死的資源表,替換成一個動態的表 /router/admin/index.js ```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介面的編寫方