• 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

  • 相关阅读:
    AJ学IOS(47)之网易彩票帮助界面UIWebView的运用
    AJ学IOS(46)之网易彩票幸运大转盘
    AJ学IOS(45)之常用的小功能比如打电话、打开网址、发邮件、发短信打开其他应用。
    AJ学IOS(44)之网易彩票自定义图片在右边的Button_弹出view_ios6,7简单适配
    AJ学IOS(43)之网易彩票底部自定义TabBar实现切换
    AJ学IOS(42)UI之核心动画CAAnimationGroup以及其他
    数组扁平化方法
    Echart数据转换(水平数据变成垂直数据)
    划重点简易版
    获取鼠标点击位置坐标
  • 原文地址:https://www.cnblogs.com/BM-laoli/p/12976627.html
Copyright © 2020-2023  润新知