浏览器中的 JavaScript 组成部分由 JS核心语法 + WebAPI 组成

浏览器的 js 的运行依赖于 浏览器中有针对 解释js代码 的特有引擎,不同浏览器都拥有自家的 js解释引擎,性能最好的解释引擎为谷歌的 Chrome V8
浏览器的 JavaScript 可以直接操作 浏览器的 DOM 和 BOM

浏览器提供了很多内置API,如 DOM BOM Ajax 的API,程序员可以通过js调用这些API实现功能或者获得有用的信息,最后将待执行的JS代码依赖浏览器内置的JavaScript解析引擎实现对网页内容操作的效果
浏览器的 JavaScript 的运行环境

运行环境 是指代码正常运行所需要的必要环境,例如 Chrome 浏览器运行环境 中由 V8引擎、内置API (如:DOM、Canvas、XMLHttpRequest、js内置对象等…)
JavaScript 可用于后端开发
与 java、python、php一样,JavaScript也可以做后端开发,依赖Node.js 和依赖 Node.js的支持插件 使一切都有无限的可能 ( [Express] 跨平台桌面应用、[Electron] Web应用、[restify] API接口、读写和操作数据库、创建实用命令行工具辅助前端开发等…)
Node.js 背景
Node.js发布于2009年5月,由Ryan Dahl开发,是一个基于Chrome V8引擎的JavaScript运行环境

Node.js 的官网地址:https://nodejs.org/zh-cn

与 谷歌浏览器的 JavaScript运行的原理相似,由 V8引擎 + 内置API 构成,V8引擎用于解析js代码,内置API 提供对于系统的基础支持功能操作
浏览器 是 JavaScript 的前端运行环境
Node.js 是 JavaScript 的后端运行环境
Node.js 中无法调用 DOM 和 BOM 等浏览器内置 API

区分 LTS 版本和 Current 版本的不同
LTS 长期维护版本适用于企业级项目,追求稳定,推荐安装该版本
Current 尝鲜版本为测试版本,适用于追求尝试新特性的用户。但可能存在隐藏性BUG或者安全隐患
*常用终端指令
打开指令内容
操作 | 方法 |
---|---|
切换盘符 | 盘符名称+冒号 |
查看当前文件夹的内容 | dir |
进入单级文件夹(当前目录下的文件夹) | cd +文件夹名称(可加/可不加) |
回退单级文件夹 | cd.. |
进入多级文件夹 | cd +文件夹名称1/文件夹名称2/… |
直接退到盘符 | cd\或cd/ |
清屏 | cls |
退出 | exit |
打开文件 | 完整文件名(包括后缀名) |
删除文件 | del +完整文件名(包括后缀名) |
打开远程桌面 | mstsc |
打开控制面板 | control |
查看所有端口的使用情况 | netstat -ano |
定位上一次命令 | ↑键 |
补全路径(后缀识别) | tab键 |
终端是专门为开发人员设计的,用于实现人机交互的一种方式
验证 Node.js 是否安装完成,可发送 windows + R 输入 cmd 调出终端,在终端输入 node -v 验证,显示版本号则安装成功。
fs模块是Node.js提供、用来操作文件的模块。它提供了一系列方法和属性,用来满足用户对文件的操作需求
如果需要在 jacaScript 代码中,使用 fs模块来操作文件,需要使用如下方式先导入它:
const fs = require(‘fs’);
操作
fs.readFile(path[,options],callback); // 读取指定文件夹中的内容 文件路径 回调函数
fs.writeFile(); // 向指定文件中写入内容
// 导入 fs 模块 该模块用途为为操作文件
const fs = require(‘fs’);
/*
调用 fs.readFile()方法 获取文件 (异步操作)
参数1:必选 读取文件存放路径
参数2:可选 读取文件采用的编码格式 一般默认指定 utf8
参数3:必选 读取失败和成功的结果 返回的 回调函数 err data
*/
fs.readFile(‘file/hello.txt’, ‘utf8’, function (err,data) {
// 判断是获取否成功
if (err) {
// 获取失败 打印错误提示
return console.log(‘获取失败:’ + err.message);
} else {
// 获取成功 打印结果
console.log(‘获取成功:’ + data);
}
})
// 导入 fs 模块 该模块用途为为操作文件
const fs = require(‘fs’);
/*调用 fs.readFile()方法 获取文件
参数1:必选 写入文件存放路径 如若不存在该文件 将自动生成 (但不能创建文件夹)
参数2: 必选 写入的内容 一般写入字符串
参数3:可选 写入文件采用的编码格式 一般默认指定 utf8
参数4:必选 写入失败和成功的结果 返回的 回调函数 err data
*/
fs.writeFile(‘file/hello.txt’, ‘好耶’, function (err, data) {
// 判断是写入否成功
if (err) {
// 写入失败 打印错误提示
return console.log(‘写入失败:’ + err.message);
} else {
// 写入成功 打印结果
console.log(‘写入成功’);
}
})
在使用 fs 模块操作文件时,如果提供的操作路径是以 ./ 或者 ../ 开头的相对路径时,很容易出现路径动态拼接的问题
原因:代码在运行时,会以执行 node 命令时所处的目录,动态拼接处被操作文件的完整路径
以绝对路径作为解决方法 不推荐
// 出现路径错误 如果要解决这个问题 可以提供一个完整的文件存放路径
// 路径的\会被js误会成转义,因此需要修改为\\
// 该解决方法 移植性非常差 不利于维护
fs.readFile(‘D:\\word\\成绩.txt‘, ‘utf8’, function (err, data) {
…
}
使用 __dirname作为当前文件的所处目录 推荐
// __dirname 表示当前文件所处的位置 并对后面文件进行拼接
// __dirname 不会随着执行 node命令的位置的变化而变化
fs.readFile(__dirname + ‘/成绩.txt’, ‘utf8’, function (err, data) {
…
}
因此,在将来的所有访问文件路径中,避免以./ ../ 开头作为相对路径,都应该采用 __dirname 识别当前js文件路径
path模块 是 Node.js 官方提供的、用来处理路径的模块,它提供了一系列的方法和属性,用来满足用户对路径的处理需求
例如:
path.join() 方法 用来将多个路径片段拼接成一个完整的路径字符串
path.basename() 方法 用来从路径字符串中,将文件名解析出来
如果要在 JavaScript 代码中,使用path模块来处理路径,则需要使用如下方式先导入它:
const path = require(‘path’);
操作:
// 参数1 …paths<string>路径片段的序列 返回值 String
path.join([…paths]);
// 参数1 path<string>必选参数,表示一个路径的字符串 ext<string>可选参数 表示文件扩展名
path.basename(path[, ext]); // 返回<string> 表示路径中的最后一部分
// 参数1 path<string> 必选参数 路径的字符串 返回 得待的扩展名字符串
path.extname(path); // 获取路径中的文件扩展名部分
path.join() 会将参数中的路径进行规范化处理,返回 单个路径
const path = require(‘path’);
// ../ 返回上一级 会直接抵消前一个路径的选择
const pathStr = path.join(‘/js/../img/../nodejs作业/考试成绩整理/change.js’);
console.log(pathStr); // 输出: \nodejs作业\考试成绩整理\change.js
const pathStr2 = path.join(__dirname , ‘file/hello.txt’);
console.log(pathStr2); // 输出:当前文件所处目录(绝对路径)\file\hello.txt
建议未来都采用 path.join() 作为文件路径的拼接,而不是采用 + 进行路径拼接
// path.join() 会对文件路径的 拼接过程中存在的 ../ ./ 进行优化或屏蔽 使其指向合理且正确的位置
fs.readFile(path.join(__dirname , ‘file/hello.txt’), ‘utf8’, function (err, data) {
if (err) {
return console.log(‘获取文件路径失败:’ + err);
} else {
console.log(data);
}
});
使用 path.basename() 方法,可以从一个文件路径中,获取到文件的名称部分
const path = require(‘path’);
// 定义文件的存放路径
const fpath = ‘/img/bqb/01.png’;
// 获取路径中的最后一个文件
const fullName = path.basename(fpath);
// 如若参数2中传入扩展名 则会隐藏该扩展名
const fullName2 = path.basename(fpath, ‘.png’);
console.log(fullName); // 01.png
console.log(fullName2); // 01
使用 path.extname() 方法,可以获取路径中扩展名的部分
const path = require(‘path’);
// 文件存放路径
const fpath = ‘/nodejs作业/点赞/praise.txt’
// 获取路径中的文件扩展名部分
const fext = path.extname(fpath);
console.log(fext); // .txt
将 path.basename() 与 path.extname() 搭配使用,可以动态获取 文件名全名 和 仅文件名
// 路径
let url=’../02-购物车素材/img/arrow-prev.gif’
// 获取文件名称 保留扩展名
const fullname=path.basename(url);
// 获取文件名称 裁剪扩展名 搭配 extname
const fullname2=path.basename(url,path.extname(url));
客户端、服务器
在网络节点中负责消耗资源的电脑,叫做客户端,负责对外提供网络资源的电脑,叫做服务器。服务器和普通电脑的区别在于,服务器上安装了 web服务器软件,比如 IIS、Apache 等,通过安装这些服务器软件,就能把一台普通的电脑变成一台 web服务器
IP地址
IP地址 就是互联网上每台计算机的唯一地址,因此 IP地址 具有唯一性,如果把 “个人电脑” 比作 “一台电话”,那么 “IP地址” 就相当于 “电话号码”,只有在知道对方 IP地址 的前提下,才能与对应的电脑之间进行数据通信。
IP地址的格式通常使用 “点分十进制” 表示成 (a,b,c,d) 形式,其中 a,b,c,d 都是 0~255 之间的十进制整数,例如 用点分十进制 表示的IP地址 (192.168.1.1)
域名 和 域名服务器
域名(Domain Name) 即为 域名服务器(DNS Domain Name Server) 提供的绑定IP地址的访问门牌,IP和域名是一一对应关系,使用者只需要通过好记的域名访问对应的服务器即可,对应的转换工作由域名服务器实现。
本地服务器为 127.0.0.1 对应的域名为 localhost
端口号
在一台电脑中,可以运行各种web服务。每个web服务都对应着一个唯一的端口号,客户端发过来的网络请求,通过端口号,可以被准确的交给对应的 web服务进行处理。端口具有唯一性,不能多个应用使用一个端口
例如:web网页应用为 80 端口, URL中默认使用该端口,通常被省略
http模块 是 Node.js 官方提供的、用来创建 web服务器 的模块,通过 http模块 提供的 http.createServer() 方法,就能方便的把一台普通的电脑,变成一台 Web服务器,从而对外提供 Web 资源服务
> 1. 导入 http模块
> 2. 创建 web服务器实例
> 3. 为服务器实例绑定 request 事件
> 4. 启动 服务器
如果要在 JavaScript 代码中,使用path模块来处理路径,则需要使用如下方式先导入它:
const http = require(‘http’);
下方为一个基础的服务器的组成 监听事件 返回内容:
// 引用 http 模块
const http = require(‘http’);
// 创建服务器
const server = http.createServer();
// 服务器监听 请求事件 当客户端请求到来的时候,该事件被触发,
// request 提供两个参数request
和response
表示请求和响应的信息
server.on(‘request’, (req, res) => {
// 获取客户端请求的 路径
const url = req.url;
// 获取客户端请求的 方式
const method = req.method;
// 以模板字符串的方式 拼接信息
const tip = `客户端访问的地址为${url},访问的方式是${method}`;
// 修改响应头 目的是为了避免中文乱码
res.setHeader(‘Content-Type’, ‘text/html;charset=utf-8’);
// 控制台打印 请求事件
console.log(‘访问事件:’ + tip);
// 服务器返回的信息
res.end(‘您请求了这次访问,信息为:’ + tip);
});
// 监听端口 回调函数 打印信息
server.listen(81, () => {
console.log(‘服务器已开启 地址为: http://127.0.0.1:81’);
});
引用 node:querystring 包
为了方便解析 req.url 中携带的 ? 后的参数,Node.js 内置了 node:querystring 包
// querystring 会对传递过来的 GET查询参数 进行解析,转为对象格式
const querystr = require(‘node:querystring‘);
let obj = querystr.parse(‘user=smm&&pws=233’);
console.log(obj); // { user: ‘smm’, pws: ‘233’ }
使用 node:querystring 包 获取 req.url 中的查询参数
发起 http://127.0.0.1:81/?user=smm&type=vip 请求,返回 {“user”:”smm”,”type”:”vip”}
const http=require(‘http’);
const querystring=require(‘node:querystring‘);
// 创建 服务器
const server=http.createServer((req,res)=>{
// 获取 请求方式
const method=req.method;
// 获取 请求路径 + 查询参数
const url=req.url;
// 针对 url 中的携带的查询参数 解析成请求对象
req.query=querystring.parse(url.split(‘?’)[1]);
// 返回数据 对象转字符串
res.end(JSON.stringify(req.query));
});
// 监听端口
server.listen(81,()=>{
console.log(‘服务器已在 http://127.0.0.1:81 运行’);
})
http传输 post请求体 时,可以当作两个水桶互相传输水流,期间数据是一点点移动到另一个水桶的,而不是一次性
const http = require(‘http’);
const querystring = require(‘node:querystring’);
// 创建 服务器
const server = http.createServer((req, res) => {
// 如果 客户端请求方式为 POST
if (req.method == ‘POST’) {
// 定义变量 存储 POST 传输过来的请求体
let postData = ”;
// 监听 请求体传输 事件
req.on(‘data‘, chunk => {
// 对 http包 进行 持续拼接
postData += chunk.toString();
});
// 监听 请求体传输完成 事件
req.on(‘end‘, () => {
console.log(postData); // user=smm
// 返回 请求体 对象字符串
res.end(JSON.stringify(querystring.parse(postData)));
});
// 打印请求体类型
console.log(req.headers[‘content-type’]);
}
});
// 监听端口
server.listen(81, () => {
console.log(‘服务器已在 http://127.0.0.1:81 运行’);
})
编程领域的模块化,就是遵守固定的规则,把一个大文件拆成独立并互相依赖的多个小模块
1. 提高了代码的复用性
2. 提高了代码的可维护性
3. 可以实现按需加载
创建的js文件,就是一个自定义模块,引用的方式为 require(模块名 or 文件路径);
1. 内置模块 内置模块是 Node.js 官方提供的,例如 fs path http 等
2. 自定义模块 用户创建的每个 .js 文件,都是自定义模块
3. 第三方模块 由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需下载
// 内置模块
const fs = require(‘fs’);
// 自定义模块
const custom = require(‘./custom.js’);
// 第三方模块
const moment = require(‘moment’);
注意:使用 require() 方法加载其他模块时,会执行被加载模块中的代码,require会尝试处理没有后缀的文件,以.js,.json,.node的顺序去补充扩展名
Node.js 中的模块化的作用域
和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种访问限制叫做模块作用域。它有效的阻止了全局变量污染。
Node.js 中向外共享模块作用域中的成员
要想共享模块内的方法或属性,需要使用module对象。
在每个 .js 自定义模块中都有一个 module对象,它里面存储了和当前模块有关的信息
module属性 | 类型 | 说明 |
---|---|---|
id | string | 本模块中使用,则其值为:“.” 如果被 导出,则其值 与 moudle.filename 相等 |
path | string | 文件存储路径 |
exports | Objet | 向外共享的成员对象 |
filename | string | 当前模块完整文件名 带路径 |
loaded | bool | 当值为false表示模块未加载完毕,属性值为true时,表示模块加载完毕 |
children | list | 包括当前模块的所有子模块对象 |
paths | list | – |
对外共享模块对象
在自定义模块中,可以使用 module.exports 对象,将模块内的成员共享出去,供外界使用
// 向 module.exports 对象上挂载 username 属性
module.exports.username = ‘张三’;
// 向 module.exports 对象上挂载 seyHi 方法
module.exports.seyHi = function () {console.log(‘hello’);};
接收共享的对象
外界用 require(); 方法导入的自定义模块时,得到的就是 module.exports 所指向的对象
// 引用模块暴露的 成员方法
const m=require(‘youJsName.js’);
console.log(m); // { username: ‘张三’, seyHi: [Function (anonymous)] }
exports对象
由于 module.exports 单词写起来比较复杂,为了简化向外共享成员的代码,Node 提供了 exports对象,默认情况下,exports 和 module.exports 指向同一个对象。最终共享的结果还是以 module.exports 指向的对象为准
console.log(module.exports === exports); // true
时刻谨记,require(); 引用永远为 module.exports 指向的对象。如下图所示,当 modules.exports 重新赋值新对象时,指向的下标不再与 exports 相同,因此 exports 指向的对象相当于被舍弃

当 exports 重新赋值新对象时,指向的下标不再与 modules.exports相同,因此 exports 指向的对象也相当于被舍弃

当 exports 与 modules.exports 同时拥有各自赋值的属性时,由于引用的皆为同一个下标的对象,因此它们各自赋值的内容将同时存在此下标的对象内

当 exports 重新赋值对象时,原与 module.exports 指向的下标不再相同,然后 module.exports 又被 exports 重新赋值指向相同的下标。因此当 module.exports 再次赋值时,它们各自接下来赋值的内容将又同时存在相同的下标对象内

注意:为了放置混乱,建议大家不要在同一个模块中同时使用 exports 和 module.exports
Node.js 遵循 CommonJS 模块化规定,CommonJS 规定了模块的特征和各模块之间如何相互依赖
CommonJS规定:
1. 每个模块内部,module变量代表当前模块
2. module变量是一个对象。它的 exports 属性 (即 module.exports) 是对外的接口
3. 加载某个模块,其实是加载该模块的 module.exports 属性 (require() 方法用于加载模块)
国外又一家IT公司,叫做 npm 。它有旗下全球最大的包共享平台网站,可以在这个网站上搜索到任何你需要的包,只要有足够的耐心。

搜索包: https://www.npmjs.com/ 网站上搜索自己所需要的包
下载包: https://registry.npmjs.org/ 服务器上下载自己需要的包
下载并安装 Node.js 时已经预装了 npm 包管理系统,可在终端输入 npm -v 查看是否安装,如若已安装会提示当前 npm 版本号:
# cmd 检查是否安装 npm
npm -v
导入包的办法为如下方式,其中 install 可以直接简写为 i:
npm install 包的完整名称
npm i 包的完整名称
npm i 包的完整名称@指定版本号
npm i 包的完整名称 包的完整名称 …
输入后,npm 包管理系统会自动下载该包,同时在终端显示进度和结果,下载的包会放置在终端运行路径下的 node_modules 文件夹中,且该文件夹内部还有 package-lock.json 的配置文件 和 若干文件夹

若要使用 CommonJS方式 引用该第三方模块,如时间格式化,只要输入 以下代码 即可调用该包:
// 引入第三方模块 时间格式化 并将模块赋值给 moment
const moment=require(‘moment’);
第三方包文件内容
在 npm 生成的文件中,node_modules 文件夹 用于存放所有已安装到项目的包,require() 导入第三方包时,就是从该目录中查找并加载包。该文件夹下的 package-lock.json 配置文件是用来记录 node_modules 目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等
package-lock.json 配置文件参数
{
“lockfileVersion”: 1,
“requires”: true,
“node_modules/moment”: {
# 版本号
“version”: “2.29.3”,
# 下载地址
“resolved”: “https://registry.npmjs.org/moment/-/moment-2.29.3.tgz”,
“integrity”: “sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==”,
}
}
第三方包版本参数
包的版本号是以 “点分十进制” 形式进行定义的,总共有三位数字,例如 2.24.0 其中每一位数字所代表的含义如下:
第一位数字:大版本
第二位数字:功能版本
第三位数字:Bug修复版本
版本号提升规则:只要前面的版本号增长了,则后面的版本号归零
多人协作的问题
当代码需要提交给多人协作开发时候,所引用的第三方包自然也需要提供,但放置的文件夹 node_modules 经过长期的下载和积累可能过于庞大,不方便团队成员之间共享项目源代码

此时就要在项目的根目录中创建并使用 package.json 记录项目中安装的哪些包,从而方便剔除 node_modules 目录之后,在团队成员之间共享项目的源代码
使用 npm 包管理工具提供的快捷命令 npm init -y 可在执行命令所处的目录中快速创建 package.json 配置文件
当使用下方命名生成 package.json 后,npm 会在之后 运行 npm install 命名安装包的时候自动把包的名称和版本号,记录在 package.json 中
注意:该命令只允许在英文目录下成功运行,项目文件夹名称不可使用中文,不能出现空格
// 如若 node_modules 文件夹内容过大,在项目开发上传时,需要将 node_modules 添加到 .gitignore 忽略文件中
上传包
npm init -y
包管理配置文件
npm 规定,在项目根目录中,必须提供一个叫做 package.json 的包管理配置文件,用来记录与项目有关的一些配置信息,例如:
1. 项目的名称、版本号、描述等
2. 项目中都用到了哪些包
3. 哪些包只在开发期间会用到
4. 哪些包在开发和部署时都需要用到
dependencies 节点
该节点记录了某些在开发和项目上线之后都需要用到的第三方包名与其对应的版本号
package.json 初始化结构
{
“name”: “项目名”,
“version”: “1.0.0”,
“description”: “”,
“main”: “index.js”,
“scripts”: {
“test”: “echo \”Error: no test specified\” && exit 1″
},
“keywords”: [],
“author”: “”,
“license”: “ISC”
}
package.json 文件中,有一个 dependencies 节点,用来记录该目录中使用 npm install 命名安装了哪些包
# 终端中使用 npm下载 moment 第三方模块
npm i moment
生成 dependencies 节点,记录安装的 moment 第三方模块
{
“name”: “mod_word”,
“version”: “1.0.0”,
“description”: “”,
“main”: “index.js”,
“scripts”: {
“test”: “echo \”Error: no test specified\” && exit 1″
},
“keywords”: [],
“author”: “”,
“license”: “ISC”,
“dependencies“: {
“moment“: “^2.29.3”
}
}
devDependencies 节点
该节点记录仅在开发阶段会用到,在项目上线之后不会用到的第三方包名与其对应的版本号
* 仅部分第三方包 需要进行此项操作 *
* 无需手动修改配置操作,仅需要在下载第三方包的指令时的 npm i 包名 后加上 -D *
# 安装指定包,并记录到 devDependencies 节点中
npm i 完整包名 -D
# 上述为简写方式 等同于下方完整写法
npm install 完整包名 –save-dev
一次性安装所有包
项目中存在 package.json 文件时,该文件通常作为 该项目中所有用到的第三方包的记录,如若没有 package.json 在项目初期就需要创建 该文件,方便记录和交接其他团队下载
npm init
当我们拿到一个剔除 node_modules 的项目之后,需要先把所有的第三方包下载到项目中,才能将项目运行起来,否则会报类似如下错误:
# 由于项目运行依赖于 xxx 第三方包,如果没有提前安装好这个包,就会报错
Error: Cannot find module “xxx”
要想一次性完整的恢复项目中第三方所有依赖包,可以运行如下命令:
# 执行 npm install 命令时,npm 包管理工具会先读取 package.json 中的 dependencies 节点
# 读取到记录中所有依赖包的名称和版本号后,npm 包管理工具会将这些包一次性下载到项目中
npm install
可以在终端指定目录路径下运行 npm uninstall 命名,来卸载指定的包,如若使用该命令,也会将目录中 package.json 下的 dependencies 记录的对应包移除
npm uninstall 完整的包名
在使用 npm 下载第三方模块时,默认以国外服务器节点 https://registry.npmjs.org/ 服务器进行下载,网络数据的传输要经过漫长的海底光缆,因此下载速度会很慢。
为了解决下载过慢的问题,应采用不同地域的下载镜像节点会提高下载的效率:
// 镜像(Mirroring)是一种文件的存储形式,一个磁盘的数据在另一个磁盘上存在一个完全相同的副本即为镜像
# 查看或检查当前的下载镜像源
npm config get registry
# 将下载的镜像源切换成淘宝镜像
npm config set registry=https://registry.npm.taobao.org/
对此,为了更方便的切换下载第三方包的镜像源,我们可以安装 nrm 小工具,我们可将它设置为全局包,用于快速切换下载源
# 通过 npm 包管理器,将 nrm 安装为全局可用工具
npm i nrm -g
# 查看所有可用的镜像源
nrm ls
# 将下载的镜像源切换为 taobao 镜像
nrm use taobao
效果如下

使用 npm 包管理下载的包,供分为两大类,分别是:
项目包
全局包
项目包
那些被安装到项目的 node_modules 目录中的包,都是项目包。项目包又分为两类,分别是:
开发依赖包 (被记录到 devDependencies 节点的包,只在开发期间会用到)
核心依赖包 (被记录到 dependencies 节点的包,在开发期间和项目上线都会用到)
全局包
在执行 npm install 命令时,如果提供了 -g 参数,则会把包安装为全局包。当然,只有工具性质的包 或者 包的文档中建议设为全局 的情况下才有全局安装的必要性,因为它们提供了好用的终端命令。
全局包默认会安装到 C:\User\用户目录\AppData\Roaming\npm\node_modules 目录下
# 全局安装指定的包
npm i 包名 -g
# 卸载全局安装的包
npm uninstall 包名 -g
# 查看全局包路径
npm root -g
规范包结构
在我们需要对自己的包进行上传的时候,需要了解第三方包的规范组成结构,它的组成结构必须符合以下三点要求:

1. 包必须以单独的目录而存在
2. 包的顶级目录下必须包含 package.json 包管理配置文件
3. package.json 中必须包含 name,version,main 这三个属性,分别代表 包的名字、版本号、包的入口
初始化包的基本结构
【实现时间格式化、HTML标签实体转义】
创建 smm-tools 文件夹,作为包的根目录,并在该文件夹中,新建如下三个文件:
文件名 | 说明 |
---|---|
package.json | 包的配置文件 |
index.js | 包的入口文件 |
README.md | 包的说明文档 |
初始化 package.json
{
# 包名
“name”: “smm-tools”,
#版本号
“version”: “1.0.0”,
# 包入口文件 (引入项目时 会自动检索并调用 main 参数下对应的 js文件)
“main”: “index.js”,
# 简短描述信息
“description”: “提供了格式化时间,HTMLEscape的功能”,
# 关键字
“keywords”: [
“smmcat”,
“dataFormat”,
“escape”
],
# 开源许可协议
“license”: “ISC”
}
暴露模块&模块拆分
制作包的时候,为了方便后期维护,建议将各个不同的功能进行拆分,放置在对应的不同js文件中,再集合在 package.json 中 main 参数所指定的 js 入口文件
dateFormat.js
// 定义格式化时间函数
function dateFromat(dateStr) {
…
}
// 补零
function padZero(n) {
return n > 9 ? n : ‘0’ + n;
}
// 暴露模块
module.exports = {
dateFromat
}
htmlEscape.js
// 实现 html标签的转义
function htmlEscape(htmlStr) {
return htmlStr.replace(/<|>|”|&/g, (match => {
switch (match) {
… }
}
));
}
function htmlUnEscape(str) {
return str.replace(/<|>|"|&/g, (match) => {
switch (match) {
…
}
});
}
// 暴露模块
module.exports = {
htmlEscape,
htmlUnEscape
}
index.js 引入各自模块
// 引入拆分的模块
const date = require(‘./src/dateFormat’);
const escape = require(‘./src/htmlEscape’);
// 采用 ES6 展开运算符方式 给暴露模块 添加对象方法
module.exports = {
…date,
…escape
}
撰写 README.md 说明文件
该文件内容 在项目上传成功后会显示在包的展示页面,文件内可按自己喜欢的方式介绍或演示包的使用、安装、开源协议、注意事项等。(语法可查看 md说明)
## 安装
“`
npm install smmcat-tools
“`
## 导入
“`
jsconst smmcat = require(‘smmcat-tools’);
“`
## 开源协议
ISC
检查文件
至此,项目文件配置完成,即将等待上传

注册 npm账号
终端绑定 npm账号
npm 注册完成后,可以在终端中执行 npm login 命令,依次输入用户名、密码、邮箱后,即可登录成功
npm login
将本地目录下的包上传至 npm
*注意: npm 的下载节点必须为官网 https://registry.npmjs.org/ 节点*
切换节点至官方节点
# 将下载的镜像源切换成官方节点
npm config set registry=https://registry.npmjs.org/
# 或采用 nrm 方法
nrm use npm
上传包
将终端切换到包的根目录上,运行 npm publish 命令,即可发布到 npm 上 (包名不能重复)
npm publish

在 npm 删除已发布的包
*注意:npm unpublish 命令只能删除72小时以内的包 超过则不能再删除了*
* 因此,发布包时候需要慎重,尽量不要往 npm 上发布没有意义的包 *
运行 npm unpublish 包名 –force 命令,即可从 npm 删除已发布的包
npm unpublish 包名 –force
优先从缓存中加载
不论是内置模块、自定义模块、第三方模块都会在首次被 require() 调用后会被缓存,意味着 模块在接下来的 调用过程不会执行多次
require(‘moment’); // 仅首次会执行
require(‘moment’);
require(‘moment’);
require(‘moment’);
内置模块优先级最高
内置模块是由 Node.js 官方提供的模块,因此加载优先级最高。若 node_modules 目录下由与内置模块相同的包时(如 fs),require(‘fs’); 会选择 Node.js 官方内置模块的 fs
自定义模块必须以路径开头
使用 require(); 加载自定义模块时,即使是在相同目录,也必须指定以 ./ 开头,因此,加载自定义模块时如果没有指定 ./ 或 ../ 这样的 路径标识符 时,则 node 会把它当作 内置模块 或 第三方模块 进行加载
Node 会尝试补全自定义模块 扩展名
在使用 require(); 导入自定义模块时,如果省略了文件扩展名,则 Node.js 会按顺序分别尝试加载模块文件:
1. 按照 完整引用名 进行加载 ↲
2. 补全 .js 扩展名进行加载 ↲
3. 补全 .json 扩展名进行加载 ↲
4. 补全 .node 扩展名进行加载 ↲
5. 加载失败 终端报错

第三方模块加载时 会依次向上级查找文件
如果传递给 require(); 的模块的标识符不是一个内置模块,也没有以 ‘./’ 或 ‘../’ 开头,则 Node.js 会从当前模块的父级目录开始,尝试从 /node_modules 文件夹内加载对应标识符的第三方模块
这种查找方式,会依次向上级遍历文件夹,寻找是否存在 node_modules 目录,直到顶级目录(如 C:/),如果还不存在,提示报错

模块目录中若无指定入口 则默认选择 index.js
当把目录作为模块标识符,传递给 require() 进行加载时,有三种加载方式:
1. 在被加载的目录下查找一个叫做 package.json 文件,并寻找 main 属性,作为 require() 加载的入口
2. 如果目录中无 package.json 文件,或 main 入口不存在或无法解析,则 Node.js 会试图加载目录下的 index.js 文件
3. 如以上两步都失败了,则 Node.js 会在终端打印错误消息,报告模块的缺失 Error:Cannot find module ‘xxx’

简介
Express 是基于 Node.js 平台的 快速、开放、极简的 web开发框架,通俗理解:Express 的作用 和 Node.js 内置的 http 模块类似,是专门用来创建 Web服务器的
Express的本质 就是一个 npm 上的第三方包,提供了快速创建 Web 服务器的便捷方法
中文官网:http://www.expressjs.com.cn
用途
Express 是 基于内置 http 模块进一步封装开发出来的,能极大提高开发效率。使用它,我们可以方便、快捷的开发 Web服务器 和 API接口服务器
web 网站服务器: 专门对外提供 Web 网页资源的服务器
API 接口服务器: 专门对外提供 API 接口的服务器
安装
在项目所处的目录中,运行如下终端命令,即可将 express 安装到项目中使用
npm i express@4.17.1
基础设施
// 导入 express
const express = require(‘express’);
// 创建 web服务器
const app = express();
// 启动 web服务器
app.listen(81, () => {
console.log(‘服务器已运行在 http://127.0.0.1:81’);
});
监听 GET POST 请求
通过 app.get() 和 app.post() 方法,可以监听客户端的 GET 和 POST 请求
参数1:客户端请求 url地址
参数2: 请求对应的处理函数 (req 请求对象 , res 响应对象)
// 监听 get 请求
app.get(‘请求URL’, function (req, res) {/*处理函数*/ });
// 监听 post 请求
app.post(‘请求URL’, function (res, req) {/*处理函数*/ });
获取 URL 中携带的查询参数
通过 req.query 对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数
// 监听 get 请求
app.get(‘/’, function (req, res) {
// 通过 req.query 可以获取到客户端发送过来的查询参数
// 默认情况下 req.query 是一个空对象
// 例如 http:127.0.0.1:81?name=”zs” 返回的 req.query 的内容就为 {name:”zs”}
let reqData = req.query;
// 向客户端发送 reqData
res.send(reqData);
});
获取 URL 中的动态参数
通过 req.params 对象,可以访问到 URL 中,通过 : 匹配到的动态参数
// 监听 get 请求
app.get(‘/user/:id’, (req, res) => {
// 通过 req.params 可以访问到 URL 中 通过 : 匹配到的动态参数
// 默认情况下 req.params 是一个空对象 里面存着 : 动态匹配到的值
// 例如 http:127.0.0.1:81/user/1 返回的 req.params 的内容就为 {id:”1″}
let reqParams = req.params;
// 向客户端发送 reqParams res.send(reqParams);
});
获取 URL 中多个动态参数
以此类推,用 /: 分割,得到多个对象成员
app.get(‘/:id/:user’,(req,res)=>{
// 获得两个对象成员 例如 http://127.0.0.1:81/1/zs 得到 {id:’1′,user:’zs’}
res.send(req.params);
})
托管单个静态资源
使用 express.static() 可以非常方便的创建一个静态资源服务器,
例如 app.use(express.static(‘./public’)); ,它就可直接将同级目录下的 public文件夹的内容资源 对外开放访问。
const express =require(‘express’);
const app=express();
/*
express 提供了一个非常好用的函数
express.static() 可以非常方便的创建一个静态资源服务器,
例如,通过如下代码就可以将 pyblic目录下的图片、css文件、javaScript文件对外开放访问
app.use(express.static(‘public’));
注意:存放静态文件的目录不会出现在URL中
*/
app.use(express.static(‘./clock’));
app.listen(81,()=>{
console.log(‘服务器运行在 http://127.0.0.1:81’);
});
托管多个静态资源
如果要托管多个静态资源,请多次调用 express.static() 函数,
访问静态资源文件时,express.static() 函数会根据目录的添加的顺序查找所需的文件
// 当客户端发起资源请求时 会首先遍历 clock 目录
app.use(express.static(‘./clock’));
// 若 clock 目录不存在 则会尝试查找 flie 目录
app.use(express.static(‘./flie’));
挂载路径前缀
默认设置静态服务器,是直接在URL 的 “/” 下访问,例如:http://127.0.0.1/,因此 “/” 就作为了开放的 flie目录
app.use(express.static(‘./flie’));
如果希望在托管的静态资源访问路径之前,将该静态资源挂载在其他路径前缀,则可以使用如下的方式
// 客户端访问 http://127.0.0.1:81/flie/ 可访问该静态目录
app.use( ‘/flie’ , express.static(‘./flie’));
安装
使用 nodemon 工具包 可方便调试代码,当代码保存时使用该包提供的 nodemon 文件路径 指令将会加载新代码并自动重启服务器
npm i -g nodemon
使用
终端使用 nodemon app.js 代替 node app.js 加载js文件,当监听到项目变动时会自动重启项目。极大方便开发和调试

路由的概念
广义来说,路由就是 映射关系,例如水果和水果标签价格,电梯按钮与对应楼层关系

Express 路由的概念
在 express 中,路由指的是客户端请求与服务器处理函数之间的映射关系,
express 的路由分三部分组成,分别是 请求的类型、请求的 URL地址、处理函数
// 请求的类型 请求的URL地址 处理函数
app.get( ‘/’ , function (req,res) {res.send( ‘hello!’ ); } );
路由的匹配过程
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数
在匹配时,会按照路由的顺序进行匹配,如果 请求类型 和 请求的URL 同时匹配成功,则 Express 会将这次请求,转交给对应 function函数 进行处理

路由匹配的特性如上,因此路由匹配应该注意:
1. 按照定义的先后顺序进行匹配
2. 请求类型和请求的URL同时匹配成功,才会调用对应的处理函数
// 挂载路由 为了后期维护 不建议将来以这种方式操作路由
app.get(‘./‘, (req, res) => { res.send(‘Get请求’) });
app.post(‘./‘, (req, res) => { res.send(‘Post请求’) });
模块化路由
为了方便对路由进行模块化管理,Express 不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块
1. 创建路由模块对应的 js 文件
2. 调用 express.Router() 函数创建路由对象
3. 向路由对象上挂载具体的路由
4. 使用 module.exports 向外共享路由对象
5. 使用 app.use() 函数注册路由模块 ( app.use() 函数的作用是用来注册全局中间件 )
路由模块
// 导入 express
const express = require(‘express’);
// 创建路由对象
const 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;
注册路由模块
依据上方路由模块代码,使用 require() 引入并使用 app.use() 注册到 express 中
// 导入路由模块
const userRouter=require(‘./router/user.js’);
// 使用 app.use() 注册路由模块
app.use(userRouter);
使用 app.use() 中参数1可设置 访问前缀,实现为对应的路由挂载统一的访问前缀
// 设置前最后 该路由模块中的所有路由都需要添加 /api/ 前缀才可访问
app.use(‘/api’,userRouter);
中间件的概念
中间件 (Middleware),特指业务流程的中间处理环节

Express 中间件的调用流程
客户端发起请求 → 全局或局部中间件组 处理 → 对应路由
当一个请求到达 Express的服务器之后,可以连续调用多个中间件,从而对这次请求进行 预处理

Express 的中间件,本质上就是一个 function处理函数,与路由函数不同的是,它的行参不仅有 req,res 还多了有 next
// 中间件处理函数
app.get(‘/’, function (req, res, next) {
next();
})
Express 的 next() 作用
next() 函数是实现 多个中间件连续调用 的关键,它表示把流转关系转交给下一个中间件或者路由
通过 next() 将上一个 中间件函数 处理后的数据移交给下一个 中间件函数 或 最终响应给客户端处理后的内容

简单的中间件函数
const mw = function (req, res, next) {
// 把流转关系 转交给下一个中间件或路由
next();
}
Express 的 中间件作用
类似 客户端发给 对应路由前 过滤器效果
可以连续使用 app.use() 定义多个中间件,多个中间件之间 共享同一份 req 和 res,基于这样的特性,我们可以在上游的中间件中,统一为 req 或 res 对象添加自定义属性 或 方法,供下游的中间件或路由进行使用
客户端请求到达服务器,会按照 中间件 定义的先后顺序 依次进行调用 (代码的从上到下解析)
中间件实现了为服务器的响应或接收 统一 执行某项操作的效果,避免了重复定义代码
// 上游中间件
app.use(function(req,res,next){
// 赋值 req 的 a 成员自定义属性
req.a=’233′;
next();
})
app.use(function(req,res,next){
// 打印 上游为 req 定义的成员 a
console.log(req.a);
next();
})
app.get(‘ / ‘, function(req , res ){
res.send(req.a); // 服务器返回 233
})
Express 的 全局中间件
将 中间件处理函数 提交给 app.use() 中,该 中间件 就为 全局生效的中间件
客户端发起的所有请求,都会经过 全局生效中间件 的 处理或过滤,最后提交给 对应路由
全局中间件
// 定义 中间件函数
const mw = function (req, res, next) {
// 打印 携带的查询参数,请求类型
console.log(req.query,req.method);
// 把流转关系 转交给下一个中间件或路由
next();
}
// 全局生效 mw 中间件函数
app.use(mw);
上方代码实现了 当客户端发起每一次请求,服务器都会打印 客户端的请求查询参数 和 请求类型
全局中间件 简化形式
// 全局生效 mw 中间件函数
app.use(function (req, res, next) {
// 打印 携带的查询参数,请求类型
console.log(req.query, req.method);
// 把流转关系 转交给下一个中间件或路由
next();
});
Express 的 局部中间件
不使用 app.use() 定义的中间件,叫做局部中间件,示例如下:
定义单个路由
// 局部中间件
const only = function (req, res, next) {
console.log(‘这是局部中间件’);
next();
}
// URL user 下调用 局部中间件
onlyapp.get(‘/user’,only,function(req,res){
res.send(‘User page’);
})
定义多个中间件
可以在路由中,通过如下两种等价的方式,使用多个局部中间件,
注意:第一个参数永远是 URL地址,最后一个参数永远是请求处理函数
// 以下两种写法是 完全等价 的
app.get(‘/’, mw1, mw2, (req, res) => {
res.send(‘Home Page’);
})
// [a,b] 方式赋值为 展开运算符
app.get(‘/’, [mw1, mw2], (req, res) => {
res.send(‘Home Page’);
})
中间件的使用事项
使用中间件时,考虑常规隐患问题,需要注意以下事项:
1. 一定要在 路由之前 注册中间件
2. 客户端发送过来的请求,可以连续调用多个 中间件进行处理
3. 执行完成中间件的业务代码之后,不要忘记调用 next() 函数
4. 为了 防止代码逻辑混乱,调用 next() 函数后不要再写额外代码
5. 连续调用多个中间件时,多个中间件之间,共享 req 和 res 对象
中间件的分类
为了方便理解和记忆中间件的使用,Express官方把 常见的中间件用法,分为5大类,分别是:
1. 应用级别的中间件 ( 例如:app.use() 或 app.get() 绑定到 app 实例的中间件 )
2. 路由级别的中间件 ( express.Router() 实例上的中间件,叫做路由级别中间件 )
3. 错误级别的中间件 ( 捕获整个项目中发生的错误,从而防止项目异常崩溃 )
4. Express 内置的中间件 ( express.static 快速托管静态资源 、express.json 解析 JSON 格式的请求体数据 、express.urlencoded 解析 URL-encoded 格式的请求体数据 )
5. 第三方的中间件
错误级别中间件示例
用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃,它的 function函数中,必须有 4 个参数,形参顺序从前到后,分别是 (err,req,res,next)
注意:需要将 错误级别中间件 注册在所有路由之后,以便正常捕获所有异常
// 捕获错误
app.use(function (err, req, res, next) {
console.log(‘发生了错误:’ + err.message);
// 返回错误信息
res.send(‘Error:’ + err.message);
})
Express 内置中间件示例
Express 内置了一些实用的中间件( 如之前说的 express.static 创建静态资源服务器 ),其中 express.json() 与 express.urlencoded() 是用来解析请求体内容,这两个中间件方法都有兼容性问题 ( >4.16.0 )
// 配置解析 application/json 格式数据的内置中间件
app.use(express.json());
// 配置解析 application/x-www-form-urlencoded 格式数据的内置中间件
// extended : falese 对URL-encoded的数据的解析采用querystring库
app.use(express.urlencoded({ extended: false }))
// 配置完成后,就可以解析 JSON格式 与 x-ww-form-urlencoded 的 请求体内容
// 若不配置 此中间件,body 参数默认为 undefined
app.post(‘/’ , function ( req , res) {
res,send( req.body );
})


Express 自定义中间件
自定义中间件,顾名思义,就是自己写的中间件。依赖 app.use() 提供的参数实现需要的效果
// 导入 Node 内置方法
quertstringlet qs = require(‘node:querystring’);
// 创建解析 URL-encoded 格式的请求体 中间件函数
const myResolver = function (req, res, next) {
// 临时存储
let str = ”;
// 监听 传输请求
req.on(‘data’, (chunk) => {
// 请求体的传输并不是一次性完成的
// 因此 data 传输事件会多次响应
str += chunk;
})
// 监听 传输完成事件
req.on(‘end’, () => {
// 将得到的请求体内容 解析成 对象
const body = qs.parse(str);
// 赋值 body 给 req 的 body成员
req.body = body;
// 传给下一个中间件或路由
next();
})
}
// 暴露模块
module.exports = myResolver
跨域问题

浏览器存在安全策略缘故,若写的接口不做响应头处理直接调用会被CORS拒绝,因此不支持跨域请求
解决接口跨域问题的方案主要有两种:
CORS (主流的解决方案 推荐使用)
JSONP (有缺陷的解决方法 只支持 GET)
CORS
CORS (Cross-Origin Resource Sharing 跨域资源共享) ,由一系列 HTTP响应头 组成,这些响应头决定浏览器是否阻止前端 JS 代码跨域获取资源
浏览器的同源安全策略默认会阻止网页“跨域”获取资源,但如果接口服务器配置了 CORS 相关的 HTTP响应头,就可以解决浏览器端的跨域访问限制

Express 针对跨域问题,有专门针对解决跨域的第三方模块,引用该模块即可解决跨域问题
npm i cors
调用 cors中间件时,注意放置的位置,需要在路由之前引用 cors
// 导入 express 模块
const express = require(‘express’);
// 创建服务器
const app = express();
// cors 可以解决跨域问题
const cors = require(‘cors‘);
// 配置 中间件 cors
app.use(cors());
注意事项
cors 主要在服务器端进行配置,客户端浏览器无需任何额外的配置,即可请求开启了cors的接口。
cors 也存在兼容性问题,只有支持 XMLhttpRequest Level2 的浏览器才可以正常访问开启了 cors 的服务器端口
CORS 响应头部 Access-Control-Allow-Origin
响应头部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:
Access-Control-Allow-Origin: <origin> | *
其中,origin 参数的值指定了允许访问该资源的外域 URL,如下方只允许来自 https://smmcat.cn 的跨域请求
res.setHeader(‘Access-Control-Allow-Origin‘, ‘http:smmcat.cn’);
若不需要设置跨域限制,则无需设置 Access-Control-Allow-Origin 字段 或直接写 * 声明
res.setHeader(‘Access-Control-Allow-Origin‘, ‘*’);
CORS 响应头部 Access-Control-Allow-Headers
默认情况下,CORS 仅支持客户端向服务器发送如下的 9 个请求头:
Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type (值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)
如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败
// 允许客户端额外向服务器发送 Content-Type 请求头和 X-Custom-Header 请求头
// 注意 多个请求头之间使用英文的逗号进行分割
res.setHeader(‘Access-Control-Allow-Headers‘ , ‘Content-Type , X-Custom-Header’);
CORS 响应头部 Access-Control-Allow-Methods
默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求,如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端 通过 Access-Control-Allow-Methods 来指定实际请求所允许使用的 HTTP方法
// 只允许 POST、GET、DELETE、HEAD 请求方法
res.setHeader(‘Access-Control-Allow-Methods‘ , ‘POST,GET,DELETE,HEAD’);
// 允许所有的 HTTP 请求方法
res.setHeader(‘Access-Control-Allow-Methods‘ , ‘*’);
简单请求
同时满足以下两大条件的请求,就是属于简单请求:
1. 请求方式:GET、POST、HEAD 三者之一
2. HTTP 头部信息不超过以下几个字段 : 无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width、Content-Type (值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)

简单请求:客户端与服务器在每次交互时,它们之间仅会发起一次请求
预检请求
只要符合以下任何一个条件的请求,都需要进行预检请求:
1. 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
2. 请求头中 包含自定义头部字段
3. 向服务器发送了 application/json 格式的数据

预检请求:在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这次的 OPTION 请求称为 “预检请求”。 服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据
实际操作方式
JSONP 不属于 Ajax 请求,因为它没有使用 XMLHttpRequest 对象,它也仅只支持 GET 请求,实现该接口步骤为:
1. 获取客户端发送过来的回调函数的名字
2. 得到要通过 JSONP 形式发送给客户端的数据
3. 根据前两步得到的数据,拼接出一个函数调用的字符串
4. 把上一步拼接得到的字符串 响应给客户端的 <script> 标签进行解析
注意:如果项目中已经配置了 CORS 跨域资源共享,为防止冲突,必须在配置 CORS 中间件之前声明 JSONP 的接口
客户端发送 JSONP 请求
// 注意使用 jquery 发起 jsonp 请求时,需要修改 dataType 内容为 jsonp
// 可引用 在线jquery包 进行测试
$(‘#btn_jsonp’).on(‘click’, function () {
$.ajax({
method: ‘GET’,
url: ‘http://127.0.0.1:81/api/jsonp’,
dataType: ‘jsonp’,
success: function (res) {
console.log(res);
}
})
})
服务器响应 JSONP 请求
// 必须在配置 CORS 中间件之前声明 JSONP 的接口
// jsonp 需要在 cors 前声明
app.get(‘/api/jsonp’, (req, res) => {
// 获得客户端发送的函数名称
const funName = req.query.callback;
// 定义对象数据
const data = { name: ‘smm’, age: 24 };
// 拼接模板字符串 将 对象数据使用JSON.stringify() 转字符串
const scriptStr = ${funName}(${JSON.stringify(data)})
// 返回 模板字符串
res.send(scriptStr);
});
* 首先 本地需安装 MySQL数据库 *
关于 MySQL相关知识,可查阅另一个文档
Node.js 要与 MySQL建立连接,需要使用 npm 先下载 mySQL模块
npm i mysql
建立与 mysql 数据库的连接
// 导入 mysql 模块
const mysql = require(‘mysql‘);
// 建立与 mysql 数据库的连接
const db = mysql.createPool({
host: ‘127.0.0.1’, // 数据库IP地址
user: ‘root’, // 登录数据库账号
port: ‘3306’, // 数据库端口
password: ‘smmmax’, // 登录数据库密码
database: ‘my_db_01’ // 指定需要操作的数据库
});
验证是否连接成功
// 检测 mysql 模块运行 (SELECT 1 无任何作用,仅用于检测)
db.query(‘SELECT 1‘, (err, result) => {
if (err) return console.log(err.message);
// 打印查询结果
// 提示[ RowDataPacket { ‘1’: 1 } ] 数据库连接成功
console.log(result);
})
可使用 db.query() 在 参数1 中插入对应的 SQL 代码
1. 当使用 SELECT 查询指令时,返回的是数组,
2. 当使用的是 INSERT INTO 等操作指令时,返回的是反馈对象
查询数据
// 在 参数1 中插入 SQL 代码
db.query(‘SELECT * FROM users‘, (err, result) => {
// 若操作出错 打印错误
if (err) return console.log(err.message);
// 打印数据
console.log(result);})
插入数据
// SQL语句 插入内容 (其中英文 ? 表示占位符)
const sqlStr = ‘INSERT INTO users (username,password) VALUES (?,?)‘;
// 执行数据库指令 插入内容 (参数2中的数组数据依次为 ? 占位符填充内容)
db.query(sqlStr, [‘smm’, ‘1233’], (err, result) => {
// 若操作出错 打印错误
if (err) return console.log(err.message);
// 若返回的对象中的 affectedRows 值为 1
if (result.affectedRows === 1) {
// 打印 成功消息
console.log(‘插入数据成功’);
};
})
使用 SET ? 简化SQL语句 (性能上SET会高效一些)
// 简化插入过程
// SQL 语句后面 SET 参数可以携带 键值对
// 该 键值对 的 对象 需与 数据库中的 字段 相对应
const user2 = { username: ‘smm’, password: ‘2333’ }
const sqlStr2 = ‘INSERT INTO users SET ?‘;
db.query(sqlStr2, user2, (err, result) => {
if (err) return console.log(err.message);
if (result.affectedRows === 1) {
console.log(‘数据插入成功’);
}
})
更新数据
// SQL语句 更新内容 (其中英文 ? 表示占位符)
const sqlStr = ‘UPDATE user SET username=? , paddword=? WHERE id=?’;
// 执行数据库指令 更新内容 (参数2中的数组数据依次为 ? 占位符填充内容)
db.query(sqlStr, [‘smm’, ‘1233’,1], (err, result) => {
// 若操作出错 打印错误
if (err) return console.log(err.message);
// 若返回的对象中的 affectedRows 值为 1
if (result.affectedRows === 1) {
// 打印 成功消息
console.log(‘更新数据成功’);
};
})
SQlite3
是轻量级的嵌入式 关系型数据库,无需服务器即可运行,常用于本地存储、移动应用和小型项目
注意事项
- Windows 权限问题
在命令行中右键选择 “以管理员身份运行”,避免文件被锁定。 - 弃用警告(WARN deprecated)
这些警告来自 npm 内部依赖包的版本过旧,不影响 SQLite3 的核心功能,可暂时忽略。 - Node.js 版本兼容性
确保使用 Node.js LTS 版本(如 18.x 或 20.x),避免使用过新或过旧的版本
使用操作
安装
npm install sqlite3
# 若无 Visual Studio 编译环境,强制使用预编译二进制包
npm install sqlite3 --sqlite3_binary_host_mirror=https://registry.npmmirror.com/-/binary/sqlite3
测试
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database(':memory:'); // 内存数据库
db.serialize(() => {
db.run('CREATE TABLE test (id INT, name TEXT)');
db.run('INSERT INTO test VALUES (1, "Hello")');
db.all('SELECT * FROM test', (err, rows) => {
if (err) throw err;
console.log(rows); // 应输出 [ { id: 1, name: 'Hello' } ]
});
});
db.close();
二次封装成简易操作方式
由于 SQlite3
的指令接近 sql
指令,为此可进行二次封装:
使用二次封装包文件
import db from './db.js';
// | 类型 | 存储方式 | 自动处理 | 示例值 |
// |-----------|---------------------|---------------------------|---------------------|
// | JSON | TEXT(序列化字符串) | 自动序列化/反序列化 | `{ theme: "dark" }` |
// | STRING | TEXT | 直接存储 | `"U10001"` |
// | TEXT | TEXT | 直接存储 | `"Alice"` |
// | INTEGER | INTEGER | 直接存储 | `25` |
// | REAL | REAL | 直接存储 | `99.99` |
// | BOOLEAN | INTEGER (0/1) | 自动转换为 0/1 | `true` → `1` |
// | TIMESTAMP | INTEGER/TEXT | 支持 `CURRENT_TIMESTAMP` | `1716844800000` |
db.defineModel('users', {
userid: { type: 'INTEGER', primary: true, autoInc: true },
username: { type: 'STRING', unique: true },
password: 'STRING',
status: 'INTEGER',
isDisable: 'BOOLEAN',
weeksTime: 'INTEGER',
postTime: 'INTEGER',
qualityTime: 'INTEGER',
lastSubmit: { type: 'TIMESTAMP', default: 'CURRENT_TIMESTAMP' }
}, {
primary: 'userid'
});
db.defineModel('article', {
articleid: { type: 'INTEGER', primary: true, autoInc: true },
bibleIndex: 'INTEGER',
chapter: 'INTEGER',
total: 'INTEGER',
notes: 'STRING',
createTime: { type: 'TIMESTAMP', default: 'CURRENT_TIMESTAMP' },
commentList: { type: 'JSON', default: [] }
}, {
primary: 'articleid'
});
export default db;
服务端渲染的 Web开发模式
服务端渲染的概念:服务器发送给客户端的 HTML 页面,是在服务器通过字符串拼接,动态生成的,因此客户端不需要使用 Ajax 这样的技术额外的请求页面的数据
优点:
1. 前端耗时少,因为服务器端负责动态生成的 HTML 内容,浏览器只需要直接渲染页面即可,尤其是移动端,更省电
2. 有利于 SEO,因为服务器端响应的是完整的 HTML 页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO
缺点:
1. 占用服务器端资源,即服务器端完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力
2. 不利于前后端分离,开发效率低,使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于项目高效开发
前后端分离的 Web开发模式
前后端分离的概念,依赖于 Ajax 技术的广泛应用,简而言之,就是后端只负责提供 API接口,前端使用 Ajax 调用接口的开发模式
优点:
1. 开发体验好,前端专注于 UI 页面开发,后端专注于 Api 开发,且前端有更多的选择性
2. 用户体验好,Ajax 技术的广泛应用,极大的提高了用户体验,轻松实现页面的局部刷新
3. 减轻了服务器端的渲染压力,因为页面最终是在每个用户的浏览器中生成
缺点:
不利于 SEO,因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫无法爬取页面的有效信息。
(利用 Vue、React 等前端框架 SSR 技术可以解决该问题)
选择 Web开发模式
企业级网站,没有复杂的交互,推荐使用服务端渲染模式
后台管理项目,较多交互性需求,不需考虑SEO,推荐使用前后端分离开发模式
介绍
身份认证 (Authentication) 又称 “身份验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。使用 身份认证 是为了确认当前所声称为某种身份的用户,确实是所声称的用户。
服务器端 与 前后端分离 认证方案
对于服务端渲染和前后端分离这两种开发模式来说,分别有不同的身份认证方案:
1. 服务器端渲染推荐使用 Session 认证机制
2. 前后端分离推荐使用 JWT 认证机制
HTTP协议的无状态性
指的是客户端的每次 HTTP请求都是独立的,没有直接关系,服务器不会主动保留每次 HTTP 请求的状态

使用 Session 验证机制
将客户端的信息记录到服务器端,使用 Cookie 保留对应客户端信息。类似会员卡的机制
Cookie 是存储在用户浏览器中的一段不超过 4kb 的字符串,它由一个名称(name),一个值(value)和其他几个用于控制 Cookie 有效期、安全性、使用范围的 可选属性 组成。
不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器
Cookie 的几大特征:
1. 自动发送
2. 域名独立
3. 过期时限
4. 4kb限制
Cookie 在身份认证中的作用

客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中,
随后,当客户端浏览器每次请求服务器时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的方式发送给服务器,服务器即可验明客户端的身份
* Cookie 不具有安全性,很容易被浏览器提供的读写 Cookie 的 Api 进行伪造 *
提高身份验证的安全性

服务端生成的 Cookie,它对应着开辟空间中用户的详细信息,当发起交互连接时,与客户端所存服务器的 Session ID 的 Cookie 的进行匹配。
该验证机制既保护了客户端的隐私安全,又完成了基本的验证,这就是 Session验证机制

在 Express 项目中,只需要安装 express-session 中间件,即可在项目中使用 session 认证
通过 req.session 对象可访问和使用 session对象,从而使用或操作存储用户的关键信息
引入 express-session 模块
npm i express-session
配置 session
// 导入 Session 中间件
var session = require(‘express-session‘);
// 引用和配置 Session 中间件
app.use(session({
secret: ‘smmKey’, // 用于加密 可任意字符
resave: false, // 固定写法
saveUninitialized: true, // 固定写法
}));
创建静态服务器
// 创建静态服务器 session 依赖静态服务器关联交互 数据
app.use(express.static(‘./admin’));
关联 用户登录信息
// 解析 POST 提交过来的表单数据
app.use(express.urlencoded({ extended: false }))
// 登录并存储用户登录信息
app.post(‘/add_login’, (req, res) => {
// 存储用户的登录信息 和 登录状态
req.session.user = req.body;
req.session.islogin = true;
res.send({ status: 0, msg: ‘登录成功’ })
})
清除 用户登录信息
app.post(‘/logout’, (req, res) => {
// 清空 对应访问者存储的 session信息
req.session.destroy();
res.send(‘退出成功’);
})
Session 局限性
Session 认证机制需要配合 Cookie 才能实现,由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后续接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证
JWT ( JSON web Token ) 是目前最流行的跨域认证解决方案

JWT 验证
与 Session 不同的是,JWT验证方式是不对每个客户端请求的数据开设空间进行存储,而是采用对客户端传过来的 Token 值进行加密解密的方式。从而获取各个用户的信息对象。
用户的信息通过 Token 字符串的形式保存在客户端浏览器中,服务器通过还原 Token 字符串的形式来认证用户的身份。
JWT 组成部分
JWT 通常由三部分组成,分别是 Header 头部、Payload 有效荷载、Signature 签名,
三者之间使用英文 “.” 分割。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTYyOTQ5NTcsImV4cCI6MTY1NjI5NDk5N30.qR3CK1ZeFpYo8HWHbEv1yKvP8jMLqEgKA11x-jS0sI0
Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串
Header 和 Signature 是安全性相关的部分,只是为了保证 Token 的安全性

Header.Payload.Signature
JWT 的使用方式
客户端收到服务器返回的 JWT 之后,通常会将它存储在 localStorage 或 sessionStorage 中
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证,推荐的做法是把 JWT 放在 HTTP 请求头 的 Authorization 字段中
Authorization: Bearer <token>
下载 JWT相关包
运行如下命令,安装 JWT 相关包
npm install jsonwebtoken express-jwt@5.3.3
jsonwebtoken 用于生成 JWT 字符串
express-jwt 用于将 JWT 字符串解析还原成 JSON 对象 * 注意上方版本 *
导入 JWT包 至项目
const jwt = require(‘jsonwebtoken‘); // 用于生成 token
const expressJWT = require(‘express-jwt‘); // 用于解析 token
创建 Token 密匙字符串
const secreKey = ‘smmmax_boom’ // 用于加密和解密
创建解析 Token密匙 中间件
/*
expressJWT() 用于解析 token值,返回实际加密前的内容参数对象
{secret} 解密的密匙 (需要与加密密匙一致)
.unless({path}) 排除(不需要参与解密的路由 url) 配置成功 express-jwt 中间件后; 会自动解析传入的 token值 并挂载到 req.user 属性中
*/
app.use(expressJWT({secret:secreKey}).unless({path:[/^\/api\//]}))
生成 JWT 字符串 (token)
app.post(‘/api/login’,function( req,res )=>{
…
/*
jwt.sign() 用于生成 token密钥
参数1:用户的信息对象
参数2:加密的密匙
参数3:配置对象,可以配置当前 token 的有效期
*/
const tokenStr = jwt.sign({ username: req.body.username }, secreKey, { expiresIn: ’30s’ })
…
将 Token值 内容发送给客户端
res.send({
status: 200,
message: ‘登录成功!’,
token: tokenStr // 发送给客户端 token字符串
})
})
客户端发起请求 携带 Token值
传入数据

获取用户传入的 Token值 返回 解密后的内容
// 这是另一个有权限的 API 接口
app.get(‘/admin/getinfo’, function (req, res) {
res.send({
status: 200,
message: ‘获取用户信息成功!’,
data: req.user // 要发送给客户端的用户信息
})
})
向服务器解析token的路由发送 token值

服务器解析客户端请求头的 Authorization 的 Token 值
// 解析 Token 如果全局使用了 expressJWT 时,可不用在此次调用 expressJWT 中间件
app.get('/decodeToken', expressJWT({ secret: secretKey }), (req, res) => {
// req.user 包含了解密后的 token 内容
// 但在 express-jwt 后续版本中,req.user 被换成了 req.auth
res.send({status:200,data:req.user,message:'获取用户信息成功!'});
});
解决客户端传入的 token值 有误 导致服务器崩溃 问题
// 错误中间件需写在路由后面
app.use((err,req,res,next)=>{
if(err.name===’UnauthorizedError‘){
// 错误是由 token 解析失败 或者 token失效 导致
return res.send({status:401,message:’无效的 token’})
}
// 其他原因导致的错误
res.send({status:500,message:’未知错误’})
})
bcryptjs
是一个用于 密码哈希(Hashing) 和 盐值加密(Salting) 的 Node.js 库,专门设计用于安全存储用户密码。
它的核心作用是 防止密码泄露,即使数据库被入侵,攻击者也无法轻易还原原始密码
可调整哈希的计算复杂度(如 saltRounds: 10
),使暴力破解更加困难
基本使用
安装
npm install bcryptjs
对密码进行加密
import bcrypt from "bcryptjs";
const password = "123456"
const keys = bcrypt.hashSync(password, 10) // 输出类似:$2a$10$N9qo8uy...
验证密码是否正确
const password = "123456"
const keys = bcrypt.hashSync(password, 10)
// 验证 哈希密码 和 明文密码 是否一致
const check = bcrypt.compareSync(password, keys) // true
该模块化规范是由 JavaScript社区多次尝试推出的模块化规范,并一同提出 AMD、CMD等模块化规范,AMD 与 CMD 适用于浏览器端的 JS模块化,而 CommonJS 适用于服务端的 JS模块化
Node.js 实现模块化 遵循了 CommonJS 的模块化规范,其中:
– 导入其它模块使用 require() 方法
– 模块对外共享成员使用 module.exports 对象
// 模块化文件
const pi = 3.1415;
// 暴露模块
module.exports = {
pi
}
// 导入模块文件
const { pi } = require(‘./pi.js’);
console.log(pi); // 3.14
为了解决各种模块化的差异性、局限性。降低学习难度和开发成本,官方正式推出了 ES6 模块化规范
ES6 模块化规范中定义:
– 每个 js 文件都是一个独立的模块
– 导入其他模块成员使用 import 关键字
– 向外共享模块成员使用 export 关键字
如若需要体验学习 ES6 的模块化语法,可以按照如下两个步骤进行配置
– 确保安装了 v14.15.1 或更高版本的 node.js
– 在 package.json 的根节点中添加 “type”:”module” 节点
默认导出 && 默认导入
使用 export default
let num1 = 100;
let fnn = function(){
console.log(‘fnn is runing’);
}
// 默认导出 只能定义一个 否则报错
export default {
num1,
fnn,
}
// 默认导入 接收时在名称合法的情况下可任意 命名
import smm from ‘./mod/index.js’;
console.log( smm.num1 ); // 100
smm.fnn(); // fnn is runing
按需导入 && 按需导出
使用 export let 变量 = 值/方法
// 按需导出 文件
export let s1 = 100;
export let s2 = 200;
export let fnn = function () {
console.log(‘fnn is runing’);
}
export default {
a: 20,
}
// 按需导入 文件 名称必须一致 ,如若标识符存在冲突可用 as 关键词替换
import { s1, s2 , fnn as fnn1 } from ‘./mod/index.js’;
console.log(s1); // 100
console.log(s2); // 200
fnn1(); // fnn is runing
* 按需导入可以与默认导入一起使用,注意引用格式 *
// 按需导入 可以 与 默认导入 配合使用,默认导入的成员由 smm 对象中调用
import smm,{ s1, s2, fnn as fnn1 } from ‘./mod/index.js’;
直接导入
如果只想单纯的执行某个模块中的代码,并不需要得到模块中向外共享的成员,可以直接使用 import
// 包中的内容 只有一个 for循环
for(let i=0;i<=10;i++){
console.log(i);
}
// 引用该包后 该包内的预留的代码逻辑直接被执行
import ‘./mod/index.js’; // 1 2 3 4 5 6 7 8 9 10
多层回调函数的相互嵌套,为了保障异步进程按顺序进行需要不断进行嵌套操作,就形成了回调地狱,比如:
setTimeout(() => {
console.log(‘锄禾日当午’);
setTimeout(() => {
console.log(‘汗滴禾下土’);
setTimeout(() => {
console.log(‘谁知盘中餐’);
setTimeout(() => {
console.log(‘粒粒皆辛苦’);
}, 2000);
}, 2000);
}, 2000);},
2000);
回调地狱的缺点
代码的耦合性太强,牵一发而动全身,难以维护
大量冗余代码相互嵌套,代码的可读性变差
旧方法 实现异步处理顺序执行
使用 Node.js 自带的 fs包,可以异步的方式读取文件,但是如若需要按顺序执行异步队列,则需要采用不断嵌套的方式,实现等待上一条执行完毕后执行下一条的效果
// 尝试读取 01.txt 文件
fs.readFile(‘./01.txt’, ‘utf-8’, (err1, r1) => {
// 提取失败 返回错误提示
if (err1) return console.log(err1.message);
console.log(r1);
// 尝试读取 02.txt 文件
fs.readFile(‘./02.txt’, ‘utf-8’, (err2, r2) => {
// 提取失败 返回错误提示
if (err2) return console.log(err2.message);
console.log(r2);
// 尝试读取 03.txt 文件
fs.readFile(‘./03.txt’, ‘utf-8’, (err3, r3) => {
// 提取失败 返回错误提示
if (err3) return console.log(err3.message);
console.log(r3);
})
})
})

下载npm包
由于nodejs官方提供的fs模块仅支持以回调函数的方式读取文件, 不支持promise的回调方式,因此,需要先运行如下命令,安装then-fs这个第三方包,从而支持我们基于promise的方式读取文件内容:
npm then-fs
调用then-fs提供的readFile()方法,可以异步地读取文件的内容,它的返回值是Promise的实例对象,因此可以调用.then()方法为每一个Promise异步操作指定成功和失败之后的回调函数:
import thenFs from ‘then-fs’;
/*
实现顺序排序
*/
thenFs.readFile(‘./01.txt’, ‘utf-8’).then((r1) => {
console.log(r1);
return thenFs.readFile(‘./02.txt’, ‘utf-8’);
})
// 返回的结果作为一个 thenFs对象 因此用 .then接收
.then((r2) => {
console.log(r2);
return thenFs.readFile(‘./03.txt’, ‘utf-8’);
})
// 返回的结果作为一个 thenFs对象 因此用 .then接收
.then((r3) => {
console.log(r3);
})
// 捕获异常 使用 .catch接收
.catch(
(err) => {
console.log(err);
}
);

出现缘由
为了解决回调地狱的问题,ES6 (ECMAScript 2015) 中新增了 Promise 的概念
1. Promise 是一个构造函数,可以 使用 new 创建 Promise 实例 const p = new Promise();
2. Promise.prototype 上包含了一个 .then() 方法,实例化对象通过原型链的方式可以 访问到该方法 p.then();
3. .then() 方法用来预先指定成功和失败的回调函数 p.then( 成功的回调函数[必选] , 失败的回调函数[可选] );
Promise 特性
【1】 Promise
的状态一经改变就不能再改变
【2】 then
和catch
都会返回一个新的Promise
【3】 catch
不管被连接到哪里,都能捕获上层未捕捉过的错误
【4】在Promise
中,返回任意一个非 promise
的值都会被包裹成 promise
对象,例如return 2
会被包装为return Promise.resolve(2)
【5】Promise
的 .then
或者 .catch
可以被调用多次, 但如果Promise
内部的状态一经改变,并且有了一个值,那么后续每次调用.then
或者.catch
的时候都会直接拿到该值
【6】 .then
或者 .catch
中 return
一个 error
对象并不会抛出错误,所以不会被后续的 .catch
捕获
【7】.then
或 .catch
返回的值不能是 promise 本身,否则会造成死循环
【8】.then
或者 .catch
的参数期望是函数,传入非函数则会发生值透传
【9】 .then
方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,再某些时候你可以认为catch
是.then
第二个参数的简便写法
【10】.finally
方法也是返回一个Promise
,他在Promise
结束的时候,无论结果为resolved
还是rejected
,都会执行里面的回调函数
更多特性的内容,可以查阅该 博客说明
new Promise() 创建构造函数
Promise() 可传入两个参数,一个为resolve,一个为reject;这两个方法 ”理论“ 上作为异步函数 成功和失败的返回值
当调用 Promise 实例时,.then 方法挂载 resolve 的返回值,.catch 方法挂载 reject 返回值
// getStr 返回一个 Promise对象
function getStr() {
const p = new Promise((resolve, reject) => {
// 模拟网络请求处理
setTimeout(() => {
// 成功后使用 resolve 返回数据
resolve(‘这是异步后返回的结果’);
}, 3000);
});
return p;
}
// then 会等待 resolve方法的返回值 作为行参
getStr().then((data) => {
console.log(data);
});
Promise.all() 等待队列机制
Promise.all() 方法会发起并行的 Promise 异步操作,等all()中实参的 List 中所有的异步操作全部结束后才会执行下一步的 .then 操作(等待机制)
数组中 Promise 实例的顺序,就是最终结果的顺序
import thenFs from “then-fs”;
// 定义数组 存放异步操作
const promiseArr = [
thenFs.redFile(‘./01.txt’, ‘utf8’),
thenFs.redFile(‘./02.txt’, ‘utf8’),
thenFs.redFile(‘./03.txt’, ‘utf8’),
];
// 将 Promise 的数组 作为 Promise.all() 的参数
Promise.all(promiseArr).then((result) => {
console.log(result); // [‘xxx’,’xxx’,’xxx’];
})
// 捕获 错误信息
.catch((err) => {
console.log(err.message);
});
Promise.race() 赛跑机制
Promise.race() 方法会发起并行的 Promise 异步操作,只要任何一个异步操作完成,就会立即执行下一步的 .then 操作,只返回首个完成的异步操作 (赛跑机制)
.race() 常用于等待超时的截断操作,避免异步请求操作长时间等待
import thenFs from “then-fs”;
// 定义数组 存放异步操作
const promiseArr = [
thenFs.readFile(‘./01.txt’, ‘utf8’),
thenFs.readFile(‘./02.txt’, ‘utf8’),
thenFs.readFile(‘./03.txt’, ‘utf8’),
];
// 将 Promise 的数组 作为 Promise.race() 的参数
Promise.race(promiseArr).then(result=>{
console.log(result); // ‘xxx’
})
基于 Promise 封装读文件方法
下方代码封装了Node.js 的 fs 模块。传入要读取文件路径 fpath 参数后,使用 Promise 方法将 fs 读取成功的数据使用 .then 返回
import fs from ‘fs’;
// 封装 getFile 函数 使用 then 返回读取结果
function getFile(fpath) {
// 实例化 Promise 对象
return new Promise((resolve, reject) => {
// fs 读取文件操作
fs.readFile(fpath, ‘utf-8’, (err, dataStr) => {
// 失败结果使用 reject 返回
if (err) return reject(err.message);
// 将读取的成功结果使用 resolve 返回
resolve(dataStr);
})
});
}
// 实现 .then 返回读取文件结果
getFile(‘./01.txt’).then((data) => {
console.log(data);
})
出现原因
async / await 是 ES8(ECMAScript 2017) 引入的新语法,用来简化 Promise 异步操作,在 async / await 出现之前,开发者智能通过链式 .then() 方式处理 异步操作。
.then() 虽然解决了回调地狱,但是 .then 链式会容易造成 代码冗余、阅读性差、不易理解
如果返回值是一个 Promise 实例对象,可以用 await 去修饰,接收后的值不再是 Promise 实例,而是一个得到的真正的值。如果方法内部用到了 await ,需要在方法前面用 asyuc 修饰
// 定义 异步方法
async function getFile() {
// 等待 Promise构造函数 返回值
const r1 = await thenFs.readFile(‘./01.txt’, ‘utf8’);
console.log(r1);
// 等待 Promise构造函数 返回值
const r2 = await thenFs.readFile(‘./02.txt’, ‘utf8’);
console.log(r2);
}
// 执行异步方法
getFile();
如果在 function 中使用了 await,则 function 必须被 async 修饰
在 async 方法中,第一个 await 之前的代码会同步执行, await 之后的代码会异步执行
JavaScript是单线程的语言
JavaScript是一门单线程执行的编程语言,也就是说,同一时间只能做一件事

同步任务和异步任务
为了防止某个耗时任务导致程序假死问题,JavaScript把待执行的任务分为了两类:
同步任务 (Synchronous)
1. 又叫做非耗时任务,指的是在主线程上排队执行的那些任务
2. 只有前一个任务完成,才能执行后一个任务
异步任务 (asynchronous)
1. 又叫做耗时任务,异步任务由 JavaScript 委托给宿主环境进行执行
2. 当异步任务被执行完成之后,会通知 JavaScript 主线程执行异步任务的回调函数
执行过程

JavaScript 主线程
1. 同步任务由 JavaScript 主线程次序执行,异步委派到宿主环境
2. 已完成的异步任务对应的回调函数,会加入到任务队列中等待执行
3. JavaScript 主线程的执行栈被清空后,会读取任务队列中的回调函数,次序执行
4. JavaScript 主线程不断重复上面的4个步骤
宿主环境 线程
1. 异步任务会挂载到 宿主环境 中去执行,不占用 同步任务 的主线程
2. 宿主环境中的 异步任务先后完成时,会先后的将异步任务返回的结果 挂载在 回调任务队列
3. 等待同步任务执行完成后,依据先后的顺序执行 异步回调任务队列的 结果
import fs from “fs”;
function whatGetNum(num) {
const p = new Promise((resolve, resject) => {
setTimeout(() => {
resolve(num);
}, 3000);
})
return p;}
console.log(‘A’);
// 立即执行 异步任务
(async (num) => {
const p = await whatGetNum(num);
console.log(p);
})(233)
/*
即使定时器为 0 它并不会立即执行
它还是会被挂载到宿主环境中执行
只不过它由于是立即完成的,会立即加入到回调任务队列中
等待主线程清空后,被主线程调用执行
*/
setTimeout(() => {
console.log(‘D’);
}, 0);
console.log(‘B’);
console.log(‘C’);
/*
其中: A 和 B C 都是属于同步任务,会根据代码的先后顺序依次被执行
而 D 和 233 属于异步任务,他们的回调函数会被加入到任务队列中,等待主线程空闲时再执行
结果为 A B C D 233
*/
事件循环
JavaScript 主线程从 “任务队列” 中读取异步任务的回调函数,放到执行栈中依次执行,这个过程是循环不断的。
所以整个这种运行机制又成为 EventLoop (事件循环)
简介
JavaScript 把异步任务做了进一步划分,异步任务又分为两类,分别是:
宏任务 (macrotask)
1. 异步 Ajax 请求
2. setTimeout、setInterval
3. 异步文件读写操作
4. 其他宏任务
微任务 (microtask)
1. Promise.then、.catch 和 .finally
2. process.nextTick
3. 其他微任务

执行顺序

异步队列中,每宏任务执行完成后,都会检查是否存在待执行的微任务,当有微任务时,会执行完所有微任务后再执行下一个宏任务,否则直续执行下一个宏任务
案例演示
1. 先执行所有代码中的 同步任务
2. 再执行代码中的 微任务
3. 再执行下一个宏任务
// 异步 宏任务
setTimeout(() => {
console.log(‘1’);
}, 0);
new Promise((resolve) => {
// 同步 在实例化 Promise 时立即执行
console.log(‘2’);
// 异步 宏任务
resolve();
}).then(function () {
// 异步 微任务
console.log(‘3’);
});
// 同步
console.log(‘4’);
/*
输出结果 2 4 3 1
*/

本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容
webpack是一个模块包,它可以打包并整合、压缩、剔除无用代码的技术,减少文件数量,缩减代码体积,提高打开网站速度

Yarn 是一个软件包管理器,还可以作为项目管理工具。无论你是小型项目还是大型单体仓库(monorepos),
无论是业余爱好者还是企业用户,Yarn 都能满足你的需求。支持打包 ES6 与 CommonJS 引入方式的模块
使用 webpack 的前置准备工作需要:
1. 初始化文件夹包环境,得到 package.json
2. 下载 webpack 等模块包
3. 在package.json 自定义命令,为打包做准备
安装 yarn
# 全局安装 yarn
npm install -g yarn
# 验证 安装
yarn -version
使用 yarn 初始化包环境
# 初始化 包环境
yarn init
/*
* 后续配置信息(一般默认一直回车即可):
* question name (trybase): 包名
* question version (1.0.0): 版本号
* question description: 描述
* question entry point (index.js): 包 主入口
* question repository url: Git 的地址
* question author: 作者
* question license (MIT): 开源许可
* question private: 包 是否私有
*/
使用 yarn 安装 webpack
# 下载 wbepack 与 webpack 相关工具 并记录到 开发环境
yarn add webpack webpack-cli -D
在 package.json 修改指令
我们可以在包项目的 package.json 文件的 scripts 属性上 修改并设置 自定义命令 (下方 webpack 指令被修改成 build)
{
“name”: “base”,
“version”: “1.0.0”,
“main”: “index.js”,
“author”: “smmcat”,
“license”: “MIT”,
“devDependencies”: {
“webpack”: “^5.74.0”,
“webpack-cli”: “^4.10.0”
},
“scripts”: {
“build“: “webpack“
}
}
示例 合并模块代码
webpack 可以将多个模块化的代码以最压缩、代码简化的方式拼合在一起。实现方法需要创建基本文件结构:
默认入口文件位置:./src/index.js
默认出口文件位置:./dist/main.js
根目录
㇗src
···㇗ … (模块文件)
···㇗index.js (打包 入口文件)
㇗dist
···㇗main.js (打包后生成的文件)
㇗node_modules
···㇗ …
㇗yarn.lock
㇗webpack.config.js (webpack 配置文件)
㇗ package.json (yarn包 配置文件)
㇗ …

1. 在包根目录中创建 src 文件夹,并在该文件夹下创建 index.js 与 对应的模块 ./add/add.js 文件
// [add.js] 暴露模块
export const addFn = (a, b) => a + b;
// [index.js] webpack 打包入口
import { addFn } from “./add/add.js”;
console.log(addFn(2,5));
完成编写后,在包目录中执行终端命令 yarn build,会创建 dist 文件夹并对两个 js代码进行合并打包,生成 main.js 文件,结果如下:
* 警告: mode 选项没有进行配置时,会默认使用 生产环境 进行 极致压缩 *
// [main.js] 打包后的代码 (去除注释、重构代码逻辑)
(()=>{“use strict”;console.log(7)})();
重新打包
src 下项目代码发生改变后,可再次打包,对于新的代码内容,打包后默认会重新将新的内容压缩和合并在 dist 文件下的 main.js 文件
# 再次打包
yarn build
在项目根目录创建 webpack.config.js 文件,写入指定内容可自定义 webpack 的默认配置。
>>> 更多内容 可参阅文档
# [webpack.config.js]
const path = require(‘path’);
/*
* 该配置文件 可直接修改 webpack包 默认值
* 如下方 修改entry 入口文件路径 和 output.path 出口文件路径 并 path.filename 命名名称
*/
module.exports={
entry:’./src/main.js’, // 入口 配置文件
output:{ // 出口配置 路径/名称
path:path.resolve(__dirname,’dist’), // 路径
filename:’bundle.js’, // 文件名
}}
上方配置生效后,webpack 的默认入口文件被修改成 src 文件夹下的 main.js ,而执行打包后的合并文件放置在了 dist 文件夹下的 bundls.js 中
根目录
㇗src
···㇗ … (模块文件)
···㇗main.js (打包 入口文件)
㇗dist
···㇗bundle.js (打包后生成的文件)
㇗node_modules
···㇗ …
㇗yarn.lock
㇗webpack.config.js (webpack 配置文件)
㇗ package.json (yarn包 配置文件)
㇗ …

具体流程如图所示:

执行 webpack 命令,找到配置文件,入口和依赖关系,打包代码输出到指定位置
* webpack 默认只能处理 js 文件,依靠各种插件的支持,可以扩展更多支持的格式 *
html-webpack-plugin 插件
说明

HtmlWebpackPlugin
简化了 HTML 文件的创建,以便为webpack 包提供服务。这对于那些文件名中包含哈希值,并且哈希值会随着每次编译而改变的 webpack 包特别有用。可以让该插件为生成一个 HTML 文件,使用 lodash 模板提供模板,或者使用自己的 loader >>>> 相关文档
该插件将生成一个 HTML5 文件, 在 body 中使用 script
标签引入所有 webpack 生成的 bundle。 只需添加该插件到 webpack 配置中
yarn 安装
yarn add html-webpack-plugin -D
配置 webpack.config.js 并 引用插件
// 导入 HtmlWebpackPlugin 插件
const HtmlWebpackPlugin = require(‘html-webpack-plugin‘);
module.exports = {
// … 省略其他 自定义配置内容
// 插件配置 参数为数组对象
plugins: [
// 引用 实例化 插件
new HtmlWebpackPlugin({
// 模板 html文件 对应的引入 js 的 html 文件的路径
template: ‘./pubilc/index.html’,
}),
],
}
配置生效后,输入 yarn build 后,会在合并 js 文件的同时,并给对应 template 参数路径下的 html 文件 添加引入 出口 js 的路径,并压缩 html 文件
css-loader style-loader 插件

css-loader 让 webpack 能处理 css类型的文件 相关文档
style-loader 把 css 插入到 DOM 中 相关文档
yarn 安装
yarn add css-loader style-loader -D
配置 webpack.config.js 并 引用插件
module.exports = {
// … 省略其他 自定义配置内容
// 加载器配置
module:{
// 规则
rules:[
// 每个下标存入 具体的规则对象
{
// 匹配 .css结尾的文件 /i忽略大小写
test:/\.css$/i,
/*
* 使用下方 两个 loader 处理 .css 文件
* 处理流程 从右向左 因此不能颠倒顺序
* css-loader: webpack 解析 css文件 打包进 js 中
* style-loader: css代码 插入到 DOM 中 (style标签)
*/
use:[‘style-loader’,’css-loader’],
}
],
}
}
向工具 标记导入的 css文件
使用 import ‘./xxx.css‘ 导入的 css 样式会被 css-loader 解析
# [index.js] 代码中 引用 css文件
import ‘./index.css‘
配置生效后,输入 yarn build 后,会在合并 js 文件的同时,并对所有 引入的 .css 文件的 css样式 进行解析,并打包进 出口js中,并插入到 DOM 中
less less-loader 插件
less-loader:识别代码中的 less文件 相关文档
less:将less 编译成 css
并需要安装 css-loader、style-loader
yarn 安装
yarn add less less-loader -D
配置 webpack.config.js 并 引用插件
module.exports = {
// … 省略其他 自定义配置内容
// 加载器配置
module:{
// 规则
rules:[
// 每个下标存入 具体的规则对象
{
// 匹配 .css结尾的文件 /i忽略大小写
test:/\.less$/,
/*
* 使用下方 三个 loader 处理 .less 文件
* 处理流程 从右向左 因此不能颠倒顺序
* less-loader: 将 less 代码转换成 css 代码
* css-loader: webpack 解析 css文件 打包进 js 中
* style-loader: css代码 插入到 DOM 中 (style标签)
*/
use:[‘style-loader’,’css-loader’,’less-loader’],
}
],
}
}
向工具 标记导入的 less文件
使用 import ‘./xxx.less‘ 导入的 less 样式会被 less-loader 解析
# [index.js] 代码中 引用 less文件
import ‘./index.less‘
babel 插件

babel是针对ES6语法对低版本浏览器的语法进行兼容降级处理,为要进行低版本浏览器适配,如IE浏览器。
babel用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中

安装
# npm 安装 (若为webpack 4.x版本 只能安装 babel-loader@8.x babel@7.x)
npm install -D babel-loader @babel/core @babel/preset-env
# yarn 安装 (若为webpack 4.x版本 只能安装 babel-loader@8.x babel@7.x)
yarn add babel-loader @babel/core @babel/preset-env -D
babel: 一个 JavaScript 编译器,把高版本js语法降级处理输出兼容的低版本语法
babel-loader:可以让 webpack 转译打包的js代码
配置
配置 webpack.config.js 并 引用插件
module.exports = {
// ... 省略其他 自定义配置内容
// 加载器配置
module:{
// 规则
rules:[
{
test: /\.m?js$/,
/*
* exclude 内容为 不去匹配该文件目录路径下的文件
* 一般在考虑兼容后 安装的第三方包也必定是考虑兼容问题的
*/
exclude: /(node_modules|bower_components)/,
use: {
// 使用 babel-loader 的 loader 处理js文件
loader: 'babel-loader',
// 加载器的选项
options: {
/*
* 预设: @babel/preset-env 降级规则
* 按照 填入的降级规则 进行降级
*/
presets: ['@babel/preset-env'],
}
}
}
],
}
}
配置成功后,打包时候将会自动将 js代码中 相关的 ES6 语法的代码进行降级处理
CSS文件独立导出
webpack 4.0以后,官方推荐使用mini-css-extract-plugin插件来打包css文件(从css文件中提取css代码到单独的文件中,对css代码进行代码压缩等)
使用 mini-css-extract-plugin 插件去对导入的 .css 文件进行处理,当执行打包时,会将入口文件中有 引入关联 的CSS文件 一并导出到 出口文件夹设置的对应的位置
安装
# npm 安装方式
npm i -D mini-css-extract-plugin
# yarn 安装方式
yarn add mini-css-extract-plugin -D
配置
配置 webpack.config.js 并 引用插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
// ... 省略其他 自定义配置内容
// 加载器配置
module: {
rules: [
// 指定多个配置规则
{
test: /\.css$/i,
use: [
MiniCssExtractPlugin.loader,
//1. css-loader按照common.js规范,将样式文件输出到js中
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
// 输出 同名css文件
filename: 'css/[name].css'
})
]
}
使用
// 以模块加载的方式 在入口文件 引入 CSS文件
import './css/index.css';
说明
资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader,
若在 webpack 5 之前,通常使用:
raw-loader
将文件导入为字符串url-loader
将文件作为 data URI 内联到 bundle 中file-loader
将文件发送到输出目录
引用图片资源
src/index.js
// 载入图片文件
import imgObj from './assets/12+.jpg';
let theImg = document.createElement('img');
theImg.src = imgObj;
document.body.appendChild(theImg);
解析图片资源
在 webpack.config.js 文件内 使用 webpack 自带 asset 模块处理图片路径
如果图片的大小小于8kb,则会将图片转成 base64格式,打包进js,直接把图片文件 输出到 dist 下
module.exports = {
// ... 省略其他 自定义配置内容
// 加载器配置
module:{
// 规则
rules:[
// 图片文件处理配置 (仅适用于 webpack5 版本)
{
test:/\.(jpg|gif|png|jpeg)$/,
/*
* asset 是 webpack5 自带的静态资源处理配置
* 当使用 asset 作为处理模块时 webpack
* 会自动把目标当作静态处理资源进行打包
*/
type:'asset',
}
],
}
}
处理字体图标文件
为避免字体文件同名,在名字后面添加随机哈希值,方便打包后独立区分
配置 webpack.config.js 并 引用插件
module.exports = {
// ... 省略其他 自定义配置内容
// 加载器配置
module:{
// 规则
rules:[
{
// webpack5 默认内部不认识这些文件 所以当作静态资源直接输出即可
test: /\.(eot|svg|ttf|woff|woff2)$/,
// 仅复制文件 不转换
type: 'asset/resource',
// 生成文件名 - 定义规则
generator: {
/*
* 保存在 font/ 目录下
* [name] 使用原名
* [hash:6] 哈希值(随机值)
* [ext] 使用原后缀名
*/
filename: 'font/[name].[hash:6][ext]'
}
}
],
}
}
asset 处理模块
资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
1. asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
2. asset/inline 导出一个资源的 data URI (例如转成bsae64位图片)。之前通过使用 url-loader 实现。
3. asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
4. asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。
module.exports = {
// ... 省略其他 自定义配置内容
// 加载器配置
module:{
// 规则
rules:[
{
// 音频文件的放置
test: /\.(mp3|MP3|amr|wav|m4a)$/,
// 仅复制文件 不转换
type: 'asset/resource',
// 生成文件名 - 定义规则
generator: {
/*
* 保存在 'record/ 目录下
* [name] 使用原名
* [hash:6] 哈希值(随机值)
* [ext] 使用原后缀名
*/
filename: 'record/[name].[hash:6][ext]'
}
}
],
}
}
使用理由
使用 webpack 打包代码过程中,频繁改动代码意味着需要频繁打包才能看到最新效果
但是打包的时间过长,导致写代码过程中大多数时间都是等待打包完成。效率降低
因此。我们需要使用 webpack开发服务器 去帮助我们更高效的 开发代码。
处理机制
把代码运行在内存中,自动更新,实时返回给浏览器显示
安装
webpack-dev-server 可用于快速开发应用程序 文档说明
# yarn 安装 webpack-dev-server
yarn add webpack-dev-server -D
# npm 安装 webpack-dev-server
npm i -D webpack-dev-server
修改启动命令
在 根目录的 package.json 中配置
"scripts": {
"build": "webpack",
"serve": "webpack serve"
}
启动 服务器
打包项目中需要存在 html 文件,方便直接在浏览器看到更新变化: 安装 html-webpack-plugin 插件
yarn serve
配置项
如若有需要,可在 webpack.config.js 配置
const path = require('path');
module.exports = {
//...
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
compress: true,
port: 9000,
},
};