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 =  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查询语句

  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('http://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 =  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

  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接口的编写方式了

项目Git地址

https://github.com/BM-laoli/UniversalPackforNpm

原文地址:https://www.cnblogs.com/BM-laoli/p/12976627.html