Node.js学习
一、初识Node.js
1.1 Node.js是什么?
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用 JavaScript 编写,并且 Node.js 是一个事件驱动的,非阻塞的,基于 I/O 的异步操作的 JavaScript 运行环境。
1.2 Node.js的核心特性
Npde.js 的核心特性有:
- 基于事件驱动,非阻塞的异步 I/O
- 模块化
- 跨平台
- 快速、高效
- 丰富的第三方模块
1.3 使用Node运行JavaScript代码
Node.js 运行 JavaScript 代码的方式有很多,最常用的是使用 Node.js 的命令行工具。 当我们要使用Node运行一个js文件时,可以使用以下命令:
node Node_Example.js# 运行结果:HELLO Node.js二、Node.js 的模块系统
2.1 fs模块
fs模块是Node.js最常用的模块,主要是对文件进行读写、创建、删除、修改、移动等操作。
它的导入方法也很简单,使用require()方法导入即可
const fs = require('fs');2.1.1 读取文件内容
# // 导入fs 模块const fs = require('fs')// 操作文件fs.readFile('./files/1.txt', 'utf-8', function (err, dataStrs) { # // 打印失败的结果 # // 如果读取成功则err的值为null # // 如果读取失败则err的值为错误信息 undefined console.log(err); console.log("-----"); # // 打印成功的结果 console.log(dataStrs);})
# 1.txt 文件内容的输出结果:null-----11112.1.2 异步读取文件
const fs = require('fs')fs.readFile('./files/1.txt', 'utf-8', function (err, dataStrs) { if (err) { return console.log('文件读取失败', err.message) } return console.log('文件读取成功', dataStrs)})# 输出结果:文件读取成功 11112.1.3 写入文件内容
- 使用fs.writeFile()方法写入文件内容, 可以向指定的文件中写入内容,语法格式如下:
fs.writeFile(file, data[, options], callback])-
注意:当我们重复调用fs.writeFile()方法写入文件的时候,会覆盖掉之前的内容。
-
fs.writeFile()的示例代码:
const fs = require('fs')# // 调用fs.writeFile方法写入文件内容# // 参数1:写入文件的路径# // 参数2:写入的内容# // 参数3:回调函数fs.writeFile('./files/2.txt', 'hello world', function(err){# // 如果没有出错,则返回null,如果出错,则返回错误信息 console.log(err);})# 输出结果:null2.1.4 异步写入文件内容
const fs = require('fs')# // 调用fs.writeFile方法写入文件内容# // 参数1:写入文件的路径# // 参数2:写入的内容# // 参数3:回调函数fs.writeFile('./files/3.txt', 'hello world', function(err){// 如果没有出错,则返回null,如果出错,则返回错误信息undefinedif(err){ return console.log('写入失败', err.message)} console.log('3.txt,写入成功');})# 输出结果为:3.txt,写入成功2.1.5 成绩整理案例:
此案例是结合前面fs模块的读写操作进行对成绩.txt文件的操作,将读取到的内容进行整理,并写入新的文件中。
const fs = require('fs')fs.readFile('./files/成绩.txt', 'utf-8', function (err, dataStrs) { if (err) { return console.log('文件读取失败', err.message); } console.log('文件读取成功', dataStrs);
// 1.将数据进行分割 const arrOld = dataStrs.split(' ') console.log(arrOld);
// 2.遍历分割后的数据成为一个新数组 const arrNew = [] arrOld.forEach(item => { arrNew.push(item.replace('=', ': ')) }); console.log(arrNew);
// 3.进行数据的拼接 const newStr = arrNew.join('\r\n') console.log(newStr);
// 4.将拼接后的数据写入到文件中 fs.writeFile('./files/整理后的成绩.txt', newStr, function (err) { if (err) { return console.log('文件写入失败', err.message) } return console.log('文件写入成功')})})
// 输出结果:小红: 99小白: 100小黄: 70小黑: 66小绿: 88文件写入成功2.1.6 fs模块-路径动态拼接的问题
在使用fs模块操作文件时,如果提供的操作路径是以./或../开头的相对路径,很容易出现路径动态拼接错误的问题。 原因:代码在运行的时候,会以执行node命令时所处的目录,动态拼接出被操作文件的完整路径
示例代码:
const fs = require('fs')
fs.readFile('./files/1.txt', 'utf-8', function (err, dataStrs) { if(err) { return console.log('文件读取失败', err.message) } console.log('文件读取成功', dataStrs);
})# 当我们没有处在node.js的根目录下时,在控制台执行以下命令时,会出现报错,文件无法被读取node ./code/ndoe 动态路径错误问题.js- 解决方法:使用
__dirname来获取当前文件的所在目录,并拼接成完整的文件路径
示例代码:
const fs = require('fs')
fs.readFile(__dirname + './files/1.txt', 'utf-8', function (err, dataStrs) { if(err) { return console.log('文件读取失败', err.message) } console.log('文件读取成功', dataStrs);
})2.2 path路径模块
2.2.1 什么是path路径模块?
path模块是Node.js中官方提供,用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对路径处理的需求
例如:
path.join()方法用来拼接路径`path.basename()方法用来从路径字符串中,将文件名解析出来`
当然如果要在JavaScript代码中,使用path模块来处理路径,则需要先导入模块,代码如下:
const path = require('path');2.2.2 path.join()方法
使用path.join()方法来拼接路径,可以把多个路径片段拼接成一个完整的路径字符串,代码如下:
- 示例一:
// 导入模块const path = require('path')const pathStr = path.join('/a', '/b/c', '../', './d','e')// 注意../是可以抵消一层路径的console.log(pathStr) // 输出结果: \a\b\d\e- 示例二:
// 导入模块const path = require('path')const pathStr1 = path.join(__dirname, './files/1.txt')console.log(pathStr1)
// 输出结果: D:\Node.js基础\day1\CODE.\files\1.txt// 这里是根据我电脑上的路径来写的,只做演示作用示例二的方法更加方便简洁,因此在实际开发中,我们推荐使用示例二。
在今后的开发中,凡是涉及到路径拼接的操作,都要使用path.join()方法来拼接路径,不要直接使用 + 进行字符串的拼接
- 示例代码:
const fs = require('fs');const path = require('path');
fs.readFile(path.join(__dirname, '/files/example.txt'), 'utf8', (err, dataStr) => { if (err) { console.error('文件读取失败', err.message); } console.log(dataStr);})
// 输出结果:path.join路径演示2.2.3 path.basename()方法
path.basename() 的语法格式:
使用path.basename()方法可以获取路径中的最后一部分,经常通过这个方法获取路径中的文件名,语法格式如下:
path.basename(pathp[, ext])- path
必选参数 - ext
可选参数, 表示文件扩展名 - 返回:
表示路径中的最后一部分
path.basename() 的使用示例:
const path = require('path');const fpath = '/a/b/c/index.html';
var fullName = path.basename(fpath);console.log(fullName); // 输出结果: index.htmlconst path = require('path')const gpath = '/a/b/c/index.html'
// 参数二为可选参数, 表示文件扩展名var nameWithoutExt = path.basename(gpath, '.html')consolo.log(nameFullName) // 输出结果: index2.2.4 path.extname()
path.extname()的语法格式:
使用path.extname()方法,可以获取路径中你的扩展名部分, 语法格式如下:
path.extname(path)- path
必选参数,表示一个路径的字符串 - 返回
返回得到的扩展名字符串
path.extname()的代码示例:
const path = require('path')// 这是文件的存放路径const fpath = '/a/b/c/index.html'
const fext = path.extname(fpath)console.log(fext)
// 输出结果:.html2.2.5 时钟案例
接下来我将演示一个时钟案例,此案例目的是为了能够让学习者更好的理解node.js中模块化的设定和使用。
这是我们要解耦的html文件中的源码:
可以看到该源码中包含了css 和 javascript部分 后面我们将对这两部分进行解耦
<!DOCTYPE html><html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>index首页</title> <style> html, body { margin: 0; padding: 0; height: 100%; background-image: linear-gradient(to bottom right, red, gold); }
.box { width: 400px;56 collapsed lines
height: 250px; background-color: rgba(255, 255, 255, 0.6); border-radius: 6px; position: absolute; left: 50%; top: 40%; transform: translate(-50%, -50%); box-shadow: 1px 1px 10px #fff; text-shadow: 0px 1px 30px white;
display: flex; justify-content: space-around; align-items: center; font-size: 70px; user-select: none; padding: 0 20px;
/* 盒子投影 */ -webkit-box-reflect: below 0px -webkit-gradient(linear, left top, left bottom, from(transparent), color-stop(0%, transparent), to(rgba(250, 250, 250, .2))); } </style></head>
<body> <div class="box"> <div id="HH">00</div> <div>:</div> <div id="mm">00</div> <div>:</div> <div id="ss">00</div> </div>
<script> window.onload = function () { // 定时器,每隔 1 秒执行 1 次 setInterval(() => { var dt = new Date() var HH = dt.getHours() var mm = dt.getMinutes() var ss = dt.getSeconds()
// 为页面上的元素赋值 document.querySelector('#HH').innerHTML = padZero(HH) document.querySelector('#mm').innerHTML = padZero(mm) document.querySelector('#ss').innerHTML = padZero(ss) }, 1000) }
// 补零函数 function padZero(n) { return n > 9 ? n : '0' + n } </script></body>
</html>- 创建一个clock.js文件
创建一个名为clock.js的文件,并写入以下代码:
// 1.导入模块const fs = require('fs')const path = require('path')
// 2.创建正则表达式,用于匹配对应的字符串// 2.1.匹配style标签const regiStyle = /<style>[\s\S]*<\/style>/// 2.2.匹配script标签const regiScript = /<script>[\s\S]*<\/script>/
// 调用fs.readFile()方法,读取文件fs.readFile(path.join(__dirname, './index.html'), 'utf8', function (err, dataStr) { if(err) { return console.log('读取文件失败!' + err.message) } console.log('读取文件成功!', dataStr)
// 调用方法, 传入dataStr读取到的内容
59 collapsed lines
// 1.拆分HTML中的Css结构方法 resoloveCss(dataStr)
// 2.拆分HTML中的JS结构方法 resolveJs(dataStr)
// 3.创建Html文件 resolveHtml(dataStr)
})
// 创建方法// 1.resolve CSS 方法function resoloveCss(htmlStr) { // 1.提取(匹配html页面中符合条件的字符串) const r1 = registStyle.exec(htmlStr) console.log(r1);
// 2.替换 const newCss = r1[0].replace('<style>', '').replace('</style>', '') console.log('替换后的newHtml为:', newCss); // 3.写入 fs.writeFile(path.join(__dirname, './files/newCss.css'), newCss, function (err) { if(err) { return console.log('写入文件失败', err.message); } console.log('写入文件成功'); })}
// 2.resolveJs()方法function resolveJs(htmlData) { // 1.提取(符合的字符串) 匹配成功则返回数组,失败则返回null const s1 = registScript.exec(htmlData) console.log(s1); // 2.替换 const newS1 = s1[0].replace('<script>', '').replace('</script>', '') console.log('newS1替换结构为:', newS1); // 3.写入 fs.writeFile(path.join(__dirname, './files/newJs.js'), newS1, function(err) { if(err) { return console.log('写入文件失败', err.message); } console.log('写入文件成功'); })}
// 3.创建Html文件function resolveHtml(htmlData) { const newHtml = htmlData.replace(registStyle, '<link rel="stylesheet" href="./newCss.css">').replace(registScript, '<script src="./newJs.js"></script>') console.log('newHtml打印结果:', newHtml);
fs.writeFile(path.join(__dirname, './files/newHtml.html'), newHtml, function(err) { if(err) { return console.log('写入文件失败', err.message); } console.log('写入文件成功'); })}运行这个代码,会生成一个名为 newHtml.html 的文件,并写入新的 HTML 内容。
将原本的index.html中html css javascript的内容全部分离出来,并逐个写入新的文件中,最后在通过html的link方法引入css和JavaScript文件,完成页面的模块化,这点与Vue框架并无太大差别
2.3 http模块
2.3.1 什么是http模块?
在认识到http模块之前,我们先了解一些概念:
-
什么是客户端,什么是服务器?
在网络节点中,负责消费资源的电脑,叫做客户端;负责对外提供网络资源的电脑,叫做服务器。
-
而http模块是什么呢?
http模块是Node.js官方提供的、用来创建Web服务器的模块。通过http模块提供的http.createServer()方法,就能方便的把一台普通电脑,变成一台Web服务器,从而对外提供web资源服务。
2.3.2 如何使用http模块
如果希望使用http模块,首先需要引入该模块:
const http = require('http');2.3.3 进一步理解http模块的作用
服务器和普通电脑的区别在于,服务器上安装了web服务器软件,例如:IIS、Apache等。通过安装这些服务器软件,就能把一台电脑变成一台web服务器。
在Node.js中,我们不需要使用IIS、Apache等这些第三方服务器软件,因为我们可以基于Node.js提供的http模块,通过几行简单的代码,就能轻松的手写一个服务器软件,从而对外提供web服务。
2.3.4 服务器相关的概念
-
IP地址:
IP地址就是相当于互联网上每台计算机的唯一地址,因此IP地址具有唯一性,如果把“个人电脑”比作一台电话,那么IP地址就是这个电话的电话号码,只有在知道对方IP地址的前提下,才能对应电脑之间进行数据通信。IP地址的格式: 通常用点分十进制表示成(a.b.c.d)的形式,其中a,b,c,d都是0-255之间的十进制整数。例如:用点分十进制表示的IP地址(192.168.1.1)-
注意:
-
互联网中每台web服务器,都有自己的IP地址, 例如:大家可以在Windows的终端中运行ping www.baidu.com命令,就会看到百度的IP地址。 -
在开发期间,自己的电脑既是一台服务器,也是一个客户端,为了方便测试,可以在自己的浏览器中输入 127.0.0.1 这个IP地址,就可以访问到自己的电脑。
-
-
-
域名和域名服务器尽管IP地址能够唯一地标记网络上的计算机,但IP地址是一长串数字,
不直观,而且不方便于记忆,雨伞人们又发明了另一套字符串型的地址方案,即所谓的域名(Domain Name)地址。IP地址和域名是一一对应的关系,这份对应关系存放在一种叫做域名服务器(DNS, Domain Name Server)的电脑中。只需要通过好记的域名访问对应的服务器即可,对应的转换工作由域名服务器实现,因此,域名服务器就是提供IP地址和域名之间的转换服务的服务器-
注意:
-
单纯使用IP地址,互联网中的电脑也能够正常工作,但是有了域名的加持,能让互联网的世界变得更加方便。
-
在开发测试期间,
127.0.0.1对应的域名是localhost,它们都代表我们自己的这台电脑,在使用效果上没有任何区别。
-
-
-
端口号
计算机中的端口号,就好像是现实生活中的门牌号一样,通过门牌号,外卖小哥可以在整栋大楼众多的房间中,准确把外卖送到你手中。
同样的道理,在一台电脑中,可以运行成百上千个web服务,每个web服务都对应一个唯一的端口号,客户端发送过来的网络请求,通过端口号,可以被准确地交给
对应的web服务进行处理- 注意:
- 每个端口号不能同时被多个web服务占用
- 在实际应用中,URL中的80端口可以被省略
- 注意:
2.3.5 创建最基本的web服务器
-
创建web服务器的基本步骤
- 导入http模块
- 创建web服务器实例
- 为服务器实例绑定
request事件,监听客户端的请求 - 启动服务器
- 步骤一 导入http模块
const http = require('http');-
步骤二 创建web服务器实例
调用
http.createServer()方法创建一个web服务器实例
const server = http.createServer();-
步骤三 为服务器实例绑定
request事件,监听客户端的请求为服务器实例绑定
request事件,即可监听客户端发送的网络请求
// 使用服务器实例的 .on 方法,为服务器实例绑定 request 事件server.on('request', (req, res) => {// 只要有客户端来请求外卖自己的服务器,就会触发request事件,从而调用这个事件处理函数console.log('Someone has made a request to our server!');})-
步骤四 启动服务器
调用服务器实例的 .listen 方法,即可启动当前的web服务器实例:
// 调用server.listen(端口号,cb回调), 即可启动服务器server.listen(80, () => {console.log('Our server is running!');}) -
req请求对象
只要服务器接收到了客户端的请求,就会调用通过
server.on()为服务器绑定request事件处理函数如果想在事件处理器函数中,
访问与客户端相关的数据或属性, 可以使用如下的方式:const http = require('http');const server = http.createServer();server.on('request', (req) => {// req是请求对象,它包含了与客户端相关的数据和属性,例如:// req.url 是客户端请求的URL地址const url = req.url;// req.method 是客户端的method请求类型const method = req.method;// 拼接url和methodconst str = `You request url is ${method} and request method is ${url}`;// 打印strconsole.log(str);})server.listen(80, () => {console.log('server running at http://127.0.0.1');})
2.3.6 res响应对象
在服务器的request事件处理函数中,如果想访问与服务器相关的数据或属性,可以使用如下的方式:
server.on('request', (req, res) => { // res 是响应对象,它包含了与服务器相关的数据和属性,例如: // 要发送到客户端的字符串 const str = `You request url is ${req.url} and request method is ${req.method}` // res.end 方法的作用: // 向客户端发送指定的内容,并结束这次请求的处理过程 res.end(str)})- 解决
中文乱码问题
当调用res.end()方法时,Node.js会默认将数据转换成UTF-8编码,但是对于中文字符,这个编码方式能会导致乱码, 此时, 需要手动设置内容的编码格式, 解决办法也很简单, 设置响应头 Content-Type: text/html; charset=utf-8即可
server.on('request', (req, res) => { const str = '中文乱码' // 设置响应头 res.setHeader('Content-Type', 'text/html; charset=utf-8') // 返回响应数据 res.end(str)})2.3.7 根据不同的url响应不同的html内容
-
核心实现步骤
- 获取
请求的url地址 - 设置
默认的响应内容为404 Not Found - 判断用户请求的是否为
/或者/index.html首页 - 判断用户请求的是否为
/about.html关于页面 - 设置Content-Type响应头
- 使用
res.end()方法把内容,响应给客户端
- 获取
-
示例代码
const http = require('http');const server = http.createServer()server.on('request', (req, res) => {const url = req.url;// 设置默认的内容为 404 Not Foundlet content = `<h1>'404 Not Found'</h1>`;if(url === '/') {content = `<h1>'首页'</h1>`;}else if(url === '/about.html') {content = `<h1>'关于'</h1>`;}// 设置响应头 防止中文乱码res.setHeader('Content-Type', 'text/html; charset=utf-8');// 返回 页面content 的响应内容res.end(content);})server.listen(80, () => {console.log('http server running at http://127.0.0.1');})
2.3.8 http时钟web服务器案例
在path路径模块的章节中,我们已经实现了一个http时钟服务器,现在我们来实现一个http时钟服务器,这个服务器会返回当前时间。
const fs = require('fs');const path = require('path');const http = require('http');
const server = http.createServer();
server.on('request',(req,res) => {
const url = req.url; // 这里的所表达的意思是代表根目录的意思 const fpath = path.join(__dirname, url);
fs.readFile(fpath, 'utf-8', (err, dataStr) => { if(err) { res.end('404 Not Found') return } res.end(dataStr) })
})server.listen(80, () => { console.log('http server running at http://127.0.0.1');})需要注意的时候,当我们运行这个服务器,进入的默认URL是空路径,也就是根目录,需要我们手动在网址输入框添加/clock/index.html路径访问这个文件夹中的html文件,才能显示页面。
三、Node.js中的模块化
3.1 什么是模块化?
在编程领域中的模块化,就是遵守固定的规则,把一个大文件拆成独立并互相依赖的多个小模块。
把代码进行模块化拆分的好处是:
- 提高了代码的
复用性 - 提高了代码的
可维护性 - 可以实现
按需加载
3.2 模块化的规范
模块化规范就是对代码进行模块化的拆分与组合时,需要遵守的那些规则
例如:
- 使用什么样的语法格式来
引用模块 - 在模块中使用什么样的语法格式
向外暴露成员
模块化规范的好处: 大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用,利人利己。
3.3 Node.js 中模块化的分类
Node.js中根据模块来源的不同,将模块分为了3大类,分别是:
- 内置模块(内置模块是由Node.js官方提供的, 例如
fs、path、http等) - 自定义模块(用户创建的每个.js文件,都是自定义模块)
- 第三方模块(由第三方开发出来的模块,例如
express、mongoose、lodash等)
3.4 加载模块
使用强大的require()方法,可以加载内置模块、用户自定义模块、第三方提模块进行使用。
例如:
// 1. 加载内置模块const fs = require('fs');// 2.加载自定义模块 (提供自定义模块的路径)const myModule = require('./myModule');// 3.加载第三方模块const request = require('request');-
值得注意的是,require()方法返回的是一个对象,对象中包含
模块的导出内容。同时,require()方法会缓存模块,即多次调用require()方法,返回的都是同一个对象。 -
关于在使用require加载用户自定义模块期间是可以
省略.js后缀的,例如:require('./myModule')。 -
关于使用require()方法加载第三方模块,
require()方法会自动将模块路径中的./转换成__dirname。例如:require('./myModule')会被转换成require(__dirname + '/myModule')。
3.5 Node.js中的模块作用域
3.5.1 什么是模块作用域?
和函数作用域类似,在自定义模块中定于的变量、方法等成员,只能在当前模块内被访问。这种模块级别的访问限制,叫做模块作用域。
当我们定义了一个自定义模块命名为custom.js 在其模块内部定义一个函数:
const username = '张三'
function showName() { console.log('我的名字是' + username)}当我们在其他模块中引用这个模块时,无法直接访问模块内部定义的变量和函数:
const custom = require('./custom')
console.log(custom) // 输出空对象- 模块作用域的好处:模块作用域能够很好的防止全局变量污染的问题
3.5.2 向外共享模块作用域中的成员
module对象
在每个.js自定义模块中都有一个module对象,它里面存储了和当前模块的有关的信息 我们可以在控制台打印module
console.log(module)
// module对象 输出结果:{ id: '.', path: 'D:\\Node.js基础\\day2', exports: {}, filename: 'D:\\Node.js基础\\day2\\07.module属性.js', loaded: false, children: [], paths: [ 'D:\\Node.js基础\\day2\\node_modules', 'D:\\Node.js基础\\node_modules', 'D:\\node_modules' ], Symbol(kIsMainSymbol): true, Symbol(kIsCachedByESMLoader): false, Symbol(kURL): undefined, Symbol(kFormat): undefined, Symbol(kIsExecuting): true}module对象是一个大对象,在这个大对象中包含许多属性,其中对象中有 exports 属性,这个属性是一个对象,这个对象就是我们向模块外共享的成员。
module.exports对象
在自定义模块中,可以使用module.exports对象,将模块中的成员共享出去。
这里我们自定义一个模块,在模块中定义一个变量,并使用module.exports对象将变量共享出去。
module.exports.username = '张三'
module.exports.showName = function () { console.log('我的名字是' + this.username)}在其他模块中,可以通过require方法加载模块,并获取模块中的成员。
const username= require('./modules/自定义模块')
console.log(username)
// username的输出结果是一个对象:{ username: '张三', showName: [Function (anonymous)] }当然,外界在使用require方法导入自定义模块时,得到的就是module.exports所指向的对象。
- 共享成员时的
注意点
使用require方法导入模块时,导入的结果,永远以module.exports指向的对象为准
这里我们依旧定义一个自定义模块,进行演示
module.exports.username = '张三'module.exports.age = 18module.exports.showName = function () { console.log('我的名字是' + this.username)}
// 为module.exports提供一个全新对象module.exports = { nickname: '小张', age: 19, showName: function () { console.log('我的名字是' + this.nickname) }}在外部引用我们自定义好的模块
const username = require('./modules/自定义模块2')
console.log(username);
// 输出结果:{ nickname: '小张', age: 19, showName: [Function: showName] }我们不难发现,module.exports是有赋值的,新的值会覆盖掉旧的值。
exports对象
由于modules.exports单词写起来笔记复杂,为了简化向外共享成员的代码,Node提供了exports对象。默认情况下,exports和module.exports指向同一个对象,最终共享的结果,还是以module.exports为准。
console.log(exports);
console.log(module.exports);
console.log(exports === module.exports);
// 输出结果:{}{}true- exports 和 module.exports 的使用误区
需要注意的是,require()模块时,得到的永远是module.exports指向的对象
这里我们定义一个模块用于演示二者的关系
exports.a = 1;module.exports = { b: 2 c: 3};显而易见的是,当我们外部引用此模块并打印出对象的属性时,得到的是module.exports指向的对象,即{b: 2, c: 3}
那么,exports和module.exports指向的顺序反过来呢?我们可以尝试一下:
module.exports.a =1exports = { b: 2 c: 3};
// 输出结果:{a: 1 }从以上的输出结果来看,无论顺序如何变化,我们得到的永远是module.exports指向的对象
当我们同时使用exports和module.exports定义两个变量,又会发送什么呢?这里我们举一个实例
exports.a = 1;module.exports.b = 2;
// 输出结果:{ a: 1, b: 2 }从该实例的输出结果来看,我们发现当exports和module.exports在定义不同变量时候,彼此并不会冲突,而是会合并成一个对象,并返回给调用者。
3.6 CommonJS模块化规范
Node.js遵循了CommonJS模块化规范,CommonJS规定了模块的特效和各模块之间如何相互依赖。
- CommonJS的规定:
- 每个模块内部,
module变量代表当前模块 - module变量是一个对象,它的exports属性(即
module.exports)是对外的接口) - 加载某个模块,其实是加载该模块的module.exports属性,
require()方法用于加载模块。
3.7 npm与包
3.7.1 什么是包?
Node.js中的第三方模块又叫做包
- 包的来源:
不同于Node.js中的内置模块与自定义模块,包是由第三方个人或团队开发出来的, 免费供所有人使用
Node.js中的包都是免费且开源的,不需要付费即可免费下载使用
- 为什么需要包?
由于Node.js的内置模块仅提供了一些底层的API,导致在基于内置模块进行项目开发时,代码量会非常大,因此,Node.js提供了第三方模块,使得开发人员可以快速开发项目,省去很多重复的代码编写工作。
包是基于内置模块封装出来的,提供了更高级,更方便的API,极大的提高了开发效率。
包和内置模块之间的关系,类似于JQuery和浏览器内置API之间的关系。
- 从哪里下载包?
国外有一家IT公司,叫做npm, Inc.这家公司旗下有一个非常著名的网站: https://www.npmjs.com/ ,它是全球最大的包共享平台,你可以从这个网站上搜索到任何你需要的包,只要你有足够的耐心!
到目前位置,全球约1100 多万的开发人员,通过这个包共享平台,开发并共享了超过120 多万个包供我们使用。npm, Inc.公司提供了一个地址为 https://registry,npmjs.org/ 的服务器,来对外共享所有的包,我们可以从这个服务器上下载自己所需要的包。
- 如何下载包?
npm, Inc.公司提供了一个包管理工具,我们可以使用这个包管理工具,从 https://registry,npmjis.org/ 服务器把需要的包下载到本地使用。
这个包管理工具的名字叫做 Node Package Manager(简称 npm包管理工具),这个包管理工具随着Node.js的安装包一起被安装到了用户的电脑上。
大家可以在终端中执行npm -v命令,来查看自己电脑上所安装的npm包管理工具的版本号
3.7.2 npm 初体验
初次装包完成后,在项目文件夹中多一个叫node_modules的文件夹,里面存放着npm安装的包和package-lock.json的配置文件
其中node_modules用来存放所有已安装到项目中的包。require()导入第三方包时,就是从这个目录查找并加载包。
package-lock.json文件用来记录node_modules中安装的包的下载信息,例如包的名字、版本号、下载地址等。
- 安装指定版本的包
默认情况下,使用npm install命令安装包的时候,汇总的安装最新版本的包,如果需要安装指定版本的包,可以在包名之后,通过@符号指定版本号, 例如:
npm install express@4.17.1- 包的
语义化版本规范
包的版本是以 “点分十进制” 形式进行定义的,总共有三位数字,例如4.17.1
其中每一位数字的含义如下:
-
主版本号
-
功能版本
-
Bug修改版本
-
多人协作问题
由于第三方包的体积过大,不方便团队成员之间共享项目源代码
解决方案也很简单, 只需要共享时剔除node_modules
在项目根目录中,创建一个叫做package.json的配置文件,即可用来记录项目中安装了哪些包, 从而方便剔除node_modiles目录之后,在团队成员之间共享项目的源代码。
今后在项目开发中,一定要把node_modules文件夹,添加到.gitignore 忽略文件中
- 快速创建package.json
npm包管理工具提供了一个快捷命令,可以在指向命令时所处的目录中,快速创建package,json这个包管理
npm init -y- 值得注意的是
npm init -y该命令只能在英文的目录下成功运行并且只需运行一遍 - 运行npm install 命令安装包的时候,npm包管理工具会自动把
包的名称和版本号,记录到package.json中。
3.7.3 dependencies节点
在package.json文件中,有一个dependencies节点,专门用来记录使用npm install命令安装了哪些包。
3.7.4 devDependencies节点
如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到devDependencies节点中。
与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到dependencies节点中。
可以使用以下命令将包记录到Devdependencies节点中
# 简写npm i 包名 -D
# 完整写法npm install 包名 --save-dev3.7.5 卸载包命令
npm uninstall3.7.6 包的分类
使用npm包管理工具下载的包,共分为两大类,分别是:
-
项目包
那些被安装到
项目node_modules目录中的包,都是项目包。 -
全局包
2.1
开发依赖包(被记录到devDependencies节点中的包,只会在开发期间用到)2.2
核心依赖包(被记录到dependencies节点中的包,在开发期间和项目上线之后都会用到)在执行npm install命令时,如果提供了
-g参数, 则会把包安装为全局包。Terminal window # 安装全局包npm -intall 包名 -g# 卸载全局包npm uninstall 包名 -g-
注意:
-
只有
工具性质的包,才有全局安装的必要性,因为它们提供了好用的终端命令。 -
判断某个包是否需要全局安装才能使用,可以
参考官方提供的使用说明即可。
-
-
3.7.7 i5ting_toc morkdown转换html模块
i5ting_toc是一个可以把md文档转为html页面的小工具,使用步骤如下:
npm install -g i5ting_toc
i5ting_toc -f 要转换的md文件路径 -o3.7.8 规范的包结构
在清楚了包的概念,以及如何下载和使用包之后,接下来,我们深入了解以下`包的内部结构
一个规范的包,它的组成结构,必须符合以下3点要求:
- 包必须以
单独的目录而存在 - 包的顶级目录下的要必须包含
package.json这个包管理配置文件 - package.json中必须包含
name,version,main这三个属性,分别代表包的名字、版本号、包的入口
3.7.9 开发属于自己的包
-
需要实现的功能
1.1
格式化日期1.2
转义HTML中的特殊字符1.3
还原HTML中的特殊字符 -
初始化包的基本结构
2.1 新建tools文件夹,作为
包的根目录2.2 在tools文件夹中,新建如下三个文件:
-
package.json(包管理配置文件)
-
index.js(包的入口文件)
-
README.md(包的说明文档)
- 初始化package.json文件
{"name": "tools","version": "1.0.0","main": "index.js","description": "提供了格式化时间, HTMLEscape相关的功能","keywords": ["itheima","dateformat","escape"],"license": "ISC"}- 在index.js中定义方法
// 这是包的入口文件function dateFormat(dateStr) {const dt = new Date(dateStr)const y = padZero(dt.getFullYear())const m = padZero(dt.getMonth() + 1)const d = padZero(dt.getDate())const hh = padZero(dt.getHours())const mm = padZero(dt.getMinutes())const ss = padZero(dt.getSeconds())return `${y}-${m}-${d}-${hh}-${mm}-${ss}`}// 提供补零方法function padZero(n) {return n > 9 ? n : '0' + n}39 collapsed lines// html转义方法function htmlEscape(htmlStr) {return htmlStr.replace(/<|>|"|&/g, (match) => {switch(match) {case '<':return '<'case '>':return '>'case '"':return '"'case '&':return '&'}})}// html还原方法function htmlReturn(str) {return str.replace(/<|>|"|&/g, (match) => {switch(match) {case '<':return '<'case '>':return '>'case '"':return '"'case '&':return '&'}})}// 暴露方法module.exports = {dateFormat,htmlEscape,htmlReturn} -
-
将不同的功能进行模块化拆分
3.1 将格式化时间的功能,拆分到src ->
dateForamt.js中3.2 将处理HTML字符串的功能,拆分到src ->
htmlEscape.js中3.3 在index.js中,导入两个模块,得到需要向外共享的方法
3.4 在index.js中,使用modules.exports把对应的方法共享出去
-
编写包的说明文档
包根目录中的README.md文件,是包的使用说明文档,通过它,我们可以事先把包的使用说明,以markdown的格式写出来,方便用户参考
README文件中具体写什么内容,没有强制性要求;只要能够清晰地包的作用、用法、注意事项等描述清楚即可。
我们所创建这个包的README.MD文档中,会包含以下6项内容: 安装方式、导入方式、格式化时间、转义HTML中的特殊字符、还原HTML中的特殊字符、开源协议
-
发布包
5.1 注册npm账号
访问https://www.npmjs.com/网站,点击sign up按钮,进入注册用户界面
填写账号相关的信息:Full Name、Public Email、Username、Password
点击Create an Account按钮,注册账号
5.2 登录npm账号
npm账号注册完成后,可以在终端执行
npm login命令,依次输入用户名,密码,邮箱后,即可登录成功。5.2 把包发布到npm上
将终端切换到包的根目录之后,运行
npm publish命令,即可将包发布到npm上(注意:包名不能雷同) -
删除已发布的包
运行npm unpublish 包名 --force命令,即可从npm删除已发布的包
- 注意:
-
npm unpublish 命令只能删除
72小时以内发布的包 -
npm unpublish删除的包,在
24小时内不允许重复发布 -
发布包的时候要慎重,
尽量不要往npm上发布没有意义的包
-
3.8 模块的加载机制
我们从一张图来了解Node.js中模块的加载机制

-
优先从缓存中加载
模块在第一次加载后会被缓存。这也意味着多次调用rrequire()不会导致模块的代码被执行多次。- 注意: 不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存中加载,从而
提供模块的加载效率
- 注意: 不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存中加载,从而
-
内置模块的加载机制
内置模块是由Node.js官方提供的模块,内置模块的加载优先级别最高。
例如,require(“fs”)始终返回内置的fs模块,即使在node_modules目录下有名字相同的包也叫做fs。
自定义模块的加载机制
使用require()记载自定义模块时,必须指定./或../开头的路径标识符。在加载自定义模块时,如果没有指定./或../这样的路径标识符,则node会把它当作内置模块或第三方模块进行加载
同时,在使用require()导入自定义模块时,如果省略了文件的扩展名,则Node.js会按顺序分别尝试加载以下的文件:
-
按照确切的文件名进行加载
-
补全
.js扩展名进行加载 -
补全
.node扩展名进行加载 -
补全
json扩展名进行加载 -
加载失败,终端报错
-
第三方模块的加载机制
如果传递require()的模块标识符不是一个内置模块,也没有以./或../开头,则Node.js会从当前模块的父目录开始,尝试从/node_modules文件夹中加载第三方模块。
如果没有找到对应的第三方模块,则移动到上一层父目录中,进行加载,直到文件系统的根目录
例如,假设在C:\User\Node\project\example.js文件里调用了require('tools'),则Node.js会按以下顺序查找:
-
C:\User\Node\project\node_modules\tools
-
C:\User\Node\node_modules\tools
-
C:\User\node_modules\tools
-
C:\node_modules\tools
-
如果都没有,则终端报错
-
目录作为模块
当把目录作为模块标识符,传递给require()进行加载的时候,有三种加载方式:
-
在被加载的目录下查找一个叫做package.json的文件,并寻找main属性,作为require()加载的入口
-
如果目录里没有package.json文件,或者main入口不存在或无法解析,则Node.js将会试图加载目录下的
index.js文件 -
如果以上两步都失败了,则Node.js会在终端打印错误消息,报告模块的缺失: Error: connot find module ‘xxx’
四、 Express框架
4.1 初识Express
4.1.1 什么是Express?
官方给出的概念: Express是基于Node.js平台,快速、开放、极简的Web开发框架。
通俗的理解:Express的作用和Node.js内置的http模块类似,是专门用来创建Web服务器的。
Express的本质: 就是一个npm上的第三方包,提供了快速创建Web服务器的便捷方法。
Express的中文官网: http://www.expressjs.com.cn/
那么不使用Express能否创建web服务器?
答案是可以的,使用Node.js提供的原生http模块即可。
不过http内置模块用起来很复杂,开发效率低,而EXpress是基于内置的http模块进一步封装处理的,能够极大的提高开发效率
http内置模块和Express是什么关系?
类似于浏览器中的Web API 和 JQuery的关系。后者是基于前者进一步封装出来的。
4.1.2 Express能够做什么?
对于前端程序员来说,最常见的两种服务器,分别是:
-
Web网站服务器:专门对外提供Web网页资源的服务器。
-
API接口服务器:专门对外提供API接口的服务器。
使用Express,我们可以方便、快速的创建Web网站的服务器或API接口的服务器。
4.2 Express的基本使用
- 安装
在项目所处的目录中,运行如下的终端命令,即可将express安装到项目中使用:
npm i express4.2.1 创建最基本的Web服务器
// 1. 导入expressconst express = require('express')// 2.创建web服务器const app = express()
// 调用app.listen(端口号,启动成功后的回调函数),启动服务器app.listen(80, ()=> { console.log('express server running at http://127.0.0.1')})4.2.2 监听GET请求
通过app.get()方法,可以监听客户端的GET请求,具体的语法格式如下:
app.get('请求URL', function(req,res) {/* 处理函数 */})4.2.3 监听POST请求
通过app.post()方法,可以监听客户端的POST请求,具体的语法格式如下:
app.post('请求URL', function(req,res) {/* 处理函数 */})4.2.4 把内容响应给客户端
通过res.send()方法,可以把处理好的内容,发送给客户端:
app.get('/user',(req,res) => { // 向客户端发送JSON对象 res.send({name: 'zs', age: 20, gender:'男'})})
app.post('/user', (req,res) => { // 向客户端发送文本内容 res.send('请求成功')})4.2.5 获取URL中携带的查询参数
通过req.query对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数:
app.get('/', (req,res) => { // req.query默认是一个空对象 // 客户端使用? name = zs&age=20 这种查询字符串形式,发送到服务器的参数, // 可以通过 req.query 对象访问到,例如: // req.query.name req.query.age
console.log(req.query)})4.2.6 获取URL中的动态参数
通过req.params对象,可以访问到URL中,通过:匹配到的动态参数:
app.get('/user/:id', (req, res) => { // req.params 默认是一个空对象 // 里面存放着通过:动态匹配到的参数值
console.log(req.params)})4.3 托管静态资源
4.3.1 express.static()
express提供了一个非常好用的函数,叫做express.static(), 通过它,我们可以非常方便地创建一个静态资源服务器,通过如下代码就可以将public目录下的图片、css文件、JavaScript文件对外开发访问了:
app.use(express.static(`public`))现在,你就可以访问public目录中的所有文件了
- 注意: Express在
在指定的静态目录中查找文件,并对外提供资源的访问路径。因此,存在静态文件的目录名不会出现在URL中。
4.3.2 托管多个静态资源目录
如果要托管多个静态资源目录, 请多次调用express.static()函数:
app.use(express.static(`public`))app.use(express.static(`files`))访问静态资源文件时,express.static()函数会根据目录的添加顺序查找所需的文件。
4.3.3 挂载路径前缀
app.use('/public', express.static('public'))4.4 nodemon
4.4.1 为什么要使用nodemon?
在编写调试Node.js项目中,如果修改了项目的代码,则需要频繁的手动close掉,然后再重新启动,非常繁琐。
现在,我们可以使用nodemon这个工具,它能够监听项目文件的变动,当代码被修改后,nodemon会自动帮我们重启项目,极大方便了开发和调试。
- 安装nodemon
在终端中,运行如下命令,即可以将nodemon安装为全局可用的工具:
npm i -g nodemon4.4.2 使用nodemon
当基于Nodejs编写了一个网站应用的时候,传统的方式,是运行nodeapp.js命令,来启动项目。这样做的坏处是:
代码被修改之后,需要手动重启项目。
现在,我们可以将 node命令替换为 nodemon命令,使用nodemon appjs来启动项目。这样做的好处是:代码 被修改之后,会被nodemon监听到,从而实现自动重启项目的效果。
node app.js# 将上面的终端命令,替换为下面的终端命令,即可实现自动重启项目的效果nodemon app.js4.5 Express路由
4.5.1 什么是路由?
广义上来讲,路由就是映射关系
4.5.2 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')})4.5.3 路由的匹配过程
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求的URL同时匹配成功,则Express会将这次请求,转交给对应的function函数进行处理。
-
路由匹配的注意点:
-
按照定义的
先后顺序进行匹配 -
请求类型和请求的URL同时匹配成功,才会调用对应的处理函数
-
4.5.4 路由的模块化
为了方便对路由进行模块化的管理,Express不建议将路由直接挂在到app上,而是推荐将路由抽离为单独的模块。
将路由抽离为单独的模块的步骤如下:
-
创建路由模块对应的.js文件
-
调用
express.Router()函数创建路由对象 -
向路由对象上挂载具体的路由
-
使用
modules.exports向外共享路由对象 -
使用
app.use()函数注册路由模块
实例:
// 导入expressvar express = require('express');// 创建路由对象var router = express.Router();// 挂载路由router.get('/user/list', function(req, res) { res.send('Get User List');});
router.post('/user/add', function(req, res) { res.send('Add New User')})// 向外导出路由对象module.exports = router;在app.js中,使用app.use()函数,注册路由模块:
const express = require('express')const app = express()// 导入路由模块const useRouter = require('./02.创建路由模块')// 使用路由模块app.use(useRouter)// app.use() 函数的作用,就是来注册全局中间件
app.listen(80, () => { console.log('at 127.0.0.1');})4.5.5 为路由模块添加前缀
类似于托管静态资源时,为静态资源统一挂载访问前缀一样,路由模块添加前缀的方式也非常简单:
cosnt useRouter = require('./user.js')app.use('/api', useRouter)4.6 中间件
4.6.1 中间件的概念
中间件,就是一段代码,在处理请求和响应的过程中,执行某些特定的任务。
像安检一样 你要进机场 ✈️ → 先过安检 → 才能上飞机。
上一级的输出,作为下一级的输入
4.6.2 中间件的调用流程
当一个请求到达Express服务器时,可以连续调用多个中间件,从而对这次请求进行预处理。

4.6.3 中间件的格式
Express的中间件,本质上就是一个function处理喊出,Express中间件的格式如下:
var express = require('express')var app = express()
app.get('/', function(req, res, next) { next();})
app.litten(3000);- 注意: 中间件函数的形参列表中, 必须包含
next参数,而路由处理函数中只包含req和res参数。
next函数的作用:
next() 函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由。
4.6.4 定义中间件函数
中间件函数的定义格式如下:
const myMiddleware = function(req, res, next) { console.log('这是一个简单的中间件函数'); // 传递给下一个中间件或路由 next();}4.6.5 全局生效的中间件
客户端发起任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。
// 可以使用app.use()方法,注册全局生效的中间件app.use(myuMiddleware)有意思的是, app.use()方法,可以简化全局注册中间件的过程。
// 这是定义全局中间件的简化形式app.use((req, res, next) => { console.log('这是一个简单的中间件函数'); next();})4.6.6 中间件的作用
多个中间件之间,共享同一个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('hello world' + req.startTime);})
app.get('/user',(req,res) => { res.send('user page' + req.startTime);})
app.listen(3000, () => { console.log('server is running at port 3000');})4.6.7 定义多个全局中间件
可以使用app.use()方法,一次定义多个全局中间件,这样客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用。
app.use((req, res, next) => { console.log('这是第一个全局中间件');})
app.use((req, res, next) => { console.log('这是第二个全局中间件');})
app.use((req, res, next) => { console.log('这是第三个全局中间件');})4.6.8 定义局部中间件
不使用app.use()方法,定义中间件,这种定义方式,叫做局部中间件。
const myMiddleware = (req, res, next) => { console.log('局部中间件'); next();}
app.get('/', myMiddleware, (req, res) => { res.send('hello world');})
// 上面的局部中间件,只对get请求的'/'进行拦截处理,不会影响下面的路由app.get('/user', (req, res) => { res.send('user page');})4.6.9 定义多个局部中间件
可以在路由中,通过如下两种等价的方式,使用多个局部中间件:
app.get('/', [myMiddleware1, myMiddleware2], (req, res) => {res.send('hello world');})4.6.10 了解中间件的5个使用注意事项
-
一定要在路由之前注册中间件
-
客户端发送过来的请求,可以连续调用多个中间件进行处理。
-
执行完中间件的业务代码之后,不要忘记
调用next()函数。 -
为了防止代码逻辑混乱,调用next()函数后不要再写额外的代码。
-
连续调用多个中间件时,多个中间件之间,
共享req和res对象。
4.7 中间件的分类
为了方便理解和记忆中间件的使用,Express官方把常见的中间件用法,分成了5大类:
-
应用级别的中间件
-
路由级别的中间件
-
错误级别的中间件
-
Express内置中间件
-
第三方中间件
4.7.1 应用级别的中间件
通过app.use()或app.get()或app.post(),绑定到app实例上的中间件,叫做应用级别的中间件,代码示例如下:
// 应用级别中间件(全局中间件)app.use((req, res, next) => { console.log('这是应用级中间件'); next();})
// 应用级别中间件(局部中间件)app.get('/', (req, res) => { console.log('这是局部中间件'); res.send('hello world');})4.7.2 路由级别的中间件
绑定到express.Router()实例上的中间件,叫做路由级别的中间件。它的用法和应用级别中间件没有任何区别,只不过,应用级别中间件是绑定到app实例上,路由级别中间件绑定到router实例上, 代码示例如下:
var app = express()var router = express.Router()
router.use((req, res, next) => { console.log('这是路由级中间件'); next();})
app.use('/', router)4.7.3 错误级别的中间件
错误级别中间件的作用: 专门来捕获整个项目中发送的异常错误,从而防止项目异常崩溃的问题。
app.get('/', (req, res, next) => { // 1. 路由 throw new Error('抛出一个错误') // 2. 抛出一个错误 res.send('hello world');})
app.use((err, req, res, next) => { // 2.错误级别中间件 console.log('发送了错误' + eer.message); // 2.1在服务器中打印错误信息 res.send('Error: ' + err.message); // 2.2发送错误信息给客户端})- 注意: 错误级别的中间件,
必须注册在所有路由之后。
4.7.4 Express内置中间件
自Express 4.16.0版本起,Express内置了很多中间件,这些中间件,不需要单独安装,直接引入使用, 极大的提高了Express开发效率。
-
express.static()快速托管静态资源的内置中间件,例如: HTML文件、图片、CSS样式等(无兼容性)。 -
express.json()解析JSON格式的请求体数据(有兼容性, 仅在4.16.0以上版本可用)` -
express.urlencoded()解析URL-encoded格式的请求体数据(有兼容性, 仅在4.16.0以上版本可用)`
使用方法:
app.use(express.static('./public'))app.use(express.json())app.use(express.urlencoded({encoded: false}))下面对express.json 和 express.urlencoded()中间件的使用,进行演示:
const express = require('express');const app = express();
// 调用express内置中间件:解析json格式数据app.use(express.json())app.use(express.urlencoded({extended: false}))// 定义路由app.post('/user', (req, res) => { // 获取数据 console.log(req.body);19 collapsed lines
res.send('ok')})
app.post('/book', (req,res) => { // 在服务器端,可以通过req.body来获取json格式数据和urlencoded格式数据 console.log(req.body); res.send('ok')})
app.listen(80, () => { console.log('http://127.0.0.1')})
// 输出结果:
// 1. 浏览器发送json格式数据{ name: 'zhangsan', age: 18 }// 2. 浏览器发送urlencoded格式数据[Object: null prototype] { boookname: '水浒传', author: '施耐庵' }4.7.5 第三方中间件
非Express官方提供的中间件,我们自己编写的,或者别人封装好的,都是第三方中间件,可以按需下载或配置第三方中间件,从而提高项目的开发效率
例如: @express@4.16.0之前的版本中,经常使用body-parser这个第三方中间件,来解析请求体数据,使用步骤如下:
-
运行
npm install body-parser按照中间件 -
使用
require导入中间件 -
调用
app.use()注册并使用中间件
实例:
const express = require('express');const app = express();
const parser = require('body-parser');
// 导入解析表单的中间件app.use(parser.urlencoded({extended: false}))
app.post('/user', (req,res) => { // 使用req.body接收post数据 console.log(req.body); res.send('ok')})
app.listen(80, () => { console.log('express server running at http://127.0.0.1');})- 注意: Express内置的express.urlencoded中间件,就是基于body-parser这个第三方中间件进一步封装出来的。
4.7.6 自定义中间件
自己动手模拟一个类似于express.urlencoded这样的中间件,来解析POST提交到服务器的表单数据。
实现步骤:
-
定义中间件
-
监听req的data事件
-
监听req的end事件
-
使用querystring模块解析实体数据
-
将解析出来的数据对象挂载为req.body
-
将自定义中间件封装为模块
完整代码如下:
const express = require('express');const app = express();const querystring = require('querystring');// 注册全局中间件// 这是用于解析表单数据的中间件app.use((req, res, next)=> { // 定义中间件的具体业务逻辑 let str = ''; // 监听req对象的data事件(客户端发送过来的新的请求体数据) req.on('data', (chunk) => {28 collapsed lines
// 定义中间件具体的业务逻辑 // 1.定义一个str字符串,专门用来存储客户端发送过来的请求体数据 str += chunk; // 2.监听req的data事件 req.on('data',(chunk)=> { str += chunk; }) // 3.监听req的end事件 req.on('end', () => {
// TODO: 把字符串格式的请求体数据,解析成对象格式 const body = querystring.parse(str); // 把解析后的结果,挂载到req.body属性上,由于这个中间件,在所有路由中,req.body,都是可用的 req.body = body; // 4.把next(),调用下一个中间件或者路由 next(); }) })})
app.post('/user', (req, res) => { console.log(req.body); res.send('ok');})
app.listen(80, () => { console.log('http://127.0.0.1');})当然了,最好是进行封装,封装为一个自定义的中间件模块,方便调用, 使用exports导出
module.exports = bodyParser;4.8 使用Express写接口
4.8.1 创建基本服务器
创建一个最基本的服务器,并监听80端口:
// 导入expressconst express = require('express');// 创建服务器实例const app = express();
// 在这里写你的业务代码
// 监听端口,启动服务app.listen(80, () => { console.log('http://127.0.0.1');})4.8.2 创建API路由模块
创建一个API路由模块,并使用app.use()注册这个路由模块,这样,就可以使用API路由模块中的路由了。
const express = require('express');const apiRouter = express.Router();
// 在这里挂载对应的路由
module.exports = apiRouter;
// app.js[导入并注册路由模块]const router = require('./apiRouter');app.use('/api', router);4.8.3 编写GET接口
const express = require('express');const app = express();const router = express.Router();
// 在这里挂载对应的路由router.get('/get', (req, res) => { // 通过 req.query 获取客户端字符串,发送到服务器的数据 const query = req.query;
// 调用res.send()方法,向客户端响应处理的结果
res.send({ status: 0, //表示成功,1表示失败 message: 'GET请求成功', //状态的描述 data: query //需要响应给客户端的数据 });});
module.exports = router;后续我们只需在外部引入该文件,并使用app.use()方法挂载该路由模块即可
4.8.3 编写POST接口
// 定义post接口router.post('/post', (req, res) => { const body = req.body;
res.send({ status: 0, message: 'POST请求成功', data: body })});- 注意: 两个请求接口,可以放在同一个文件中,也可以放在不同的文件中,引入方法依旧使用
app.use()
4.9 跨域资源共享
4.9.1 接口的跨域问题
刚才编写的GET和POST接口,存在一个很严重的问题: 不支持跨域请求。
造成这个问题的原因是,接口使用的http协议,而用本地打开的html文件l’l文件使用的是file协议。
浏览器对file协议不支持跨域请求,所以接口无法访问。
4.9.2 使用cors中间件解决跨域问题
cors中间件,是一个第三方的中间件,用于解决跨域问题。
使用步骤:
-
安装cors中间件:
npm install cors -
引入cors中间件:
const cors = require('cors') -
使用cors中间件:
app.use(cors())
4.9.3 什么是cors?
cors是Cross-Origin Resource Sharing的缩写,中文翻译为跨域资源共享。
由一系列HTTP响应头组成,这些HTTP响应头决定浏览器是否阻止前端JS代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页跨域获取资源。但如果接口服务器配置了CORS相关的HTTP响应头,就可以解决浏览器前端的跨域访问限制。
CORS的注意事项:
-
CORS主要在
服务器端进行配置,客户端浏览器无须做任何额外的配置,即可请求开启了CORS的接口。 -
CORS在浏览器中
有兼容性,只有支持XMLHttpRequest Level 2的浏览器才能使用CORS。
4.9.4 CORS相关的三个响应头
- Access-Control-Allow-Origin:表示允许的源,即允许的域名。
其语法如下:
Access-Control-Allow-Origin: <origin> | *其中, orgigin参数的值指定了允许访问该资源的外域URL
例如,下面的字段值将只允许来自http://www.example.com的请求访问该资源:
res.setHeader('Access-Control-Allow-Origin', 'http://www.example.com');- Access-Control-Allow-
Headers:表示允许的请求头字段。
默认情况下, CORS仅支持客户端向服务器发送如下的9个请求头:
- Accept、Acccept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type
如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过设置Access-Control-Allow-Headers对额外的请求头进行声明,否则这次请求会失败。
- CORS-Allow-Methods:
表示允许的请求方法。
默认情况下,CORS仅支持客户端发起的GET、POST、HEAD请求。
如果客户端希望通过PUT、DELETE、PATCH等请求方式向服务器发送请求,则需要在服务器端,通过设置Access-Control-Allow-Methods对请求方式进行声明,否则这次请求会失败。
示例:
// 只允许POST、PUT、DELETE、PATCH请求res.setHeader('Access-Control-Allow-Methods', 'POST, PUT, DELETE, PATCH');// 允许所有请求方式res.setHeader('Access-Control-Allow-Methods', '*');4.9.5 CORS简单请求与预检请求
- 简单请求
同时满足以下条件时,为简单请求:
-
请求方式: GET、POST、HEAD
-
预检请求
只要符合以下任何一个条件的请求。都需要进行预检请求:
-
请求方法为
GET、POST、HEAD、之外的请求Method类型 -
请求头中包含
自定义头部字段 -
向服务器发送了
application/json格式的请求体
在浏览器与服务器正是通信之前,浏览器会先发送OPTION请求进行预检,已获知服务器是否允许该实际请求,所以这一次的OPTION请求称为预检请求。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。`
-
简单请求和预检请求的区别
-
降低请求的特点: 客户端与服务器之间只会发送一次请求。
-
预检请求的特点:客户端与服务器之间会发送两次请求,OPTION预检请求成功之后,才会发起真正的请求。
-
五、MySQL模块
5.1 安装并配置mysql模块
在项目中操作数据库的步骤:
-
安装操作MySQL 数据库的第三方模块(
mysql) -
通过mysql模块
连结到MySQL数据库 -
通过mysql模块
执行SQL语句
- 步骤一 安装mysql模块
# 由于老版本的mysql模块不支持最新版本的数据库协议,所以安装mysql2模块npm install mysql2- 步骤二 连接数据库
// 1.导入mysql模块const mysql = require('mysql2')
// 2.创建数据库连接对象const db = mysql.createPool({ host: '127.0.0.1', user: 'root', password: '123456', database: 'my_db_01',})
// 测试 mysql 模块能否正常工作
db.query('select 1', (err, results) => { // 如果执行 SQL 语句失败了,则 err 的值是一个 错误对象,否则 err 的值为 null if(err) { return console.log(err.message) } console.log(results)})最终的输出结果为
[ { '1': 1 } ]5.2 查询和插入数据
- 查询数据
// 查询users表中的所有数据db.query('select * from users', (err, results) => { // 如果执行 SQL 语句失败了,则 err 的值是一个 错误对象,否则 err 的值为 null if (err) { return console.log(err.message) } // console.log(results) console.log(results)})- 插入数据
// 插入数据const user = `INSERT INTO users (username, password) VALUES (?, ?)`db.query(user, ['admin', '123456'], (err, results) => { if (err) { return console.log(err.message) }// 注意 如果执行的是 insert into 插入语句,则results 是一个对象// 在这个对象中有一个属性叫 affectedRows// 可以通过 affectedRows 来判断是否插入成功 if(results.affectedRows === 1) { console.log('插入数据成功') }})5.3 更新和删除数据
- 更新数据
// 更新数据
const user = `UPDATE users SET username=?, password=? WHERE id=?`
db.query(user, ['admin123', '123456', 2], (err, results) => { if (err) { return console.log(err.message) } // 注意 如果执行的是 insert into 插入语句,则results 是一个对象 // 在这个对象中有一个属性叫 affectedRows if(results.affectedRows === 1) { console.log('更新数据成功') }})- 删除数据
// 删除数据const user = `DELETE FROM users WHERE id=?`
db.query(user, [2], (err, results) => { if (err) { return console.log(err.message) } if (results.affectedRows === 1) { console.log('删除数据成功') }else { console.log('没有此用户') }})5.4 标记删除
使用逻辑删除,将数据标记为删除,而不是真正的删除数据。
- 在表结构中添加字段
ALTER TABLE users ADD status TINYINT DEFAULT 0;- 删除数据
const sql = 'update users set status = 1 where username = ?'
// 删除数据
const user = `UPDATE users SET status=? WHERE id=?`
db.query(user, [1, 1], (err, results) => { if (err) { return console.log(err.message) } if (results.affectedRows === 1) { console.log('标记删除数据成功') }else { console.log('没有此用户') }})六、 Web开发模式
目前主流Web开发模式有两种,分别是:
-
基于
服务器端渲染的传统Web开发模式 -
基于
前后端分离的Web开发模式
6.1 服务器渲染的Web开发模式
服务器渲染的概念: 服务器发送给客户端的HTML页面,是在服务器通过字符串的拼接,动态生成的。 因此,客户端不需要使用Ajax这样的技术额外请求页面的数据。代码示例如下:
app.get('/idnex.html', (req, res) { // 1. 要渲染的数据 const user = {name: 'zs', gae: 20} // 2. 服务器端通过字符串的拼接,动态生成HTML内容 const html = `<h1>姓名:$${user.name}, 年龄:${user.age} </h1>` // 3. 把生成好的页面内容响应给客户端,因此,客户端拿到的是带有真实数据的HTML页面 res.send(html)})服务器渲染的优缺点
-
优点:
-
前端耗时少。
-
有利于SEO。
-
-
缺点:
-
占用服务器端资源。
-
不利于前后端分类。
-
6.2 前后端分离的Web开发模式
前后端分离的概念: 前后端分离的开发模式,依赖于Ajax技术的广泛应用。简而言之,前后端分离的Web开发模式,就是后端只负责API接口,前后和使用Ajax调用接口的开发模式。
前后端分离的Web开发模式的优缺点
-
优点:
-
开发体验好。
-
用户体验好。
-
减轻了服务器端的渲染压力。
-
-
缺点:
- 不利于SEO
- 不过利用VUE、React等前端框架的SSR(server side render)技术能够很好的解决SEO问题。
6.3 如何选择Web开发模式
不谈业务场景而盲目选择使用何种开发模式都是耍流氓。
-
比如企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的SEO,则这时我们就需要使用服务器端渲染。
-
而类似后台管理项目,交互性比较强,不需要考虑SEO,那么就可以使用前后端分离的开发模式。
另外,具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器渲染+其他页面前后端分离的开发模式。
七、身份认证的概念
7.1 什么是身份认证?
身份认证(Authentication)又称’身份验证’、‘鉴权’, 是指通过一定的手段,完成对用户身份的确认。
7.2 为什么需要身份认证?
身份认证是保证用户安全的必要条件。是为了确认当前所声称为某种身份的用户,确实是所声称的用户。
7.2.1 不同开发模式下的身份认证
对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
-
服务器端推荐使用
session或者cookie进行身份认证。 -
前后端分离推荐使用
token或者jwt进行身份认证。
八、 session认证机制
8.1 HTPP协议的无状态性
了解HTTP协议无状态性是进一步学习Session认证机制的必要条件。
HTTP协议是无状态的,即一次请求和响应之间没有状态信息。这意味着,每次请求和响应之间都是新的,没有 Previous Request 和 Previous Response 的概念。
8.1.1 如何突破HTTP无状态性?
对于超市来说,为了方便收银员在进行结算时给VIP用户打折,超市可以为每个VIP用户发放会员卡。
现实生活中的会员卡身份认证方式,在Web开发中的专业术语叫做Cookie
8.2 什么是Cookie?
Cookie是存储在用户浏览器中的一段不超过4KB的字符串。它由一个名称(Name),一个值(Value)和其它几个用于控制Cookie有效期、安全性、使用范围的可选属性组成。
不同域名下的Cookie各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的Cookie一同发送到服务器。
Cookie的几大特性:
-
自动发送
-
域名独立
-
过期时限
-
4KB限制
8.2.1 Cookie在身份证中的作用
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的Cookie,客户端会自动将Cookie保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
8.2.2 Cookie不具有安全性
由于Cookie是存储在浏览器中的,而且浏览器也提供了读写Cookie的API,因此Cookie很容易被伪造,不具有安全性,因此不建议服务器将终于的隐私数据,通过Cookie的形式发送给浏览器。
- 注意: 千万不要使用Cookie存储重要且隐私的数据,比如用户的身份认证、密码等。
8.2.3 提高身份认证的安全性
为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡之后,可以在收银机上进行刷卡认证。只有在收银机确认存在的会员卡,才能被正常使用。
这种会员卡+刷卡认证的设计理念,就是Session认证机制的精髓。
8.2.4 Session的工作原理

8.3 在Express中使用Session认证
- 安装Session模块
npm install express-session- 配置Session
// 1.导入Session模块var session = require('express-session');// 2.配置Session中间件app.use(session({ secret: 'keyboard cat', resave: false, saveUninitialized: true}))8.3.1 向session中保存数据
当express-session中间件生效时,会为req对象添加session属性,即req.session, 这个属性是一个对象,可以保存用户信息。
实例:
app.post('/api/login', (req, res) => { // 判断用户名密码是否正确 if (req.body.username !== 'admin' && req.body.password !== '123') { return res.send({ status: 1, msg: '登录失败' }) }
// 登录成功 req.session.user = req.body // 保存用户名到session对象中 req.sesstion.isLogin = true // 保存登录状态
res.send({ status: 0, msg: '登录成功' })})8.3.2 从session中获取数据
可以直接从req.session中获取之前存储在session中的数据
实例:
// 获取用户姓名的接口app.get('/api/username', (req, res) => { // 判断用户是否登录 if(!req.session.isLogin) { return res.send({ status: 1, msg: '获取用户名失败' }) } res.send({ status: 0, msg: '获取用户名成功', username: req.session.user.username })})8.3.3 删除session数据
调用req.session.destroy()方法即可清空服务器的session信息
实例:
// 退出登录的接口app.post('/api/logout', (req, res) => { // 销毁session数据 req.session.destroy() res.send({ status: 0, msg: '退出登录成功' })})九、JWT认证机制
9.1 了解Session认证的局限性
Session认证机制需要配合Cookie才能实现,由于Cookie默认不支持跨域访问,所以当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域Session认证。
- 注意:
-
当前端请求后端接口
不存在跨域问题的时候,推荐使用Session身份认证机制。 -
当前端请求后端接口
存在跨域问题时,推荐使用JWT认证机制。
9.2 什么是JWT?
JWT(JSON Web Token)是一种用于在客户端和服务器之间传递数据的安全方法。是目前最流行的跨域认证解决方案。
9.3 JWT的工作原理

总结: 用户的信息通过Token字符串的新式,保存在客户端浏览器中。服务器通过还原Token字符串的形式来认证用户的身份。
9.4 JWT的组成部分
JWT 由三部分组成:header(头部)、payload(有效负载)、signature(签名)。
三者之间使用英文的”.”分隔,格式如下:
Header.Payload.Signature9.4.1 JWT的三个部分各自代表的含义
JWT 的三个部分组成部分,从前到后分别是Hheader, payload, signature。
-
Payload部分才是真正的用户信息,它是用户信息经过加密城市的字符串。
-
Header和Signature部分是安全性相关的部分,只是为了保证Token的安全性。
9.5 JWT的使用方式
客户端收到服务器返回的JWT后,通常会将它存储在localStorage或者SessionStorage中。
此后,客户端每次与服务器通信,都要带上这个JWT的字符串,从而进行身份认证,推荐的做法是把JWT放在HTPP请求头的Authorization字段中。
示例代码:
Authorization: Bearer <token>9.5.1 安装JWT相关的包
npm install jsonwebtoken express-jwt其中:
-
jsonwebtoken用于生成JMT字符串 -
express-jwt用于将JWT字符串解析还原成JSON对象
9.5.2 导入JWT相关的包
使用require()函数,分别导入JWT相关的两个包:
// 1. 导入用于生成JWT字符串的包const jwt = require('jsonwebtoken');
// 2. 导入用于将客户发送过来的JWT字符串,解析还原成JSON对象的包const expressJwt = require('express-jwt');9.5.3 定义secret密钥
为了保证JWT字符串的安全性,防止JWT字符串被恶意篡改, 我们需要专门定义一个用于加密和解密的secret密钥。
-
当生成JWT字符串时,需要使用secret密钥对用户的信息
进行加密,最终得到加密的JWT字符串 -
当把JWT字符串解析还原成JSON对象的时候,需要使用secret密钥
进行解密
const secretKey = 'this is a secret key'9.5.4 在登录成功后生成JWT字符串
调用jsonwebtoken的sign()方法,将用户的信息加密成JWT字符串,响应给客户端:
// 登录接口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 属性发送给客户端
token = jwt.sign({username: userinfo.username}, secretKey, {expiresIn: '30s'})
res.send({ status: 200, message: '登录成功!', token: token // 要发送给客户端的 token 字符串 })})9.5.5 将JWT字符串还原为JSON对象
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的Authorization字段, 将Token字符串发送到服务器进行身份认证。
此时,服务器可以通过express-jwt中间件,将客户端发送过来的Token字符串还原为JSON对象。
// 使用app.use()注册中间件// express.JWT({secret: secretKey}) 就是用来解析Token的中间件// .unless({path: [/^\/api\//]}) 指定哪些接口不需要访问权限(即不需要提供 token 令牌),以数组的形式传入app.use(expressJWT({ secret: secretKey, algorithms: ['HS256'] }).unless({ path: [/^\/api\//] }))// 登录接口9.5.6 使用req.auth获取用户信息
当express-jwt这个中间件配置成功之后,即可在那些有权限的接口中,使用req.user对象,来访问从JWT字符串中解析出来的用户信息了,示例代码如下:
app.get('/admin/getinfo', (req, res) => { console.log(req.user) res.send({ status: 200, message: '获取用户信息成功', data: req.auth })})9.5.7 捕获解析JWT失败后产生的错误
当使用express-jwt解析Token字符串,如果客户端发送过来的Token字符串过期或者不合法,会产生一个解析失败的错误,影响项目的正常运行。我们可以通过Express的错误中间件,捕获这个错误并进行相关的处理,示例代码如下:
app.use(function (err, req, res, next) { // token解析失败导致的错误 if(err.name === 'UnauthorizedError'){ return res.status(401).json({status: 401, message: '无效的Token'}) } // 其他原因导致的错误 res.send({status: 500, message: '未知错误'})})支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
Summer Pockets