一、前言

本文目标

本文是博主总结了之前的自己在做的很多个项目的一些知识点,当然我在这里不会过多的讲解业务的流程,而是建立一个小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

NodeJS——大汇总(一)(只需要使用这些东西,就能处理80%以上业务需求,全网最全node解决方案,吐血整理)的更多相关文章

  1. C#开源系统大汇总(个人收藏)

    C#开源系统大汇总 一.AOP框架        Encase 是C#编写开发的为.NET平台提供的AOP框架.Encase 独特的提供了把方面(aspects)部署到运行时代码,而其它AOP框架依赖 ...

  2. 大礼包!ANDROID内存优化(大汇总)

    写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上把网上搜集的各种内存零散知识点进行汇总.挑选.简化后整理而成. 所以我将本文定义为一个工具类的文章,如果你在A ...

  3. android app性能优化大汇总(内存性能优化)

    转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上 ...

  4. ANDROID内存优化——大汇总(转)

    原文作者博客:转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! ANDROID内存优化(大汇总——上) 写在最前: 本文的思路主要借鉴了20 ...

  5. C#开源资源大汇总

    C#开源资源大汇总     C#开源资源大汇总 一.AOP框架        Encase 是C#编写开发的为.NET平台提供的AOP框架.Encase 独特的提供了把方面(aspects)部署到运行 ...

  6. ANDROID内存优化(大汇总——中)

    转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上 ...

  7. ANDROID内存优化(大汇总——上)

    转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上 ...

  8. IE6 浏览器常见兼容问题 大汇总(23个)

    IE6以及各个浏览器常见兼容问题 大汇总 综述:虽然说IE6在2014年4月将被停止支持,但是不得不说的是,IE6的市场并不会随着支持的停止而立刻消散下去,对于WEB前端开发工程师来说,兼容IE6 兼 ...

  9. C/C++ 笔试、面试题目大汇总 转

    C/C++ 笔试.面试题目大汇总 这些东西有点烦,有点无聊.如果要去C++面试就看看吧.几年前网上搜索的.刚才看到,就整理一下,里面有些被我改了,感觉之前说的不对或不完善. 1.求下面函数的返回值( ...

随机推荐

  1. 数学--数论--HDU 2197 本原串 (推规律)

    由0和1组成的串中,不能表示为由几个相同的较小的串连接成的串,称为本原串,有多少个长为n(n<=100000000)的本原串? 答案mod2008. 例如,100100不是本原串,因为他是由两个 ...

  2. HDOJ 4699 Editor 对顶栈模拟

    Editor Time Limit: 3000/2000 MS (Java/Others) Memory Limit: 131072/131072 K (Java/Others) Total Subm ...

  3. RF(数据库测试)

    1.下载 DatabaseLibrary 库 pip install robotframework-databaselibrary 2.下载 pymysql 库(作为中间件) pip install ...

  4. python requests 接口测试

    1.get方法请求接口 url:显而易见,就是接口的地址url啦 headers:请求头,例如:content-type = application/x-www-form-urlencoded par ...

  5. java程序猿面试系列之jvm专题

    前言 因为疫情的影响,现在都变成金五银六了.为了方便大家,在此开一个程序猿面试系列.总结各大公司所问的问题,希望能够帮助到大家,适合初中级java程序猿阅读. 1. Java类实例化时,JVM执行顺序 ...

  6. redis 6.0下redis-cluster-proxy代理尝试

    伴随着Redis6.0的发布,作为最令人怦然心动的特性之一,Redis官方同时推出Redis集群的proxy了:redis-cluster-proxy,https://github.com/Redis ...

  7. Element upload组件上传图片与回显图片

    场景:新增商品时需要添加商品主图,新增成功之后可编辑 上传图片: <el-form-item label="专区logo:" style="height:160px ...

  8. redis系列之5----redis实战(redis与spring整合,分布式锁实现)

    本文是redis学习系列的第五篇,点击下面链接可回看系列文章 <redis简介以及linux上的安装> <详细讲解redis数据结构(内存模型)以及常用命令> <redi ...

  9. windows远程执行命令总结

    1. 利用Impacket Impacket是一个Python类库,用于对SMB1-3或IPv4 / IPv6 上的TCP.UDP.ICMP.IGMP,ARP,IPv4,IPv6,SMB,MSRPC, ...

  10. 记一次jackson序列化Boolean的坑

    @Data public class CouponTemplateDto { /** * 优惠券类型id */ private Long couponTypeId; /** * 优惠券模板id */ ...