node.js 学习日志

一、初始 Node.js

node.js是一个基于Chrome V8引擎的 JavaScript 运行环境

node运行环境.PNG

官网:https://nodejs.org/zh-cn

查看已安装的 Node.js 的版本号

  • 打开终端,在终端输入命令 node –v 后,按下回车键,即可查看已安装的 Node.js 的版本号。

终端使用node命令执行js文件(推荐)

  • vscode打开代码文件夹
  • 在文件上shift+右键–> 在终端中打开
    • 好处是,终端中执行node命令的文件夹,已经定位好了,我们不用切换文件夹了
  • 终端中,node js文件,回车

二、fs 文件系统模块

fs 模块是 Node.js 官方提供的、用来操作文件的模块。它提供了一系列的方法和属性,用来满足用户对文件的操作需求。

例如

  • fs.readFile() 方法,用来读取指定文件中的内容
  • fs.writeFile() 方法,用来向指定的文件中写入内容
const fs = require('fs')

2.1 fs.readFile() 语法格式

readFile语法.PNG

2.2 fs.writeFile() 语法格式

writeFile语法.PNG

2.3 成绩录入案例

const fs = require('fs')

fs.readFile('../files/成绩.txt', 'utf8', function(err, dataStr){
    if (err) {
        return console.log('读取文件失败!' + err.message);
    }
    // console.log('读取文件成功!'+ dataStr);
    // 把成绩字符串按照 空格 划分为数组
    const arrOld = dataStr.split(' ')
    // 循环追加每一项到新数组中,对每一项进行“=”替换操作
    const arrNew = []
    arrOld.forEach(item => {
        arrNew.push(item.replace('=', ': '))
    });
    // 把数组转化为 回车符 划分的字符串
    const newStr = arrNew.join('\r\n')
    // console.log(newStr);

    fs.writeFile('../files/成绩ok.txt', newStr, function(res){
        if (res) {
            return console.log('成绩录入失败' + res.message);
        }
        console.log('成绩录入成功!\n');
    })
})

2.4 文件路径动态拼接问题

方法一:文件位置可以写绝对路径

'/'='\\'

方法二:_ _dirname 表示当前文件所在目录

可以拼接上相对文件路径

三、path 模块

处理路径模块,有一系列方法和属性

例如

  • path.join() 方法:将多个路径片段拼接成一个完整的字符串 **

  • path.basename() 方法:从路径字符串中,找文件的文件名

  • path.extname() 方法:从路径字符串中,找文件的后缀

3.1 path.join()语法格式

path.join语法.PNG

3.2 path.basename()语法格式

path.basename语法.PNG

3.3 path.extname() 语法格式

path.extname语法.PNG

事例

const path = require('path')

const pathStr1 = path.join('/a', '/b/c', '../', './d', 'e')
console.log(pathStr1); // a\b\d\e

// 尽量使用join
const pathStr2 = path.join(__dirname, '../files/1.txt')
console.log(pathStr2); 

let fpath = '/a/b/c/inde.html'
let fname1 = path.basename(fpath)
console.log(fname1);
let fname2 = path.basename(fpath, '.html')
console.log(fname2);

// 找字符串中,最后一个点及之后的字符
console.log( path.extname('index.html') ); // .html
console.log( path.extname('index..html') ); // .html
console.log( path.extname('a.b.c.d.html') ); // .html
console.log( path.extname('asdfas/asdfa/a.b.c.d.html') ); // .html
console.log( path.extname('adf.adsf') ); // .adsf

3.4 时钟分离案例

时钟.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>时钟首页</title>
    <style>
        .box {
            width: 400px;
            height: 250px;
            border: 5px solid black;
            margin: 100px auto;
            line-height: 250px;
            text-align: center;
            font-size: 70px;
            font-weight: 700;
            background-color: pink;
        }
    </style>
</head>

<body>
    <div class="box"></div>

    <script>
        setInterval(function () {
            function getTimer() {
                var time = new Date();
                var h = time.getHours();
                h = h < 10 ? '0' + h : h;
                var m = time.getMinutes();
                m = m < 10 ? '0' + m : m;
                var s = time.getSeconds();
                s = s < 10 ? '0' + s : s;
                return h + ':' + m + ':' + s;
            }
            var box = document.querySelector('.box');
            box.innerHTML = getTimer();
        })
    </script>
</body>
</html>

分离.js

// 1.1导入fs
const fs = require('fs')
// 1.2导入path
const path = require('path')

//1.3定义正则表达式,分别匹配<style></style>和<script></script>标签
// [\s\S]表示匹配空格和非空格
const regStyle = /<style>[\s\S]*<\/style>/
const regScript = /<script>[\s\S]*<\/script>/

// 2.1调用fs.readFile()方法读取文件
fs.readFile(path.join(__dirname, '../01-时钟.html'), 'utf8', function (err, dataStr) {
    // 2.2文件读取失败
    if (err) {
        console.log('文件读取失败' + err.message)
    }
    // 2.3读取文件成功后,调用对应的三个方法,分别解析出css,js,html文件
    resolveCSS(dataStr)
    resolveJS(dataStr)
    resolveHTML(dataStr)
})
// 3.1定义处理css样式的方法
function resolveCSS(htmlStr) {
    // 3.2使用正则提取需要的内容
    const r1 = regStyle.exec(htmlStr)
    // 3.3将提取出来的样式字符串,进行字符串的replace替换操作
    const newCSS = r1[0].replace('<style>', '').replace('</style>', '')
    // console.log(newCss)
    // 3.4调用fs.writeFile()方法,将提取的样式,写入到clock目录中index.css的文件里边
    fs.writeFile(path.join(__dirname, '../clock/index.css'), newCSS, function (err) {
        if (err) return console.log('写入css样式失败!' + err.message)
        console.log('写入样式成功!')
    })
}

// 4.1定义处理js脚本的方法
function resolveJS(htmlStr) {
    // 4.2使用正则提取对应的<script></script>标签
    const r2 = regScript.exec(htmlStr)
    //4.3将提取出来的样式字符串,进行字符串的replace替换操作
    const newJS = r2[0].replace('<script>', '').replace('</script>', '')
    // console.log(newJS)
    // 4.4调用fs.writeFile()方法,将提取的脚本,写入到clock目录中index.js的文件里边
    fs.writeFile(path.join(__dirname, '../clock/index.js'), newJS, function (err) {
        if (err) return console.log('写入js脚本失败!' + err.message)
        console.log('写入脚本成功!')
    })
}

// 5.1定义处理HTMl结构的方法
function resolveHTML(htmlStr) {
    // 5.2将内嵌的style标签和script标签替换为外联的link和script标签
    const newHTML = htmlStr.replace(regStyle, '<link rel="stylesheet" href="index.css"/>').replace(regScript, '<script src="index.js"></script>')
    // 5.3写入index.html这个文件
    fs.writeFile(path.join(__dirname, '../clock/index.html'), newHTML, function (err) {
        if (err) return console.log('写入html失败!' + err.message)
        console.log('写入html成功!')
    })
}

四、querystring模块

  1. querystring.parse() – 把查询字符串转成对象
  2. querystring.stringify() – 把对象转成查询字符串
const querystring = require('querystring');

// querystring -- 查询字符串
/**
 * 什么是查询字符串
 *  发送请求的时候,携带的字符串参数
 *  booksname=xxx&author=xxx
 */

 let str = 'id=3&bookname=xiyouji&author=tangseng';

 // 一:把查询字符串转成对象
 let obj = querystring.parse(str);
//  console.log(obj); // {id: '3', bookname: 'xiyouji', author: 'tangseng'}

 // 二:把对象转成查询字符串
 console.log( querystring.stringify(obj) ); // id=3&bookname=xiyouji&author=tangseng

五、http模块

5.1 搭建服务器的步骤

① 导入 http 模块

② 创建 web 服务器实例

③ 为服务器实例绑定 request 事件,监听客户端的请求

④ 启动服务器

// ① 导入 http 模块
const http = require('http')
// ② 创建 web 服务器实例
const server = http.createServer()
// 只要客户端发来请求,则会触发下面的事件
server.on('request', (req, res) =>{
    console.log('Someone visit our web server');
})
// ④ 启动服务器 
server.listen(8000, ()=>{
    // 服务器启动之后,会触发这个回调函数
    console.log('http server running at http://127.0.0.1:8000');
})

req 请求对象

访问与客户端相关的数据属性

// req 对象,是根据 http.IncomingMessage类得来的对象
// 通过这个对象,我们可以得到请求的相关信息
req.url // 表示请求的url
req.method // 表示请求方式
req.headers // 表示请求头

res 响应对象

访问与服务器相关的数据属性

// 所有和响应相关的信息,都通过res来完成。res这里是response的意思。
// console.log(res instanceof http.ServerResponse); // true
// res.end(); // 用于做出响应
// res.setHeader(name, value); // 设置响应头
// res.statusCode // 设置响应状态码
// res.writeHead(状态码, {响应头})

// 告诉浏览器,响应的数据是什么类型、什么编码
res.setHeader('Content-Type', 'text/html; charset=utf-8');
// res.statusCode = 404; // 随便设置的状态码,实际中应该是200
res.writeHead(200, {
// key: value
'Content-Type': 'text/html; charset=utf-8',
'Author': 'LaoTang', // 随便写的,实验一下。值不能用中文
});
res.end('你好浏览器,你的请求我收到了,这是给你的回应');

当调用res.end()方法,向客户端发送中文内容的时候,会出现乱码问题,此时需要手动设置内容的编码格式

res.setHeader('Content-Type', 'text/html; charset=utf-8');

5.2 访问时钟案例

const fs = require('fs')
const path = require('path')
const http = require('http')

const server = http.createServer()
server.on('request', (req, res) => {
    let url = req.url
    let fpath = ''
    // console.log(url);
    if (url === '/' || url === '/index') {
        fpath = path.join(__dirname, '../clock/index.html')
    } else {
        fpath = path.join(__dirname, '../clock', url)
    }
    // console.log(fpath);
    fs.readFile(fpath, 'utf8', (err, dataStr) => {
        if (err) return res.end('<h1>404 Not Found!</h1>')
        res.end(dataStr)
    })
})
server.listen(8000, () => {
    console.log('http server running at http://127.0.0.1:8000');
})

六、模块化

把系统划分为若干模块的过程,模块是可组合、分解和更换的单元

要遵守模块化规范

好处

① 提高了代码的复用性

② 提高了代码的可维护性

③ 可以实现按需加载

6.1 Node.js中的模块化

  • 内置模块(官方提供)
  • 自定义模块(用户自定义的 .js 文件)
  • 第三方模块
6.1.1 加载模块

require() 方法,加载需要的内置模块、用户自定义模块和第三方模块

注意:加载用户自定义模块,可以省略 .js 后缀名

6.1.2 模块作用域

防止全局变量污染

6.1.3 向外共享模块作用域中的成员

在每个 .js 自定义模块中都有一个 module 对象,它里面存储了和当前模块有关的对象

可以使用 module.exports 对象。将模块内的成员共享出去,供外界使用。默认 module.exports={}

注意:require() 方法导入模块时,导入结果,永远以module.exports 指向的对象为准

还可以使用 exports 对象来向外共享模块作用域中的成员,他和 module.exports 指向同一个对象。最终也要以module.exports 指向的对象为准

注意:require() 模块得到的永远是 module.exports

6.1.4 Node.js 中的模块化规范

Node.js遵循CommonJS模块化规范,CommonJS 规定了 模块的特性各模块之间如何相互依赖

CommoneJS规定:

CommonJS规定.PNG

6.2 npm 与包

npm,inc. 公司,包搜索平台:https://www.npmjs.com/

包下载:https://registry.npmjs.org/

npm包管理工具:Node Package Manager

npm -v // 查看已安装版本号

在项目中安装指定包,默认最新版本的包;指定版本的在包名后用 @ 符号

npm install 包的完整名称

简写:
npm i 完整的包名称

npm install 包的完整名称@2.22。2
6.2.1 包管理配置文件

初次安装后

npm包.PNG

npm包文件.PNG

注意:在今后的项目开发中,一定要把node_modules文件夹添加到gitignore忽略文件中,别人可以根据package.json配置文件来下载。

npm init -y
// 作用:在执行命令所处的目录中,快速新建 package.json 文件

注意:只能在英文目录下成功运行(不能使用中文,也不能使用空格);只要执行 npm -install 包名 就会自动把包版本号和名称记录到package.json中

注意:可以直接调用 npm install(简写npm i)命令,直接安装package.json文件中的dependencies下的所有包

卸载包

npm uninstall 包名
简写(npm un 包名)

npm uninstall --save 包名
删除的同时也会把依赖信息全部删除
简写(npm un 包名)

安装包到devDependencies节点(开发会用到):

npm i 包名 -D  // 简写
npm i 包名 --save-dev // 完整

npm help

  • 查看使用帮助

npm 命令 --help

  • 查看具体命令的使用帮助(npm uninstall --help)
6.2.2 解决下载包速度慢的问题

淘宝 NPM 镜像服务器:https://registry.npmmirror.com/

① 切换 npm 的下载包镜像源

// 查看当前的下包镜像源
npm config get registry
// 切换为淘宝 NPM 镜像服务器下载包
npm config set registry=https://registry.npmmirror.com/
#查看npm配置信息
npm config list;

② nrm工具

方便切换包下载镜像源

// 通过 npm 包管理工具,将nrm安装为全局可用工具
npm i nrm -g
// 查看所有可用镜像源
nrm ls
// 将下包的镜像源切换为 taobao 镜像
nrm use taobao

nrm错误

nrm错误1.PNG

get-ExecutionPolicy  // 查看当前策略  Restricted

set-ExecutionPolicy RemoteSigned // 设置为 RemoteSigned

nrm错误2.PNG

// 重新安装
npm install -g nrm open@8.4.2 -save
// 应该使用 open 的 CommonJs规范的包 ,现在 open v9.0.0 是 ES Module 版本的包

③ 安装淘宝的cnpm

npm install -g cnpm --registry=https://registry.npmmirror.com/

#在任意目录执行都可以
#--global表示安装到全局,而非当前目录
#--global不能省略,否则不管用
npm install --global cnpm

安装包的时候把以前的npm替换成cnpm

#走国外的npm服务器下载包,速度比较慢
npm install 包名;

#使用cnpm就会通过淘宝的服务器来下载包
cnpm install 包名;

如果不想安装cnpm又想使用淘宝的服务器来下载:

npm install 包名 --registry=https://registry.npmmirror.com/

④ 全局安装nodemon模块

  • 安装命令

    npm i nodemon -g
    
  • nodemon的作用:

  • 代替node命令,启动服务的,当更改代码之后,nodemon会自动帮我们重启服务。

6.2.3 包分类

项目包:开发依赖包(devDependencies节点中)、核心依赖包(dependencies节点中)

全局包:一般是有工具性质的包,可以根据官方使用说明来安装

npm install -g 全局包名
npm uninstall -g 全局包名

i5ting_toc 全局包

可以把md文档转为 html 页面的小工具

npm install -g i5ting_toc // 安装为全局包

// md 转化为 html
i5ting_toc -f 要转换的md文件路径 -o
6.2.4 开发属于自己的包

规范的包内部结构

规范包结构.PNG

我的包:index.js(可划分到src根目录下)、package.json(name、version、main、description、keywords、license)、README.md(安装、导入、调用、开源协议)

特别地:es6展开运算符 ... ,展开所有属性

登录npm login或者npm adduser

发布包npm publish (注意包名不能重复、切换到包的根目录)

删除包npm unpublish 包名 --force (删除已发布的包)

删除包.PNG

6.3 模块的加载机制

模块在第一次加载后会被缓存

内置模块:优先加载

自定义模块

自定义模块.PNG

第三方模块

第三方模块.PNG

目录模块

目录模块.PNG

七、Express

Express是基于Node.js平台的快速、开放、极简的Web开发框架(作用和内置http模块类似)

可以方便快速创建 Web网站API接口 的服务器

// 调用
const express = require('express')

// 创建服务器
const app = express()

// 监听GET请求
app.get('/user', (req, res) => {
    // 调用 express 提供的 res.send() 向客户端响应一个 JSON 对象
    res.send({ name: 'ssy', age: 20, gender: '男' })
})

// 监听POST请求
app.post('/user', (req, res) => {
    // 调用 express 提供的 res.send() 向客户端响应一个 文本字符串
    res.send('请求成功!')
})

app.get('/', (req, res) => {
    // req.query 可以获取客户端发送过来的 查询参数
    // 默认是一个空对象
    console.log(req.query);
    res.send(req.query)
})

// :id 为动态参数, id 可以为别的 ids 等等;可以有多个动态参数
app.get('/user/:ids/:name', (req, res) => {
    // req.params 获取URL中动态匹配的参数,默认也是一个空对象
    console.log(req.params);
    res.send(req.params)
})

// 用将 express.static() clock 文件夹对外提供为静态资源,但是访问路径不能有clock,只能是它的子文件
app.use(express.static('./clock'))
// 如果访问index.html,会根据先后顺序,先调用clock中的,而不调用files中的
app.use(express.static('./files'))

// 挂载路径前缀,访问的是必须加 /clock
app.use('/files', express.static('./files'))

// 启动监听
app.listen(8001, () => {
    console.log('express server runing at http://127.0.0.1:8001');
})

7.1 Express 路由

客户端与服务器之间的映射关系

7.1.1 简单挂载

分三部分:请求的类型、请求的URL地址、处理函数

app.METHOD(PATH, HANDLER)
7.1.2 模块化路由

为了方便对路由进行模块化的管理,Express 不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块。

将路由抽离为单独模块的步骤如下:

  1. 创建路由模块对应的 .js 文件

    • 创建router/login.js 存放 登录、注册、验证码三个路由
    • 创建router/heroes.js 存放 和英雄相关的所有路由
  2. 调用 express.Router() 函数创建路由对象

const express = require('express');
const router = express.Router();
  1. 向路由对象上挂载具体的路由
// 把app换成router,比如
router.get('/xxx/xxx', (req, res) => {});
router.post('/xxx/xxx', (req, res) => {});
  1. 使用 module.exports 向外共享路由对象
module.exports = router;
  1. 使用 app.use() 函数注册路由模块 – app.js (app.use()作用就是注册全局中间键件)
// app.js 中,将路由导入,注册成中间件
const login = require('./router/logon.js');
app.use(login)

// app.use(require('./router/heroes.js'));
app.use( require(path.join(__dirname, 'router', 'heores.js')) );

7.2 Express 中间件

对请求进行预处理,本质上就是function处理函数

注意:中间件函数的形参列表中,必须包含 next 参数(代表一个函数) ,而路由处理函数只包含 req 和 res。

next函数:实现多个中间件连续调用

7.2.1 定义中间件函数

使中间件全局生效

app.use(中间件函数)

不使用 app.use(中间件函数) 定义的中间件,就是局部生效的中间件

可以将其添加到路由中

const express = require('express')
const app = express()

const mv1 = function (req, res, next) {
    console.log('这是第一个局部生效的中间件函数');
    // 把流转关系,转交给下一个中间件或路由
    next()
}
const mv2 = function (req, res, next) {
    console.log('这是第二个局部生效的中间件函数');
    // 把流转关系,转交给下一个中间件或路由
    next()
}

// // 将 mv 注册为全局生效的中间件
// app.use(mv)
// 或者直接写中间件,可以拼接路径
app.use((req, res, next) => {
    console.log('这是第一个最简单的中间件函数');

    // 获取请求到达时间
    const time = Date.now()
    // 为req对象挂载自定义属性,从而共享给后面的路由
    req.startTime = time

    // 把流转关系,转交给下一个中间件或路由
    next()
})

// 定义多个全局中间件
app.use((req, res, next) => {
    console.log('这是第二个最简单的中间件函数');
    // 把流转关系,转交给下一个中间件或路由
    next()
})

app.get('/', (req, res) => {
    console.log('调用了GET这个路由!');
    res.send('Home page.' + req.startTime)
})

app.get('/user1', mv1, (req, res) => {
    console.log('调用了GET这个路由!');
    res.send('Home page.' + req.startTime)
})
// mv1,mv2  还可以写成 [mv1,mv2]
app.get('/user2', mv1, mv2, (req, res) => {
    console.log('调用了GET这个路由!');
    res.send('Home page.' + req.startTime)
})

app.post('/', (req, res) => {
    console.log('调用了POST这个路由!');
    res.send('User page.' + req.startTime)
})

// 启动监听
app.listen(8003, () => {
    console.log('http://127.0.0.1:8003');
})
7.2.2 中间件作用

共享同一个req和res,可以为其挂载属性和方法

可以定义多个中间件(全局、局部个数随意)

7.2.3 中间件注意事项

中间件注意事项.PNG

7.2.4 中间件分类

① 应用级别中间件

  • 绑定到app.use()上,或者app.get()app.post()

② 路由级别中间件

  • 绑定到 router 实例上

③ 错误级别中间件

  • 捕获错误
  • 必须有四个形参,(err, req, res, next)
  • 必须定义在所有路由之后
const express = require('express')
const app = express()

// 定义路由
app.get('/', (req, res) => {
    // 人为制造错误
    throw new Error('服务器内部发生了错误!')
    res.send('Home Paeg!')
})

// 定义错误级别的中间件,捕获所有异常
app.use((err, req, res, next) => {
    console.log('发生了错误!' + err.message);
    res.send('Error!' + err.message)
})

app.listen(8004, ()=>{
    console.log('http://127.0.0.1:8004');
})

④ Express 内置的中间件

内置中间件.PNG

const express = require('express')
const app = express()

// 配置中间件
// express.json() 解析表单中的JSON数据
app.use(express.json())

// express.urlencoded() 解析表单中的 urlencoded 格式数据(键值对)
app.use(express.urlencoded({ extended: false }))

// 定义路由
app.get('/', (req, res) => {
    // 使用 req.body 来接收客户端发送的 请求体数据
    // 默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 underfined
    console.log(req.body);
    res.send('Home Paeg!')
})

app.post('/', (req, res) => {
    // 默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 {}
    console.log(req.body);
    res.send('Home Paeg!')
})

app.listen(8005, () => {
    console.log('http://127.0.0.1:8005');
})

⑤ 第三方的中间件

第三方中间件.PNG

可参考:http://expressjs.com/en/resources/middleware.html

7.2.5 自定义解析表单数据中间件
const express = require('express')
const querystring = require('querystring')

// 导入自定义处理表单数据的中间模块
const customBodyParser = require('./19-custom-body-parser')

const app = express()

// // 解析表单数据中间件
// app.use((req, res, next) => {
//     // 业务逻辑
//     // 顶一个空字符串,接收客户端发送过来的数据
//     let str = ''
//     // 监听 req 对象的 data 事件(客户端发送过来的新的请求体数据)
//     req.on('data', (chunk) => {
//         // 拼接请求体数据,隐式转换为字符串
//         str += chunk
//     })
//     // 监听 req 对象的 end 事件,获取 str
//     req.on('end', () => {
//         // 在 str 中存放的是完整的请求体数据
//         console.log(str);
//         // TODO: 把字符串格式的请求体数据,转化为对象格式
//         let body = querystring.parse(str)
//         console.log(body);

//         // 将body挂载到req上
//         req.body = body
//         next()
//     })
// })

// 将自定义的中间件模块,注册为全局可用的中间件
app.use(customBodyParser)

app.post('/user', (req, res) => {
    res.send(req.body)
})

app.listen(8006, () => {
    console.log('http://127.0.0.1:8006');
})

7.3 使用 Express 写接口

  1. 创建 Express 服务器
  2. 创建 API 路由模块
  3. 编写 GET 接口
  4. 编写 POST 接口

API.js

const express = require('express')
const router = require('./21-apiRouter')
const app = express()

// 配置解析表单数据的中间件
app.use(express.urlencoded({ extended: false }))

app.use('/api', router)

app.listen(8007, () => {
    console.log('http://127.0.0.1:8007');
})

21-apiRouter.js

// 路由模块

const express = require('express')
const router = express.Router()

// 挂载路由
router.get('/get', (req,res)=>{
    // 通过 req.query 获取客户端通过查询字符串,发送到服务器的数据
    const query = req.query
    // 处理结果
    res.send({
        status: 0, // 0 表示处理成功, 1 表示处理失败
        msg: 'GET 请求成功!', // 状态描述
        data: query // 需要响应给客户端的数据
    })

})
router.post('/post', (req,res)=>{
    // 通过 req.body 获取请求体中包含的 url-encoded 格式数据
    const body = req.body
    // 处理结果
    res.send({
        status: 0, // 0 表示处理成功, 1 表示处理失败
        msg: 'POST 请求成功!', // 状态描述
        data: body // 需要响应给客户端的数据
    })
})

// 导出路由对象
module.exports = router

7.4 CORS跨域资源共享

JSONP只支持GET请求(之前ajax部分讲到),推荐使用CORS中间件

npm install cors // 安装
const cors = require('cors') // 导入中间件
app.use(cors()) // 配置中间件

跨域访问

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>跨域访问</title>
    <script src="https://cdn.staticfile.org/jquery/3.7.0/jquery.min.js"></script>
</head>
<body>
    <button id="getbtn">GET请求</button>
    <button id="postbtn">POST请求</button>
    <script>
        $('#getbtn').on('click', function() {
            $.ajax({
                type: 'GET',
                url: 'http://127.0.0.1:8007/api/get',
                data: {
                    name: 'ssy',
                    age: 23
                },
                success: function(e){
                    console.log(e);
                }
            })
        })

        $('#postbtn').on('click', function() {
            $.ajax({
                type: 'POST',
                url: 'http://127.0.0.1:8007/api/post',
                data: {
                    name: 'ssy',
                    age: 23
                },
                success: function(e){
                    console.log(e);
                }
            })
        })
    </script>
</body>
</html>

CORS注意事项

cors注意事项.PNG

7.4.1 CORS响应头部

Access-Control-Allow-Origin

res.setHeader('Access-Control-Allow-Origin','*') // *代表可以允许任何域名访问

Access-Control-Allow-Headers

cors请求头.PNG

Access-Control-Allow-Methods

cors请求方式.PNG

7.4.2 请求分类

1.简单请求

cors简单请求.PNG

2.预检请求

cors预检请求.PNG

3.区别

cors请求区别.PNG

中间件、路由、Promise流程图.png

7.4.3 JSONP请求
const express = require('express')
const router = require('./21-apiRouter')
const cors = require('cors')

const app = express()

// 配置解析表单数据的中间件
app.use(express.urlencoded({ extended: false }))

// 必须在配置 CORS 中间件之前,配置JSONP的接口
app.get('/api/jsonp', (req, res) => {
    // TODO: 定义 JSONP 接口的具体实现过程
    // 获取客户端放过来的回调函数名字
    const funcName = req.query.callback
    // 得到通过 JSONP 形式发送给客户端的数据
    const data = { name: 'ssy', age: 23 }
    // 拼接一个函数调用的字符串
    const scriptStr = `${funcName}(${JSON.stringify(data)})`
    // 把字符串响应给客户端的<script>标签进行解析
    res.send(scriptStr)
})

// 在路由之前,配置cors中间件,解决接口跨域问题
app.use(cors())

// 路由模块定义为全局中间件
app.use('/api', router)

app.listen(8007, () => {
    console.log('http://127.0.0.1:8007');
})
<script>
    $('#JSONPbtn').on('click', function() {
    $.ajax({
        url: 'http://127.0.0.1:8007/api/jsonp',
        dataType: 'jsonp',
        success: function(e){
            console.log(e);
        }
    })
})
</script>

八、数据库与身份认证

8.1 数据库

常见数据库.PNG

8.2 数据库操作

-- 通过 * 把 users 表中的所有数据查询出来
-- select * from users

-- 从 users 表中吧 username 和 password 对应的数据查询出来
-- select username , password from users

-- 向 users 表中,插入新数据,username 的值为 ***,password 的值为 ***
-- insert into users (username,password) values ('***','***')
-- select * from users

-- 将 id 为 4 的用户密码,更新成 ***
-- update users set password='***' where id=4
-- select * from users

-- 更新 id 为 2 的用户,把用户密码更新为 lisi123 同时,把用户的状态更新为 1
-- update users set password='lisi123', status=1 where id=2
-- select * from users

--  删除 users 表中,id 为 4 的用户
-- delete from users where id=4
-- select * from users

-- 演示 where 子句的使用
-- select * from users where status=1
-- select * from users where id>=2
-- select * from users where username<>'李四'
-- select * from users where username!='李四'

-- 使用 AND 来显示所有状态为 0 且 id 小于 3 的用户
-- select * from users where status=0 and id<3

-- 使用 OR 来显示所有状态为 1 或 username 为 张三 的用户
-- select * from users where status=1 or username='张三'

-- 对 users 表中的数据,按照 status 字段进行升序排序
-- select * from users order by status
-- select * from users order by status asc

-- 按照 id 对结果进行降序的排序,desc 表示降序排序,asc 表示升序排序(默认情况下,就是升序排序)
-- select * from users order by id desc

-- 对 users 表中的数据,先按照 status 进行降序排序,再按照 username 字母的·顺序,进行升序的排序
-- select * from users order by status desc,username asc

-- 使用 count(*) 来统计 users 表中,状态为 0 用户的总数量
-- select count(*)from users where status=0

-- 使用 AS 关键字给列起别名
-- select count(*) as total from users where status=0
-- select username as uname , password as upwd from users

8.3 在Express中操作MySQL

8.3.1 安装、配置、测试
// npm i mysql
// 安装完成后,导入mysql模块
const mysql = require('mysql')
// 建立与 mysql 数据库的连接
const db = mysql.createPool({
    host: '127.0.0.1', // 数据库的 IP 地址
    user: 'root', // 登录数据库的账号
    password: '123456', // 登录数据库的密码
    database: 'node_db_01' // 指定要操作的数据库
})

// 检测 mysql 模块能否正常工作
db.query('select 1', (err, results)=>{
    // mysql 模块工作期间报错了
    if(err) return console.log(err.message);
    // 能够成功的执行 SQL 语句,打印出 [ RowDataPacket { '1': 1 } ]
    console.log(results);
})
8.3.2 查询、插入、更新和删除
const sqlStr = 'select * from users'
db.query(sqlStr, (err, results) => {
    // 查询失败
    if (err) return console.log(err.message);
    // 查询成功
    // 注意:如果执行的是 select 查询语句,则执行的结果是 数组
    console.log(results);
})

// let user1 = {username: 'ss', password: '666666'}
// 定义 SQL 语句, ? 表示占位符, 不能直接写数据
let sqlStr1 = 'INSERT INTO users (username, password) VALUES (?, ?)'
db.query(sqlStr1, [user1.username, user1.password], (err, results) => {
    // 失败
    if (err) return console.log(err.message);
    // 成功
    // 结果是对象,可以通过 affectedRows 来判断是否插入成功
    if(results.affectedRows == 1) {
        console.log('插入数据成功!');
    }
})

// 插入数据的便捷方式 数据属性名要和数据库里的表名一一对应
let user2 = {username: 'ssy', password: 'admin'}
// 定义 SQL 语句,SET ?
let sqlStr2 = 'INSERT INTO users SET ?'
db.query(sqlStr2, user2, (err, results) => {
    // 失败
    if (err) return console.log(err.message);
    // 成功
    // 结果是对象,可以通过 affectedRows 来判断是否插入成功
    if(results.affectedRows == 1) {
        console.log('插入数据成功!');
    }
})

// 更新数据对象
let user3 = { id: 7, username: 'hh', password: '000000' }
// 定义 SQL 语句, ? 表示占位符, 不能直接写数据
let sqlStr3 = 'UPDATE users SET username=?, password=? WHERE id=?'
db.query(sqlStr3, [user3.username, user3.password, user3.id], (err, results) => {
    // 失败
    if (err) return console.log(err.message);
    // 成功
    // 结果是对象,可以通过 affectedRows 来判断是否插入成功
    if (results.affectedRows == 1) {
        console.log('更新数据成功!');
    }
})

// 更新数据对象的便捷方式 数据属性名要和数据库里的表名一一对应
let user4 = { id: 7, username: 'aaaa', password: '0000' }
// 定义 SQL 语句
let sqlStr4 = 'UPDATE users SET ? WHERE id=?'
db.query(sqlStr4, [user4, user4.id], (err, results) => {
    // 失败
    if (err) return console.log(err.message);
    // 成功
    // 结果是对象,可以通过 affectedRows 来判断是否插入成功
    if (results.affectedRows == 1) {
        console.log('更新数据成功!');
    }
})

// 删除数据
// 定义 SQL 语句
let sqlStr5 = 'DELETE FROM users WHERE id=?'
// db.query() 执行 SQL 语句要为,占位符指定数据,只有一个占位符可以省略 数组写法
db.query(sqlStr5, 10, (err, results) => {
    // 失败
    if (err) return console.log(err.message);
    // 成功
    // 结果是对象,可以通过 affectedRows 来判断是否插入成功
    if (results.affectedRows == 1) {
        console.log('删除数据成功!');
    }
})

// 标记删除--更新数据
// 定义 SQL 语句
let sqlStr6 = 'UPDATE users SET status=? WHERE id=?'
// db.query() 执行 SQL 语句要为,占位符指定数据,只有一个占位符可以省略 数组写法,多个必须写数组
db.query(sqlStr6, [1, 7], (err, results) => {
    // 失败
    if (err) return console.log(err.message);
    // 成功
    // 结果是对象,可以通过 affectedRows 来判断是否插入成功
    if (results.affectedRows == 1) {
        console.log('标记删除数据成功!');
    }
})

8.4 前后端身份认证

8.4.1 Web开发模式
  1. 服务器端渲染

服务器端渲染.PNG

  1. 前后端分离

前后端分离.PNG

8.4.2 身份认证

通过一定的手段,完成用户身份的确认

对于服务器段渲染前后端分离这两种开发模式来说,分别有着不同的身份认证方案:

服务器段渲染推荐使用Session认证机制

前后端分离推荐使用JWT认证机制

8.4.3 Session 认证机制

突破http无状态性,可以颁发 Cookie 身份认证

Cookie 是存储在用户浏览器中的一段不超过4kb的字符串,它由一个名称(Name)、一个值(Value)和其他几个用于控制Cookie有效期安全性使用范围可选属性组成

特性

① 自动发送

② 域名独立

③ 过期时限

④ 4KB 限制

因为Cookie没有安全性,所以需要提高身份认证的安全性,可以添加一个认证机制

Session 工作原理

session工作原理.PNG

8.4.4 在 Express 中使用 Session 认证
const express = require('express')
// npm i express-session 安装
const session = require('express-session')

const app = express()

app.use(session({
    secret: 'pluto_ssy', // 可以是任何字符串
    resave: false, // 固定写法
    saveUninitialized: false // 固定写法
}))
// 解析POST提交过来的数据
app.use(express.urlencoded({ extended: false }))
// // 托管静态资源
// app.use(express.static('./page'))

// 登录
app.post('/api/login', (req, res) => {
    if (req.body.username != 'admin' || req.body.password != '000000') {
        return res.send({ status: 1, msg: '登录失败!' })
    }

    // 将用户的登录信息保存到session中
    // 只有配置中间件后,才有req.seesion
    req.session.user = req.body // 将用户信心存到session中
    req.session.islogin = true // 将用户登录状态,存储到 Session 中
    res.send({ status: 0, msg: '登陆成功!' })
})

// 获取用户名
app.get('/api/username', (req, res) => {
    if (!req.session.islogin) {
        return res.render({ status: 1, msg: 'fail' })
    }
    // 从req.session上获取用户名
    res.send({ status: 0, msg: 'success', username: req.session.user.username })
})

// 退出登录
app.post('/api/logout', (req, res) => {
    // destroy() 清空当前客户端登录用户对应 session 信息
    req.session.destroy()
    res.send({
        status: 0,
        msg: '退出登录成功!'
    })
})

app.listen(8008, () => {
    console.log('http://127.0.0.1:8008');
})

Session局限.PNG

8.4.5 JWT 认证机制

工作原理

JWT工作原理.PNG

组成部分: Header(头部)、Rayload(有效荷载)、Signature(签名)

JWT三部分.PNG

使用方式:客户端通常把token储存在 localStorage 和 是、sessionStorage中,推荐把 JWT 放在 HTTP 请求头的 Authorization 字段中

8.4.6 在 Express 中使用 JWT

安装:

npm i jsonwebtoken express-jwt@6.1.0
  • jsonwebtoken 用于生成 JWT 字符串
  • express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
const express = require('express')
const jwt = require('jsonwebtoken')
// 版本问题 express-jwt由于7.x.x版本导出方式改变所致,可以降低版本或者修改引入方式
// 注意 :入参option需要带上 algorithms: ["HS256"]
// 6.1.0 版本
const expressJWT = require('express-jwt')
const cors = require('cors')

const app = express()
// 定义 secret 密钥,建议命名为 secretKey
const secretKey = 'pluto_ssy'

// 解析 post 表单数据的中间件
app.use(express.urlencoded({ extended: false }))
// 在路由之前,配置cors中间件,解决接口跨域问题
app.use(cors())

// expressJWT({ secret: secretKey }) 就是用来解析 Token 的中间件, 成功后自动挂载 req.user(高版本没有)
// .unless({ path: [/^\/api\//] }) 用来指定哪些接口不需要访问权限
app.use(expressJWT({ secret: secretKey, algorithms: ['HS256'] }).unless({ path: [/^\/api\//] }))

// 登录接口
app.post('/api/login', (req, res) => {
    if (req.body.username != 'admin' || req.body.password != '000000') {
        return res.send({ status: 1, msg: '登录失败!' })
    }
    // 登陆成功
    res.send({
        status: 200,
        message: '登陆成功',
        // 调用 jwt.sign() 生成 JWT 字符串,三个参数分别是:用户信息对象(不要使用密码)、加密密钥、配置对象
        token: jwt.sign({ username: req.body.username }, secretKey, { expiresIn: '1h' })
    })
})

// 有权限的 API 接口  访问的时候 请求头需要携带 Authorization=Bearer token命令
app.get('/admin/getinfo', function (req, res) {
    // req.user 是配置 express-jwt 成功后自动挂载的 用户信息(高版本没有)
    console.log(req.user);
    res.send({
        status: 200,
        message: '获取用户信息成功!',
        data: req.user
    })
})

// 定义错误全局中间件,捕获解析 jwt 错误
app.use((err, req, res, next) => {
    // token 解析失败导出错误
    if (err.name == 'UnauthorizedError') {
        return res.send({ status: 401, message: '无效的token!' })
    }
    // 其他问题
    res.send({ status: 500, message: '未知错误!'})
})

app.listen(8009, () => {
    console.log('http://127.0.0.1:8009');
})

九、项目概述

9.1 大事件项目后台主代码

大事件项目目录.PNG

主文件:app.js

// 导入
const express = require('express')
// 导入cors 允许跨域访问
const cors = require('cors')
// 导入 Joi
const Joi = require('joi')
const expressJWT = require('express-jwt')

// 导入全局配置文件
const config = require('./config')
// 导入自定义路由模块
const userRouter = require('./router/user')
// 导入用户信息路由模块
const userinfoRouter = require('./router/userinfo')
// 导入文章分类路由模块
const artCateRouter = require('./router/artcate')
// 导入文章的路由魔铠
const articleRouter = require('./router/article')

// 创建实例
const app = express()

// 配置跨域中间件
app.use(cors())
// 配置解析表单数据的中间件 只能解析 application/x-www-form-urlencoded 格式
app.use(express.urlencoded({ extended: false }))
// 在路由之前封装 res.cc 函数,解决响应消息
app.use((req, res, next) => {
    // status 默认为1,表示失败的情况
    // err 的值,可能是一个错误对象,也可能是一个错误的描述字符串
    res.cc = (err, status = 1) => {
        res.send({
            status,
            message: err instanceof Error ? err.message : err
        })
    }
    // 注意添加 next()
    next()
})
// 在路由之前配置解析 Token 中间件,自动挂载 req.user 
app.use(expressJWT({ secret: config.jwtSecretKey }).unless({ path: /^\/api/ }))
// 使用自定义路由模块
app.use('/api', userRouter)
// 使用用户信息路由
app.use('/my', userinfoRouter)
// 使用文章分类路由
app.use('/my/article', artCateRouter)
// 使用文章路由
app.use('/my/article', articleRouter)

// 托管静态资源
app.use('/upload', express.static('./upload'))

// 定义错误级别中间件
app.use(function (err, req, res, next) {
    // Joi 参数校验失败
    if (err instanceof Joi.ValidationError) {
        return res.cc(err)
    }
    // token 身份认证失败的错误
    if(err.name == 'UnauthorizedError') return res.cc('身份认证失败!')
    // 未知错误
    res.cc(err)
})

// 指定端口号,启动服务器
app.listen(8010, function () {
    console.log('api server running at http://127.0.0.1:8010');
})

配置文件:config.js

// 全局配置文件

module.exports = {
    // 配置加密解密的 token 密钥
    jwtSecretKey: 'pluto_ssy ^_^',
    expiresIn: '1h' // token 有效期
}

导入的包文件:package.json

{
  "dependencies": {
    "@escook/express-joi": "^1.1.1",
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "express-jwt": "^5.3.3",
    "joi": "^17.4.0",
    "jsonwebtoken": "^8.5.1",
    "multer": "^1.4.2",
    "mysql": "^2.18.1"
  }
}

数据库配置模块:db/index.js

// 导入模块
const mysql = require('mysql')

// 创建数据库连接对象
const db = mysql.createPool({
    host: '127.0.0.1',
    user: 'root',
    password: '123456',
    database: 'node_db_01'
})

// 向外共享 db 数据库连接对象
module.exports = db

9.2 用户注册登录模块

路由模块 router/user.js

// 路由模块
const express = require('express')
// 导入 @escook/express-joi
const expressJoi = require('@escook/express-joi')

// 导入用户路由处理函数对应模块
const user_handler = require('../router_handler/user')
// 导入验证规则对象,{ reg_login_schema } 结构赋值
const { reg_login_schema } = require('../schema/user')

const router = express.Router()

// 调用验证中间件 expressJoi
router.post('/reguser', expressJoi(reg_login_schema), user_handler.regUser)

// 调用验证中间件 expressJoi
router.post('/login', expressJoi(reg_login_schema), user_handler.login)

module.exports = router

路由模块操作函数 router_handler/user.js

// 导入bcryptjs包
// 优点:只加密不解密;同一字符串,加密结果不同
const bcrypt = require('bcryptjs')
// 导入 生成 token 的包
const JWT = require('jsonwebtoken')

// 导入全局配置文件
const config = require('../config')
// 导入数据库操作模块
const db = require('../db/index')

// 注册新用户的处理函数,exports是简写模式
exports.regUser = (req, res) => {
    // 获取客户端提交到服务器的用户信息
    const userinfo = req.body
    // 不用if判断了
    // // 对表单中的数据进行合法性校验
    // if (!userinfo.username || !userinfo.password) {
    //     // return res.send({ status: 1, messaeg: '用户名或密码不合法!' })
    //     return res.cc('用户名或密码不合法!')
    // }

    // 定义 SQL 语句,查询用户名是否被占用
    const sqlStr = 'SELECT * FROM ev_users WHERE username=?'
    db.query(sqlStr, userinfo.username, (err, results) => {
        // 执行 SQL 语句失败
        if (err) {
            // return res.end({ status: 1, message: err.message })
            return res.cc(err)
        }
        if (results.length > 0) {
            // return res.send({ status: 1, message: '用户名被占用,请更换其他用户名!' })
            return res.cc('用户名被占用,请更换其他用户名!')
        }
        // 用户名可以使用
        // 调用 bcrypt.hashSync() 对密码进行加密
        // console.log(userinfo);
        userinfo.password = bcrypt.hashSync(userinfo.password, 10)
        // console.log(userinfo);

        // 定义插入新用户的 SQL 语句
        const sql = 'INSERT INTO ev_users SET ?'
        db.query(sql, { username: userinfo.username, password: userinfo.password }, (err, results) => {
            // 执行 SQL 语句失败
            if (err) {
                // return res.send({ status: 1, message: err.message })
                return res.cc(err)
            }
            // 判断影响行数是否为1
            if (results.affectedRows !== 1) {
                // return res.send({ status: 1, message: '注册用户失败,请稍后再试。'})
                return res.cc('注册用户失败,请稍后再试。')
            }
            // 注册成功
            // res.send({ status: 0, message: '注册成功!' })
            res.cc('注册成功!', 0)
        })
    })
}
// 登录的处理函数
exports.login = (req, res) => {
    // 接受表单数据
    const userinfo = req.body
    // 定义 SQL 语句
    const sql = 'SELECT * FROM ev_users where username=?'
    db.query(sql, userinfo.username, (err, results) => {
        // 执行 SQL 语句失败
        if (err) {
            return res.cc(err)
        }
        // 执行 SQL 语句成功,但是获取到的数据不等于1
        if (results.length !== 1) {
            return res.cc('登录失败!')
        }

        // 判断密码是否正确
        const compareResult = bcrypt.compareSync(userinfo.password, results[0].password)
        if (!compareResult) return res.cc('登录失败!')

        // 验证成功,生成 JWT 的 token 字符串
        // ES6 高级语法,展开和剔除敏感信息
        const user = { ...results[0], password: '', user_pic: '' }
        // 对用户信息进行加密,生成 token 字符串
        const tokenStr = JWT.sign(user, config.jwtSecretKey, { expiresIn: config.expiresIn })
        // console.log(tokenStr);
        // 把 tokenStr 响应给客户端
        res.send({
            status: 0,
            message: '登陆成功!',
            token: 'Bearer ' + tokenStr // 方便客户端使用,直接拼接上 'Bearer '
        })
    })
}

验证规则模块 schema/user.js

// 前端验证为辅,后端验证为主
// 导入 Joi 来定义验证规则
const Joi = require('joi')

// 命名保持一致,ES6简写
const username = Joi.string().alphanum().min(2).max(10).required()
const password = Joi.string().pattern(/^[\S]{6,15}$/).required()

const id = Joi.number().integer().min(1).required()
const nickname = Joi.string().required()
const email = Joi.string().email().required()

// dataUri() 表示 data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
const avatar = Joi.string().dataUri().required()

// 定义验证注册和登录表单数据的规则对象
exports.reg_login_schema = {
    body: {
        username,
        password
    }
}

// 定义更新用户基本信息的验证规则对象
exports.update_userinfo_schema = {
    body: {
        id,
        nickname,
        email
    }
}

// 定义更新用户密码的验证规则对象
exports.update_password_schema = {
    body: {
        oldPwd: password,
        newPwd: Joi.not(Joi.ref('oldPwd')).concat(password) // 不能等于原密码,且规则与原密码相同
    }
}

// 定义验证头像的规则
exports.update_avatar_schema = {
    body: {
        avatar
    }
}

9.3 用户信息管理模块

路由模块 router/userinfo.js

const express = require('express')
// 导入 @escook/express-joi
const expressJoi = require('@escook/express-joi')

// 导入路由处理函数模块
const userinfo_handler = require('../router_handler/userinfo')
// 导入验证规则对象,{ update_userinfo_schema } 结构赋值
const { update_userinfo_schema, update_password_schema, update_avatar_schema } = require('../schema/user')

// 调用路由函数
const router = express.Router()

// 挂载路由
// 获取用户基本信息
router.get('/userinfo', userinfo_handler.getUserInfo)

// 更新用户基本信息
router.post('/userinfo', expressJoi(update_userinfo_schema), userinfo_handler.updateUserInfo)

// 更新密码的路由
router.post('/updatepwd', expressJoi(update_password_schema), userinfo_handler.updatePassword)

// 更换用户头像路由
router.post('/update/avatar', expressJoi(update_avatar_schema), userinfo_handler.updateAvatar)

module.exports = router

路由模块操作函数 router_handler/userinfo.js

// 导入数据库操作模块
const db = require('../db/index')
// 导入处理密码的模块
const bcrypt = require('bcryptjs')

// 获取用户基本信息的处理函数
exports.getUserInfo = (req, res) => {
    // 定义 SQL 语句查询用户信息
    const sql = 'SELECT id, username, nickname, email, user_pic FROM ev_users where id=?'
    db.query(sql, req.user.id, (err, results) => {
        // 执行 SQL 语句失败
        if (err) return res.cc(err)
        // 执行 SQL 语句成功,查询结果为空
        if (results.length !== 1) return res.cc('获取用户信息失败!')

        // 用户信息获取成功
        res.send({
            status: 0,
            message: '获取用户信息成功!',
            data: results[0]
        })
    })
}

// 获取用户基本信息的处理函数
exports.updateUserInfo = (req, res) => {
    // 定义 SQL 语句
    const sql = 'UPDATE ev_users SET ? WHERE id=?'
    db.query(sql, [req.body, req.body.id], (err, results) => {
        // 执行 SQL 语句失败
        if (err) return res.cc(err)
        // 执行 SQL 语句成功,但是影响行数不等于 1
        if (results.effectedRows != 1) return res.cc('更新用户基本信息失败!')
        // 更新成功!
        res.cc('更新用户信息成功!', 0)
    })
}

// 重置用户密码的处理函数
exports.updatePassword = (req, res) => {
    // 根据id查询用户的信息
    const sql = 'SELECT * FROM ev_users where id=?'
    db.query(sql, req.user.id, (err, results) => {
        // 执行 SQL 语句失败
        if (err) return res.cc(err)
        // 判断是否存在
        if (results.length != 1) return res.cc('用户不存在!')

        // 判断用户输入的旧密码是否正确
        const compareResult = bcrypt.compareSync(req.body.password, results[0].password)
        if (!compareResult) return res.cc('旧密码错误!')

        // 更新数据库的密码
        // 定义更新用户的 SQL 语句
        const sqlStr = 'UPDATE ev_users SET password=? WHERE id=?'
        // 对新密码进行加密
        const newPwd = bcrypt.hashSync(req.body.newPwd, 10)
        db.query(sqlStr, [newPwd, req.user.id], (err, results) => {
            // 执行 SQL 语句失败
            if (err) return res.cc(err)
            // 执行 SQL 语句成功,但是影响行数不等于 1
            if (results.effectedRows != 1) return res.cc('更新密码失败!')
            // 更新密码成功
            res.cc('更新密码成功!', 0)
        })
    })
}

// 更新用户头像的处理函数
exports.updateAvatar = (req, res) => {
    // 定义更新用户头像的 SQL 语句
    const sql = 'UPDATE ev_users SET user_pic=? WHERE id=?'
    db.query(sql, [req.body.avatar, req.user.id], (err, results) => {
        // 执行 SQL 语句失败
        if (err) return res.cc(err)
        // 影响的行数是否等于 1
        if (results.affectedRows != 1) return res.cc('更换头像失败!')
        // 成功
        res.cc('更换头像成功!', 0)
    })
}

验证规则模块 schema/userinfo.js

// 导入定义验证规则的模式
const Joi = require('joi')

// 定义 分类名称 和 分类别名 的校验规则
const name = Joi.string().required()
const alias = Joi.string().alphanum().required()
// 定义 分类Id 校验规则
const id = Joi.number().integer().min(1).required()

// 校验规则对象 - 添加分类,并共享出去
exports.add_cate_schema = {
    body: {
        name,
        alias
    }
}
// 校验规则对象 - 删除分类,并共享出去
exports.delete_cate_schema = {
    params: {
        id
    }
}
// 校验规则对象 - 根据id查询分类,并共享出去
exports.get_cate_schema = {
    params: {
        id: id
    }
}
// 校验规则对象 - 更新分类,并共享出去
exports.update_cate_schema = {
    body: {
        id: id,
        name,
        alias
    }
}

9.4 文章分类管理模块

路由模块 router/artcate.js

// 导入 express
const express = require('express')
// 导入验证数据的中间件
const expressJoi = require('@escook/express-joi')

// 导入文章分类的路由处理模块
const artCate_handler = require('../router_handler/artcate')
// 导入验证规则的对象
const { add_cate_schema, delete_cate_schema, get_cate_schema, update_cate_schema } = require('../schema/artcate')

const router = express.Router()

// 获取文章分类列表的路由
router.get('/cates', artCate_handler.getArtCates)
// 新增文章分类的路由
router.post('/addcates', expressJoi(add_cate_schema), artCate_handler.addArtCates)
// 删除文章分类的路由
router.get('/deletecate/:id', expressJoi(delete_cate_schema), artCate_handler.deleteCateById)
// 根据id获取文章分类的路由
router.get('/cates.:id', expressJoi(get_cate_schema), artCate_handler.getArtCateById)
// 根据id更新文章分类的路由
router.post('/updatecate', expressJoi(update_cate_schema), artCate_handler.updateCateById)

module.exports = router

路由模块操作函数 router_handler/artcate.js

// 路由处理模块

// 导入数据库操作模块
const db = require('../db/index')

// 获取文章分类的处理函数
exports.getArtCates = (req, res) => {
    // 定义查询文章分类列表数据的 SQL 语句
    const sql = 'SELECT * FROM ev_article_cate WHERE is_delete=0 ORDER BY ASC'
    db.query(sql, (err, results) => {
        // 执行 SQL 错误
        if (err) return res.cc(err)
        res.send({
            status: 0,
            message: '获取文章分类成功!',
            data: results
        })
    })
}

// 新增文章分类的处理函数
exports.addArtCates = (req, res) => {
    // 定义查重 SQL 语句
    const sql = 'SELECT * FROM ev_article_cate WHERE name=? or alias=?'
    db.query(sql, [req.body.name, req.body.alias], (err, results) => {
        // 执行 SQL 错误
        if (err) return res.cc(err)

        // 判断是否是标记删除的
        // console.log(results[0].isdelete);

        // 判断数据lenth
        if (results.length == 2) return res.cc('分类名称与分类别名被占用,请更换后重试!')
        // length 等于 1 的三种情况
        if (results.length == 1 && results[0].name == req.body.name && results[0].alias == req.body.alias) return res.cc('分类名称与分类别名被占用,请更换后重试!')
        if (results.length == 1 && results[0].name == req.body.name) return res.cc('分类名称被占用,请更换后重试!')
        if (results.length == 1 && results[0].alias == req.body.alias) return res.cc('分类别名被占用,请更换后重试!')

        // 分类名称和分类别名都可用,添加操作
        const sql = 'INSERT INTO ev_article_cate SET ?'
        db.query(sql, req.body, (err, results) => {
            // 执行 SQL 错误
            if (err) return res.cc(err)
            if (results.affectedRows != 1) return res.cc('新增文章分类失败!')
            // 成功
            res.cc('新增文章分类成功!', 0)
        })
    })
}

// 删除文章分类
exports.deleteCateById = (req, res) => {
    // 定义判断重复删除的 SQL 语句
    const sql = 'SELECT * FROM ev_article_cate WHERE id=?'
    db.query(sql, req.params.id, (err, results) => {
        // 执行 SQL 错误
        if (err) return res.cc(err)
        // 查看当前删除状态,判断是否已经删除
        if (results[0].is_delete == 1) return res.cc('不能重复删除!')
        // 不是重复删除,定义标记删除 SQL 语句
        const sql = 'UPDATE ev_article_cate SET is_delete=1 WHERE id=?'
        db.query(sql, req.params.id, (err, results) => {
            // 执行 SQL 错误
            if (err) return res.cc(err)
            // 判断影响行数
            if (results.affectedRows != 1) return res.cc('删除文章分类失败!')
            // 成功删除
            res.cc('删除文章分类成功!', 0)
        })
    })
}

// 根据id获取文章分类
exports.getArtCateById = (req, res) => {
    // 定义根据id 查询文章分类的 SQL 语句
    const sql = 'SELECT * FROM ev_article_cate WHERE name=? or alias=?'
    db.query(sql, req.params.id, (err, results) => {
        // SQL 语句执行失败
        if (err) return res.cc(err)
        // 判断数据lenth
        if (results.length != 1) return res.cc('获取文章分类失败!')

        // 成功
        res.send({
            status: 0,
            message: '获取文章分类数据成功!',
            data: results[0]
        })
    })
}

// 根据id更新文章分类
exports.updateCateById = (res, req) => {
    // 定义查重 SQL 语句, 将本id 排除,查询别的
    const sql = 'SELECT * FROM ev_article_cate WHERE id<>? AND (name=? or alias=?)'
    db.query(sql, [req.body.id, req.body.name, req.body.alias], (err, results) => {
        // 执行 SQL 错误
        if (err) return res.cc(err)

        // 判断是否是标记删除的
        // console.log(results[0].isdelete);

        // 判断数据lenth
        if (results.length == 2) return res.cc('分类名称与分类别名被占用,请更换后重试!')
        // length 等于 1 的三种情况
        if (results.length == 1 && results[0].name == req.body.name && results[0].alias == req.body.alias) return res.cc('分类名称与分类别名被占用,请更换后重试!')
        if (results.length == 1 && results[0].name == req.body.name) return res.cc('分类名称被占用,请更换后重试!')
        if (results.length == 1 && results[0].alias == req.body.alias) return res.cc('分类别名被占用,请更换后重试!')
        
        // 更新操作
        const sql = 'UPDATE ev_article_cate SET ? WHERE id=?'
        db.query(sql, [req.body, req.body.id], (req, res) => {
            // 执行 SQL 语句失败
            if (err) return res.cc(err)
            // 判断影响行数
            if (results.affectedRows != 1) return res.cc('更新文章分类失败!')
            // 成功
            res.cc('更新文章分类成功!', 0)
        })
    })
}

验证规则模块 schema/artcate.js

// 导入定义验证规则的模式
const Joi = require('joi')

// 定义 分类名称 和 分类别名 的校验规则
const name = Joi.string().required()
const alias = Joi.string().alphanum().required()
// 定义 分类Id 校验规则
const id = Joi.number().integer().min(1).required()

// 校验规则对象 - 添加分类,并共享出去
exports.add_cate_schema = {
    body: {
        name,
        alias
    }
}
// 校验规则对象 - 删除分类,并共享出去
exports.delete_cate_schema = {
    params: {
        id
    }
}
// 校验规则对象 - 根据id查询分类,并共享出去
exports.get_cate_schema = {
    params: {
        id: id
    }
}
// 校验规则对象 - 更新分类,并共享出去
exports.update_cate_schema = {
    body: {
        id: id,
        name,
        alias
    }
}

9.5 文章信息管理模块

路由模块 router/article.js

// 文章路由模块
const express = require('express')
// 导入处理路径内置模块 path
const path = require('path')
// 安装并导入 multer@1.4.2  解析 Formdata 格式表单数据
const multer = require('multer')
// 导入验证数据的中间件
const expressJoi = require('@escook/express-joi')

// 导入路由处理函数模块
const article_handler = require('../router_handler/article')
// 导入验证规则的对象
const { add_article_schema, list_article_schema, delete_article_schema, select_article_schema, update_article_schema } = require('../schema/article')


// 创建路由实例对象
const router = express.Router()
// 创建 multer 的实例对象,通过 dest 属性指定文件存放路径
const upload = multer({ dest: path.join(__dirname, '../upload') })

// 发布文章的路由
// upload.single() 是一个局部生效的中间件,用来解析 FormData 格式的表单数据
// 将文件类型的数据,解析并挂载 req.file 属性中
// 将文本类型的数据,解析并挂载 req.body 属性中
router.post('/add', upload.single('cover_img'), expressJoi(add_article_schema), article_handler.addArticle)

// 获取文章列表的路由
router.get('/list', expressJoi(list_article_schema), article_handler.getArticle)

// 根据文章id删除文章的路由
router.get('/delete/:id', expressJoi(delete_article_schema), article_handler.deleteArticleById)

// 根据文章id获取文章详情的路由
router.get('/:id', expressJoi(select_article_schema), article_handler.getArticleById)

// 根据文章id更新文章的路由
router.post('/edit', upload.single('cover_img'), expressJoi(update_article_schema), article_handler.updateArticleById)

module.exports = router

路由模块操作函数 router_handler/article.js

// 路由处理函数模块

// 导入 path 模块
const path = require('path')

// 导入数据库操作模块
const db = require('../db/index')

// 发布文章
exports.addArticle = (req, res) => {
    // console.log(req.file);
    if (!req.file || req.file.fieldname !== 'cover_img') return res.cc('文章封面是必选参数!')
    // 数据合法,后续发布操作
    const articleInfo = {
        // 展开就是 标题、内容、发布状态、分类id
        ...req.body,
        // 文章封面的存放路径
        cover_img: path.join('/upload', req.file.filename),
        // 文章的发布时间
        pub_date: new Date(),
        // 文章作者的id
        author_id: req.user.id
    }
    // console.log(articleInfo);
    // 定义发布文章的 SQL 语句
    const sql = 'INSERT INTO ev_articles SET ?'
    db.query(sql, articleInfo, (err, results) => {
        if (err) return res.cc(err)
        if (results.affectedRows != 1) return res.cc('发布新文章失败!')
        res.cc('发布文章成功!', 0)
    })
}

// 获取文章列表
exports.getArticle = (req, res) => {
    // 接收所有的请求参数
    // let pagenum = req.query.pagenum;
    // let pagesize = req.query.pagesize;
    // let cate_id = req.query.cate_id;
    // let state = req.query.state;
    // 使用解构的方式,获取请求参数
    let { pagenum, pagesize, cate_id, state } = req.query;

    // 生成where条件,完成筛选工作
    let w = '';
    if (cate_id) {
        w += ' AND cate_id=' + cate_id // 注意前面有空格
    }
    if (state) {
        w += ` AND state = '${state}'` // 注意前面有空格
    }

    // 查询总记录数
    db.query('SELECT count(*) AS total FROM ev_articles WHERE author_id=?' + w, req.user.id, (err, results) => {
        // 执行SQL语句失败
        if (err) return res.cc(err)
        if (results.length == 0) return res.cc('获取文章列表数目失败!')
        // console.log(results);
        // 成功
        let total = results[0].total

        // 下面查询数据,把结果响应给客户端
        // 下面是SQL比较长,可以先在 Navicat中查询,没问题,复制过来
        let sql = `SELECT a.Id, a.title, a.pub_date, a.state, c.name cate_name FROM ev_articles a
            JOIN ev_article_cate c ON a.cate_id=c.id
            JOIN ev_users u ON u.id=a.author_id
            WHERE u.id = ? ${w}
            LIMIT ${(pagenum - 1) * pagesize}, ${pagesize}`;

        db.query(sql, req.user.id, (err, results) => {
            // 执行SQL语句失败
            if (err) return res.cc(err)
            if (results.length == 0) return res.cc('获取文章列表失败!')
            // 成功
            res.send({
                status: 0,
                message: "获取文章列表成功!",
                data: results,
                total: total
            })
        })
    })
}

// 根据文章id删除文章
exports.deleteArticleById = (req, res) => {
    // 定义判断重复删除的 SQL 语句
    const sql = 'SELECT * FROM ev_articles WHERE id=?'
    db.query(sql, req.params.id, (err, results) => {
        // 执行 SQL 错误
        if (err) return res.cc(err)
        // 查看当前删除状态,判断是否已经删除
        if (results[0].is_delete == 1) return res.cc('不能重复删除!')
        // 不是重复删除
        // 根据文章id删除文章的 SQL 语句
        let sql = 'UPDATE ev_articles SET is_delete=1 WHERE id=?'
        db.query(sql, req.params.id, (err, results) => {
            // 执行SQL语句失败
            if (err) return res.cc(err)
            if (results.affectedRows != 1) return res.cc('删除文章失败!')
            // 成功
            res.cc('删除成功!', 0)
        })
    })
}

// 根据文章id获取文章
exports.getArticleById = (res, req) => {
    // SQL
    const sql = 'SELECT * FROM ev_articles WHERE id=?'
    db.query(sql, req.params.id, (err, results) => {
        // sql 执行失败
        if (err) return res.cc(err)
        // 判断结果长度
        if (results.length != 1) return res.cc('获取文章失败!')
        // 成功
        res.send({
            status: 0,
            message: '获取文章成功!',
            data: results[0]
        })
    })
}

// 根据文章id更新文章
exports.updateArticleById = (req, res) => {
    // console.log(req.file);
    if (!req.file || req.file.fieldname !== 'cover_img') return res.cc('文章封面是必选参数!')
    // 数据合法,后续发布操作
    const articleInfo = {
        // 展开
        ...req.body,
        // 文章封面的存放路径
        cover_img: req.file.fieldname == '/upload/cover_img' ? req.file.filename : path.join('/upload', req.file.filename)
    }
    // 定义查重 SQL 语句, 将本id 排除,查询别的
    const sql = 'SELECT * FROM ev_articles WHERE id<>? AND title=?'
    db.query(sql, [req.body.id, req.body.title], (err, results) => {
        // 执行 SQL 错误
        if (err) return res.cc(err)

        // 判断是否是标记删除的
        // console.log(results[0].isdelete);

        // 判断数据lenth
        if (results.length == 1) return res.cc('文章名称被占用,请更换后重试!')

        // 更新操作
        const sql = 'UPDATE ev_articles SET ? WHERE id=?'
        db.query(sql, [articleInfo, req.body.id], (err, results) => {
            // 执行 SQL 语句失败
            if (err) return res.cc(err)
            // 判断影响行数
            if (results.affectedRows != 1) return res.cc('更新文章失败!')
            // 成功
            res.cc('更新文章成功!', 0)
        })
    })
}

验证规则模块 schema/article.js

// 导入定义验证规则的模式
const Joi = require('joi')

// 定义 标题、分类id、内容、发布状态 的校验规则
const title = Joi.string().required()
const cate_id = Joi.number().integer().min(1).required()
const content = Joi.string().required().allow('')
const state = Joi.string().valid('已发布', '草稿').required()

// 定义 页码、每页显示条数 的校验规则
const pagenum = Joi.number().integer().min(1).default(1).required()
const pagesize = Joi.number().integer().min(1).max(50).required()

// 定义 id 校验规则
const id = Joi.string().required()

// 校验规则对象 - 发布文章,并共享出去
exports.add_article_schema = {
    body: {
        title,
        cate_id,
        content,
        state
    }
}

// 校验规则对象 - 查问文章列表,并共享出去
exports.list_article_schema = {
    query: {
        pagenum,
        pagesize,
        cate_id: Joi.string(),
        state: Joi.string().valid('已发布', '草稿')
    }
}

// 校验规则对象 - 查询文章列表,并共享出去
exports.delete_article_schema = {
    params: {
        id: id
    }
}

// 校验规则对象 - 根据id获取文章,并共享出去
exports.select_article_schema = {
    params: {
        id: id
    }
}

// 校验规则对象 - 根据id更新文章,并共享出去
exports.update_article_schema = {
    body: {
        id: Joi.number().integer().min(1).required(),
        title: title,
        cate_id: cate_id,
        content: content,
        state: state
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在区分服务器Node.js和测试环境Node.js时,可以通过以下几种方式进行区分: 1. 环境变量:在服务器和测试环境中,可以设置不同的环境变量来区分它们。可以在服务器上设置一个名为"NODE_ENV"的环境变量,值为"production",而在测试环境中设置为"development"或其他自定义值。然后在Node.js应用程序中,可以通过访问process.env.NODE_ENV来获取当前环境变量的值,从而区分服务器和测试环境。 2. 配置文件:可以为服务器和测试环境分别创建不同的配置文件。在服务器上使用一个名为"config.production.js"的配置文件,而在测试环境中使用一个名为"config.development.js"的配置文件。然后在Node.js应用程序中,根据当前环境加载相应的配置文件,从而区分服务器和测试环境。 3. 日志输出:在Node.js应用程序中,可以在服务器和测试环境中分别输出不同的日志信息。可以在服务器上输出生产环境相关的日志信息,而在测试环境中输出开发环境相关的日志信息。通过查看日志输出,可以区分服务器和测试环境。 4. 数据库连接:在Node.js应用程序中,可以使用不同的数据库连接来区分服务器和测试环境。可以在服务器上使用一个生产环境的数据库连接,而在测试环境中使用一个开发环境的数据库连接。通过查看数据库连接信息,可以区分服务器和测试环境。 5. 端口监听:在Node.js应用程序中,可以在服务器和测试环境中分别监听不同的端口。可以在服务器上监听80或443端口,而在测试环境中监听其他非常用端口。通过查看应用程序监听的端口,可以区分服务器和测试环境。 这些是区分服务器Node.js和测试环境Node.js的几种常见方式。根据具体的需求和场景,可以选择适合的方式来区分它们。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Pluto_ssy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值