Node.js:万字总结黑马教程,学懂node.js看这一篇就够了

一、初识 Nodejs

Node.js 中文网

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

  • 基于 Express 框架 (opens new window),可以快速构建 Web 应用
  • 基于 Electron 框架 (opens new window),可以构建跨平台的桌面应用
  • 基于 restify 框架 (opens new window),可以快速构建 API 接口项目
  • 读写和操作数据库、创建实用的命令行工具辅助前端开发、etc…

二、fs 文件系统模块

2.1 fs.readFile 读取文件

语法:

fs.readFile(path[, options], callback)
  • path:文件路径
  • options:配置选项,若是字符串则指定编码格式
    1. encoding:编码格式(utf-8)
    2. flag:打开方式
  • callback:回调函数
    1. err:错误信息
    2. data:读取的数据,如果未指定编码格式则返回一个 Buffer

2.2 fs.writeFile 写入文件

语法:

fs.writeFile(file, data[, options], callback)
  • file:文件路径
  • data:写入内容
  • options:配置选项,包含 encoding, mode, flag;若是字符串则指定编码格式
  • 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('\r\n') 
   const arrNew = []
   
   //对Old数组的每一项,将'='替换为':',并push到新数组中
   arrOld.forEach(item => {
    
    
       arrNew.push(item.replace('=', ':'))
   })
   
   //以回车分隔将数组串联成一字符串
   const newStr= arrNew.join('\r\n')
   // 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 模块操作文件时,如果提供的操作路径是以 ./ 或 …/ 开头的相对路径时,容易出现路径动态拼接错误的问题

  • 原因: 代码在运行的时候,会以执行 node 命令时所处的目录,动态拼接出被操作文件的完整路径

  • 解决方案:在使用 fs 模块操作文件时,直接提供完整的路径,从而防止路径动态拼接的问题
    __dirname 获取文件所处的绝对路径

fs.readFile(__dirname + '/files/1.txt', 'utf8', function(err, data) {
    
    
  ...
})
  • 原理:无论在哪个文件位置执行,绑定的路径都以读取的js文件位置为基准,在这个位置的基础上去访问它的上下级文件自然是对的

三、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) 		// \a\d\e

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对象

  • 自定义模块中都有一个 module 对象,存储了和当前模块有关的信息
  • 在自定义模块中,可以使用 module.exports 对象,将模块内的成员共享出去,供外界使用
  • 导入自定义模块时,得到的就是 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)

输出:

在这里插入图片描述

  • 默认情况下,exports 和 module.exports 指向同一个对象。
  • 最终共享的结果,以 module.exports 指向的对象为准
//自定义模块.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 模块化规范

  • 每个模块内部,module 变量代表当前模块
  • module 变量是一个对象,module.exports 是对外的接口
  • 加载某个模块即加载该模块的 module.exports 属性

5.6 模块加载机制

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

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

自定义模块加载

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

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

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

第三方模块加载

  • 若导入第三方模块, Node.js 会从当前模块的父目录开始,尝试从 /node_modules 文件夹中加载第三方模块
  • 如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录
  • 例如,假设在 C:\Users\bruce\project\foo.js 文件里调用了 require(‘tools’),则 Node.js 会按以下顺序查找:
  1. C:\Users\bruce\project\node_modules\tools
  2. C:\Users\bruce\node_modules\tools
  3. C:\Users\node_modules\tools
  4. C:\node_modules\tools

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

  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 包的定义

  • Node.js 中的第三方模块又叫做包
  • 不同于 Node.js 中的内置模块与自定义模块,包是由第三方个人或团队开发出来的,免费供所有人使用
  • 由于 Node.js 的内置模块仅提供了一些底层的 API,导致在基于内置模块进行项目开发的时,效率很低
  • 包是基于内置模块封装出来的,提供了更高级、更方便的 API,极大的提高了开发效率
  • 包和内置模块之间的关系,类似于 jQuery 和 浏览器内置 API 之间的关系

6.2 包的使用

  • 包查找地址: https://www.npmjs.com/
  • 包下载地址:https://registry.npmjs.org/
  • 用npm下载包,下载node的同时也将npm下载了下来,可以在终端中用npm -v查看npm的版本
  • 可在官网看对应包的使用说明
//在终端中(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

  • npm init -y
  • 命令只能在英文且无空格的目录下运行
  • 运行npm install命令安装包时,npm包管理工具会自动将包的名称和版本号,记录到package.json 的dependencies节点中
"dependencies": {
    
    
    "moment": "^2.29.4"
}

一次性安装所有需要的包

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

卸载包

npm uninstall 要卸载的包名

deDependencies节点

  • 如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到 devDependencies 节点中

  • 与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到 dependencies 节点中

  • 可使用如下的命令,将包记录到 devDependencies 节点中:

npm i 包名 -D
//完整写法如下
npm install 包名 --save-dev
  • 可以到官网看install安装说明,有–save-dev的就可以用这个方法丢devDependencies 节点里

解决下载速度慢的问题

为了更方便的切换下包的镜像源,我们可以安装 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 install 命令时,如果提供了 -g 参数,则会把包安装为全局包
  • 全局包会被安装到 C:\Users\用户目录\AppData\Roaming\npm\node_modules 目录下(新版本在安装目录下 或者你之前安装时配的路径中)
npm i 包名 -g					//全局安装指定的包
npm uninstall 包名 -g		//卸载全局安装的包
  • 注意:
    只有工具性质的包,才有全局安装的必要性,因为它们提供了好用的终端命令
    判断某个包是否需要全局安装后才能使用,可参考官方提供的使用说明

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

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

6.5 规范的包结构

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

  • 包必须以单独的目录而存在
  • 包的顶级目录下要必须包含 package.json 这个包管理配置文件
  • package.json 中必须包含 name,version,main 这三个属性,分别代表包的名字、版本号、包的入口。

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

七、Express

7.1 基本定义与使用

Express中文网

  • 基于 Node.js 平台,快速、开放、极简的 Web 开发框架

  • Express 是用于快速创建服务器的第三方模块。

安装 Express:

npm install express
//黑马课程学习建议使用@4.17.1
  1. 创建服务器,监听客户端get、post请求,并把内容响应给客户端
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&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'))

当public和files中都有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

作用:

  • 在编写调试 Node.js 项目的时候,如果修改了项目的代码,则需要频繁的手动 close 掉,然后再重新启动,非常繁琐
  • 现在,我们可以使用 nodemon(https://www.npmjs.com/package/nodemon) 这个工具,它能够监听项目文件的变动,当代码被修改后,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路由

  • 每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数
  • 在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求的 URL 同时匹配成功,则 Express 会将这次请求,转交给对应的 function 函数进行处理
  • 理解:打电话给运营商客服,按下1到0的键对应不同服务,这种按键和服务绑定的映射关系,就叫路由

路由匹配的注意点:

  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 中间件

  • 中间件是指业务流程的中间处理环节
  • 当一个请求到达 Express 的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理
  • Express 的中间件,本质上就是一个 function 处理函数,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.路由级别的中间件

  • 绑定到 express.Router() 实例上的中间件,叫做路由级别的中间件
  • 用法和应用级别中间件没有区别,应用级别中间件是绑定到 app 实例上,路由级别中间件绑定到 router 实例上
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 项目的开发效率和体验:

  • express.static 快速托管静态资源的内置中间件,例如: HTML 文件、图片、CSS 样式等(无兼容性)
  • express.json 解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
  • express.urlencoded 解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
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* 自定义中间件

演示body-parser,仅作为演示作用,该中间件和之前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-www-form-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 三者之一
  • HTTP 头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type(只有三个值 application/x-www-formurlencoded、multipart/form-data、text/plain)

预检请求

  • 请求方式为 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 模块

  • 安装 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('更新数据成功')
})
  • 删除数据
    删除数据时,推荐根据id这样的唯一标识来删除对应的数据
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('删除数据成功')
})
  • 使用 delete 语句会真正删除数据,保险起见,使用标记删除的形式,模拟删除的动作。即在表中设置状态status字段,标记当前的数据是否被删除
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 问题。

如何选择

  • 企业级网站,主要功能是展示,没有复杂交互,且需要良好的 SEO,可考虑服务端渲染
  • 后台管理项目,交互性强,无需考虑 SEO,可使用前后端分离
  • 为同时兼顾首页渲染速度和前后端分离开发效率,可采用首屏服务器端渲染 + 其他页面前后端分离的开发模式

8.4 身份认证

  • 身份认证又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户的确认
  • 如各大网站的手机验证码登录、账号密码验证等
  • 服务端渲染推荐使用Session认证机制
  • 前后端分离推荐使用JWT认证机制

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

安装

  • jsonwebtoken 用于将用户的信息生成 JWT 字符串
  • express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
//安装多个包中间用空格隔开
npm install jsonwebtoken express-jwt

定义 secret 密钥

  • 为保证 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),仅包含基础知识内容,供搜索备忘。

猜你喜欢

转载自blog.csdn.net/m0_51487301/article/details/125959702