Web服务器开发
ip地址和端口号
- ip地址用来定位计算机
- 端口号用来定位具体的应用程序
- 一切需要联网通信的软件都会占用一个端口号
- 端口号的范围从0-65536之间
- 在计算机中有一些默认端口号,最好不要去使用
— 例如http服务的80 - 我们在开发的过程中使用一些简单好记的就可以了,例如3000、5000等没什么含义
介绍终端
- 命令行窗口(小黑屏)、CMD窗口、终端、shell
- 开始菜单 –> 运行 –> CMD –> 回车
- 常用的指令:
dir列出当前目录下的所有文件
cd 目录名 进入指定的目录
md 目录名 创建一个文件夹
rd 目录名 删除一个文件夹 - 目录
. 表示当前目录
.. 表示上一级目录./style.css 同一级目录
../style.css 上一级目录 - 环境变量
需重启命令行窗口此时配置完用户变量后就可以直接用命令打开hello文件了1
2
3
4%USERPROFILE%\AppData\Local\Microsoft\WindowsApps;;
D:\Microsoft VS Code\bin;
C:\Users\Lenovo\AppData\Roaming\npm;
C:\Users\Lenovo\Desktop\hello
- 当我们在命令行窗口打开一个文件,或者调用一个程序时
系统会首先在当前目录下寻找文件程序,如果找到了则直接打开
如果没有找到则会依次到环境变量path的路径去寻找,直到找到为止
如果没有找到则报错 - 我们可以将一些经常需要的访问程序和文件的路径添加到path中,
这样我们就可以在任意位置来访问这些文件和程序了
引入其他模块
- 在node中,通过require()函数来引入外部的模块
- require()可以传递一个文件的路径作为参数,node将会自动根据该路径来引入外部模块这里的路径,如果使用相对路径,必须以.或..
1
2var md=require("./test1")
// 返回的对象是模块
模块化 - 在Node中,一个js文件就是一个模块
- 在Node中每一个js文件中的js代码都是独立运行在一个函数中
而不是全局作用域,所以一个模块的中的变量和函数在其他模块中无法访问 - 向外部暴露属性或方法
我们可以通过exports来向外部暴露变量和方法
只需要将需要暴露给外部的变量或方法设置为exports的属性即可模块分为两大类1
2
3exports.x="我是02.module.js中的x";
exports.y="我是02.module.js中的y";
exports.fn=function(){}
核心模块- 由node引擎提供的模块
- 核心模块的标识就是模块的名字常见的核心模块有文件操作的
1
2
3
4var fs=require("fs")
var os=require("os")
var http=require("http")
var path=require("path")fs
模块,http服务构建的http
模块,path路径操作模块,os操作系统信息模块…
文件模块 - 由用户自己创建的模块
- 文件模块的标识就是文件的路径(绝对路径,相对路径)
相对路径使用.或..开头
在Node中,没有全局作用域,只有模块作用域
外部访问不到内部
内部也访问不到外部
require 方法有两个作用:
- 加载文件模块并执行里面的代码
- 拿到被加载文件模块导出的接口对象
在每个文件模块中都提供了一个对象:exports(默认是一个空对象) 可以利用exports将外部文件挂载导exports上被其他文件调用
a.js1
2
3
4
5
6
7var bExports=require('./b')
console.log(bExports.foo)
// hello
console.log(bExports.add(10,30))
// 40
bExports.readFile('./a.js')
// 和文件管理中的readFile不一样 只是作为一个方法调用b.js
1
2
3
4
5
6
7
8
9
10
11var foo='bbb'
exports.foo='hello'
exports.add=function(x,y){
return x+y
}
// function add(x,y){
// return x+y
// } 不起作用
exports.readFile=function(path,callback){
console.log('文件路径',path)
}
global
- 在node中有一个全局对象global,它的作用和网页中的window类似
在全局创建的的变量都会作为global的属性保存
在全局中创建的函数都会作为global的方法保存
关于运行Node.js执行函数
- 当node在执行模块中的代码时,它会首先在代码的最顶部,添加如下代码
1
2
3
4function(exports,require,module,_filename,_dirname){
// 在代码的最底部,添加了如下代码
} - 实际上模块中的代码都是包装在一个函数中执行的,并且在函数执行时,同时传递了五个实参
exports - 该对象用来将变量或函数暴露到外部
require
- 函数,用来引入外部的模块
module
- module代表的是当前模块本身
- exports就是module的属性
- 既可以使用exports导出,也可以使用module.exports导出
_filename
c:\Users\Lenovo\Desktop\html\03.html\xpy\node.js\test2.js:2
- 当前模块的完整路径
_dirname
c:\Users\Lenovo\Desktop\html\03.html\xpy\node.js
- 当前模块所在文件夹的路径
关于exports和module.exports
- 通过exports只能使用.的方式向外暴露内部变量
1
exports.xxx=xxx
- module.exports既可以通过.的形式,也可以直接赋值///(直接用这种方式以免出错)
例子
module.exports.属性=属性值;
module.exports.方法=函数;1
2module.exports.xxx=xxx
module.exports={}
包package
- CommonJS的包规范允许我们将一组相关的模块组合到一起,形成一组完整的工具
- CommonJS的包规范由包结构和包描述文件两个部分组成
- 包结构
用于组织包中的各种文件 - 包描述文件
描述包的相关信息,以供外部读取分析
包结构
实际上是一个压缩文件,解压之后为目录。符合规范的目录,应该包含如下文件:
-package.json 描述文件(说明书) 必须文件
-bin 可执行二进制文件
-lib js代码
-doc 文档
-test 单元测试
npm(Node Package Manager)
- node package manage(node包管理器)
- 通过npm命令安装jQuery包(npm install –save jquery),在安装时加上–save会主动生成说明书文件信息(将安装文件的信息添加到package.json里面)
- 对于Node而言,NPM帮助其完成了第三方模块的发布、安装依赖等。借助NPM,Node与第三方模块之间形成了很好的生态系统
npm指令1
2
3
4
5
6
7
8
9
10
11
12
13
14npm -v
<!-- 查看版本 -->
npm
<!-- 帮助说明 -->
npm search 包名
<!-- 搜索模块包 -->
npm install 包名 /i //需npm init创建package.json //entry point入口文件
npm install 包名 --save 安装包并添加到依赖中******
npm install下载当前项目所需要的包
<!-- 安装包 -->
npm install -g 包名 //全局安装
<!-- 删除包 -->
npm uninstall
npm被墙问题
npm存储包文件的服务器在国外,有时候会被墙,速度很慢,所以需要解决这个问题。
参加到配置项中
1 | npm config set registry https://npm.taobao.org; |
buffer(缓冲区)
- Buffer的结构和数组很像,操作的方法也和数组类似
- 数组中不能存储二进制文件,而二进制就是专门用来存储二进制数据的
- 使用buffer不需要引入模块,直接使用即可
- buffer中存储的数据都是二进制数据,但是在显示时候都是16禁止的形式显示
- buffer中每一个元素的范围是从00
ff 0255
将一个字符串转化为buffer1
2
3var str ="Hello Atguigu"
var buf = Buffer.from(str)
console.log(buf) //<Buffer 48 65 6c 6f 20 41 74 67 75 69 67 75> - 创建一个指定大小的buffer(最好别用)一般用buffer.alloc(10)创建n个字节的buffer
1
var buf2= new Buffer(10) //10个字节的buffer
Buffer一旦确定,则不能修改,Buffer实际上是对底层内存的直接操作通过索引,来操作buf的元素1
var buf2 = Buffer:alloc(10)
1
2
3
4
5
6buf[0] = 88;
buf[1] = 255 ;
buf[2] = 0xaa;
buf[3] = 255;
...
buf[10]=10; //打印结果上不会显示,因为超出了范围 - 注:只要数字在控制台或页面中输出一定是10进制
如果需要则调用toString(2)
读取文件
- 在node中不能操纵相关的Dom Bom ,在浏览器中也无法操作node的相关代码
- fs是file-system的简写,是文件系统的意思
- Node中如果想要进行文件操作,就必须引入fs这个核心模块
- 在fs这个核心模块中就提供了所有文件操作相关的api
- 使用require方法加载fs核心模块
1
var fs =require('fs')
- 读取文件
- 第一个参数就是要读取的路径
- 第二个参数是一个回调函数
成功
data 数据
error null
失败
data undefined
error 错误对象文件的存储是二进制数据1
fs.readFile('./data/hello.txt',function(error,data){})
- 写入文件
第一个参数:文件路径
第二个参数:文件内容
第三个参数:回调函数
良好反馈写法
1 | fs.writerFile('./data/你好.txt','大家好,给大家介绍一下,我是Node.js',function(error){ |
简单的http服务
- 在Node中专门提供了一个核心模块:http
- http这个模块的职责就是帮你创建编写服务器的
- 加载http核心模块
1
var http = require('http')
- 使用 http.creatServer()方法创建一个Web服务器
1
var server = http.createServer()
- 服务器要干嘛?
提供服务:对数据的服务
发请求
接受请求
处理请求
发送响应
注册request请求事件
当客户端请求过来,就会自动触发服务器的request请求事件,然后执行第二个参数:回调处理
Request 请求事件处理函数,需要接收两个参数:
Request 请求对象
请求对象可以用来获取客户端的一些请求信息,例如请求路径
Reponse 响应对象
响应对象可以用来给客户端发送响应消息1
2
3
4
5
6
7
8
9server.on('request', function () {
console.log('收到客户端的请求了,请求路径是:'+request.url)
// reponse 对象有一个方法,write可以用来给客户端发送响应数据
// write 可以使用多次,但是最后一次一定要用end来结束响应,否则客户端一直等待
response.write('hello')
responce.write('node.js')
// 告诉客户端,我的话说完了,你可以给用户了
response.end()
}) - 绑定端口号向不同页面发送响应不同内容
1
2
3server.listen(3000, function () {
console.log('服务器启动成功了,可以通过http://127.0.0.1:3000/来进行访问')
})
- 注:多用几组if else来呈现页面响应内容只能是二进制数据或者字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var http = require('http')
var server = http.createServer()
server.on('request', function (req, res) {
console.log('收到客户端的请求了')
var url = req.url
if (url === '/') {
res.end('index page')
} else if (url === '/login') {
res.end('login page')
} else {
res.end('404 Not Found')
}
})
server.listen(3000, function () {
console.log('服务器启动成功了,可以通过http://127.0.0.1:3000/来进行访问')
})
例如此时可以利用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16if(url==='/products'){
var products=[
{
name:'苹果15',
price:8888
},
{
name:'苹果16',
price:8888
},
{
name:'苹果17',
price:8888
}
]
}res.end(JSON.stringify(products))
来转化二进制
利用Content-type修改乱码
在Http协议里,Content-type
就是用来告诉对方我给你发的数据内容是什么类型
1 | res.setHeader('Content-type'.'text/plain',charset='utf-8') |
在服务器中编写
1 | var http = require('http') |
结合fs发送文件中的数据
不同资源对应的Content-Type是不一样的
一般只为字符数据才指定编码var parseObj=url.parse(req.url),true
第二个参数为true时可以将query中的参数变成对象parseObj.pathname
相当于(url === '/')
1 | server.on('request', function (req, res) { |
初步实现Apache功能
1 | // 监听server的request请求事件,设置请求处理函数 |
模板引擎
- 安装npm unstall art-template
- 在需要使用的文件模块中加载art-template
只需要使用require方法加载就可以了,require(‘art-template’)
参数中的art-template就是你下载的包的名字 - 查文档 查看API
1
2
3
4
5
6var template = require('art-template')
// render接收字符串
var ret = template.render(`hello {{ name }}`, {
name: 'Jack'
})
console.log(ret)
服务器渲染和客户端渲染的区别
- 客户端渲染不利于SEO搜索引擎优化
- 服务端渲染时可以被爬虫抓取到的,客户端异步渲染是很难被爬虫抓取到的
- 所以你会发现真正的网站既不是纯异步也不是纯服务端渲染出来的
- 例如京东的商品列表采用的是服务端渲染
处理网站中的静态资源
当浏览器收到HTML响应内容之后,就要开始从上到下依次解析,
当在解析过程中,如果发现:
link
script
img
iframe
video
audio
带有src或有href(link)
属性标签(具有外链资源)的时候,浏览器会自动对这些资源发起新的请求
为了方便的统一处理这些静态资源,所以我们约定把所有的静态资源都存放在public中
1 | else if(url.indexOf(`/public/`))===0 |
注意
在服务器中,文件中的路径就不要写相对路径,这时所有的资源是通过url标识来获取的
这里的请求路径写成/public/xxx
/在这里就是url根路径的意思
1 | <link href='/public/lib/bootstrap/dist/css/bootstrap.css'> |
服务器让客户端重定向
- 在状态码设置302临时重定向
statusCode
- 在响应头中通过Location告诉客户端往哪重定向
setHeader
- 如果客户端发现收到服务器的响应状态码是302就会自动去响应头中找Location
1
2
3
4
5
6
7
8
9
10
11res.statusCode=302
res.setHeader('location','/')
res.end()
else{
// 其他的都处理成404
fd.readFile('',function(err,data){
if(err){
return res.end('404 Not Found.')
}
})
}
Node.js REPL交互式解释器
- 表示一个电脑的环境,类似window的终端,我们在终端输入命令,并接受系统的响应。
使用
调出node
win+r调出cmd后输入node1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34$ node
> x = 10
10
> var y = 10
undefined
> x + y
20
> console.log("Hello World")
Hello World
undefined
> var x = 0
undefined
> do {
... x++;
... console.log("x: " + x);
... } while ( x < 5 );
x: 1
x: 2
x: 3
x: 4
x: 5
undefined
> var x = 10
undefined
> var y = 20
undefined
> x + y
30
> var sum = _
undefined
> console.log(sum)
30
undefined
CommonJS模块规范
在Node中的JavaScript还有一个重要的概念,模块系统。
- 模块作用域
- 使用require方法来加载模块
- 使用exports接口对象导出模板中的成员
加载require
语法:作用:1
var 自定义变量名=require('模块')
- 执行被加载模块中的代码
- 得到被加载模块中的
exports
导出接口对象
导出exports
- Node中是模块作用域,默认文件中所有的成员只在当前模块有效
- 对于希望可以被其他模块访问到的成员,我们需要把这些公开的成员都挂载到exports接口对象中就可以了
导出多个成员(必须在对象中):或者1
2
3
4
5
6
7
8exports.a = 123;
exports.b = function(){
console.log('bbb')
};
exports.c = {
foo:"bar"
};
exports.d = 'hello';导出单个成员(拿到的就是函数,字符串):1
2
3
4
5
6module.exports={
foo = 'hello',
add:function(){
return x+y;
}
}1
module.exports = 'hello';
以下情况会被覆盖
1 | module.exports = 'hello'; |
模块原理
1 | console.log(exports === module.exports); //true |
当给exports重新赋值后,exports!= module.exports.
最终return的是module.exports,无论exports中的成员是什么都没用。
1 | 真正去使用的时候: |
require 加载规则
优先从缓存加载
判断模块标识符
核心模块
自己写的模块(路径形式的模块)
第三方模块(node_modules)
第三方模块的标识就是第三方模块的名称(不可能有第三方模块和核心模块的名字一致)
npm
开发人员可以把写好的框架库发布到npm上
使用者通过npm命令来下载
使用方式:var 名称 = require(‘npm install【下载包】 的包名’)
node_modules/express/package.json main
如果package.json或者main不成立,则查找被选择项:index.js
如果以上条件都不满足,则继续进入上一级目录中的node_modules按照上面的规则依次查找,直到当前文件所属此盘根目录都找不到最后报错1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34// 如果非路径形式的标识
// 路径形式的标识:
// ./ 当前目录 不可省略
// ../ 上一级目录 不可省略
// /xxx也就是D:/xxx
// 带有绝对路径几乎不用(D:/a/foo.js)
// 首位表示的是当前文件模块所属磁盘根目录
// require('./a');
// 核心模块
// 核心模块本质也是文件,核心模块文件已经被编译到了二进制文件中了,我们只需要按照名字来加载就可以了
require('fs');
// 第三方模块
// 凡是第三方模块都必须通过npm下载(npm i node_modules),使用的时候就可以通过require('包名')来加载才可以使用
// 第三方包的名字不可能和核心模块的名字是一样的
// 既不是核心模块,也不是路径形式的模块
// 先找到当前文所述目录的node_modules
// 然后找node_modules/art-template目录
// node_modules/art-template/package.json
// node_modules/art-template/package.json中的main属性
// main属性记录了art-template的入口模块
// 然后加载使用这个第三方包
// 实际上最终加载的还是文件
// 如果package.json不存在或者mian指定的入口模块不存在
// 则node会自动找该目录下的index.js
// 也就是说index.js是一个备选项,如果main没有指定,则加载index.js文件
//
// 如果条件都不满足则会进入上一级目录进行查找
// 注意:一个项目只有一个node_modules,放在项目根目录中,子目录可以直接调用根目录的文件
var template = require('art-template');
模块标识符中的/和文件操作路径中的/
1 | // 咱们所使用的所有文件操作的API都是异步的 |
模块操作路径
1 | // 在模块加载中,相对路径中的./不能省略 |
package.json和package-lock.json
npm 5以前是不会有package-lock.json这个文件
npm5以后才加入这个文件
当你安装包的时候,npm都会生成或者更新package-lock.json这个文件
- npm5以后的版本安装都不要加–save参数,它会自动保存依赖信息
- 当你安装包的时候,会自动创建或者更新package-lock.json文件
- package-lock.json这个文件会包含node_modules中所有包的信息(版本,下载地址。。。)
- 这样的话重新npm install的时候速度就可以提升
- 从文件来看,有一个lock称之为锁
- 这个lock使用来锁版本的
- 如果项目依赖了1.1.1版本
- 如果你重新install其实会下载最细版本,而不是1.1.1
- package-lock.json的另外一个作用就是锁定版本号,防止自动升级
修改完代码自动重启
我们在这里可以使用一个第三方命名行工具:nodemon来帮助我们解决频繁修改代码重启服务器的问题。
nodemon是一个基于Node.js开发的一个第三方命令行工具,我们使用的时候需要独立安装:
1
2
3
4
5
6
7#在任意目录执行该命令都可以
#也就是说,所有需要 --global安装的包都可以在任意目录执行
npm install --global nodemon
npm install -g nodemon
#如果安装不成功的话,可以使用cnpm安装
cnpm install -g nodemon安装完毕之后使用:
1
2
3
4node app.js
#使用nodemon
nodemon app.js
只要是通过nodemon启动的服务,则他会监视你的文件变化,当文件发生变化的时候,会自动帮你重启服务器。
学习express
- 安装
1
cnpm install express
- 引入express
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 引入express
var express = require('express');
// 1. 创建app
var app = express();
// 2.
app.get('/',function(req,res){
// 1
// res.write('Hello');
// res.write('World');
// res.end()
// 2
// res.end('hello world');
// 3
res.send('hello world');
})
app.listen(3000,function(){
console.log('express app is runing...');
}) - 基本路由
路由:
- 请求方法
- 请求路径
- 请求处理函数
get:post:1
2
3
4//当你以get方法请求/的时候,执行对应的处理函数
app.get('/',function(req,res){
res.send('hello world');
})1
2
3
4//当你以post方法请求/的时候,执行对应的处理函数
app.post('/',function(req,res){
res.send('hello world');
})
路由的基本使用
1 | var express = require('express') |
使用Express建立基本服务器
1 | const express = require('express') |
- Express静态服务API
使用app.use
app.use不仅仅是用来处理静态资源的,还可以做很多的工作(body-parser的配置)使用静态资源1
2
3
4
5
6
7
8
9app.use(express.static('public'));
// 可以通过这个方便的创建一个静态资源服务器,利用这段代码就可以将public目录下的图片 css文件 Js文件对外开放了
// http://localhost:80/images/bg.jpg
// http://localhost:80/css/style.css
// http://localhost:80/js/login.js
// 注意:Express在指定静态目录中查找文件,并对外提供资源访问路径。因此,存放静态文件的目录不会出现在URL中
app.use(express.static('files'));
app.use('/stataic',express.static('public'));在Express中配置使用art-template的模板引擎1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// 引入express
var express = require('express');
// 创建app
var app = express();
// 开放静态资源
// 1.当以/public/开头的时候,去./public/目录中找对应资源
// 访问:http://127.0.0.1:3000/public/login.html
app.use('/public/',express.static('./public/'));
// 2.当省略第一个参数的时候,可以通过省略/public的方式来访问
// 访问:http://127.0.0.1:3000/login.html
// app.use(express.static('./public/'));
// 3.访问:http://127.0.0.1:3000/a/login.html
// a相当于public的别名
// app.use('/a/',express.static('./public/'));
//
app.get('/',function(req,res){
res.end('hello world');
});
app.listen(3000,function(){
console.log('express app is runing...');
});
- art-template官方文档
- 在node中,有很多第三方模板引擎都可以使用,不是只有
art-template
- 还有ejs,jade(pug),handlebars,nunjucks
安装:
1 | npm install --save art-template |
配置:
1 | app.engine('html', require('express-art-template')); |
使用:
1 | app.get('/',function(req,res){ |
如果希望修改默认的views视图渲染存储目录,可以:
1 | // 第一个参数views千万不要写错 |
示例:
app.js
1 | var express = require('express'); |
在Rxpress中获取表单请求数据
获取get请求数据
Express内置了一个api,可以直接通过req.query
来获取数据
1 | // 通过requery方法获取用户输入的数据 |
获取post请求数据
获取URL中的动态参数
通过req.params对象,可以访问到URL中,通过:匹配动态参数值
1 | app.get('/user/:id',(req,res) =>{ |
在Express中没有内置获取表单post请求体的api,这里我们需要使用一个第三方包body-parser
来获取数据
安装:
1 | npm install --save body-parser; |
配置:
// 配置解析表单 POST 请求体插件(注意:一定要在 app.use(router) 之前 )
1 | var express = require('express') |
模块化路由
创建路由模块
1 | // router.js |
注册路由模块
1 | const express = require('express') |
添加请求前缀
类似托管静态资源时,为静态资源统一挂载访问前缀一样,路由模块添加前缀的方式也非常简单
1 | var express = require('express') |
Express 中间件
- 中间件是指流程的中间处理环节
- 服务器收到请求后,可先调用中间件进行预处理
- 中间件是一个函数,包含 req, res, next 三个参数,next() 参数把流转关系交给下一个中间件或路由
中间件注意事项; - 在注册路由之前注册中间件(错误级别中间件除外)
- 中间件可连续调用多个
- 别忘记调用 next() 函数
- next() 函数后别写代码
- 多个中间件共享 req、 res对象
全局中间件
1 | const express = require('express') |
局部中间件
1 | const express = require('express') |
中间件分类
- 应用级别的中间件
- 通过
app.use()
或app.get()
或app.post()
,绑定到 app 实例上的中间件
- 路由级别的中间件
- 绑定到
express.Router()
实例上的中间件,叫做路由级别的中间件。用法和应用级别中间件没有区别。应用级别中间件是绑定到app
实例上,路由级别中间件绑定到router
实例上1
2
3
4
5
6
7
8
9const app = express()
const router = express.Router()
router.use(function (req, res, next) {
console.log(1)
next()
})
app.use('/', router)
- 错误级别的中间件
- 用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
- 错误级别中间件的处理函数中,必须有 4 个形参,形参顺序从前到后分别是 (err, req, res, next) 。
- 错误级别的中间件必须注册在所有路由之后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const 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')
})
- 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+ 版本中可用)1
2app.use(express.json())
app.use(express.urlencoded({ extended: false }))
示例
express.urlencoded({ extended: false })
1 | const express = require('express') |
app.use(express.json())
1 | const express = require('express') |
编写GET接口
router.js(路由)
1 | const express = require('express') |
index.js(接口启动)
1 | var express = require('express') |
编写POST接口
router.js(路由)
1 | const express = require('express') |
index.js(接口启动)
1 | var express = require('express') |
HTTP协议
(HTTP协议详细解释)[HTTP协议、GET请求和POST请求的区别 - 掘金 (juejin.cn)]
什么是HTTP协议
HTTP协议:是W3C制定的一种超文本传输协议。(通信协议:发送消息的模板提取被制定好)。
HTTP请求协议包括四部分
请求行
请求头
空白行
请求体
请求行
包括三部分:
第一部分:请求方式(7种)
get(常用的)
post(常用的)
delete(常用的)
put(常用的)
head(常用的)
options(常用的)
trace(常用的)
第二部分:URI
- URI:统一资源标识符。代表网络中某个资源的名字。但是通过URI是无法定位资源的。
- URI和URL什么关系,有什么区别?
- URL包括URI
- http://localhost:8080/servlet05/index.html 这是URL。
- /servlet05/index.html 这是URI
第三部分:HTTP版本协议号 请求头
请求的主机
主机的端口
浏览器信息
平台信息
cookie信息
…. 空白行
空白行是用来区分“请求头”和“请求体” 请求体
向服务器发送的具体数据 HTTP的响应协议(S – > B)
HTTP的相应协议包括:4部分
- 状态行
- 响应头
- 空白行
- 响应体
HTTP响应协议的具体报文:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16HTTP/1.1 200 ok 状态行
Content-Type: text/html;charset=UTF-8 响应头
Content-Length: 160
Date: Mon, 08 Nov 2021 13:19:32 GMT
Keep-Alive: timeout=20
Connection: keep-alive
空白行
<!doctype html> 响应体
<html>
<head>
<title>from get servlet</title>
</head>
<body>
<h1>from get servlet</h1>
</body>
</html>状态行(三部分组成)
第一部分:协议版本号(HTTP/1.1)
第二部分:状态码(HTTP协议中规定的响应状态号。不同的响应结果对应不同的号码。)
- 200 表示请求响应成功,正常结束。
- 400 表示访问的资源不存在,通常是因为要么是你路径写错了,要么是路径写对了,但是服务器中对应的资源并没有启动成功,总之404错误是前端错误。
- 450 表示前端发送的请求方式与后端请求的处理方法不一致发送:
- 比如:前端是POST请求,后端的处理方式按照get方法进行处理时,发送405
- 比如:前端是GET请求,后端的处理方式按照post方法进行处理时,发送405
- 500 表示服务器端的程序出现了异常。一般会认为是服务器端的错误导致的。
- 以4开始的,一般是浏览器端的错误导致的。
- 以5开始的,一般是服务器端的错误导致的。
第三部分:状态的描述信息
- ok 表示正常成功结束
- not found 表示资源找不到
响应头
- 响应的内容类型
- 响应的内容长度
- 响应的时间
- …
空白行
- 用来分隔“响应头”和“响应体”的。
响应体
- 响应体就是响应的正文,这些内容是一个长的字符串,这个字符串被浏览器渲染,解释并执行,最终展示出效果。
具体报文:GET请求
1 | GET /servlet05/getServlet?username=lucy&userpwd=1111 HTTP/1.1 请求行 |
HTTP请求协议的具体报文:POST请求
1 | POST /servlet05/postServlet HTTP/1.1 请求行 |
get请求和post请求的区别
- GET把参数包含在URL中,POST通过request body传递参数
所以get的参数长度有限制,而post无限制 - GET在浏览器回退时是无害的,而POST会再次提交请求。
- GET请求只能进行url编码,而POST支持多种编码方式。
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
- 对参数的数据类型,GET只接受ASCII字符,而POST没有限制
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息(相对安全,因为安全和加不加密有关)
第二种解释
get请求:发送数据的时候,数据会挂在URI的后面,并且在URI后面添加一个“?”,“?”后面是数据。这样会导致发送的数据会显示在浏览器的地址栏上。(get请求在“请求行”上发送数据)
post请求:发送数据的时候,在请求体当中发送。不会回显到浏览器的地址栏上,也就是说post发送的数据,在浏览器地址栏上看不到。(post在“请求体”当中发送数据)
get请求:只能发送普通的字符串。并且发送的字符串长度有限制,不同的浏览器限制不同。这个没有明确的规范。
get请求:无法发送大数据量。
get请求:支持缓存
post请求:可以发送任何类型的数据,包括普通字符串,流媒体等信息:视频、声音、图片。
post请求:可以发送大数据量,理论上没有长度限制。
get请求:在W3C中是这样说的:get请求比较适合从服务端获取数据。
post请求:在W3C中是这样说的:post请求比较适合从服务端传送数据。 可能好多人,一看到说get请求会把内容显示在地址栏上,就是不安全的
GET请求是安全的
et请求是绝对安全的,为什么?因为get请求只是为了从服务器上获取数据。不会对服务器造成威胁。
POST请求是危险的
为什么?因为post请求是向服务器提交数据,如果这些数据通过后门的方式进入到服务器当中,服务器是很危险的。另外post是为了提交数据,所以一般情况下拦截请求的时候,大部分会选择拦截(监听)post请求。
GET请求和POST请求如何选择,什么时候使用GET请求,什么时候使用POST请求?
- 怎么选择GET请求和POST请求呢?衡量标准是什么呢?你这个请求是想获取服务器端的数据,还是想向服务器发送数据,如果你是想从服务器上获取资源,建议使用GET请求,如果是为了向服务器提交数据,建议使用POST请求。
- 大部分的form表单提交,都是post方式,因为form表单中要填写大量的数据,这些数据是收集用户的信息,一般是需要传给服务器,服务器将这些数据保持或修改等。
- 如果表单中又敏感信息,还是建议使用post请求,因为get请求会回显铭感信息到浏览器地址栏上。(例如:密码信息)
- 做文件上传,一定是post请求。要传的数据不只是普通文本。
- 其他情况都可使用get请求
- 不管你是get请求还是post请求,发送的请求数据格式是完全相同的,只不过位置不同,格式都是统一的:
- name=value&name=value&name=value&name=value
- name是什么?
- 以form表单为例:form表单中input标签的name。
- value是什么?
- 以form表单为例:form表单中input标签的value。
postman的body四个参数类型
- form-data
既可以上传键值对,也可以上传文件。当上传的字段是文件时,会有content-type来说明文件类型;
content-disposition用来说明字段的一些信息;由于有boundary隔离,所以multipart/form-data既可以上传文件,也可以上传键值对,它采用了键值对的方式,所以可以上传多个文件。 - x-www-form-urlencoded:
就是application/x-www-from-urlencoded,会将表单内的数据转换为键值对 - raw
- 可以上传任意格式的文本,可以上传text、json、xml、html等
- binary
相当于content-type:application/octet-stream,从字面意思得知,只可以上传二进制数据,通常用来上传文件,由于没有键值,所以,一次只能上传一个文件。
COR跨域资源共享
- 安装中间件:
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+)。
CORS 常见响应头
Access-Control-Allow-Origin
:制定了允许访问资源的外域 URL1
2res.setHeader('Access-Control-Allow-Origin', 'http://bruceblog.io')
res.setHeader('Access-Control-Allow-Origin', '*')- 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 对额外的请求头进行声明,否则这次请求会失败!
1
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 方法
1
2res.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)
在项目中操作MySQL
安装mysql模块
1
npm install mysql
建立连接
1
2
3
4
5
6
7
8const mysql = require('mysql')
const db = mysql.createPool({
host: '127.0.0.1', // 数据库的ip
user: 'root', // 登录数据库的账号
password: 'root', // 登录数据库的密码
database: 'mydb01',// 指定要操作哪个数据库
})测试是否正常工作
1
2
3
4db.query('select 1', (err, results) => {
if (err) return console.log(err.message)
console.log(results)
})出现
[ RowDataPacket { '1': 1 } ]
则说明操作正常插入语句的写法
- 普通写法
1
2
3
4
5
6
7
8const user = { sno: '2128224012', sa: 50, sna: 'yuanhao1' }
// ? 表示占位符
const sql = 'insert into users values(?, ?, ?)'
// 使用数组的形式为占位符指定具体的值
db.query(sql, [username.sno, user.sna,username.sA], (err, results) => {
if (err) return console.log(err.message)
if (results.affectedRows === 1) console.log('插入成功')
}) - 简便写法
向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速插入数据:1
2
3
4
5
6
7
8const user = { sno: '2128224012', sa: 50, sna: 'yuanhao1' }
// ? 表示占位符
const sql = 'insert into mytable set ?'
// 使用数组的形式为占位符指定具体的值
db.query(sql, user, (err, results) => {
if (err) return console.log(err.message)
if (results.affectedRows === 1) console.log('插入成功')
})
- 更新数据写法
1
2
3
4
5
6
7const users = { sno: '2128224042', sA: 5, sna: '凯因' }
const sqlStr = 'update mytable set ? where sno=?'
db.query(sqlStr, [users, users.sno], (err, results) => {
if (err) return console.log(err.message)
if (results.affectedRows === 1) console.log('更新成功')
}) - 删除数据成功
1
2
3
4
5const sqlSt = 'delete from mytable where sno=?'
db.query(sqlSt, '212822404', (err, results) => {
if (err) return console.log(err.message)
if (results.affectedRows === 1) console.log('删除成功')
}) - 标记删除
- 使用 delete 语句会真正删除数据,保险起见,使用标记删除的形式,模拟删除的动作。即在表中设置状态字段,标记当前的数据是否被删除。
1
2
3
4
5db.query('update mytable set status=1 where sno=?', '212822401', (err, results) => {
if (err) return console.log(err.message)
console.log(results)
})
Session认证机制
服务器端推荐使用Session认证机制
Express中使用Session中间件
- 安装express-session中间件
1
npm i express-session
- 配置中间件
1
2
3
4
5
6
7
8const session = require('express-session')
app.use(
session({
secret: 'Bruce', // secret密钥 的值为任意字符串
resave: false, // 固定写法
saveUninitalized: true, // 固定写法
})
) - 向中间件中存数据
中间件配置成功后,可通过req.session
访问session
对象,存储用户信息1
2
3
4
5
6
7
8
9
10app.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 // 将用户信息存储到session中
req.session.isLogin = true // 将用户的登录状态存储到session中
res.send({ status: 0, msg: 'login done' })
}) - 从 session 取数据
1
2
3
4
5
6app.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
1
2
3
4
5app.post('/api/logout', (req, res) => {
// 清空当前客户端的session信息
req.session.destroy()
res.send({ status: 0, msg: 'logout done' })
})
Express使用JWT
- 安装
- jsonwebtoken 用于生成JWT字符串
- express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
1
npm install jsonwebtoken express-jwt
- 为保证 JWT 字符串的安全性,防止其在网络传输过程中被破解,需定义用于加密和解密的 secret 密钥
- 生成 JWT 字符串时,使用密钥加密信息,得到加密好的 JWT 字符串
- 把 JWT 字符串解析还原成 JSON 对象时,使用密钥解密
1
2
3
4
5const jwt = require('jsonwebtoken')
const {expressjwt} = require('express-jwt')
// 密钥为任意字符串
const secretKey = 'Bruce'
- 生成JWT字符串
1
2
3
4
5
6
7
8
9
10app.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 对象
1
2// unless({ path: [/^\/api\//] }) 指定哪些接口无需访问权限
app.use(expressjwt({ secret: secretKey , algorithms: ["HS256"]}).unless({ path: [/^\/api\//] }))
- 获取用户信息
- 当 express-jwt 中间件配置成功后,即可在那些有权限的接口中,使用 req.user 对象,来访问从 JWT 字符串中解析出来的用户信息
1
2
3
4
5
6
7
8app.get('/admin/getinfo', (req, res) => {
console.log(req.auth)
res.send({
status: 200,
message: '获取信息成功',
data: req.auth,
})
})
- 捕获解析 JWT 失败后产生的错误
- 当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行
- 通过 Express 的错误中间件,捕获这个错误并进行相关的处理完整示例
1
2
3
4
5
6app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
return res.send({ status: 401, message: 'Invalid token' })
}
res.send({ status: 500, message: 'Unknown error' })
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// TODO_01:安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt
const jwt = require('jsonwebtoken')
const { expressjwt } = require('express-jwt')
// 解析 post 表单数据的中间件
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))
// TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'itheima No1 ^_^'
// TODO_04:注册将 JWT 字符串解析还原成 JSON 对象的中间件
// 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上
app.use(expressjwt({ secret: secretKey, algorithms: ["HS256"] }).unless({ path: [/^\/api\//] }))
// 登录接口
app.post('/api/login', function (req, res) {
// 将 req.body 请求体中的数据,转存为 userinfo 常量
const userinfo = req.body
// 登录失败
if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
return res.send({
status: 400,
message: '登录失败!',
})
}
// 登录成功
// TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
// 参数1:用户的信息对象
// 参数2:加密的秘钥
// 参数3:配置对象,可以配置当前 token 的有效期
// 记住:千万不要把密码加密到 token 字符中
const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })
res.send({
status: 200,
message: '登录成功!',
token: tokenStr, // 要发送给客户端的 token 字符串
})
})
// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
// TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
console.log(req.auth)
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.auth, // 要发送给客户端的用户信息
})
})
// TODO_06:使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err, req, res, next) => {
// 这次错误是由 token 解析失败导致的
if (err.name === 'UnauthorizedError') {
return res.send({
status: 401,
message: '无效的token',
})
}
res.send({
status: 500,
message: '未知的错误',
})
})
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(80, function () {
console.log('Express server running at http://127.0.0.1:8888')
})