NodeJS——大汇总(一)(只需要使用这些东西,就能处理80%以上业务需求,全网最全node解决方案,吐血整理)
一、前言
本文目标
本文是博主总结了之前的自己在做的很多个项目的一些知识点,当然我在这里不会过多的讲解业务的流程,而是建立一个小demon,旨在帮助大家去更加高效 更加便捷的生成自己的node后台接口项目,本文底部提供了一个 蓝图,欢迎大家下载,start,实际上,这样的一套思路打下来,基本上就已经建立手撸了一个nodejs框架出来了。大多数框架基本上都是这样构建出来的,底层的Node 第二层的KOA 或者express,第三层就是各种第三方包的加持。
注意:本文略长,我分了两个章节
本文写了一个功能比较齐全的博客后台管理系统,用来演示这些工具的使用,源代码已经分章节的放在了github之中,链接在文章底部
望周知
欢迎各位大牛指教,如有不足望谅解,这里只是提供了一个从express过渡到其它框架的文章,实际上,这篇文章所介绍的工具,也仅仅是工具啦,如果是真实开发项目,我们可能更加青睐于选择一个成熟稳定的框架,比如AdonisJS(Node版的laravel) ,NestJS(Node版的spring),EggJS.....,我更推荐NestJS,博主后期会出一些Nest教学博文,欢迎关注
至于选择Nest原因如下
二、特别提示
整体的架构思路
- 忌讳
很多时候大家做为 高技术人才(程序猿单身狗),最忌讳的事情就是什么都是还不清楚的情况下就去,吧唧的敲代码,就从个人的经验来谈,思路这种东西真的非常非常的重要
- 从更高的层次来看架构的设计
一般来讲,我们可以从两个角度来看架构的设计,一个是数据,一个http报文(res,req)
- 数据
我们看看如果从数据的扭转角度,也就是说,我们站在数据的角度,看看整体的web架构应该如何做才是相对比较合理的.
第一步,我们拿到一个需求,要做的第一件的事情就是分析数据建立模型
第二步,仔细的分析数据的扭转(如下这里假设了这样的一种)
用户点点击文章的时候,我们能进行数据的联合查询,并且把查询的数据返回给回去
- 报文
从报文的角度,看整体的架构,这里实际上也非常的简单,就是看看我们的报文到底经过了什么加工到底得到了什么样的数据,看看req,res经历了什么,就可以很好的把握 整个的后台的API设计架构,
- 结合
开发后台的时候,对于一个有追求的工程师来说,二者的完美结合才是我们不变的追求,
更快,更高效,更稳定
数据库建模约定
我们严格约定:Aritcle (库) => (对应的接口)articles
我们这里有一些约定是必须要遵守的,我认为在工作中,如果遵守这些规范,可以方便后续的各种业务的操作
约定
- 约定1
严格要求数据库是单数而且首字母的大写形式
- 约定2
严格要求请求的api接口是小写的复数形式
- 比如
Aritcle (库) => (对应的接口)articles
实操
好了,有了前面的约定还有理论,现在我们来实操
- 模型
需求:我希望建立一个博客网站,博客网站目前有如下的数据,他们的数据模型图如下(为了方便我们使用Native的模型设计,但是实际上我们这里还是使用MongoDB数据库)
以上我们详细的说明了各个数据之间的关联操作
- 代码实现
工程目录如下
具体的代码实现,这里讲解了如何在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: '删除成功' })
我们都知道,再有些情况下,我们的得到的一些结果是差不太多的,有时候,我们希望得到一些格式上统一的数据,这样就能大大的简化前端的操作。做为一名优秀的有节操的后台程序员,我们应该与前端约定一些数据的统一返回格式,这样就能大大的加快,大大的简化项目的开发
比如我习惯把一些操作的数据统一一个格式发出去
注意:我指的统一,是指没有实际的数据库讯息返回的时候,如果有数据,就老老实实返回对应的数据就好了
- 假设我们删除成功了
我们返回这样的数据
res.status(200).send({ message: '删除成功' })
- 假设我们删除失败了
// 程序设计的一个概念:中断条件
if (!user) {
return res.status(400).send({ message: '删除失败' })
}
- 假设我们需要权限
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查询语句
- 基础知识
这里我们使用的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)
- 假设有一个接口要求查询数据那么可以这样,使用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业务逻辑代码
- 入口
/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');
})
- 子路由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)
- 测试结果
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
- 我们抽离一个中间件,放在要通用的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)
- 在其他的资源中把固定写死的资源表,替换成一个动态的表
/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解决方案,吐血整理)的更多相关文章
- C#开源系统大汇总(个人收藏)
C#开源系统大汇总 一.AOP框架 Encase 是C#编写开发的为.NET平台提供的AOP框架.Encase 独特的提供了把方面(aspects)部署到运行时代码,而其它AOP框架依赖 ...
- 大礼包!ANDROID内存优化(大汇总)
写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上把网上搜集的各种内存零散知识点进行汇总.挑选.简化后整理而成. 所以我将本文定义为一个工具类的文章,如果你在A ...
- android app性能优化大汇总(内存性能优化)
转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上 ...
- ANDROID内存优化——大汇总(转)
原文作者博客:转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! ANDROID内存优化(大汇总——上) 写在最前: 本文的思路主要借鉴了20 ...
- C#开源资源大汇总
C#开源资源大汇总 C#开源资源大汇总 一.AOP框架 Encase 是C#编写开发的为.NET平台提供的AOP框架.Encase 独特的提供了把方面(aspects)部署到运行 ...
- ANDROID内存优化(大汇总——中)
转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上 ...
- ANDROID内存优化(大汇总——上)
转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上 ...
- IE6 浏览器常见兼容问题 大汇总(23个)
IE6以及各个浏览器常见兼容问题 大汇总 综述:虽然说IE6在2014年4月将被停止支持,但是不得不说的是,IE6的市场并不会随着支持的停止而立刻消散下去,对于WEB前端开发工程师来说,兼容IE6 兼 ...
- C/C++ 笔试、面试题目大汇总 转
C/C++ 笔试.面试题目大汇总 这些东西有点烦,有点无聊.如果要去C++面试就看看吧.几年前网上搜索的.刚才看到,就整理一下,里面有些被我改了,感觉之前说的不对或不完善. 1.求下面函数的返回值( ...
随机推荐
- CodeForces - 1047CEnlarge GCD(这题很难,快来看题解,超级详细,骗浏览量)
C. Enlarge GCD time limit per test1 second memory limit per test256 megabytes inputstandard input ou ...
- django源码分析——处理请求到wsgi及视图view
本文环境python3.5.2,django1.10.x系列 根据前上一篇runserver的博文,已经分析了本地调试服务器的大致流程,现在我们来分析一下当runserver运行起来后,django框 ...
- win10 安装Maven
1.将apache-maven-3.0.5-bin.zip解压到指定目录(最好不要有中文字符) 2.配MAVEN_HOME 3.验证是否安装成功 代开cmd窗口 mvn -v 4.修改本地仓库位置 ...
- spark系列-8、Spark Streaming
参考链接:http://spark.apache.org/docs/latest/streaming-programming-guide.html 一.Spark Streaming 介绍 Spark ...
- python的with
with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源. 比如文件使用后自动关闭.线程中锁的自动获取和释放等. with open('test.t ...
- 一次内核 crash 的排查记录
一次内核 crash 的排查记录 使用的发行版本是 CentOS,内核版本是 3.10.0,在正常运行的情况下内核发生了崩溃,还好有 vmcore 生成. 准备排查环境 crash 内核调试信息rpm ...
- Linux安装tomcat并部署JavaWeb项目
前提条件: 安装tomcat前请确认一下信息: 系统安装了JDK,且JDK版本应与javaWeb所使用的JDK一致,具体操作可参见Linux下安装JDK. 打包了javaWeb的.war 文件,具体操 ...
- Elasticsearch系列---几个高级功能
概要 本篇主要介绍一下搜索模板.映射模板.高亮搜索和地理位置的简单玩法. 标准搜索模板 搜索模板search tempalte高级功能之一,可以将我们的一些搜索进行模板化,使用现有模板时传入指定的参数 ...
- SpringMVC 拦截返回值,并自定义
有关取代mvc:annotation-driven使用自定义配置请看: http://blog.csdn.net/cml_blog/article/details/45222431 1.在项目开发中, ...
- SpringMVC底层执行原理
一个简单的HelloSpringMVC程序 先在web,xml中注册一个前端控制器(DispatcherServlet) <?xml version="1.0" encodi ...