1. 简介

Node.js是一个开源的,跨平台的JavaScript运行环境。通俗来讲,node.js就是一款应用程序,是一款软件,它可以运行JavaScript。

1.1 Node.js的作用

1.开发服务器应用

众所周知,网页的构建是通过HTML、CSS、JavaScript来完成的,其中HTML负责控制结构、CSS负责控制样式、JS负责控制交互和效果。当在本机双击HTML文件后,页面就能够打开,但是这个网页只能在本机看到。如果想要该网页能够让每个人都能够访问,就需要使用服务器了。因为服务器能够保存我们写好的HTML、CSS、JavaScript,其他用户能够在自己的电脑上通过url来向我们的服务器发送请求,发送请求后,服务器能够将这些资源返回给用户的浏览器,然后浏览器就可以对这些资源做解析,页面就能够呈现了(所有的用户都能够通过url来访问服务器)。在这个过程中,node.js就运行在服务器端,它会对用户的请求做处理,并且把这些资源返回给浏览器。

2.开发工具类应用

目前前端开发中非常重要的三款工具Webpack、vite、Babel,它们可以提高前端项目的开发效率和质量,但都是借助于Node.js开发能力而实现的。所以可以借助node.js来创建一些属于自己的工具,来提高开发效率。

3.开发桌面端应用

对于代码编辑工具VSCode、设计工具Figma、接口测试工具Postman这三款软件都是借助electron框架,而这个框架又是借助node.js开发出来的,所以在学习了node.js后还可以去开发一些桌面端应用程序。

1.2 node.js编码注意事项

1.Node.js中不能使用BOM和DOM的API,可以使用console和定时器API。

  • BOM(Browser Object Model,浏览器对象模型)是与浏览器窗口进行互动的对象结构。它提供了一组可以操作浏览器窗口、历史记录、定时器等浏览器相关信息的API

  • DOM(Document Object Model,文档对象模型)则是一个与平台和语言无关的接口,它将网页的结构化文档表示为一个树形结构,允许程序和脚本动态地访问和更新文档的内容、结构和样式。

简而言之,BOM API 允许开发者与浏览器窗口和浏览器本身进行交互,而 DOM API 则允许开发者访问和操作网页的内容和结构。两者都是 JavaScript 编程中的重要组成部分,使得开发者能够创建动态和交互式的网页应用。

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,它允许 JavaScript 运行在服务器端。由于 Node.js 运行在服务器端,而不是在浏览器环境中,因此它不提供浏览器特有的 API,比如 BOM 和部分 DOM API。

2.Node.js中的顶级对象为global,也可以用globalThis访问顶级对象。

  • 在 Node.js 环境中,全局对象被称为 global。这意味着在 Node.js 中定义的全局变量和函数会自动成为 global 对象的属性和方法。例如,如果你在 Node.js 脚本中定义了一个全局变量 var myGlobalVar = 'Hello, world!';,那么你可以通过 global.myGlobalVar 来访问这个变量。

  • globalThis 是一个相对较新的全局属性,它在所有环境中(包括浏览器和 Node.js)都指向全局对象。这意味着无论在哪个环境中,你都可以使用 globalThis 来访问全局作用域下的变量和函数。在 Node.js 中,globalThis 指向的就是 global 对象。

2. Buffer(缓冲器)

Buffer中文译为缓冲区,是一个类似于Array的对象,用于表示固定长度的字节序列。换句话说,Buffer就是一段固定长度的内存空间,用于处理二进制数据。

2.1 介绍

1.概念:Buffer是一个类似于数值的对象,用于表示固定长度的字节序列。Buffer本质是一段内存空间,专门用来处理二进制数据

2.特点

  • Buffer大小固定且无法调整
  • Buffer性能较好,可以直接对计算机内存进行操作
  • 每个元素的大小为1字节(byte)

2.2Buffer的使用

1.buffer的创建有三种方法,分别是alloc、allocUnsafe和from。如下程序所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
//1. alloc
let buf = Buffer.alloc(10); //创建一个10字节的buffey,Buffer是nodejs的内置模块,在启动时已经被加载进来,不需要手动导入就可以使用,它可以理解为全局变量
console.log(buf); //用alloc创建的方法,每个二进制位都会归0(清0)

//2.allocUnsafe
let buf_2 = Buffer.allocUnsafe(1000); //这种方法创建的buffer,可能包含旧的内存数据(之前用完但没有清0的),但速度比alloc方法快
console.log(buf_2);

//3.from
let buf_3 = Buffer.from('hello'); //这种方法可以将一个字符串或数组转为buffer
let buf_4 = Buffer.from([105, 108, 111, 118, 101, 121, 111, 117]);
console.log(buf_3);
console.log(buf_4);

2.Buffer与字符串的转换

1
2
3
//buffer与字符串的转换
let buf_1 = Buffer.from([105,108,111,118,101,121,111,117]);
console.log(buf_1.toString()); //默认采用的是utf-8的方式进行转换

3.Buffer的读写:可以直接通过[]的方式对数据进行处理

1
2
3
4
5
6
let buf = Buffer.from('hello');
console.log(buf[0]); //将h通过utf-8进行转换为十进制数 结果:104
console.log(buf[0].toString(2)); //将h转换为2进制数 结果:1101000
console.log(buf); //先打印buf内容 结果<Buffer 68 65 6c 6c 6f>
buf[0] = 95; //对第一位h进行修改
console.log(buf.toString()); //打印修改的结果(转换成字符串了) 结果:_ello

4.注意事项

如果数值超过255,则超过8位,数据会被舍弃:

1
2
3
let buf = Buffer.from('hello');
buf[0] = 361; //舍弃高位数字(超过8位的,大于255的) 361=000101101001 => 0110 1001,即69
console.log(buf); //打印结果为 <Buffer 69 65 6c 6c 6f>

中文是utf-8的中文,一个中文一般占3个字节:

1
2
let buf = Buffer.from('你好');     //是6个字节
console.log(buf); //结果:<Buffer e4 bd a0 e5 a5 bd>

3. 计算机基础

3.1 计算机基本组成

1.CPU:中央处理器,是整个计算机运算和控制的中心

2.内存:是存储数据的一个介质,可以在里面存放大量的0和1这样的数据

  • 特点:读写速度较快,断电丢失数据。程序在运行时都会载入到内存当中,让CPU高速的对这个数据进行执行和处理

3.硬盘:也可以存储很多0和1这样的数据,

  • 特点:读写速度较慢,断电不丢失数据。平时下载的程序,如英雄联盟、浏览器、QQ等这些程序下载安装完后都是放到了硬盘里面。

4.显卡:负责处理视频信号的,当有信息需要呈现,需要在显示器中显示时,就会将信号传递给显卡,显卡处理完毕后,再将信号传递给显示器,然后显示器最终显示。

5.主板:是一块大的集成电路板,上面有很多插槽,很多元器件就是插在插槽里面,通过主板联系在一起的。

除了上面器件,再通过一些外设,如显示器、键盘、鼠标和音响等,整个电脑就算组装完毕了。

3.2 程序运行的基本流程

当所有器件都组装完毕之后,计算机也不能正常去运行。因为还缺少一个操作系统,常见的操作系统有Windows、Linux和MacOS。操作系统也是一种应用程序(010101..),用来管理和调度硬件资源。可以理解为,操作系统能够让cpu去执行哪个程序。

装系统:就是将操作系统这个程序安装在硬盘的这样一个过程。当把这个操作系统程序装到硬盘之后,电脑就可以开机运行了。

运行流程:首先会先将Windows相关的一些程序文件载入到内存里面,载入内存之后,CPU就可以运行了。CPU执行的时候,如果发现有视频信号需要在显示器上去呈现,就会交给显卡去处理,显卡处理完之后,再交由显示器去呈现。再处理过程当中,如果遇到了声音信号,这时候会交给声卡,声卡再将信号传递给外部的播放设备(如耳机、音响等)。它们一结合,就会呈现出视频和声音一起播放的效果

程序一般保存在硬盘中(还有一些是存在软盘这样一些介质里面的),软件安装的过程就是将程序写入硬盘的过程。程序在运行时会加载进入内存,然后由CPU读取并执行程序。

4. fs模块

fs模块可以实现与硬盘的交互。例如文件的创建、删除、重命名、移动,还有文件内容的写入、读取,以及文件夹的相关操作。

文件写入的应用场景:当需要持久化保存数据的时候,应该想到文件写入。

  • 下载文件
  • 安装软件
  • 保存程序日志,如Git
  • 编辑器保存文件
  • 视频录制

文件读取应用场景

  • 电脑开机
  • 程序运行
  • 编辑器打开文件
  • 查看图片
  • 播放视频
  • 播放音乐
  • Git查看日志
  • 上传文件
  • 查看聊天记录

4.1 读写操作

1.writeFile异步写入

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

参数说明:

  • file:文件名(不存在会自动创建)
  • data:待写入的数据
  • options:选项设置(可选)
  • callback:写入回调(当写入完成之后,会自动调用该回调函数)

测试程序:在下面程序中,代码先自上而下运行,当运行到writeFile时,会进行磁盘的写入,它会将这个磁盘写入交给另一个线程(IO线程)去完成,而主线程会继续向下执行。当IO线程写入完毕之后,会将回调函数压入到队列当中,等js的主线程给初始化的代码执行完毕之后,就会从任务队列中将这个回调函数取出来执行。如下面程序的结果是先打印12,再打印写入成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//需求:新建一个文件夹,座右铭.txt,写入的内容:三人行,必有我师焉
//1. 导入 fs 模块
const fs = require('fs');

//2. 写入文件
fs.writeFile('./座右铭.txt', '三人行,必有我师焉', err=>{ //当写入完成之后,会自动调用该回调函数,并将错误传递给这个函数
//err写入失败:错误对象;写入成功:null
if(err){
console.log('写入失败');
return;
}
console.log('写入成功');
});
console.log(3+9);

2.writeFileSync同步写入

语法:fs.writeFileSync(file, data)

参数说明:

  • file:文件名(不存在会自动创建)
  • data:待写入的数据

测试程序:创建了一个data.txt文件,向里面写入了test

1
2
const fs = require('fs');
fs.writeFileSync('./data.txt', `test`);

3.appendFile异步追加

1
2
3
4
5
6
7
8
const fs = require('fs');
fs.appendFile('./座右铭.txt', '择其善者而从之,择其不善者而改之',err=>{
if(err){
console.log('写入失败');
return;
}
console.log('写入成功');
});

4.appendFileSync同步追加

1
2
const fs = require('fs');
fs.appendFileSync('./座右铭.txt', '\r\n温故而知新,可以为师矣'); // \r\n表示换行

5.writeFile实现追加写入

1
2
3
4
5
6
7
8
fs.writeFile('./座右铭.txt', 'love love love',{flag: 'a'}, err=>{
//a是追加;w是写入(覆盖);r是读;默认是w
if(err){
console.log('写入失败');
return;
}
console.log('写入成功');
});

6.createWriteStream流式写入

createWriteStream会根据传入的参数路径,与它建立一个通道,啥时候想写,就通过write往通道里面传入内容即可。

1
2
3
4
5
6
const ws = fs.createWriteStream('./观书有感.txt');     //引入写入流对象(接收一个文件路径的参数)
ws.write('半亩方糖一签开\r\n');
ws.write('天光云影共徘徊\r\n');
ws.write('问渠那得清如许\r\n');
ws.write('有源头活水来\r\n');
ws.close(); //关闭通道

特点:程序打开一个文件是需要消耗资源的,流式写入可以减少打开关闭文件的次数。

流式写入方式适用于大文件写入或者频繁写入的场景,writeFile适用于写入频率较低的场景

7.readFile异步读取

1
2
3
4
5
6
7
8
fs.readFile('./观书有感.txt',(err,data) => {
//err接收错误的信息;data用来接收读取的文件内容
if(err){
cconsole.log('读取失败~~');
return;
}
console.log(data.toString());
});

8.readFileSync同步读取

1
2
let data = fs.readFileSync('./观书有感.txt');
console.log(data.toString());

9.createReadStream流式读取

在绑定事件中,当从文件当中读取出来一块数据之后,就会执行一次回调,并把读取到的内容传递给形参chunk。这种方法相比其他读取方法,效率会更高。

1
2
3
4
5
6
7
8
9
10
//创建读取流对象
const rs = fs.createReadStream('./video.mp4');
//绑定data事件(参数:事件的名字;带参数chunk的回调函数)
rs.on('data', chunk => {
console.log(chunk.length); //65536字节 => 64KB(每次读取的大小)
});
//当文件读取完毕之后,会触发该事件(可选事件)
rs.on('end', () => {
console.log('读取完成');
});

10.文件移动和重命名

可以使用renamerenameSync来移动或重命名文件或文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//进行重命名,将座右铭.txt改为论语.txt
fs.rename('./座右铭.txt', './论语.txt', err=>{
if(err){
console.log('操作失败');
return;
}
console.log('操作成功');
});

//进行移动
fs.rename('./data.txt', '../资料/data.txt', err=>{
if(err){
console.log('操作失败');
return;
}
console.log('操作成功');
});

11.删除文件

unlink方法对应的同步方法是unlinkSyncrm方法对应的同步方法是reSync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fs.unlink('./论语.txt', err=>{
if(err){
console.log('操作失败');
return;
}
console.log('操作成功');
});

fs.rm('./data.txt', err=>{
if(err){
console.log('操作失败');
return;
}
console.log('操作成功~');
});

4.2 文件夹操作

通过node,js可以对文件夹进行创建、读取、删除等操作

方法 说明
mkdir/mkdirSync 创建文件夹
readdir/readdirSync 读取文件夹
rmdir/rmdirSync 删除文件夹

1.创建文件夹mkdir

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//创建单个文件夹html
fs.mkdir('./html', err=>{
if(err){
console.log('创建失败');
return;
}
console.log('创建成功');
});

//递归创建多层文件夹
fs.mkdir('./a/b/c',{recursive: true}, err=>{
if(err){
console.log('创建失败');
return;
}
console.log('创建成功');
});

2.读取文件夹readdir

1
2
3
4
5
6
7
fs.readdir('./', (err, data) => {
if(err){
console.log('读取失败');
return;
}
console.log(data);
});

3.删除文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//删除单层文件夹
fs.rmdir('./html', err => {
if(err){
console.log('删除失败');
return;
}
console.log('删除成功');
});

//递归删除多层文件夹
fs.rm('./a', {recursive: true}, err => { //如果用rmdir也可以,但有警告
if(err){
console.log('删除失败');
return;
}
console.log('删除成功');
});

4.查看资源的状态

1
2
3
4
5
6
7
8
9
fs.stat('./video.mp4', (err,data) => {
if(err){
console.log('操作失败');
return;
}
console.log(data); //打印出指定文件的状态数据
console.log(data.isFile()); //是否是文件
console.log(data.isDirectory()); //是否是文件夹
});

5.’全局变量’__dirname

相对路径参照物是命令行的工作目录,所以有时候在代码中写相对路径时,会因为命令行的工作目录而导致编写出错。

__dirname:保存的是所在文件的所有目录的绝对路径

1
fs.writeFileSync(__dirname + '/index.txt', 'love');      //在当前文件下的index.txt文件中写入love

5. path模块

path模块提供了操作路径的功能,下面是几个常用的API:

API 说明
path.resolve 拼接规范的绝对路径
path.sep 获取操作系统的路径分隔符
path.parse 解析路径并返回对象
path.basename 获取路径的基础名称
path.dirname 获取路径的目录名
path.extname 获取路径的扩展名

6. http模块

1.想要获取请求的数据,需要通过request对象

含义 语法
请求方法 request.method
请求版本 request.httpVersion
请求路径 request.url
URL路径 require(‘url’).parse(request.url).pathname
URL查询字符串 require(‘url’).parse(request.url,true).query
请求头 request.headers
请求体 request.on(‘data’,function(chunk{}))

6.1 使用

1.用nodejs创建了一个HTTP服务端,在浏览器输入http://127.0.0.1:9000](http://127.0.0.1:9000/)后,回车,网页上会显示`Hello HTTP Server`。如果监听的端口为80的话,浏览器输入127.0.0.1即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//1.导入http模块
const http = require('http');

//2.创建服务对象(当收到请求,就会调用回调函数)
//request是对请求报文的一个封装对象,可以获取到请求报文里面的相关内容;response是对响应报文的一个封装,可以设置响应给对端的结果
const server =http.createServer((request,response) => { //当服务接收到http请求的时候,该函数就会执行
//获取请求的方法
//console.log(request.method);
//获取请求的url
//console.log(request.url); //只包含url中的路径与查询字符串(不包含协议、ip和端口)
//获取请求头
//console.log(request.headers);
//response.end('Hello HTTP Server'); //设置响应体,并结束响应
response.setHeader('content-type', 'text/html;charset=utf-8');
response.end('你好,世界'); //设置响应体(是中文的话会出现乱码,需要设置响应的字符集)
});

//3.监听端口,启动服务
server.listen(9000, ()=>{ //参数:端口号9000;回调函数(当这个服务启动成功后会执行)
console.log('服务已经启动.....');
});

HTTP协议默认端口是80,HTTPS协议的默认端口是443,HTTP服务开发常用端口有3000、8080、8090和9000等

如果端口被其他程序占用,可以使用资源监视器找到占用端口的程序,然后使用任务管理器关闭对应的程序。

2.获取请求体的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const http = require('http');

//request是对请求报文的一个封装对象,可以获取到请求报文里面的相关内容;response是对响应报文的一个封装,可以设置响应给对端的结果
const server = http.createServer((request,response) => { //当服务接收到http请求的时候,该函数就会执行
//1.声明一个变量(接收响应体的结果)
let body = '';
//2.给request绑定事件data
request.on('data',chunk => { //参数:给request绑定的一个data事件,回调函数
body += chunk; //将取出的数据放到body中
});
//3.绑定end事件(把可读流里面的数据都读完了,会触发end事件)
request.on('end', () => {
console.log(body); //打印请求体的内容
response.end('Hello HTTP');
});
});

//监听端口,启动服务
server.listen(9000, ()=>{ //参数:端口号9000;回调函数(当这个服务启动成功后会执行)
console.log('服务已经启动.....');
});

3.获取请求路径与查询字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const http = require('http');
const url = require('url'); //导入url模块

//request是对请求报文的一个封装对象,可以获取到请求报文里面的相关内容;response是对响应报文的一个封装,可以设置响应给对端的结果
const server = http.createServer((request,response) => { //当服务接收到http请求的时候,该函数就会执行
//解析request.url
console.log(request.url); //这是包含了路径和查询字符串
//通过url的parse解析request.url的结果
let res = url.parse(request.url, true); //参数2为true,可以使得结构体中的query属性(存放字符串的)变为一个对象,方便获得value值
//console.log(res); //可以得到请求的一个结构体(key:value)
let pathname = res.pathname; //获取路径
//console.log(pathname);
let keyword = res.query.keyword; //获得keyword对应的value值(因为解析时,参2为true,所以可以这样调用)
console.log(keyword);
response.end('url');
});

//监听端口,启动服务
server.listen(9000, ()=>{ //参数:端口号9000;回调函数(当这个服务启动成功后会执行)
console.log('服务已经启动.....');
});

案例:实现一个http的服务端,当url的路径是login时,访问的是登录界面;当url的路径是reg时,访问的是注册界面;当路径是其他时,访问的是Not Found界面。

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
//1.导入http模块
const http = require('http');

//2.创建服务对象
const server = http.createServer((request,response) => { //当服务接收到http请求的时候,该函数就会执行
//获取请求方法
let {method} = request;
//获取请求的url路径
let {pathname} = new URL(request.url,'http://127.0.0.1'); //参1:路径和key-value值
response.setHeader('content-type', 'text/html;charset=utf-8');
//判断
if(method === 'GET' && pathname === '/login'){
//登录的情形
response.end('登录界面');
}else if(method === 'GET' && pathname === '/reg'){
//注册的情形
response.end('注册界面');
}else{
//情况情形
response.end('Not Found');
}
});

//监听端口,启动服务
server.listen(9000, ()=>{ //参数:端口号9000;回调函数(当这个服务启动成功后会执行)
console.log('服务已经启动.....');
});

4.设置响应报文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const http = require('http');

const server = http.createServer((request,response) => { //当服务接收到http请求的时候,该函数就会执行
//1.设置响应状态码
response.statusCode = 203;
//2.设置状态描述
response.statusMessage = 'lxxl';
//3.响应头
response.setHeader('content-type', 'text/html;charset=utf-8'); //响应内容的类型和编码字符
response.setHeader('Server', 'Node.js'); //标识服务端的名字的
response.setHeader('myheader', 'test test test'); //也可以设置自己想加的
//4.响应头设置
response.write('lxxl'); //设置响应体,可以有多个write
response.end(); //一般使用了write,就不用end写(为空即可),end只能有一个
});

//监听端口,启动服务
server.listen(9000, ()=>{ //参数:端口号9000;回调函数(当这个服务启动成功后会执行)
console.log('服务已经启动.....');
});

案例:创建一个HTTP服务端,返回给浏览器一个注册界面(用html写好的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//node.js部分的程序
const http = require('http');
const fs = require('fs');

//创建服务对象
const server = http.createServer((request,response) => { //当服务接收到http请求的时候,该函数就会执行
//读取文件内容
let html = fs.readFileSync(__dirname + '/reg.html'); //拼接文件名,并读取
response.end(html); //设置响应体
});

//监听端口,启动服务
server.listen(9000, ()=>{ //参数:端口号9000;回调函数(当这个服务启动成功后会执行)
console.log('服务已经启动.....');
});
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<!-- html部分的程序 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h4 style="text-align: center;">青春不常在,抓紧谈恋爱</h4>
<table width="600" style="margin: 0 auto; border: 1px solid #ccc; padding: 20px;">
<!-- 第一行 -->
<tr>
<td>性别:</td>
<td>
<input type="radio" name="sex" id="nan"> <label for="nan"></label>
<input type="radio" name="sex" id="nv"> <label for="nv"></label>
</td>
</tr>
<!-- 第二行 -->
<tr>
<td>生日:</td>
<td>
<select>
<option>--请选择年份--</option>
<option>1998</option>
<option>1999</option>
<option>2000</option>
<option>2001</option>
</select>
<select>
<option>--请选择月份--</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
</select>
<select>
<option>--请选择日--</option>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
</select>
</td>
</tr>
<!-- 第三行 -->
<tr>
<td>所在地区:</td>
<td> <input type="text" value="中国上海"> </td>
</tr>
<!-- 第四行 -->
<tr>
<td>婚姻状况:</td>
<td>
<input type="radio" name="marry" id="one" checked="checked"><label for="one">未婚</label>
<input type="radio" name="marry" id="two"><label for="two">已婚</label>
<input type="radio" name="marry" id="three"><label for="three">离婚</label>
</td>
</tr>
<!-- 第五行 -->
<tr>
<td>学历:</td>
<td> <input type="text"> </td>
</tr>
<!-- 第六行 -->
<tr>
<td>喜欢的类型:</td>
<td>
<input type="checkbox" name="love"> 妩媚的
<input type="checkbox" name="love"> 可爱的
<input type="checkbox" name="love"> 性感的
<input type="checkbox" name="love"> 开朗的
</td>
</tr>
<!-- 第七行 -->
<tr>
<td>个人介绍:</td>
<td>
<textarea></textarea>
</td>
</tr>
<!-- 第八行 -->
<tr>
<td></td>
<td>
<input type="submit" value="免费注册">
</td>
</tr>
<!-- 第九行 -->
<tr>
<td></td>
<td>
<input type="checkbox" checked="checked"> 我同意注册条款和会员加入标准
</td>
</tr>
<!-- 第十行 -->
<tr>
<td></td>
<td>
<a href="#">我是会员,立即登录</a>
</td>
</tr>
<!-- 第十一行 -->
<tr>
<td></td>
<td>
<h5>我承诺</h5>
<ul>
<li>年满18岁</li>
<li>抱着严肃的态度</li>
<li>真诚的寻找另一半</li>
</ul>
</td>
</tr>
</table>
</body>
</html>

效果:

补充:HTTP服务在哪个文件夹中寻找静态资源,哪个文件夹就是静态资源目录,也称为网站根目录

5.设置资源类型(mime类型)

媒体类型是一种标准,用来表示文档、文件或字节流的性质和格式。

HTTP服务可以设置响应头Content-Type来表示响应体的MIME类型,浏览器会根据该类型决定如何处理资源。下面是常见文件对应的mime类型:

1
2
3
4
5
6
7
8
9
html: 'text/html'
css: 'text/css'
js: 'text/javascript'
png: 'image/png'
jpg: 'image/jpeg'
gif: 'image/gif'
mp4: 'video/mp4'
mp3: 'audio/mpeg'
json: 'application/json'

对于未知的资源类型,可以选择application/octet-stream类型,浏览器在遇到该类型的响应时,会对响应体内容进行独立存储,也就是常见的下载效果。

下面程序是设置响应体的MIME类型:

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
const http = require('http');
const fs = require('fs');
const path = require('path');

//声明一个变量
let mimes = {
html: 'text/html',
css: 'text/css',
js: 'text/javascript',
png: 'image/png',
jpg: 'image/jpeg',
gif: 'image/gif',
mp4: 'video/mp4',
mp3: 'audio/mpeg',
json: 'application/json'
}

//创建服务对象
const server = http.createServer((request,response) => { //当服务接收到http请求的时候,该函数就会执行
//读取请求url的路径
let {pathname} = new URL(request.url, 'http://127.0.0.1'); //获得路径
//声明一个变量
let root = __dirname + '/page'; //这是拼接资源目录
//拼接文件路径
let filePath = root + pathname; //得到对应资源目录下,对端要得到的文件的路径
fs.readFile(filePath, (err, data) => {
if(err){
response.setHeader('content-type', 'text/html;charset=utf-8');
response.statusCode = 500;
response.end('文件读取失败');
return;
}
//获取文件的后缀名
//let ext = path.extname(filePath); //如果url输入的路径是3.png,这里得到的就是.png
let ext = path.extname(filePath).slice(1); //从下标1开始截取,这里得到的是png
//获取对应的类型
let type = mimes[ext];
if(type){
//匹配到了
response.setHeader('content-type', type);
}else{
response.setHeader('content-type', 'application/octet-stream');
}
response.end(data); //设置响应体
})
});

//监听端口,启动服务
server.listen(9000, ()=>{ //参数:端口号9000;回调函数(当这个服务启动成功后会执行)
console.log('服务已经启动.....');
});

6.2 GET和POST请求

1.使用情况

  • GET请求的情况:
    • 在地址栏直接输入url访问
    • 点击a链接
    • link标签引入css
    • script标签引入css
    • video与audio引入多媒体
    • img标签引入图片
    • form标签中的method为get(不区分大小写)
    • ajax中的get请求
  • POST请求的情况:
    • form标签中的method为post(不区分大小写)
    • ajax的post请求

2.GET和POST请求的区别

  • 作用:GET主要用来获取数据,POST主要用来提交数据
  • 参数位置:GET带参数请求一般是将参数缀到URL之后,POST带参数请求一般是将参数放到请求体中
  • 安全性:POST请求相对GET安全一些,因为在浏览器中参数会暴露在地址栏
  • GET请求大小有限制,一般为2K,而POST请求则没有大小限制

7. Node.js模块化

7.1 简介

模块化的概念:将一个复杂的程序文件依据一定规则(规范)拆分成多个文件的过程称之为模块化

模块的概念:对拆分出的每个文件就是一个模块,模块的内部数据是私有的,不过模块可以暴露内部数据以便其它模块使用。

模块化的好处:

  • 防止命名冲突
  • 高复用性
  • 高维护性

7.2 操作

模块暴露数据的方式有两种:

  • module.exports = value
  • exports.name = value

注意:

  • module.exports可以暴露任意数据
  • 不能使用exports = value的形式暴露数据,模块内部module与exports的隐式关系是exports = module.exports = {}

1.创建一个需要暴露的程序me.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//声明一个函数
function tiemo(){
console.log('贴膜.....');
}

//捏脚
function niejiao(){
console.log('捏脚.....');
}

//暴露数据1
//module.exports = tiemo; //这里是只暴露了iemo函数
// module.exports = {
// tiemo,
// niejiao
// }

//暴露数据2
exports.niejiao = niejiao;
exports.tiemo = tiemo;

引用的程序

1
2
3
4
5
const me = require('./me.js');

//调用函数
me.tiemo();
me.niejiao();

2.导入模块

在模块中使用require传入文件路径即可引入文件:const test = require('./me.js');

require使用的一些注意事项:

  • 对于自己创建的模块,导入时路径建议写相对路径,且不能省略./../
  • js和json文件导入时可以不用写后缀,c/c++编写的node扩展文件也可以不用写后缀,但是一般用不到
  • 如果导入其它类型的文件,会以js文件进行处理
  • 如果导入的路径是个文件夹,则会首先检测该文件夹下package.json文件中的main属性对应的文件,如果存在则导入,反之如果文件不存在会报错。如果main属性不存在,或者package.json不存在,则会尝试导入文件夹下的index.js和index.json,如果还是没有找到,就会报错。
  • 导入node.js内置模块时,直接require模块的名字即可,无需加./../

8. 包管理工具

8.1 简介

包英文单词是package,代表了一组特定功能的源码集合

包管理工具:是管理包的应用软件,可以对包进行下载安装、更新、删除和上传等操作。借助包管理工具,可以快速开发项目,提升开发效率。包管理工具是一个通用的概念,很多编程语言都有包管理工具。

常用的包管理工具有:npm、yarm、cnpm

8.2 npm

npm是node.js官方内置的包管理工具,是必须要掌握的工具

初始化:创建一个空目录,然后以此目录作为工作目录启动命令行工具,执行npm init

npm init命令的作用是将文件夹初始化为一个包,交互式创建package.json文件,它是包的配置文件,每个包都必须要有package.json

1.属性解释

1
2
3
4
5
6
7
8
9
10
11
{
"name": "npm", //包的名字
"version": "1.0.0", //包的版本
"main": "index.js", //包的入口文件
"scripts": { //脚本配置
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "", //作者
"license": "ISC", //开源证书
"description": "" //包的描述
}

初始化过程中注意事项:

  • package name不能使用中文、大写,默认值是文件夹的名称,所以文件夹名称也不能使用中文和大写
  • version要求x.x.x的形式定义,x必须是数字,默认值是1.0.0
  • package.json开源手动创建与修改
  • 使用npm init -y或者npm init --yes极速创建package.json

2.生产环境与开发环境

开发环境是程序员专门用来写代码的环境,一般是指程序员的电脑,开发环境的项目一般只能程序员自己自己访问

生产环境是项目代码正式运行的环境,一般是指正式的服务器电脑,生产环境的项目一般每个客户都可以访问

3.生产依赖与开发依赖

可以在安装时设置选项来区分依赖的类型,如:

类型 命令 补充
生产依赖 npm i -S uniq -S等效于–save,-S是默认选项
开发依赖 npm i -D less -D等效于–save-dev

其中生产依赖的包信息保存在package.json中dependencies属性,开发依赖保存的包信息保存在package.json中的devDependencies属性

生产依赖是指这个依赖即在开发阶段使用,也在最终的运行当中使用

开发依赖是指这个依赖只在开发阶段使用,过了开发阶段就没有用了

4.全局安装

上面介绍的是局部安装,即只能在打开终端的那个文件夹下使用。可以执行安装选项-g进行全局安装,如npm i -g nodemon,全局安装完成之后就可以在命令行的任何位置运行nodemon命令了。这个命令的作用是自动重启node应用程序

说明:

  • 可以通过npm root -g查看全局安装包的位置
  • 不是所有的包都适合全局安装,只有全局类的工具才适合,可以通过查看包的官方文档来确定安装方式。

5.安装包依赖

在项目协作中有一个常用的命令就是npm i,通过该命令可以依据package.jsonpackage-lock.json的依赖声明安装项目依赖

node_modules文件夹大多数情况都不会存入版本库,因为当一个项目很大时,会用到很多的依赖包,而项目的依赖包都存在node_modules文件夹中,如果将该项目上传到Git,其他人下载时就会很麻烦。所以就可以下载除了node_modules文件夹的其它所有文件,然后通过npm i来下载项目所需要的依赖。

6.安装指定的包和删除依赖

如果当前包的版本不匹配,或要安装指定版本的包,可以通过npm i <包名@版本号>,如npm i jquery@1.11.2

删除依赖包可以使用:

  • 局部删除:npm remove 包名或者npm r 包名
  • 全局删除:npm remove -g 包名

7.配置命名别名

通过配置命名别名可以更简单的执行命令,如在package.json中的scripts属性中有如下配置:

1
2
3
4
5
6
7
8
{
.....
"scripts": {
"server": "node server.js",
"start": "node index.js",
},
.....
}

配置完之后,可以使用别名执行命令,如:

1
2
npm run server           //执行的是node server.js
npm run start //执行的是node index.js

不过start别名比较特别,使用时可以省略,如npm start

8.3 npm配置淘宝镜像

用npm也可以使用淘宝镜像,配置的方式有两种

  • 直接配置
  • 工具配置

1.直接配置

执行如下命令即可完成配置

1
npm config set registry https://registry.npmmirror.com/

2.工具配置(建议使用)

使用nrm配置npm的镜像地址npm registry manager

  • 安装nrm:npm i -g nrm
  • 修改镜像:nrm use taobao
  • 检查是否配置成功(可选),如果是https://registry.npmmirror.com/,则表名成功:npm config list

补充:查看支持的镜像地址:nrm ls

然后就可以通过nrm use 要使用的镜像来切换镜像地址了,如切换为官方地址nrm use npm

注意:淘宝镜像是只读的,只能下载,不能上传。比如说写好一个工具包,想要上传到npm去给其他人用,就需要切换为npm的官方地址才可以上传。

8.4 管理发布包

1.创建与发布

可以将自己开发的工具包发布到npm服务上,方便自己和其他开发者使用,操作步骤如下:

  • 创建文件夹,并创建文件index.js,在文件中声明函数,使用module.exports暴露
  • npm初始化工具包,package.json填写包的信息(包的名字是唯一的)
  • 注册账号https://www.npmjs.com/signup
  • 激活账号(必须)—–>填写验证码
  • 修改为官方的官方镜像(命令行中运行nrm use npm)
  • 命令行npm login填写相关用户信息
  • 命令行下npm publish提交包

2.更新包

后续可以对自己发布的包进行更新,操作如下:

  • 更新包中的代码
  • 测试代码是否可用
  • 修改package.json中的版本号(在原来基础上做++操作,相当于自己修改一次,也修改一次版本号)
  • 发布更新npm publish

3.删除包

执行命令npm unpublish --force即可

删除包需要满足的条件:

  • 要是包的作者
  • 发布小于24小时
  • 大于24小时后,没有其他包依赖,并且每周小于300下载量,并且只有一个维护者

4.nvm使用

下载地址:Releases · coreybutler/nvm-windows (github.com),选择nvm-setup.exe下载即可

常用的命令:

命令 说明
nvm list available 显示所有可以下载的Node.js版本
nvm list 显示已安装的版本
nvm install 18.12.1 安装18.12.1版本的Node.js
nvm install latest 安装最新版本的Node.js
nvm uninstall 18.12.1 删除某个版本的Node.js
nvm use 18.12.1 切换18.12.1版本的Node.js

9. express框架

express是一个基于Node.js平台的极简、灵活的WEB应用开发框架,官方网址:https://www.expressjs.com.cn/

简单来说,express是一个封装好的工具包,本身是一个npm包,所以可以通过npm安装。express封装了很多功能,便于开发WEB应用(HTTP服务)。

9.1 操作

首先是先创建一个文件夹,在该文件夹下的终端执行`npm init’,先生成一个包,然后执行npm i express,在该目录下下载express工具包,这样在该目录下就可以使用该工具包了。如下是程序程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1.导入express
const express = require('express');

//2.创建应用对象
const app = express();

//3.创建路由
app.get('/home', (req, res) => { //req是对请求报文的一个封装对象;res是响应报文的一个封装对象
//当浏览器把请求发送过来之后,如果请求的方法是get,并且请求url的路径是/home,就会执行后面的回调函数,用它来对浏览器响应结果
res.end('hello express');
});

//4.监听端口,启动服务
app.listen(3000, () => {
console.log('服务已经启动,端口3000正在监听中......');
});

1.express路由

路由确定了应用程序如何响应客户端对特定端点的请求。

一个路由的组成有请求方法路径回调函数组成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//1.get方法,路径为/home
app.get('/home', (req, res) => {
res.end('网址首页');
});
//2.post方法,路径为/login
app.post('/login', (req, res) => {
res.end('注册页面');
});
//3.匹配所有方法,路径为/test
app.all('/test', (req, res) => {
res.end('test test');
});
//4.404响应(当没有匹配成功路由时,执行该路由)
app.all('*', (req, res) => {
res.end('404 not Found');
});

2.获取请求报文参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get('/request', (req, res) => {      //req是对请求报文的一个封装对象;res是响应报文的一个封装对象
//原生操作
console.log(req.method); //获取请求方式
console.log(req.url); //获取请求路径和key-value
console.log(req.httpVersion); //获取http版本
console.log(req.headers); //获取请求头

//express操作
console.log(req.path); //获取请求路径
console.log(req.query); //获取key和value值,是用{}封装的
console.log(req.ip); //获取ip
console.log(req.get('host')); //获取ip和端口
res.end('hello express');
});

同时,为了更加的灵活运用,也可以通过占位符的方式来作为路径。在如下程序中,浏览器的路径只要是任意字符.html都可以匹配成功路由。

1
2
3
4
app.get('/:id.html', (req, res) => {    //通过:id占位符来完成,浏览器只要是
console.log(req.params.id); //获取路径(这里的id要与上面的占位符名字一样)
res.end('hello express');
});

3.响应设置

express框架封装了一些API来方便给客户端响应数据,并且兼容原生HTTP模块的获取方式

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
app.get('/singer/:id.html', (req, res) => {      //req是对请求报文的一个封装对象;res是响应报文的一个封装对象
//原生响应
res.statusCode = 404; //状态码
res.statusMessage = 'love'; //状态描述
res.setHeader('xxx', 'yyy'); //响应头
res.write('hello express '); //响应体

//express响应
res.status(500); //状态码
res.set('aaa', 'bbb'); //响应头
res.send('您好 express'); //响应体
res.status(500).set('abc', 'def').send('这都是OK的'); //也可以直接这样操作

//express其他的响应方法
//跳转响应
res.redirect('http://atguigu.com'); //页面会跳转到http://atguigu.com网址去
//下载响应
res.download(__dirname + '/package.json'); //会下载指定目录下的文件
//JSON响应
res.json({
name: 'lxx',
slogon: '让天下没有难学的技术'
}); //显示json内容
//响应文件内容
res.sendFile(__dirname + '/test.html'); //会显示test.html的这个页面

});

9.2 express中间件

中间件本质是一个回调函数,中间件函数可以像路由回调一样访问请求对象(request),响应对象(response)。

中间件的作用:就是使用函数封装公共操作,简化代码

中间件的类型:

  • 全局中间件:就是指一个请求发过来后,这个全局中间件就会执行,执行完毕后才去执行路由回调。(每一个请求过来,全局中间件一定会执行)
  • 路由中间件:就是指一个请求发过来后,路由中间件是和路由放在一起的,只有满足了某一个路由规则,它所对应的中间件才会去执行。

1.全局中间件实现:当客户端通过浏览器访问该服务器时,会对应的记录它的url和ip,然后保保存到一个文件中。

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
//1.导入模块
const express = require('express');
const fs = require('fs');
const path = require('path');

//2.创建应用对象
const app = express();

//3.声明一个中间件函数
function recoremiddleware(req, res, next){ //参数:请求报文对象;响应报文对象;内部函数,执行之后会指向后续的路由回调或者是中间件函数回调
//获取url和ip
let {url, ip} = req;
//将信息保存再文件中access.log(通过追加的方式写入)
fs.appendFileSync(path.resolve(__dirname, './access.log'), `${url} ${ip}\r\n`); //文件的路径;写入的内容。注意$要与反引号结合使用
//调用next,它会去执行后续的回调函数,如路由回调
next(); //如果不执行这行,那么就只记录了,没有回应给客户端
}

//4.使用中间件函数
app.use(recoremiddleware);

//5.创建路由
app.get('/home', (req, res) => {
res.send('前台首页');
});

app.get('/admin', (req, res) => {
res.send('后台首页');
});

app.all('*', (req, res) => {
res.send('<h1>404 Not Found</h1>');
});

//6.监听端口,启动服务
app.listen(3000, () => {
console.log('服务已经启动,端口3000正在监听中......');
});

2.路由中间件实现:当客户端通过浏览器发送请求后,先通过url来匹配执行哪个路由,当匹配完后,要执行中间件里面的代码,即判断有无携带code=520,如果有,就执行对应的路由回调;如果没有携带,就回应一个暗号错误。(防盗链可以用类似的方法实现)

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
//1.导入模块
const express = require('express');
const fs = require('fs');
const path = require('path');

//2.创建应用对象
const app = express();

//3声明一个中间件函数
function recoremiddleware(req, res, next){
//判断URL中是否code参数等于520
if(req.query.code === '520'){
next();
}else{
res.send('暗号错误');
}
}

//4.创建路由
app.get('/home',recoremiddleware, (req, res) => { //对该路由设置了中间件
res.send('前台首页');
});

app.get('/admin',recoremiddleware, (req, res) => { //对该路由设置了中间件
res.send('后台首页');
});

app.all('*', (req, res) => {
res.send('<h1>404 Not Found</h1>');
});

//5.监听端口,启动服务
app.listen(3000, () => {
console.log('服务已经启动,端口3000正在监听中......');
});

9.3 处理静态资源的中间件

使用注意事项:

  • index.html文件为默认打开的资源
  • 如果静态资源与路由规则同时都匹配,谁先匹配,谁就响应
  • 路由响应动态资源,静态资源中间件响应静态资源

当添加了静态资源中间件设置,再浏览器发送请求时,如果路径声明没有写(即/),服务端这边会先找index.html页面,即到public这个静态目录里面去找,找到了就读取内容,响应给浏览器。但如果路由规则也设置了/,那么谁先匹配上,就执行谁(执行顺序从上到下)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//1.导入模块
const express = require('express');
const fs = require('fs');
const path = require('path');

//2.创建应用对象
const app = express();

//静态资源中间件设置,将当前文件夹下的public目录作为网站的根目录
app.use(express.static(__dirname + '/public')); //当前这个目录中都是一些静态资源

//3.创建路由
app.get('/', (req, res) => {
res.send('前台首页');
});

//4.监听端口,启动服务
app.listen(3000, () => {
console.log('服务已经启动,端口3000正在监听中......');
});

比如说对应的public下有一个index.html文件,如下所示:

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
background-color: #ADD8E6; /* 浅蓝色 */
}
</style>
</head>
<body>
<h4 style="text-align: center;">青春不常在,抓紧谈恋爱</h4>
<table width="600" style="margin: 0 auto; border: 1px solid #ccc; padding: 20px;">
<!-- 第一行 -->
<tr>
<td>性别:</td>
<td>
<input type="radio" name="sex" id="nan"> <label for="nan"></label>
<input type="radio" name="sex" id="nv"> <label for="nv"></label>
</td>
</tr>
<!-- 第二行 -->
<tr>
<td>生日:</td>
<td>
<select>
<option>--请选择年份--</option>
<option>1998</option>
<option>1999</option>
<option>2000</option>
<option>2001</option>
</select>
<select>
<option>--请选择月份--</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
</select>
<select>
<option>--请选择日--</option>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
</select>
</td>
</tr>
<!-- 第三行 -->
<tr>
<td>所在地区:</td>
<td> <input type="text" value="中国上海"> </td>
</tr>
<!-- 第四行 -->
<tr>
<td>婚姻状况:</td>
<td>
<input type="radio" name="marry" id="one" checked="checked"><label for="one">未婚</label>
<input type="radio" name="marry" id="two"><label for="two">已婚</label>
<input type="radio" name="marry" id="three"><label for="three">离婚</label>
</td>
</tr>
<!-- 第五行 -->
<tr>
<td>学历:</td>
<td> <input type="text"> </td>
</tr>
<!-- 第六行 -->
<tr>
<td>喜欢的类型:</td>
<td>
<input type="checkbox" name="love"> 妩媚的
<input type="checkbox" name="love"> 可爱的
<input type="checkbox" name="love"> 性感的
<input type="checkbox" name="love"> 开朗的
</td>
</tr>
<!-- 第七行 -->
<tr>
<td>个人介绍:</td>
<td>
<textarea></textarea>
</td>
</tr>
<!-- 第八行 -->
<tr>
<td></td>
<td>
<input type="submit" value="免费注册">
</td>
</tr>
<!-- 第九行 -->
<tr>
<td></td>
<td>
<input type="checkbox" checked="checked"> 我同意注册条款和会员加入标准
</td>
</tr>
<!-- 第十行 -->
<tr>
<td></td>
<td>
<a href="#">我是会员,立即登录</a>
</td>
</tr>
<!-- 第十一行 -->
<tr>
<td></td>
<td>
<h5>我承诺</h5>
<ul>
<li>年满18岁</li>
<li>抱着严肃的态度</li>
<li>真诚的寻找另一半</li>
</ul>
</td>
</tr>
</table>
</body>
</html>

执行效果:

9.4 EJS模板引擎

模板引擎是分离用户界面和业务数据的一种技术,是一个高效的Javascript的模板引擎。

1.ejs普通渲染

创建一个文件夹,在文件夹的终端下执行npm i ejs,这样在该目录下就可以使用ejs工具包了。

先创建一个2-lxx.html文件,这里只是简单写了一行代码

1
<h2>我爱你 <%= china %></h2>

然后再创建一个js文件,来完成跨文件渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
//导入模块
const ejs = require('ejs');
const fs = require('fs');

//字符串
let china = '中国';
//声明变量(读取其它文件的内容)
let str = fs.readFileSync('./2-lxx.html').toString(); //读取2-lxx.html文件里面的内容,想要toString,不然是buffer类型

//使用ejs渲染
let result = ejs.render(str, {china:china}); //对str进行一个解析,找到<%=这些标识,然后把里面的内容替换为参2里面对应属性的value值

console.log(result); //输出的是<h2>我爱你 中国</h2>

2.ejs条件渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//导入模块
const ejs = require('ejs');

let isLogin = true;

//使用ejs渲染(这里也可以放到其它文件里,再调用)
let result = ejs.render(`
<% if(isLogin){ %>
<span>欢迎回来</span>
<% }else{ %>
<button>登录</button> <button>登录</button>
<% } %>
`, {isLogin: isLogin}); //对str进行一个解析,找到<%=这些标识,然后把里面的内容替换为参2里面对应属性的value值

console.log(result); //输出的是<h2>我爱你 中国</h2>

9.5 Express应用程序生成器

通过应用生成器工具express-generator可以快速创建一个应用的骨架。可以通过npx命令来运行Express应用程序生成器。

全局安装这个包:npm install -g express-generator

创建骨架:express -e generator,-e表示添加ejs模板引擎的支持,generator表示文件夹的名称(将代码装到哪个文件夹下)。

在生成骨架的文件夹下,安装依赖:npm i

启动服务端:npm start

9.6 案例实践-记账本

首先是创建一个文件夹,在该文件夹下创建一个应用裤架:express -e accounts

在accounts文件夹下安装依赖:npm i

需要用到一个小型的数据库lowdb:npm i lowdb@1.0.0

对每一个订单进行编号id,安装shortid:npm i shortid

10. API接口

10.1 简介

接口是前后端通信的桥梁,可以理解为一个接口就是服务中的一个路由规则,根据请求响应结果。这里所指的接口是数据接口,与编程语言(java、go等)中的接口语法不同。

接口的作用:实现前后端通信

接口的组成:请求方法、接口地址(URL)、请求参数、响应结果

10.2 RESTful API接口

RESTful API是一种特殊风格的接口,主要特点有如下几个:

  • URL 中的路径表示 资源 ,路径中不能有动词 ,例如create , delete , update等这些都不能有

  • 操作资源要与 HTTP请求方法 对应

  • 操作结果要与HTTP响应状态码对应

操作 请求类型 URL 返回
新增歌曲 POST /song 返回新生成的歌曲信息
删除歌曲 DELETE /song/10 返回一个空文档
修改歌曲 PUT /song/10 返回更新后的歌曲信息
修改歌曲 PATCH /song/10 返回更新后的歌曲信息
获取所有歌曲 GET /song 返回歌曲列表数组
获取单个歌曲 GET /song/10 返回单个歌曲信息

10.3 json-server

json-server本身是一个JS编写的工具包,可以快速搭建RESTful API服务

搭建临时的接口服务操作步骤:

1.全局安装json-server

1
npm i -g json-server

2.创建JSON文件(db.json),编写基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
{
"books": [
{ "id": 1, "name": "活着", "singer": "余华" },
{ "id": 2, "name": "西游记", "singer": "吴承恩" },
{ "id": 3, "name": "平凡的世界", "singer": "路遥" }
],
"songs": [
{ "id": 1, "name": "知足", "singer": "五月天" },
{ "id": 2, "name": "不能说的秘密", "singer": "周杰伦" }
]
}

3.以JSON文件所在文件夹作为工作目录,执行如下命令:

1
json-server --watch db.json

完成过后,客户端(浏览器)就可以通过http://127.0.0.1:3000/后面加上路径来访问对应的数据了。

11. 会话控制

11.1 简介

所谓会话控制就是对会话进行控制。HTTP是一种无状态的协议,它没有办法区分多次的请求是否来自于同一个客户端,无法区分用户,所以可以通过会话控制来解决该问题。

常见的会话控制技术有三种:

  • cookie
  • session
  • token

cookie是HTTP服务器发送到用户浏览器并保存在本地的一小块数据,简单来说:

  • cookie是保存在浏览器端的一小块数据
  • cookie是按照域名划分保存的

示例:

域名 cookie
www.baidu.com a=100;b=200
www.bilibili.com xid=1020abce121;hm=112411213
jd.com x=100;ocw=12414cce

1.特点

浏览器向服务器发送请求时,会自动将当前域名下可用的cookie设置在请求头中,然后传递给服务器。这个请求头的名字也叫cookie,所以将cookie理解为一个HTTP的请求头也是可以的。

2.cookie运行流程

  • 浏览器这边会先把我们的账号密码等信息传递给服务器,服务器就会返回一个属于我们的cookie。注意的是,服务器是通过响应报文里面响应头为set-cookie传递给我们的。

  • 当服务器返回一个我们的cookie后,浏览器就会将set-cookie后面的内容进行一个存储,于是就将cookie信息保存在我们当前这个域名的cookie下面。

  • 当保存完毕之后,浏览器下次再向该服务器发送请求时,就会自动携带这个域名下面的cookie,然后传递给服务器。此时服务器就可以通过对这个信息的解析来得知请求的一个发送者,所以在返回结果时,就可以通过对应的发送者来响应对应的内容,这样就实现了用户的识别,实现了会话控制。

11.3 express框架使用cookie

在一个文件夹下的终端通过npm init创建一个包的配置文件,再在该目录下通过npm i express安装express工具包。

1.设置和删除cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//导入模块
const express = require('express');

//创建应用对象
const app = express();

//添加cookie
app.get('/set-cookie', (req, res) => {
//res.cookie('name', 'zhangsan'); //会在浏览器关闭的时候销毁
res.cookie('name', 'lisi', {maxAge: 60*1000}); //参3是设置生命周期,这里设置了存活1分钟(期间关闭浏览器也可以的)
res.cookie('theme', 'blue'); //这里又设置一个cookie
res.send('home');
});

//删除cookie
app.get('/remove-cookie', (req, res) => {
res.clearCookie('name'); //删除cookie name
res.send('删除成功~~~');
});

//启动服务
app.listen(3000);

2.获取cookie

如果要在express框架中获取cookie,还需要通过npm i cookie-parser来下载工具包cookie-parser。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//导入模块
const express = require('express');
const cookieParser = require('cookie-parser');

//创建应用对象
const app = express();
app.use(cookieParser());

//添加cookie
app.get('/set-cookie', (req, res) => {
res.cookie('name', 'lisi', {maxAge: 60*1000}); //参3是设置生命周期,这里设置了存活1分钟(期间关闭浏览器也可以的)
res.cookie('theme', 'blue'); //这里又设置一个cookie
res.send('home');
});

//获取cookie
app.get('/get-cookie', (req, res) => {
console.log(req.cookies); //获取cookie name
res.send(`欢迎您 ${req.cookies.name}`);
});

//启动服务
app.listen(3000);

11.4 session

session是保存在服务器端的一块数据,保存当前访问用户的相关消息。它的作用是实现会话控制,可以识别用户的身份,快速获取当前用户的相关消息。

1.session的运行流程

  • 用户提供自己的账号和密码传递给服务器,服务器接收到后会对其进行校验,检查填写的内容是否正确、与数据库里面的填写的内容是否匹配。如果填写没有问题,服务器会为当前的访问者创建一个对象,可以理解为是session对象。在这个对象里面会保存当前用户的一些基本信息,如用户名、用户id、用户邮箱等。除此之外,服务器端还会在这个对象当中生成一个独一无二的id标识,即session_id。
  • 有了这个session对象之后,一方面服务器会将该对象存到session对象池里面(每个用户有了session对象之后都会被存到服务器那边的session对象池里面),一方面会将这个id以响应cookie的形式返回给浏览器,浏览器接收到后就会将这个cookie信息保存起来。
  • 有了cookie之后,以后再向服务器发送请求时,它就会带着cookie向服务器发送请求。服务器接收到该请求后,服务器就会从cookie里面把这个session_id取出来,取出来后就可以到session对象池里面去匹配寻找,即在存放session对象的容器里面去找,如果找到了跟这个session_id匹配的数据,就知道当前的用户是谁了。

11.5 express框架使用session

在一个文件夹下的终端通过npm init创建一个包的配置文件,再在该目录下通过npm i express安装express工具包。

然后还需要安装两个工具包,终端执行命令:npm i express-session connect-mongo

下面程序是session在express框架下的使用,同时结合了mongodb数据库来辅助观察存储的数据。

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
//导入模块express
const express = require('express');
//引入模块 express-session connect-mongo
const session = require("express-session");
const MongoStore = require('connect-mongo');

//创建应用对象
const app = express();

//设置 session 的中间件
//解释:app.use是设置中间件的,session是一个函数,接收一个对象类型的参数,返回一个函数
app.use(session({
name: 'sid', //设置cookie的name,默认值是:connect.sid
secret: 'atguigu', //参与加密的字符串(又称签名)
saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id
resave: true, //是否在每次请求时重新保存session
store: MongoStore.create({
mongoUrl: 'mongodb://127.0.0.1:27017/bilibili' //数据库的连接配置,将当前信息存储在mongodb里面的bilibili下面
}),
cookie: {
httpOnly: true, // 开启后前端无法通过 JS 操作来访问cookie
maxAge: 1000 * 60 // 是控制后端sessionID和发送给浏览器的cookie的过期时间的,这里设置了1分钟
},
}))

//首页,打开后即可完成在mongodb数据库中的bilibili下创建一个session数据库
app.get('/', (req, res) => {
res.send('home');
});

//添加cookie,在url输入username=admin&password=admin即可完成得到session_id,它会存储在数据库中
app.get('/login', (req, res) => {
//username=admin&password=admin
if(req.query.username === 'admin' && req.query.password === 'admin'){
//设置session信息
req.session.username = 'admin';
req.session.uid = '258aefccc';
//成功响应
res.send('登录成功');
}else{
res.send('登录失败~~~');
}
});

//session读取
app.get('/cart', (req, res) => {
//检测session是否存在用户数据
if(req.session.username){
res.send(`购物车页面,欢迎您 ${req.session.username}`);
}else{
res.send('您还没有登录~~~');
}
});

//session销毁,从数据库中也销毁了该信息
app.get('/logout', (req, res) => {
req.session.destroy(() => {
res.send('退出成功~~~');
});
});

//启动服务
app.listen(3000);

11.6 session和cookie的区别

cookie和session的区别主要在于如下几点:

  1. 所在的位置

    • cookie:浏览器端
    • session:服务端
  2. 安全性

    • cookie是以明文的方式存放在客户端的,安全性相对较低
    • session存放于服务器中,所以安全性相对较好
  3. 网络传输量

    • cookie设置的内容过多会增大报文体积,会影响传输效率
    • session数据存储在服务器,只是通过cookie传递id,所以不影响传输效率
  4. 存储限制

    • 浏览器限制单个cookie保存的数据不能超过4k,且单个域名下的存储数量也有限制
    • session数据存储在服务器中,所以没有这些限制

11.7 token

token是服务端生成并返回给HTTP客户端的一串加密字符串,token中保存着用户信息。它的作用是实现会话控制,可以识别用户的身份,主要用于移动端APP。

1.token的工作流程

  • 客户端将账号和密码通过请求发送给服务器,服务端检测了所提交的信息之后,如果没有问题,就会创建出token。创建好后,就会将该信息返回给客户端。
  • 客户端收到服务器返回的token信息后,下次再发送请求时,客户端就会带着token传递给服务器,服务器就会对传来的token做校验,并且从token里面提取出用户的信息,继而识别用户的身份。

可以发现,token和cookie很像,cookie也是服务端校验用户的信息以后,将cookie返回给客户端,下次客户端发送请求时,再带着cookie给服务器。但它们之间也还是有一些区别的,cookie是自动携带的,而token是手动携带,比如说客户端给服务器发请求了,它得自己将token放在请求报文里面,然后再向服务器发请求。但cookie不是,当客户端要发送请求给服务器时,浏览器就会自动的将信息放在请求报文里面传递给服务器。

2.token的特点

  • 服务端压力更小

    • 数据存储在客户端
  • 相对更安全

    • 数据加密
    • 可以避免CSRF(跨站请求伪造)
  • 扩展性更强

    • 服务间可以共享
    • 增加服务节点更简单

3.JWT

JWT是目前最流行的跨域认证解决方案,可用于基于token的身份验证。JWT是token的生成于校验更规范。

使用:在文件夹的终端通过npm init创建一个包的配置文件,再通过npm i jsonwebtoken下载jsonwebtoken工具包。

创建出一个token:

1
2
3
4
5
6
7
8
9
10
//导入模块
const jwt = require('jsonwebtoken');

//创建生成token -----> let token = jwt.sign(用户数据,加密字符串,配置对象)
let token = jwt.sign({
username: 'lixx'
}, 'atguigu', {
expiresIn: 60, //token的声明周期,单位是秒
});
console.log(token);

得到的结果为:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpeHgiLCJpYXQiOjE3MjY0NzMzNzcsImV4cCI6MTcyNjQ3MzQzN30.mrmErdIPfWz7lLEdZRXSQdV7F9HrtYJVEuEwFSzmad0

验证token:

1
2
3
4
5
6
7
8
9
10
let t = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxpeHgiLCJpYXQiOjE3MjY0NzMzNzcsImV4cCI6MTcyNjQ3MzQzN30.mrmErdIPfWz7lLEdZRXSQdV7F9HrtYJVEuEwFSzmad0';

//验证生成的token
jwt.verify(t, 'atguigu', (err, data) => {
if(err){
console.log('校验失败~~~');
return;
}
console.log(data);
});

如果在指定的token生命周期里面执行上面的验证代码,都会出现如下信息:

1
{ username: 'lixx', iat: 1726473377, exp: 1726473437 }