Node.js入门学习笔记有哪些?
- 内容介绍
- 文章标签
- 相关推荐
本文共计24438个文字,预计阅读时间需要98分钟。
Node.js简介:已掌握相关技术,如HTML、CSS、JavaScript,了解浏览器中的JavaScript组成、运行环境和运行环境定义,即指代码正确运行所需的环境。V8引擎负责解析和执行JavaScript代码。
Node.js简介 已经掌握了那些技术- HTML
- CSS
- JavaScript
运行环境是指 代码正确运行所使用的必要环境
- V8引擎负责解析和执行JavaScript代码。
- 内置API是由
运行环境提供的特殊接口,只能在所属的运行环境中被调用。
Node.js 是一个基于 Chrome's V8 的JavaScript运行环境。
Node.js的官方运行环境
Node.js是一个单线程的事件驱动的非阻塞性I/O模型
Node.ja可以做什么注意:
- 浏览器是 JavaScript 的前端运行环境。
- Node.js 是 JavaScript 的后端运行环境。
- Node.js 中无法调用 DOM 和 BOM 等浏览器内置 API。
Node.js 作为一个 JavaScript 的运行环境,仅仅提供了基础的功能和 API。然而,基于 Node.js 提供的这些基础能,很多强大的工具和框架如雨后春笋,层出不穷,所以学会了 Node.js ,可以让前端程序员胜任更多的工作和岗位:
1.基于 Express 框架
可以快速构建 Web 应用
2.基于 Electron 框架
可以构建跨平台的桌面应用
3.基于 restify 框架
可以快速构建 API 接口项目
4 .读写和操作数据库、创建实用的命令行工具辅助前端开发、etc…
Node.js学习路径-
JavaScript学习路径
JavaScript 基础语法 + 浏览器内置 API(DOM + BOM) + 第三方库(jQuery、art-template 等) -
Node.js学习路径
JavaScript 基础语法 + Node.js 内置 API 模块(fs、path、127.0.0.1:8080') }) req请求对象只要服务器接收到了客户端的请求,就会调用通过
res响应对象server.on()为服务器绑定的request 事件处理函数。如果想在事件处理函数中,`访问与客户端相关的数据或属性,可以使用如下的方式:在服务器的requset事件处理函数中,如果想访问与服务器相关的数据或属性,可以使用如下的方式。
server.on('request', (req, res) => { // req.url 是客户端请求的 URL 地址 const url = req.url // req.method 是客户端请求的 method 类型 const method = req.method const str = `Your request url is ${url}, and request method is ${method}` console.log(str) // 调用 res.end() 方法,向客户端响应一些内容 res.end(str) })解决中文乱码通过 res.setHeader参数,设置响应头,解决中文乱码问题。
const 127.0.0.1') })
127.0.0.1'); }) 实现clock时钟的web服务器注意:
解决中文乱码问题的解决方案是固定的。
charset和utf-8中间不能有空格,否则就会出现中文乱码。- 核心思路
把文件的实际存放路径,作为每个资源的请求url地址。
- 服务器充当的角色就是一个字符串的搬运工
- 实现步骤
- 导入需要的模块
- 创建基本的web服务器
- 将资源的请求url地址映射为文件的存放路径
- 读取文件内容并响应给客户端
- 优化资源请求路径
- 导入需要模块
- 127.0.0.1');
})
监听GET请求
通过
app.get()方法,可以监听客户端的 GET 请求,具体的语法格式如下:
监听POST请求app.get('/liyu', (req, res) => { //调用express提供的res.send方法向客户端提供一个json对象 res.send({ name: 'xz', age: 36, gender: '男' }) })通过
app.post()方法,可以监听客户端的 POST 请求,具体的语法格式如下:
把内容响应给客户端app.post('/liyu', (req, res) => { //调用express提供的res.send方法向客户端提供一个文本服务器 res.send('请求成功') })通过
res.send()方法,可以把处理好的内容,发送给客户端:app.get('/liyu', (req, res) => { //调用express提供的res.send方法向客户端提供一个json对象 res.send({ name: 'xz', age: 36, gender: '男' }) })获取 URL 中携带的查询参数通过
req.query对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数:
获取 URL 中的动态参数app.get('/', (req, res) => { //通过req.query获取客户端发送过来的查询参数 console.log(req.query); })通过
req.params对象,可以访问到 URL 中,通过 : 匹配到的动态参数:app.get('/liyu/:id', (req, res) => { //req.params是动态匹配到的url参数,默认空对象 console.log(req.params); res.send(req.params) })
托管静态资源注意:这里的id是一个动态的参数
- eexpress.static()
express 提供了一个非常好用的函数,叫做 express.static(),通过它,我们可以非常方便地创建一个静态资源服务器,例如,通过如下代码就可以将 public 目录下的图片、CSS 文件、JavaScript 文件对外开放访问了:
app.use(express.static('public')) app.use(express.static('files'))现在,你就可以访问 public 目录中的所有文件了:
localhost:3000/images/bg.jpg
托管多个静态资源目录注意:
Express 在指定的静态目录中查找文件,并对外提供资源的访问路径。因此,存放静态文件的目录名不会出现在 URL 中。如果要托管多个静态资源目录,请多次调用 express.static() 函数:
app.use(express.static('public'))
挂载注意:
访问静态资源文件时,express.static() 函数会根据目录的添加顺序查找所需的文件。路径前缀如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下的方式:
app.use('/files', express.static('./files'))现在,访问files时,就必须加上/files才可以访问到.
localhost:3000/files/index.html
Express路由 路由是什么广义上来讲,路由就是映射关系。
Express 中的路由在 Express 中,路由指的是
客户端的请求与服务器处理函数之间的映射关系。
Express 中的路由分 3 部分组成,分别是请求的类型、请求的 URL 地址、处理函数,格式如下:app.METHOD(PATH,HANDLER)例子//匹配GET请求,且请求URL为/ app.get('/',funcation(req,res){ res.send('hello World!') }) // 匹配POST请求,且请求URL为/ app.post('/',funcation(req,res){ res.send('Got a POST request') })路由的匹配过程每当一个请求到达服务器之后,
需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。在匹配时,会按照路由的顺序进行匹配,如果
请求类型和请求的 URL同时匹配成功,则 Express 会将这次请求,转交给对应的 function 函数进行处理。
路由的使用 最简单的写法注意点:
- 按照定义的
先后顺序进行匹配 请求类型和请求的url同时匹配成功才会调用对应的处理函数.
在Express中使用路由最简单的方式,就是把路由挂载到 app 上,示例如下:
const express = require('express') const app = express() // 挂载路由 app.get('/', (req, res) => { res.send('hello world.') }) app.post('/', (req, res) => { res.send('Post Request.') }) app.listen(80, () => { console.log('127.0.0.1') })模块化路由为了
方便对路由进行模块化的管理,Express不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块。将路由抽离为单独模块的步骤如下:- 创建路由模块对应的 .js 文件.
- 调用 express.Router() 函数创建路由对象
- 向路由对象上挂载具体的路由
- 使用 module.exports 向外共享路由对象
- 使用 app.use() 函数注册路由模块
- 创建路由模块
// 这是路由模块 // 1. 导入 express const express = require('express') // 2. 创建路由对象 const router = express.Router() // 3. 挂载具体的路由 router.get('/user/list', (req, res) => { res.send('Get user list.') }) router.post('/user/add', (req, res) => { res.send('Add new user.') }) // 4. 向外导出路由对象 module.exports = router- 注册路由模块
为路由模块添加前缀const express = require('express') const app = express() // app.use('/files', express.static('./files')) // 1. 导入路由模块 const router = require('./03.router') // 2. 注册路由模块 app.use('/api', router) // 注意: app.use() 函数的作用,就是来注册全局中间件 app.listen(80, () => { console.log('127.0.0.1') })类似于托管静态资源时,为静态资源统一挂载访问前缀一样,路由模块添加前缀的方式也非常简单:
app.use('/api', router)Express 中间件 什么是中间件中间件(Middleware ),特指
现实生活中的例子业务流程的中间处理环节.在处理污水的时候,一般都要经过
三个处理环节,从而保证处理过后的废水,达到排放标准。处理污水的这三个中间处理环节,就可以叫做中间件。
Express 中间件的调用流程当一个请求到达 Express 的服务器之后,可以
连续调用多个中间件,从而对这次请求进行预处理。
Express 中间件的格式注意:上一个中间件的输出会作为下一个中间件的输入。
Express 的中间件,本质上就是一个 function 处理函数,Express 中间件的格式如下:
next 函数的作用注意:
中间件函数的形参列表中,必须包含 next 参数。而路由处理函数中只包含 req 和 res。
定义中间件函数next 函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由。// 定义一个最简单的中间件函数 const mw = function (req, res, next) { console.log('这是最简单的中间件函数') // 把流转关系,转交给下一个中间件或路由 next() } // 将 mw 注册为全局生效的中间件 app.use(mw)全局生效的中间件客户端发起的
任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。
通过调用app.use(中间件函数),即可定义一个全局生效的中间件,示例代码如下:// 将 mw 注册为全局生效的中间件 app.use(mw)定义全局中间件的简化形式// 这是定义全局中间件的简化形式 app.use((req, res, next) => { console.log('这是最简单的中间件函数') next() })中间件的作用多个中间件之间,共享一份req和res,基于这样的特性,我们可以在上游的中间件中,统一为req和res为对象添加自定义的属性和方法,供下游的中间件或路由进行使用。
const express = require('express') const app = express() // 这是定义全局中间件的简化形式 app.use((req, res, next) => { // 获取到请求到达服务器的时间 const time = Date.now() // 为 req 对象,挂载自定义属性,从而把时间共享给后面的所有路由 req.startTime = time next() }) app.get('/', (req, res) => { res.send('Home page.' + req.startTime) }) app.get('/user', (req, res) => { res.send('User page.' + req.startTime) }) app.listen(80, () => { console.log('127.0.0.1') })定义多个全局中间件可以使用 app.use()
连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用,示例代码如下:const express = require('express') const app = express() //定义第一个全局中间件 app.use((req, res, next) => { console.log('调用了第一个全局中间件'); next() }) app.use((req, res, next) => { console.log('调用了第二个全局中间件'); next() }) //定义一个路由 app.get('/user', (req, res) => { res.send('User page!') }) app.listen(80, () => { console.log('127.0.0.1'); })局部生效的中间件不使用app.use() 定义的中间件,叫做局部生效的中间件,示例代码如下:// 导入 express 模块 const express = require('express') // 创建 express 的服务器实例 const app = express() //定义中间件函数 const mv1 = (req, res, next) => { console.log('定义了一个局部生效的中间件'); next() } //创建路由 app.get('/', mv1, (req, res) => { res.send('Home page!') }) app.get('/user', (req, res) => { res.send('User page!') }) // 调用 app.listen 方法,指定端口号并启动web服务器 app.listen(80, function () { console.log('Express server running at 127.0.0.1') })
定义多个局部中间件注意:
中间件只会再添加的路由函数中起作用。可以在路由中,通过如下两种等价的方式,使用多个局部中间件:
const mw1 = (req, res, next) => { console.log('调用了第一个局部生效的中间件') next() } const mw2 = (req, res, next) => { console.log('调用了第二个局部生效的中间件') next() } // 2. 创建路由,两种方式是“完全等价的” app.get('/', [mw1, mw2], (req, res) => { res.send('Home page.') }) app.get('/admin', mw1, mw2, (req, res) => { res.send('Admin page.') }) app.get('/user', (req, res) => { res.send('User page.') })
了解中间件的5个注意事项注意:在第二步创建路由时,两种方式是
完全等价的,按照自己的喜好进行设置。- 一定要在
路由之前注册中间件 - 客户端发送过来的请求,
可以连续调用多个中间件进行处理 - 执行完中间件的业务代码之后,
不要忘记调用 next() 函数 - 为了
防止代码逻辑混乱,调用 next() 函数后不要再写额外的代码 - 连续调用多个中间件时,多个中间件之间,
共享req 和 res 对象
Express 官方把
graph TD A((中间件)) --> B((应用级别的中间件)) A --> C((路由级别的中间件)) A --> D((错误级别的中间件)) A --> F((Express内置的中间件)) A --> G((第三方的中间件))常见的中间件用法,分成了5 大类,分别是:应用级别的中间件通过 app.use() 或 app.get() 或 app.post() ,绑定到 app 实例上的中间件,叫做应用级别的中间件,代码示例如下:
路由级别的中间件绑定到
express.Router()实例上的中间件,叫做路由级别的中间件。它的用法和应用级别中间件没有任何区别。只不过,应用级别中间件是绑定到 app 实例上,路由级别中间件绑定到 router 实例上,代码示例如下:错误级别的中间件错误级别中间件的
作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。格式: 错误级别中间件的 function 处理函数中,
必须有 4 个形参,形参顺序从前到后,分别是 (err, req, res, next)。
Express内置的中间件注意:
错误级别的中间件,必须注册在所有路由之后!自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验:
express.static快速托管静态资源的内置中间件,例如: HTML 文件、图片、CSS 样式等(无兼容性)express.json解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)express.urlencoded解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
// 导入 express 模块 const express = require('express') // 创建 express 的服务器实例 const app = express() // 注意:除了错误级别的中间件,其他的中间件,必须在路由之前进行配置 // 通过 express.json() 这个中间件,解析表单中的 JSON 格式的数据 app.use(express.json()) // 通过 express.urlencoded() 这个中间件,来解析 表单中的 url-encoded 格式的数据 app.use(express.urlencoded({ extended: false })) app.post('/user', (req, res) => { // 在服务器,可以使用 req.body 这个属性,来接收客户端发送过来的请求体数据 // 默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 undefined console.log(req.body) res.send('ok') }) app.post('/book', (req, res) => { // 在服务器端,可以通过 req,body 来获取 JSON 格式的表单数据和 url-encoded 格式的数据 console.log(req.body) res.send('ok') }) // 调用 app.listen 方法,指定端口号并启动web服务器 app.listen(80, function () { console.log('Express server running at 127.0.0.1') })第三方的中间件非 Express 官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。在项目中,大家可以
按需下载并配置第三方中间件,从而提高项目的开发效率。例如:在 express@4.16.0 之前的版本中,经常使用 body-parser 这个第三方中间件,来解析请求体数据。使用步骤如下:
- 运行 npm install body-parser 安装中间件
- 使用 require 导入中间件
- 调用 app.use() 注册并使用中间件
使用Express写接口注意:Express 内置的 express.urlencoded 中间件,就是基于 body-parser 这个第三方中间件进一步封装出来的
- 创建基本的服务器
- 创建API路由模块
- 编写GET接口
- 编写POST接口
- 服务器代码
// 导入 express const express = require('express') // 创建服务器实例 const app = express() // 配置解析表单数据的中间件 app.use(express.urlencoded({ extended: false })) // 必须在配置 cors 中间件之前,配置 JSONP 的接口 app.get('/api/jsonp', (req, res) => { // TODO: 定义 JSONP 接口具体的实现过程 // 1. 得到函数的名称 const funcName = req.query.callback // 2. 定义要发送到客户端的数据对象 const data = { name: 'zs', age: 22 } // 3. 拼接出一个函数的调用 const scriptStr = `${funcName}(${JSON.stringify(data)})` // 4. 把拼接的字符串,响应给客户端 res.send(scriptStr) }) // 一定要在路由之前,配置 cors 这个中间件,从而解决接口跨域的问题 const cors = require('cors') app.use(cors()) // 导入路由模块 const router = require('./16.apiRouter') // 把路由模块,注册到 app 上 app.use('/api', router) // 启动服务器 app.listen(80, () => { console.log('express server running at 127.0.0.1') })- API 路由模块
const express = require('express') const router = express.Router() // 在这里挂载对应的路由 router.get('/get', (req, res) => { // 通过 req.query 获取客户端通过查询字符串,发送到服务器的数据 const query = req.query // 调用 res.send() 方法,向客户端响应处理的结果 res.send({ status: 0, // 0 表示处理成功,1 表示处理失败 msg: 'GET 请求成功!', // 状态的描述 data: query, // 需要响应给客户端的数据 }) }) // 定义 POST 接口 router.post('/post', (req, res) => { // 通过 req.body 获取请求体中包含的 url-encoded 格式的数据 const body = req.body // 调用 res.send() 方法,向客户端响应结果 res.send({ status: 0, msg: 'POST 请求成功!', data: body, }) }) // 定义 DELETE 接口 router.delete('/delete', (req, res) => { res.send({ status: 0, msg: 'DELETE请求成功', }) }) module.exports = routerCORS跨域资源共享 接口的跨域问题刚才编写的 GET 和 POST接口,存在一个很严重的问题:不支持跨域请求。
解决接口跨域问题的方案主要有两种:- CORS(主流的解决方案,推荐使用)
- JSONP(有缺陷的解决方案:只支持 GET 请求)
cors 是 Express 的一个第三方中间件。通过安装和配置 cors 中间件,可以很方便地解决跨域问题。使用步骤分为如下 3 步:
- 运行 npm install cors 安装中间件
- 使用 const cors = require('cors') 导入中间件
- 在路由之前调用 app.use(cors()) 配置中间件
CORS (Cross-Origin Resource Sharing,跨域资源共享)由一系列
HTTP 响应头组成,这些 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源。浏览器的
同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了 CORS 相关的 HTTP 响应头,就可以解除浏览器端的跨域访问限制。
CORS 响应头部 - Access-Control-Allow-Origin注意:
- CORS 主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口。
- CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启了 CORS 的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。
响应头部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:
Access-Controlj-Allow-Origin:< origin > | *其中,origin 参数的值指定了
允许访问该资源的外域 URL。
例如,下面的字段值将只允许来自 itcast.cn 的请求:res.setHeader('Access-Control-Allow-Origin','itcast.cn')如果指定了 Access-Control-Allow-Origin 字段的值为通配符 * ,表示允许来自任何域的请求,示例代码如下:
res.setHeader('Access-Control-Allow-Origin','*')CORS 响应头部 - Access-Control-Allow-Headers默认情况下,CORS
仅支持客户端向服务器发送如下的 9 个请求头:
Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type (值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)如果客户端向服务器
发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers对额外的请求头进行声明,否则这次请求会失败!//允许客户端额外向服务器发送Content-Type 请求头和X-Custom-Header请求头 //注意:多个请求头之间使用英文的逗号进行分隔 res.setHeader('Access-Control-Allow-Headers','Content-Type,X-Custom-Header')
CORS 响应头部 - Access-Control-Allow-Methods注意:多个请求头之间使用英文的逗号进行分隔
默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。
如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods
来指明实际请求所允许使用的 HTTP 方法。示例代码如下:
//只允许 POST,GET,DELETE,HEAD请求方法 res.setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD') //允许所有的HTTP请求方法 res.setHeader('Access-Control-Allow-Methods','*')CORS请求的分类客户端在请求CORS接口时,根据
请求方式和请求头的不同,可以将 CORS 的请求分为两大类,分别是:- 简单请求
- 预检请求
同时满足以下两大条件的请求,就属于简单请求:
请求方式:GET、POST、HEAD 三者之一HTTP 头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type(只有三个值application/x-www-form- urlencoded、multipart/form-data、text/plain)
只要符合以下任何一个条件的请求,都需要进行预检请求:
- 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
- 请求头中包含自定义头部字段
- 向服务器发送了 application/json 格式的数据
简单请求和预检请求的区别在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
简单请求的特点:客户端与服务器之间只会发生一次请求。
跨域写JSONP接口 回顾jsonp的概念和特点
预检请求的特点:客户端与服务器之间会发生两次请求,OPTION 预检请求成功之后,才会发起真正的请求。
创建JSONP接口的注意事项概念:浏览器端通过< script >标签的src属性,请求服务器上的数据,同时服务器返回一个函数调用。这种请求方式叫JSONP。
特点:JSONP不属于真正Ajax请求,因为它没有使用XMLttpReuest这个对象。JSONP仅支持GET请求,不支持POST,PUT,DELETE等请求。如果项目中
已经配置了 CORS跨域资源共享,为了防止冲突,必须在配置 CORS 中间件之前声明 JSONP 的接口。否则JSONP 接口会被处理成开启了 CORS 的接口。示例代码如下:const express = require('express') // 创建服务器实例 const app = express() // 配置解析表单数据的中间件 app.use(express.urlencoded({ extended: false })) // 必须在配置 cors 中间件之前,配置 JSONP 的接口 app.get('/api/jsonp', (req, res) => { // TODO: 定义 JSONP 接口具体的实现过程 // 1. 得到函数的名称 const funcName = req.query.callback // 2. 定义要发送到客户端的数据对象 const data = { name: 'zs', age: 22 } // 3. 拼接出一个函数的调用 const scriptStr = `${funcName}(${JSON.stringify(data)})` // 4. 把拼接的字符串,响应给客户端 res.send(scriptStr) })在网页中使用jQuery发起JSONP请求调用 $.ajax() 函数,提供 JSONP 的配置选项,从而发起 JSONP 请求,示例代码如下:
<body> <button id="btnGET">GET</button> <button id="btnPOST">POST</button> <button id="btnDelete">DELETE</button> <button id="btnJSONP">JSONP</button> <script> $(function () { // 1. 测试GET接口 $('#btnGET').on('click', function () { $.ajax({ type: 'GET', url: '127.0.0.1/api/get', data: { name: 'zs', age: 20 }, success: function (res) { console.log(res) }, }) }) // 2. 测试POST接口 $('#btnPOST').on('click', function () { $.ajax({ type: 'POST', url: '127.0.0.1/api/post', data: { bookname: '水浒传', author: '施耐庵' }, success: function (res) { console.log(res) }, }) }) // 3. 为删除按钮绑定点击事件处理函数 $('#btnDelete').on('click', function () { $.ajax({ type: 'DELETE', url: '127.0.0.1/api/delete', success: function (res) { console.log(res) }, }) }) // 4. 为 JSONP 按钮绑定点击事件处理函数 $('#btnJSONP').on('click', function () { $.ajax({ type: 'GET', url: '127.0.0.1/api/jsonp', dataType: 'jsonp', success: function (res) { console.log(res) }, }) }) }) </script> </body>数据库与身份认证- 常用的数据库代码
-- 通过 * 把 users 表中所有的数据查询出来 -- select * from users -- 从 users 表中把 username 和 password 对应的数据查询出来 -- select username, password from users -- 向 users 表中,插入新数据,username 的值为 tony stark password 的值为 098123 -- insert into users (username, password) values ('tony stark', '098123') -- select * from users -- 将 id 为 4 的用户密码,更新成 888888 -- update users set password='888888' where id=4 -- select * from users -- 更新 id 为 2 的用户,把用户密码更新为 admin123 同时,把用户的状态更新为 1 -- update users set password='admin123', 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<>'ls' -- select * from users where username!='ls' -- 使用 AND 来显示所有状态为0且id小于3的用户 -- select * from users where status=0 and id<3 -- 使用 or 来显示所有状态为1 或 username 为 zs 的用户 -- select * from users where status=1 or username='zs' -- 对users表中的数据,按照 status 字段进行升序排序 -- select * from users order by status -- 按照 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实际开发中库、表、行、字段的关系- 在实际项目开发中,一般情况下,每个项目都对应独立的数据库。
- 不同的数据,要存储到数据库的不同表中,例如:用户数据存储到 users 表中,图书数据存储到 books 表中。
- 每个表中具体存储哪些信息,由字段来决定,例如:我们可以为 users 表设计 id、username、password 这 3 个
字段。 - 表中的行,代表每一条具体的数据。
- PK(Primary Key)主键、唯一标识
- NN(Not Null)值不允许为空
- UQ(Unique)值唯一
- AI(Auto Increment)值自动增长
- 安装操作 MySQL 数据库的第三方模块(mysql)
- 通过 mysql 模块连接到 MySQL 数据库
- 通过 mysql 模块执行 SQL 语句
mysql 模块是托管于 npm 上的第三方模块。它提供了在 Node.js 项目中连接和操作 MySQL 数据库的能力。想要在项目中使用它,需要先运行如下命令,将 mysql 安装为项目的依赖包:
npm install mysql配置mysql模块在使用 mysql 模块操作 MySQL 数据库之前,必须先对 mysql 模块进行必要的配置,主要的配置步骤如下:
测试mysql模块是否可以正常工作调用 db.query() 函数,指定要执行的 SQL 语句,通过回调函数拿到执行的结果:
const mysql = require('mysql') const db = mysql.createPool({ host: '127.0.0.1', user: 'root', password: 'root', database: 'bookman', }) //检查mysql模块是否正常使用 db.query('SELECT 1', (err, results) => { if (err) return console.log(err.message); //只要可以输出[ RowDataPacket { '1': 1 } ]的结果,就证明数据库链接没问题。 console.log(results); })查询语句查询表中数据示例:
const mysql = require('mysql') const db = mysql.createPool({ host: '127.0.0.1', user: 'root', password: 'root', database: 'bookman', }) db.query('SELECT * FROM tb_book', (err, results) => { //查询失败 if (err) return console.log(err.message); //查询成功 //如果执行的是select查询语句,则返回的是数组 console.log(results); })插入数据向表中插入数据,示例代码如下。
- 便捷方式
//插入数据 简化形式 const user = { username: 'liyu2', password: '49023dfs!2' } //待执行的sql语句 const sqlStr = 'INSERT INTO tb_user SET ?' //使用数组的形式,依次为?占位符指定具体的值。 db.query(sqlStr, user, (err, results) => { //失败了 if (err) return console.log(err.message); //成功了 if (results.affectedRows) { console.log('插入数据成功'); } })注意:
这种方法适合插入多项属性时使用
db.query里的第二个值一定是对象,如果是多个值,就用中括号包含
如果执行的是insert into语句,则result是一个对象
可以通过affectedRows属性,来判断是否插入成功- 初始形式
//插入数据 const list = { username: 'liyu3', password: '23123' } //待执行的sql语句 const sqlstr = 'INSERT INTO tb_user SET ?' //使用数组的形式,依次为?占位符指定具体的值。 db.query(sqlstr, [list.username, list.password], (err, results) => { //失败了 if (err) return console.log(err.message); //成功了 //注意:如果执行的是insert into语句,则result是一个对象 //可以通过affectedRows属性,来判断是否插入成功 if (results.affectedRows) { console.log('插入数据成功'); } })更新数据可以通过如下方式,更新表中的数据:
//需要更新的数据 const update = { username: 'liyu', password: '3343' } //要执行的SQL语句 const updateSql = 'UPDATE tb_user SET password=? WHERE username = ?' //3.调用db.query()执行 SQL 语句的同时,依次为占位符指定具体的值 db.query(updateSql, [update.password, update.username], (err, results) => { if (err) return console.log(err.message); if (results.affectedRows) { console.log('更新状态成功'); } })注意:
执行了update后,返回的也是一个对象,可以通过.affectedRows来判断是否成功- 便捷方式
const update2 = { username: 'liyu2', password: '111' } const updateSql1 = 'UPDATE tb_user SET ? WHERE username=?' db.query(updateSql1, [update2, update2.username], (err, results) => { if (err) return console.log(err.message); if (results.affectedRows) { console.log('更新成功'); } })
删除数据注意:
在是使用便捷方式开发代码时需要注意,db.query里的第二参数里,第一个值一定是对象。在删除数据时,推荐根据 id 这样的唯一标识,来删除对应的数据。示例如下:
const sqlStr = 'delete from tb_user where username = ?' db.query(sqlStr, 'liyu2', (err, results) => { if (err) return console.log(err.message); if (results.affectedRows) { console.log('删除成功'); } })注意:
调用db.query()执行SQL语句时,为占位符指定具体的值
如果SQL里有多个占位符,则必须使用数组为每个占位符指定具体的值
如果SQL里只有一个占位符,则可以省略数组标记删除使用 DELETE 语句,会把真正的把数据从表中删除掉。为了保险起见,
前后端的身份认证 Web开发模式推荐使用标记删除的形式,来模拟删除的动作。
所谓的标记删除,就是在表中设置类似于status这样的状态字段,来标记当前这条数据是否被删除。
当用户执行了删除的动作时,我们并没有执行 DELETE 语句把数据删除掉,而是执行了 UPDATE 语句,将这条数据对应
的 status 字段标记为删除即可。目前主流的 Web 开发模式有两种,分别是:
- 基于服务端渲染的传统 Web 开发模式
- 基于前后端分离的新型 Web 开发模式
服务端渲染的概念:服务器
发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用 Ajax 这样的技术额外请求页面的数据。代码示例如下:- 服务端渲染的优缺点
优点:
- 前端耗时少。因为服务器端负责动态生成 HTML 内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
- 有利于SEO。因为服务器端响应的是完整的 HTML 页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO。
前后端分离的 Web 开发模式缺点:
- 占用服务器端资源。即服务器端完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力。
- 不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于项目高效开发。
前后端分离的概念:前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。简而言之,前后端分离的 Web 开发模式,就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式。
- 前后端分离的优缺点
优点:
- 开发体验好。前端专注于 UI 页面的开发,后端专注于api 的开发,且前端有更多的选择性。
- 用户体验好。Ajax 技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。
- 减轻了服务器端的渲染压力。因为页面最终是在每个用户的浏览器中生成的。
如何选择前后端的身份认证缺点:
- 不利于 SEO。因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方案:利用 Vue、React 等前端框架的 SSR (server side render)技术能够很好的解决 SEO 问题!)
不谈业务场景而盲目选择使用何种开发模式都是耍流氓。
- 比如企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的 SEO,则这时我们就需要使用服务器端渲染;
- 而类似后台管理项目,交互性比较强,不需要考虑 SEO,那么就可以使用前后端分离的开发模式。
另外,具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器端渲染 + 其他页面前后端分离的开发模式。
身份认证身份认证(Authentication)又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。
- 日常生活中的身份认证随处可见,例如:高铁的验票乘车,手机的密码或指纹解锁,支付宝或微信的支付密码等。
- 在 Web 开发中,也涉及到用户身份的认证,例如:各大网站的手机验证码登录、邮箱密码登录、二维码登录等。
对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
- 服务端渲染推荐使用 Session 认证机制
- 前后端分离推荐使用 JWT 认证机制
HTTP 协议的无状态性,指的是客户端
如何突破 HTTP 无状态的限制的每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态。对于超市来说,为了方便收银员在进行结算时给 VIP 用户打折,超市可以为每个 VIP 用户发放会员卡。
什么是cookie注意:
现实生活中的会员卡身份认证方式,在 Web 开发中的专业术语叫做 Cookie。Cookie 是
存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie有效期、安全性、使用范围的可选属性组成。不同域名下的 Cookie 各自独立,每当客户端发起请求时,会
自动把当前域名下所有未过期的 Cookie一同发送到服务器。cookie的几大特性:
- 自动发送
- 域名独立
- 过期时限
4kb限制
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
Cookie 不具有安全性
由于
Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此Cookie 很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器。
提高身份认证的安全性注意:
千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等。为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡之后,可以在收银机上进行刷卡认证。只有收银机确认存在的会员卡,才能被正常使用。
这种“
Session的工作原理 在Express中使用Session认证会员卡+刷卡认证”的设计理念,就是 Session 认证机制的精髓。在 Express 项目中,只需要安装 express-session 中间件,即可在项目中使用 Session 认证:
npm install express-session配置express-session中间件express-session 中间件安装成功后,需要通过 app.use() 来注册 session 中间件,示例代码如下:
// TODO_01:请配置 Session 中间件 const session = require('express-session') app.use(session({ secret: 'itmeima', resave: false, saveUninitialized: true, }))
向 session 中存数据当 express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息:
// 登录的 API 接口 app.post('/api/login', (req, res) => { // 判断用户提交的登录信息是否正确 if (req.body.username !== 'admin' || req.body.password !== '000000') { return res.send({ status: 1, msg: '登录失败' }) } // TODO_02:请将登录成功后的用户信息,保存到 Session 中 //注意:只有成功配置了express-session中间件后,才可以使用req.session这个中间件 req.session.user = req.body //将用户信息保存到session中 req.session.islogin = true //用户的登陆状态 res.send({ status: 0, msg: '登录成功' }) })
从 session 中取数据可以直接从req.session对象上获取之间存储的数据。
// 获取用户姓名的接口 app.get('/api/username', (req, res) => { // TODO_03:请从 Session 中获取用户的名称,响应给客户端 //判断用户登陆 if (!req.session.islogin) { return res.send({ status: 1, msg: 'fail' }) } res.send({ status: 0, msg: 'success', username: req.session.user.username }) })
清空session调用 req.session.destroy() 函数,即可清空服务器保存的 session 信息。
// 退出登录的接口 app.post('/api/logout', (req, res) => { // TODO_04:清空 Session 信息 req.session.destroy() req.send({ status: '0', mag: '退出登陆成功' }) })JWT 认证机制 了解 Session 认证的局限性Session 认证机制
需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
JWT注意:
- 当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
- 当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制。
JWT(英文全称:JSON Web Token)是目前最流行的跨域认证解决方案。
JWT的工作原理
JWT的组成总结:用户的信息通过Tocken字符串的形式,保存在客户端浏览器中,服务器通过还原Tocken字符串的形式来验证用户的身份。
session认证和JWT认证的区别:
session认证存放在服务器中,JWT认证存放在浏览器中Header(头部),Payload(有效荷载),Signature(签名)
三者之间用英文的“.”分隔,格式如下:Header.Payload,SignatureJWT 的三个部分各自代表的含义JWT 的三个组成部分,从前到后分别是 Header、Payload、Signature。
其中:- Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
- Header 和 Signature 是安全性相关的部分,只是为了保证 Token 的安全性。
客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage 或 sessionStorage 中。
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是把 JWT 放在 HTTP 请求头的
Authorization 字段中,格式如下:Authorization:Bearer < token >在 Express 中使用 JWT 安装 JWT 相关的包运行如下命令,安装两个JWT相关的包:
npm install jsonwebtoken express-jwt
导入JWT相关的包其中:
jsonwebtoken 用于生成 JWT 字符串
express-jwt 用于将 JWT 字符串解析还原成 JSON 对象使用require() 函数,分别导入JWT相关的两个包:
// TODO_01:安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt const jwt = require('jsonwebtoken') const expressJwt = require('express-jwt')定义 secret 密钥为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的 secret 密钥:
- 当生成 JWT 字符串的时候,需要使用 secret 密钥对用户的信息进行加密,最终得到加密好的 JWT 字符串
- 当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 密钥进行解密
// TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey const secretKey = 'liyu No1'在登录成功后生成 JWT 字符串调用 jsonwebtoken 包提供的 sign() 方法,将用户的信息加密成 JWT 字符串,响应给客户端:
// 登录成功 // TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端 const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' }) res.send({ status: 200, message: '登录成功!', token: 'tokenStr' // 要发送给客户端的 token 字符串 })将JWT字符串还原为JSON对象客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证。
此时,服务器可以通过 express-jwt 这个中间件,自动将客户端发送过来的 Token 解析还原成 JSON 对象:
app.use(expressJwt({ secret: 'secretKey' }.unless({ path: [/^\/api\//] })))使用 req.user 获取用户信息当 express-jwt 这个中间件配置成功之后,即可在那些有权限的接口中,使用 req.user 对象,来访问从 JWT 字符串中解析出来的用户信息了,示例代码如下:
// 这是一个有权限的 API 接口 app.get('/admin/getinfo', function (req, res) { // TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端 console.log(req.user); res.send({ status: 200, message: '获取用户信息成功!', data: req.user, // 要发送给客户端的用户信息 }) })捕获解析JWT失败后产生的错误当使用express-jwt解析Token字符串时,如果客户端发送过来的Tocken字符串过期,或者不合法,就会产生一个解析失败的错误,我们可以使用Express的错误中间件,捕获这个错误并进行相关的处理,示例代码如下:
大事件项目 Headline
1. 初始化 1.1 创建项目大事件后台 API 项目,API 接口文档请参考 www.showdoc.cc/escook?page_id=3707158761215217
- 新建
api_server文件夹作为项目根目录,并在项目根目录中运行如下的命令,初始化包管理配置文件:
npm init -y- 运行如下的命令,安装特定版本的
express:
npm i express@4.17.1- 在项目根目录中新建
app.js作为整个项目的入口文件,并初始化如下的代码:
// 导入 express 模块 const express = require('express') // 创建 express 的服务器实例 const app = express() // write your code here... // 调用 app.listen 方法,指定端口号并启动web服务器 app.listen(3007, function () { console.log('api server running at 127.0.0.1:3007') })1.2 配置 cors 跨域- 运行如下的命令,安装
cors中间件:
npm i cors@2.8.5- 在
app.js中导入并配置cors中间件:
// 导入 cors 中间件 const cors = require('cors') // 将 cors 注册为全局中间件 app.use(cors())1.3 配置解析表单数据的中间件- 通过如下的代码,配置解析
application/x-www-form-urlencoded格式的表单数据的中间件:
app.use(express.urlencoded({ extended: false }))1.4 初始化路由相关的文件夹-
在项目根目录中,新建
router文件夹,用来存放所有的路由模块路由模块中,只存放客户端的请求与处理函数之间的映射关系
-
在项目根目录中,新建
router_handler文件夹,用来存放所有的路由处理函数模块路由处理函数模块中,专门负责存放每个路由对应的处理函数
- 在
router文件夹中,新建user.js文件,作为用户的路由模块,并初始化代码如下:
const express = require('express') // 创建路由对象 const router = express.Router() // 注册新用户 router.post('/reguser', (req, res) => { res.send('reguser OK') }) // 登录 router.post('/login', (req, res) => { res.send('login OK') }) // 将路由对象共享出去 module.exports = router- 在
app.js中,导入并使用用户路由模块:
// 导入并注册用户路由模块 const userRouter = require('./router/user') app.use('/api', userRouter)1.6 抽离用户路由模块中的处理函数目的:为了保证
路由模块的纯粹性,所有的路由处理函数,必须抽离到对应的路由处理函数模块中- 在
/router_handler/user.js中,使用exports对象,分别向外共享如下两个路由处理函数:
/** * 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用 */ // 注册用户的处理函数 exports.regUser = (req, res) => { res.send('reguser OK') } // 登录的处理函数 exports.login = (req, res) => { res.send('login OK') }- 将
/router/user.js中的代码修改为如下结构:
const express = require('express') const router = express.Router() // 导入用户路由处理函数模块 const userHandler = require('../router_handler/user') // 注册新用户 router.post('/reguser', userHandler.regUser) // 登录 router.post('/login', userHandler.login) module.exports = router2. 登录注册 2.1 新建 ev_users 表- 在
my_db_01数据库中,新建ev_users表如下:
在 API 接口项目中,需要安装并配置
mysql这个第三方模块,来连接和操作 MySQL 数据库- 运行如下命令,安装
mysql模块:
npm i mysql@2.18.1- 在项目根目录中新建
/db/index.js文件,在此自定义模块中创建数据库的连接对象:
// 导入 mysql 模块 const mysql = require('mysql') // 创建数据库连接对象 const db = mysql.createPool({ host: '127.0.0.1', user: 'root', password: 'admin123', database: 'my_db_01', }) // 向外共享 db 数据库连接对象 module.exports = db2.3 注册 2.3.0 实现步骤- 检测表单数据是否合法
- 检测用户名是否被占用
- 对密码进行加密处理
- 插入新用户
- 判断用户名和密码是否为空
// 接收表单数据 const userinfo = req.body // 判断数据是否合法 if (!userinfo.username || !userinfo.password) { return res.send({ status: 1, message: '用户名或密码不能为空!' }) }2.3.2 检测用户名是否被占用- 导入数据库操作模块:
const db = require('../db/index')- 定义 SQL 语句:
const sql = `select * from ev_users where username=?`- 执行 SQL 语句并根据结果判断用户名是否被占用:
db.query(sql, [userinfo.username], function (err, results) { // 执行 SQL 语句失败 if (err) { return res.send({ status: 1, message: err.message }) } // 用户名被占用 if (results.length > 0) { return res.send({ status: 1, message: '用户名被占用,请更换其他用户名!' }) } // TODO: 用户名可用,继续后续流程... })2.3.3 对密码进行加密处理为了保证密码的安全性,不建议在数据库以
明文的形式保存用户密码,推荐对密码进行加密存储
在当前项目中,使用
bcryptjs对用户密码进行加密,优点:- 加密之后的密码,无法被逆向破解
- 同一明文密码多次加密,得到的加密结果各不相同,保证了安全性
- 运行如下命令,安装指定版本的
bcryptjs:
npm i bcryptjs@2.4.3- 在
/router_handler/user.js中,导入bcryptjs:
const bcrypt = require('bcryptjs')- 在注册用户的处理函数中,确认用户名可用之后,调用
bcrypt.hashSync(明文密码, 随机盐的长度)方法,对用户的密码进行加密处理:
// 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串 userinfo.password = bcrypt.hashSync(userinfo.password, 10)2.3.4 插入新用户- 定义插入用户的 SQL 语句:
const sql = 'insert into ev_users set ?'- 调用
db.query()执行 SQL 语句,插入新用户:
db.query(sql, { username: userinfo.username, password: userinfo.password }, function (err, results) { // 执行 SQL 语句失败 if (err) return res.send({ status: 1, message: err.message }) // SQL 语句执行成功,但影响行数不为 1 if (results.affectedRows !== 1) { return res.send({ status: 1, message: '注册用户失败,请稍后再试!' }) } // 注册成功 res.send({ status: 0, message: '注册成功!' }) })2.4 优化 res.send() 代码在处理函数中,需要多次调用
res.send()向客户端响应处理失败的结果,为了简化代码,可以手动封装一个 res.cc() 函数- 在
app.js中,所有路由之前,声明一个全局中间件,为 res 对象挂载一个res.cc()函数 :
// 响应数据的中间件 app.use(function (req, res, next) { // status = 0 为成功; status = 1 为失败; 默认将 status 的值设置为 1,方便处理失败的情况 res.cc = function (err, status = 1) { res.send({ // 状态 status, // 状态描述,判断 err 是 错误对象 还是 字符串 message: err instanceof Error ? err.message : err, }) } next() })2.5 优化表单数据验证表单验证的原则:前端验证为辅,后端验证为主,后端永远不要相信前端提交过来的任何内容
在实际开发中,前后端都需要对表单的数据进行合法性的验证,而且,后端做为数据合法性验证的最后一个关口,在拦截非法数据方面,起到了至关重要的作用。
单纯的使用
if...else...的形式对数据合法性进行验证,效率低下、出错率高、维护性差。因此,推荐使用第三方数据验证模块,来降低出错率、提高验证的效率与可维护性,让后端程序员把更多的精力放在核心业务逻辑的处理上。- 安装
@hapi/joi包,为表单中携带的每个数据项,定义验证规则:
npm install @hapi/joi@17.1.0- 安装
@escook/express-joi中间件,来实现自动对表单数据进行验证的功能:
npm i @escook/express-joi- 新建
/schema/user.js用户信息验证规则模块,并初始化代码如下:
const joi = require('@hapi/joi') /** * string() 值必须是字符串 * alphanum() 值只能是包含 a-zA-Z0-9 的字符串 * min(length) 最小长度 * max(length) 最大长度 * required() 值是必填项,不能为 undefined * pattern(正则表达式) 值必须符合正则表达式的规则 */ // 用户名的验证规则 const username = joi.string().alphanum().min(1).max(10).required() // 密码的验证规则 const password = joi .string() .pattern(/^[\S]{6,12}$/) .required() // 注册和登录表单的验证规则对象 exports.reg_login_schema = { // 表示需要对 req.body 中的数据进行验证 body: { username, password, }, }- 修改
/router/user.js中的代码如下:
const express = require('express') const router = express.Router() // 导入用户路由处理函数模块 const userHandler = require('../router_handler/user') // 1. 导入验证表单数据的中间件 const expressJoi = require('@escook/express-joi') // 2. 导入需要的验证规则对象 const { reg_login_schema } = require('../schema/user') // 注册新用户 // 3. 在注册新用户的路由中,声明局部中间件,对当前请求中携带的数据进行验证 // 3.1 数据验证通过后,会把这次请求流转给后面的路由处理函数 // 3.2 数据验证失败后,终止后续代码的执行,并抛出一个全局的 Error 错误,进入全局错误级别中间件中进行处理 router.post('/reguser', expressJoi(reg_login_schema), userHandler.regUser) // 登录 router.post('/login', userHandler.login) module.exports = router- 在
app.js的全局错误级别中间件中,捕获验证失败的错误,并把验证失败的结果响应给客户端:
const joi = require('@hapi/joi') // 错误中间件 app.use(function (err, req, res, next) { // 数据验证失败 if (err instanceof joi.ValidationError) return res.cc(err) // 未知错误 return res.cc(err) })2.6 登录 2.6.0 实现步骤- 检测表单数据是否合法
- 根据用户名查询用户的数据
- 判断用户输入的密码是否正确
- 生成 JWT 的 Token 字符串
- 将
/router/user.js中登录的路由代码修改如下:
// 登录的路由 router.post('/login', expressJoi(reg_login_schema), userHandler.login)2.6.2 根据用户名查询用户的数据- 接收表单数据:
const userinfo = req.body- 定义 SQL 语句:
const sql = `select * from ev_users where username=?`- 执行 SQL 语句,查询用户的数据:
db.query(sql, userinfo.username, function (err, results) { // 执行 SQL 语句失败 if (err) return res.cc(err) // 执行 SQL 语句成功,但是查询到数据条数不等于 1 if (results.length !== 1) return res.cc('登录失败!') // TODO:判断用户输入的登录密码是否和数据库中的密码一致 })2.6.3 判断用户输入的密码是否正确核心实现思路:调用
bcrypt.compareSync(用户提交的密码, 数据库中的密码)方法比较密码是否一致返回值是布尔值(true 一致、false 不一致)
具体的实现代码如下:
// 拿着用户输入的密码,和数据库中存储的密码进行对比 const compareResult = bcrypt.compareSync(userinfo.password, results[0].password) // 如果对比的结果等于 false, 则证明用户输入的密码错误 if (!compareResult) { return res.cc('登录失败!') } // TODO:登录成功,生成 Token 字符串2.6.4 生成 JWT 的 Token 字符串核心注意点:在生成 Token 字符串的时候,一定要剔除 密码 和 头像 的值
- 通过 ES6 的高级语法,快速剔除
密码和头像的值:
// 剔除完毕之后,user 中只保留了用户的 id, username, nickname, email 这四个属性的值 const user = { ...results[0], password: '', user_pic: '' }- 运行如下的命令,安装生成 Token 字符串的包:
npm i jsonwebtoken@8.5.1- 在
/router_handler/user.js模块的头部区域,导入jsonwebtoken包:
// 用这个包来生成 Token 字符串 const jwt = require('jsonwebtoken')- 创建
config.js文件,并向外共享 加密 和 还原 Token 的jwtSecretKey字符串:
module.exports = { jwtSecretKey: 'itheima No1. ^_^', }- 将用户信息对象加密成 Token 字符串:
// 导入配置文件 const config = require('../config') // 生成 Token 字符串 const tokenStr = jwt.sign(user, config.jwtSecretKey, { expiresIn: '10h', // token 有效期为 10 个小时 })- 将生成的 Token 字符串响应给客户端:
res.send({ status: 0, message: '登录成功!', // 为了方便客户端使用 Token,在服务器端直接拼接上 Bearer 的前缀 token: 'Bearer ' + tokenStr, })2.7 配置解析 Token 的中间件- 运行如下的命令,安装解析 Token 的中间件:
npm i express-jwt@5.3.3- 在
app.js中注册路由之前,配置解析 Token 的中间件:
// 导入配置文件 const config = require('./config') // 解析 token 的中间件 const expressJWT = require('express-jwt') // 使用 .unless({ path: [/^\/api\//] }) 指定哪些接口不需要进行 Token 的身份认证 app.use(expressJWT({ secret: config.jwtSecretKey }).unless({ path: [/^\/api\//] }))- 在
app.js中的错误级别中间件里面,捕获并处理 Token 认证失败后的错误:
// 错误中间件 app.use(function (err, req, res, next) { // 省略其它代码... // 捕获身份认证失败的错误 if (err.name === 'UnauthorizedError') return res.cc('身份认证失败!') // 未知错误... })3. 个人中心 3.1 获取用户的基本信息 3.1.0 实现步骤- 初始化 路由 模块
- 初始化 路由处理函数 模块
- 获取用户的基本信息
- 创建
/router/userinfo.js路由模块,并初始化如下的代码结构:
// 导入 express const express = require('express') // 创建路由对象 const router = express.Router() // 获取用户的基本信息 router.get('/userinfo', (req, res) => { res.send('ok') }) // 向外共享路由对象 module.exports = router- 在
app.js中导入并使用个人中心的路由模块:
// 导入并使用用户信息路由模块 const userinfoRouter = require('./router/userinfo') // 注意:以 /my 开头的接口,都是有权限的接口,需要进行 Token 身份认证 app.use('/my', userinfoRouter)3.1.2 初始化路由处理函数模块- 创建
/router_handler/userinfo.js路由处理函数模块,并初始化如下的代码结构:
// 获取用户基本信息的处理函数 exports.getUserInfo = (req, res) => { res.send('ok') }- 修改
/router/userinfo.js中的代码如下:
const express = require('express') const router = express.Router() // 导入用户信息的处理函数模块 const userinfo_handler = require('../router_handler/userinfo') // 获取用户的基本信息 router.get('/userinfo', userinfo_handler.getUserInfo) module.exports = router3.1.3 获取用户的基本信息- 在
/router_handler/userinfo.js头部导入数据库操作模块:
// 导入数据库操作模块 const db = require('../db/index')- 定义 SQL 语句:
// 根据用户的 id,查询用户的基本信息 // 注意:为了防止用户的密码泄露,需要排除 password 字段 const sql = `select id, username, nickname, email, user_pic from ev_users where id=?`- 调用
db.query()执行 SQL 语句:
// 注意:req 对象上的 user 属性,是 Token 解析成功,express-jwt 中间件帮我们挂载上去的 db.query(sql, req.user.id, (err, results) => { // 1. 执行 SQL 语句失败 if (err) return res.cc(err) // 2. 执行 SQL 语句成功,但是查询到的数据条数不等于 1 if (results.length !== 1) return res.cc('获取用户信息失败!') // 3. 将用户信息响应给客户端 res.send({ status: 0, message: '获取用户基本信息成功!', data: results[0], }) })3.2 更新用户的基本信息 3.2.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 实现更新用户基本信息的功能
- 在
/router/userinfo.js模块中,新增更新用户基本信息的路由:
// 更新用户的基本信息 router.post('/userinfo', userinfo_handler.updateUserInfo)- 在
/router_handler/userinfo.js模块中,定义并向外共享更新用户基本信息的路由处理函数:
// 更新用户基本信息的处理函数 exports.updateUserInfo = (req, res) => { res.send('ok') }3.2.2 验证表单数据- 在
/schema/user.js验证规则模块中,定义id,nickname,email的验证规则如下:
// 定义 id, nickname, emial 的验证规则 const id = joi.number().integer().min(1).required() const nickname = joi.string().required() const email = joi.string().email().required()- 并使用
exports向外共享如下的验证规则对象:
// 验证规则对象 - 更新用户基本信息 exports.update_userinfo_schema = { body: { id, nickname, email, }, }- 在
/router/userinfo.js模块中,导入验证数据合法性的中间件:
// 导入验证数据合法性的中间件 const expressJoi = require('@escook/express-joi')- 在
/router/userinfo.js模块中,导入需要的验证规则对象:
// 导入需要的验证规则对象 const { update_userinfo_schema } = require('../schema/user')- 在
/router/userinfo.js模块中,修改更新用户的基本信息的路由如下:
// 更新用户的基本信息 router.post('/userinfo', expressJoi(update_userinfo_schema), userinfo_handler.updateUserInfo)3.2.3 实现更新用户基本信息的功能- 定义待执行的 SQL 语句:
const sql = `update ev_users set ? where id=?`- 调用
db.query()执行 SQL 语句并传参:
db.query(sql, [req.body, req.body.id], (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 执行 SQL 语句成功,但影响行数不为 1 if (results.affectedRows !== 1) return res.cc('修改用户基本信息失败!') // 修改用户信息成功 return res.cc('修改用户基本信息成功!', 0) })3.3 重置密码 3.3.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 实现重置密码的功能
- 在
/router/userinfo.js模块中,新增重置密码的路由:
// 重置密码的路由 router.post('/updatepwd', userinfo_handler.updatePassword)- 在
/router_handler/userinfo.js模块中,定义并向外共享重置密码的路由处理函数:
// 重置密码的处理函数 exports.updatePassword = (req, res) => { res.send('ok') }3.3.2 验证表单数据核心验证思路:旧密码与新密码,必须符合密码的验证规则,并且新密码不能与旧密码一致!
- 在
/schema/user.js模块中,使用exports向外共享如下的验证规则对象:
// 验证规则对象 - 重置密码 exports.update_password_schema = { body: { // 使用 password 这个规则,验证 req.body.oldPwd 的值 oldPwd: password, // 使用 joi.not(joi.ref('oldPwd')).concat(password) 规则,验证 req.body.newPwd 的值 // 解读: // 1. joi.ref('oldPwd') 表示 newPwd 的值必须和 oldPwd 的值保持一致 // 2. joi.not(joi.ref('oldPwd')) 表示 newPwd 的值不能等于 oldPwd 的值 // 3. .concat() 用于合并 joi.not(joi.ref('oldPwd')) 和 password 这两条验证规则 newPwd: joi.not(joi.ref('oldPwd')).concat(password), }, }- 在
/router/userinfo.js模块中,导入需要的验证规则对象:
// 导入需要的验证规则对象 const { update_userinfo_schema, update_password_schema } = require('../schema/user')- 并在
重置密码的路由中,使用update_password_schema规则验证表单的数据,示例代码如下:
router.post('/updatepwd', expressJoi(update_password_schema), userinfo_handler.updatePassword)3.3.3 实现重置密码的功能- 根据
id查询用户是否存在:
// 定义根据 id 查询用户数据的 SQL 语句 const sql = `select * from ev_users where id=?` // 执行 SQL 语句查询用户是否存在 db.query(sql, req.user.id, (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 检查指定 id 的用户是否存在 if (results.length !== 1) return res.cc('用户不存在!') // TODO:判断提交的旧密码是否正确 })- 判断提交的 旧密码 是否正确:
// 在头部区域导入 bcryptjs 后, // 即可使用 bcrypt.compareSync(提交的密码,数据库中的密码) 方法验证密码是否正确 // compareSync() 函数的返回值为布尔值,true 表示密码正确,false 表示密码错误 const bcrypt = require('bcryptjs') // 判断提交的旧密码是否正确 const compareResult = bcrypt.compareSync(req.body.oldPwd, results[0].password) if (!compareResult) return res.cc('原密码错误!')- 对新密码进行
bcrypt加密之后,更新到数据库中:
// 定义更新用户密码的 SQL 语句 const sql = `update ev_users set password=? where id=?` // 对新密码进行 bcrypt 加密处理 const newPwd = bcrypt.hashSync(req.body.newPwd, 10) // 执行 SQL 语句,根据 id 更新用户的密码 db.query(sql, [newPwd, req.user.id], (err, results) => { // SQL 语句执行失败 if (err) return res.cc(err) // SQL 语句执行成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('更新密码失败!') // 更新密码成功 res.cc('更新密码成功!', 0) })3.4 更新用户头像 3.4.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 实现更新用户头像的功能
- 在
/router/userinfo.js模块中,新增更新用户头像的路由:
// 更新用户头像的路由 router.post('/update/avatar', userinfo_handler.updateAvatar)- 在
/router_handler/userinfo.js模块中,定义并向外共享更新用户头像的路由处理函数:
// 更新用户头像的处理函数 exports.updateAvatar = (req, res) => { res.send('ok') }3.4.2 验证表单数据- 在
/schema/user.js验证规则模块中,定义avatar的验证规则如下:
// dataUri() 指的是如下格式的字符串数据: // data:image/png;base64,VE9PTUFOWVNFQ1JFVFM= const avatar = joi.string().dataUri().required()- 并使用
exports向外共享如下的验证规则对象:
// 验证规则对象 - 更新头像 exports.update_avatar_schema = { body: { avatar, }, }- 在
/router/userinfo.js模块中,导入需要的验证规则对象:
const { update_avatar_schema } = require('../schema/user')- 在
/router/userinfo.js模块中,修改更新用户头像的路由如下:
router.post('/update/avatar', expressJoi(update_avatar_schema), userinfo_handler.updateAvatar)3.4.3 实现更新用户头像的功能- 定义更新用户头像的 SQL 语句:
const sql = 'update ev_users set user_pic=? where id=?'- 调用
db.query()执行 SQL 语句,更新对应用户的头像:
4.1.2 新增两条初始数据 4.2 获取文章分类列表 4.2.0 实现步骤db.query(sql, [req.body.avatar, req.user.id], (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 执行 SQL 语句成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('更新头像失败!') // 更新用户头像成功 return res.cc('更新头像成功!', 0) })4. 文章分类管理 4.1 新建 ev_article_cate 表 4.1.1 创建表结构- 初始化路由模块
- 初始化路由处理函数模块
- 获取文章分类列表数据
- 创建
/router/artcate.js路由模块,并初始化如下的代码结构:
// 导入 express const express = require('express') // 创建路由对象 const router = express.Router() // 获取文章分类的列表数据 router.get('/cates', (req, res) => { res.send('ok') }) // 向外共享路由对象 module.exports = router- 在
app.js中导入并使用文章分类的路由模块:
// 导入并使用文章分类路由模块 const artCateRouter = require('./router/artcate') // 为文章分类的路由挂载统一的访问前缀 /my/article app.use('/my/article', artCateRouter)4.2.2 初始化路由处理函数模块- 创建
/router_handler/artcate.js路由处理函数模块,并初始化如下的代码结构:
// 获取文章分类列表数据的处理函数 exports.getArticleCates = (req, res) => { res.send('ok') }- 修改
/router/artcate.js中的代码如下:
const express = require('express') const router = express.Router() // 导入文章分类的路由处理函数模块 const artcate_handler = require('../router_handler/artcate') // 获取文章分类的列表数据 router.get('/cates', artcate_handler.getArticleCates) module.exports = router4.2.3 获取文章分类列表数据- 在
/router_handler/artcate.js头部导入数据库操作模块:
// 导入数据库操作模块 const db = require('../db/index')- 定义 SQL 语句:
// 根据分类的状态,获取所有未被删除的分类列表数据 // is_delete 为 0 表示没有被 标记为删除 的数据 const sql = 'select * from ev_article_cate where is_delete=0 order by id asc'- 调用
db.query()执行 SQL 语句:
db.query(sql, (err, results) => { // 1. 执行 SQL 语句失败 if (err) return res.cc(err) // 2. 执行 SQL 语句成功 res.send({ status: 0, message: '获取文章分类列表成功!', data: results, }) })4.3 新增文章分类 4.3.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 查询
分类名称与分类别名是否被占用 - 实现新增文章分类的功能
- 在
/router/artcate.js模块中,添加新增文章分类的路由:
// 新增文章分类的路由 router.post('/addcates', artcate_handler.addArticleCates)- 在
/router_handler/artcate.js模块中,定义并向外共享新增文章分类的路由处理函数:
// 新增文章分类的处理函数 exports.addArticleCates = (req, res) => { res.send('ok') }4.3.2 验证表单数据- 创建
/schema/artcate.js文章分类数据验证模块,并定义如下的验证规则:
// 导入定义验证规则的模块 const joi = require('@hapi/joi') // 定义 分类名称 和 分类别名 的校验规则 const name = joi.string().required() const alias = joi.string().alphanum().required() // 校验规则对象 - 添加分类 exports.add_cate_schema = { body: { name, alias, }, }- 在
/router/artcate.js模块中,使用add_cate_schema对数据进行验证:
// 导入验证数据的中间件 const expressJoi = require('@escook/express-joi') // 导入文章分类的验证模块 const { add_cate_schema } = require('../schema/artcate') // 新增文章分类的路由 router.post('/addcates', expressJoi(add_cate_schema), artcate_handler.addArticleCates)4.3.3 查询分类名称与别名是否被占用- 定义查重的 SQL 语句:
// 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句 const sql = `select * from ev_article_cate where name=? or alias=?`- 调用
db.query()执行查重的操作:
// 执行查重操作 db.query(sql, [req.body.name, req.body.alias], (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 判断 分类名称 和 分类别名 是否被占用 if (results.length === 2) 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('分类别名被占用,请更换后重试!') // TODO:新增文章分类 })4.3.4 实现新增文章分类的功能- 定义新增文章分类的 SQL 语句:
const sql = `insert into ev_article_cate set ?`- 调用
db.query()执行新增文章分类的 SQL 语句:
db.query(sql, req.body, (err, results) => { // SQL 语句执行失败 if (err) return res.cc(err) // SQL 语句执行成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('新增文章分类失败!') // 新增文章分类成功 res.cc('新增文章分类成功!', 0) })4.4 根据 Id 删除文章分类 4.4.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 实现删除文章分类的功能
- 在
/router/artcate.js模块中,添加删除文章分类的路由:
// 删除文章分类的路由 router.get('/deletecate/:id', artcate_handler.deleteCateById)- 在
/router_handler/artcate.js模块中,定义并向外共享删除文章分类的路由处理函数:
// 删除文章分类的处理函数 exports.deleteCateById = (req, res) => { res.send('ok') }4.4.2 验证表单数据- 在
/schema/artcate.js验证规则模块中,定义 id 的验证规则如下:
// 定义 分类Id 的校验规则 const id = joi.number().integer().min(1).required()- 并使用
exports向外共享如下的验证规则对象:
// 校验规则对象 - 删除分类 exports.delete_cate_schema = { params: { id, }, }- 在
/router/artcate.js模块中,导入需要的验证规则对象,并在路由中使用:
// 导入删除分类的验证规则对象 const { delete_cate_schema } = require('../schema/artcate') // 删除文章分类的路由 router.get('/deletecate/:id', expressJoi(delete_cate_schema), artcate_handler.deleteCateById)4.4.3 实现删除文章分类的功能- 定义删除文章分类的 SQL 语句:
const sql = `update ev_article_cate set is_delete=1 where id=?`- 调用
db.query()执行删除文章分类的 SQL 语句:
db.query(sql, req.params.id, (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // SQL 语句执行成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('删除文章分类失败!') // 删除文章分类成功 res.cc('删除文章分类成功!', 0) })4.5 根据 Id 获取文章分类数据 4.5.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 实现获取文章分类的功能
- 在
/router/artcate.js模块中,添加根据 Id 获取文章分类的路由:
router.get('/cates/:id', artcate_handler.getArticleById)- 在
/router_handler/artcate.js模块中,定义并向外共享根据 Id 获取文章分类的路由处理函数:
// 根据 Id 获取文章分类的处理函数 exports.getArticleById = (req, res) => { res.send('ok') }4.5.2 验证表单数据- 在
/schema/artcate.js验证规则模块中,使用exports向外共享如下的验证规则对象:
// 校验规则对象 - 根据 Id 获取分类 exports.get_cate_schema = { params: { id, }, }- 在
/router/artcate.js模块中,导入需要的验证规则对象,并在路由中使用:
// 导入根据 Id 获取分类的验证规则对象 const { get_cate_schema } = require('../schema/artcate') // 根据 Id 获取文章分类的路由 router.get('/cates/:id', expressJoi(get_cate_schema), artcate_handler.getArticleById)4.5.3 实现获取文章分类的功能- 定义根据 Id 获取文章分类的 SQL 语句:
const sql = `select * from ev_article_cate where id=?`- 调用
db.query()执行 SQL 语句:
db.query(sql, req.params.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], }) })4.6 根据 Id 更新文章分类数据 4.6.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 查询
分类名称与分类别名是否被占用 - 实现更新文章分类的功能
- 在
/router/artcate.js模块中,添加更新文章分类的路由:
// 更新文章分类的路由 router.post('/updatecate', artcate_handler.updateCateById)- 在
/router_handler/artcate.js模块中,定义并向外共享更新文章分类的路由处理函数:
// 更新文章分类的处理函数 exports.updateCateById = (req, res) => { res.send('ok') }4.6.2 验证表单数据- 在
/schema/artcate.js验证规则模块中,使用exports向外共享如下的验证规则对象:
// 校验规则对象 - 更新分类 exports.update_cate_schema = { body: { Id: id, name, alias, }, }- 在
/router/artcate.js模块中,导入需要的验证规则对象,并在路由中使用:
// 导入更新文章分类的验证规则对象 const { update_cate_schema } = require('../schema/artcate') // 更新文章分类的路由 router.post('/updatecate', expressJoi(update_cate_schema), artcate_handler.updateCateById)4.5.4 查询分类名称与别名是否被占用- 定义查重的 SQL 语句:
// 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句 const sql = `select * from ev_article_cate where Id<>? and (name=? or alias=?)`- 调用
db.query()执行查重的操作:
// 执行查重操作 db.query(sql, [req.body.Id, req.body.name, req.body.alias], (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 判断 分类名称 和 分类别名 是否被占用 if (results.length === 2) 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('分类别名被占用,请更换后重试!') // TODO:更新文章分类 })4.5.5 实现更新文章分类的功能- 定义更新文章分类的 SQL 语句:
const sql = `update ev_article_cate set ? where Id=?`- 调用
db.query()执行 SQL 语句:
5.2 发布新文章 5.2.0 实现步骤db.query(sql, [req.body, req.body.Id], (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // SQL 语句执行成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('更新文章分类失败!') // 更新文章分类成功 res.cc('更新文章分类成功!', 0) })5. 文章管理 5.1 新建 ev_articles 表- 初始化路由模块
- 初始化路由处理函数模块
- 使用 multer 解析表单数据
- 验证表单数据
- 实现发布文章的功能
- 创建
/router/article.js路由模块,并初始化如下的代码结构:
// 导入 express const express = require('express') // 创建路由对象 const router = express.Router() // 发布新文章 router.post('/add', (req, res) => { res.send('ok') }) // 向外共享路由对象 module.exports = router- 在
app.js中导入并使用文章的路由模块:
// 导入并使用文章路由模块 const articleRouter = require('./router/article') // 为文章的路由挂载统一的访问前缀 /my/article app.use('/my/article', articleRouter)5.2.2 初始化路由处理函数模块- 创建
/router_handler/article.js路由处理函数模块,并初始化如下的代码结构:
// 发布新文章的处理函数 exports.addArticle = (req, res) => { res.send('ok') }- 修改
/router/article.js中的代码如下:
const express = require('express') const router = express.Router() // 导入文章的路由处理函数模块 const article_handler = require('../router_handler/article') // 发布新文章 router.post('/add', article_handler.addArticle) module.exports = router5.2.3 使用 multer 解析表单数据注意:使用
express.urlencoded()中间件无法解析multipart/form-data格式的请求体数据。当前项目,推荐使用 multer 来解析
multipart/form-data格式的表单数据。www.npmjs.com/package/multer- 运行如下的终端命令,在项目中安装
multer:
npm i multer@1.4.2- 在
/router_handler/article.js模块中导入并配置multer:
// 导入解析 formdata 格式表单数据的包 const multer = require('multer') // 导入处理路径的核心模块 const path = require('path') // 创建 multer 的实例对象,通过 dest 属性指定文件的存放路径 const upload = multer({ dest: path.join(__dirname, '../uploads') })- 修改
发布新文章的路由如下:
// 发布新文章的路由 // upload.single() 是一个局部生效的中间件,用来解析 FormData 格式的表单数据 // 将文件类型的数据,解析并挂载到 req.file 属性中 // 将文本类型的数据,解析并挂载到 req.body 属性中 router.post('/add', upload.single('cover_img'), article_handler.addArticle)- 在
/router_handler/article.js模块中的addArticle处理函数中,将multer解析出来的数据进行打印:
// 发布新文章的处理函数 exports.addArticle = (req, res) => { console.log(req.body) // 文本类型的数据 console.log('--------分割线----------') console.log(req.file) // 文件类型的数据 res.send('ok') })5.2.4 验证表单数据实现思路:通过 express-joi 自动验证 req.body 中的文本数据;通过 if 判断手动验证 req.file 中的文件数据;
- 创建
/schema/article.js验证规则模块,并初始化如下的代码结构:
// 导入定义验证规则的模块 const joi = require('@hapi/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() // 验证规则对象 - 发布文章 exports.add_article_schema = { body: { title, cate_id, content, state, }, }- 在
/router/article.js模块中,导入需要的验证规则对象,并在路由中使用:
// 导入验证数据的中间件 const expressJoi = require('@escook/express-joi') // 导入文章的验证模块 const { add_article_schema } = require('../schema/article') // 发布新文章的路由 // 注意:在当前的路由中,先后使用了两个中间件: // 先使用 multer 解析表单数据 // 再使用 expressJoi 对解析的表单数据进行验证 router.post('/add', upload.single('cover_img'), expressJoi(add_article_schema), article_handler.addArticle)- 在
/router_handler/article.js模块中的addArticle处理函数中,通过if判断客户端是否提交了封面图片:
// 发布新文章的处理函数 exports.addArticle = (req, res) => { // 手动判断是否上传了文章封面 if (!req.file || req.file.fieldname !== 'cover_img') return res.cc('文章封面是必选参数!') // TODO:表单数据合法,继续后面的处理流程... })5.2.5 实现发布文章的功能- 整理要插入数据库的文章信息对象:
// 导入处理路径的 path 核心模块 const path = require('path') const articleInfo = { // 标题、内容、状态、所属的分类Id ...req.body, // 文章封面在服务器端的存放路径 cover_img: path.join('/uploads', req.file.filename), // 文章发布时间 pub_date: new Date(), // 文章作者的Id author_id: req.user.id, }- 定义发布文章的 SQL 语句:
const sql = `insert into ev_articles set ?`- 调用
db.query()执行发布文章的 SQL 语句:
// 导入数据库操作模块 const db = require('../db/index') // 执行 SQL 语句 db.query(sql, articleInfo, (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 执行 SQL 语句成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('发布文章失败!') // 发布文章成功 res.cc('发布文章成功', 0) })- 在
app.js中,使用express.static()中间件,将uploads目录中的图片托管为静态资源:
// 托管静态资源文件 app.use('/uploads', express.static('./uploads')) - eexpress.static()
- 核心思路
本文共计24438个文字,预计阅读时间需要98分钟。
Node.js简介:已掌握相关技术,如HTML、CSS、JavaScript,了解浏览器中的JavaScript组成、运行环境和运行环境定义,即指代码正确运行所需的环境。V8引擎负责解析和执行JavaScript代码。
Node.js简介 已经掌握了那些技术- HTML
- CSS
- JavaScript
运行环境是指 代码正确运行所使用的必要环境
- V8引擎负责解析和执行JavaScript代码。
- 内置API是由
运行环境提供的特殊接口,只能在所属的运行环境中被调用。
Node.js 是一个基于 Chrome's V8 的JavaScript运行环境。
Node.js的官方运行环境
Node.js是一个单线程的事件驱动的非阻塞性I/O模型
Node.ja可以做什么注意:
- 浏览器是 JavaScript 的前端运行环境。
- Node.js 是 JavaScript 的后端运行环境。
- Node.js 中无法调用 DOM 和 BOM 等浏览器内置 API。
Node.js 作为一个 JavaScript 的运行环境,仅仅提供了基础的功能和 API。然而,基于 Node.js 提供的这些基础能,很多强大的工具和框架如雨后春笋,层出不穷,所以学会了 Node.js ,可以让前端程序员胜任更多的工作和岗位:
1.基于 Express 框架
可以快速构建 Web 应用
2.基于 Electron 框架
可以构建跨平台的桌面应用
3.基于 restify 框架
可以快速构建 API 接口项目
4 .读写和操作数据库、创建实用的命令行工具辅助前端开发、etc…
Node.js学习路径-
JavaScript学习路径
JavaScript 基础语法 + 浏览器内置 API(DOM + BOM) + 第三方库(jQuery、art-template 等) -
Node.js学习路径
JavaScript 基础语法 + Node.js 内置 API 模块(fs、path、127.0.0.1:8080') }) req请求对象只要服务器接收到了客户端的请求,就会调用通过
res响应对象server.on()为服务器绑定的request 事件处理函数。如果想在事件处理函数中,`访问与客户端相关的数据或属性,可以使用如下的方式:在服务器的requset事件处理函数中,如果想访问与服务器相关的数据或属性,可以使用如下的方式。
server.on('request', (req, res) => { // req.url 是客户端请求的 URL 地址 const url = req.url // req.method 是客户端请求的 method 类型 const method = req.method const str = `Your request url is ${url}, and request method is ${method}` console.log(str) // 调用 res.end() 方法,向客户端响应一些内容 res.end(str) })解决中文乱码通过 res.setHeader参数,设置响应头,解决中文乱码问题。
const 127.0.0.1') })
127.0.0.1'); }) 实现clock时钟的web服务器注意:
解决中文乱码问题的解决方案是固定的。
charset和utf-8中间不能有空格,否则就会出现中文乱码。- 核心思路
把文件的实际存放路径,作为每个资源的请求url地址。
- 服务器充当的角色就是一个字符串的搬运工
- 实现步骤
- 导入需要的模块
- 创建基本的web服务器
- 将资源的请求url地址映射为文件的存放路径
- 读取文件内容并响应给客户端
- 优化资源请求路径
- 导入需要模块
- 127.0.0.1');
})
监听GET请求
通过
app.get()方法,可以监听客户端的 GET 请求,具体的语法格式如下:
监听POST请求app.get('/liyu', (req, res) => { //调用express提供的res.send方法向客户端提供一个json对象 res.send({ name: 'xz', age: 36, gender: '男' }) })通过
app.post()方法,可以监听客户端的 POST 请求,具体的语法格式如下:
把内容响应给客户端app.post('/liyu', (req, res) => { //调用express提供的res.send方法向客户端提供一个文本服务器 res.send('请求成功') })通过
res.send()方法,可以把处理好的内容,发送给客户端:app.get('/liyu', (req, res) => { //调用express提供的res.send方法向客户端提供一个json对象 res.send({ name: 'xz', age: 36, gender: '男' }) })获取 URL 中携带的查询参数通过
req.query对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数:
获取 URL 中的动态参数app.get('/', (req, res) => { //通过req.query获取客户端发送过来的查询参数 console.log(req.query); })通过
req.params对象,可以访问到 URL 中,通过 : 匹配到的动态参数:app.get('/liyu/:id', (req, res) => { //req.params是动态匹配到的url参数,默认空对象 console.log(req.params); res.send(req.params) })
托管静态资源注意:这里的id是一个动态的参数
- eexpress.static()
express 提供了一个非常好用的函数,叫做 express.static(),通过它,我们可以非常方便地创建一个静态资源服务器,例如,通过如下代码就可以将 public 目录下的图片、CSS 文件、JavaScript 文件对外开放访问了:
app.use(express.static('public')) app.use(express.static('files'))现在,你就可以访问 public 目录中的所有文件了:
localhost:3000/images/bg.jpg
托管多个静态资源目录注意:
Express 在指定的静态目录中查找文件,并对外提供资源的访问路径。因此,存放静态文件的目录名不会出现在 URL 中。如果要托管多个静态资源目录,请多次调用 express.static() 函数:
app.use(express.static('public'))
挂载注意:
访问静态资源文件时,express.static() 函数会根据目录的添加顺序查找所需的文件。路径前缀如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下的方式:
app.use('/files', express.static('./files'))现在,访问files时,就必须加上/files才可以访问到.
localhost:3000/files/index.html
Express路由 路由是什么广义上来讲,路由就是映射关系。
Express 中的路由在 Express 中,路由指的是
客户端的请求与服务器处理函数之间的映射关系。
Express 中的路由分 3 部分组成,分别是请求的类型、请求的 URL 地址、处理函数,格式如下:app.METHOD(PATH,HANDLER)例子//匹配GET请求,且请求URL为/ app.get('/',funcation(req,res){ res.send('hello World!') }) // 匹配POST请求,且请求URL为/ app.post('/',funcation(req,res){ res.send('Got a POST request') })路由的匹配过程每当一个请求到达服务器之后,
需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。在匹配时,会按照路由的顺序进行匹配,如果
请求类型和请求的 URL同时匹配成功,则 Express 会将这次请求,转交给对应的 function 函数进行处理。
路由的使用 最简单的写法注意点:
- 按照定义的
先后顺序进行匹配 请求类型和请求的url同时匹配成功才会调用对应的处理函数.
在Express中使用路由最简单的方式,就是把路由挂载到 app 上,示例如下:
const express = require('express') const app = express() // 挂载路由 app.get('/', (req, res) => { res.send('hello world.') }) app.post('/', (req, res) => { res.send('Post Request.') }) app.listen(80, () => { console.log('127.0.0.1') })模块化路由为了
方便对路由进行模块化的管理,Express不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块。将路由抽离为单独模块的步骤如下:- 创建路由模块对应的 .js 文件.
- 调用 express.Router() 函数创建路由对象
- 向路由对象上挂载具体的路由
- 使用 module.exports 向外共享路由对象
- 使用 app.use() 函数注册路由模块
- 创建路由模块
// 这是路由模块 // 1. 导入 express const express = require('express') // 2. 创建路由对象 const router = express.Router() // 3. 挂载具体的路由 router.get('/user/list', (req, res) => { res.send('Get user list.') }) router.post('/user/add', (req, res) => { res.send('Add new user.') }) // 4. 向外导出路由对象 module.exports = router- 注册路由模块
为路由模块添加前缀const express = require('express') const app = express() // app.use('/files', express.static('./files')) // 1. 导入路由模块 const router = require('./03.router') // 2. 注册路由模块 app.use('/api', router) // 注意: app.use() 函数的作用,就是来注册全局中间件 app.listen(80, () => { console.log('127.0.0.1') })类似于托管静态资源时,为静态资源统一挂载访问前缀一样,路由模块添加前缀的方式也非常简单:
app.use('/api', router)Express 中间件 什么是中间件中间件(Middleware ),特指
现实生活中的例子业务流程的中间处理环节.在处理污水的时候,一般都要经过
三个处理环节,从而保证处理过后的废水,达到排放标准。处理污水的这三个中间处理环节,就可以叫做中间件。
Express 中间件的调用流程当一个请求到达 Express 的服务器之后,可以
连续调用多个中间件,从而对这次请求进行预处理。
Express 中间件的格式注意:上一个中间件的输出会作为下一个中间件的输入。
Express 的中间件,本质上就是一个 function 处理函数,Express 中间件的格式如下:
next 函数的作用注意:
中间件函数的形参列表中,必须包含 next 参数。而路由处理函数中只包含 req 和 res。
定义中间件函数next 函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由。// 定义一个最简单的中间件函数 const mw = function (req, res, next) { console.log('这是最简单的中间件函数') // 把流转关系,转交给下一个中间件或路由 next() } // 将 mw 注册为全局生效的中间件 app.use(mw)全局生效的中间件客户端发起的
任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。
通过调用app.use(中间件函数),即可定义一个全局生效的中间件,示例代码如下:// 将 mw 注册为全局生效的中间件 app.use(mw)定义全局中间件的简化形式// 这是定义全局中间件的简化形式 app.use((req, res, next) => { console.log('这是最简单的中间件函数') next() })中间件的作用多个中间件之间,共享一份req和res,基于这样的特性,我们可以在上游的中间件中,统一为req和res为对象添加自定义的属性和方法,供下游的中间件或路由进行使用。
const express = require('express') const app = express() // 这是定义全局中间件的简化形式 app.use((req, res, next) => { // 获取到请求到达服务器的时间 const time = Date.now() // 为 req 对象,挂载自定义属性,从而把时间共享给后面的所有路由 req.startTime = time next() }) app.get('/', (req, res) => { res.send('Home page.' + req.startTime) }) app.get('/user', (req, res) => { res.send('User page.' + req.startTime) }) app.listen(80, () => { console.log('127.0.0.1') })定义多个全局中间件可以使用 app.use()
连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用,示例代码如下:const express = require('express') const app = express() //定义第一个全局中间件 app.use((req, res, next) => { console.log('调用了第一个全局中间件'); next() }) app.use((req, res, next) => { console.log('调用了第二个全局中间件'); next() }) //定义一个路由 app.get('/user', (req, res) => { res.send('User page!') }) app.listen(80, () => { console.log('127.0.0.1'); })局部生效的中间件不使用app.use() 定义的中间件,叫做局部生效的中间件,示例代码如下:// 导入 express 模块 const express = require('express') // 创建 express 的服务器实例 const app = express() //定义中间件函数 const mv1 = (req, res, next) => { console.log('定义了一个局部生效的中间件'); next() } //创建路由 app.get('/', mv1, (req, res) => { res.send('Home page!') }) app.get('/user', (req, res) => { res.send('User page!') }) // 调用 app.listen 方法,指定端口号并启动web服务器 app.listen(80, function () { console.log('Express server running at 127.0.0.1') })
定义多个局部中间件注意:
中间件只会再添加的路由函数中起作用。可以在路由中,通过如下两种等价的方式,使用多个局部中间件:
const mw1 = (req, res, next) => { console.log('调用了第一个局部生效的中间件') next() } const mw2 = (req, res, next) => { console.log('调用了第二个局部生效的中间件') next() } // 2. 创建路由,两种方式是“完全等价的” app.get('/', [mw1, mw2], (req, res) => { res.send('Home page.') }) app.get('/admin', mw1, mw2, (req, res) => { res.send('Admin page.') }) app.get('/user', (req, res) => { res.send('User page.') })
了解中间件的5个注意事项注意:在第二步创建路由时,两种方式是
完全等价的,按照自己的喜好进行设置。- 一定要在
路由之前注册中间件 - 客户端发送过来的请求,
可以连续调用多个中间件进行处理 - 执行完中间件的业务代码之后,
不要忘记调用 next() 函数 - 为了
防止代码逻辑混乱,调用 next() 函数后不要再写额外的代码 - 连续调用多个中间件时,多个中间件之间,
共享req 和 res 对象
Express 官方把
graph TD A((中间件)) --> B((应用级别的中间件)) A --> C((路由级别的中间件)) A --> D((错误级别的中间件)) A --> F((Express内置的中间件)) A --> G((第三方的中间件))常见的中间件用法,分成了5 大类,分别是:应用级别的中间件通过 app.use() 或 app.get() 或 app.post() ,绑定到 app 实例上的中间件,叫做应用级别的中间件,代码示例如下:
路由级别的中间件绑定到
express.Router()实例上的中间件,叫做路由级别的中间件。它的用法和应用级别中间件没有任何区别。只不过,应用级别中间件是绑定到 app 实例上,路由级别中间件绑定到 router 实例上,代码示例如下:错误级别的中间件错误级别中间件的
作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。格式: 错误级别中间件的 function 处理函数中,
必须有 4 个形参,形参顺序从前到后,分别是 (err, req, res, next)。
Express内置的中间件注意:
错误级别的中间件,必须注册在所有路由之后!自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验:
express.static快速托管静态资源的内置中间件,例如: HTML 文件、图片、CSS 样式等(无兼容性)express.json解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)express.urlencoded解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
// 导入 express 模块 const express = require('express') // 创建 express 的服务器实例 const app = express() // 注意:除了错误级别的中间件,其他的中间件,必须在路由之前进行配置 // 通过 express.json() 这个中间件,解析表单中的 JSON 格式的数据 app.use(express.json()) // 通过 express.urlencoded() 这个中间件,来解析 表单中的 url-encoded 格式的数据 app.use(express.urlencoded({ extended: false })) app.post('/user', (req, res) => { // 在服务器,可以使用 req.body 这个属性,来接收客户端发送过来的请求体数据 // 默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 undefined console.log(req.body) res.send('ok') }) app.post('/book', (req, res) => { // 在服务器端,可以通过 req,body 来获取 JSON 格式的表单数据和 url-encoded 格式的数据 console.log(req.body) res.send('ok') }) // 调用 app.listen 方法,指定端口号并启动web服务器 app.listen(80, function () { console.log('Express server running at 127.0.0.1') })第三方的中间件非 Express 官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。在项目中,大家可以
按需下载并配置第三方中间件,从而提高项目的开发效率。例如:在 express@4.16.0 之前的版本中,经常使用 body-parser 这个第三方中间件,来解析请求体数据。使用步骤如下:
- 运行 npm install body-parser 安装中间件
- 使用 require 导入中间件
- 调用 app.use() 注册并使用中间件
使用Express写接口注意:Express 内置的 express.urlencoded 中间件,就是基于 body-parser 这个第三方中间件进一步封装出来的
- 创建基本的服务器
- 创建API路由模块
- 编写GET接口
- 编写POST接口
- 服务器代码
// 导入 express const express = require('express') // 创建服务器实例 const app = express() // 配置解析表单数据的中间件 app.use(express.urlencoded({ extended: false })) // 必须在配置 cors 中间件之前,配置 JSONP 的接口 app.get('/api/jsonp', (req, res) => { // TODO: 定义 JSONP 接口具体的实现过程 // 1. 得到函数的名称 const funcName = req.query.callback // 2. 定义要发送到客户端的数据对象 const data = { name: 'zs', age: 22 } // 3. 拼接出一个函数的调用 const scriptStr = `${funcName}(${JSON.stringify(data)})` // 4. 把拼接的字符串,响应给客户端 res.send(scriptStr) }) // 一定要在路由之前,配置 cors 这个中间件,从而解决接口跨域的问题 const cors = require('cors') app.use(cors()) // 导入路由模块 const router = require('./16.apiRouter') // 把路由模块,注册到 app 上 app.use('/api', router) // 启动服务器 app.listen(80, () => { console.log('express server running at 127.0.0.1') })- API 路由模块
const express = require('express') const router = express.Router() // 在这里挂载对应的路由 router.get('/get', (req, res) => { // 通过 req.query 获取客户端通过查询字符串,发送到服务器的数据 const query = req.query // 调用 res.send() 方法,向客户端响应处理的结果 res.send({ status: 0, // 0 表示处理成功,1 表示处理失败 msg: 'GET 请求成功!', // 状态的描述 data: query, // 需要响应给客户端的数据 }) }) // 定义 POST 接口 router.post('/post', (req, res) => { // 通过 req.body 获取请求体中包含的 url-encoded 格式的数据 const body = req.body // 调用 res.send() 方法,向客户端响应结果 res.send({ status: 0, msg: 'POST 请求成功!', data: body, }) }) // 定义 DELETE 接口 router.delete('/delete', (req, res) => { res.send({ status: 0, msg: 'DELETE请求成功', }) }) module.exports = routerCORS跨域资源共享 接口的跨域问题刚才编写的 GET 和 POST接口,存在一个很严重的问题:不支持跨域请求。
解决接口跨域问题的方案主要有两种:- CORS(主流的解决方案,推荐使用)
- JSONP(有缺陷的解决方案:只支持 GET 请求)
cors 是 Express 的一个第三方中间件。通过安装和配置 cors 中间件,可以很方便地解决跨域问题。使用步骤分为如下 3 步:
- 运行 npm install cors 安装中间件
- 使用 const cors = require('cors') 导入中间件
- 在路由之前调用 app.use(cors()) 配置中间件
CORS (Cross-Origin Resource Sharing,跨域资源共享)由一系列
HTTP 响应头组成,这些 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源。浏览器的
同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了 CORS 相关的 HTTP 响应头,就可以解除浏览器端的跨域访问限制。
CORS 响应头部 - Access-Control-Allow-Origin注意:
- CORS 主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口。
- CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启了 CORS 的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。
响应头部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:
Access-Controlj-Allow-Origin:< origin > | *其中,origin 参数的值指定了
允许访问该资源的外域 URL。
例如,下面的字段值将只允许来自 itcast.cn 的请求:res.setHeader('Access-Control-Allow-Origin','itcast.cn')如果指定了 Access-Control-Allow-Origin 字段的值为通配符 * ,表示允许来自任何域的请求,示例代码如下:
res.setHeader('Access-Control-Allow-Origin','*')CORS 响应头部 - Access-Control-Allow-Headers默认情况下,CORS
仅支持客户端向服务器发送如下的 9 个请求头:
Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type (值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)如果客户端向服务器
发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers对额外的请求头进行声明,否则这次请求会失败!//允许客户端额外向服务器发送Content-Type 请求头和X-Custom-Header请求头 //注意:多个请求头之间使用英文的逗号进行分隔 res.setHeader('Access-Control-Allow-Headers','Content-Type,X-Custom-Header')
CORS 响应头部 - Access-Control-Allow-Methods注意:多个请求头之间使用英文的逗号进行分隔
默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。
如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods
来指明实际请求所允许使用的 HTTP 方法。示例代码如下:
//只允许 POST,GET,DELETE,HEAD请求方法 res.setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD') //允许所有的HTTP请求方法 res.setHeader('Access-Control-Allow-Methods','*')CORS请求的分类客户端在请求CORS接口时,根据
请求方式和请求头的不同,可以将 CORS 的请求分为两大类,分别是:- 简单请求
- 预检请求
同时满足以下两大条件的请求,就属于简单请求:
请求方式:GET、POST、HEAD 三者之一HTTP 头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type(只有三个值application/x-www-form- urlencoded、multipart/form-data、text/plain)
只要符合以下任何一个条件的请求,都需要进行预检请求:
- 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
- 请求头中包含自定义头部字段
- 向服务器发送了 application/json 格式的数据
简单请求和预检请求的区别在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
简单请求的特点:客户端与服务器之间只会发生一次请求。
跨域写JSONP接口 回顾jsonp的概念和特点
预检请求的特点:客户端与服务器之间会发生两次请求,OPTION 预检请求成功之后,才会发起真正的请求。
创建JSONP接口的注意事项概念:浏览器端通过< script >标签的src属性,请求服务器上的数据,同时服务器返回一个函数调用。这种请求方式叫JSONP。
特点:JSONP不属于真正Ajax请求,因为它没有使用XMLttpReuest这个对象。JSONP仅支持GET请求,不支持POST,PUT,DELETE等请求。如果项目中
已经配置了 CORS跨域资源共享,为了防止冲突,必须在配置 CORS 中间件之前声明 JSONP 的接口。否则JSONP 接口会被处理成开启了 CORS 的接口。示例代码如下:const express = require('express') // 创建服务器实例 const app = express() // 配置解析表单数据的中间件 app.use(express.urlencoded({ extended: false })) // 必须在配置 cors 中间件之前,配置 JSONP 的接口 app.get('/api/jsonp', (req, res) => { // TODO: 定义 JSONP 接口具体的实现过程 // 1. 得到函数的名称 const funcName = req.query.callback // 2. 定义要发送到客户端的数据对象 const data = { name: 'zs', age: 22 } // 3. 拼接出一个函数的调用 const scriptStr = `${funcName}(${JSON.stringify(data)})` // 4. 把拼接的字符串,响应给客户端 res.send(scriptStr) })在网页中使用jQuery发起JSONP请求调用 $.ajax() 函数,提供 JSONP 的配置选项,从而发起 JSONP 请求,示例代码如下:
<body> <button id="btnGET">GET</button> <button id="btnPOST">POST</button> <button id="btnDelete">DELETE</button> <button id="btnJSONP">JSONP</button> <script> $(function () { // 1. 测试GET接口 $('#btnGET').on('click', function () { $.ajax({ type: 'GET', url: '127.0.0.1/api/get', data: { name: 'zs', age: 20 }, success: function (res) { console.log(res) }, }) }) // 2. 测试POST接口 $('#btnPOST').on('click', function () { $.ajax({ type: 'POST', url: '127.0.0.1/api/post', data: { bookname: '水浒传', author: '施耐庵' }, success: function (res) { console.log(res) }, }) }) // 3. 为删除按钮绑定点击事件处理函数 $('#btnDelete').on('click', function () { $.ajax({ type: 'DELETE', url: '127.0.0.1/api/delete', success: function (res) { console.log(res) }, }) }) // 4. 为 JSONP 按钮绑定点击事件处理函数 $('#btnJSONP').on('click', function () { $.ajax({ type: 'GET', url: '127.0.0.1/api/jsonp', dataType: 'jsonp', success: function (res) { console.log(res) }, }) }) }) </script> </body>数据库与身份认证- 常用的数据库代码
-- 通过 * 把 users 表中所有的数据查询出来 -- select * from users -- 从 users 表中把 username 和 password 对应的数据查询出来 -- select username, password from users -- 向 users 表中,插入新数据,username 的值为 tony stark password 的值为 098123 -- insert into users (username, password) values ('tony stark', '098123') -- select * from users -- 将 id 为 4 的用户密码,更新成 888888 -- update users set password='888888' where id=4 -- select * from users -- 更新 id 为 2 的用户,把用户密码更新为 admin123 同时,把用户的状态更新为 1 -- update users set password='admin123', 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<>'ls' -- select * from users where username!='ls' -- 使用 AND 来显示所有状态为0且id小于3的用户 -- select * from users where status=0 and id<3 -- 使用 or 来显示所有状态为1 或 username 为 zs 的用户 -- select * from users where status=1 or username='zs' -- 对users表中的数据,按照 status 字段进行升序排序 -- select * from users order by status -- 按照 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实际开发中库、表、行、字段的关系- 在实际项目开发中,一般情况下,每个项目都对应独立的数据库。
- 不同的数据,要存储到数据库的不同表中,例如:用户数据存储到 users 表中,图书数据存储到 books 表中。
- 每个表中具体存储哪些信息,由字段来决定,例如:我们可以为 users 表设计 id、username、password 这 3 个
字段。 - 表中的行,代表每一条具体的数据。
- PK(Primary Key)主键、唯一标识
- NN(Not Null)值不允许为空
- UQ(Unique)值唯一
- AI(Auto Increment)值自动增长
- 安装操作 MySQL 数据库的第三方模块(mysql)
- 通过 mysql 模块连接到 MySQL 数据库
- 通过 mysql 模块执行 SQL 语句
mysql 模块是托管于 npm 上的第三方模块。它提供了在 Node.js 项目中连接和操作 MySQL 数据库的能力。想要在项目中使用它,需要先运行如下命令,将 mysql 安装为项目的依赖包:
npm install mysql配置mysql模块在使用 mysql 模块操作 MySQL 数据库之前,必须先对 mysql 模块进行必要的配置,主要的配置步骤如下:
测试mysql模块是否可以正常工作调用 db.query() 函数,指定要执行的 SQL 语句,通过回调函数拿到执行的结果:
const mysql = require('mysql') const db = mysql.createPool({ host: '127.0.0.1', user: 'root', password: 'root', database: 'bookman', }) //检查mysql模块是否正常使用 db.query('SELECT 1', (err, results) => { if (err) return console.log(err.message); //只要可以输出[ RowDataPacket { '1': 1 } ]的结果,就证明数据库链接没问题。 console.log(results); })查询语句查询表中数据示例:
const mysql = require('mysql') const db = mysql.createPool({ host: '127.0.0.1', user: 'root', password: 'root', database: 'bookman', }) db.query('SELECT * FROM tb_book', (err, results) => { //查询失败 if (err) return console.log(err.message); //查询成功 //如果执行的是select查询语句,则返回的是数组 console.log(results); })插入数据向表中插入数据,示例代码如下。
- 便捷方式
//插入数据 简化形式 const user = { username: 'liyu2', password: '49023dfs!2' } //待执行的sql语句 const sqlStr = 'INSERT INTO tb_user SET ?' //使用数组的形式,依次为?占位符指定具体的值。 db.query(sqlStr, user, (err, results) => { //失败了 if (err) return console.log(err.message); //成功了 if (results.affectedRows) { console.log('插入数据成功'); } })注意:
这种方法适合插入多项属性时使用
db.query里的第二个值一定是对象,如果是多个值,就用中括号包含
如果执行的是insert into语句,则result是一个对象
可以通过affectedRows属性,来判断是否插入成功- 初始形式
//插入数据 const list = { username: 'liyu3', password: '23123' } //待执行的sql语句 const sqlstr = 'INSERT INTO tb_user SET ?' //使用数组的形式,依次为?占位符指定具体的值。 db.query(sqlstr, [list.username, list.password], (err, results) => { //失败了 if (err) return console.log(err.message); //成功了 //注意:如果执行的是insert into语句,则result是一个对象 //可以通过affectedRows属性,来判断是否插入成功 if (results.affectedRows) { console.log('插入数据成功'); } })更新数据可以通过如下方式,更新表中的数据:
//需要更新的数据 const update = { username: 'liyu', password: '3343' } //要执行的SQL语句 const updateSql = 'UPDATE tb_user SET password=? WHERE username = ?' //3.调用db.query()执行 SQL 语句的同时,依次为占位符指定具体的值 db.query(updateSql, [update.password, update.username], (err, results) => { if (err) return console.log(err.message); if (results.affectedRows) { console.log('更新状态成功'); } })注意:
执行了update后,返回的也是一个对象,可以通过.affectedRows来判断是否成功- 便捷方式
const update2 = { username: 'liyu2', password: '111' } const updateSql1 = 'UPDATE tb_user SET ? WHERE username=?' db.query(updateSql1, [update2, update2.username], (err, results) => { if (err) return console.log(err.message); if (results.affectedRows) { console.log('更新成功'); } })
删除数据注意:
在是使用便捷方式开发代码时需要注意,db.query里的第二参数里,第一个值一定是对象。在删除数据时,推荐根据 id 这样的唯一标识,来删除对应的数据。示例如下:
const sqlStr = 'delete from tb_user where username = ?' db.query(sqlStr, 'liyu2', (err, results) => { if (err) return console.log(err.message); if (results.affectedRows) { console.log('删除成功'); } })注意:
调用db.query()执行SQL语句时,为占位符指定具体的值
如果SQL里有多个占位符,则必须使用数组为每个占位符指定具体的值
如果SQL里只有一个占位符,则可以省略数组标记删除使用 DELETE 语句,会把真正的把数据从表中删除掉。为了保险起见,
前后端的身份认证 Web开发模式推荐使用标记删除的形式,来模拟删除的动作。
所谓的标记删除,就是在表中设置类似于status这样的状态字段,来标记当前这条数据是否被删除。
当用户执行了删除的动作时,我们并没有执行 DELETE 语句把数据删除掉,而是执行了 UPDATE 语句,将这条数据对应
的 status 字段标记为删除即可。目前主流的 Web 开发模式有两种,分别是:
- 基于服务端渲染的传统 Web 开发模式
- 基于前后端分离的新型 Web 开发模式
服务端渲染的概念:服务器
发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用 Ajax 这样的技术额外请求页面的数据。代码示例如下:- 服务端渲染的优缺点
优点:
- 前端耗时少。因为服务器端负责动态生成 HTML 内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
- 有利于SEO。因为服务器端响应的是完整的 HTML 页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO。
前后端分离的 Web 开发模式缺点:
- 占用服务器端资源。即服务器端完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力。
- 不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于项目高效开发。
前后端分离的概念:前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。简而言之,前后端分离的 Web 开发模式,就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式。
- 前后端分离的优缺点
优点:
- 开发体验好。前端专注于 UI 页面的开发,后端专注于api 的开发,且前端有更多的选择性。
- 用户体验好。Ajax 技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。
- 减轻了服务器端的渲染压力。因为页面最终是在每个用户的浏览器中生成的。
如何选择前后端的身份认证缺点:
- 不利于 SEO。因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方案:利用 Vue、React 等前端框架的 SSR (server side render)技术能够很好的解决 SEO 问题!)
不谈业务场景而盲目选择使用何种开发模式都是耍流氓。
- 比如企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的 SEO,则这时我们就需要使用服务器端渲染;
- 而类似后台管理项目,交互性比较强,不需要考虑 SEO,那么就可以使用前后端分离的开发模式。
另外,具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器端渲染 + 其他页面前后端分离的开发模式。
身份认证身份认证(Authentication)又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。
- 日常生活中的身份认证随处可见,例如:高铁的验票乘车,手机的密码或指纹解锁,支付宝或微信的支付密码等。
- 在 Web 开发中,也涉及到用户身份的认证,例如:各大网站的手机验证码登录、邮箱密码登录、二维码登录等。
对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
- 服务端渲染推荐使用 Session 认证机制
- 前后端分离推荐使用 JWT 认证机制
HTTP 协议的无状态性,指的是客户端
如何突破 HTTP 无状态的限制的每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态。对于超市来说,为了方便收银员在进行结算时给 VIP 用户打折,超市可以为每个 VIP 用户发放会员卡。
什么是cookie注意:
现实生活中的会员卡身份认证方式,在 Web 开发中的专业术语叫做 Cookie。Cookie 是
存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie有效期、安全性、使用范围的可选属性组成。不同域名下的 Cookie 各自独立,每当客户端发起请求时,会
自动把当前域名下所有未过期的 Cookie一同发送到服务器。cookie的几大特性:
- 自动发送
- 域名独立
- 过期时限
4kb限制
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
Cookie 不具有安全性
由于
Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此Cookie 很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器。
提高身份认证的安全性注意:
千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等。为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡之后,可以在收银机上进行刷卡认证。只有收银机确认存在的会员卡,才能被正常使用。
这种“
Session的工作原理 在Express中使用Session认证会员卡+刷卡认证”的设计理念,就是 Session 认证机制的精髓。在 Express 项目中,只需要安装 express-session 中间件,即可在项目中使用 Session 认证:
npm install express-session配置express-session中间件express-session 中间件安装成功后,需要通过 app.use() 来注册 session 中间件,示例代码如下:
// TODO_01:请配置 Session 中间件 const session = require('express-session') app.use(session({ secret: 'itmeima', resave: false, saveUninitialized: true, }))
向 session 中存数据当 express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息:
// 登录的 API 接口 app.post('/api/login', (req, res) => { // 判断用户提交的登录信息是否正确 if (req.body.username !== 'admin' || req.body.password !== '000000') { return res.send({ status: 1, msg: '登录失败' }) } // TODO_02:请将登录成功后的用户信息,保存到 Session 中 //注意:只有成功配置了express-session中间件后,才可以使用req.session这个中间件 req.session.user = req.body //将用户信息保存到session中 req.session.islogin = true //用户的登陆状态 res.send({ status: 0, msg: '登录成功' }) })
从 session 中取数据可以直接从req.session对象上获取之间存储的数据。
// 获取用户姓名的接口 app.get('/api/username', (req, res) => { // TODO_03:请从 Session 中获取用户的名称,响应给客户端 //判断用户登陆 if (!req.session.islogin) { return res.send({ status: 1, msg: 'fail' }) } res.send({ status: 0, msg: 'success', username: req.session.user.username }) })
清空session调用 req.session.destroy() 函数,即可清空服务器保存的 session 信息。
// 退出登录的接口 app.post('/api/logout', (req, res) => { // TODO_04:清空 Session 信息 req.session.destroy() req.send({ status: '0', mag: '退出登陆成功' }) })JWT 认证机制 了解 Session 认证的局限性Session 认证机制
需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
JWT注意:
- 当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
- 当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制。
JWT(英文全称:JSON Web Token)是目前最流行的跨域认证解决方案。
JWT的工作原理
JWT的组成总结:用户的信息通过Tocken字符串的形式,保存在客户端浏览器中,服务器通过还原Tocken字符串的形式来验证用户的身份。
session认证和JWT认证的区别:
session认证存放在服务器中,JWT认证存放在浏览器中Header(头部),Payload(有效荷载),Signature(签名)
三者之间用英文的“.”分隔,格式如下:Header.Payload,SignatureJWT 的三个部分各自代表的含义JWT 的三个组成部分,从前到后分别是 Header、Payload、Signature。
其中:- Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
- Header 和 Signature 是安全性相关的部分,只是为了保证 Token 的安全性。
客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage 或 sessionStorage 中。
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是把 JWT 放在 HTTP 请求头的
Authorization 字段中,格式如下:Authorization:Bearer < token >在 Express 中使用 JWT 安装 JWT 相关的包运行如下命令,安装两个JWT相关的包:
npm install jsonwebtoken express-jwt
导入JWT相关的包其中:
jsonwebtoken 用于生成 JWT 字符串
express-jwt 用于将 JWT 字符串解析还原成 JSON 对象使用require() 函数,分别导入JWT相关的两个包:
// TODO_01:安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt const jwt = require('jsonwebtoken') const expressJwt = require('express-jwt')定义 secret 密钥为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的 secret 密钥:
- 当生成 JWT 字符串的时候,需要使用 secret 密钥对用户的信息进行加密,最终得到加密好的 JWT 字符串
- 当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 密钥进行解密
// TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey const secretKey = 'liyu No1'在登录成功后生成 JWT 字符串调用 jsonwebtoken 包提供的 sign() 方法,将用户的信息加密成 JWT 字符串,响应给客户端:
// 登录成功 // TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端 const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' }) res.send({ status: 200, message: '登录成功!', token: 'tokenStr' // 要发送给客户端的 token 字符串 })将JWT字符串还原为JSON对象客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证。
此时,服务器可以通过 express-jwt 这个中间件,自动将客户端发送过来的 Token 解析还原成 JSON 对象:
app.use(expressJwt({ secret: 'secretKey' }.unless({ path: [/^\/api\//] })))使用 req.user 获取用户信息当 express-jwt 这个中间件配置成功之后,即可在那些有权限的接口中,使用 req.user 对象,来访问从 JWT 字符串中解析出来的用户信息了,示例代码如下:
// 这是一个有权限的 API 接口 app.get('/admin/getinfo', function (req, res) { // TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端 console.log(req.user); res.send({ status: 200, message: '获取用户信息成功!', data: req.user, // 要发送给客户端的用户信息 }) })捕获解析JWT失败后产生的错误当使用express-jwt解析Token字符串时,如果客户端发送过来的Tocken字符串过期,或者不合法,就会产生一个解析失败的错误,我们可以使用Express的错误中间件,捕获这个错误并进行相关的处理,示例代码如下:
大事件项目 Headline
1. 初始化 1.1 创建项目大事件后台 API 项目,API 接口文档请参考 www.showdoc.cc/escook?page_id=3707158761215217
- 新建
api_server文件夹作为项目根目录,并在项目根目录中运行如下的命令,初始化包管理配置文件:
npm init -y- 运行如下的命令,安装特定版本的
express:
npm i express@4.17.1- 在项目根目录中新建
app.js作为整个项目的入口文件,并初始化如下的代码:
// 导入 express 模块 const express = require('express') // 创建 express 的服务器实例 const app = express() // write your code here... // 调用 app.listen 方法,指定端口号并启动web服务器 app.listen(3007, function () { console.log('api server running at 127.0.0.1:3007') })1.2 配置 cors 跨域- 运行如下的命令,安装
cors中间件:
npm i cors@2.8.5- 在
app.js中导入并配置cors中间件:
// 导入 cors 中间件 const cors = require('cors') // 将 cors 注册为全局中间件 app.use(cors())1.3 配置解析表单数据的中间件- 通过如下的代码,配置解析
application/x-www-form-urlencoded格式的表单数据的中间件:
app.use(express.urlencoded({ extended: false }))1.4 初始化路由相关的文件夹-
在项目根目录中,新建
router文件夹,用来存放所有的路由模块路由模块中,只存放客户端的请求与处理函数之间的映射关系
-
在项目根目录中,新建
router_handler文件夹,用来存放所有的路由处理函数模块路由处理函数模块中,专门负责存放每个路由对应的处理函数
- 在
router文件夹中,新建user.js文件,作为用户的路由模块,并初始化代码如下:
const express = require('express') // 创建路由对象 const router = express.Router() // 注册新用户 router.post('/reguser', (req, res) => { res.send('reguser OK') }) // 登录 router.post('/login', (req, res) => { res.send('login OK') }) // 将路由对象共享出去 module.exports = router- 在
app.js中,导入并使用用户路由模块:
// 导入并注册用户路由模块 const userRouter = require('./router/user') app.use('/api', userRouter)1.6 抽离用户路由模块中的处理函数目的:为了保证
路由模块的纯粹性,所有的路由处理函数,必须抽离到对应的路由处理函数模块中- 在
/router_handler/user.js中,使用exports对象,分别向外共享如下两个路由处理函数:
/** * 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用 */ // 注册用户的处理函数 exports.regUser = (req, res) => { res.send('reguser OK') } // 登录的处理函数 exports.login = (req, res) => { res.send('login OK') }- 将
/router/user.js中的代码修改为如下结构:
const express = require('express') const router = express.Router() // 导入用户路由处理函数模块 const userHandler = require('../router_handler/user') // 注册新用户 router.post('/reguser', userHandler.regUser) // 登录 router.post('/login', userHandler.login) module.exports = router2. 登录注册 2.1 新建 ev_users 表- 在
my_db_01数据库中,新建ev_users表如下:
在 API 接口项目中,需要安装并配置
mysql这个第三方模块,来连接和操作 MySQL 数据库- 运行如下命令,安装
mysql模块:
npm i mysql@2.18.1- 在项目根目录中新建
/db/index.js文件,在此自定义模块中创建数据库的连接对象:
// 导入 mysql 模块 const mysql = require('mysql') // 创建数据库连接对象 const db = mysql.createPool({ host: '127.0.0.1', user: 'root', password: 'admin123', database: 'my_db_01', }) // 向外共享 db 数据库连接对象 module.exports = db2.3 注册 2.3.0 实现步骤- 检测表单数据是否合法
- 检测用户名是否被占用
- 对密码进行加密处理
- 插入新用户
- 判断用户名和密码是否为空
// 接收表单数据 const userinfo = req.body // 判断数据是否合法 if (!userinfo.username || !userinfo.password) { return res.send({ status: 1, message: '用户名或密码不能为空!' }) }2.3.2 检测用户名是否被占用- 导入数据库操作模块:
const db = require('../db/index')- 定义 SQL 语句:
const sql = `select * from ev_users where username=?`- 执行 SQL 语句并根据结果判断用户名是否被占用:
db.query(sql, [userinfo.username], function (err, results) { // 执行 SQL 语句失败 if (err) { return res.send({ status: 1, message: err.message }) } // 用户名被占用 if (results.length > 0) { return res.send({ status: 1, message: '用户名被占用,请更换其他用户名!' }) } // TODO: 用户名可用,继续后续流程... })2.3.3 对密码进行加密处理为了保证密码的安全性,不建议在数据库以
明文的形式保存用户密码,推荐对密码进行加密存储
在当前项目中,使用
bcryptjs对用户密码进行加密,优点:- 加密之后的密码,无法被逆向破解
- 同一明文密码多次加密,得到的加密结果各不相同,保证了安全性
- 运行如下命令,安装指定版本的
bcryptjs:
npm i bcryptjs@2.4.3- 在
/router_handler/user.js中,导入bcryptjs:
const bcrypt = require('bcryptjs')- 在注册用户的处理函数中,确认用户名可用之后,调用
bcrypt.hashSync(明文密码, 随机盐的长度)方法,对用户的密码进行加密处理:
// 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串 userinfo.password = bcrypt.hashSync(userinfo.password, 10)2.3.4 插入新用户- 定义插入用户的 SQL 语句:
const sql = 'insert into ev_users set ?'- 调用
db.query()执行 SQL 语句,插入新用户:
db.query(sql, { username: userinfo.username, password: userinfo.password }, function (err, results) { // 执行 SQL 语句失败 if (err) return res.send({ status: 1, message: err.message }) // SQL 语句执行成功,但影响行数不为 1 if (results.affectedRows !== 1) { return res.send({ status: 1, message: '注册用户失败,请稍后再试!' }) } // 注册成功 res.send({ status: 0, message: '注册成功!' }) })2.4 优化 res.send() 代码在处理函数中,需要多次调用
res.send()向客户端响应处理失败的结果,为了简化代码,可以手动封装一个 res.cc() 函数- 在
app.js中,所有路由之前,声明一个全局中间件,为 res 对象挂载一个res.cc()函数 :
// 响应数据的中间件 app.use(function (req, res, next) { // status = 0 为成功; status = 1 为失败; 默认将 status 的值设置为 1,方便处理失败的情况 res.cc = function (err, status = 1) { res.send({ // 状态 status, // 状态描述,判断 err 是 错误对象 还是 字符串 message: err instanceof Error ? err.message : err, }) } next() })2.5 优化表单数据验证表单验证的原则:前端验证为辅,后端验证为主,后端永远不要相信前端提交过来的任何内容
在实际开发中,前后端都需要对表单的数据进行合法性的验证,而且,后端做为数据合法性验证的最后一个关口,在拦截非法数据方面,起到了至关重要的作用。
单纯的使用
if...else...的形式对数据合法性进行验证,效率低下、出错率高、维护性差。因此,推荐使用第三方数据验证模块,来降低出错率、提高验证的效率与可维护性,让后端程序员把更多的精力放在核心业务逻辑的处理上。- 安装
@hapi/joi包,为表单中携带的每个数据项,定义验证规则:
npm install @hapi/joi@17.1.0- 安装
@escook/express-joi中间件,来实现自动对表单数据进行验证的功能:
npm i @escook/express-joi- 新建
/schema/user.js用户信息验证规则模块,并初始化代码如下:
const joi = require('@hapi/joi') /** * string() 值必须是字符串 * alphanum() 值只能是包含 a-zA-Z0-9 的字符串 * min(length) 最小长度 * max(length) 最大长度 * required() 值是必填项,不能为 undefined * pattern(正则表达式) 值必须符合正则表达式的规则 */ // 用户名的验证规则 const username = joi.string().alphanum().min(1).max(10).required() // 密码的验证规则 const password = joi .string() .pattern(/^[\S]{6,12}$/) .required() // 注册和登录表单的验证规则对象 exports.reg_login_schema = { // 表示需要对 req.body 中的数据进行验证 body: { username, password, }, }- 修改
/router/user.js中的代码如下:
const express = require('express') const router = express.Router() // 导入用户路由处理函数模块 const userHandler = require('../router_handler/user') // 1. 导入验证表单数据的中间件 const expressJoi = require('@escook/express-joi') // 2. 导入需要的验证规则对象 const { reg_login_schema } = require('../schema/user') // 注册新用户 // 3. 在注册新用户的路由中,声明局部中间件,对当前请求中携带的数据进行验证 // 3.1 数据验证通过后,会把这次请求流转给后面的路由处理函数 // 3.2 数据验证失败后,终止后续代码的执行,并抛出一个全局的 Error 错误,进入全局错误级别中间件中进行处理 router.post('/reguser', expressJoi(reg_login_schema), userHandler.regUser) // 登录 router.post('/login', userHandler.login) module.exports = router- 在
app.js的全局错误级别中间件中,捕获验证失败的错误,并把验证失败的结果响应给客户端:
const joi = require('@hapi/joi') // 错误中间件 app.use(function (err, req, res, next) { // 数据验证失败 if (err instanceof joi.ValidationError) return res.cc(err) // 未知错误 return res.cc(err) })2.6 登录 2.6.0 实现步骤- 检测表单数据是否合法
- 根据用户名查询用户的数据
- 判断用户输入的密码是否正确
- 生成 JWT 的 Token 字符串
- 将
/router/user.js中登录的路由代码修改如下:
// 登录的路由 router.post('/login', expressJoi(reg_login_schema), userHandler.login)2.6.2 根据用户名查询用户的数据- 接收表单数据:
const userinfo = req.body- 定义 SQL 语句:
const sql = `select * from ev_users where username=?`- 执行 SQL 语句,查询用户的数据:
db.query(sql, userinfo.username, function (err, results) { // 执行 SQL 语句失败 if (err) return res.cc(err) // 执行 SQL 语句成功,但是查询到数据条数不等于 1 if (results.length !== 1) return res.cc('登录失败!') // TODO:判断用户输入的登录密码是否和数据库中的密码一致 })2.6.3 判断用户输入的密码是否正确核心实现思路:调用
bcrypt.compareSync(用户提交的密码, 数据库中的密码)方法比较密码是否一致返回值是布尔值(true 一致、false 不一致)
具体的实现代码如下:
// 拿着用户输入的密码,和数据库中存储的密码进行对比 const compareResult = bcrypt.compareSync(userinfo.password, results[0].password) // 如果对比的结果等于 false, 则证明用户输入的密码错误 if (!compareResult) { return res.cc('登录失败!') } // TODO:登录成功,生成 Token 字符串2.6.4 生成 JWT 的 Token 字符串核心注意点:在生成 Token 字符串的时候,一定要剔除 密码 和 头像 的值
- 通过 ES6 的高级语法,快速剔除
密码和头像的值:
// 剔除完毕之后,user 中只保留了用户的 id, username, nickname, email 这四个属性的值 const user = { ...results[0], password: '', user_pic: '' }- 运行如下的命令,安装生成 Token 字符串的包:
npm i jsonwebtoken@8.5.1- 在
/router_handler/user.js模块的头部区域,导入jsonwebtoken包:
// 用这个包来生成 Token 字符串 const jwt = require('jsonwebtoken')- 创建
config.js文件,并向外共享 加密 和 还原 Token 的jwtSecretKey字符串:
module.exports = { jwtSecretKey: 'itheima No1. ^_^', }- 将用户信息对象加密成 Token 字符串:
// 导入配置文件 const config = require('../config') // 生成 Token 字符串 const tokenStr = jwt.sign(user, config.jwtSecretKey, { expiresIn: '10h', // token 有效期为 10 个小时 })- 将生成的 Token 字符串响应给客户端:
res.send({ status: 0, message: '登录成功!', // 为了方便客户端使用 Token,在服务器端直接拼接上 Bearer 的前缀 token: 'Bearer ' + tokenStr, })2.7 配置解析 Token 的中间件- 运行如下的命令,安装解析 Token 的中间件:
npm i express-jwt@5.3.3- 在
app.js中注册路由之前,配置解析 Token 的中间件:
// 导入配置文件 const config = require('./config') // 解析 token 的中间件 const expressJWT = require('express-jwt') // 使用 .unless({ path: [/^\/api\//] }) 指定哪些接口不需要进行 Token 的身份认证 app.use(expressJWT({ secret: config.jwtSecretKey }).unless({ path: [/^\/api\//] }))- 在
app.js中的错误级别中间件里面,捕获并处理 Token 认证失败后的错误:
// 错误中间件 app.use(function (err, req, res, next) { // 省略其它代码... // 捕获身份认证失败的错误 if (err.name === 'UnauthorizedError') return res.cc('身份认证失败!') // 未知错误... })3. 个人中心 3.1 获取用户的基本信息 3.1.0 实现步骤- 初始化 路由 模块
- 初始化 路由处理函数 模块
- 获取用户的基本信息
- 创建
/router/userinfo.js路由模块,并初始化如下的代码结构:
// 导入 express const express = require('express') // 创建路由对象 const router = express.Router() // 获取用户的基本信息 router.get('/userinfo', (req, res) => { res.send('ok') }) // 向外共享路由对象 module.exports = router- 在
app.js中导入并使用个人中心的路由模块:
// 导入并使用用户信息路由模块 const userinfoRouter = require('./router/userinfo') // 注意:以 /my 开头的接口,都是有权限的接口,需要进行 Token 身份认证 app.use('/my', userinfoRouter)3.1.2 初始化路由处理函数模块- 创建
/router_handler/userinfo.js路由处理函数模块,并初始化如下的代码结构:
// 获取用户基本信息的处理函数 exports.getUserInfo = (req, res) => { res.send('ok') }- 修改
/router/userinfo.js中的代码如下:
const express = require('express') const router = express.Router() // 导入用户信息的处理函数模块 const userinfo_handler = require('../router_handler/userinfo') // 获取用户的基本信息 router.get('/userinfo', userinfo_handler.getUserInfo) module.exports = router3.1.3 获取用户的基本信息- 在
/router_handler/userinfo.js头部导入数据库操作模块:
// 导入数据库操作模块 const db = require('../db/index')- 定义 SQL 语句:
// 根据用户的 id,查询用户的基本信息 // 注意:为了防止用户的密码泄露,需要排除 password 字段 const sql = `select id, username, nickname, email, user_pic from ev_users where id=?`- 调用
db.query()执行 SQL 语句:
// 注意:req 对象上的 user 属性,是 Token 解析成功,express-jwt 中间件帮我们挂载上去的 db.query(sql, req.user.id, (err, results) => { // 1. 执行 SQL 语句失败 if (err) return res.cc(err) // 2. 执行 SQL 语句成功,但是查询到的数据条数不等于 1 if (results.length !== 1) return res.cc('获取用户信息失败!') // 3. 将用户信息响应给客户端 res.send({ status: 0, message: '获取用户基本信息成功!', data: results[0], }) })3.2 更新用户的基本信息 3.2.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 实现更新用户基本信息的功能
- 在
/router/userinfo.js模块中,新增更新用户基本信息的路由:
// 更新用户的基本信息 router.post('/userinfo', userinfo_handler.updateUserInfo)- 在
/router_handler/userinfo.js模块中,定义并向外共享更新用户基本信息的路由处理函数:
// 更新用户基本信息的处理函数 exports.updateUserInfo = (req, res) => { res.send('ok') }3.2.2 验证表单数据- 在
/schema/user.js验证规则模块中,定义id,nickname,email的验证规则如下:
// 定义 id, nickname, emial 的验证规则 const id = joi.number().integer().min(1).required() const nickname = joi.string().required() const email = joi.string().email().required()- 并使用
exports向外共享如下的验证规则对象:
// 验证规则对象 - 更新用户基本信息 exports.update_userinfo_schema = { body: { id, nickname, email, }, }- 在
/router/userinfo.js模块中,导入验证数据合法性的中间件:
// 导入验证数据合法性的中间件 const expressJoi = require('@escook/express-joi')- 在
/router/userinfo.js模块中,导入需要的验证规则对象:
// 导入需要的验证规则对象 const { update_userinfo_schema } = require('../schema/user')- 在
/router/userinfo.js模块中,修改更新用户的基本信息的路由如下:
// 更新用户的基本信息 router.post('/userinfo', expressJoi(update_userinfo_schema), userinfo_handler.updateUserInfo)3.2.3 实现更新用户基本信息的功能- 定义待执行的 SQL 语句:
const sql = `update ev_users set ? where id=?`- 调用
db.query()执行 SQL 语句并传参:
db.query(sql, [req.body, req.body.id], (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 执行 SQL 语句成功,但影响行数不为 1 if (results.affectedRows !== 1) return res.cc('修改用户基本信息失败!') // 修改用户信息成功 return res.cc('修改用户基本信息成功!', 0) })3.3 重置密码 3.3.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 实现重置密码的功能
- 在
/router/userinfo.js模块中,新增重置密码的路由:
// 重置密码的路由 router.post('/updatepwd', userinfo_handler.updatePassword)- 在
/router_handler/userinfo.js模块中,定义并向外共享重置密码的路由处理函数:
// 重置密码的处理函数 exports.updatePassword = (req, res) => { res.send('ok') }3.3.2 验证表单数据核心验证思路:旧密码与新密码,必须符合密码的验证规则,并且新密码不能与旧密码一致!
- 在
/schema/user.js模块中,使用exports向外共享如下的验证规则对象:
// 验证规则对象 - 重置密码 exports.update_password_schema = { body: { // 使用 password 这个规则,验证 req.body.oldPwd 的值 oldPwd: password, // 使用 joi.not(joi.ref('oldPwd')).concat(password) 规则,验证 req.body.newPwd 的值 // 解读: // 1. joi.ref('oldPwd') 表示 newPwd 的值必须和 oldPwd 的值保持一致 // 2. joi.not(joi.ref('oldPwd')) 表示 newPwd 的值不能等于 oldPwd 的值 // 3. .concat() 用于合并 joi.not(joi.ref('oldPwd')) 和 password 这两条验证规则 newPwd: joi.not(joi.ref('oldPwd')).concat(password), }, }- 在
/router/userinfo.js模块中,导入需要的验证规则对象:
// 导入需要的验证规则对象 const { update_userinfo_schema, update_password_schema } = require('../schema/user')- 并在
重置密码的路由中,使用update_password_schema规则验证表单的数据,示例代码如下:
router.post('/updatepwd', expressJoi(update_password_schema), userinfo_handler.updatePassword)3.3.3 实现重置密码的功能- 根据
id查询用户是否存在:
// 定义根据 id 查询用户数据的 SQL 语句 const sql = `select * from ev_users where id=?` // 执行 SQL 语句查询用户是否存在 db.query(sql, req.user.id, (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 检查指定 id 的用户是否存在 if (results.length !== 1) return res.cc('用户不存在!') // TODO:判断提交的旧密码是否正确 })- 判断提交的 旧密码 是否正确:
// 在头部区域导入 bcryptjs 后, // 即可使用 bcrypt.compareSync(提交的密码,数据库中的密码) 方法验证密码是否正确 // compareSync() 函数的返回值为布尔值,true 表示密码正确,false 表示密码错误 const bcrypt = require('bcryptjs') // 判断提交的旧密码是否正确 const compareResult = bcrypt.compareSync(req.body.oldPwd, results[0].password) if (!compareResult) return res.cc('原密码错误!')- 对新密码进行
bcrypt加密之后,更新到数据库中:
// 定义更新用户密码的 SQL 语句 const sql = `update ev_users set password=? where id=?` // 对新密码进行 bcrypt 加密处理 const newPwd = bcrypt.hashSync(req.body.newPwd, 10) // 执行 SQL 语句,根据 id 更新用户的密码 db.query(sql, [newPwd, req.user.id], (err, results) => { // SQL 语句执行失败 if (err) return res.cc(err) // SQL 语句执行成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('更新密码失败!') // 更新密码成功 res.cc('更新密码成功!', 0) })3.4 更新用户头像 3.4.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 实现更新用户头像的功能
- 在
/router/userinfo.js模块中,新增更新用户头像的路由:
// 更新用户头像的路由 router.post('/update/avatar', userinfo_handler.updateAvatar)- 在
/router_handler/userinfo.js模块中,定义并向外共享更新用户头像的路由处理函数:
// 更新用户头像的处理函数 exports.updateAvatar = (req, res) => { res.send('ok') }3.4.2 验证表单数据- 在
/schema/user.js验证规则模块中,定义avatar的验证规则如下:
// dataUri() 指的是如下格式的字符串数据: // data:image/png;base64,VE9PTUFOWVNFQ1JFVFM= const avatar = joi.string().dataUri().required()- 并使用
exports向外共享如下的验证规则对象:
// 验证规则对象 - 更新头像 exports.update_avatar_schema = { body: { avatar, }, }- 在
/router/userinfo.js模块中,导入需要的验证规则对象:
const { update_avatar_schema } = require('../schema/user')- 在
/router/userinfo.js模块中,修改更新用户头像的路由如下:
router.post('/update/avatar', expressJoi(update_avatar_schema), userinfo_handler.updateAvatar)3.4.3 实现更新用户头像的功能- 定义更新用户头像的 SQL 语句:
const sql = 'update ev_users set user_pic=? where id=?'- 调用
db.query()执行 SQL 语句,更新对应用户的头像:
4.1.2 新增两条初始数据 4.2 获取文章分类列表 4.2.0 实现步骤db.query(sql, [req.body.avatar, req.user.id], (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 执行 SQL 语句成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('更新头像失败!') // 更新用户头像成功 return res.cc('更新头像成功!', 0) })4. 文章分类管理 4.1 新建 ev_article_cate 表 4.1.1 创建表结构- 初始化路由模块
- 初始化路由处理函数模块
- 获取文章分类列表数据
- 创建
/router/artcate.js路由模块,并初始化如下的代码结构:
// 导入 express const express = require('express') // 创建路由对象 const router = express.Router() // 获取文章分类的列表数据 router.get('/cates', (req, res) => { res.send('ok') }) // 向外共享路由对象 module.exports = router- 在
app.js中导入并使用文章分类的路由模块:
// 导入并使用文章分类路由模块 const artCateRouter = require('./router/artcate') // 为文章分类的路由挂载统一的访问前缀 /my/article app.use('/my/article', artCateRouter)4.2.2 初始化路由处理函数模块- 创建
/router_handler/artcate.js路由处理函数模块,并初始化如下的代码结构:
// 获取文章分类列表数据的处理函数 exports.getArticleCates = (req, res) => { res.send('ok') }- 修改
/router/artcate.js中的代码如下:
const express = require('express') const router = express.Router() // 导入文章分类的路由处理函数模块 const artcate_handler = require('../router_handler/artcate') // 获取文章分类的列表数据 router.get('/cates', artcate_handler.getArticleCates) module.exports = router4.2.3 获取文章分类列表数据- 在
/router_handler/artcate.js头部导入数据库操作模块:
// 导入数据库操作模块 const db = require('../db/index')- 定义 SQL 语句:
// 根据分类的状态,获取所有未被删除的分类列表数据 // is_delete 为 0 表示没有被 标记为删除 的数据 const sql = 'select * from ev_article_cate where is_delete=0 order by id asc'- 调用
db.query()执行 SQL 语句:
db.query(sql, (err, results) => { // 1. 执行 SQL 语句失败 if (err) return res.cc(err) // 2. 执行 SQL 语句成功 res.send({ status: 0, message: '获取文章分类列表成功!', data: results, }) })4.3 新增文章分类 4.3.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 查询
分类名称与分类别名是否被占用 - 实现新增文章分类的功能
- 在
/router/artcate.js模块中,添加新增文章分类的路由:
// 新增文章分类的路由 router.post('/addcates', artcate_handler.addArticleCates)- 在
/router_handler/artcate.js模块中,定义并向外共享新增文章分类的路由处理函数:
// 新增文章分类的处理函数 exports.addArticleCates = (req, res) => { res.send('ok') }4.3.2 验证表单数据- 创建
/schema/artcate.js文章分类数据验证模块,并定义如下的验证规则:
// 导入定义验证规则的模块 const joi = require('@hapi/joi') // 定义 分类名称 和 分类别名 的校验规则 const name = joi.string().required() const alias = joi.string().alphanum().required() // 校验规则对象 - 添加分类 exports.add_cate_schema = { body: { name, alias, }, }- 在
/router/artcate.js模块中,使用add_cate_schema对数据进行验证:
// 导入验证数据的中间件 const expressJoi = require('@escook/express-joi') // 导入文章分类的验证模块 const { add_cate_schema } = require('../schema/artcate') // 新增文章分类的路由 router.post('/addcates', expressJoi(add_cate_schema), artcate_handler.addArticleCates)4.3.3 查询分类名称与别名是否被占用- 定义查重的 SQL 语句:
// 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句 const sql = `select * from ev_article_cate where name=? or alias=?`- 调用
db.query()执行查重的操作:
// 执行查重操作 db.query(sql, [req.body.name, req.body.alias], (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 判断 分类名称 和 分类别名 是否被占用 if (results.length === 2) 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('分类别名被占用,请更换后重试!') // TODO:新增文章分类 })4.3.4 实现新增文章分类的功能- 定义新增文章分类的 SQL 语句:
const sql = `insert into ev_article_cate set ?`- 调用
db.query()执行新增文章分类的 SQL 语句:
db.query(sql, req.body, (err, results) => { // SQL 语句执行失败 if (err) return res.cc(err) // SQL 语句执行成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('新增文章分类失败!') // 新增文章分类成功 res.cc('新增文章分类成功!', 0) })4.4 根据 Id 删除文章分类 4.4.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 实现删除文章分类的功能
- 在
/router/artcate.js模块中,添加删除文章分类的路由:
// 删除文章分类的路由 router.get('/deletecate/:id', artcate_handler.deleteCateById)- 在
/router_handler/artcate.js模块中,定义并向外共享删除文章分类的路由处理函数:
// 删除文章分类的处理函数 exports.deleteCateById = (req, res) => { res.send('ok') }4.4.2 验证表单数据- 在
/schema/artcate.js验证规则模块中,定义 id 的验证规则如下:
// 定义 分类Id 的校验规则 const id = joi.number().integer().min(1).required()- 并使用
exports向外共享如下的验证规则对象:
// 校验规则对象 - 删除分类 exports.delete_cate_schema = { params: { id, }, }- 在
/router/artcate.js模块中,导入需要的验证规则对象,并在路由中使用:
// 导入删除分类的验证规则对象 const { delete_cate_schema } = require('../schema/artcate') // 删除文章分类的路由 router.get('/deletecate/:id', expressJoi(delete_cate_schema), artcate_handler.deleteCateById)4.4.3 实现删除文章分类的功能- 定义删除文章分类的 SQL 语句:
const sql = `update ev_article_cate set is_delete=1 where id=?`- 调用
db.query()执行删除文章分类的 SQL 语句:
db.query(sql, req.params.id, (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // SQL 语句执行成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('删除文章分类失败!') // 删除文章分类成功 res.cc('删除文章分类成功!', 0) })4.5 根据 Id 获取文章分类数据 4.5.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 实现获取文章分类的功能
- 在
/router/artcate.js模块中,添加根据 Id 获取文章分类的路由:
router.get('/cates/:id', artcate_handler.getArticleById)- 在
/router_handler/artcate.js模块中,定义并向外共享根据 Id 获取文章分类的路由处理函数:
// 根据 Id 获取文章分类的处理函数 exports.getArticleById = (req, res) => { res.send('ok') }4.5.2 验证表单数据- 在
/schema/artcate.js验证规则模块中,使用exports向外共享如下的验证规则对象:
// 校验规则对象 - 根据 Id 获取分类 exports.get_cate_schema = { params: { id, }, }- 在
/router/artcate.js模块中,导入需要的验证规则对象,并在路由中使用:
// 导入根据 Id 获取分类的验证规则对象 const { get_cate_schema } = require('../schema/artcate') // 根据 Id 获取文章分类的路由 router.get('/cates/:id', expressJoi(get_cate_schema), artcate_handler.getArticleById)4.5.3 实现获取文章分类的功能- 定义根据 Id 获取文章分类的 SQL 语句:
const sql = `select * from ev_article_cate where id=?`- 调用
db.query()执行 SQL 语句:
db.query(sql, req.params.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], }) })4.6 根据 Id 更新文章分类数据 4.6.0 实现步骤- 定义路由和处理函数
- 验证表单数据
- 查询
分类名称与分类别名是否被占用 - 实现更新文章分类的功能
- 在
/router/artcate.js模块中,添加更新文章分类的路由:
// 更新文章分类的路由 router.post('/updatecate', artcate_handler.updateCateById)- 在
/router_handler/artcate.js模块中,定义并向外共享更新文章分类的路由处理函数:
// 更新文章分类的处理函数 exports.updateCateById = (req, res) => { res.send('ok') }4.6.2 验证表单数据- 在
/schema/artcate.js验证规则模块中,使用exports向外共享如下的验证规则对象:
// 校验规则对象 - 更新分类 exports.update_cate_schema = { body: { Id: id, name, alias, }, }- 在
/router/artcate.js模块中,导入需要的验证规则对象,并在路由中使用:
// 导入更新文章分类的验证规则对象 const { update_cate_schema } = require('../schema/artcate') // 更新文章分类的路由 router.post('/updatecate', expressJoi(update_cate_schema), artcate_handler.updateCateById)4.5.4 查询分类名称与别名是否被占用- 定义查重的 SQL 语句:
// 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句 const sql = `select * from ev_article_cate where Id<>? and (name=? or alias=?)`- 调用
db.query()执行查重的操作:
// 执行查重操作 db.query(sql, [req.body.Id, req.body.name, req.body.alias], (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 判断 分类名称 和 分类别名 是否被占用 if (results.length === 2) 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('分类别名被占用,请更换后重试!') // TODO:更新文章分类 })4.5.5 实现更新文章分类的功能- 定义更新文章分类的 SQL 语句:
const sql = `update ev_article_cate set ? where Id=?`- 调用
db.query()执行 SQL 语句:
5.2 发布新文章 5.2.0 实现步骤db.query(sql, [req.body, req.body.Id], (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // SQL 语句执行成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('更新文章分类失败!') // 更新文章分类成功 res.cc('更新文章分类成功!', 0) })5. 文章管理 5.1 新建 ev_articles 表- 初始化路由模块
- 初始化路由处理函数模块
- 使用 multer 解析表单数据
- 验证表单数据
- 实现发布文章的功能
- 创建
/router/article.js路由模块,并初始化如下的代码结构:
// 导入 express const express = require('express') // 创建路由对象 const router = express.Router() // 发布新文章 router.post('/add', (req, res) => { res.send('ok') }) // 向外共享路由对象 module.exports = router- 在
app.js中导入并使用文章的路由模块:
// 导入并使用文章路由模块 const articleRouter = require('./router/article') // 为文章的路由挂载统一的访问前缀 /my/article app.use('/my/article', articleRouter)5.2.2 初始化路由处理函数模块- 创建
/router_handler/article.js路由处理函数模块,并初始化如下的代码结构:
// 发布新文章的处理函数 exports.addArticle = (req, res) => { res.send('ok') }- 修改
/router/article.js中的代码如下:
const express = require('express') const router = express.Router() // 导入文章的路由处理函数模块 const article_handler = require('../router_handler/article') // 发布新文章 router.post('/add', article_handler.addArticle) module.exports = router5.2.3 使用 multer 解析表单数据注意:使用
express.urlencoded()中间件无法解析multipart/form-data格式的请求体数据。当前项目,推荐使用 multer 来解析
multipart/form-data格式的表单数据。www.npmjs.com/package/multer- 运行如下的终端命令,在项目中安装
multer:
npm i multer@1.4.2- 在
/router_handler/article.js模块中导入并配置multer:
// 导入解析 formdata 格式表单数据的包 const multer = require('multer') // 导入处理路径的核心模块 const path = require('path') // 创建 multer 的实例对象,通过 dest 属性指定文件的存放路径 const upload = multer({ dest: path.join(__dirname, '../uploads') })- 修改
发布新文章的路由如下:
// 发布新文章的路由 // upload.single() 是一个局部生效的中间件,用来解析 FormData 格式的表单数据 // 将文件类型的数据,解析并挂载到 req.file 属性中 // 将文本类型的数据,解析并挂载到 req.body 属性中 router.post('/add', upload.single('cover_img'), article_handler.addArticle)- 在
/router_handler/article.js模块中的addArticle处理函数中,将multer解析出来的数据进行打印:
// 发布新文章的处理函数 exports.addArticle = (req, res) => { console.log(req.body) // 文本类型的数据 console.log('--------分割线----------') console.log(req.file) // 文件类型的数据 res.send('ok') })5.2.4 验证表单数据实现思路:通过 express-joi 自动验证 req.body 中的文本数据;通过 if 判断手动验证 req.file 中的文件数据;
- 创建
/schema/article.js验证规则模块,并初始化如下的代码结构:
// 导入定义验证规则的模块 const joi = require('@hapi/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() // 验证规则对象 - 发布文章 exports.add_article_schema = { body: { title, cate_id, content, state, }, }- 在
/router/article.js模块中,导入需要的验证规则对象,并在路由中使用:
// 导入验证数据的中间件 const expressJoi = require('@escook/express-joi') // 导入文章的验证模块 const { add_article_schema } = require('../schema/article') // 发布新文章的路由 // 注意:在当前的路由中,先后使用了两个中间件: // 先使用 multer 解析表单数据 // 再使用 expressJoi 对解析的表单数据进行验证 router.post('/add', upload.single('cover_img'), expressJoi(add_article_schema), article_handler.addArticle)- 在
/router_handler/article.js模块中的addArticle处理函数中,通过if判断客户端是否提交了封面图片:
// 发布新文章的处理函数 exports.addArticle = (req, res) => { // 手动判断是否上传了文章封面 if (!req.file || req.file.fieldname !== 'cover_img') return res.cc('文章封面是必选参数!') // TODO:表单数据合法,继续后面的处理流程... })5.2.5 实现发布文章的功能- 整理要插入数据库的文章信息对象:
// 导入处理路径的 path 核心模块 const path = require('path') const articleInfo = { // 标题、内容、状态、所属的分类Id ...req.body, // 文章封面在服务器端的存放路径 cover_img: path.join('/uploads', req.file.filename), // 文章发布时间 pub_date: new Date(), // 文章作者的Id author_id: req.user.id, }- 定义发布文章的 SQL 语句:
const sql = `insert into ev_articles set ?`- 调用
db.query()执行发布文章的 SQL 语句:
// 导入数据库操作模块 const db = require('../db/index') // 执行 SQL 语句 db.query(sql, articleInfo, (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err) // 执行 SQL 语句成功,但是影响行数不等于 1 if (results.affectedRows !== 1) return res.cc('发布文章失败!') // 发布文章成功 res.cc('发布文章成功', 0) })- 在
app.js中,使用express.static()中间件,将uploads目录中的图片托管为静态资源:
// 托管静态资源文件 app.use('/uploads', express.static('./uploads')) - eexpress.static()
- 核心思路

