前言:这篇博客是跟着黑马程序员的 node.js 入门课程写下的。
链接地址: b站黑马程序员node.js课程
结合自己的理解,做了部分补充,可放心食用。
以下是正文:
JavaScript 核心语法
包括 变量、数据类型、循环、分支、判断、函数、作用域、this 等
WebAPI
包括 DOM 操作、 BOM 操作、 基于XMLHttpRequest 的Ajax 操作 等
待执行的js代码 -> 被 JavaScript 解析引擎 解析
不同的浏览器使用不同的JavaScript解析引擎
浏览器 | 引擎 |
---|---|
Chrome 浏览器 | V8(性能最好) |
Firefox 浏览器 | OdinMonkey |
Safri 浏览器 | JSCore |
IE 浏览器 | Chakra |
node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境.(也就是说webApi 不能在nodejs中运行)
包括两部分
V8 引擎(即JavaScript运行环境,学过了)
内置 API (包括内置模块 fs,path, http,js内置对象,querystring,等等)
但是内置api 最终也是交给v8 引擎执行,本质上是代码
node 作为一个 JavaScript 的运行环境,仅仅提供了基础的功能和api。然而,基于nodejs 提供的这些基础功能,有很多强大的工具和框架。
总之,node功能强大,可以增强前端程序员的行业竞争力
学习路径
JavaScript 基础语法 + node.js 内置模块 +第三方api 模块(express, mysql)
fs 模块是 nodejs 官方提供的、用来操作文件的模块。它提供了一系列的方法和属性,用来满足用户对文件的操作需求。
如果要在 JavaScript 代码中,使用 fs 模块来操作文件, 则需要使用如下的方式先导入它:
const fs = require('fs')
方法:
fs.readFile() 方法,用来读取指定文件中的内容
语法格式如下
fs.readFile(path[, options], callback)
参数:
谨记:
callback(err, data) 中 data 用来声明接收数据的时候接收的是一个二进制文件,是一个object类型,不是string。如果要输出的话要用data.toString() 方法
fs.writeFile() 方法,用来向指定的文件写入内容
语法格式
fs.writeFile(file, data[, options], callback)
参数:
参数1:必选参数,字符串,需要指定一个文件路径的字符串,表示文件的存放路径
参数2:必选参数,字符串表示要写入的内容
参数3:可选参数,表示以什么格式写入文件,默认值是utf-8
参数4:必选参数,文件写入完成以后的回调函数
说明:
如果文件写入成功,则回调函数接收到的参数值为null,否则为错误对象
只能用来创建文件,不能用来创建目录
如果是文件名未建立,则会建立。如果已有同名文件,则会覆盖写入。
考试成绩整理
使用 fs 文件系统模块, 将素材目录下成绩.txt文件中的考试数据,整理到成绩-ok.txt文件中。
整理前,成绩.txt 文件中的数据格式如下:
小红=99 小白=100 小黄=70 小黑=66 小绿=88
整理完成之后,希望得到成绩-ok.txt文件中的数据格式如下:
小红: 99
小白: 100
小黄: 70
小黑: 66
小绿: 88
实现步骤
const fs = require('fs')
fs.readFile('./成绩.txt',(err,data) => {
if (err) {
console.log(err.message)
return
}
// 4.1 先把处理的数据, 按照空格进行分割成数组
const arr = data.toString().split(' ')
// 4.2 循环分割后的数组,对每一项数据,进行字符串的替换操作
const newArr = []
for (let i = 0;i < arr.length; i++) {
newArr[i] = arr[i].replace('=',': ')
}
// 4.3 把新数组中的每一项,进行合并,得到一个新的字符串
const newData = newArr.join('\n')
fs.writeFile('./成绩-ok.txt',newData,(err) => {
if (err) {
console.log('写入失败'+err.message)
} else {
console.log('写入成功!');
}
})
})
代码在运行的时候,如果是相对路径。操作文件的目录 = 执行 node 命令时所处的目录 + 操作路径 动态凭借出来的。所以在不是当前代码文件的目录层执行文件时,容易出现动态拼接错误的问题。
代码如下:
PS C:\Users\MI\Desktop\process> node .\node\readFile.js
[Error: ENOENT: no such file or directory, open 'C:\Users\MI\Desktop\process\1.txt'] {
errno: -4058,
code: 'ENOENT',
syscall: 'open',
path: 'C:\\Users\\MI\\Desktop\\process\\1.txt'
}
绝对路径:移植性非常差,不利于维护。可能在你的电脑上是这个路径,到了别人的电脑上又是另外一个路径。就找不到了
解决方案
__dirname
这个dirname 是返回你这个 fs 模块这个代码文件的绝对路径字符串。不会跟着node 命令的路径变换而变换
所以 fs 模块 路径参数可以使用__dirname
+文件目录拼接起来执行。
代码:
fs.readFile(__dirname + '/file/1.txt', () => {})
如果写的是./file/1.txt
则会报错,所以推荐使用path.join()方法
path 模块是node.js 官方提供的、用来处理路径的模块。它提供了一系列的方法和属性,来满足用户对路径的处理需求。
使用:
先导入
const path = require('path')
方法:
path.join() 方法,用来将多个路径片段凭借成一个完整的路径字符串
path.join([...paths])
参数:
paths
路径片段序列
返回值:
path.basename() 方法,返回路径字符串的最后一个部分,通常用于解析文件名
path.basename(path[, ext])
参数:
path
必选参数,字符串,表示一个路径的字符串
ext
可选参数,字符串,表示文件的扩展名。如果扩展名命中则不会显示扩展名
将素材目录下的 index.html 页面,拆分成三个文件,分别是:
并且将拆分出来的 3 个文件, 存放到clock 目录中。
和
标签什么是客户端,什么是服务器?
在网络节点中,负责消费资源的计算机,叫做客户端;负责对外提供网络资源的计算机,叫做服务器。
服务器和普通电脑的区别在于,服务器上安装了 web 服务器软件。通过安装这些软件,就能把一台普通的电脑变成一台 web 服务器。
http 模块是 node.js 提供系统模块,用来创建 web 服务器。
在 nodejs 中,我们可以通过几行代码,手写一个服务器软件,从而对外提供web服务
导入 http 模块
const http = require('http')
创建 web 服务器实例
调用 http.createServer() 方法
const server = http.createServer()
为服务器实例绑定 request 事件,即可 监听客户端的请求
使用服务器实例的 .on() 方法,为服务器绑定一个 request 事件。只要有客户端来请求我们自己的服务器,就会触发 request 事件,从而调用这个事件处理函数.其中这个事件处理函数接收两个参数,第一个是请求对象,包含了与客户端相关的数据和属性。
server.on('request', (req, res) => {
console.log('有客户端访问')
res.end()
})
第二个是响应对象,包含了与服务器相关的内容和属性.
可以使用res.end() 向客户端发送指定的内容,并结束这次请求的处理过程
启动服务器
调用服务器的实例的 .listen(端口号,callback) 方法,即可启动当前的 web 服务器实例
server.listem(80, () => {
})
问题:请求的 req 信息中并没有包含客户端发出的端口信息,那么 res 向客户端发送信息时,是如何知道应该发往客户端的哪个端口的呢?
需要设置 响应头 Content-Type的值为 text/html;charset=utf-8
res.setHeader('Content-Type','text/html;charset=utf-8')
核心思路
把文件的实际存放路径,作为每个资源的请求 url 地址。
实现步骤
ip 地址就是互联网上每台计算机的唯一地址, 因此 ip 地址具有唯一性。就像电话号码。
ip 地址的格式:通常用 ”点分十进制”表示成(a.b.c.d)的形式。其中,a,b,c,d 都是0~255之间的十进制数(实际上每一个数都是八位2进制数,所以ipv4是32为 ip 地址)
注意:
localhost.首先问本地域名服务器,然后问局域网域名服务器,最后问公网域名服务器。
每一个 web服务 使用唯一个端口号。
http服务默认端口80,https默认端口443.可以被省略。
学习目标
模块化 是指解决一个复杂问题时,自顶向下逐层把系统分成若干模块的过程。对于整个系统来说,模块时可组合、分解和更换的单元。
编程领域中的模块化
编程领域中的模块化, 就是遵守固定规则,把一个大文件拆成独立并且相互依赖的多个小模块
把代码进行模块化拆分的好处:
模块化规范 就是对经醒模块化的拆分与组合时,需要遵循的那些规则
例如:
模块化规范的好处: 大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用,利人利己。
node.js 中根据模块来源的不同,将模块分为了 3 大类。
使用require() 方法
const fs = require('fs')
const custom = require('./路径.js')
加载用户自定义模块时可以省略.js 的扩展名
const moment = require(‘moment’)
和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在模块内被访问,这种模块级别的访问限制,称为模块作用域。只能访问自定义模块导出(export)的变量和方法
防止了全局变量污染的问题
说明:(浏览器script脚本引入的方式会导致声明的变量声明在window上的问题)
在每个.js 自定义模块中都有一个 module 对象,它里面存储了和当前模块有关的信息,打印如下
Module {
id: '.',
path: 'C:\\Users\\MI\\Desktop\\process\\node',
exports: {},
parent: null,
filename: 'C:\\Users\\MI\\Desktop\\process\\node\\test.js',
loaded: false,
children: [],
paths: [
'C:\\Users\\MI\\Desktop\\process\\node\\node_modules',
'C:\\Users\\MI\\Desktop\\process\\node_modules',
'C:\\Users\\MI\\Desktop\\node_modules',
'C:\\Users\\MI\\node_modules',
'C:\\Users\\node_modules',
'C:\\node_modules'
]
}
module.exports对象
在自定义模块中,可以使用 module.exports 对象,将模块内的成员共享出去,供外界使用
外界用 require() 方法导入自定义模块时,得到的就是 module.exports 所指的对象
导出
module.exports.username = 'zs'
module.exports.sayHi = ()=>{ console.log('hello');}
导入:
const custom = require('./custom')
console.log(custom);
输出:
{ username: 'zs', sayHi: [Function (anonymous)] }
证明exports 是一个对象,username 和 sayHi 是挂载的属性名
let username = 'zs'
module.exports = username
username = 'ls'
console.log(module.exports);
结果
zs
说一说赋值操作,赋值操作就是如果一个变量是基本类型的话,赋值操作就是把这个变量的值赋给另外一个变量。那么这两个变量就无关了。
如果这个变量是引用类型的话,就是把这个变量的存储的地址值赋给这个变量。这两个变量由于指向同一个地址所以会保持同步。
测试:
const Fn = ()=>{}
module.exports = Fn
导出的就是这个函数体
const Fn = () => {}
module.exports = { Fn }
导出的是一个对象,对象里面有 Fn 这个键,值是一个函数体
exports 对象
一个对象 相当于 let exports = module.exports
exports只能使用语法来向外暴露内部变量:如exports.xxx 否则会改变 exports 的指向地址,变得与module.exports 的指向不一致。而require()导入的对象是以module.exports 存储的值或者地址值
看下面具体说明:
exports 和 module.exports 的使用误区
const fn = () => {
console.log('this is a.fn')
}
console.log(exports === module.exports);
exports = { fn }
console.log(exports === module.exports);
console.log(exports);
console.log(module.exports);
输出:
true
false
{ fn: [Function: fn] }
{}
解释:
一开始,exports对象存储的地址是从module.exports存储的地址,所以他们存储的是同一个地址值。所以是判断相等为 true.
但是你给 exports 赋值另外一个变量或者值的时候,它就不再是指向和 module.exports 同一个地址,所以判断相等为 false
这个时候导出出去的对象就是module.exports 里面的空对象而非 exports 了
看以下module.exports 该变为一个值类型会如何
const a = 100
module.exports = a
console.log(module.exports);
console.log(exports);
输出
100
{}
nodejs 遵循了 CommonJS 模块化规范,CommonJS 规定了模块的特性和各模块之间如何相互依赖
CommonJS 规定:
每个模块内部,module 变量代表当前模块
module 变量是一个对象, 它的exports 属性是对外的接口。就是说exports 实际上是一个键
require加载某个模块,其实就是加载该模块的 module.exports 属性。
nodejs 中的第三方模块又叫做 包
第三方模块和包指的是同一个概念,只不过叫法不同
不同于 node.js 中的内置模块与自定义模块,包是由第三方个人或团队开发出来的,免费供所有人使用
由于 nodejs 的内置模块仅提供了一些底层 API, 导致在基于内置模块进行项目开发时,效率很低。
包是基于内置模块封装出来的,提供了更高级、更方便的 API, 极大的提高了开发效率。
搜包:npm.Inc 公司旗下的著名网站:www.npmjs.com, 是全球最大的包共享平台。找包需要耐心
下载包:npm公司有一个地址为 http://registry.npmjs.org/的服务器,对外共享所有的包。我们可以在这里下载
Node Package Manager (npm 包管理工具),用这个从上面哪个服务器把包下载到本地使用。
这个包管理工具会随着 Node.js 的安装包一起被安装到 本地。
npm -v
命令看npm 包管理工具版本号
npm install 包名
简写: npm i 包名
导入包
require('包名')
与安装时的包名一致
包名具有唯一性。所以安装包和发布包都是以包名为准
初次装包完成后,在项目文件夹下多了 node_modules 的文件夹和 package-lock.json 的配置文件
默认安装最新版本,用@符号来指定版本
npm i 包名@版本号
会覆盖之前安装的包,包名的一致的会覆盖安装
包的版本号是以 “点分十进制” 形式进行定义的,总共有三位数字,例如2.24.0
其中每一位数字所代表的含义如下:
版本号提升规则:只要前面的版本号增长了,则后面的版本号为零
npm 规定,在项目根目录中,必须提供一个叫做 package.json 的配置文件。
用来记录与项目有关的配置信息。
第三方的包体积过大,不方便团队成员之间共享代码。
在项目的根目录中,创建一个叫做 package.json 的配置文件,用来记录项目中安装了哪些包以及版本。
删除 node_modules 后,团队成员之间共享代码。把 node_modules 文件夹添加到 .gitignore
npm 包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,快速创建 package.json 配置文件
npm init -y
注意:
使用的时候再通过npm install
即可下载 package.json 中所有记录的包
npm uninstall 包名
同时package.json 和 package-lock.json 中的该包的信息也会删除
如果某些包只在项目开发阶段会用到,再项目上线后不会用到。则把这些包记录到 devDependencies 节点中。
如果某些包在开发和项目上线之后都需要用到,则把这些包记录到 dependencies 节点中
npm install 包名 --save-dev
简写npm i 包名 -D
怎么判断该写到哪个节点呢?
安装的那个包的文档里一般会说明
把 npm 的下包镜像源改到国内
镜像源就是下包的服务器地址
# 查看当前的下包镜像源
npm config get registry
# 将下包的镜像源改到国内
npm config set registry=国内地址
# 检查镜像源是否下载成功
npm config get registry
全局安装 nrm 这个小工具,利用 nrm 提供的终端命令,可以快速查看和切换下包的镜像源
npm i nrm -g
# 查看所有可用的镜像源
nrm ls
# 将下包的镜像源切换为 taobao 镜像
nrm use taobao
使用 npm 包管理工具下载的包,共分为两大类,分别是
项目包
那些 安装到项目的 node_modules 目录中的包,都是项目包
项目包又分为两类,分别是:
全局包
在执行npm install 命令时,如果提供了 -g 参数,则会把包安装为全局包
全局安装包安装在我一个找不到的目录下,不很重要,留个疑问
注意:
i5ting_toc 是一个可以把 md 文档转为 html 页面的小工具,使用
npm i -g i5ting_toc
# 调用 i5ting_toc
i5ting_toc -f 要转化的md文件路径 -o
规范的包结构必须符合以下三点要求
事实证明,包可以套娃,我可以在包里又引用别的包或者包的部分功能函数
格式化日期
// 1.导入自己的包
const tmk = require('tmk')
// 功能1:格式化日期
const date = tmk.dateFormat(new Date())
// 输出 2022-03-23 18:42:45
console.log(date)
转义 HTML 中的特殊字符
// 导入自己的包
const tmk = require('tmk')
// 转义 HTML 中的特殊字符
const htmlStr = '你好!©小黄!
'
const str = tmk.htmlEscape(htmlStr)
/* <h1 style="color: red;">你好!©<span>小黄!</span></h1>*/
console.log(str)
还原 HTML 中的特殊字符
// 导入自己的包
const tmk = require('tmk')
// 还原 HTML 中的特殊字符
const rawHTML = tmk.htmlUmEscape(str)
// 输出你好! 小黄!
console.log(rawHTML)
包名具有唯一性
初始版本是1.0.0
描述会在网站被搜索时显示出来
license 许可协议相关内容
{
"name": "tmk-tool",
"version": "1.0.0",
"main": "index.js",
"description": "提供了格式化时间,转义和反转义HTML标签的功能",
"keywords": ["tmk","dateFormat","escape","unEscape"],
"license": "ISC"
}
包根目录的 README.md 文件,是包的使用说明文档。我们可以把包的使用说明,以 markdown 的格式写出来,给用户参考。
README 文件中具体写什么内容,没有强制性的要求;只要能够清晰的把包的作用、用法、注意事项描述清楚就可以了。
我们创建的这个包的 README.md 文档中,会包含以下6项内容:
安装方式、导入方式、格式化时间、转义 html 中的特殊字符、还原 html 中的特殊字符、开源协议
npmjs.com 注册账号
终端 登录
命令 npm login
注意:登录前 必须要把镜像源改为官方服务器,否则会登录失败
发布包
在包的根目录下运行 npm publish
命令,即可发布包
删除已经发布的包
运行 npm unpublish 包名 --force
命令, 删除已经发布的包
注意:
npm unpublish
命令只能删除 72 小时以内发布的包npm unpublish
删除的包,在24小时以内不允许重复发布模块在第一次加载之后会被缓存。
内置模块的加载优先级最高
例如,就算在 node_modules 下又第三方模块也叫做 fs, require(‘fs’) 返回的也是内置 fs 模块
使用require() 加载自定义模块时,必须指定以./ 或者…/ 开头的路径标识符。
在加载自定义模块时,如果没有以上路径标识符,则node 会把它当作内置模块或者第三方模块进行加载
同时,在加载自定义模块时,如果省略了文件的扩展名,则 node 会依次尝试加载以下文件
code: 'MODULE_NOT_FOUND',
自定义模块不会自动往父级目录去查找,只会在本级目录查找,然后依次进行文件扩展名补全,没有就报错
如果传递给 require() 的模块标识符不是一个内置模块,也没有 ./ 或者 …/ 开头,则 node.js 会从当前模块的父目录的 /node_modules 文件夹中加载第三方模块
如果没有找到对应的第三方模块,则移动到上一层目录的/node_modules中,进行加载,直到文件系统的根目录的/node_modules。
当把目录作为模块标识符,传递给 require() 进行加载的时候,有三种加载方式:
'MODULE_NOT_FOUND'
express 的作用和node.js 内置的 http 模块类似,是专门来创建 Web 服务器的
本质:就是一个 npm 的第三方包,提供了快熟创建 Web 服务器的边界方法
地址: http://www.expressjs.com.cn
不使用 express 能否创建 Web 服务器?
能, 使用原生的 http 模块就可以创建一个服务器
内置的 http 模块用起来很复杂,开发效率低;express 是基于内置的 http 模块就进一步封装出来的,能够极大的提高开发效率
对于前端程序员,最常见的两种服务器,分别是:
使用 express,可以方便、快速的创建 Web 网站服务器 或者API 的接口服务器
npm i express@4.17.1
np
通过express 实例.get() 方法,监听客户端的GET请求
语法格式:
app.get('请求url',(req, res)=>{//处理函数})
通过 express的实例.post() 方法,监听客户端的 post 请求
语法格式:
app.post('请求url'(req, res)=>{//处理函数})
res.send() 方法
app.get('/user', (req, res) => {
res.send( { name:'zs' } )
})
app.post('/user', (req, res) => {
res.send('请求成功')
})
通过 req.query 对象,可以获取到客户端通过查询字符串的形式,发送到服务器的参数。会以一个对象的形式封装起来
语法格式:
app.get('/',(req, res) => {
// req.query 默认是一个空对象 {}
// 输入 http://localhost:80?name=zs&age=20
// 输出 { name: 'zs', age: '20' }
console.log(req.query)
})
通过 req.params 对象,可以访问 URL 中,通过: 匹配到的动态参数
语法格式:
app.get('/user/:id', (req, res) => {
// req.params 默认是一个空对象
// 里面存放着通过 : 动态匹配到的参数值
// 访问 http://localhost:80/user/1
// 输出 { id: '1' }
console.log(req.params)
})
多个动态参数使用
app.get('/user/:id/:name', (req, res) => {
// 输入 http://localhost:80/user/3/zs
// 输出 { id: '3', name: 'zs' }
console.log(req.params)
})
express 提供了一个函数,叫做express.static(),通过它,我们可以非常方便的创建一个静态资源服务器
例如,通过如下代码就可以将 public 目录下的图片、css文件、 JavaScript 文件对外开访问了
app.use(express.static('public'))
现在,你就可以访问 public 目录中的所有文件了:
例如:http://localhost:3000/images/bj.jps
注意: express 在指定的静态目录中查找文件,并对外提供资源访问路径。因此,static 那个目录名不会出现在 url 中
如果要托管多个静态文件目录里面的文件
app.use(express.static('public'))
app.use(express.static('static'))
访问时,express.static() 函数会根据目录的添加顺序查找所需的文件
如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下的方式:
app.use('/public', express.static('public'))
现在,你就可以通过带有/public 前缀地址来访问 public 目录中的文件了
http://localhost:3000/public/images/bj.jpg
注意:这个 / 必须要有,否则不能识别到
这个 public 不一定要与文件夹名相同,只是拿这个前缀名和这个要公开的文件夹名对应起来而已
使用 nodemon 这个工具,它能够监听项目文件的变动。当代码被修改后, nodemon 会自动帮我们重启项目,极大方便了开发和调试。
在终端中,运行如下命令,即可将 nodemon 安装为全局可用的工具
npm install -g nodemon
我们可以将 node 命令 替换为 nodemon 命令,使用 nodemon app.js 来启动项目。
这样做以后,代码被修改之后,会被 nodemon 监听到,从而实现自动重启项目的效果。
广义上来讲,路由就是映射关系。
在 express 中,路由指的是 客户端的请求 与 服务器处理函数 之间的映射关系
express 中的路由分 3 部分组成,分别是请求的类型(get、post)、请求的 url 地址,处理函数
app.METHOD(PATH, handler)
express 中路由的例子
app.get('/', () => {
res.send('这是url为 / 的get请求的路由')
})
app.post('/', () => {
res.send('这是url为 / 的post请求的路由')
})
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配时,会按照代码写路由从上到下的顺序进行匹配,如果请求类型和请求的 url 同时匹配成功,则 express 会将这次请求,转发给对应的函数处理
注意:
const express = require('express')
const app = express()
// 挂载路由
app.get('/', (req, res) => { res.send() })
app.post('/', (req, res) => { res.send() })
app.listen(80, ()=>{})
模块化路由
为了方便对路由进行模块化的管理, 建议将路由抽离为单独的模块
步骤如下:
创建路由模块对应的 .js 文件
调用 express.Router() 函数创建路由对象
向路由对象上挂载具体的路由
使用 module.exports 向外共享路由对象
使用 app.use() 函数注册路由模块
var express = require('express')
var router = express.Router()
router.get('/', ()=>{})
router.post('/', ()=>{})
module.exports = router
注意:app.use() 函数的作用,就是来注册全局中间件
为路由模块添加前缀
const userRouter = require('./router')
// 为 userRouter 添加前缀
app.use('/api',userRouter)
有输入与输出的 中间环节
const mw = (req, res, next) => {
console.log('这是一个中间件函数')
// 当前中间件的业务处理完毕后,必须调用 next() 函数
// 表示把流转关系转交给下一个中间件或路由
next()
}
客户端发起任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。
通过调用 app.use(中间件函数),即可定义一个全局生效的中间件
const mw = (req, res, next) => {
console.log('这是一个中间件函数')
next()
}
// 全局生效的中间件
app.use(mw)
这个 use 必须写在路由函数的前面,否则起不到作用。到它 next 时候路由已经匹配完了,所以它流转不到路由。
app.use((req, res, next)=>{
console.log()
next
})
多个中间件之间,用的是同一个 req 和 res。于此,我们可以在上有的中间件中,统一为 req 或 res 对象添加自定义的属性或者方法,供下游的中间件或路由进行使用。
例如,返回请求到达服务器的时间
app.use((req, res, next) => {
req.time = Date.now()
next()
})
app.get('/', (req, res)=>{
res.send('到达服务器的时间:'+req.time)
})
app.post('/',(req, res) => {
res.send('到达服务器的时间:'+req.time)
})
可以使用 app.use() 连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序以此进行调用。
app.use((req, res, next) => {
console.log('调用了第一个全局中间件')
})
app.use((req, res, next) => {
console.log('调用了第二个全局中间件')
})
不使用 app.use() 定义的中间件,叫做局部生效的中间件
const mw = (req, res, next) => {
console.log('这是中间件函数')
next()
}
// mw 这个中间件只在当前路由中生效,这种用法属于 局部生效的中间件
app.get('/', mw, (req, res) => {
})
可以在路由中,通过如下两种 等价 的方式,使用多个局部组件
app.get('/', mw1, mw2, (req, res) => {})
app.get('/', [mw1, mw2], (req, res) => {})
express 官方把常见的中间件用法, 分成了 5 大类, 分别是:
应用级别的中间件
通过 app.use() 或 app.get() 或 app.post(),绑定到 app 实例上的中间件,叫做应用级别的中间件。
就是前文中的全局中间件和局部中间件
路由级别的中间件
绑定到 express.Router() 实例上的中间件,叫做路由级别的中间件。
它的用法和应用级别中间件没有区别。只不过,应用级别的中间件是绑定到 app 实例上,路由级别的中间件绑定到 router 实例上。代码如下
const router = express.Router()
const mw1 = (req, res, next) => {
next()
}
const mw2 = (req, res, next) => {
next()
}
router.get('/',[mw1, mw2], (req, res)=>{
res.send('ok')
})
错误级别的中间件
作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
格式:错误级别中间件的处理函数中,必须有 4 个形参,分别是 (err, req, res, next )
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)
next()
})
注意:错误级别中间件,必须注册在所有路由之后。才能捕获到前面发生了的错误。否则在前面是不能捕获到后面发生的错误的。
express 内置的中间件
express 内置了 3 个常用的中间件
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 }))
使用案例:
// 注意:除了错误级别的中间件,其它中间件必须在路由之前进行配置
app.use(express.urlencoded({ extended: false }))
app.post('/', (req, res) => {
// 在服务器,可以使用 req.body 这个属性,来接收客户端发送过来的请求体数据
// 如果不配置解析表单数据的中间件,则 req.body 默认等于 undefined
console.log(req.body);
res.send('ok')
})
第三方的中间件
由第三方开发出来的中间件叫做第三方中间件。在项目中,需要下载、导入、注册使用中间件
例如:使用 body-parser 这个中间件
需求描述
手动模拟一个类似于 express.unlencoded 这样的中间件,来解析 post 提交到服务器的表单数据.
实现步骤:
定义中间件
监听 req 的 data 事件
在中间件,需要监听 req 对象的 data 事件,来获取客户端发送到服务器的数据。
如果数据量过大,无法一次性发送完毕,则客户端会把数据切割后,分批发送到服务器。所以 data 事件可能会触发多次,每次触发 data 事件时,获得到数据只是完整数据的一部分,需要手动对接收到的数据进行拼接。
let str = ''
req.on('data', (chunk) => {
str += chunk
})
监听 req 的 end 事件
当请求体数据接收完毕之后,会自动触发 req 的 end 事件。
因此,我们可以在 req 的 end 事件中,拿到并处理完整的请求体数据
req.on('end',() => {
console.log(str)
})
使用 querystring 模块解析请求体数据
nodejs 内置了一个 querystring 模块,专门用来处理查询字符串。通过这个模块提供的 parse() 函数,可以轻松的把查询字符串,解析成对象的格式。(这个函数只能解析url编码即urlencoded而不能解析json编码,json 编码中的空格符、换行符、tab符等统统都会解析错误)
const qs = require('querystring')
// 调用 parse() 方法,把查询字符串解析为对象
const body = qs.parse(str)
将解析出来的数据对象挂载为 req.body
将自定义的中间件封装为模块
const bodyParser = (req, res, next) => {
}
module.exports = bodyParser
使用:
// 导入
const bodyParser = require('bodyParser')
// 注册
app.use(bodyParser)
// 局部注册
app.get('/', bodyParser, (req, res) => {})
创建基本的接口
创建 api 路由模块
// apiRouter 路由模块
const express = require('express')
const apiRouter = express.Router()
module.exports = apiRouter
使用:
const apiRouter = require('./apiRouter.js')
app.use('/api,apiRouter')
编写 get 接口
apiRouter.get('/get',(req, res) => {
// 获取到客户端通过查询字符串,发送到服务器的数据
const query = req.query
// 调用 res.send() 方法,把数据响应给客户端
res.send({
status: 0,
msg: 'GET请求成功',
data:query
})
})
注意:同样是获取参数, req.params,req.query,req.body的用法区别
req.params 是用来解析匹配动态参数的,解析为对象
http://localhost:80/api/get/1
apiRouter.get('/get/:id',()=>{})
req.query 是解析字符串的,即把?后面的内容解析为对象
http://localhost:80/api/get?id=1
apiRouter.get('/get',()=>{})
req.body 是用来挂载解析完的 post数据的,如果获取的是urlencoded 格式的请求体数据,必须配置中间件app.use(express.urlencoded({extend:false}),它会把数据解析出来,挂载在 req.body 上
编写 POST 接口
apiRouter.post('/post', (req, res) => {
// 获取客户端通过请求体,发送到服务器的 URL-encoded 数据
const body = req.body
// 调用 res.send() 方法,把数据响应给客户端
res.send({
status: 0,
msg: 'POST请求成功',
data: body
})
})
上文编写的 GET 和 POST 接口,存在一个很严重的问题: 不支持跨域请求。
解决接口跨域问题的方案主要有两种:
cors 是 express 的一个第三方中间件。通过安装和配置 cprs 中间件,可以很方便的解决跨域问题。
使用步骤分以下 3 步:
CORS (Cross-Origin Resource Sharing, 跨域资源共享) 由一系列 HTTP 响应头组成, 这些 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源。
跨域资源返回的时候会被浏览器拦截。
响应头部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:
Access-Control-Allow-Origin: | *
其中,origin 参数的指定了允许访问改资源的外域 URL
*
代表通配符
例如,下面的字段值将只允许来自 http://itcast.cn 的请求
res.setHeader('Access-Control-Allow-Origin', 'http://itcast.cn')
默认情况下,cors 仅支持客户端向服务器发送如下的 9 个请求头:
如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败!
// 允许客户端额外向服务器发送 Content-Type 请求头和 X-Custom-Header 请求头
// 注意: 多个请求头之间使用英文逗号进行分割
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')
默认情况下,cors 仅支持客户端发起 GET、POST、HEAD 请求
如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Allow-Methods 来指明实际请求所允许使用的 HTTP 方法
// 只允许 POST、GET、DELETE、HEAD 请求方法
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD')
// 允许所有的 HTTP 方法
res.setHeader('Access-Control-Allow-Methods', '*')
客户端在请求 CORS 接口时,根据请求方式和请求头的不同,可以将 cors 的请求分为两大类,分别是:
简单请求
同时满足以下两大条件的请求,就属于简单请求
预检请求
只要符合以下任何一个条件的请求,都需要进行徐建请求:
在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为 “预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
简单请求的特点: 客户端与服务器之间只会发生一次请求。
预检请求的特点:客户端与服务器之间会发生两次请求, OPTION 预检请求成功之后,才会发起真正的请求。
数据库(database)是用来组织、存储和管理数据的仓库
为了方便管理互联网世界中的数据,就有了数据库管理系统的概念(简称:数据库)。用户可以对数据库中的数据进行新增、查询、更新、删除等操作。
在传统型数据库中,数据的组织结构分为数据库(database)、数据表(table)、数据行(row)、字段(field)这 4 大部分组成
和excel表类似
SQL(structured query language)是结构化查询语言,专门用来访问和处理数据库的变成语言。能够让我们以编程的形式,操作数据库里面的数据。
三个关键点:
重点掌握如何使用 SQL 从数据表中:
查询数据(select)、插入数据(insert into)、更新数据(update)、删除(delete)
额外需要掌握 4 种 SQL 语法:
where 条件、 and 和 or 运算符、order by 排序、count(*)函数
select 语句用于从表中查询数据。执行的结果被储存在一个结果表中(称为结果集)。语法格式如下:
-- 从 FROM 指定的表中,查询出 所有的数据。* 表示所有列
SELECT * FROM 表名称
-- 从 FROM 指定的表中,查询出指定的列名称(字段)的数据
SELECT 列名称 FROM 表名称
SQL 语句中关键字对大小写 不敏感。但是表名列名大小写是敏感
注释是:–空格
INSERT INTO 语句用于向表中插入新的数据行,语法格式:
-- 语法解读:向指定的表中,插入如下激烈数据,列的值通过 values 一一指定
-- 注意: 列和值要一一对应,多个列和多个值之间,使用英文逗号分隔
INSERT INTO table_name(列1,列2,···) VALUES('值1', '值2',···)
update 语句用于修改表中的数据,语法格式:
-- 语法解读:
-- 用 UPDATE 指定要更新哪个表中的数据
-- 用 SET 指定列对应的新值
-- 用 WHERE 指定更新的条件
UPDATE 表名称 SET 列名称 = '新值',列名称 = '新值' WHERE 列名称 = '某值'
符合 where 条件数据行的列会被设置为新值
DELETE 语句用于删除表中的行。语法格式如下:
DELETE FROM 表名称 WHERE 列名称 = '值'
WHERE 子句用于限定选择的标准。在 SELECT/UPDATE/DELETE 语句中,皆可使用 WHERE 子句来限定选择的标准。
SELECT 列名称 FROM 表名称 WHERE 列 运算符 值
where 之前称为主语句
操作符 | 描述 |
---|---|
= | 等于 |
<>和!= | 不等于 |
> | 大于 |
< | 小于 |
>= | 大于等于 |
<= | 小于等于 |
BETWEEN | 在某个范围内 |
LIKE | 搜索某种模式 |
AND 和 OR 可在 WHERE 子语句中把两个或多个条件结合起来
AND 表示同时同时满足多个条件 相当于与运算符
OR 表示只要满足两个条件中的一个条件即可,相当于 或 运算符
ORDER BY 语句用于根据指定的列对结果集进行排序
ORDER BY 语句默认按照升序对记录进行排序,关键字是ASC
如果希望按照降序对记录进行排序, 则可以使用 DESC 关键字
语法:
SELECT 列名称 FROM 表名称 ORDER BY 列名称 DESC|ASC
对于先按一个字段进行排列,在按另外一个字段进行排列。中间用逗号分隔
SELECT 列名称 FROM 表名称 ORDER BY 列名称 DESC|ASC ,列名称 DESC|ASC
COUNT(*) 函数用于返回查询结果的总数据条
SELECT COUNT(*) FROM 表名称
查询user表中 status 为0的总数据条数:
SELECT COUNT(*) FROM users WHERE status = 0
给查询出来的列名称设置别名,可以设置 AS 关键字,示例如下:
SELECT COUNT(*) AS total FROM users WHERE status = 0
设置是暂时的,不会被保存
mysql 模块是托管于 npm 上的第三方模块。它提供了在 Node.js 项目中连接和操作 MySQL 数据库的能力。
npm install mysql
在使用 mysql 模块操作 MySQL 数据库之前,必须先对 mysql 模块进行必要的配置,主要的配置步骤如下:
// 导入 mysql 模块
const mysql = require('mysql')
// 建立与 MySQL 数据库进行连接
const db = mysql.createPool({
host: '', // 数据库的 IP 地址
user: '', // 登录数据库的账号
password: '', // 登录数据库的密码
database: '' // 指定要操作哪个数据库
})
调用 db.query() 函数,指定要执行的 SQL 语句,通过回调函数拿到执行的结果:
// 检测 mysql 模块能否正常工作
db.query('SELECT 1', (err, results) => {
if (err) return console.log(err.message)
// 只要能打印出 [ RowDataPacket { '1': 1 } ] 的结果,就证明数据库连接正常
console.log(results)
})
// 查询 account 表中的所有的用户数据
db.query('SELECT * FROM account', (err, results) => {
// 查询失败
if (err) return console.log(err.message)
// 查询成功
console.log(results)
})
如果执行的是 SELECT 语句,则返回的是一个数组
向 account 表中新增数据,其中 username 为 zs, password 为 123456. 示例代码
const user = { username: 'zs', password: '123456', roleid: '0'}
// 待执行的 SQL 语句,其中英文的 ? 为占位符
const sqlStr = 'INSERT INTO account (username, password, roleid) VALUES (?, ?, ?)'
// 使用数组的形式,依次为 ? 占位符指定具体的值
db.query(sqlStr, [user.username, user.password, user.roleid], (err, results) => {
if (err) return console.log(err.message)
if(results.affectedRows === 1) {console.log('插入数据成功')}
})
如果执行的是 INSERT INTO 语句,则返回的是一个对象
更新表中username=‘zs’的行的密码为’new’
// 要更新的数据对象
const user = { username: 'zs', password: 'new'}
const sqlStr = 'UPDATE account SET password=? WHERE username=?'
db.query(sqlStr, [user.password,user.username], (err, results) => {
if (err) return console.log(err.message)
if(results.affectedRows === 1) {console.log('更新数据成功')}
})
删除 username = ‘zs’ 的行
const sqlStr = 'DELETE FROM account WHERE username=?'
// 如果 sql 语句中有多个占位符,则必须使用数组为每个占位符从先到后指定具体的值
// 如果只有一个,则可以直接使用数据
db.query(sqlStr, 'zs', (err, results) => {
if (err) return console.log(err.message)
if(results.affectedRows === 1) {console.log('删除数据成功')}
})
DELETE 语句返回的也是一个对象
使用 DELETE 语句,会把真正的数据删除。为了保险起见,使用标记删除,来模拟删除的动作。
所谓 标记删除,就是设置一个 status 状态字段,来标记当前这条数据是否被删除。
当用户执行了删除动作,我们可以执行 UPDATE 语句, 将这条数据的 status 字段标记为删除即可。
目前主流的开发模式分为两种,分别是:
服务端渲染概念: 服务器发送给客户端的 HTML页面,是在服务端通过字符串的拼接,动态生成的。因此客户端不需要使用 Ajax 这样的技术额外的请求页面的数据
apiRouter.get('/index.html', (req, res) => {
// 要渲染的数据
const user = { name: 'zs', age: 20}
// 服务器端通过字符串的拼接,动态生成 HTML 内容
const html = `姓名: ${user.name},年龄: ${user.age}
`
// 把生成好的页面内容响应给客户端。客户端拿到的是带有真实数据的 HTML 页面
res.send(html)
})
优点:
缺点:
概念:前后端分离的开发模式,就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式。
优点:
开发体验好。前端专注于 UI 页面的开发,后端专注于 api 的开发,让前端有了更多的选择性。
用户体验好。Ajax 技术的广泛应用,极大的提高了用户的体验,可以实现页面的局部刷新
减轻了服务器端的渲染压力。因为页面最终是在每个用户的浏览器中生成的。
缺点:
具体使用何种开发模式并不绝对,为了同时兼顾首页渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器端渲染 + 其它页面前后端分离的开发模式
身份认证(Authentication) 又称 “身份验证”、“鉴权”,指通过一定的手段,完成对用户身份的确认。
确认当前所声称为某种身份的用户,确实是所声称的用户。
对于服务端渲染 和前后端分离这两种开发模式,有不同的身份认证方案:
指的是客户端每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态。
那么在多次请求之间,就需要客户端主动告诉服务器。自己是上次请求的哪一个用户。
通过 服务器 给客户端在注册的时候生成 cookie。然后客户端在请求的时候带上这个cookie。就等于是告诉服务器我是注册的谁了
cookie 是存储在用户浏览器中一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value) 和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成。
键=值; 用英文分号隔开各个cookie值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ov6KfUrS-1654486520756)(%E9%BB%91%E9%A9%AC%E7%A8%8B%E5%BA%8F%E5%91%98nodejs%20%E5%85%A5%E9%97%A8.assets/1651031770747.png)]
不同域名下的 Cookie 各自独立,不能互相访问。每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器。
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端身份。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PHuzAuTX-1654486520756)(%E9%BB%91%E9%A9%AC%E7%A8%8B%E5%BA%8F%E5%91%98nodejs%20%E5%85%A5%E9%97%A8.assets/1651034361366.png)]
npm i express-session
express-session 中间件安装成功之后,需要通过 app.use() 来注册 session 中间件。
// 导入 session 中间件
const session = require('express-session')
// 配置 Session 中间件
app.use(session({
secret: 'keyboard cat', // secret 属性的值可以为任意字符串
resave: false, // 固定写法
saveUninitialized: true // 固定写法
}))
当 express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息:
app.post('/login',(req, res) => {
// 判断用户提交的登录信息是否正确
if (req.body.username !== 'admin' || req.body.password !== '123456') {
return res.send({ status: 1, msg: '登录失败'})
}
// 只有成功配置了 express-session 这个中间件之后,req里面才会有 session 属性对象
req.session.user = req.body // 将用户的信息,存储到 Session 中
req.session.islogin = true
res.send({ status: 0, msg: '登录成功'})
})
可以通过req.session.user.username
直接拿取到数据
req.session.对象.键名
app.get('/username', (req, res) => {
if(!req.session.islogin) {
return res.send({status: 1, msg: '获取失败'})
}
res.send({
status: 0,
msg: '获取成功',
username: req.session.user.username
})
})
调用 req.session.destory() 函数,即可清空服务器保存的 session 信息
app.post('/logout', (req, res) => {
req.session.destroy()
res.send({
status: 0,
msg: '退出登录成功'
})
})
只会清空当前用户的 session.因为相当于是每一个 HTML 访问都会执行一边上述代码。
注意:form表单中的 button 和 submit 类型的 input 会有默认的提交行为。会通过url?的方式提交给 action 规定的页面。如果action没有设置,则会提交给本页面的url地址。
可以通过设置onclick事件组织其默认行为,代码如下:
btn.onclick = (e) => {
e.preventDefault()
}
form表单post请求的是content-type 格式是 application/x-www-form-urlencoded
使用axios发送post请求时,如果content-type为application/json,那么请求体中数据部分必须是一个json对象 。如果需要提交 content-type为application/x-www-form-urlencoded或multipart/form-data时,请求体中数据必须是formdata格式。代码如下:
let params = new FormData()
params.append('file', this.file)
params.append('id', localStorage.getItem('userID'))
axios.post(URL, params, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then(res => {
if (res.data.code === 0) {
this.$router.go(-1)
}
}).catch(error => {
alert('更新用户数据失败' + error)
})
如何解析:
如果是 application/x-www-form-urlencoded 格式的数据
后端应使用
app.use(express.urlencoded({ extended: false }))
解析数据并会挂载在req.body 上
如果是 application/json 格式的数据
后端应使用
const bodyParser = require('body-parser')
app.use(bodyParser.json())
session的导入和配置必须在静态页面托管之前,然后使用静态页面托管的访问,否则无法发挥作用。
代码如下:
const session = require('express-session')
app.use(session({
secret: 'keyboard cat', // secret 属性的值可以为任意字符串
resave: false, // 固定写法
saveUninitialized: true // 固定写法
}))
app.use(express.static('../session'))
必须使用静态托管地址访问,session中间件才能get
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tIKRFar5-1654486520757)(%E9%BB%91%E9%A9%AC%E7%A8%8B%E5%BA%8F%E5%91%98nodejs%20%E5%85%A5%E9%97%A8.assets/1651390491269.png)]
Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
注意: