# sh-server **Repository Path**: coolwg/sh-server ## Basic Information - **Project Name**: sh-server - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 3 - **Created**: 2021-05-09 - **Last Updated**: 2021-08-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## Express ### express 介绍 - Express 是一个第三方模块,用于快速搭建服务器(替代http模块) - Express 是一个基于 Node.js 平台,快速、开放、极简的 **web 开发框架**。 - Express保留了http模块的基本API,使用express的时候,也能使用http的API - 使用express的时候,仍然可以使用http模块的方法,比如 res.end()、req.url - express还额外封装了一些新方法,能让我们更方便的搭建服务器 - express提供了中间件功能,其他很多强大的第三方模块都是基于express开发的 - [Express 官网](http://expressjs.com/) - [Express 中文文档(非官方)](http://www.expressjs.com.cn/) - [Express GitHub仓库](https://github.com/expressjs/express) - [菜鸟教程](https://www.runoob.com/w3cnote/express-4-x-api.html) - [腾讯云开发者手册](https://cloud.tencent.com/developer/doc/1079) - 百度自行搜索 ### 安装 express 项目文件夹中,执行 `npm i express`。即可下载安装express。 > 注意:express不能安装在express文件夹中。否则安装失败。 ### 使用Express构造Web服务器 使用Express构建Web服务器步骤: 1) 加载 express 模块 2) 创建 express 服务器 3) 开启服务器 4) 监听浏览器请求并进行处理 ```js // 使用express 搭建web服务器 // 1) 加载 express 模块 const express = require('express'); // 2) 创建 express 服务器 const app = express(); // 3) 开启服务器 app.listen(3006, () => console.log('express服务器开始工作了')); // 4) 监听浏览器请求并进行处理 app.get('GET请求的地址', 处理函数); app.post('POST请求的地址', 处理函数); ``` ### express封装的新方法 express之所以能够实现web服务器的搭建,是因为其内部对核心模块http进行了封装。 封装之后,express提供了非常方便好用的方法。 比如前面用到的 `app.get() 和 app.post()` 就是express封装的新方法。 下面再介绍一个 `res.send()` 方法 - 该方法可以代替之前的 res.end 方法,而且比 res.end 方法更好用 - res.send() 用于做出响应 - 响应的内容同样不能为数字 - 如果响应的是JS对象,那么方法内部会自动将对象转成JSON格式。 - 而且会自动加Content-Type响应头 - 如果已经做出响应了,就不要再次做出响应了。 ```js const express = require('express'); const app = express(); app.listen(3006, () => console.log('启动了')); // 写接口 app.get('/api/test', (req, res) => { // res.end('hello world,哈哈哈'); // 响应中文会乱码,必须自己加响应头 // res.end(JSON.stringify({ status: 0, message: '注册成功' })); // 只能响应字符串或者buffer类型 // express提供的send方法,可以解决上面的两个问题 res.send({ status: 0, message: '注册成功' }); // send方法会自动设置响应头;并且会自动把对象转成JSON字符串 }); ``` > 请注意,在express中,我们仍然可以使用http模块中的方法和属性,比如req.url。 ## 案例 - 登录注册接口 ### 使用GIT管理项目 ``` - bigevent-server - index.js - db.js - .gitignore - package.json - package-lock.json ---- 被忽略 - node_modules ---- 被忽略 ``` 搭建好项目目录之后。使用Git初始化。 设置忽略文件(`.gitignore`),这个忽略文件中记录的文件、文件夹不会添加到暂存区,不会提交到本地仓库,当然也就不会推送到远程仓库。 ``` # git的忽略文件 # 忽略文件中指定的 文件、文件夹 不会被添加到暂存区,不会提交到本地仓库,不会推送到远程仓库 node_modules package-lock.json ``` 设置好忽略文件之后,下一步add、commit、push。即可。 **如果误把该忽略的文件add了,commit了。怎么办?** - `git rm -r --cached 文件` (只移除本地仓库的文件,不删除工作区的文件) - `git add .` (重新添加一次) - `git commit -m 'xxx'` (重新提交即可) 这样做完,如果发现vscode文件颜色没有变化,重启vscode再看看。 **忽略文件的语法** [官方文档](https://git-scm.com/book/zh/v2/Git-%E5%9F%BA%E7%A1%80-%E8%AE%B0%E5%BD%95%E6%AF%8F%E6%AC%A1%E6%9B%B4%E6%96%B0%E5%88%B0%E4%BB%93%E5%BA%93) ``` # 只忽略根目录里面的 node_modules /node_modules # 忽略所有叫做 node_modules 的文件夹 node_modules/ # 忽略所有的 .a 文件 *.a # 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件 !lib.a # 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO /TODO # 忽略任何目录下名为 build 的文件夹 build/ # 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt doc/*.txt # 忽略 doc/ 目录及其所有子目录下的 .pdf 文件 doc/**/*.pdf ``` ### 创建数据表 | 字段 | 类型 | 长度 | 不是null | 主键 | 其他 | | -------- | -------- | ---- | -------- | ---- | ---------- | | id | int | | √ | 🔑 | √ 自动递增 | | username | varchar | 10 | √ | | | | password | char | 32 | √ | | | | user_pic | longtext | | | | | | nickname | varchar | 10 | | | | | email | varchar | 30 | | | | ### 使用ApiPost模拟注册请求 ![image-20210122173242412](README.assets/image-20210122173242412.png) ### 写接口 ```js // 完成接口项目 // 前面三行启动服务 const express = require('express'); const app = express(); app.listen(3006, () => console.log('启动了')); // 配置 + 写接口 // -------------------- 注册接口 ---------------------- // 请求体:username password app.post('/api/reguser', (req, res) => { // 1. 接口要接收数据 // 2. 判断账号是否已经被占用了 // 3. 如果没有被占用,把账号密码添加到数据库 }); ``` ### 服务端使用 req.body 接收请求体 请求体就是客户端提交的数据(username和password)。 ```js // 完成接口项目 // 前面三行启动服务 const express = require('express'); const app = express(); app.listen(3006, () => console.log('启动了')); // 配置 + 写接口 app.use(express.urlencoded({ extended: true })); // -------------------- 注册接口 ---------------------- // 请求体:username password app.post('/api/reguser', (req, res) => { // 1. 接口要接收数据 console.log(req.body); // { username: 'laotang', password: '123456' } // 2. 判断账号是否已经被占用了 // 3. 如果没有被占用,把账号密码添加到数据库 }); ``` > 代码写完,一定要使用ApiPost发送请求,测试代码。 ### 验证用户名是否存在 思路:根据用户名查询,看是否能够查到数据。 - 没有查询数据,说明这个用户名不存在,能够使用 - 如果查到数据库,说明这个用户名已经存在,不能使用 ```js // 完成接口项目 // 前面三行启动服务 const express = require('express'); const app = express(); app.listen(3006, () => console.log('启动了')); // 配置 + 写接口 app.use(express.urlencoded({ extended: true })); // -------------------- 注册接口 ---------------------- // 请求体:username password app.post('/api/reguser', (req, res) => { // 1. 接口要接收数据 console.log(req.body); // { username: 'laotang', password: '123456' } let { username, password } = req.body; // 2. 判断账号是否已经被占用了 db('select * from user where username="${username}"', (err, result) => { if (err) throw err; // console.log(result); // 查到信息,result是非空数组;没有查到信息,result是空数组 if (result.length > 0) { res.send({ status: 1, message: '用户名被占用了' }); } else { // 没有被占用 // 3. 如果没有被占用,把账号密码添加到数据库 } }) }); ``` ### 完成注册 如果用户名可用,则添加到数据表中,完成注册 ```js // -------------------- 注册接口 ---------------------- // 请求体:username password app.post('/api/reguser', (req, res) => { // 1. 接口要接收数据 // console.log(req.body); // { username: 'laotang', password: '123456' } let { username, password } = req.body; // 2. 判断账号是否已经被占用了 db(`select * from user where username='${username}'`, (err, result) => { if (err) throw err; // console.log(result); // 查到信息,result是非空数组;没有查到信息,result是空数组 if (result.length > 0) { res.send({ status: 1, message: '用户名被占用了' }); } else { // 没有被占用 // 3. 如果没有被占用,把账号密码添加到数据库 db(`insert into user set username='${username}', password='${password}'`, (e, r) => { if (e) throw e; res.send({ status: 0, message: '注册成功' }); }); } }); }); ``` ### 对密码进行md5加密 安全起见,数据表中不能存储明文密码。必须存储加密后的密码,而且应该使用一种不可逆的加密方案。 常用的加密方式是 md5。 - 下载安装第三方加密模块,并解构里面的 md5 方法 `let { md5 } = require('utility')` - 对密码进行加密 `password = md5(password)` ### 客户端模拟登录请求 ![image-20210123105526396](README.assets/image-20210123105526396.png) ### 完成登录接口 - 接口要接收账号和密码(前面已经配置好 app.use(express.urlencoded({ extended: true }))),所以还是直接使用req.body - 对密码进行加密 - 使用账号和加密的密码当条件,查询。 ```js /** * 登录接口 * 请求方式:POST * 接口地址:/api/login * 请求体:username | password */ app.post('/api/login', (req, res) => { // console.log(req.body); // { username: 'laotang', password: '123456' } let { username, password } = req.body; password = md5(password); // 使用username和加密的密码当做条件,查询。 let sql = `select * from user where username='${username}' and password='${password}'`; db(sql, (err, result) => { if (err) throw err; // console.log(result); // 没有查到结果得到空数组; 查到结果得到非空数组 if (result.length > 0) { res.send({ status: 0, message: '登录成功' }) } else { res.send({ status: 1, message: '账号或密码错误' }) } }) }); ``` ### 创建token 使用第三方模块 jsonwebtoken 创建token字符串。 - 下载安装 npm i jsonwebtoken - 加载模块 const jwt = require('jsonwebtoken'); - 调用 jwt.sign() 方法创建token - 参数1:必填,对象形式;希望在token中保存的数据 - 参数2:必填,字符串形式;加密的钥匙;后续解密token的时候,还需要使用。 - 参数3:可选,对象形式;配置项,比如可以配置token的有效期 - 参数4:可选,函数形式;生成token之后的回调 - 生成的token前面,必须拼接 `Bearer ` 这个字符串。 ```js if (result.length > 0) { // 登录成功,生成token // 在token中保存用户的id // token前必须加 “Bearer ”,注意空格 let token = 'Bearer ' + jwt.sign({ id: result[0].id }, 'sfsws23s', { expiresIn: '2h' }); res.send({ status: 0, message: '登录成功', token }) } else { res.send({ status: 1, message: '账号或密码错误' }) } ``` ## Express路由 - 路由:即请求和处理程序的映射关系。 - 使用路由的好处: - 降低匹配次数,提高性能 - 分类管理接口,更易维护与升级 - 使用步骤: ``` /** * 使用路由文件的步骤 * 1. 加载express模块 * 2. 创建 router 对象 * 3. 把接口挂载到 router 对象上 * 4. 导出 router 对象 * * index.js 中 * 5. 加载路由模块,并注册成中间件 */ ``` - 注意事项: - 路由文件如果没有导出 router,那么在 入口文件中不要注册中间件,否则报错 - 哪个路由文件中使用了db,自己加载(谁用谁加载) ![image-20210123121226492](README.assets/image-20210123121226492.png) ***login.js*** ```js // 加载所需的模块 const db = require('../db'); // const utils = require('utility'); // { noop: fun..., try: fun..., md5: fun..., sha1: fun... } let { md5 } = require('utility'); let jwt = require('jsonwebtoken'); // --------------------- 使用路由的步骤 ---------------------- // 1. 加载express const express = require('express'); // 2. 创建路由对象,实则是个函数类型 const router = express.Router(); // 3. 写接口,把接口挂载到 router 上 router.post('/reguser', (req, res) => {}); router.post('/login', (req, res) => {}); // 一定要导出 router module.exports = router; ``` ***index.js*** ```js // 三行必须的代码,启动服务 const express = require('express'); const app = express(); app.listen(3006, () => console.log('启动了')); // 下面一行的配置的意思,接收客户端提交的请求体;并且赋值给req.body (req.body = { 客户端提交的数据 }) app.use(express.urlencoded({ extended: true })); // 加载自定义的路由模块,注册中间件 let loginRouter = require('./routers/login'); app.use('/api', loginRouter); ``` ## Express中间件 ### 中间件介绍 - 中间件(Middleware ),特指业务流程的中间处理环节。 - 中间件,是express最大的特色,也是最重要的一个设计 - 很多第三方模块,都可以当做express的中间件,配合express,开发更简单。 - 一个express应用,是由各种各样的中间件组合完成的 - 中间件,本质上就是一个函数 ### 中间件原理 为了理解中间件,我们先来看一下我们现实生活中的自来水厂的净水流程。 ![image-2020033130641861](README.assets/image-20200331130641861.png) - 在上图中,自来水厂从获取水源到净化处理交给用户,中间经历了一系列的处理环节 - 我们称其中的每一个处理环节就是一个中间件。 - 这样做的目的既提高了生产效率也保证了可维护性。 express中间件原理: ![image-2020033104703510](README.assets/image-20200331004703510.png) ### 中间件的几种形式 ```js // 下面的中间件,只为当前接口 /my/userinfo 这个接口服务 app.get('/my/userinfo', 中间件函数); // 下面的几个中间件,是处理 /api/login 接口的 app.post('/api/login', 中间件函数, 中间件函数, 中间件函数, 中间件函数 .....); // app.use 中的中间件,可以处理所有的GET请求和所有的POST请求,没有指定路径,那么处理所有接口 app.use(中间件函数); // 下面的中间件函数,只处理 /api 开头的接口 app.use('/api', 中间件函数); // 下面的中间件函数,处理 /abcd 、 /abd 这两个接口 app.use('/abc?d', 中间件函数); ``` > app.get或者app.post表示写接口,必须写接口地址; > > app.use() 参数1路由前缀,可以省略。另外无论是GET还是POST方式的请求,都会进入该中间件。 ### 中间件语法 - 中间件就是一个函数 - 中间件函数中有四个基本参数, err、req、res、next - 如果写两个参数,那么两个参数肯定是 req 和 res - 如果写三个参数,那么三个参数肯定是 req, res 和 next - 如果写四个参数,那么就是全部的参数,这个中间件叫做错误处理中间件。 - 把写好的中间件函数,传递给 `app.get()`、`app.post()`、`或app.use()`使用 ### 中间件的特点 ![image-20210124225632141](README.assets/image-20210124225632141.png) - 每个中间件函数,共享req对象、共享res对象,即所有的req对象是一个对象;所有的res是一个对象 - 比如上述中间件1,为 req 对象添加了 body属性。中间件5中可以直接使用。 - 不调用`next()`,请求会卡在当前中间件;调用 `next()` 表示将请求交给下一个中间件处理。 - 上述中间件中的 `next()` 保证了 **请求 ~ 响应** 能够完整的进行完。 - **没有**给 `next()` 传递参数,则正常进入下一个中间件。 - **有**给 `next(err)` 传递参数,则直接进入错误处理中间件 - 所有请求都可以进入使用`app.use()`注册的中间件,但要注意前缀 - 中间件1,给出接口前缀 `/api` ,所以只有 `/api` 开头的接口才能进入 - 中间件2,给出接口前缀 `/my` ,所以只有 `/my` 开头的接口才能进入 - 中间件3,没有给出接口前缀,所以任何接口都能进入 - 错误处理中间件,必须传递 err、req、res、next四个参数,而且要放到所有接口的后面 - 一般用于同一处理错误信息。 - 错误处理中间件,也可以继续`next(错误)` ,把错误交给后续的错误处理中间件处理。 ### 中间件分类 - 应用级别的中间件(**index.js 中的中间件,全局有效** ) - 路由级别的中间件(**路由文件中的中间件,只在当前路由文件中有效**) - 错误处理中间件(**四个参数都要填,一般放到最后**) - 内置中间件(express自带的,比如 `express.urlencoded({ extended: true })`) - 第三方中间件(比如multer、express-jwt、express-session、....) > 实际开发中,自己写中间件的机会并不大,一般都有对应的第三方中间件。 ## 案例 - 服务端数据验证 - 客户端提交数据给服务器,服务器端也要进行验证。 - 开发领域,有一句话,叫做 “**永远不要相信客户端的数据**” ### 任务 验证,账号必须是2~10位,且只能使用数字、字母下划线组合,且必须是字母开头 验证,密码必须是6~12位,且不能有空格 ### 验证流程 ![image-20210123163811094](README.assets/image-20210123163811094.png) ### 验证中间件 ```js // 必须在这里,注册中间件,完成数据的验证 router.use((req, res, next) => { // 获取username和password let { username, password } = req.body; // 验证用户名 if (!/^[a-zA-Z][0-9a-zA-Z_]{1,9}$/.test(username)) { next('用户名只能包含数组字母下划线,长度2~10位,字母开头'); } else if (!/^\S{6,12}$/.test(password)) { next('密码6~12位且不能出现空格'); } else { next(); } }); ``` ### 错误处理中间件 ```js // 错误处理中间件 router.use((err, req, res, next) => { // err 就是前面next过来的参数值 res.send({ status: 1, message: err }) }); ``` ## 服务端身份认证 ### JWT原理回顾 对于前后端分类模式的开发,大多使用 JWT(json web token)进行身份认证。 前面已经讲过JWT的原理了,使用下图回顾一下。 ![image-20210124230843893](README.assets/image-20210124230843893.png) ### 分析服务端该做什么 大事件中已经把前端该做的完成了,剩下的就是后端的任务了。 前端: - 登录成功后,保存token - 请求 `/my/xxx` 接口时,在请求头中加入 `Authorization` 字段,值为token。 后端: - 登录接口生成token - 判断,如果客户端请求的是 `/my/xxx` 接口,要解析并验证token的真伪 ![image-20210124232450756](README.assets/image-20210124232450756.png) ### 代码实现认证 选择使用 [express-jwt](https://gitee.com/mirrors_auth0/express-jwt) 第三方模块进行身份认证。从模块名可以看出,该模块是专门配合express使用的。 下载安装:`npm i express-jwt` 后端 index.js 中,当接收到一个请求后,先解析并验证token。 ```js const jwt = require('express-jwt'); // app.use(jwt().unless()); // jwt() 用于解析token,并将 token 中保存的数据 赋值给 req.user // unless() 完成身份认证 app.use(jwt({ secret: 'sfsws23s', // 生成token时的 钥匙,必须统一 algorithms: ['HS256'] // 必填,加密算法,无需了解 }).unless({ path: ['/api/login', '/api/reguser'] // 除了这两个接口,其他都需要认证 })); ``` 上述代码完成后,当一个请求发送到服务器后,就会验证请求头中的 Authorization 字段了。 - 如果没有问题 1. 将token中保存的 **用户id** 赋值给 **req.user** 2. next()。 - 如果有问题,则 **next(错误信息)**。 所以,还需要在index.js 最后,加入错误处理中间件,来提示token方面的错误。文档中抄下来,修改。 ```js app.use((err, req, res, next) => { if (err.name === 'UnauthorizedError') { // res.status(401).send('invalid token...'); res.status(401).send({ status: 1, message: '身份认证失败!' }); } }); ``` ## 案例 - 个人中心接口 ### 设计数据表 因为已经做过登录注册了,所以,这里不用再次创建了。 ### 使用路由模块 ***/routers/user.js*** ```js // user路由文件 const express = require('express'); const router = express.Router(); // 导出 module.exports = router; ``` index.js中加载路由模块,注册中间件 ***index.js*** ```js app.use('/my/user', require('./routers/user')); ``` ### 获取用户信息接口 ApiPost模拟请求 ![image-20210125094953747](README.assets/image-20210125094953747.png) 接口代码: ```js // *************************** 获取用户信息 ***************************/ /** * 请求方式:GET * 接口地址:/my/user/userinfo * 参数: 无 */ router.get('/userinfo', (req, res) => { // console.log(req.user); // { id: 1, iat: 1611537302, exp: 1611544502 } // return; // 查询当前登录账号的信息,并不是查询所有人的信息 db('select * from user where id=' + req.user.id, (err, result) => { if (err) throw err; res.send({ status: 0, message: '获取用户信息成功', data: result[0] }) }); }); ``` ### 更新用户信息接口(有问题) 客户端模拟请求: ![image-20210125100901716](README.assets/image-20210125100901716.png) 代码实现: ```js // *************************** 更新用户信息 ***************************/ /** * 请求方式:POST * 接口地址:/my/user/userinfo * Content-Type: application/x-www-form-urlencoded * 请求体:email | nickname | id */ router.post('/userinfo', (req, res) => { // console.log(req.body); // { nickname: '老汤', email: '23323@qq.com', id: '1' } let { id, nickname, email } = req.body; if (id != req.user.id) return res.send({ status: 1, message: '无权更新' }); let sql = `update user set nickname='${nickname}', email='${email}' where id=${id}`; db(sql, (err, result) => { if (err) throw err; res.send({ status: 0, message: '更新用户信息成功' }); }) }); ``` ### 更换头像接口 客户端模拟请求: ![image-20210125105236185](README.assets/image-20210125105236185.png) 代码实现: ```js // *************************** 更换头像接口 ***************************/ /** * 请求方式:POST * 接口地址:/my/user/avatar * Content-Type: application/x-www-form-urlencoded * 请求体:avatar */ router.post('/avatar', (req, res) => { // console.log(req.body); // { avatar: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHg' } let sql = `update user set user_pic='${req.body.avatar}' where id=${req.user.id}`; db(sql, (err, result) => { if (err) throw err; res.send({ status: 0, message: '更换头像成功' }) }) }); ``` ### 重置密码接口 ## ApiPost认证 登录之后,把token字符串,保存到ApiPost的全局变量中。 ![image-20210125103014458](README.assets/image-20210125103014458.png) 登录之后,就会在ApiPost的全局变量中,保存上一个token。可以点击小眼睛查看: ![image-20210125103125298](README.assets/image-20210125103125298.png) 其他需要认证的接口,选择认证,Bearer auth认证,填写 {{token}} 即可。 ApiPost发送请求的时候,会自动携带Authorization 这个请求头,并且会在token字符串前加上“Bearer ”,从而完成身份认证。 ![image-20210125103257284](README.assets/image-20210125103257284.png) ## 案例 - 类别管理接口 ### 设计数据表并添加模拟数据 **导入SQL**。(user、category、article表中的数据有关联。必须全部导入,然后使用账号 admin、密码admin登录) ### 使用路由模块 ***/routers/category.js*** ```js // category路由文件 const express = require('express'); const router = express.Router(); // 导出 module.exports = router; ``` index.js中加载路由模块,注册中间件 ***index.js*** ```js app.use('/my/category', require('./routers/category')); ``` ### 完成获取分类列表数据的接口 **注意加载db.js** ```js // ----------------- 获取分类的接口 ------------------ /** * 请求方式:GET * 接口地址: /my/category/list * 参数:无 */ router.get('/list', (req, res) => { // 调用db函数,查询所有的分类 db('select * from category', (err, result) => { if (err) throw err; // 没有错误的话,做出响应 res.send({ status: 0, message: '获取分类成功', data: result }); }); }); ``` ### 删除分类的接口 客户端发送请求,并且传递id参数 ![image-20201225145042976](README.assets/image-20201225145042976.png) 服务端代码: ```js // ----------------- 删除分类的接口 ------------------ /** * 请求方式:GET * 接口地址: /my/category/delete * 请求参数: id(分类id),类型querystring */ router.get('/delete', (req, res) => { // 获取id参数 var id = req.query.id; // 删除数据表中的数据 db('delete from category where id=' + id, (err, result) => { // 做出响应 if (err) throw err; if (result.affectedRows > 0) { res.send({status: 0, message: '删除分类成功'}); } else { res.send({status: 1, message: '删除分类失败'}) } }); }); ``` ### 添加分类的接口 ![image-20201225150033489](README.assets/image-20201225150033489.png) 服务端代码: 由于 index.js 中,有 `app.use(express.urlencoded({ extended: true }))` ,所以这里直接使用 `req.body` 接收请求体。 ```js // ----------------- 添加分类的接口 ------------------ /** * 请求方式:POST * 接口地址: /my/category/add * 请求参数: name(类别名称) | alias(类别别名) * Content-Type: application/x-www-form-urlencoded */ router.post('/add', (req, res) => { // 1. 接收客户端提交的数据(name和alias) // console.log(req.body); // { name: '娱乐', alias: 'yule' } let { name, alias } = req.body; // 2. 添加到数据库 let sql = `insert into category set name='${name}', alias='${alias}'`; db(sql, (err, result) => { if (err) throw err; // 3. 做出响应 res.send({ status: 0, message: '添加分类成功' }) }); }); ``` ### 更新分类接口 ![image-20201225153614367](README.assets/image-20201225153614367.png) 服务端代码: ```js // ----------------- 修改分类的接口 ------------------ /** * 请求方式:POST * 接口地址: /my/category/update * 请求参数: name(类别名称) | alias(类别别名) | id(分类的id) * Content-Type: application/x-www-form-urlencoded */ router.post('/update', (req, res) => { // 1. 接收客户端提交的数据(id、name、alias) // console.log(req.body); // { id: '1', name: '科技', alias: 'keji' } let { id, name, alias } = req.body; // 2. 执行update语句,修改数据 let sql = `update category set name='${name}', alias='${alias}' where id=${id}`; db(sql, (err, result) => { if (err) throw err; if (result.affectedRows > 0) { res.send({ status: 0, message: '修改分类成功' }) } else { res.send({ status: 1, message: '修改分类失败' }) } }) }); ``` ## 案例 - 文章相关接口 ### 使用路由模块 ***/routers/article.js*** ```js // category路由文件 const express = require('express'); const router = express.Router(); // 导出 module.exports = router; ``` index.js中加载路由模块,注册中间件 ***index.js*** ```js app.use('/my/article', require('./routers/article')); ``` ### 分页获取文章接口 **注意引入 db.js** > 直接复制代码。因为这个接口中,涉及到 MySQL 连表查询和统计查询。 ```js // ---------------- 分页获取文章列表 ---------------- // 接口要求: /** * 请求方式:GET * 请求的url:/my/article/list * 请求参数: * - pagenum -- 页码值 * - pagesize -- 每页显示多少条数据 * - cate_id -- 文章分类的Id * - state -- 文章的状态,可选“草稿”或“已发布” */ router.get('/list', (req, res) => { // console.log(req.query); // 设置变量,接收请求参数 let { pagenum, pagesize, cate_id, state } = req.query; // console.log(cate_id, state); // 根据cate_id 和 state制作SQL语句的条件 let w = ''; if (cate_id) { w += ` and cate_id=${cate_id} `; } if (state) { w += ` and state='${state}' `; } // 分页查询数据的SQL(该SQL用到了连表查询,并且使用了很多变量组合) let sql1 = `select a.id, a.title, a.state, a.pub_date, c.name cate_name from article a join category c on a.cate_id=c.id where author_id=${req.user.id} and is_delete=0 ${w} limit ${(pagenum - 1) * pagesize}, ${pagesize}`; // 查询总记录数的SQL,查询条件和前面查询数据的条件 必须要一致 let sql2 = `select count(*) total from article a join category c on a.cate_id=c.id where author_id=${req.user.id} and is_delete=0 ${w}`; // 分别执行两条SQL(因为db查询数据库是异步方法,必须嵌套查询) db(sql1, (err, result1) => { if (err) throw err; db(sql2, (e, result2) => { if (e) throw e; res.send({ status: 0, message: '获取文章列表数据成功', data: result1, total: result2[0].total }); }) }) }); ``` ### 添加文章接口 ```js // ---------------- 添加文章接口 -------------------- /** * 接口地址:/my/article/add * 请求方式:POST * 请求体:title | content | cate_id | state | cover_img * Content-Type: multipart/form-data */ ``` 这是我们遇到的第一个请求体为FormData类型的接口。 服务端获取FormData类型的数据,需要使用第三方模块 [multer](https://github.com/expressjs/multer/blob/master/doc/README-zh-cn.md)。 安装:`npm i multer` 加载:`const multer= require('multer')` 配置上传文件路径:`const upload = multer({ desc: 'uploads/' })` 接口中使用: ```js router.post('/add', upload.single('cover_img'), (req, res) => { // upload.single() 方法用于处理单文件上传 // cover_img 图片字段的名字 // 通过 req.body 接收文本类型的请求体,比如 title,content等 // 通过 req.file 获取上传文件信息 }); ``` 只要客户端请求这个接口,就会**自动创建** `uploads` 文件夹,并把文件上传到该文件夹。 此时,可以使用ApiPost测试: ![image-20210125001007531](README.assets/image-20210125001007531.png) 注意,大事件接口文档规定,客户端只能提交 “title、content、cate_id、state、cover_img” 5个值。 而,article 数据表中,还要求添加 `author_id`(用户id)、`pub_date`(发布时间),这两个字段的值只能自己来添加了。 发布时间的处理,需要使用 `moment` 模块,自行安装。 ```js var multer = require('multer') var upload = multer({ dest: 'uploads/' }); // 配置上传文件的目录 const moment = require('moment'); router.post('/add', upload.single('cover_img'), (req, res) => { // req.body 表示文本信息 // req.file 表示上传的文件信息 // console.log(req.file); // req.file.filename 表示上传之后的文件名 // 把数据添加到数据表中存起来 // req.body = { title: 'xx', content: 'xx', cate_id: 1, state: 'xx' } let { title, content, cate_id, state } = req.body; // 其他字段 let pub_date = moment().format('YYYY-MM-DD HH:mm:ss'); let cover_img = req.file.filename; let author_id = req.user.id; // console.log(obj); // return; let sql = `insert into article set title='${title}', content='${content}', cate_id=${cate_id}, state='${state}', pub_date='${pub_date}', cover_img='${cover_img}', author_id=${author_id}`; db(sql (err, result) => { if (err) throw err; if (result.affectedRows > 0) { res.send({ status: 0, message: '发布成功' }) } else { res.send({ status: 1, message: '发布失败' }) } }) }); ``` ### 删除文章接口 客户端模拟请求: ![image-20210125144230959](README.assets/image-20210125144230959.png) 删除,可以做成 “软删除” 效果。即不是真的删除,而是把 is_delete 字段改为 1. - is_delete = 0 ,正常的文章。获取文章的时候,只获取 id_delete=0 的文章。 - is_delete = 1, 表示已删除的文章。 ```js // ---------------- 删除文件接口 -------------------- /** * 请求方式:GET * 接口地址:/my/article/delete/2 * 请求参数:id,url参数 */ // router.get('/delete/:id/:age/:name', (req, res) => { router.get('/delete/:id', (req, res) => { let id = req.params.id; let sql = `update article set is_delete=1 where id=${id} and author_id=${req.user.id}`; db(, (err, result) => { if (err) throw err; if (result.affectedRows > 0) { res.send({ status: 0, message: '删除成功' }) } else { res.send({ status: 1, message: '删除失败' }) } }) }); ``` ### 更新文章接口 ```js router.post('/update', upload.single('cover_img'), (req, res) => { // 和添加文章接口差不多,要注意,客户端多提交了文章id,这是我们修改文章的条件 // req.body = { title: 'xx', content: 'xx', cate_id: 1, state: 'xx', id: 6 } let { title, content, cate_id, state, id } = req.body; // 其他字段(发布时间,不是修改时间,所以不需要改了,用户id也不需要改) let cover_img = req.file.filename; // console.log(obj); // return; let sql = `update article set title='${title}', content='${content}', cate_id=${cate_id}, state='${state}', cover_img='${cover_img}' where id=${id}`; db(sql, (err, result) => { if (err) throw err; if (result.affectedRows > 0) { res.send({ status: 0, message: '修改文章成功' }) } else { res.send({ status: 1, message: '修改文章失败' }) } }) }); ``` ### 根据id获取一篇文章接口 ```js // ---------------- 根据id获取一篇文章接口 ----------- router.get('/:id', (req, res) => { // 怎么获取url中的参数,答,使用express提供的 req.params // console.log(req.params.id); db('select * from article where id='+req.params.id, (err, result) => { if (err) throw err; res.send({ status: 0, message: '获取文章成功', data: result[0] }) }) }); ``` ## 小结Express接收客户端数据 ![image-20210125150815861](README.assets/image-20210125150815861.png) > req.user 是 express-jwt 解密token之后,把token里面保存的用户信息赋值给 req.user; ## 使用前端代码测试接口 ### 同源策略 同宗同源:比如,你和你的亲兄弟、亲姐妹,叫叫做同源。 浏览器中,也有同源策略。指的是打开页面的URL和Ajax请求的URL比较,比较他俩是否同源。 比如,打开页面的URL:http://127.0.0.1:5501/login.html 发送Ajax请求的URL:http://localhost:3006/api/login 判断两个URL是否同源,查看协议、主机地址、端口号,如果这三项都相同,则称这两个URL同源,否则非同源。 如果非同源,则以下三种行为受到限制: - DOM无法操作 - cookie不会自动携带 - Ajax请求无效 ![image-20210125154857091](README.assets/image-20210125154857091.png) 这就属性跨域请求,即**违反了同源策略的请求,就是跨域请求**。 ### 解决跨域-CORS CORS,叫做跨域资源共享,是XHR2.0中提出的一种新的解决跨域的方案。从IE10开始支持。 CORS方案的实现,是通过服务器的响应头来实现的。 服务器要设置:`Access-Control-Allow-Origin: '*或者一个具体的源'` 这里直接使用第三方模块 cors 来解决跨域。 - 安装 npm i cors - 加载 const cors = require('cors'); - 使用 app.use(cors()); // 注意,必须把这个中间件,放到最前面。 > 解决跨域的第二种方案,叫做JSONP方案。目前知道。