浅谈nodejs基础(文件读写+express+mysql+身份验证等)

342 阅读30分钟

Node笔记

一、文件的操作

1、读取/写入 文件

1.1 导入文件

const fs = require('fs')

1.2 读取文件

参数1:文件路径(必选) 参数2:编码(可选) 参数3:回调函数(err=错误信息, dataStr=读取成功时文件的内容)(必选)

fs.readFile('/1.txt', 'utf8', function(err, dataStr) {})

1.3 写入文件

参数1:写入文件的路径(必选); 参数2:写入的内容(必选); 参数3:写入的格式(可选); 参数4:回调函数

  • fs.writeFile()只能可以创建文件,但不能创建路径,也就是如果路径中存在不存在的文件夹路径则会报错。
  • fs.writeFile()会将原有的文件内容覆盖。
fs.writeFile('1.txt', '我是要写入的内容', 'utf8', (err) => {}) // 该方法会覆盖文件的原有内容

2、动态路径

__dirname 表示当前目录的绝对路径。

3、拼接路径 - join

3.1 导入模块

const path = require('path') // 涉及到与路径相关的操作时导入
const fs = require('fs')    // 涉及到与文件相关的操作时导入

3.2 拼接

fs.readFile(path.join(__dirname, '/file/1.txt'), 'utf8', function(err, dataStr) {})

4、获取路径中的文件名

path.basename(path[,ext]) ​ 参数1:文件路径(必选);参数2:文件后缀名(可选)

const fpath = 'a/b/c/index.html'
var fullName = path.basename(fpath)
console.log(fullName) // 输出 index.htmlvar name = path.basename(fpath, '.html')
console.log(name) // 输出index

5、获取文件扩展名

path.extname(path) ​ 参数1:文件的路径(必选);

const fpath = 'a/b/c/index.html'
let name = path.extname(fpath)
console.log(name)  // 输出.html

二、http模块

1、基本概念

  • IP:127.0.0.1可以访问本台计算机提供的服务,其域名是localHost,默认端口是80。
  • 若端口是80则可以省略,其他都不行。
  • 每个端口只能提供一个webSeve。
  • IP相当于地址,端口号相当于门牌号。

2、创建服务器并提供服务

2.1 基本步骤

  1. 导入http模块。

    const http = require("http")

  2. 创建web服务器实例。 const server = http.createServer()

  3. 为服务器绑定request事件,监听客户端的请求。 server.on("request", (req, res) => {}) req:请求对象,包含了与客户端相关的数据和属性。例如:req.url表示请求的路径(端口号后面的路径,如index.html)、req.method表示请求的方式(get、post等)。 res:响应对象,包括与服务器相关的数据和属性。例如:res.end(data)表示向客户端发送data,并结束这次请求的处理过程。

  4. 启动服务器。 server.listen(80, () => {}) 参数1:要启动服务器的端口,即该服务器要在哪个端口运行。 参数2:启动成功后的回调。

2.2 解决中文乱码问题

若在server.on请求里面通过res.end()发送中文字符给客户端则会显示乱码。 ​ 因此,可以在给客户端返回信息之前可以设置响应头的格式。如下:

server.on("request", (req, res) => {
    //返回的格式为html,且编码方式为utf-8
    res.setHeader('Content-Type', 'text/html; charset=utf-8') 
    res.end("我是中文字符")
})

三、node中的模块化

1、基本知识

  • 模块分为三大类:内置模块、自定义模块(例如js文件,可以省略.js后缀名)、第三方模块(也称为包,是基于内置模块封装出来的)。

  • 使用模块时用require导入,导入时会自动执行对应模块中的代码。

  • 模块作用域:默认情况下,模块中的成员无法被其他模块所访问。(防止了全局变量污染的问题即变量名冲突)

  • 每个模块中都含有module这个对象,里面存储了与当前模块有关的信息。

  • 在模块中可以使用module.exports向外暴露(提供)属性或方法。(为了化简这种写法,node提供了一个exports对象,该对象默认与module.exports指向同一个对象。最终用require获取到的值以module.exports为准。)

    // 方法1:
    module.exports.name = 'fyf' // 等同于exports.name = 'fyf'
    
    // 方法2:
    const name = 'fyf'
    module.exports = {
    	name: name
    }
    

2、文件的认识

  • 当安装完一个包后,项目中会多出node_modules文件夹和package-lock.json的配置文件。
  • node_modeles文件中存放的是所有已安装的包,在项目中require包时都是从这里去查找。
  • package-lock.json存放的是每个包的详细记录,包括包的名字、版本号、下载地址等。
  • 注意!不要修改node_modules文件夹和package-lock.json中的任何代码,npm包管理工具会自动维护它们。

3、版本号识别(以3.15.8为例)

  • 第一个数字:大版本,从底层开始大幅度的修改重构代码以符合当前开发环境。
  • 第二个数字:功能版本,进行一些功能函数的添加等。
  • 第三个数字:bug版本,没有添加新功能,只是修复了一些bug。
  • 只要前面的版本号增加了,后面的数字都要归0。

4、项目运行及安装包

  • npm规定在项目的根目录需要有一个package.json文件,该文件包含了项目的基本信息,记录了项目中使用了哪些包及包的版本号、描述信息。
  • 在导入一个项目时,可以使用 npm init -y 来快速创建一个 package.json 文件。(-y是yes的缩写,表示需要手动配置的选项自动选择默认配置)
  • 注意!上述命令只能在英文路径下才能运行成功。因此在开发项目时不要使用中文和空格路径。
  • 建立完package.json文件后,可以使用npm install来安装项目中所依赖的包,它会根据package.json的包记录信息来安装对应版本的包。
  • 如果一个包在开发时需要用到而项目上线后不会用到则可以在安装包时在后面加上--save-dev(也可以简写-D),这样这个包的信息就会被记录到package.json文件的devDependencies中,否则会记录到dependencies中。
  • 可以在包的文档中查看下载指令以悉知该包是否在项目上线后需要用到。

5、npm安装包速度慢的原因及解决

  • 原因:因为npm下载时通过国外的服务器来下载,从国内访问国外服务器需要通过海底光缆,数据的传输需要经过漫长的海底光缆从而导致速度慢。

  • 解决:通过淘宝镜像进行下载。淘宝在国内搭建了一个服务器,专门把国外官方服务器的包每隔一段时间同步到国内的服务器,然后在国内提供下包服务,从而提高了下载速度。(所谓镜像,就是一个磁盘上的数据在另一个磁盘上存在一个完全相同的副本,该副本即称为镜像)。解决步骤如下:

    1. 在终端中使用以下命令查看下包的服务器地址。 npm config get registry
    2. 将下包的服务器地址更改为淘宝镜像源。 npm config set registry=https://registry.npm.taobao.org/
  • 安装nrm来管理下包的镜像源。

    npm install nrm -g // 在全局安装nrm
    
    // 使用nrm查看所有可用的镜像源
    nrm ls
    
    // 切换镜像源为淘宝镜像
    nrm use taobao
    

6、开发自己的包

  1. 新建一个文件夹,里面有三个文件,分别是包的入口文件index.js,包的使用说明:README.md,记录包的信息:package.json。

  2. package.json说明包的相关信息:

    // package.json
    {
    	"name": "fyf-package",  // 包的名称,要注意不能与npm上的包名称雷同,否则会发布失败
    	"version": "1.0.0",	// 包的版本
    	"main": "index.js",	// 包的入口文件
    	"description": "提供了格式化时间等功能", // 包的描述信息
    	"keywords": ["fyf", "dateFormat"], // 搜索包的关键词
    	"license": "ISC" // 开源协议
    }
    
  3. package.json 文件中 main 属性的作用,如果导入一个包时,没有指定具体的文件名,则 node 会自动查找 package.json 文件中是否存在main属性,若存在则将他作为导入的文件。

  4. 编写README.md 文件的格式:

    • 安装方式
    • 代码功能示例及注释
    • 开源协议
    ##安装方式```js
    npm install fyf-package
    ​```
    ##格式化时间```
    ...相关代码及注释
    ​```
    ##开源协议
    ISC
    

7、发布和删除包

7.1 发布包

  • 先把镜像源切换为官方服务器。
  • 在终端输入 npm login 来登录 npm 账号。
  • 进入到包所在的目录。
  • 在终端输入 npm publish 进行发布。

7.2 删除包

  • 在终端输入 npm unpublish 包名 --force,即可从npm上删除对应的包。 注意!通过这个命令只能删除72小时内发布的包。 通过这个命令删除的包在24小时内不能再次发布相同的包。

8、模块的缓存机制

  • 模块默认会从缓存中进行加载。
  • 如果多次用require去调用同一个包,只有第一次会执行模块中的代码。之后模块会被缓存,因此后面都从缓存中加载,因而不会再次执行模块中的代码。

9、模块的加载机制

  • 内置模块的加载优先级是最高的。例如:在加载内置模块fs时,同时存在node_modules文件夹中也含有fs第三方模块,此时会加载内置模块而不是第三方模块。

  • 在加载自定义模块时,应在路径中添加./或者../路径,否则node会把它当作内置模块或第三方模块来加载。

  • 若在加载模块时省略了扩展名,node会按以下顺序对文件名进行补全并加载:

    1. 按照文件名确切加载。
    2. 补全.js扩展名进行加载。
    3. 补全.json扩展名进行加载。
    4. 补全.node扩展名进行加载。
    5. 加载失败,终端报错。
  • 第三方模块的加载机制:如果require()的标识符不是一个内置模块,也没有以'./'或'../'开头,则node.js会从当前模块的父目录开始,从/node_modules文件夹中加载第三方模块。若没有找到,则再移动到上一层目录中查找,直到文件系统的根目录。例如,在 "C:\User\project\foo.js" 文件里调用了require("tools"),则node会按以下顺序进行查找。

    1. C:\User\project**node_modules**\tools
    2. C:\User**node_modules**\tools
    3. C:**node_modules**\tools
    4. 报错
  • 目录作为模块的标识符传递给require()进行加载时,会有以下加载方式:

    1. 在被加载的目录中查找package.json的文件,并寻找main属性,作为require()加载的入口。
    2. 如果目录里没有package.json文件,或main属性不存在或无法解析,则node会试图加载目录下的index.js。
    3. 若以上两种方式都失败,则报错。

四、Express

Express 的作用和 node 内置的 http 模块类似,是专门用来创建Web服务器的。 ​ Express 本质是 npm 上的第三方包,提供了比http更加便捷的方法。Express 是基于http模块封装出来的。 ​ Express 可以用来创建 web 网站的服务器和 api 接口服务器。

1、初识Express

1.1 安装

npm install express

1.2 创建web服务器

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

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

// 3、监听客户端请求
// 3.1 监听get请求
// 参数1:要监听的路径
// 参数2:处理函数(参数1:请求对象;参数2:响应对象)
app.get('/user', (req, res) => {
  // 向客户端发送内容
  res.send({
    name: 'fyf',
    age: 20,
    sex: '男'
  })
})

// 3.2 监听post请求
app.post('/user', (req, res) => {
  res.send('请求成功')
})

// 4、启动web服务器
app.listen(80, () => {
  console.log('启动express服务器成功!');
})

1.3 获取 url 中传递的参数

// 可以在监听请求的回调函数中,通过 req.query 来获取通过 url 地址传递的参数(query传参)。
// 例如,若客户端传递过来 ?name=fyf&age=20,则:
app.get('/', (req, res) => {
	console.log(req.query)  // 输出{name: 'fyf', age: '20'}
})

// 还可以通过 req.params 来获取动态参数(动态路由传参)
// 例如,若客户端传递过来 /:1254
app.get('/user/:id', (req, res) => {
	console.log(req.params)  // 输出{id: '1254'}
})

1.4 托管静态资源

可通过express.static()来托管静态资源。例如,若public文件夹下有三个文件(index.js、index.html、index.css)

// 进行静态资源共享
app.use(express.static(./public)) // 注意,public不会出现在路径中
// 客户端可通过 '域名/index.html' 即可访问到 public 文件夹下的 index.html 文件

// 若要让public也出现在路径中,则要添加第一个参数(注意,该参数可以随意命名,建议与共享目录同名)。
// 情况1:
app.use('/public', express.static(./public))
// 客户端可通过 '域名/public/index.html' 即可访问到 public 文件夹下的 index.html 文件

// 情况2:
app.use('/abc', express.static(./public))
// 客户端可通过 '域名/abc/index.html' 即可访问到 public 文件夹下的 index.html 文件

若要托管多个静态资源目录,则可多次调用 app.use(express.static(目录)) 来实现。注意:若不同目录中存在相同的文件名,则会按照添加托管的顺序优先查找对应的文件。如果采用两个参数的写法,则可以避免该问题产生。

1.5 nodemon工具(全局安装)

在修改完node代码后,需要手动重新启动服务器。安装nodemon后,使用nodemon命令来启动项目,这样修改完代码后会自动帮助我们重启服务器。

安装方式:npm install nodemon -g ​ 启动方式:nodemon 项目路径

2、Express 路由

2.1 路由的概念

路由由三部分组成:请求的类型、请求的url地址、处理函数。

// 添加一个路由监听get请求
app.get('/', (req, res) => {...})

// 添加一个路由监听post请求
app.post('/', (req, res) => {...})

2.2 路由的匹配规则

路由会按照定义路由的先后顺序来进行匹配。只有请求类型 和 请求的url地址相匹配才会调用对应的回调。例如,若客户端使用get方式请求/user路径,并有如下路由,则:

app.post('/', (req, res) => {...})
app.get('/home', (req, res) => {...})
app.post('/user', (req, res) => {...})
app.get('/user', (req, res) => {...}) // 匹配成功
app.get('/user', (req, res) => {...})

2.3 路由模块化

Express不建议我们直接将路由挂载到app上,而是建议我们将路由划分成许多模块,再挂载到app上。

步骤

  1. 创建对应的路由.js文件。
  2. 调用 express.Route() 来创建一个路由对象。
  3. 添加要挂载的具体路由。
  4. 导出路由对象。
  5. 使用 app.use() 方法将路由挂载到app上。 app.use() 的作用就是用来注册全局中间件。全局中间件的作用是:每次请求都会执行该中间件的函数。

2.4 Express的中间件

中间件本质就是一个处理函数。若函数的最后一个参数中是 next 参数,则该函数为中间件。例如:

const mw = (req, res, next) => {
	next()
}

next() 函数是实现多个中间件连续调用的关键,他表示把函数执行转交给下一个中间件或路由。因此当此次中间件处理完业务后,必须调用 next() 将处理权限转交给下一个中间件或路由(如果后面还有中间件则转交给中间件,否则转交给路由)。多个中间件的执行顺序是按照定义中间件的先后顺序执行的。 ​ 多个中间件之间共享同一份 req 和 res 对象。因此,在前面的中间件中为 req 或 res 添加属性或方法可以被后面的中间件中获取。例如:在到达每个页面时,都要显示实时时间,则可通过中间件来简化代码。

// 注册全局中间件
app.use((req, res, next) => {
	// 在中间件中给 req 赋值一个日期变量,则就可以在后续的中间件或路由中来调用,节省了代码。
	req.startTime = new Date()
	next()
})

app.get('/home', (req, res) => {
	res.send(req.startTime)
})

app.get('/user', (req, res) => {
	res.send(req.startTime)
})
2.4.1 全局中间件与局部中间件

使用 app.use() 注册的中间件称为全局中间件,反之称为局部中间件。

// 全局中间件
app.use(mw)

// 局部中间件
app.get('/user', mw, (req, res) => {...}) // mw 只在请求 /user 地址时才会生效。

// 定义多个局部中间件(执行顺序按照传入参数的顺序)
app.get('/user', mw1, mw2, mw3, (req, res) => {...})
// 等价于
app.get('/user', [mw1, mw2, mw3], (req, res) => {...})
2.4.2 中间件的注意事项
  • 中间件不能在注册路由之后注册,否则不生效。(错误级别中间件除外)
  • 调用了 next() 之后,就不要在 next() 后面写其他代码,以防代码逻辑混乱。
  • 客户端发送过来的请求,可以使用多个中间件进行处理。
  • 执行完中间件的函数之后,不要忘记调用next()。
  • 多个中间件之间共享同一份 req 和 res 对象。
2.4.3 中间件的分类
  • 应用级别中间件(绑定到 app 实例上的中间件)

    app.use(mw) app.get('/user', mw, (req, res) => {...})

  • 路由级别中间件(绑定到 router 实例上的中间件)

    router.use((req, res, next) => {...})

  • 错误级别中间件(必须有4个形参)。错误级别中间件的用途在于捕获错误,防止程序崩溃。假如在错误级别中间件之前有中间件在处理过程中发生了错误,就会直接将执行权限移交给错误级别中间件进行处理,从而防止了程序崩溃。错误级别中间件定义如下: app.use((err, req, res, next) => {...}) 注意,错误级别中间件必须放在所有路由之后,否则可能不能正常工作。

  • 内置中间件。

  • 第三方中间件。

2.4.4 Express内置中间件
  • express.static() 用来托管静态资源。(无兼容,任意版本可用)
  • express.json() 用来解析JSON格式的请求体数据。(4.16.0+ 版本可用)
  • express.urlencoded() 用来解析 URL-encoded 格式的请求体数据。(4.16.0+ 版本可用)
// 配置解析 application.json 格式数据的内置中间件
app.use(express.json())

// 配置解析 application/x-www-form-urlencoded 格式数据的内置中间件
app.use(express.urlencoded({ extended: false }))

---------------------------------------------------------------------------
// 若客户端以 json 格式向服务器发送数据,则服务器可以这样拿到数据
// 配置解析表单数据的中间件
app.use(express.json())

app.post('/user', (req, res) => {
  // 若不配置解析表单的中间件,则 req.body 默认为undefined
  console.log(req.body)
  res.send(req.body)
})
2.4.5 自定义中间件

可以自定义一个中间件来实现 express.urlencoded({ extended: false } 的功能。如下:

// 导入 node 内置模块,该模块提供了 parse 函数可以将字符串解析成对象形式且可以将汉字转码。
const qs = require('querystring')

function mw = (req, res, next) => {
	// 创建一个字符串来接收客户端发送的数据
	let str = ''
	// 如果接收到客户端发送过来的数据,会自动调用 req 的 data 事件
	// 如客户端发送的数据过大,客户端会将数据拆分成多个数据块发送,等全部发送完会响应 req 的 end 事件。
	req.on('data', (chunk) => {
		str += chunk
	})
	// 若响应了该事件则表明数据已经接收完毕
	req.on('end', () => {
		const body = qs.parse(str)
		req.body = body
		next()
	})
}

3、编写 get 和 post 接口

// user-api.js
const express = require('express')
const router = express.Router()

// get 接口
router.get('/get', (req, res) => {
  res.send({
    status: 0, // 0表示成功,1表示失败
    msg: 'get 请求成功',
    data: req.body
  })
})

// post 接口
router.post('/post', (req, res) => {
  res.send({
    status: 0, // 0表示成功,1表示失败
    msg: 'post 请求成功',
    data: req.body
  })
})

module.exports = router

----------------------------------------------------------
const express = require('express')
const router = require('./api/user-api')

const app = express()
// 使用解析表单中间件
app.use(express.urlencoded({extended: false}))
// 使用路由
app.use('/api', router)
// 开启服务器
app.listen(5656, () => {
  console.log('启动服务器成功!')
})

4、跨域问题

以上编写的 get 和 post 接口存在跨域问题,若要解决跨域有以下两种方案:

  • CORS (主流的解决方案,推荐使用)
  • JSONP (有缺陷的解决方案:只支持 get 请求)

4.1 使用 CORS 解决跨域问题

安装cors ​ npm install cors ​ 注意,CORS有浏览器兼容问题,只有在(IE10+、Chrome4+、FireFox3.5+)才能正常使用。

使用方式:

const cors = require('cors')
// 必须在使用路由前配置中间件
app.use(cors()) // 要注意cors带括号!
// 配置路由
...
// 启动服务器
...
4.1.1 响应头
  • Access-Control-Allow-Origin 用来设置允许来自哪些域的请求。

    // 允许任何域请求
    res.setHeader('Access-Control-Allow-Origin', '*')
    // 只允许来自 https://ifyf.top 的请求
    res.setHeader('Access-Control-Allow-Origin', 'https://ifyf.top')
    
  • 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三者之一) 如果客户端发送了这9个之外的请求头,则需要服务端通过 Access-Control-Allow-Headers 配置额外的请求头,否则这次请求会失败。

    // 允许服务端额外向服务器发送 Content-Type 和 X-custom-Header 请求头
    // 多个请求头直接用逗号分割
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-custom-Header')
    
  • Access-Control-Allow-Methods 用来额外添加允许请求的方式。 默认情况下,CORS只支持客户端发起的get、post、head请求,若需要支持其他请求方式则需额外配置,否则会请求失败。

    // 支持所有请求
    res.setHeader('Access-Control-Allow-Methods', '*')
    // 额外支持put请求
    res.setHeader('Access-Control-Allow-Methods', 'PUT')
    
4.1.2 请求分类
(1) 简单请求

请求的方式和请求所携带的请求头是 CORS 中默认支持的格式,则此次请求为简单请求。

(2) 预检请求
  • 请求方式不是CORS默认支持的方式。

  • 请求头不是CORS默认支持的格式。

  • 向服务器发送了 application/json 格式的数据。

    总结:简单请求的对立面即是预检请求。

  • 预检请求过程:在浏览器和服务器正式通信之前,浏览器会向服务器先发送 option 请求进行预检,以获知服务器是否允许该实际请求,这一次的option请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实的数据。

(3) 简单请求和预检请求的区别
  • 简单请求:客户端与服务器之间只会发生一次请求。
  • 预检请求:客户端与服务器之间会发生两次请求,option预检请求成功后才会发起真正的请求。

4.2 使用 JSONP 解决跨域问题

使用

  • JSONP 不属于真正的 ajax 请求,因为它没有使用 XMLHttpRequest 这个对象。
  • JSONP 仅支持 get 请求。

注意:在配置 JSONP 接口时,同时配置了CORS中间件,则必须把 JSONP 接口写在 CORS 中间件之前。

// 必须把 JSONP 接口写在 CORS 中间件之前
app.get('/api/jsonp', (req, res) => {...})

// 配置 cors 中间件
app.use(cors())

JSONP 实现接口步骤

  1. 获取客户端发送过来的回调函数的名字。
  2. 定义通过 JSONP 形式发送给客户端的数据。
  3. 根据前两步得到的数据,拼接出一个函数调用的字符串。
  4. 把上一步拼接得到的字符串,响应给客户端的
app.get('/api/jsonp', (req, res) => {
  // 1. 获取客户端发送过来的回调函数的名字
  const funcName = req.query.callback
  // 2. 定义通过 JSONP 形式发送给客户端的数据。
  const data = {name: 'fyf', age: 18}
  // 3. 拼接出一个函数调用的字符串。
  const str = `${funcName}(${JSON.stringify(data)})`
  // 4. 响应给客户端的 <script> 标签进行解析执行。
  res.send(str)
})

五、数据库

5.1 常见数据库

  • MySQL 数据库(目前使用最广泛、流行度最高的开源免费数据库;Community + Enterprise,前者为社区免费版,后者为收费版)
  • Oracle 数据库(收费)
  • SQL Server 数据库(收费)
  • Mongodb 数据库(Comunity + Enterprise)

其中,MySQL 、Oracle 、SQL Server 属于传统型数据库(又叫做:关系型数据库 或 SQL 数据库),这三者设计理念相同,用法类似。 Mongodb 数据库属于新型数据库(又叫做:非关系型数据库 或 NoSQL 数据库),它在一定程度上弥补了传统型数据库的缺陷。

5.2 传统数据库的数据组织结构

  • 数据库(包含多个数据表)
  • 数据表(存有多个数据行)
  • 数据行(拥有多个数据列)
  • 字段(也就是列名),每个字段都有对应的数据类型。

5.3 安装并配置MySQL

对于开发人员来说,只需要安装 MySQL Server 和 MySQL Workbench 这两个软件。

  • MySQL Server: 专门用来提供数据存储和服务的软件。
  • MySQL Workbench: 可视化的 MySQL 管理工具,通过它,可以方便的操作存储在 MySQL Server 中的数据。

5.4 数据表里面的字段的复选框

  • PK:Primary Key 主键,唯一标识。
  • NN:Not Null 值不允许为空。
  • UQ:Unique 值唯一,不允许出现两个相同的值。
  • AI:Auto Increment 值自动增长。(一般用于ID)

5.5 SQL语句

SQL 语句中的关键字(select、from等)对大小写不敏感,也就是大写和小写是一样的效果。 ​ SQL 语句中的注释使用 --

-- 查询表中的内容,格式: SELECT 列名 FROM 表名
-- 查询 users 表中的用户名
SELECT username from users
-- 查询 users 表中的用户名和密码
SELECT username, password from users

-- 向表中插入数据,格式: INSERT INTO 表名(列名) VALUES (值)
-- 向 users 表中插入 username 和 password,值分别为 coderwhy 和 666666
INSERT INTO users(username, password) values ('coderwhy', '666666')
-- 等同于 INSERT INTO users SET username='coderwhy', password='666666'

-- 更新/修改表中的内容,格式: UPDATE 表名 SET 列名=新值 WHERE 条件
-- 将 users 表中 id 为 4 的用户密码改为123321
UPDATE users SET password='123321' WHERE id=4
-- 将 users 表中 id 为 3 的用户密码改为654321,状态改为1
UPDATE users SET password='654321', status=1 WHERE id=3

-- 删除表中的内容,格式: DELETE FROM 表名 WHERE 条件   
注意,若不写条件则会把整张表给删除!!!
-- 删除 users 表中,username 为 lck 的信息。
DELETE FROM users WHERE username='lck'

5.6 SQL中的运算符及子句

  • where 字句用来限定条件。

  • and 表示两个条件同时满足,相当于与运算。

  • or 表示两个条件满足其一即可,相当于或运算。

  • ORDER BY 语句用于根据指定的列对结果集进行排序,默认是升序排序(ASC,可省略),可以使用 DESC 来实现降序排序 。

    -- 表示查询 users 表中的所有内容,并按照 status 字段进行升序排序。
    SELECT * FROM users ORDER BY status
    -- 等同于
    SELECT * FROM users ORDER BY status ASC
    
    -- 查询 users 表中的所有内容,并先按照 status 进行降序排序,再按照 username 的字母顺序进行升序排序
    SELECT * FROM users order by status DESC, username ASC
    
  • count() 表示计算满足某个条件的数量,列名默认为count()

    -- 计算 users 表中 status 为 0 的用户数量
    SELECT COUNT(*) from users WHERE status=0
    -- 计算 users 表中有多少条信息
    SELECT COUNT(*) from users 
    
  • AS 可以为筛选出来的列设置别名

    -- 将筛查选出来的数量的列名设置为 total
    SELECT COUNT(*) AS total from users 
    

5.7 在项目中运用 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: '01_my_db'  // 指定要操作的数据库
})
// 3、检测是否正常连接上数据库,测试完之后若正常就可以注释掉了。
db.query('select 1', (err, results) => {
  if (err) {
    return console.log('连接数据库失败', err.message)
  }
  console.log(results) // 若打印出[ RowDataPacket { '1': 1 } ] 则表示连接正常
})
// 4、执行 mysql 语句,例如:查询 users 表中的数据
const sql = 'select * from users'
db.query(sql, (err, results) => {
    if (err) ...
    console.log(results) // 如果是传入的 sql 语句是 select 语句,则得到的 results 是一个数组。
})

// 往 users 表中添加数据 方式一:
// 1. 先定义要插入的数据
const user = {
    username: 'von',
    password: '363300'
}
// 2. 定义SQL语句,不确定的值可以先用 问号 来占位
const sqlStr = 'INSERT INTO users(username, password) VALUES (?,?)'
// 插入语句,传入的第二个参数为一个数组时,会按照顺序填充第一个参数里面出现的问号。
db.query(sqlStr, [user.username, user.password], (err, res) => {
    if (err) ...
    if(res.affectedRows === 1) { // 验证数据是否插入成功(实质是判断是否影响了表中行的数据)
        console.log('插入数据成功')
    }
})

// 方式二:简便写法(前提是对象中的属性要与数据表中的字段一一对应)
const user = {
  username: '李四',
  password: '256256'
}
const sqlStr = 'INSERT INTO users SET ?'
db.query(sqlStr, user, (err, res) => {
  if (err) {
    return console.log('插入数据失败', err.message)
  }
  if (res.affectedRows === 1) {
    console.log('插入数据成功')
  }
})

// 更新数据同理,这里只举例渐变写法(更新 users 表中 id 为 3 的数据)
const user = {
  id: 3,
  username: '王五',
  password: '789789'
}
const sqlStr = 'UPDATE users SET ? WHERE id=?'
db.query(sqlStr, [user, user.id], (err, res) => {
  if (err) {
    return console.log('更新数据失败', err.message)
  }
  if (res.affectedRows === 1) {
    console.log('更新数据成功')
  }
})

注意事项

  • 标记删除法:为了保险起见,对于删除数据的操作我们一般采用在数据表中用一个字段来表示该条数据的状态(status),当用户执行删除数据时,实际上是执行了更新语句,将状态设置为删除状态即可。这样就可以避免用户误删数据,方便数据恢复。

六、Web 开发模式

6.1 开发模式分类

  • 服务端渲染(通过字符串拼接的方式由服务器直接返回整个页面)

    优点:

    1. 前端耗时少。因为浏览器只负责渲染页面即可,无需发送网络请求来获取数据。
    2. 有利于SEO。因为服务端响应的是完整的 HTML 页面,更有利于爬虫爬取信息。

    缺点:

    1. 占用服务器资源。
    2. 不利于前后端分离,开发效率低。
  • 客户端渲染

    优点:

    1. 开发体验好。前端专注 UI 页面的开发,后端专注 api 的开发。
    2. 用户体验好。可以轻松实现网页的局部刷新。
    3. 减轻了服务器端的渲染

    缺点:

    1. 不利于SEO。解决:利用Vue、React 等前端框架的 SSR 技术能够很好的解决SEO问题。

6.2 应用场景

  • 企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的SEO,就可以使用服务端渲染。
  • 类似后台管理系统,交互性比较强,不需要考虑SEO,那么就可以使用客户端渲染(前后端分离模式)。
  • 另外,还有一些网站为了兼顾以上两种,采用了首屏服务端渲染 + 其他页面前后端分离的开发模式。

6.3 Session 身份认证

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

6.3.1 Session 认证机制

HTTP 协议的无状态性:是指客户端的每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态。 ​ 解决 HTTP 的无状态限制:Cookie。客户端在第一次向服务器发送请求的时候,服务器会通过响应头的形式,向客户端发送一个身份认证的 Cookie ,客户端会自动将 Cookie 保存在浏览器中。以后客户端每次请求服务器的时候,都会把与身份认证相关的 Cookie 通过请求头的形式发送给服务器,以让服务器验证身份。

什么是 Cookie? ​ Cookie 是存储在用户浏览器中一段不超过 4KB 的字符串。它由一个名称、一个值和其他几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成。 ​ 每个域名都有自己独立的 Cookie,不同域名下的 Cookie 不能互相访问。 ​ 客户端在每次访问服务器时,会自动把当前域名的未过期的 Cookie 一同发送给服务器,以让服务器识别身份。

Cookie 的安全性? ​ Cookie 不具有安全性。Cookie 是存储在浏览器中的,而浏览器也提供了存储 Cookie 的API,因此 Cookie 很容易被为伪造。因此不建议服务器将重要的隐私数据(如:密码)通过 Cookie 发送给浏览器。

6.3.2 Session 的工作原理

image-20220918002209780.png

6.3.3 在项目中使用 Session

安装 ​ npm install express-session

使用

// 1、导入模块
const session = require('express-session')

// 2、使用并配置 session 中间件
app.use(express({
    secret: 'keyboard cat', // secret 属性的值可以为任意字符串
    resave: false,  // 固定写法
    saveUninitialized: true // 固定写法
}))
// 在配置完 session 后,就可以使用 req.session 来访问和使用 session 对象,在配置完 session 之前 req 没有这个属性。
// 存储登录的用户信息
app.post('/api/login', (req, res) => {
   	if (req.body.username === 'admin' && req.body.password === '666666') {
        req.session.userInfo = req.body // 记录用户信息
        req.session.isLogin = true   // 记录登陆状态
        res.send({status: 0, msg: '登陆成功'})
    } else {
        res.send({status: 1, msg: '登陆失败'})
    }
})
// 清空 session 中的数据
app.post('/api/logout', (req, res) => {
  req.session.destory()   // 清空当前客户端 session 中的数据(只会清空当前用户)
  res.send({status: 0, msg: '退出登录成功'})
})

6.3.4 Session 认证的局限性

  • 需要配合 Cookie 才能实现。
  • 默认不支持跨域访问,当涉及到前端跨域访问后端接口时,需要提供很多的额外配置,才能实现跨域 session 认证。

6.4 JWT 身份认证

当前端请求后端接口不存在跨域问题时,推荐使用 Session 身份认证机制。 当前端需要跨域请求后端接口时,推荐使用 JWT 身份认证机制。

6.4.1 JWT 身份认证机制

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

6.4.2 JWT 工作原理

image-20220918092414485.png 当客户端再次发起请求时,需通过发送请求头 Authorization 字段,值为 Bearer + 空格 + token 给客户端才能被正常解析。

6.4.3 JWT 的组成部分

JWT 通常由三部分组成,分别是 Header(头部)、payload(有效荷载)、signature(签名)。三者之间使用.分隔。

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

6.4.4 在项目中使用 JWT

安装 ​ npm install jsonwebtoken express-jwt ​ 其中,jsonwebtoken 是用于生成 JWT 字符串。 ​ express-jwt 是用于将 JWT 字符串解析还原成 JSON 对象。

使用

const jwt = requrie('jsonwebtoken')
const { expressjwt } = require('express-jwt')

// 1、定义 secret 密钥(用于对 token 进行加密和解密,越复杂越好)
const secretKey = 'fyf is cool ^_^'

// 2、判断登录成功后,调用 jwt.sign() 方法来生成 JWT 字符串。并通过 token 属性发送给客户端。
// 参数1:用户的信息对象(注意,不要把登陆密码传进去)
// 参数2:加密密钥
// 参数3:配置对象,可以配置当前 token 的有效期
app.post('/login', (req, res) => {
  const user = req.body
  if (user.username === 'admin' && user.password === '123456') {
    const tokenStr = jwt.sign({ username: user.username }, secretKey, { expiresIn: '30s' })
    res.send({
      status: 200,
      msg: '登陆成功',
      token: tokenStr
    })
  } else {
    res.send({status: 1, msg: '登陆失败'})
  }
})

// 3、调用 expressJWT 方法将 JWT 字符串还原为 JSON 对象
// 可以使用 .unlee() 方法来指定哪些路径不需要访问权限即可直接访问。以下表示以/api/开头的路径不需要访问权限。
// 当配置完此中间件后,用户信息就被挂载到 req.user(最新版是req.auth) 上,就可以在 req.user/req.auth 属性中获取用户信息。
app.use(expressjwt({secret: secretKey, algorithms: ["HS256"]}).unless({ path: [/^/api//] }))

app.get('/getUser', (req, res) => {
  res.send({
    status: 200,
    msg: '获取用户数据成功',
    data: req.auth
  })
})

// 4、可以使用一个错误级别中间件来捕获 token 过期或无效的所导致的错误
app.use((err, req, res, next) => {
  if (err.name === 'UnauthorizedError') { // 如果错误是由 token 导致的
    return res.send({status: 401, msg: 'token 校验失败'})
  } else {
    res.send({status: 500, msg: '未知错误'})
  }
})

在学习中随手做的笔记,难免出错,较为简陋。如有修改建议,不吝赐教。
转载请注明出处,谢谢。