web前端开发学习路线图
https://www.bilibili.com/read/cv10431130/?from=articleDetail
什么是Nods
Node.js是一个基于Chrome V8引擎的Javascript运行环境
浏览器是JavaScript的前端运行环境
Node.js是JavaScript的后端运行环境
Node.js可以做什么
Node.js作为JavaScript的运行环境,仅仅提供了基础的功能和API。然而,基于Node.js提供的这些基础功能,很多强大的工具和框架如雨后春笋,层出不穷,所以学会了Node.js,可以让前端程序员胜任更多的工作和岗位:
基于Express框架(http://www.expressjs.com.cn/),可以快读构建Web应用
基于Electron框架(http://electronjs.org/),可以构建跨平台的桌面应用
基于restify框架(http://restify.com/),可以快速构建API接口项目
读写和操作数据库、创建使用的命令行工具辅助前端开发、etc……
查看已安装的Node.js的版本号
打开终端,在终端输入命令node -v后,按下回车键,即可查看已安装的Node.js的版本号
Windows系统快速打开终端的方式:
使用快捷键(Windows徽标键+R)打开运行面板,输入cmd后直接回车,即可打开终端
什么是终端
终端(英文:Terminal)是专门为开发人员设计的,用于实现人机交互的一种方式。
在Node.js环境中执行JavaScript代码
打开终端
输入node要执行的js文件的路径
终端中的快捷键
使用↑键,可以快速定位到上一次执行的命令
使用tab键,能够快速补全路径
使用esc键,能够快速清空当前已输入的命令
输入cls命令,能够快速清空当前已输入的命令
什么是fs文件系统模块
fs模块是Node.js官方提供的、用来操作文件的模块。它提供了一系列的方法和属性,用来满足用户对文件的操作需求。
fs.readFile()方法,用来读取指定文件中的内容
fs.writeFile()方法,用来向指定的文件中写入内容
判断文件是否读取成功
可以判断err对象是否为null,从而知晓文件读取的结果
判断文件是否写入成功
练习:整理成绩
fs模块-路径动态拼接的问题
在使用fs模块操作文件时,如果提供的操作路径是以./或…/开头的相对路径时,很容易出现路径动态拼接错误的问题
原因:代码在运行的时候,会以执行node命令时所处的目录,动态拼接出被操作文件的完整路径。
解决方案:在使用fs模块操作文件时,直接提供完整的路径,不要提供./或…/开头的相对路径,从而防止路径动态拼接的问题
什么是path路径模块
path模块是Node.js官方提供的、用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对路径的处理需求。
例如:
path.join()方法,用来将多个路径片段拼接成一个完整的路径字符串
path.basename()方法,用来从路径字符串中,将文件名解析出来
path.extname()方法,可以获取路径中的扩展名部分
path.join()
path.basename()
path.extname()
注意:
fs.writeFile()方法只能用来创建文件,不能用来创建路径
重复调用fs.wirteFile()写入同一个文件,新写入的内容会覆盖之前的旧内容
什么是客户端、什么是服务器
在网络节点中,负责消费资源的电脑,叫做客户端;负责对外提供网络资源的电脑,叫做服务器。
什么是http模块
http模块是Node.js官方提供的、用来创建web服务器的模块。通过http模块提供的http.createServer()方法,就能方便的把一台普通的电脑,变成一台Web服务器,从而对外提供Web资源服务。
进一步理解http模块的作用
服务器和普通电脑的区别在于,服务器上安装了web服务器软件,例如:IIS、Apache等。通过安装这些服务器软件,就能把一台普通的电脑变成一台web服务器。
IP地址
IP地址就是互联网上每台计算机的唯一地址,因为IP地址具有唯一性。如果把“个人电脑”比作“一台电话”,那么IP地址就相当于电话号码,只有在知道对方IP地址的前提下,才能与对应的电脑之间进行数据通信。
IP地址的格式:通常用“点分十进制”表示(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例如:用点分十进制表示的IP地址(192.168.1.1)
1.互联网中每台Web服务器,都有自己的IP地址,例如:大家可以在Windows的终端中运行ping www.baidu.com命令,即可查看到百度服务器的IP地址。
2.在开发期间,自己的电脑既是一台服务器,也是一个客户端,为了方便测试,可以在自己的浏览器中输入127.0.0.1这个IP地址,就能把自己的电脑当做一台服务器进行访问了。
域名和域名服务器
尽管IP地址能够唯一地标记网络上的计算机,但IP地址是一长串数字,不直观,而且不便于记忆,于是人们又发明了另一套字符型的地址方案,即所谓的域名(Domain Name)地址。
IP地址和域名是一一对应的关系,这份对应关系存放在一种叫做域名服务器(DNS,Domain 那么server)的电脑中,使用者只需通过好记的域名访问对应的服务器即可,对应的转换工作由域名服务器实现。因此,域名服务器就是提供IP地址和域名之间的转换服务器的服务器。
注意:
1.单纯使用IP地址,互联网中的电脑也能够正常工作,但是有了域名的加持,能让互联网的世界变得更加方便。
2.在开发测试期间,127.0.0.1对应的域名是localhost,它们都代表我们直接的这台电脑,在使用效果上没有任何区别。
端口号
计算机中的端口号,就好像是现实生活中的门牌号一样。通过门牌号,外卖小哥可以在整栋大楼众多的房间中,准确把外卖送到你的手中。
同样的道理,在一台电脑中,可以运行成百上千个web服务。每个web服务都对应一个唯一的端口号。客户端发送过来的网络请求,通过端口号,可以被准确地交给对应的web服务进行处理。
1.每个端口号不能同时被多个web服务占用
2.在实际应用中,URL中的80端口可以被省略
创建web服务器的基本步骤
导入http模块
创建web服务器实例
为服务器实例绑定request事件,监听客户端的请求
启动服务器
req请求对象
只要服务器接收到了客户端的请求,就会调用通过server.on()为服务器绑定的request事件处理函数。如果箱子啊时间处理函数中,访问与客户端相关的数据或属性,可以使用如下的方式:
res响应对象
在服务器的request事件处理函数中,如果想访问与服务器相关的数据或属性,可以使用如下的方式:
解决中文乱码问题
当调用res.end()方法,向客户端发送中文内容的时候,会出现乱码问题,此时,需要手动设置内容的编码格式:
res.setHeader(‘Content-Type’,‘text/html;charset=utf8’)
根据不同url响应不同的html内容
时钟web服务器案例
const http = require("http");
const fs = require("fs");
const path = require("path");
const server = http.createServer();
server.on("request", (req, res) => {
const url=req.url
const fpath=path.join(__dirname,url)
fs.readFile(fpath,'utf8',(err,dataStr)=>{
if(err){
return res.end('404 Not Found')
}else{
return res.end(dataStr)
}
})
});
server.listen(80, () => {
console.log("server listen at http://127.0.0.1");
});
优化资源的请求路径
const http = require('http')
const fs=require('fs')
const path=require('path')
const server=http.createServer()
server.on('request',(req,res)=>{
const url=req.url
// const fpath=path.join(__dirname,url)
let fpath=''
if(url=='/'){
fpath=path.join(__dirname,'/clock/index.html')
}else{
fpath=path.join(__dirname,'/clock',url)
}
fs.readFile(fpath,'utf8',(err,dataStr)=>{
if(err) return res.end('404 Not Found!')
res.end(dataStr)
})
})
server.listen(80,()=>{
console.log('server is running')
})
什么是模块化
模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元。
变成领域中的模块化
变成领域中的模块化,就是遵守固定的规则,把一个大文件拆成独立并互相依赖 的多个小模块。
把代码进行模块化拆分的好处
提高了代码的复用性
提高了代码的可维护性
可以实现按需加载
模块化规范
模块化规范就是对代码进行模块化拆分与组合时,需要遵守的那些规则。
模块化规范的好处:大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用,利人利己。
Node.js中模块的分类
Node.js中根据模块来源的不同,将模块分为了3大类,分别是:
内置模块(内置模块是由Node.js官方提供的,例如fs、path、http等)
自定义模块(用户创建的每个.js文件,都是自定义模块)
第三方模块(由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需要先下载)
加载模块
使用强大的require()方法,可以加载需要的内置模块、用户自定义模块、第三方模块进行使用。
注意:使用require()方法加载其它模块时,会执行被加载模块中的代码。
Node.js中的模块化
什么是模块作用域
和函数作用域相似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域。
模块作用域的好处
防止了全局变量污染的问题
module对象
在每个.js自定义模块中都有一个module对象,它里面存储了和当前模块有关的信息。
module.exports对象
在自定义模块中,可以使用module.exports对象,讲模块内的成员共享出去,供外界使用。
外界使用require()方法导入自定义模块时,得到的就是module.exports所指向的对象。
共享成员时的注意点
使用require()方法导入模块时,导入的结果,永远以module.exports指向的对象为准。
运行结果:
exports对象
由于module.exports单词写起来比较复杂,为了简化向外共享成员的代码,Node提供了exports对象。默认情况下,exports和module.exports指向同一个对象。最终共享的结果,还是以module.exports指向的对象为准。
exports和module.exports的使用误区
时刻谨记,require()模块时,得到的永远是module.exports所指向的对象
注意:为了防止混乱,建议大家不要在同一个模块中同时使用exports和module.exports
Node.js中的模块化规范
Node.js遵循了CommonJS模块化规范,CommonJS规定了模块的特性和各模块之间如何相互依赖。
CommonJS规定:
1.每个模块内部,module变量代表当前模块
2.module变量是一个对象,它的exports属性(即module.exports)是对外的接口。
3.加载某个模块,其实是加载该模块的module.exports属性。require()方法用于加载模块。
npm与包
什么是包?
Node.js中的第三方模块又叫做包。就像电脑和计算机指的是相同的东西,第三方模块和包指的是同一个概念,只不过叫法不同。
包的来源
不同于Node.js中的内置模块与自定义模块,包是由第三方个人或团队开发出来的,免费供所有人使用。
注意:Node.js中的包都是免费且开源的,不需要付费即可免费下载使用。
为什么需要包
由于Node.js的内置模块仅提供了一些底层的API,导致在基于内置模块进行项目开发时,效率很低。包是基于内置模块封装出来的,提供了更高级、更方便的API,极大的提高了开发效率。
包和内置模块之间的关系,类似于jQuery和浏览器内置API之间的关系。
从哪里下载包
国外有一家IT公司,叫做npm,Inc.这家公司旗下有一个非常著名的网站:https://www.npmjs.com,它是全球最大的包共享平台,你可以从这个网站上搜索到任何你需要的包,只要你有足够的耐心!
到目前为止,全球约1100多万的开发人员,通过这个包共享平台,开发并共享了超过120多万个包供我们使用。
npm -v 查看npm包管理工具的版本号
i5ting_toc
i5ting_toc是一个可以把md文档转为html页面的小工具,使用步骤如下:
发布包
登录npm账号
npm账号注册完成后,可以在终端中执行npm login命令,依次输入用户名、密码、邮箱后,即可登录成功。
// 切换到npm服务器
nrm use npm
// 登录到npm上
npm login
// 把包发布到npm上
npm publish
删除已发布的包
npm unpublish 包名 --force
// 注意:npm unpublish命令只能删除72小时以内发布的包
// npm unpublish删除的包,在24小时内不允许重复发布
// 发布包的时候要慎重,尽量不要往npm上发布没有意义的包!
模块的加载机制
优先从缓存中加载
模块在第一次加载后会被缓存。这也意味着多次调用require()不会导致模块的代码被执行多次。
注意:不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从换从中加载,从而提高模块的加载效率。
内置模块的加载机制
内置模块是由Node.js官方提供的模块,内置模块的加载优先级最高。
自定义模块的加载机制
使用require()加载自定义模块时,必须指定以…/或./开头的路径标识符。在加载自定义模块时,如果没有指定./或…/这样的路径标识符,则node会把它当做内置模块或第三方模块进行加载。
同时,在使用require()导入自定义模块时,如果省略了文件的扩展名,则Node.js会按顺序分别尝试加载以下的文件:
1.按照确切的文件名进行加载
2.补全.js扩展名进行加载
3.补全.json扩展名进行加载
4.补全.node扩展名进行加载
5.加载失败,终端报错
第三方模块的加载机制
如果传递给require()的模块标识符不是一个内置模块,也没有以./或…/开头,则Node.js会从当前模块的父目录开始,尝试从/node_modules文件夹中加载第三方模块。
如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
例如,假设在’C:\Users\iteheima\project\foo.js’文件里调用了require(‘tools’),则Node.js会按以下顺序查找:
- C:\Users\itheima\project\node_modules\tools
- C:\Users\itheima\node_modules\tools
- C:\Users\node_modules\tools
- C:\node_modules\tools
目录作为模块
当把目录作为模块标识符,传递给require()进行加载的时候,有三种加载方式: - 在被加载的目录下查找一个叫做package.json的文件,并寻找main属性,作为require()加载的入口
- 如果目录里面没有package.json文件,或者main入口不存在或无法解析,则Node.js将会试图加载目录下的index.js文件
- 如果以上两步都失败了,则Node.js会在终端打印错误消息,报告模块的缺失:Error:Cannot find module ‘xxx’
Express简介
什么是Express
官方给出的概念:Express是基于Node.js平台,快速、开放、极简的Web开发框架。
通俗的理解:Express的作用和Node.js内置的http模块类似。是专门用来创建Web服务器的。
Express的本质:就是一个npm上的第三方包,提供了快速创建Web服务器的便捷方法。
Express的中文官网:http://www.expressjs.com.cn/
进一步理解Express
思考:不使用Express能否创建Web服务器?
答案:能,使用Node.js提供的原生的http模块即可。
思考:既生瑜何生亮(有了http内置模块,为什么还要用Express)?
答案:http内置模块用起来很复杂,开发效率低;Express是基于内置的http模块进一步封装出来的,能够极大的提高开发效率。
思考:http内置模块与Express是什么关系?
答案:类似于浏览器中Web API与jQuery的关系。后者是基于前者进一步封装出来的。
Express能做什么
对于前端程序员来说,最常见的两种服务器,分别是:
Web网站服务器:专门对外提供Web网页资源的服务器。
API接口服务器:专门对外提供API接口
使用Express,我们可以方便、快速的创建Web网站的服务器或API接口的服务器。
Express的基本使用
安装express
npm i express@4.17.1
创建基本的web服务器
// 1.导入express
const express=require('express')
// 2.创建web服务器
const app=express()
// 3.启动web服务器
app.listen(80,()=>{
console.log('express server running at http://127.0.0.1')
})
监听GET请求
通过app.get()方法,可以监听客户端的GET请求,具体的语法格式如下:
app.get('请求url',function(req,res){/*处理函数*/})
监听POST请求
通过app.post()方法,可以监听客户端的POST请求,具体的语法格式如下:
app.post('请求url',function(req,res){/*处理函数*/})
把内容响应给客户端
通过res.send()方法,可以把处理好的内容,发送给客户端:
app.get('/user',(req,res)=>{
// 向客户端发送JSON对象
res.send({name:'zs',age:20,gender:'男' })
})
app.post('/user',(req,res)=>{
// 向客户端发送文本内容
res.send('请求成功')
})
完整代码如下:
// 1.导入express
const express = require("express");
// 2.创建web服务器
const app = express();
// 4.监听客户端的GET和POST请求并向客户端响应具体的内容
app.get('/user', (req, res) => [
// 调用express提供的res.send()方法,向客户端响应一个JSON对象
res.send({ name: "zs", age: 20, gender: "男" }),
]);
app.post('/user', (req, res) => {
// 调用express提供的res.send()方法,向客户端响应一个文本字符串
res.send("请求成功");
});
// 3.启动web服务器
app.listen(80, () => {
console.log("express server running at http://127.0.0.1");
});
获取URL中携带的查询参数
通过req.query对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数:
app.get('/',(req,res)=>{
// 通过req.query可以获取到客户端发送过来的查询参数
// 注意:默认情况下,req.query是一个空对象
console.log(req.query)
res.send(req.query)
})
获取URL中的动态参数
通过req.params对象,可以访问到URL中,通过:匹配到的动态参数:
// 注意:这里的:id是一个动态的参数
app.get('/user/:id',(req,res)=>{
// req.params是动态匹配到的URL参数,默认是一个空对象
console.log(req.params)
res.send(req.params)
})
托管静态资源
express.static()
express提供了一个非常好用的函数,叫做express.static(),通过它,我们可以非常方便地创建一个静态资源服务器,例如,通过如下代码就可以将pulic目录下的图片、CSS文件、JavaScript文件对外开放访问了:
const express=require('express')
const app=express()
app.use(express.static('./clock'))
app.listen(80,()=>{
console.log('express server running at http://127.0.0.1')
})
现在,你就可以访问public目录中的所有文件了:
http://localhost:3000/images/bg.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/login.js
注意:Express在指定的静态目录中共查找文件,并对外提供资源的访问路径。因此,存放静态文件的目录名不会出现在URL中。
托管多个静态资源目录
如果要托管多个静态资源目录,请多次调用express.static()函数:
app.use(express.static('public')
app.use(express.static('files')
访问静态资源文件时,express.static()函数会根据目录的添加顺序查找所需的文件。
挂载路径前缀
如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下的方式:
app.use('/public',express.static('public'))
现在,你就可以通过带有/public前缀地址来访问public目录中的文件了:
http://localhost:3000/public/images/bg.jpg
http://localhost:3000/public/css/style.css
http://localhost:3000/public/js/login.js
nodemon
安装nodemon
使用nodemon
路由的概念
什么是路由
广义上来讲,路由就是映射关系。
Express中的路由
在Express中,路由指的是客户端的请求与服务器处理函数之间的映射关系。
Express中的路由油3部分组成,分别是请求的类型、请求的URL地址、处理函数,格式如下:
app.METHOD(PATH,HANDLER)
Express中路由的例子
// 匹配GET请求,且请求URL为 /
app.get('/',function(req,res){
res.send('Hello World!')
})
// 匹配POST请求,且请求URL为 /
app.post('/',function(req,res){
res.send('Got a POST request)
})
路由的匹配过程
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求的URL同时匹配成功,则Express会将这次请求,转交给对应的function函数进行处理。
Express路由
模块化路由
为了方便对路由进行模块化的管理,Express不建议将路由直接挂载到app上,而是推荐将路由抽离为单独的模块。将路由抽离为单独模块的步骤如下:
- 创建路由模块对应的.js文件
- 调用express.Router()函数创建路由对象
- 向路由对象上挂载具体的路由
- 使用module.exports向外共享路由对象
- 使用app.use()函数注册路由模块
创建路由模块
// 这是路由模块
// 1. 导入express
const express=require('express')
// 2. 创建路由对象
const router=express.Router()
// 3. 挂载具体的路由
router.get('/user/list',(req,res)=>{
res.send('Get User List.')
})
router.post('/',(req,res)=>{
res.send('POST User List')
})
module.exports=router
注册路由模块
const express=require('express')
const app=express()
// 导入路由模块
const router=require('./router')
// 注册路由模块
app.use(router)
app.listen(80,()=>{
console.log('http://127.0.0.1')
})
为路由模块添加前缀
类似于托管静态资源时,为静态资源统一挂载访问前缀一样,路由模块添加前缀的方式也非常简单:
// 导入路由模块
const userRouter=require('./router/user.js')
// 使用app.use()注册路由模块,并添加统一的访问前缀/api
app.use('/api',userRouter)
中间件的概念
什么是中间件
中间件(Middleware),特指业务流程的中间处理环节。
Express中间件的调用流程
当一个请求到达Express的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理。
Express中间件的格式
Express的中间件,本质上就是一个function处理函数,Express中间件的格式如下:
next函数的作用
next函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由。
定义中间件函数
const express=require('express')
const app=express()
// 定义一个最简单的中间件函数
const mw=function(req,res,next){
console.log('这是最简单的中间件函数')
// 把流转关系转交给下一个中间件或路由
next()
}
app.listen(80,()=>{
console.log('http://127.0.0.1')
})
全局生效的中间件
客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。
通过调用app.use(中间件函数),即可定义一个全局生效的中间件,示例代码如下:
const express=require('express')
const app=express()
// 定义一个最简单的中间件函数
const mw=function(req,res,next){
console.log('这是最简单的中间件函数')
// 把流转关系转交给下一个中间件或路由
next()
}
// 将mw注册为全局生效的中间件
app.use(mw)
app.get('/',(req,res)=>{
res.send('Home Page')
})
app.get('/user',(req,res)=>[
res.send('User Page')
])
app.listen(80,()=>{
console.log('http://127.0.0.1')
})
定义全局中间件的简化形式
app.use((req,res,next)=>{
console.log('这是最简单的中间件函数')
// 把流转关系转交给下一个中间件或路由
next()
})
中间件的作用
多个中间件之间,共享同一份req和res。基于这样的特性,我们可以在上游的中间件中,统一为req活res对象添加自定义的属性或方法,供下游的中间件或路由进行使用。
const express=require('express')
const app=express()
app.use((req,res,next)=>{
// 获取到请求到达服务器的时间
const time=Date.now()
// 为req对象,挂载自定义属性,从而把时间共享给后面的所有路由
req.startTime = time
next()
})
app.get('/',(req,res)=>{
res.send('Home Page'+req.startTime)
})
app.get('/user',(req,res)=>[
res.send('User Page'+req.startTime)
])
app.listen(80,()=>{
console.log('http://127.0.0.1')
})
定义多个全局中间件
可以使用app.use()连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件爱你定义的先后顺序一次进行调用。
const express=require('express')
const app=express()
// 定义第一个全局中间件
app.use((req,res,next)=>{
console.log('调用了第一个中间件')
next()
})
// 定义第二个全局中间件
app.use((req,res,next)=>{
console.log('调用了第二个中间件')
next()
})
// 定义一个路由
app.get('/',(req,res)=>{
res.send('User Page.')
})
app.listen(80,()=>{
console.log('http://127.0.0.1')
})
局部生效的中间件
不使用app.use()定义的中间件,叫做局部生效的中间件。示例代码如下:
const express = require("express");
const app = express();
// 定义中间件函数
const mw1=((req,res,next)=>{
console.log('调用了局部生效的中间件')
next()
})
// 创建路由
app.get('/',mw1,(req,res)=>{
res.send('Home Page.')
})
app.listen(80,()=>{
console.log('Express server running at http://127.0.0.1')
})
定义多个局部中间件
可以在路由中,通过如下两种等价的方式,使用多个局部中间件:
const express = require("express");
const app = express();
const mw1 = (req, res, next) => {
console.log("调用了第一个局部中间件");
next();
};
const mw2 = (req, res, next) => {
console.log("调用了第二局部中间件");
next();
};
// 以下两种写法是“完全等价”的,可根据自己的喜好,选择任意一种方式进行使用
app.get("/", mw1, mw2, (req, res) => {
res.send("Page.");
});
app.get("/user", [mw1, mw2], (req, res) => {
res.send("Page.");
});
app.listen(80, () => {
console.log("http://127.0.0.1");
});
了解中间件的5个使用注意事项
1.一定要在路由之前注册中间件
2.客户端发送过来的请求,可以连续调用多个中间件进行处理
3.执行完中间件的业务代码之后,不要忘记调用next()函数
4.为了防止代码逻辑混乱,调用next()函数后不要再写额外的代码
5.连续调用多个中间件时,多个中间件之间,共享req和res对象
中间件的分类
为了方便的阿加理解和记忆中间件的使用,Express官方把常见的中间件用法,分成了5大类,分别是:
1.应用级别的中间件
2.路由级别的中间件
3.错误级别的中间件
4.Express内置的中间件
5.第三方的中间件
应用级别的中间件
通过app.use()或app.get()或app.post(),绑定到app实例上的中间件,叫做应用级别的中间件,代码示例如下:
路由级别的中间件
绑定到express.Router()实例上的中间件,叫做路由级别的中间件。它的用法和应用级别中间件没有任何区别。只不过,应用级别中间件是绑定到app实例上,路由级别中间件绑定到router实例上。
错误级别的中间件
错误级别中间件的作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
格式:错误级别中间件的function处理函数中,必须有4个形参,形参顺序从前到后,分别是(err,req,res,next)
const express=require('express')
const app=express()
app.get('/',(req,res)=>{
throw new Error('请求错误!')
res.send('Home Pgae.')
})
// 定义错误级别的中间件,捕获整个项目的异常错误,从而防止程序的崩溃
app.use((err,req,res,next)=>{
console.log('发生了错误!'+err.message)
res.send('Error!'+err.message)
})
app.listen(80,()=>{
console.log('http://127.0.0.1')
})
注意:错误级别的中间件,必须注册在所有路由之后!
Express内置的中间件
自Express4.16.0版本开始,Express内置了3个常用的中间件,极大的提高了Express项目的开发效率和体验
1.express.static快速托管静态资源的内置中间件,例如HTML文件、图片、CSS样式等(无兼容性)
2.express.json解析JSON格式的请求体数据(有兼容性,仅在4.16.0+版本中可用)
3.express.urlencoded解析URL-encoded格式的请求体数据(有兼容性,仅在4.16.0版本中可用)
const express=require('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(80,()=>{
console.log('Express server running at http://127.0.0.1')
})
第三方中间件
非Express官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。在项目中,大家可以按需下载并配置第三方中间件,从而提高项目的开发效率。
例如:在express@4.16.0之前的版本中,经常使用body-parser这个第三方中间件,来解析请求体数据。使用步骤如下:
- 运行npm install body-parser安装中间件
- 使用require导入中间件
- 调用app.use()注册并使用中间件
const express=require('express')
const app=express()
// 导入解析表单数据的中间件 body-parser
const parser=require('body-parser')
// 使用app.use()注册中间件
app.use(parser.urlencoded({extended:false}))
app.post('/user',(req,res)=>{
console.log(req.body)
res.send('ok')
})
app.listen(80,()=>{
console.log('http://127.0.0.1')
})
自定义中间件
需求描述与实现步骤:自己手动模拟一个类似于express.urlencoded这样的中间件,来解析POST提交到服务器的表单数据。
实现步骤:
- 定义中间件
- 监听req的data事件
- 监听req的end事件
- 使用querystiring模块解析请求体数据
- 将解析出来的数据对象挂载为req.body
- 将自定义中间件封装为模块
const express = require("express");
const app = express();
// 导入Node.js内置的querystring模块
const qs = require("querystring");
// 这是解析表单数据的中间件
app.use((req, res, next) => {
// 定义中间件具体的业务逻辑
// 定义一个str字符串,专门用来存储客户端发送过来的请求体数据
let str = "";
// 监听req的data事件
req.on("data", (chunk) => {
str += chunk;
});
// 监听req的end事件
req.on("end", () => {
// 在str中存放的是完整的请求体数据
// console.log(str)
// TODO:把字符串格式的请求体数据,解析成对象格式
const body = qs.parse(str);
console.log(body);
req.body = body;
next();
});
});
app.post("/user", (req, res) => {
res.send(req.body);
});
app.listen(80, () => {
console.log("http://127.0.0.1");
});
拆分中间件custom-body-parser
// 导入Node.js内置的querystring模块
const qs = require("querystring");
const bodyParser = (req, res, next) => {
// 定义中间件具体的业务逻辑
// 定义一个str字符串,专门用来存储客户端发送过来的请求体数据
let str = "";
// 监听req的data事件
req.on("data", (chunk) => {
str += chunk;
});
// 监听req的end事件
req.on("end", () => {
// 在str中存放的是完整的请求体数据
// console.log(str)
// TODO:把字符串格式的请求体数据,解析成对象格式
const body = qs.parse(str);
console.log(body);
req.body = body;
next();
});
};
module.exports = bodyParser;
使用自己封装的中间件
const express = require("express");
const app = express();
// 导入自己封装的中间件模块
const customBodyParser=require('./custorm-body-parser')
// 将自定义的中间件函数,注册为全局可用的中间件
app.use(customBodyParser);
app.post("/user", (req, res) => {
res.send(req.body);
});
app.listen(80, () => {
console.log("http://127.0.0.1");
});
使用Express写接口
调用API路由接口模块
const express = require("express");
// 导入路由模块
const router = require("./12.js");
const app = express();
// 把路由模块,注册到app上
app.use("/api", router);
app.listen(80, () => {
console.log("Express server running at http://127.0.0.1");
});
编写GET接口
const express = require("express");
const router = express.Router();
// 在这里挂载对应的路由
router.get("/get", (req, res) => {
// 通过req.query获取客户端通过查询字符串,发送到服务器的数据
const query = req.query;
// 调用req.send()方法,向客户端响应处理的结果
res.send({
status: 0, // 0表示处理成功,1表示处理失败
msg: "GET请求成功!", // 状态的描述
data: query, // 需要响应给客户端的数据
});
});
module.exports = router;
编写POST接口
const express = require("express");
const router = express.Router();
// 在这里挂载对应的路由
router.get("/get", (req, res) => {
// 通过req.query获取客户端通过查询字符串,发送到服务器的数据
const query = req.query;
// 调用req.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
})
});
module.exports = router;
调用API路由接口模块
const express = require("express");
const app = express();
// 配置解析表单数据的中间件
app.use(express.urlencoded({extended:false}))
// 导入路由模块
const router = require("./12.js");
// 把路由模块,注册到app上
app.use("/api", router);
app.listen(80, () => {
console.log("Express server running at http://127.0.0.1");
});
在线版MDN网址:https://www.staticfile.net/
接口的跨域问题
刚才编写的GET和POST接口,存在一个很严重的问题:不支持跨域请求。解决接口跨域问题的方案主要有两种:
1.CORS(主流的解决方案,推荐使用)
2.JSONP(有缺陷的解决方案,只支持GET请求)
使用cors中间件解决跨域问题
cors是Express的一个第三方中间件。通过安装和配置cors中间件,可以很方便地解决跨域问题。使用步骤分为如下3步:
- 运行npm install cors安装中间件
- 使用const cors = require(“cors”)导入中间件
- 在路由之前调用app.use(cors())配置中间件
const express = require("express");
const app = express();
// 配置解析表单数据的中间件
app.use(express.urlencoded({extended:false}))
// 一定要在路由之前,配置cors这个中间件,从而解决接口跨域的问题
const cors = require("cors")
app.use(cors())
// 导入路由模块
const router = require("./12.js");
// 把路由模块,注册到app上
app.use("/api", router);
app.listen(80, () => {
console.log("Express server running at http://127.0.0.1");
});
什么是CORS
CORS(Cross-Origin Resource Sharing,跨域资源共享)由一系列HTTP响应头组成,这些HTTP响应头决定浏览器是否阻止前端JS代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了CORS相关的HTTP响应头,就可以解除浏览器端的跨域访问限制。
CORS的注意事项
1.CORS主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了CORS的接口。
2.CORS在浏览器中有兼容性。只有支持XMLHttpRequest Level2的浏览器,才能正常访问开启了CORS的服务器端接口(例如:IE10+、Chrome4+、FireFox3.5+)。
CORS跨域资源共享
CORS响应头部-Access-Control-Allow-Origin
响应头部中可以携带一个Access-Control-Allow-Origin字段,其语法如下:
Access-Control-Allow-Origin: <origin> | *
其中,origin参数的值指定了允许访问该资源的外域URL
例如,下面的字段值将只允许来自http://itcast.cn的请求
res.setHeader('Access-Control-Allow-Origin','http://itcast.cn')
如果指定了Access-Control-Allow-Origin字段的值为通配符*,表示允许来自任何域的请求,示例代码如下:
res.setHeader('Access-Control-Allow-Origin','*')
CORS响应头部-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三者之一)
如果客户端向服务器发送了额外的请求头信息,则需要再服务器端,通过Access-Control-Allow-Headers对额外的请求头进行生命,否则这次请求会失败!
// 允许客户端格外向服务器发送 Content-Type 请求头和 X-Custom-Header 请求头
// 注意:多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers','Content-Type,X-custom-Header')
CORS响应头部-Access-Control-Allow-Methods
默认情况下,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接口时,根据请求方式和请求头的不同,可以将CORS的请求分为两大类,分别是:
1.简单请求
2.预检请求
简单请求:
同时满足以下两大条件的请求,就属于简单请求:
- 请求方式:GET、POST、HEAD三者之一
- HTTP头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type(只有三个值application/x-form-urlencoded、multipart/form-data、text/plain)
预检请求
- 请求方式为GET、POST、HEAD之外的请求Method类型
- 请求头中包含自定义头部字段
- 向服务器发送了application/json格式的数据
在浏览器与服务器正式通信之前,浏览器会先发送OPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTION请求成为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
简单请求和预检请求的区别
简单请求的特点:客户端与服务器之间只会发生一次请求
预检请求的特点:客户端与服务器之间会发生两次请求,OPTION预检请求成功之后,才会发生真正的请求。
JSONP接口
概念:浏览器通过
创建JSONP接口的注意事项
如果项目中已经配置了CORS跨域资源共享,为了防止冲突,必须在配置CORS中间件之前声明JSONP的接口。否则JSONP接口会被处理成开启了CORS的接口。
// 优先创建JSONP接口【这个接口不会被处理成CORS接口】
app.get('/api/jsonp',(req,res)=>{})
// 再配置CORS中间件【后续的所有接口,都会被处理成CORS接口】
app.use(cors())
// 这是一个开启了CORS的接口
app.get('/api/get',(req,res)=>{})
实现JSONP接口的步骤
- 获取客户端发送过来的回调函数的名字
- 得到要通过JSONP形式发送给客户端的数据
- 根据前两步得到的数据,拼接出一个函数调用的字符串
- 把上一步拼接得到的字符串,响应给客户端的script标签进行解析执行
实现JSONP接口的具体代码
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("./12.js");
// 把路由模块,注册到app上
app.use("/api", router);
app.listen(80, () => {
console.log("Express server running at http://127.0.0.1");
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.staticfile.net/jquery/3.4.1/jquery.min.js"></script>
</head>
<body>
<button id="btnGET">GET</button>
<button id="btnPOST">POST</button>
<button id="btnDelete">DELETE</button>
<button id="btnJSONP">JSONP</button>
<script>
$(function () {
// 测试GET接口
$("#btnGET").on("click", function () {
$.ajax({
type: "GET",
url: "http://127.0.0.1/api/get",
data: { name: "zs", age: 20 },
success: function (res) {
console.log(res);
},
});
});
// 测试POST接口
$("#btnPOST").on("click", function () {
$.ajax({
type: "POST",
url: "http://127.0.0.1/api/post",
data: { bookname: "水浒传", author: "施耐庵" },
success: function (res) {
console.log(res);
},
});
});
// 为删除按钮绑定点击事件处理函数
$("#btnDelete").on("click", function () {
$.ajax({
type: "DELETE",
url: "http://127.0.0.1/api/delete",
success: function (res) {
console.log(res);
},
});
});
$("#btnJSONP").on("click", function () {
$.ajax({
method: "GET",
url: "http://127.0.0.1/api/jsonp",
dataType: "jsonp",
success: function (res) {
console.log(res);
},
});
});
});
</script>
</body>
</html>
数据库的基本概念
什么是数据库
数据库(database)是用来组织、存储和管理数据的仓库。
当今世界是一个充满着数据的互联网世界,充斥着大量的数据。数据的来源有很多,比如出行记录、消费记录、浏览的网页、发送的消息等等。除了文本类型的数据,图像、音乐、声音都是数据。
为了方便管理互联网世界中的数据,就有了数据库管理系统的概念(简称:数据库)。用户可以对数据库中的数据进行新增、查询、更新、删除等操作。
常见的数据库及分类
市面上的数据库有很多种,最常见的数据库有如下几个:
MySQL数据库(目前使用最广泛、流行度最高的开源免费数据库;Community+Enterprise)
Oracle数据库(收费)
SQL Server数据库(收费)
Mongodb数据库(Community+Enterprise)
其中,MySQL、Oracle、SQL Server属于传统型数据库(又叫做:关系型数据库或SQL数据库),这三者的涉及理念相同,用法比较类似。
而Mongodb属于新型数据库(又叫做:非关系型数据库或MySQL数据库),它在一定程度上弥补了传统型数据库的缺陷。
传统型数据库的数据组织结构
数据的组织结构:指的就是数据以什么样的结构进行存储。
传统型数据库的数据组织结构,与Excel中数据的组织结构比较类似。
因此,我们可以对比着Excel来了解和学习传统型数据库的数据组织结构。
Excel的数据组织结构
每个Excel中,数据的组织结构分别为工作薄、工作表、数据行、列这4大部分组成。
传统型数据库的数据组织结构
在传统型数据库中,数据的组织结构分为数据库(database)、数据表(table)、数据行(row)、字段(field)这4大部分组成。
实际开发中库、行、字段的关系
1.在实际项目开发中,一般情况下,每个项目都对应独立的数据库。
2.不同的数据,要存储到数据库的不同表中,例如:用户数据存储到user是表中,图书数据存储到books表中。
3.每个表中具体存储那些信息,由字段来决定,例如:我们可以为users表设计id、username、password这3个字段。
4.表中的行,代表每一条具体的数据。
安装并配置MySQL
了解需要安装那些MySQL相关的软件
对于开发人员来说,只需要安装MySQL Server和MySQL Workbench这两个软件,就能满足开发的需要了。
MySQL Server:专门用来提供数据存储和服务的软件
MySQL Workbench:可视化的MySQL管理工具,通过它,可以方便的操作存储在MySQL Server中的数据
(密码:admin123)
DataType数据类型:
- int整数
- varchar(len)字符串
- tinyint(1)布尔值
字段的特殊标识:
4. PK(Primary Key)主键、唯一标识
5. NN(Not Null)值不允许为空
6. UQ(Unique)值唯一
7. AI(Auto Increment)值自动增长
什么是SQL
SQL(英文全称:Structured Query Language)是结构化查询语言,专门用来访问和处理数据库的编程语言。能够让我们以编程的形式,操作数据库里面的数据。
三个关键点:
- SQL是一门数据库编程语言
- 使用SQL语言编写出来的代码,叫做SQL语句
- SQL语言只能在关系型数据库中使用(例如MySQL、Oracle、SQL Server)。非关系型数据库(例如Mongdb)不支持SQL语言
SQL能做什么
- 从数据库中查询数据
- 向数据库中插入新的数据
- 更新数据库中的数据
- 从数据库删除数据
- 可以创建新数据库
- 可在数据库中创建新表
- 可在数据库中创建存储过程、视图
- etc……
SQL的学习目标
重点掌握如何使用SQL从数据表中:
查询数据(select)、插入数据(insert into)、更新数据(update)、删除数据(delete)
额外需要掌握的4种SQL语法:
where条件、and和or运算符、order by排序、count(*)函数
语法
SELECT语句用于从表中查询数据。执行的结果被存储在一个结果表中(称为结果集)。语法格式如下:
-- 这是注释
-- 从FROM指定的【表中】,查询出【所有的】数据。*表示【所有列】
SELECT * FROM 表名称
-- 从FROM指定的【表中】,查询出指定列名称(字段)的数据
SELECT 列名称 FROM 表名称
注意:SQL语句中关键字对大小写不敏感。SELECT等效于select,FROM等效于from。
SELECT * 示例
我们希望从users表中选取所有的列,可以使用符号 * 取代列的名称,示例如下:
-- 通过 * 把users表中所有的数据查询出来
select * from users
SELECT列名称 示例
如需获取名为“username”和“password”的列的内容(从名为“users”的数据库表),请使用下面的SELECT语句:
-- 从users表中把username和password对应的数据查询出来
-- 注意:多个列之间,使用英文逗号进行分隔
SELECT username, password FROM users
SQL的INSERT INTO语句
语法
INSERT INTO语句用于向数据表中插入新的数据行,语法格式如下:
-- 语法解读:向指定的表中,插入如下几列数据,列的值通过values一一指定
-- 注意:列和值要一一对应,多个列和多个值之间,使用英文的逗号分隔
INSERT INTO table_name (列1, 列2……) VALUES (值1, 值2……)
INSERT INTO示例
向users表中,插入一条username为tony stark,password为098123的用户数据,示例如下:
-- 向users表中,插入新数据,username的值为tony stark password的值为098123
insert into users (username,password) values ('tony stark','098123')
SQL的UPDATE语句
语法
Update语句用于修改表中的数据。语法格式如下:
-- 语法解读:
-- 1. 用UPDATE指定要更新哪个表中的数据
-- 2. 用SET指定列对应的新值
-- 3. 用WHERE指定更新的条件
UPDATE 表名称 SET 列名称 = 新值 WHERE 列名称 = 某值
UPDATE示例-更新某一行中的一个列
把users表中id为6的用户密码,更新为888888。示例如下:
update users set password = '888888' where id = 6
UPDATE示例-更新某一行中的若干列
把users表中id为2的用户密码和用户状态,分别更新为admin123和1。示例如下:
-- 更新id为2的用户,把用户密码更新为admin123 同时,把用户的状态更新为1
update users set password = 'admin123',status = 1 where id =2
SQL的DELETE语句
语法
DELETE语句用于删除表中的行。语法格式如下
-- 语法解读:
-- 从指定的表中,根据WHERE条件,删除对应的数据行
DELETE FROM 表名称 WHERE 列名称 = 值
DELETE示例
从users表中,删除id为4的用户。示例如下:
-- 删除users表中,id为5的用户
delete from users where id = 5
SQL的WHERE子句
语法
WHERE子句用于限定选择的标准。在SELECT、UPDATE、DELETE语句中,皆可使用WHERE子句来限定选择的标准。
-- 查询语句中的WHERE条件
SELECT 列名称 FROM 表名称 WHERE 列 运算符 值
-- 更新语句中的WHERE条件
UPDATE 表名称 SET 列 = 新值 WHERE 运算符 值
-- 删除语句中的WHERE条件
DELETE FROM 表名称 WHERE 列 运算符 值
可在WHERE子句中使用的运算符
注意:在某些版本的SQL中,操作符<>可以写为!=
WHERE子句示例
可以通过WHERE子句来限定SELECT的查询条件:
-- 查询status为1的所有用户
SELECT * FROM users WHERE status = 1
-- 查询id大于2的所有用户
SELECT * FROM users WHERE id > 2
-- 查询username不等于admin的所有用户
SELECT * FROM users WHERE id <> 'admin'
-- 演示where子句的使用
select * from users where status = 1;
select * from users where id >= 2;
select * from users where username <> 'ls';
select * from users where username != 'ls';
SQL的AND和OR运算符
语法
AND和OR可在WHERE子语句中把两个活多个条件结合起来。
AND表示必须同时满足多个条件,相当于JavaScript中的&&运算符,例如if(a!==10&&a!==20)
OR表示只要满足任意一个条件即可,相当与JavaScript中的||运算符,例如if(a!==10||a!==20)
AND运算符示例
-- 使用AND来显示所有状态为0且id小于3的用户
select * from users where status = 0 and id < 3;
-- 使用or来显示所有状态为1或username为zs的用户
select * from users where status = 1 or username = 'zs';
SQL的ORDER BY子句
语法
ORDER BY语句用于根据指定的列对结果集进行排序
ORDER BY语句默认按照升序对记录进行排序
如果希望按照降序对记录进行排序,可以使用DESC关键字
ORDER BY子句 - 升序排序
对users表中的数据,按照status字段进行升序排序,示例如下:
-- 对users表中的数据,按照status字段进行升序排序
select * from users order by status;
select * from users order by status asc;
ORDER BY子句 - 降序排序
对users表中的数据,按照id字段进行降序排序,示例如下:
-- 按照id对结果 进行降序排序
select * from users order by id desc;
ORDER BY子句-多重排序
对users表中的数据,先按照status字段进行降序排序,再按照username的字母顺序,进行升序排序,示例如下:
-- 对users表中的数据,先按照status进行降序排序,再按照usename字母的顺序,进行升序排序
select * from users order by status DESC, username ASC;
SQL的COUNT(*)函数
COUNT(*)函数用于返回查询结果的总数据条数,语法格式如下:
SELECT COUNT(*) FROM 表名称
COUNT(*)示例
查询users表中status为0的总数据条数:
-- 使用COUNT(*)来统计users表中,状态为0的总数量
select count(*) from users where status = 0;
使用AS为列设置别名
如果希望给查询出来的列名称设置别名,可以使用AS关键字,示例如下:
-- 使用AS关键字给列起别名
select count(*) as total from users where status = 0;
select username as uname, password as upwd from users;
保存文件
打开文件
在项目中操作MySQL
在项目中操作数据库的步骤
- 安装操作MySQL数据库的第三方模块(mysql)
- 通过mysql模块链接到MySQL数据库
- 通过mysql模块执行SQL语句
安装与配置mysql模块
安装mysql模块
mysql模块是托管于npm上的第三方模块。它提供了Node.js项目中连接和操作MySQL数据库的能力。
想要在项目中使用它,需要先运行如下命令,将mysql安装为项目的依赖包。
npm install mysql
配置mysql模块
在使用mysql模块操作MySQL数据库之前,必须先对mysql模块进行必要的配置,主要的配置步骤如下:
// 1. 导入数据库模块
const mysql=require('mysql')
// 2. 建立MySQL数据库的连接关系
const db=mysql.createPool({
host:'127.0.0.1', // 数据库的IP地址
user:'root', // 登录数据库的账号
password:'admin123', // 登录数据库的密码
database:'my_db_01' // 指定要操作哪个数据库
})
测试mysql模块能否正常工作
调用db.query()函数,指定要执行的SQL语句,通过回调函数拿到执行的结果:
// 测试mysql模块能否正常工作
db.query("select 1", (err, results) => {
// mysql模块工作期间报错了
if (err) return console.log(err.message);
// 能够成功的执行SQL语句
console.log(results);
});
查询数据
查询users表中的所有数据
// 查询users表中所有的数据
const sqlStr = "select * from users";
db.query(sqlStr, (err, results) => {
// 查询数据失败
if (err) return console.log(err.message);
// 查询数据成功
// 注意:如果执行的是select查询语句,则执行的结果是数组
console.log(results);
});
插入数据
向users表中新增数据,其中username为Sprider-man,password为pcc321。示例代码如下:
// 1. 要插入到users表中的数据对象
const user = { username: "Spider-Man", password: "pcc321" };
// 2. 待执行的SQL语句,其中中英文的?表示占位符
const sqlStr = "insert into users (username, password) values (?,?)";
// 3. 使用数组的形式,依次为?占位符指定具体的值
db.query(sqlStr, [user.username, user.password], (err, results) => {
if (err) return console.log(err.message); // 失败
if (results.affectedRows === 1) {
console.log("插入数据成功");
} // 成功
});
插入数据的便捷方式
向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速插入数据:
// 1. 要插入到users表中的数据对象
const user = { username: 'Spider-Man2', password: 'pcc4321'}
// 2. 待执行的SQL语句,其中英文的?表示占位符
const sqlStr = 'insert into users set ?'
// 3. 直接将数据对象当做占位符的值
db.query(sqlStr, user, (err,results) => {
if (err) return console.log(err.message) //失败
if (results.affectedRows ===1) {console.log('插入数据成功')}
})
// 演示插入数据的便捷方式
const user = { username: "Spider-Man2", password: "pcc4321" };
const sqlStr = "insert into users set ?";
db.query(sqlStr, user, (err, results) => {
if (err) return console.log(err.message);
if (results.affectedRows === 1) console.log("插入数据成功");
});
更新数据
可以通过如下方式,更新表中的数据
// 1. 要更新的数据对象
const user = { id: 7, username: 'aaa', password: '000' }
// 2. 要执行的SQL语句
const sqlStr = 'update users set username = ?, password =? where id = ?'
// 3. 调用db.query()执行SQL语句的同时,使用数据依次为占位符指定具体的值
db.query(sqlStr, [user.username,user.password,user.id}, (err, results) => {
if(err) return console.log(err.message)
if(results.affectedRows === 1) {console.log('更新数据成功!')} //成功
})
// 演示如何更新用户的信息
const user = { id: 6, username: "Spider", password: "pcc4321" };
const sqlStr = "update users set username=?,password=? where id=?";
// 执行SQL语句
db.query(sqlStr,[user.username,user.password,user.id],(err,results)=>{
if(err) return console.log(err.message)
if(results.affectedRows===1) console.log('更新成功')
})
更新数据的便捷方式
更新表数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速更新表数据:
// 1. 要更新的数据对象
const user = {id: 7, username: 'aaaa', password: '0000'
// 2. 要执行的SQL语句
const sqlStr = 'update users set ? where id = ?'
// 3. 调用db.query()执行SQL语句的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr, [user,user.id], (err,results) => {
if(err) return console.log(err.message) //失败
if(results.affectedRows ===1) {console.log('更新数据成功')} // 成功
})
// 演示更新数据的便捷方式
const user={id:6,username:'aaaa',password:'0000'}
// 定义SQL语句
const sqlStr='update users set ? where id=?'
// 执行SQL语句
db.query(sqlStr,[user,user.id],(err,results)=>{
if(err) return console.log(err.message)
if(results.affectedRows===1){
console.log('更新数据成功!')
}
})
删除数据
在删除数据时,推荐根据id这样的唯一标识,来删除对应的数据。示例如下:
// 1. 要执行的SQL语句
const sqlStr = 'delete from users where id=?'
// 2. 调用db.query()执行SQL语句的同时,为占位符指定具体的值
// 注意:如果SQL语句中有多个占位符,则必须使用数组为每个占位符指定具体的值,如果SQL语句中只有一个占位符,则可以省略数组
db.query(sqlStr, 7, (err, results) => {
if(err) return console.log(err.message) // 失败
if(results.affectedRows ===1) {console.log('删除数据成功!')} // 成功
})
// 删除id为13的用户
const sqlStr = 'delete from users where id = ?'
db.query(sqlStr,13,(err,results)=>{
if(err) return console.log(err.message)
if(results.affectedRows===1){
console.log('删除数据成功!')
}
})
标记删除
使用DELETE语句,会真正的把数据从表中删除掉,为了保险起见,推荐使用标记删除的形式来模拟删除的动作。
所谓的标记删除,就是在表中设置类似于status这样的状态字段,来标记当前这条数据是否被删除。
当用户执行了删除的动作时,我们并没有执行DELETE语句把数据删除掉,而是执行了UPDATE语句,将这条数据对应的status字段标记为删除即可。
// 标记删除
const sqlStr = "update users set status=? where id=?";
db.query(sqlStr,[1,19],(err,results)=>{
if(err) return console.log(err.message)
if(results.affectedRows===1) {
console.log('标记删除成功')
}
})
前后端的身份认证
Web开发模式
目前主流的Web开发模式有两种,分别是:
- 基于服务器端渲染的传统Web开发模式
- 基于前后端分离的新型Web开发模式
服务器端渲染的Web开发模式
服务器端渲染的概念:服务器发送给客户端的HTML页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用Ajax这样的技术额外请求页面的数据
服务器端渲染的优缺点
优点:
1.前端耗时少。因为服务器端负责动态生成HTML内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
2.有利于SEO。因为服务器端响应的是完整的HTML页面内容,所以爬虫更容易爬起获得信息,更有利于SEO。
缺点:
1.占用服务器端资源。即服务器端完成HTML页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力。
2.不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,尤其对于前后复杂度高的项目,不利于项目高效开发。
前后端分离的Web开发模式
前后端分离的概念:前后端分离的开发模式,依赖于Ajax技术的广泛应用。简而言之,齐纳后盾分离的Web开发模式,就是后端只负责提供API接口,前端使用Ajax调用接口的开发模式。
前后端分离的优缺点
优点:
1.开发体验好,前端专注于UI页面的开发,后端专注于api的开发,且前端有更多的选择性。
2.用户体验好,Ajax技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。
3.减轻了服务器端的渲染压力。因为页面最终是在每个用户的浏览器中生成的。
缺点:
1.不利于SEO。因为完整的HTML页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方案:利用Vue、React等前端框架的SSR(server side render)技术能够很好的解决SEO问题!)
如何选择Web开发模式
比如企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的SEO,则这时我们就需要使用服务器端渲染;
而类似后台管理项目,交互性比较强,不需要考虑SEO,那么就可以使用前后端分离的开发模式。
另外,具体使用何种开发模式并不是绝对的,为了同事兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器端渲染+其他也没前后端分离的开发模式。
前后端的身份认证
身份认证
什么是身份认证
身份认证(Authentication)又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。
不同开发模式下的身份认证
对于服务器端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
1.服务器端渲染推荐使用Session认证机制
2.前后端分离推荐使用JWT认证机制
Session认证机制
HTTP协议的无状态性
了解HTTP协议的无状态性是进一步学习Session认证机制的必要前提
HTTP协议的无状态性,指的是客户端的每次HTTP请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP请求的状态。
如何突破HTTP无状态的限制
什么是Cookie
Cookie是存储在用户浏览器中的一段不超过4kb的字符串。它由一个名称(Name)、一个值(Value)和其他几个用于控制Cookie有效期、安全性、使用范围内的可选属性组成。
不同域名下的Cookie各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的Cookie一同发送到服务器。
Cookie的几大特性:
- 自动发送
- 域名独立
- 过期时限
- 4KB限制
Cookie在身份认证中的作用
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的Cookie,客户端会自动将Cookie保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
Cookie不具有安全性
由于Cookie是存储在浏览器中的,而且浏览器也提供了读写Cookie的API,因此Cookie很容易被伪造,不具有安全性,因此不建议服务器将重要的隐私数据,通过Cookie的形式发送给浏览器。
注意:千万不要使用Cookie存储重要且隐私的数据!比如用户的身份信息、密码等。
提高身份认证的安全性
“会员卡 + 刷卡认证”的设计理念,就是Session认证机制的精髓
Session的工作原理
在Express中使用Session认证
安装express-session中间件
在Express项目中,只需要安装express-session中间件,即可在项目中使用Session认证:
配置express-session中间件
express-session中间件安装成功后,需要通过app.use()来注册session中间件
// 导入express模块
const express = require('express')
// 创建express的服务器实例
const app=express()
// 配置Session中间件
const session=require('express-session')
app.use(session({
secret:'itheima',
resave:false,
saveUninitialized:true
}))
// 托管静态页面
app.use(express.static('./pages'))
// 解析POST提交过来的表单数据
app.use(express.urlencoded({extended:false}))
// 登录API接口
app.listen(80,()=>{
console.log('server running at http://127.0.0.1')
})
向session中存数据
当express-session中间件配置成功后,即可通过req-session来访问和使用session对象,从而存储用户的关键信息
// 导入express模块
const express = require("express");
// 创建express的服务器实例
const app = express();
// 配置Session中间件
const session = require("express-session");
app.use(
session({
secret: "itheima",
resave: false,
saveUninitialized: true,
})
);
// 托管静态页面
app.use(express.static("./pages"));
// 解析POST提交过来的表单数据
app.use(express.urlencoded({ extended: false }));
// 登录API接口
app.post("/api/login", (req, res) => {
// 判断用户提交的登录信息是否正确
if (req.body.username !== "admin" || req.body.password !== "000000") {
return res.send({ status: 1, msg: "登录失败" });
}
// 请将登录成功后的用户信息,保存到Session中
// 注意:只有成功配置了express-session这个中间件之后,才能够通过req点出来session这个属性
req.session.user = req.body; // 用户的信息
req.session.islogin = true; // 用户的登录状态
});
app.listen(80, () => {
console.log("server running at http://127.0.0.1");
});
从session中取数据
可以直接从req.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
调用req.session.destroy()函数,即可清空服务器保存的session信息。
// 退出登录的接口
app.post('/api/logout',(req,res)=>{
// 清空当前客户端对应的session信息
req.session.destroy()
res.send({
status:0,
msg:'退出登录成功'
})
})
JWT认证机制
了解Session认证的局限性
Session认证机制需要配合Cookie才能实现。由于Cookie默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域Session认证。
注意:
当前端请求后端接口不存在跨域问题的时候,推荐使用Session身份认证机制。
当前端需要跨域请求后端接口的时候,不推荐使用gSession身份认证机制,推荐使用JWT认证机制。
什么是JWT
JWT(英文全称:JSON Web Token)是目前最流行的跨域认证解决方案。
JWT的工作原理
总结:用户的信息通过Token字符串的形式,保存在客户端浏览器中。服务器通过还原Token字符串的形式来认证用户的身份。
JWT的组成部分
JWT通常由三部分组成,分别是Header(头部)、Payload(有效荷载)、Signature(签名)。
下面是 JWT字符串的示例:
JWT的三个部分各自代表的含义
JWT的三个组成部分,从前到后分别是Header、Payload、Signature。
Payload部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
Header和Signature是安全性相关的部分,只是为了保证Token的安全性。
JWT的使用方式
客户端收到服务器返回的JWT之后,通常会将它存储在localStorage和sessionStorage中。
此后,客户端每次与服务器通信,都要带上这个JWT的字符串,从而进行身份认证。推荐的做法是把JWT放在HTTP请求头的Authorization字段中
在Express中使用JWT
1.安装JWT相关的包
jsonwebtoken用于生成JWT字符串
express-jwt用于将JWT字符串解析还原成JSON对象
定义secret密钥
为了保证JWT字符串的安全性,防止JWT字符串在网络传输过程中被被人破解,我们需要专门定义一个用于加密和解密的secret密钥
1.当生成JWT字符串的时候,需要使用secret密钥对用户的信息进行加密,最终得到加密好的JWT字符串
2.当把JWT字符串解析还原成JSON对象的时候,需要使用secret密钥进行解密
在登录成功后生成JWT字符串
调用jsonwebtoken包提供的sign()方法,将用户的信息加密成JWT字符串,响应给客户端:
const jwt = require("jsonwebtoken");
const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: "30s" });
将JWT字符串还原为JSON对象
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的Authorization字段,将Token字符串发送到服务器进行身份认证
此时,服务器可以通过express-jwt这个中间件,自动将客户端发送过来的Token解析还原成JSON对象:
const {expressjwt} = require("express-jwt");
app.use(expressjwt({secret:secretKey,algorithms:['HS256']}).unless({path:[/^\/api\//]}))
使用req.user获取用户信息
当express-jwt这个中间件配置成之后,即可在那些有权限的接口中,使用req.对象,来访问从JWT字符串中解析出来的用户信息。
捕获解析JWT失败后产生的错误
当使用express-jwt解析Token字符串时,如火客户端发送过来的Token字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行。我们可以通过Express的错误中间件,捕获这个错误并进行相关的处理,示例代码如下: