目录

Node.js后端学习笔记ExpressMySQL

Node.js后端学习笔记:Express+MySQL

一、前置知识:Node的内置模块

fs文件系统模块

// 1、导入模块
const fs = require('fs')

// 2、读取文件内容
fs.readFile(path,[option],callback)
// option:可选参数,表示一扇门编码格式来读取文件
// 示例
fs.readFile('./files/11.txt','utf8',function(err,dataStr){})

// 3、写入文件内容
fs.writeFile(file,data,[options],function(err){})

// 4、__dirname表示当前文件所在的目录:用于动态拼接文件路径

path路径模块

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

// 2、路径拼接
path.join([...paths])
// 凡是涉及到路径拼接的操作都需要用join方法处理

// 3、获取路径中的文件名
path.basename(path,[ext])
// 不指定ext之后会输出完整的文件名,指定之后只输出不带后缀的文件名

// 4、获取路径中的文件扩展名
path.extname(path)

http模块

// 1、导入模块
const http = require('http')

// 服务器上安装了web 服务器软件(如Apache)实现了服务器功能
// Node.js直接通过http模块就能创建一个服务器对外提供服务
// 2、创建web服务器
const server = http.createServer()

// 3、绑定request事件,监听网络请求
server.on('server',(req,res) => {})

// 4、启动服务器
server.listen(port,() => {})

// 5、req对象包含与客户端相关的属性和数据,如:
// req.url是客户端请求的 URL 地址
// req.method是客户端的请求方法

// 6、res对象包含与服务器相关的数据和属性
// res.end方法用于向客户端发送指定内容,并结束这次请求

// 7、解决中文乱码问题
// 问题:服务端向客户端发送的中文内容时会出现乱码问题
// 解决:手动设置内容的编码格式(设置在响应操作之前)
res.setHeader('Content-Type','text/html;charset=utf-8')

二、开发规范:模块化

Node.js中的模块分类(三类)

  • 内置模块(详见第一章)
  • 自定义模块 - 用户创建的每个.js文件,都是自定义模块
  • 第三方模块

模块加载

  • 注意点:使用require方法加载模块时回执行被加载模块中的代码

模块导出

  • 每个.js自定义模块中都有一个module对象,内部存储了和当前模块有关的信息
  • 使用module.exports指定的对象为该模块最终导出的module对象

CommonJS模块化规范

  • 模块内module变量代表当前模块
  • module变量是一个对象,其exports属性是对外接口
  • 使用require方法加载模块时其实是加载module.exports属性

npm包

  • 从 网站上搜索自己所需要的包
  • 从 服务器上下载自己需要的包
  • node_modules 文件夹用来存放所有已安装到项目中的包。require() 导入第三方包时,就是从这个目录中查找并加载包。
  • package-lock.json 配置文件用来记录 node_modules 目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等。
  • i5ting_toc 是一个可以把 md 文档转为html 页面的小工具

三、轻量级框架:Express

Express的本质就是一个npm包,其内部封装了Node的内置http模块,用于快速创建Web服务器(包括网页资源和API接口)

Express的基本使用

const express = require('express')
// 1、创建 web 服务器
const app = express()

// 2、启动服务器
app.listen(port, () => {})

// 3、监听请求
app.get(url, (req,res) => {})
app.post(url, (req,res) => {})

// 4、响应请求
res.send(...响应内容)

// 5、获取参数
req.query // 查询参数  --  url后面通过 ? 拼接的参数
req.params // 动态参数  --  url后面通过 :拼接的参数

Express托管静态资源

// express提供了一个函数来创建一个静态资源服务器
app.use(express.static('public'))
// 此时通过http://localhost:3000/images/bf.jpg 可以访问public下的静态资源(路径无需添加public)
// 常见需要托管的静态资源包括图片、CSS文件、JS文件
// 多次调用可以同时托管多个静态资源目录

// 如需挂载目录前缀
app.use('/public',express.static('public'))

Express路由

在Express中,路由指的是客户端请求与服务器处理函数之间的映射关系

路由匹配过程:当一个请求到达服务器之后,需要经过路由的匹配(按照定义的先后顺序),当请求类型和URL同时满足时就会调用对应的回调函数处理

  • 重点一:同时满足请求类型和URL相同
  • 重点二:按照先后顺序匹配

模块化路由

将路由以模块的拆分的话容易管理,具体步骤如下:

  • 创建单独的模块.js文件
  • 调用express.Router()函数创建路由对象
  • 向路由对象上挂载具体的路由
  • 使用module.exports向外共享路由对象
  • 使用app.use()注册路由模块
// 1.导入 express
var express = require('express')
// 2.创建路由对象
var router = express.Router()

// 3.挂载获取用户列表的路由
router.get('/user/list'function(reqres){ 
	res.send('Get user list.')
})

// 4.挂载添加用户的路由
router.post('/user/add'function(reqres){ 
	res.send('Add new user.')
})

// 5.向外导出路由对象
module.exports = router
// 1、导入路由模块
const userRouter = require('./router/user.js')

// 2、注册路由模块(并添加统一路径访问前缀)
app.use('/api', userRouter)

中间件

中间件本质就是一个函数,多个中间件之间共享一份req和res

  • 可以在上游的中间件中统一为req和res对象添加自定义的属性和方法

其调用流程:请求 - > 中间件1 - > 中间件2 - > 响应

调用过程必须使用next()函数将流转关系交给下一个中间件或路由!

// 全局中间件(通过调用app.use定义)
app.use((req,res,next) => {
    console.log('这是全局生效的中间件')
    next()
})

// 局部生效的中间件(不使用app.use定义)
const mw1 = (req,res,next) => {
    console.log('这是局部中间件')
    next()
}
app.get('/', mw1, (req,res) => {})

中间件分类

// 1、应用级别的中间件(绑定到app实例上的中间件,包括全局和局部中间件)
app.use((res,res,next) => {
    next()
})

-----------------------------------------------------------
// 2、路由级别的中间件(绑定到router实例上的中间件)
const router = express.router()
router.use((res,res,next) => {
    next()
})

-----------------------------------------------------------
// 3、错误级别的中间件
// 作用:专门用来捕获整个项目中发生的异常错误,防止代码崩溃,形参包括(err,req,res,next)
app.use((err,res,res,next) => {
    console.log('服务器出错!')
    res.send('Error:' + err.message)
})
// 注意:错误级别的中间件必须注册在所有的路由之后

-----------------------------------------------------------
// 4、Express内置的中间件(三个常用的,提升开发效率)
// 4.1、express.static 快速托管HTML文件、图片、CSS样式等静态资源
// 托管public目录下的静态资源(增加访问前缀)
app.use('/public',express.static('public'))

// 4.2、express.json 解析JSON格式的请求体数据
// 配置解析 application/json 格式数据的内置中间件
app.use(express.json())

// 4.3、express.urlencoded 解析Url-encoded格式的请求体数据(表单默认的提交格式)
// 如:username=%E5%BC%A0%E4%B8%89&email=zhangsan%40example.com&password=123456
// 解码后:username=张三&email=zhangsan@example.com&password=123456
// 配置解析 application/x-www-form-urlencoded 格式数据的内置中间件
app.use(express.urlencoded({ extendedfalse }))

-----------------------------------------------------------
// 5、第三方中间件(需要安装,介绍常用的)
// body-parser  --  解析请求体数据
// 步骤一:使用 npm install body-parser 安装中间件
// 步骤二:使用 require 导入中间件
// 步骤三:调用 app.use() 注册中间件
// Express 内置的 express.urlencoded 中间件,就是基于body-parser 这个第三方中间件进一步封装出来的

自定义中间件

手动模拟一个类似于express.urlencoded这样的中间件,来解析POST 提交到服务器的表单数据(封装成模块)

// custom-body-parser.js 模块
const qs = require('querystring') // Node的内置模块,parse方法用于解析字符串为对象格式
function bodyParser(req,res,next){
    // 监听req的data事件(客服端发送数据就会触发,客户端可能会分割发送,data会多次触发,需拼接数据)
    let str = ''
    req.on('data', (chunk) => {
        str+ = chunk
    })
    // 请求体发送完毕会触发req的end事件
    req.on('end', () => {
        const body = qs.parse(str)
        req.body = body // 挂载到req.body,这样下游函数看到的就是对象格式的数据啦
        next()
    })
}
module.exports = bodyParser

// -------------------------分割线-------------------------
// index.js
const myBodyParser = require('custom-body-parser')
app.use(myBodyParser)

使用Express写接口

// app.js
const express = require('express')
const cors = require('cors')
const apiRouter = require('./apiRouter')
const app = express()

// 解决跨域问题:1、CORS中间件(记得安装);2、JSONP(仅支持GET请求)
app.use(cors())
app.use('/api', apiRouter)

app.listen(80, () => {
    console.log('Express server running at localhost')
})

// --------------------分割线----------------------------------
// apiRouter.js [路由模块]
const express = require('express')
const apiRouter = express.Router()

apiRouter.get('/get', (req,res) => {
    const query = req.query
    res.send({
        status: 0,  // 0成功,1失败
        msg: 'GET请求成功!',
        data: query
    })
})

apiRouter.post('post', (req,res) => {
    const body = req.body
    res.send({
        status: 0,
        msg: 'POST请求成功!',
        data: body
    })
})

module.exports = apiRouter

CORS跨域资源共享

Cross-Origin Resource Sharing 由一系列的HTTP响应头组成,这些响应头告诉浏览器不要阻止前端JS代码跨域获取资源(浏览器的同源安全策略会阻止网页跨域访问服务器资源)

  • Access-Control-Allow-Origin<origin> | * –> 指定允许访问该资源的外域URL,正常是通配符*

  • Access-Control-Allow-Header –> 默认CORS支持客户端向服务器发送如下九个请求头:

    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-Methods –> 默认CORS支持客户端向服务器发起GET、POST、HEAD 请求,如果客户端希望发起PUT、DELETE等请求,需要在这里配置

配置方法:

res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,X-Custom-Header')
res.setHeader('Access-Control-Allow-Methods', '*')

注意:Axios 会根据你发送的 data 的类型,智能地设置一个最合适的 Content-Type。虽然 Axios 很智能,但为了代码的清晰和可预测性,显式地设置 headers 是一个好习惯,尤其是在发送 JSON 时。

关于Access-Control-Allow-Headers详细解释:

如上面所言,默认CORS支持客户端向服务器发送如下九个请求头,每个请求头的值仅限于三者之一
如果请求头的值位于三者之外,就需要在Access-Control-Allow-Headers中声明!

比如我们最常用的最常用的是 Content-Typeapplication/json,但是application/json并不是三者之一,所以需要显示在Access-Control-Allow-Headers带上Content-Type,表示该请求头允许任何值!

简单请求和预检请求

客户端在请求CORS接口时,同时满足以下情况为简单请求,否则为非简单请求:

  • 请求方式为GET、POST、HEAD三者之一
  • HTTP头部信息不超过以下字段:无自定义头部字段、AcceptAccept-LanguageContent-LanguageDPRDownlinkSave-DataViewport-WidthWidthContent-Type(且值只能是以下三个application/x-www-formurlencodedmultipart/form-datatext/plain

简单请求:客户端直接发送真实请求

预检请求:

一旦确定为非简单请求,浏览器就在正式请求之前向服务器发送OPTION预检请求,服务器响应时告知浏览器服务器允许的请求方法和请求头。如果响应成功的话浏览器才会发起正式请求!

JSONP接口

浏览器通过<script>标签的src属性,请求服务器上的数据,同时服务器返回一个函数的调用。这种请求数据的方式就是JSONP。其支持跨域请求数据,但是只支持GET请求方式!

前端通过JSONP请求的接口只能是后端声明的JSONP接口,为了避免该接口也被处理CORS接口,后端声明JSONP接口需要在注册CORS之前进行!

// 在CORS中间件注册之前声明JSONP接口
app.get('/api/jsonp', (req,res) => {
    // 1、获取客户端发送过来的回调函数的名字
    const funcName = req.query.callback
    // 2、得到要通过JSONP格式发送给客户端的数据
    const data = { name: 'xs', age: '20' }
    // 3、拼接除一个函数调用的字符串
    const scriptStr = `${funcName}(${JSON.stringify(data)})`
    // 4、响应回客户端
    res.send(scriptStr)
})

// 然后再注册CORS中间件(后续接口都会被处理成CORS接口)
app.use(cors())
// 前端调用
// 1. 定义一个全局函数作为回调函数
function handleJsonpResponse(data) {
    console.log('收到来自JSONP接口的数据:', data);
    alert(`姓名:${data.name},年龄:${data.age}`);
    // 在这里你可以处理数据,例如更新DOM等
}

function getData() {
    // 2. 动态创建script标签
    const script = document.createElement('script');
    // 3. 设置src属性,指定接口地址并传入回调函数名
    script.src = 'http://your-backend.com/api/jsonp?callback=handleJsonpResponse';

    // 4. 将script标签添加到body中,请求开始
    document.body.appendChild(script);

    // 5. (可选)请求完成后移除script标签,避免污染DOM
    script.onload = function() {
        document.body.removeChild(script);
    };
}

四、数据库:MySQL

数据库类型

  • MySQL - 传统型数据库(又叫关系型数据库、SQL数据库),还包括Oracle、SQL Server
  • Mongodb:新型数据库(又叫非关系型数据库、NoSQL数据库)

传统型数据库的组织结构

  • 数据库(database)、数据表(table)、数据行(row)、字段(field)

安装并配置MySQL

  • 安装MySQL Server:专门用来提供数据存储和服务的软件
  • 安装MySQL Workbench:可视化的MySQL管理工具
  • 查看mysql服务是否正在运行
    • Win + R,输入 services.msc 打开“服务”窗口。
    • 在服务列表中找到以 MySQL 开头的服务(如 MySQL80)。查看其“状态”是否为“正在运行”

在项目中操作MySQL

使用MySQL数据库的第三方模块mysql连接MySQL数据库,同时可以通过SQL语句操作数据库

// 先安装:npm install mysql
// 1、导入mysql模块
const mysql = require('mysql')

// 2、建立于MySQL数据库的连接
const db = mysql.createPool({
    host: '127.0.0.1',  // 数据库的IP地址
    user: 'root',  // 数据库账号
    password: 'admin123',  // 数据库密码
    database: 'my_db_01' // 指定要操作的数据库
})

// 3、测试mysql是否可以工作
db.query('SELECT 1', (err,result) => {
    if(err){
        return console.log(err)
    }
    console.log(result) // 只要能打印出[ RowDataPacker {'1':1} ]的结果,说明连接正常
})
// ------------------------------------------------------------------

// 4、查询数据
db.query('SELECT * FROM users', (err,result) => {
    // 查询失败
    if(err) return console.log(err.message)
    // 查询成功
    console.log(result)
})
// ------------------------------------------------------------------

// 5、向user表中插入数据
const user = { username: 'lys', password: 'lys123'}
const sqlStr = 'INSERT INTO users (username,password) VALUES (?,?)'  // ? 表示占位符
// 简化写法:占位符(?,?)替换为? , [user.username,user.password] 替换为user对象
db.query(sqlStr, [user.username,user.password], (err,result) => {
    // 插入失败
    if(err) return console.log(err.message)
    // 插入成功
    if(results.affectedRows === 1){
        console.log('插入数据成功')
    }
})
// ------------------------------------------------------------------

// 6、更新表数据
const user = { id: 1, username: 'lzc', password: '000'}
const sqlStr = 'UPDATE users SET username=?,password=? WHERE id=?'
// 数组数据顺序与SQL语句的?占位符需要一致
db.query(sqlStr, [user.username,user.password,user.id], (err,result) => {
    // 更新失败
    if(err) return console.log(err.message)
    // 更新成功
    if(results.affectedRows === 1){
        console.log('更新数据成功')
    }
})
// ------------------------------------------------------------------

// 7、删除数据
const sqlStr = 'DELETE FROM users WHERE id=?'
db.query(sqlStr, 3, (err,result) => {
    // 删除失败
    if(err) return console.log(err.message)
    // 删除成功
    if(results.affectedRows === 1){
        console.log('删除数据成功')
    }
})

对数据的标记删除

使用DELETE语句会将数据真正的从表中删除。为了保险起见,推荐使用标记删除的形式,来模拟删除的动作!

所谓标记删除,就是在表中设置类似于status这样的状态字段,来标记当前这条数据已经删除!
当用户执行删除动作时,我们并没有执行DELETE语句把数据删除掉,而是执行了UPDATE语句,将这条数据对应的status字段标记为删除即可!

db.query('UPDATE USERS SET status=1 WHERE id=?', 3, (err,result) => {
    // 删除失败
    if(err) return console.log(err.message)
    // 删除成功
    if(results.affectedRows === 1){
        console.log('删除数据成功')
    }
))

五、开发模式:前后端的身份认证

Web开发模式

1、基于服务端渲染的Web开发模式:

服务端发送给客户端的页面,是在服务器通过字符串的拼接、动态生成的。因此客户端不需要使用AJAX这样的技术额外请求页面的数据

app.get('/index.html', (req,res) => {
    // 1、要渲染的数据
    const user = { name: 'xs', age: 20 }
    // 2、服务端通过字符串的拼接动态生成HTML内容
    const html = `<h1>姓名:${user.name},年龄:${user.age}</h1>`
    // 3、把生成好的页面内容响应给客户端。因此,客户端拿到的是带有真实数据的页面
    res.send(html)
})
  • 优点:前端只需要直接渲染页面,耗时少。爬虫更容易获取信息,有利于SEO
  • 缺点:服务端完成HTML页面的内容拼接,占用服务端资源。不利于前后端分离,开发效率低

2、前后端分离的Web开发模式

  • 优点:开发体验好、用户体验好(Ajax技术实现页面局部刷新)、减小服务器压力
  • 缺点:不利于SEO。完整的页面在客户端动态生成没所以爬虫无法爬取页面的有效信息(解决方案:利用Vue、React等前端框架的SSR(server side render)技术解决SEO问题)

身份认证

不同的认证方案

  • 服务端渲染推荐使用Session认证机制
  • 前后端分离推荐使用JWT认证机制

Session认证机制

1、HTTP协议的无状态性

HTTP协议的无状态性指的是客户端每次的HTTP请求都是独立的,连续多次请求之间都没有直接的关系,服务器不会主动保留每次HTTP请求的状态,为了突破HTTP协议无状态的限制,推荐使用Cookie存储用户信息

2、什么是Cookie

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

不同域名下的Cookie各自独立,每当客户端发起请求时,会自动把当前域名下的所有未过期的Cookie一同发送到服务器

Cookie的几大特性

  • 自动发送
  • 不同域名的Cookie各自独立
  • 存在过期时限
  • 4KB限制

3、Cookie在身份认证的作用

客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的Cookie,客户端会自动将Cookie保存在浏览器当中。

随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端身份!

4、Cookie具有不安全性

Cookie存储在浏览器中,并且浏览器提供了读写Cookie的API,因此Cookie容易被伪造,不具有安全性。因此不建议将隐私数据存储在Cookie中

5、Session的工作原理
https://i-blog.csdnimg.cn/direct/1d11a4ebeb254be990b74051ae6bba85.png

6、在Express使用Session认证

// 安装中间件:npm install express-session
const session = require('express-session')

// 注册中间件
app.use(session({
    secret: 'Keyboard cat', // secret属性的值可以是任意字符串
    resave: false,  // 固定写法
    saveUninitialized: trye  // 固定写法
}))

// 登录接口(访问session对象,存储信息)
app.post('/api/login', (req,res) => {
    if(req.body.username !== 'admin' || req.body.password !== '000000'){
        resturn res.send({ status: 1, msg: '登陆失败!' })
    }
    
    req.session.user = req.body   // 将用户信息存储到session中
    res.session.islogin = true    // 将用户的登录状态存储到session中
    
    res.send({ status: 0, msg: '登陆成功!' })
})

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

// 退出登录接口(清空session)
app.post('/api/logout', (req,res) => {
    res.session.destory()  // 清空session
    res.send({
        status: 0,
        msg: '退出登录成功!'
    })
})

JWT认证机制

Session认证机制需要配置Cookie实现,而由于Cookie默认不支持跨域访问。所以当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现Session认证!

JWT(JSON Web Token)是目前最流行的跨域认证解决方案!

1、JWT的工作原理
https://i-blog.csdnimg.cn/direct/161566350427474e880e0b152c3fd072.png

2、JWT的组成部分

JWT通常由三部分组成,分别是头部Header、有效载荷Payload、签名Signature。三者之间通过“ . ”隔开,格式如下:

Header.Payload.Signature

3、JWT的三个部分各自代表的含义

  • Payload部分是真正的用户信息,它是用户信息经过加密之后生成的字符串
  • Header和Signature是安全性相关的部分,只是为了保证Token的安全性

4、JWT的使用方式

客户端收到JWT之后,通常会将它存储在localStorage或者sessionStorage中。

此后,客户端每次与服务端通信,都要带上这个JWT的字符串,从而进行身份验证,推荐的做法是把JWT放在HTTP请求头的Authorization字符中,格式如下:

Authorization: Bearer <token>

5、在Express使用JWT

// 安装包 npm install jsonwebtoken express-jwt
// jsonwebtoken   用于生成JWT字符串
// express-jwt    用于将JWT字符串解析还原成JSON对象
// 1、导入
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')

// 2、定义一个用于加密和解密的密钥secret(本质是一个字符串)
const srcretKey = 'shunhahaha NO1 ^_^'

// 3、登陆成功之后加密
app.post('/api/login', (req,res) => {
    // ...
    // 登陆成功时响应
    res.send({ 
        status: 0, 
        msg: '登陆成功!',
        // sign方法生成jwt字符串,三个参数分别是:信息对象、加密密钥、配置对象
        token: jwt.sign({username: userInfo.username}, secretKey, { expire: '30s'})
    })
})

// 4、调用有权限的接口时会自动解密
// 使用app.use()来注册中间件
// expressJWT({ secret: secretKey }) 就是用来解析Token的中间件
// .unless({ path:[/^\/api\//] }) 用来指定哪些接口不需要访问权限
app.use(expressJWT({ secret: secretKey }).unless({ path:[/^\/api\//] }))

// 5、使用req.user获取用户信息
// 当配置express-jwt中间件之后,就可以在需要权限的接口通过req.user访问从JWT字符串中解析的信息了
app.get('/admin/getinfo', (req,res) => {
    // ...
    res.send({
        status: 0,
        msg: '获取用户信息成功!',
        date: res.user
    })
})

// 6、捕获JWT解析失败的错误
// 当使用express-jwt解析Token时,如果Toeken不合法或者已经过期,会产生一个失败的错误
// 可以通过Express的错误中间件捕获错误并进行处理
app.use((err,req,res,next) => {
    // token解析失败错误
    if(err.name === 'UnauthorizationError'){
        return res.send({ status: 401, message: 'token无效!'})
    }
    
    // 其他错误
    res.send({ status: 500, message: '未知错误!' })
})