一、前言

本文目标

本文是博主总结了之前的自己在做的很多个项目的一些知识点,当然我在这里不会过多的讲解业务的流程,而是建立一个小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. CodeForces - 1047CEnlarge GCD(这题很难,快来看题解,超级详细,骗浏览量)

    C. Enlarge GCD time limit per test1 second memory limit per test256 megabytes inputstandard input ou ...

  2. django源码分析——处理请求到wsgi及视图view

    本文环境python3.5.2,django1.10.x系列 根据前上一篇runserver的博文,已经分析了本地调试服务器的大致流程,现在我们来分析一下当runserver运行起来后,django框 ...

  3. win10 安装Maven

    1.将apache-maven-3.0.5-bin.zip解压到指定目录(最好不要有中文字符) 2.配MAVEN_HOME 3.验证是否安装成功  代开cmd窗口  mvn -v 4.修改本地仓库位置 ...

  4. spark系列-8、Spark Streaming

    参考链接:http://spark.apache.org/docs/latest/streaming-programming-guide.html 一.Spark Streaming 介绍 Spark ...

  5. python的with

    with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源. 比如文件使用后自动关闭.线程中锁的自动获取和释放等. with open('test.t ...

  6. 一次内核 crash 的排查记录

    一次内核 crash 的排查记录 使用的发行版本是 CentOS,内核版本是 3.10.0,在正常运行的情况下内核发生了崩溃,还好有 vmcore 生成. 准备排查环境 crash 内核调试信息rpm ...

  7. Linux安装tomcat并部署JavaWeb项目

    前提条件: 安装tomcat前请确认一下信息: 系统安装了JDK,且JDK版本应与javaWeb所使用的JDK一致,具体操作可参见Linux下安装JDK. 打包了javaWeb的.war 文件,具体操 ...

  8. Elasticsearch系列---几个高级功能

    概要 本篇主要介绍一下搜索模板.映射模板.高亮搜索和地理位置的简单玩法. 标准搜索模板 搜索模板search tempalte高级功能之一,可以将我们的一些搜索进行模板化,使用现有模板时传入指定的参数 ...

  9. SpringMVC 拦截返回值,并自定义

    有关取代mvc:annotation-driven使用自定义配置请看: http://blog.csdn.net/cml_blog/article/details/45222431 1.在项目开发中, ...

  10. SpringMVC底层执行原理

    一个简单的HelloSpringMVC程序 先在web,xml中注册一个前端控制器(DispatcherServlet) <?xml version="1.0" encodi ...