一、初识 Nodejs

Node.js 中文网

Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine
Node.js® 是一个基于 Chrome V8 引擎 的 JavaScript 运行环境

二、fs 文件系统模块

2.1 fs.readFile 读取文件

语法

fs.readFile(path[, options], callback)

2.2 fs.writeFile 写入文件

语法

fs.writeFile(file, data[, options], callback)

2.3 读写文件案例

数据

在这里插入图片描述

目标处理结果

在这里插入图片描述

//读取文件
fs.readFile('./1.txt', 'utf-8', (err, dataStr) => {
	//如果读取失败,则返回err,默认值null,故可直接用于if判断
   if (err) {
   	//直接return,不再输出后续代码
       return console.log(err)
   }
   //dataStr为成功读取到的数据
   console.log('读取成功' + dataStr)
   
//处理数据    
   //对dataStr做空格换行分隔,赋到Old数组上,并创建空的New数组
   const arrOld = dataStr.split('rn') 
   const arrNew = []
   
   //对Old数组的每一项,将'='替换为':',并push到新数组中
   arrOld.forEach(item => {
       arrNew.push(item.replace('=', ':'))
   })
   
   //以回车分隔将数组串联成一字符串
   const newStr= arrNew.join('rn')
   // console.log(typeof(newStr)) 可以看到输出类型字符串
   // console.log(newStr)          输出串联后的字符
   
//写入文件
   fs.writeFile('./1.txt',newStr , (err) => {
       //写入失败返回的err默认null,因此可作为判断条件
       if (err) {
           //err.message错误信息
           return console.log(err.message);
       }
       console.log('写入成功');
   });
})

2.4 路径动态拼接问题 __dirname

fs.readFile(__dirname + '/files/1.txt', 'utf8', function(err, data) {
  ...
})

三、path 路径模块

path 模块是 Node.js 官方提供的、用来处理路径的模块。它提供了一系列方法属性用来满足用户对路径的处理需求

3.1 路径拼接 path.join()

 path.join('','')
const path = require('path')
const fs = require('fs')

// 注意 ../ 会抵消前面的路径
// ./ 会被忽略
const pathStr = path.join('/a', '/b/c', '../../', './d', 'e')
console.log(pathStr) 		// ade

fs.readFile(path.join(__dirname, './files/1.txt'), 'utf8', function (err, dataStr) {
   if (err) {
   		return console.log(err.message)
   }
	console.log(dataStr)
})

3.2 获取路径中文件名 path.basename()

使用 path.basename() 方法可以获取路径中的最后一部分,常通过方法获取路径中的文件名

path.basename(path[, ext])
  • path: 文件路径
  • ext: 文件扩展
const path = require('path')

// 定义文件的存放路径
const fpath = '/a/b/c/index.html'

const fullName = path.basename(fpath)
console.log(fullName) 				// index.html

const nameWithoutExt = path.basename(fpath, '.html')
console.log(nameWithoutExt) // index

3.3 获取路径中文件扩展名 path.extname()

const path = require('path')

const fpath = '/a/b/c/index.html'

const fext = path.extname(fpath)
console.log(fext)								 // .html

四、http 模块

http 模块是 Node.js 官方提供的、用来创建 web 服务器的模块。

4.1 创建基本 Web 服务器

const http = require('http')

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

// 为服务器实例绑定 request 事件监听客户端请求
server.on('request', function (req, res) {
   const url = req.url
   const method = req.method
   const str = `Your request url is ${url}, and request method is ${method}`
   console.log(str)

   // 设置 Content-Type 响应头,解决中文乱码问题
   res.setHeader('Content-Type', 'text/html; charset=utf-8')
   // 向客户端响应内容
   res.end(str)
})

//注:要是报错可能是端口mysql或其他软件占用,改个端口即可
server.listen(8080, function () {
   console.log('server running at http://127.0.0.1:8080')
})

4.2 实现简单路由效果

const http = require('http')
const server = http.createServer()

server.on('request', (req, res) => {
   const url = req.url
   // 设置默认响应内容为 404 Not found
   let content = '<h1>404 Not found!</h1>'
   
   // 判断用户请求是否为 / 或 /index.html 首页    
   if (url === '/' || url === '/index.html') {
       content = '<h1>首页</h1>'
   }
   
   // 判断用户请求的是否为 /about.html 关于页面 
   else if (url === '/about.html') {
       content = '<h1>关于页面</h1>'
   }

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

server.listen(80, () => {
   console.log('server running at http://127.0.0.1')
})

五、模块化

5.1 模块化概念

模块化是指解决一个复杂问题时,自顶向下逐层把系统划分为若干模块的过程,模块是可组合、分解和更换单元

模块化作用:

模块化规范是对代码进行模块化拆分组合需要遵守的规则,如使用何种语法格式引用模块和向外暴露成员

5.2 Node.js 中模块的分类

const fs = require('fs')
const custon = require('./custom')
const moment = require('monent')

5.3 Node.js 中的模块作用域

模块作用域:和函数作用域类似,在自定义模块中定义变量方法成员,只能在当前模块内被访问,无法被外界访问,这种模块级别访问限制叫做模块作用域
模块作用域作用:防止全局变量污染

5.4 module对象 / exports对象

//自定义模块.js
const age = 20
//向module.exports对象上挂载 属性 / 方法 / 变量 
module.exports.username = 'zs'
module.exports.sayHi = function(){
    console.log('hi')
}
module.exports.age = age

//使用模块.js
const m = require('./自定义模块')
console.log(m)

输出:

在这里插入图片描述

//自定义模块.js
const age = 20
//向exports对象上挂载 属性 / 方法 / 变量 
exports.username = 'zs'
exports.sayHi = function(){
   console.log('hi')
}
module.exports.age = age

//以module.exports最终指向为准(相当于开辟了一个新对象)
module.exports = {
   nickName: '做一只猫',
   sayHello(){
       console.log('hello')
   }
}

//即便这时候再改exports指向不影响
exports = {
   nickName: '做一只猫',
   sayHello(){
       console.log('hello')
   }
}

//使用模块.js
const m = require('./自定义模块')
console.log(m)

在这里插入图片描述

5.5 CommonJS 模块化规范

5.6 模块加载机制

模块第一次加载后会被缓存,即多次调用 require() 不会导致模块的代码被执行多次提高模块加载效率。

内置模块加载
内置模块加载优先级最高。

自定义模块加载

  • 加载自定义模块时,路径要以 ./ 或 …/ 开头,否则会作为内置模块或第三方模块加载(会报错

  • 导入自定义模块时,若省略文件扩展名,则 Node.js 会按顺序尝试加载文件:

  1. 按确切的文件名加载
  2. 补全 .js 扩展名加载
  3. 补全 .json 扩展名加载
  4. 补全 .node 扩展名加载
  5. 报错

第三方模块加载

  1. C:Usersbruceprojectnode_modulestools
  2. C:Usersbrucenode_modulestools
  3. C:Usersnode_modulestools
  4. C:node_modulestools

目录作为模块加载
当把目录作为模块标识符进行加载的时候,有三种加载方式

  1. 在被加载的目录查找一个叫做 package.json 的文件,并寻找 main 属性,作为 require() 加载的入口
  2. 如果目录没有 package.json 文件,或者 main 入口不存在或无法解析,则 Node.js 将会试图加载目录下的 index.js 文件
  3. 如果以上两步都失败了,则 Node.js 会在终端打印错误消息,报告模块的缺失:Error: Cannot find module ‘xxx

例:
1 新建test文件夹,在文件夹中放 a.js , index.js 和 package.json
在这里插入图片描述

//a.js中
console.log('调用了a.js')

//index.js中
console.log('调用了index.js')

//package.json中
{
    "main": "./a.js"
}

3 新建1.js 并调用

//try.js
require('./test')

在这里插入图片描述
4 删去package.json 再次调用1.js
在这里插入图片描述
5 删去index.js 再次调用1.js
在这里插入图片描述

六、包

6.1 包的定义

6.2 包的使用

//在终端中(vs终端也可)@版本号可不加
npm install 完整的包名@版本号
//简写:
npm i 完整的包名

//例:用moment实例时间
//终端中
npm i moment
//代码区中
const moment = require('moment')
const dt = moment().format('YYYY-MM-DD HH:mm:ss')
console.log(dt)
//这个包停止维护了,但还可以使用

6.3 包管理配置文件

作用: 团队开发中第三方的体积过大,因为联网即可下载第三方包,所以不需要将包也上传项目中,而只用上传核心文件

创建packge.json

"dependencies": {
    "moment": "^2.29.4"
}

一次安装所有需要的包

npm install		//能安装dependencies节点中所有的包

卸载

npm uninstall卸载包名

deDependencies节点

npm i 包名 -D
//完整写法如下
npm install 包名 --save-dev

解决下载速度慢的问题

为了更方便的切换下包的镜像源,我们可以安装 nrm 这个小工具,利用 nrm 提供的终端命令,可以快速查看和切换下包的镜像

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

6.4 包的分类

1. 项目包
那些被安装到项目的 node_modules 目录中的包,都是项目包。

项目包又分为两类,分别是:

  • 开发依赖包(被记录到 devDependencies 节点中的包,只在开发期间会用到
  • 核心依赖包(被记录到 dependencies 节点中的包,在开发期间和项目上线之后都会用到
npm i 包名 -D 	//开发依赖包(会被记录到devDependencies节点下)
npm i 包名			//核心依赖包(会被记录到dependencies节点下)

2.全局

npm i 包名 -g					//全局安装指定的包
npm uninstall 包名 -g		//卸载全局安装的包
  • 注意:
    只有工具性质的包,才有全局安装的必要性,因为它们提供了好用的终端命令
    判断某个包是否需要全局安装后才能使用,可参考官方提供的使用说明

3.i5ting_toc
i5ting_toc 是一个可以把 md 文档转为 html 页面的小工具,使用步骤如下:

//将 i5ting_toc 安装为全局包
npm install -g i5ting_toc
//调用 i5ting_toc 实现mdhtml的功能
i5ting_toc -f 要转换md文件路径 -o

6.5 规范的包结构

一个规范的包,它的组成结构,必须符合以下 3 点要求:

注意: 以上 3 点要求是一个规范的包结构必须遵守的格式,关于更多的约束,可以参考如下网址
https://yarnpkg.com/zh-Hans/docs/package-json

七、Express

7.1 基本定义与使用

Express中文网

安装 Express:

npm install express
//黑马课程学习建议使用@4.17.1
  1. 创建服务器监听客户端getpost请求,并把内容响应客户端
const express = require('express')

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

// 监听客户端的 GET 和 POST 请求,并向客户端响应具体的内容
// 注意这里是向客户端发送相应了!
app.get('/user', (req, res) => {
	//向客户发送 JSON 对象
    res.send({ name: 'zs', age: 20, gender: '男' })
})
app.post('/user', (req, res) => {
	//向客户发送文本内容
    res.send('请求成功')
})

//启动web服务器
app.listen(80, () => {
    console.log('express server running at http://127.0.0.1')
})
  1. 获取 URL 中携带的查询参数和URL中的动态参数
const express = require('express')

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

//查询参数
app.get('/', (req, res) => {
    // 通过 req.query 可以获取客户发送过来的查询参数
    // req.query默认是一个空对象
    // 客户端使用 http://127.0.0.1?name=zs&amp;age=20这种查询字符串形式,发送服务器的参数
    // 可通过req.query对象访问到,如:req.query.name
    console.log(req.query)
    res.send(req.query)
})

// 这里的 :id 是一个动态的参数 id可以是其他任意合法的值
app.get('/user/:ids/:username', (req, res) => {
    // req.params 是动态匹配到的 URL 参数,默认是一个空对象
    console.log(req.params)
    res.send(req.params)
})

//启动web服务
app.listen(80, () => {
    console.log('express server running at http://127.0.0.1')
})

7.2 托管静态资源

1. .use(express.static())

express 提供了一个非常好用函数,叫做 express.static(),通过它,我们可以非常方便地创建一个静态资源服务器,例如,通过如下代码就可以将 public 目录下的图片、CSS 文件、JavaScript 文件对外开放访问了:

app.use(express.static('public'))

现在就可以访问public目录中的所有文件了:

http://localhost:3000/images/bg.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/login.js

注意: Express 在指定静态目录中查找文件,并对外提供资源访问路径,因此,存放静态文件的目录名不会出现在 URL 中,即public不会出现在上述http://...的URL中

2. 托管多个静态资源目录

访问静态源文件时,express.static() 函数会根据目录的添加顺序查找所需的文件

app.use(express.static('public'))
app.use(express.static('files'))

publicfiles中都有index.js,会优先访问public中的

3. 挂载路径前缀

如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下的方式

app.use('/public, express.static('public'))

现在,你就可以通过带有 /public 前缀地址来访问 public 目录中的文件了:

http://localhost:3000/public/images/kitten.jpg
http://localhost:3000/public/css/style.css
http://localhost:3000/public/js/app.js

7.3 安装nodemon

作用:

安装nodemon

在终端中,运行如下命令,即可将 nodemon 安装为全局可用的工具:

npm install -g nodemon

使用 nodemon
将 node 命令替换为 nodemon 命令,使用 nodemon app.js 来启动项目。使得代码被修改之后,会被 nodemon 监听到,从而实现自动重启项目的效果

node 1.js
nodemon 1.js
//若无法使用nodemon,管理员权限打开终端输入:set-ExecutionPolicy RemoteSigned 之后按Y

7.4 Express路由

路由匹配的注意点:

  1. 按照定义的先后顺序进行匹配
  2. 请求类型和请求的URL同时匹配成功,才会调用对应处理函数

创建路由模块:

// router.js
// 导入express
const express = require('express')

// 创建路由对象
const router = express.Router()

// 挂载具体路由
// /api统一挂载的访问前缀
router.get('/api/user/list', (req, res) => {
  res.send('Get user list.')
})
router.post('/api/user/add', (req, res) => {
  res.send('Add new user.')
})

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

注册路由模块:

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

const app = express()

// 注册路由模块,添加访问前缀
//app.use(expresss.static('./files'))
//app.use()的作用是注册全局中间件 expresss.static和router都是中间件
// '/api', 是统一挂载的访问前缀
app.use('/api', router)

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

7.5 Express 中间件

//包含 req, res, next 三个参数,next() 参数把流转关系交给下一个中间件或路由
const mw = function(req, res, next){
	next()
}

在这里插入图片描述

中间件注意事项

  1. 在注册路由之前注册中间件(错误级别中间件除外)

  2. 中间件可连续调用多个

  3. 执行完中间件的业务代码之后,不要忘记调用 next() 函数

  4. 为了防止代码逻辑混乱,调用 next() 函数后不要再写额外的代码

  5. 连续调用多个中间件时,多个中间件之间,共享 req 和 res 对象

7.5.1 全局中间件

通过 app.use() 定义的中间件为全局中间件

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

//1 常规写法
const mw = function(req, res, next){
	console.log('这是一个中间件')
	next()
}
app.use(mw)

//2 简化形式
app.use((req, res, next) => {
  console.log('这是一个中间件')
  next()
})
  1. 多个中间件之间,共享同一份 req 和 res。基于这样的特性我们可以在上游的中间件中,统一为 req 或 res 对象添加自定义的属性方法,供下游的中间件或路由进行使用
    在这里插入图片描述
app.use((req, res, next) => {
  req.startTime = Date.now()
  next()
})
app.get('/',(req, res) =>{
	res.send('hi' + startTime)
})
app.get('/',(req, res) =>{
	res.send('hello' + startTime)
})
  1. 可以使用 app.use() 连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行
//定义第一个全局中间件
app.use((req, res, next) => {
  console.log('调用了第1个全局中间件')
  next()
})
// 定义第二个全局中间件
app.use((req, res, next) => {
  console.log('调用了第2个全局中间件')
  next()
})

app.get('/user', (req, res) => {
  res.send('User page.')
})

app.listen(80, () => {
  console.log('http://127.0.0.1')
})
7.5.2 局部中间件

不使用 app.use() 定义的中间件,叫做局部生效的中间件

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

// 定义中间件函数
const mw1 = (req, res, next) => {
  console.log('调用了第一个局部生效的中间件')
  next()
}

const mw2 = (req, res, next) => {
  console.log('调用了第二个局部生效的中间件')
  next()
}
//1. 调用一个中间件
app.get('/one', mw1, (req, res) => res.send('one page.'))

// 2. 调用多个中间件的两种方式
app.get('/hello', mw2, mw1, (req, res) => res.send('hello page.'))
app.get('/about', [mw1, mw2], (req, res) => res.send('about page.'))

app.get('/user', (req, res) => res.send('User page.'))

app.listen(80, function () {
  console.log('Express server running at http://127.0.0.1')
})

7.5.3 中间件的分类

1.应用级别的中间件

通过 app.use() 或 app.get() 或 app.post() ,绑定到 app 实例上的中间件

2.路由级别的中间件

const app = express()
const router = express.Router()

router.use(function (req, res, next) {
    console.log(1)
    next()
})

app.use('/', router)

3.错误级别的中间件

  • 用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
  • 错误级别中间件的处理函数中,必须有 4 个形参,形参顺序从前到后分别是 (err, req, res, next)
  • 错误级别的中间件必须注册在所有路由之后,否则无法捕获路由抛出的错误
const express = require('express')
const app = express()

app.get('/', (req, res) => {
    throw new Error('服务器内部发生了错误!')
    res.send('Home page.')
})

// 定义错误级别的中间件,捕获整个项目的异常错误,从而防止程序崩溃
app.use((err, req, res, next) => {
    console.log('发生了错误!' + err.message)
    res.send('Error:' + err.message)
})

app.listen(80, function () {
    console.log('Express server running at http://127.0.0.1')
})

4.Express 内置中间件

自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验

app.use(express.json())
app.use(express.urlencoded({ extended: false }))

演示express.json 和 express.urlencoded

// 导入 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 http://127.0.0.1')
})

5.第三方中间件

7.5.4* 自定义中间件

演示bodyparser,仅作为演示作用,该中间件和之前express.json 和 express.urlencoded的用法区别

// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()

// 1. 导入解析表单数据的中间件 body-parser
const parser = require('body-parser')
// 2. 使用 app.use() 注册中间件
app.use(parser.urlencoded({ extended: false }))
// app.use(express.urlencoded({ extended: false }))

app.post('/user', (req, res) => {
  // 如果没有配置任何解析表单数据的中间件,则 req.body 默认等于 undefined
  console.log(req.body)
  res.send('ok')
})

// 调用 app.listen 方法,指定端口号启动web服务器
app.listen(80, function () {
  console.log('Express server running at http://127.0.0.1')
})

5.第三方中间件
注意:querystring 模块已被弃用,可改用JSON.parse

// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// 导入 Node.js 内置的 querystring 模块
const qs = require('querystring')

// 这是解析表单数据的中间件
app.use((req, res, next) => {
  // 定义中间件具体的业务逻辑
  // 1. 定义一个 str 字符串,专门用来存储客户端发送过来的请求体数据
  let str = ''
  // 2. 监听 req 的 data 事件
  req.on('data', (chunk) => {
    str += chunk
  })
  // 3. 监听 req 的 end 事件
  req.on('end', () => {
    // 在 str 中存放的是完整的请求体数据
    // console.log(str)
    // TODO: 把字符串格式的请求体数据,解析成对象格式
    const body = qs.parse(str)
    req.body = body
    next()
  })
})

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

// 调用 app.listen 方法,指定端口号启动web服务器
app.listen(80, function () {
  console.log('Express server running at http://127.0.0.1')
})

7.5.5 编写GET、POST接口

编写接口

//exp.js
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 接口(用于验证后面发送两次请求,即OPTION 请求和正常请求)
router.delete('/delete', (req, res) => {
  res.send({
    status: 0,
    msg: 'DELETE请求成功',
  })
})
module.exports = router

使用接口

//use.js
// 导入 express
const express = require('express')
// 创建服务器实例
const app = express()

// 导入路由模块
const router = require('./exp.js')
// 把路由模块,注册到 app 上
app.use('/api', router)

// 启动服务器
app.listen(80, () => {
  console.log('express server running at http://127.0.0.1')
})

7.5.6 CORS 跨域资源共享

CORS 中间件解决跨域

  • 安装中间件:npm install cors
  • 导入中间件:const cors = require(‘cors’)
  • 配置中间件:app.use(cors())

CORS

  • CORS(Cross-Origin Resource Sharing,跨域资源共享)解决跨域,是通过 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源
  • 浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了 CORS 相关的 HTTP 响应头,就可解除浏览器端的跨域访问限制
  • CORS 主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口
  • CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启了 CORS 的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。
// 导入 express
const express = require('express')
// 创建服务器实例
const app = express()

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

// 一定要在路由之前,配置 cors 这个中间件,从而解决接口跨域的问题
const cors = require('cors')
app.use(cors())

// 导入路由模块
const router = require('./exp.js')
// 把路由模块,注册到 app 上
app.use('/api', router)

// 启动服务器
app.listen(80, () => {
  console.log('express server running at http://127.0.0.1')
})

CORS 常见响应头

  • Access-Control-Allow-Origin:制定了允许访问资源的外域 URL
//允许百度访问
res.setHeader('Access-Control-Allow-Origin', 'http://www.baidu.com')
//允许所有URL访问
res.setHeader('Access-Control-Allow-Origin', '*')
  • Access-Control-Allow-Headers
  1. 默认情况下,CORS 仅支持客户端向服务器发送如下的 9 个请求头:Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type (值仅限于 text/plain、multipart/form-data、application/x-wwwform-urlencoded 三者之一)
  2. 如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')
  • Access-Control-Allow-Methods
    默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods 来指明实际请求所允许使用的 HTTP 方法
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD')
res.setHEader('Access-Control-Allow-Methods', '*')

CORS 请求分类

简单请求

预检请求

  • 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
  • 请求头中包含自定义头部字段
  • 向服务器发送了 application/json 格式的数据
  • 在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据

*编写JSON接口

// 导入 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 http://127.0.0.1')
})

八、数据库身份认证

8.1 Node 操作 mysql

配置 mysql 模块

npm install mysql
const mysql = require('mysql')

const db = mysql.createPool({
    host: '127.0.0.1',
    user: 'root',
    password: 'root',
    database: 'test',
})
db.query('select 1', (err, results) => {
   if (err) return console.log(err.message)
   console.log(results)
})

8.2 操作 mysql 数据库

db.query('select * from users', (err, results) => {
  	if (err) return console.log(err.message)
    console.log(results)
})
const user = {username: '做一只猫', password:'111111'}

// ? 表示占位符
const sql = 'insert into users values(?, ?)'

// 使用数组的形式为占位符指定具体的值
db.query(sql, [user.username, user.password], (err, results) => {
    if (err) return console.log(err.message)
    if (results.affectedRows === 1) console.log('插入成功')
})
const user = { username: '做一只猫', password: '111111' }
const sql = 'insert into users set ?'
db.query(sql, user, (err, results) => {
    if (err) return console.log(err.message)
    if (results.affectedRows === 1) console.log('插入成功')
})
const user = { username: '做一只猫', password: '111111' }
const sql = 'update users set username=?, password=? where id=?'
db.query(sql, [user.username, user.password, user.id], (err, results) => {
  	if (err) return console.log(err.message)
    if (results.affectedRows === 1) console.log('更新数据成功')
})
const user = {id: 4, username: '做一只猫', password: '111111' }
const sql = 'update users set ? where id=?'
db.query(sql, [user, user.id], (err, results) => {
  	if (err) return console.log(err.message)
    if (results.affectedRows === 1) console.log('更新数据成功')
})
const sql = 'delete from users where id=?'
//4代表user.id
//若SQL语句中有多个占位符,则必须使用数组为每个占位符指定具体的值
//若SQL语句中只有一个占位符,则可以省略
db.query(sql, 4, (err, results) => {
  	if (err) return console.log(err.message)
    if (results.affectedRows === 1) console.log('删除数据成功')
})
db.query('update users set status=1 where id=?', 7, (err, results) => {
  	if (err) return console.log(err.message)
    if (results.affectedRows === 1) console.log('删除数据成功')
})

8.3 Web 开发模式

服务端渲染

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

app.get('/index.html', (req, res) => {
  const user = { name: 'Bruce', age: 29 }
  const html = `<h1>username:${user.name}, age:${user.age}</h1>`
  res.send(html)
})
  • 优点:
  1. 前端耗时短。浏览器只需直接渲染页面,无需额外请求数据。
  2. 有利于 SEO。服务器响应的是完整的 HTML 页面内容,有利于爬虫爬取信息。
  • 缺点:
  1. 占用服务器资源。服务器需要完成页面内容的拼接,若请求比较多,会对服务器造成一定访问压力。
  2. 不利于前后端分离,开发效率低。

前后端分离

前后端分离的开发模式依赖于 Ajax 技术的广泛应用。后端只负责提供 API 接口,前端使用 Ajax 调用接口。

  • 优点:
  1. 开发体验好。前端专业页面开发,后端专注接口开发。
  2. 用户体验好。页面局部刷新,无需重新请求页面。
  3. 减轻服务器的渲染压力。页面最终在浏览器里生成
  • 缺点:

    不利于 SEO。完整的 HTML 页面在浏览器拼接完成,因此爬虫无法爬取页面的有效信息。

Vue、React 等框架的 SSR(server side render)技术解决 SEO 问题。

如何选择

8.4 身份认证

8.4.1 Session 认证机制

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

Session 工作原理
在这里插入图片描述

会员卡例子详解

1. HTTP 协议的无状态

  • 了解 HTTP 协议的无状态性是进一步学习 Session 认证机制的必要前提
  • HTTP 协议的无状态性,指的是客户端的每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态

在这里插入图片描述
2. 如何突破 HTTP 无状态的限制

对于超市来说,为了方便收银员在进行结算时给 VIP 用户打折,超市可以为每个 VIP 用户发放会员卡
在这里插入图片描述
注意: 现实生活中的会员卡身份认证方式,在 Web 开发中的专业术语叫做 Cookie

3. 关于 Cookie

  • Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符
  • 它由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成
    -不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器
  • Cookie的几大特性
  1. 自动发送
  2. 域名独立
  3. 过期时限
  4. 4KB 限制

4. Cookie 在身份认证中的作用

  • 客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中
  • 随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份
    在这里插入图片描述

5. Cookie 不具有安全

  • 由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全
  • 因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器
  • 注意区分伪造跟盗取的不同
    在这里插入图片描述
    注意: 千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等。

6. 提高身份认证的安全性

在这里插入图片描述
这种 “会员卡 + 刷卡认证” 的设计理念,就是 Session 认证机制的精髓。

8.4.2 Express 中使用 Session 认证

安装 express-session 中间件

npm install express-session

配置中间件

const session = require('express-session')
app.use(
	session({
		secret: '做一只猫', 		// secret 属性的值为任意字符
		resave: false,			//固定写法
		saveUninitalized: true	//固定写法
  })
)

向 session 中存数据
中间件配置成功后,可通过 req.session 访问 session 对象,存储用户信息

app.post('/api/login', (req, res) => {
	//判断用户提交登录信息是否正确
	if(req.body.username !== 'admin' || req.body.password !== '000000'){
		return res.send({status: 1, msg: '登录失败'})
	}
	req.session.user = req.body	//将用户的信息存储到S
	req.session.isLogin = true

 	res.send({ status: 0, msg: 'login done' })
})

从 session 取数

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) => {
 	// 清空当前客户端的session信息
	req.session.destroy()
	res.send({ status: 0, msg: '退出登录成功' })
})
8.4.3 JWT 认证机制
  • Session 认证机制需要配合 Cookie 才能实现。
  • 由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证
  • JWT(英文全称:JSON Web Token) 是目前最流行的跨域认证解决方案

注意:

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

JWT 的工作原理
用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份
在这里插入图片描述

JWT 组成部分:
Header、Payload、Signature

  • Payload 是真正的用户信息,加密后的字符串
  • Header 和 Signature 是安全性相关部分,保证 Token 安全性
  • 三者使用 . 分隔
Header.Payload.Signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTcsInVzZXJuYW1lIjoiQnJ1Y2UiLCJwYXNzd29yZCI6IiIsIm5pY2tuYW1lIjoiaGVsbG8iLCJlbWFpbCI6InNjdXRAcXEuY29tIiwidXNlcl9waWMiOiIiLCJpYXQiOjE2NDE4NjU3MzEsImV4cCI6MTY0MTkwMTczMX0.bmqzAkNSZgD8IZxRGGyVlVwGl7EGMtWitvjGD-a5U5c

JWT 使用方式:

  • 客户端会把 JWT 存储localStorage 或 sessionStorage 中
  • 此后客户端与服务端通信需要携带 JWT 进行身份认证,将 JWT 存在 HTTP 请求头 Authorization 字段中
  • 加上 Bearer 前缀
Authorization: Bearer <token>
8.4.4 Express 使用 JWT

安装

//安装多个包中间用空格隔开
npm install jsonwebtoken express-jwt

定义 secret 密钥

  1. 生成 JWT 字符串时,使用密钥加密信息,得到加密好的 JWT 字符串
  2. 把 JWT 字符串解析还原成 JSON 对象时,使用密钥解密
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')

// 密钥可以为任意字符串
const secretKey = '做一只猫No.1'

生成 JWT 字符串

app.post('/api/login', (req, res) => {
  ...
  res.send({
    status: 200,
    message: '登录成功',
    // jwt.sign() 生成 JWT 字符串
    // 参数:用户信息对象、加密密钥、配置对象-token有效期
    // 尽量不保存敏感信息,因此只有用户名,没有密码
    token: jwt.sign({username: userInfo.username}, secretKey, {expiresIn: '10h'})
  })
})

JWT 字符串还原为 JSON 对象

  • 客户端访问有权限的接口时,需通过请求头的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证
  • 服务器可以通过 express-jwt 中间件将客户端发送过来的 Token 解析还原成 JSON 对象
// unless({ path: [/^/api//] }) 指定哪些接口无需访问权限
app.use(expressJWT({ 
	secret: secretKey,
	//6.0版本之后要加上这个
	algorithms: [HS256]
}).unless({ path: [/^/api//] }))

获取用户信息

  • 当 express-jwt 中间件配置成功后,即可在那些有权限的接口中,使用 req.user 对象,来访问从 JWT 字符串中解析出来的用户信息
app.get('/admin/getinfo', (req, res) => {
  console.log(req.user)
  res.send({
    status: 200,
    message: '获取信息成功',
    //data: req.user
    //6.0版本后改用req.auth
    data: req.auth
    
  })
})

捕获解析 JWT 失败后产生的错误

  • 当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行
  • 通过 Express 的错误中间件,捕获这个错误并进行相关的处理
app.use((err, req, res, next) => {
  if (err.name === 'UnauthorizedError') {
    return res.send({ status: 401, message: 'Invalid token' })
  }
  res.send({ status: 500, message: 'Unknown error' })
})

注: 本篇博客包含时钟案例(P11-P13),时钟web案例(P18),大事件项目(P77-P96),仅包含基础知识内容,供搜索备忘。

原文地址:https://blog.csdn.net/m0_51487301/article/details/125959702

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_17903.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注