web前端之Node基础
初识Node.js与模块
◆ 能够知道什么是 Node.js
◆ 能够知道 Node.js 可以做什么
◆ 能够说出 Node.js 中的 JavaScript 的组成部分
◆ 能够使用 fs 模块读写操作文件
◆ 能够使用 path 模块处理路径
◆ 能够使用 http 模块写一个基本的 web 服务器
浏览器中的JavaScript
浏览器中的JavaScript的组成部分
- 浏览器中的JS
- ECMAScript(核心语法)ES3 --> ES4 --> ES5 --> ES6(2015) --> ES2016 -->ES2017 .....
- 变量、常量
- 数据类型
- 函数
- 流程控制(if、switch、for、while、for...in、continue、break)
- 运算符
- JS内置对象(Array、String、RegExp、Date、Math....)
- WebAPI (浏览器提供的API)
- DOM(文档对象模型,document)
- BOM(浏览器对象模型,location、history、....)
- XMLHttpRequest
- Canvas
- ...
- ECMAScript(核心语法)ES3 --> ES4 --> ES5 --> ES6(2015) --> ES2016 -->ES2017 .....
![image-
为什么JavaScript 可以在浏览器中被执行
![image-2
浏览器内核
- 浏览器内核 包括 CSS解析引擎,包括 JS解析引擎
- 目前,JS解析引擎基本上从内核中独立出来了
- 所以,平时所说的浏览器内核一般和CSS有关系
不同的浏览器使用不同的 JavaScript 解析引擎:
- Chrome 浏览器 => V8
- Firefox 浏览器=> OdinMonkey(奥丁猴)
- Safri 浏览器=> JSCore
- IE 浏览器=> Chakra(查克拉)
- etc...
其中,Chrome 浏览器的 V8 解析引擎性能最好!
浏览器中的 JavaScript 运行环境
![image-2
运行环境
指的是代码正常运行所需的必要条件。- V8 引擎负责解析和执行 JavaScript 代码。
- 内置 API 是由运行环境提供的特殊接口,只能在所属的运行环境中被调用。
Node.js简介
JavaScript 能否做后端开发
![image-2
Node.js 的出现,使得JavaScript有了做后端开发的能力。
Node.js可以做什么
Node.js 作为一个 JavaScript 的运行环境,仅仅提供了基础的功能和 API。然而,基于 Node.js 提供的这些基础功能,很多强大 的工具和框架如雨后春笋,层出不穷,所以学会了 Node.js ,可以让前端程序员胜任更多的工作和岗位!
- 基于 Express/Koa 框架(http://www.expressjs.com.cn/),可以快速构建 Web 应用
- 基于 Electron 框架(https://electronjs.org/),可以构建跨平台的桌面应用
- 基于 restify 框架(http://restify.com/),可以快速构建 API 接口项目
- 读写和操作数据库、创建实用的命令行工具辅助前端开发
- etc...
总之,Node.js 是大前端时代的“大宝剑”,有了 Node.js 这个超级 buff 的加持,前端程序员的行业竞争力会越来越强!
什么是 Node.js
Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.
Node.js
是
一个基于 Chrome V8 引擎的 JavaScript
运行环境
。
通俗的理解:Node.js 为 JavaScript 代码的正常运行,提供的必要的环境。
Node.js 的官网地址: https://nodejs.org/zh-cn/
Node.js中的JavaScript运行环境
![image-2
注意:
- 浏览器是 JavaScript 的
前端
运行环境。(浏览器是客户端安装的软件) - Node.js 是 JavaScript 的
后端
运行环境。(正常情况下,Nodejs要安装到服务器上) - Node.js 中无法调用 DOM 和 BOM 等 浏览器内置 API。
Node.js环境安装
下载安装
如果希望通过 Node.js 来运行 Javascript 代码,则必须在计算机上安装 Node.js 环境才行。
安装包可以从 Node.js 的官网首页直接下载,进入到 Node.js 的官网首页,点 击绿色的按钮,下载所需的版本后,双击直接安装即可。
进入官网(中文),可以看到如下两个版本:
![image-
- LTS 为长期稳定版,对于追求稳定性的企业级项目来说,推荐安装 LTS 版本。
- Current 为新特性尝鲜版,对于热衷于尝试新特性的用户来说,推荐安装 Current 版本的 Node.js。但是,Current 版本 中可能存在隐藏的 Bug 或安全性漏洞,因此不推荐在企业级项目中使用 Current 版本的 Node.js。
建议使用 长期支持版
查看已安装的Node.js的版本号
打开终端(黑窗口,或者蓝窗口),在终端输入命令 node –v
后,按下回车键,即可查看已安装的 Node.js 的版本号。
如果你能够看到版本号,说明你已经安装成功了。
在Node.js环境中运行JavaScript
终端窗口运行(了解)
此种方式,类似于浏览器调试工具的“Console”面板,只适合运行少量的测试代码,所以了解即可。
操作步骤:
- 打开任意终端,直接输入 node 命令并回车
- 执行你的JS代码,按回车表示执行
- 按两次“Ctrl+C”退出。
使用node命令执行JS文件(掌握)
此种方式,比较常用。可以运行写到 “xx.js
” 里面的JS代码,可以运行JS文件中的代码。
操作步骤:
- 打开终端
- 输入 “
node 要执行的js文件
”
vscode自带终端:
xxx.js
文件上,鼠标右键 --> 在终端中打开 --> 出现一个终端窗口 -->node xxx.js
注意终端的路径,注意在此路径中,是否能找到你的js文件。
常见问题
- 如果在vscode终端中,运行node命令报错,重启vscode。
- 执行node的路径一定要对(vscode中,文件上右键,在终端中打开,这样的路径肯定是对的)
- 执行文件的时候,需要保证
node xxx.js
这种格式 - node只能运行JS代码(也就是不要 node xxx.html)
- 注意,最好不要使用多个终端
终端命令
// 用 node命令 执行 js 文件 ( 文件的路径相对或者绝对路径都可以)
node ./index.js
使用 ↑ 键,可以快速定位到上一次执行的命令
使用 tab 键,能够快速补全路径
使用 esc 键,能够快速清空当前已输入的命令
输入 cls 命令,可以清空终端
系统存在问题解决
window7 的 powershell 问题: https://blog.csdn.net/appleyuchi/article/details/80155233
node 和 nodemon 问题: https://www.jianshu.com/p/321003445e13
vscode中无法使用node命令:用管理员权限打开 vscode
模块化
什么是模块化
模块化,就是把一个大的文件拆分成若干小文件,而且还能把小文件通过特定的语法组合到一起的实现过程。
比如手机、电脑....等等几乎所有,都是模块化的设计,拿电脑来说,可以把电脑拆分成显示器、键盘、硬盘、内存等一个一个的小模块,当然也能够组装到一起。
优点
模块化的优势:
- 更利于维护(比如电脑屏幕坏了,只换屏幕就可以了;比如想升级显卡,只换显卡就行了);
- 更好的复用性(比如有一块移动硬盘或U盘,大家都能用)
Node中,规定每个JS文件都是一个小模块。一个项目由许许多多的小模块(JS文件)组合而成。
Node中模块化的优势:
- 更利于维护(比如,项目需要对登录模块升级,则不会影响其他模块)
- 更好的复用性(比如有一个公共的函数,封装起来。其他所有JS文件都能使用这个函数)
了解几种模块化规范
- AMD
- CMD
- CommonJS(Node中的模块化,使用的是这种方案)
- ES6
Node使用的是CommonJS规范。
模块的分类
- 自定义模块
- NodeJS中,创建的JS文件都是自定义模块。(也就是处处皆模块)
- 内置模块(核心模块)
- 安装Node之后,自带了很多内置模块。我们可以直接加载使用他们。
- 第三方模块
- 其他人编写的模块,发布到 npm 网站 上,我们可以下载使用。
自定义模块
我们创建的每个JS文件都是一个自定义模块,并且具有模块作用域,也就是在一个模块中创建的变量、常量、函数等等一切,都只能在当前模块中使用。
- 共享(导出/暴露)内容给其他模块用,需要使用
module.exports
导出内容。module
是Node中的一个全局对象,对象包含当前模块的详细信息。module.exports
是模块的出口,通俗的说,就是导出内容用的,默认值是{}
- 比如,02-test.js 导出 age、name、fn 给其他模块用,可以
module.exports = {age, name, fn}
- 其他模块,如果需要使用上述模块导出的内容,可以使用
require()
加载let 结果 = require('模块路径')
- 比如,
let test = require('./02-test');
- 加载自定义模块,必须加路径,即使是
./
也必须加。但是可以省略后缀。
示例:
02-test.js -- 导出内容
let age = 30;
let name = 'laotang';
let height = '175cm';
let weight = '75kg';
let square = x => x * x;
// 导出age、name、fn给其他模块使用
module.exports = { age, name, square };
03-use.js -- 导入内容
let test = require('./02-test');
console.log(test); // { age: 30, name: 'laotang', square: Function... }
一个模块导出什么,另一个模块加载后,就会得到什么。
就比如,我给你三个苹果,你只能得到三个苹果,不可能得到其他的。
内置模块
内置模块是Node.js 平台自带的一套基本的 API(功能模块)。也叫做核心模块。
下面介绍几个内置模块。
注意,加载内置模块,不能写路径,这是和加载自定义模块不一样的。
path模块
path
是 Node 本身提供的 API,专门用来处理路径。- http://nodejs.cn/api/path.html
-
使用
-
加载模块
// 使用核心模块之前,首先加载核心模块 let path = require('path'); // 或者 const path = require('path');
-
调用path模块中的方法,来处理相应的问题,下面列举path模块中的几个方法
方法 作用 path.basename(path[, ext]) 返回 path 的最后一部分(文件名) path.dirname(path) 返回目录名 path.extname(path) 返回路径中文件的扩展名(包含.) path.format(pathObject) 将一个对象格式化为一个路径字符串 path.join([...paths]) 拼接路径 path.parse(path) 把路径字符串解析成对象的格式 path.resolve([...paths]) 基于当前工作目录拼接路径 const path = require('path'); // extname -- 获取文件后缀 console.log(path.extname('index.html')); // .html console.log(path.extname('index.coffee.md')); // .md // join -- 智能拼接路径 // ------------------- 智能拼接路径 ----------------------------- // console.log(path.join('a', 'b', 'c')); // a/b/c // console.log(path.join('a', 'b', 'c', 'index.css')); // a/b/c/index.css // a里面有b,b里面有../c,言外之意,c和b同级。 // console.log(path.join('a', 'b', '../c', 'index.js')); // a/c/index.js // __dirname 永远表示当前js文件的绝对路径 console.log(path.join(__dirname, 'css', 'demo.css')); // /Users/tangfengpo/Study/123/Node01/code/css/demo.css
-
fs模块
- fs,即 file system,文件系统,该模块可以实现对 文件、文件夹的操作
- http://nodejs.cn/api/fs.html
-
使用
-
加载模块
// 引入模块,引入模块的时候,可以使用var、let,但是建议使用const,因为我们不希望它改变 const fs = require('fs');
-
调用fs模块的方法,下面列举fs模块中的常用方法
API 作用 备注 fs.access(path, callback) 判断路径是否存在 fs.appendFile(file, data, callback) 向文件中追加内容 fs.copyFile(src, callback) 复制文件 fs.mkdir(path, callback) 创建目录 fs.readDir(path, callback) 读取目录列表 fs.rename(oldPath, newPath, callback) 重命名文件/目录 fs.rmdir(path, callback) 删除目录 只能删除空目录 fs.stat(path, callback) 获取文件/目录信息 fs.unlink(path, callback) 删除文件 fs.watch(filename[, options][, listener]) 监视文件/目录 fs.watchFile(filename[, options], listener) 监视文件 ..... 一大堆 // readFile -- 异步读取文件 fs.readFile('./test.json', (err, data) => { if (err) { console.log('读取文件出错'); } else { console.log(data); // 读取到的二进制数据 console.log(data.toString()); // 得到原始数据 } }); fs.readFile('./test.json', 'utf-8', (err, data) => { if (err) { console.log('读取文件出错'); } else { console.log(data); // 读取到的原始数据 } });
// writeFile -- 异步写入文件 fs.writeFile('./abc.html', 'hello world', (err) => { if (err) { console.log('写入文件失败'); } else { console.log('文件写入成功'); } });
-
querystring模块
查询字符串(id=1&name=zs&age=20)处理模块
- 处理查询字符串(请求参数)的模块
-
使用方法
-
加载模块
const querystring = require('querystring');
-
调用querystring模块中的方法
// parse -- 将查询字符串解析成JS对象 console.log(querystring.parse('id=1&name=zs&age=20')); // { id: '1', name: 'zs', age: '20' } // stringify -- 将JS对象转成查询字符串 console.log(querystring.stringify({ id: '1', name: 'zs', age: '20' })); // id=1&name=zs&age=20
-
内置模块 - http模块
http服务器处理模块,可以使用http模块
搭建服务器
- http是一个系统模块,让我们能够通过简单的流程创建一个Web服务器
入门示例
-
使用http模块搭建Web服务器
创建 Web 服务器的步骤
- 导入 http 核心模块
- 创建 server 对象(server 对象负责建立连接,接收数据)
- 注册 request 事件,当浏览器发送请求到服务器执行,设置处理请求的函数
- 监听端口(这个步骤也可以放到注册request事件之前)
// 1. 加载http模块const http = require('http');// 2. 创建服务对象,一般命名为serverconst server = http.createServer(); // create创建、server服务器// 3. 给server对象注册请求(request)事件,监听浏览器的请求。只要有浏览器的请求,就会触发该事件server.on('request', (req, res) => { // 设置响应状态码 res.statusCode = 200; // 设置响应头 res.setHeader('Content-Type', 'text/plain; charset=utf-8'); // 设置响应体 res.end('hello,欢迎访问服务器,这是服务器给你的回应');});// 4. 设置端口,开启服务server.listen(3000, () => { console.log('服务器启动了');});
- 当服务器接收到浏览器的请求后,如果没有做出响应,浏览器会等待
- 服务器的最终目的是要根据请求做出响应,肯定要调用 res.end() 方法。
req 和 res 参数
上述代码的格式基本固定。只有 请求事件 的处理函数需要说明一下。
当收到浏览器的请求后,会触发request事件,事件处理函数有两个形式参数 req 和 res。
// 代码片段server.on('request', function (req, res) { // 该函数就是处理请求响应的函数 // 形参res是响应response的简写})
- 形参 req
- 形参 req 是request的缩写,即请求。
- 通过 req 对象,可以获取到 请求相关信息。
- req.url 获取请求行中的路径
- req.method 获取请求行中的请求方法
- req.headers 获取请求头
- 形参 res
- 形参res是response的缩写,即响应
- 做出响应,需要使用 res 对象。
- statusCode 设置响应状态码,必须在end方法前调用。
- res.setHeader() 设置响应头,比如设置响应体的编码,必须在end方法前调用。
- res.end() 把响应报文(响应行、响应头、响应体)发送给浏览器,通俗的讲就是做出响应。
- end() 调用,表示做出响应
- end() 调用后,不能再设置响应状态码和响应头
- end() 的参数表示响应结果;只能填字符串。
浏览器在请求服务器的时候,默认会请求网站根目录下的
/favicon.ico
网站图标,先不要管它。
根据不同 url 地址处理不同请求
前面已经可以对浏览器的请求做出响应了,但是响应的内容总是一样的。能不能根据url的不同,做出合适的响应呢?当然可以,那么首先就需要知道浏览器请求的url是什么。
涉及到和请求相关的信息,都是通过请求响应处理函数的第一个参数完成的。
server.on('request', function (req, res) {
// 形参req 是 请求request的意思,所有和请求相关的信息,都在req对象中
})
体验注意
console.log('hello world');
// 使用绝对路径执行文件的黑窗口,尽量在文件所在的目录打开,打开方便
// node 文件路径 cd跨文件夹 ,盘符不加cd先写盘符冒号\分隔,文件有特殊符号就用引号包起来,不带会以参数传递文件报错,tab可以补齐文件路径
// ./和/当前目录,../上一级
// 最好只打开一个终端 注意文件或文件夹路径,右键在集成终端中打开 快捷键ctrl+`
// 被导入的文件会立即执行
// 导入的模块的前缀不可省略,后缀可以省略
// 模块分为:内置模块(node.js自带的核心模块) 自定义模块 第三方模块 其他人或公司组织开发的模块
// 读取文件是异步执行,放到最后执行 ,读取的文件不能省略后缀
// 错误优先 data表示读取成功的内容,err不为null,data为undefined 编码集一般不写图片音频视频不写
// 成功返回null,失败返回对象 err有两个属性name 和message
// 如果文件存在会覆盖不存在会创建
// 如果文件夹不存在就会报错
// 可以用appendFile()方法追加
// 写入失败 err是对象,成功是null
// fs.access()方法判断文件是否存在 可读 可写 fs.constants.F/R/W _OK,第三个参数是回调err 一个参数值
// dir与文件夹有关
// path.extname(url)//获取扩展名名称
// 获取扩展名
// console.log(path.extname(url));
// // 获取基础路径名称 第二个参数是移去
// console.log(path.basename(url));
// console.log(path.basename(url,'.htnl'));
// 定义一个相对路径,相对的是执行文件所在的位置,不是文件本身存在的位置
// 路径\或者/
// let url='F:\ithiema\黑马学习\就业班\数ajax大gitnode\06node\day01\1.txt'
// 相对路径相对得是执行文件所在的位置,除了在同级文件下获取文件路径用相对和绝对无区别,其他情况用绝对路径,__filename表示当前文件,__dirname变量获取当前模块文件所在目录的完整绝对路径,两者区别_filename始终是用绝对路径显示一个文件的位置,而__dirname则是该文件所在目录的绝对路径,join会选择系统支持的符号一般.\
// 端口号后一定会有/,表示网页根路径
// 找error,行号,尖尖
// querystring.stringify(对象) 转换为字符串而且自带编码
// querystring.parse(字符串) 转换为对象而且自带解码
// 协议:网络通讯过程中数据遵循的格式 https收费盈利 http免费 底层是tcp连接是三次握手,断开时是四次挥手
// 域名:域名和IP地址就是电脑在网络中的的唯一标识 127.0.0.1 localhost 本机
// 端口:确定电脑中的某个程序 0~65535 固定端口:1~1023 80/http 443/https
// 动态端口:1024~65535
// HTTP:超文本传输协议,是在互联网上应用最广泛的一种网络协议。是一个客户端和服务端请求和应答的标准(TCP),用于从WWW(超文本)服务器传输超文本到本地浏览器的传输协议。它可以使浏览器更加高效,使网络传输减少。
// HTTPS:是以安全为目标的HTTP通道,可以看做是HTTP的安全版,即HTTP+SSL层。HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL
// 计算机“端口” [1] 是英文port的义译,可以认为是计算机与外界通讯交流的出口。其中硬件领域的端口又称接口,如:USB端口、串行端口等。软件领域的端口一般指网络中面向连接服务和无连接服务的通信协议端口,是一种抽象的软件结构,包括一些数据结构和I/O(基本输入输出)缓冲区。
// 面向连接服务TCP协议和无连接服务UDP协议使用16bits端口号来表示和区别网络中的不同应用程序,网络层协议IP使用特定的协议号(TCP 6,UDP 17)来表示和区别传输层协议。
// 改变代码后需要重新启动服务 ;不可累加重启
// 导入
// const http= require('http')
// // 创建服务
// const server=http.createServer()
// // 监听请求事件
// server.on('request',(req,res)=>{
// console.log(req.method);
// console.log(req.url);
// //响应给客户端信息,不要有标签和汉字
// // res.end('I am Mr Jia ! ')
// res.statusCode=201//xhr.status 给浏览器用
// res.setHeader('Content-Type','text/html;charset=utf8')
// res.end(<h3>本次请求的方式是</h3>${req.url}
)//xhr.response.status
// })
// // 监听端口
// server.listen(8888,()=>{
// console.log('server is running at : http://127.0.0.1:8888');
// })
// 改变代码后需要重新启动服务 ;不可累加重启
// req 对象,可以获取到 请求相关信息。eq.url** 获取请求行中的路径
// - req.method 获取请求行中的请求方法
// - req.headers 获取请求头
// - 形参 req
// - 形参 req 是request的缩写,即请求。
// - 通过 req 对象,可以获取到 请求相关信息。
// - req.url 获取请求行中的路径
// - req.method 获取请求行中的请求方法
// - req.headers 获取请求头
// - 形参 res
// - 形参res是response的缩写,即响应
// - 做出响应,需要使用 res 对象。
// - statusCode 设置响应状态码,必须在end方法前调用。
// - res.setHeader() 设置响应头,比如设置响应体的编码,必须在end方法前调用。
// - res.end() 把响应报文(响应行、响应头、响应体)发送给浏览器,通俗的讲就是做出响应。
// - end() 调用,表示做出响应
// - end() 调用后,不能再设置响应状态码和响应头
// - end() 的参数表示响应结果;只能填字符串。二进制
// node中的res.end()和res.send()
// 1.如果服务器端没有数据要返回到客户端的话,就直接用res.end()。
// 2.如果服务器需要有数据返回到客户端的话,就需要用res.send().
// 区别:
// res.send([body])
// body的参数可以是Buffer对象,String,对象或Array,传入number是没反应的。
// res.end()将结束响应过程,其实这个也可以用来传数据,但是参数只限定字符串和Buffer,字符串要加上下面这句话,否则会中文乱码。
// 'content-type':'text/html'
案例-搭建静态服务器
MySQL数据库
数据库对于我们前端同学来说,就是一个了解。
什么是数据库
数据库 (database) 是用来组织、存储和管理数据的仓库。
当今世界是一个充满着数据的互联网世界,充斥着大量的数据。数据的来源有很多,比如出行记录、消费记录、浏览的网页、发送的消息等等。除了文本类型的数据,图像、音乐、声音都是数据。
为了方便管理互联网世界中的数据,就有了数据库管理系统的概念(简称:数据库)。用户可以对数据库中的数据进行新增、查询、更新、删除等操作。
- 增删改查
- 新增
- 删除
- 修改
- 查询
常见的数据库及分类
市面上的数据库有很多种,最常见的数据库有如下几个:
- MySQL 数据库(目前使用最广泛、流行度最高的的开源免费数据库;)
- Oracle 数据库(收费)
- SQL Server 数据库(收费)
- Mongodb 数据库(Community + Enterprise)
其中,MySQL、Oracle、SQL Server 属于传统型数据库(又叫做:关系型数据库 或 SQL 数据库),这三者的 设计理念相同,用法比较类似。
而 Mongodb 属于新型数据库(又叫做:非关系型数据库 或 NoSQL 数据库),它在一定程度上弥补了传统型 数据库的缺陷。
MySQL简介
MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,目前属于 Oracle 旗下产品。
我们常说数据库,其实只是一个泛指。那么数据库的结构是怎样的呢?
- 数据库服务器
- 数据库
- 数据表(真正存储数据的地方)
- 数据库
![im
真正存储数据的是数据表。数据表和我们见过的Excel表格结构基本相同。
数据表的结构和excel一模一样。
id(不允许重复) | name | age | sex | tel |
---|---|---|---|---|
1 | 王宇 | 23 | 男 | 13200008888 |
2 | 王宇 | 23 | 男 | 13300008888 |
3 | 裴志博 | 25 | 男 | 18866669999 |
4 | 李淑茵 | 32 | 女 | 13200008888 |
安装MySQL及Navicat
MySQL 服务器软件(phpStudy) ---- 存储数据,可以创建数据库、数据表
MySQL图形化管理工具(Navicat) --- 可以使用它管理(创建、增删改查等等)数据库
安装MySQL服务软件
安装phpStudy,或者wampserver。二选一。
安装过程,略
安装操作MySQL的图形化工具(Navicat)
图形化的管理工具,有很多种
- mysql-workbeach(英文版,没有中文版)
- Navicat
- phpmyadmin(需要php支持)
- 其他,基本都不跨平台
前面已经安装了MySQL软件。那么我们如何管理或者说使用它呢,对于我们来说,还需要安装一个管理MySQL的工具,我们选择就是 Navicat
。
Navicat是一个收费软件,我们可以免费试用 14 天。
MySQL服务和图形化工具的关系
![im
试用Navicat时,必须启动MySQL。(phpstudy中点击启动、wampserver打开后即启动了)
Navicat使用
必要条件
必须启动MySQL服务。
- 如果你使用phpstudy,打开phpstudy,启动MySQL。
- 如果你使用wampserver,打开wampserver软件,MySQL就启动了
连接到MySQL服务器
打开 Navicat软件,点击连接 --> MySQL。
填写如下参数:
- 连接名:随便填。
- 主机:localhost (不用改)
- 端口:3306 (不用改)
- 用户名:root (不用改)
- 密码:自己的密码是什么,就填什么。(phpstudy默认密码root、wampserver默认密码空)
填好连接参数,可以点左下角的 “测试连接”,如果成功了,点击右下角的“保存”即可。
至此,Navicat侧边栏就有一个连接了。
双击或者右键打开这个连接,就表示使用Navicat软件连接到MySQL数据库了,后面就可以管理数据库了。
创建数据库
-
在连接名称上,右键,选择 “新建数据库”
-
只需填数据库名,选择utf8编码,然后确定
创建数据表
对于前端同学来说,创建数据表只需了解即可。
比如创建一个学生信息表:
id(不允许重复) | name | age | sex | tel |
---|---|---|---|---|
1 | 王宇 | 23 | 男 | 13200008888 |
2 | 王宇 | 24 | 男 | 13300008888 |
3 | 裴志博 | 25 | 男 | 18866669999 |
... | ... | ... | ... | ... |
对于一张表,最重要的是表头的设计
对于数据库中的数据表,最重要的设计也是表头,只不过在数据库中
把表头叫做字段
下面是关于id的设计:
下面是完整的创建表:
名(表头) | 类型 | 长度 | 不是null | 键 | 其他 |
---|---|---|---|---|---|
id | int | √ | 🗝 | √ 自动递增 | |
username | varchar | 20 | √ | ||
age | int | ||||
sex | varchar | 1 |
-
id -- 自动递增 -- √
-
最后保存,填表名
student
导入导出数据表(重点)
前面介绍,前端同学可以不会创建数据表,但是必须会导入导出数据。
-
导出
- 在数据表名字上,比如
student
上,右键 --> 转储SQL文件 --> 结构和数据,选择保存位置保存即可。
- 在数据表名字上,比如
-
导入
- 在
数据库名
上面 --> 右键 --> 运行SQL文件 --> 选择SQL文件,运行即可完成导入。 - 导入注意事项,表名不能重复,如果重复会发生覆盖。
- 在
SQL语句(重点)
SQL(英文全称:Structured Query Language)是结构化查询语言,专门用来访问和处理数据库的编程语言。
SQL能做什么
-
从数据库中查询数据
-
向数据库中插入新的数据
-
更新数据库中的数据
-
从数据库删除数据
-
可以创建新数据库
-
可在数据库中创建新表
-
可在数据库中创建存储过程、视图
-
etc...
-
无所不能
查询数据
语法格式
- SQL语句,
不区分
大小写。
-- 基本的查询语法
SELECT 字段1,字段2,... FROM 表名
-- 不区分大小写
select 字段,字段,.... from 表名
-- 查询所有的字段
SELECT * FROM 表名
-- 带条件的查询
SELECT * FROM 表名 [WHERE 条件] [ORDER BY 排序字段[, 排序字段]] LIMIT [开始位置,]长度
.....
基本查询
语法:select 字段名1, 字段名2,.... from 表名
案例1: 查询所有学生的姓名和年龄
select username,age from student
案例2: 查询全部学生的全部信息
select * from student
带条件的查询
语法:select 字段 from 表名 where 条件
可以使用条件来筛选查询出的结果
-- 查询id小于10的学生
-- select * from student where 条件
-- select * from student where id<10
-- 查询id小于20的女学生
-- select * from student where id<20 and sex='女'
-- 查询年龄大于等于20小于等于30的学生
-- select * from student where age>=20 and age<=30
对查询结果排序
语法:select 字段 from 表名 order by 字段 规则 [,字段 规则 [,字段 规则 [......]]]
规则只有下面两种:
- 升序 asc (默认值)
- 降序 desc
可进行排序的字段通常是 整型 英文字符串型 日期型 (中文字符串也行,但一般不用)
-- 对查询结果进行排序
-- 查询所有的同学,并按年龄升序排序
-- select * from student order by age asc
-- select * from student order by age
-- 查询所有的同学,按年龄降序排序
-- select * from student order by age desc
-- 查询所有的同学,按年龄降序排序,如果年龄相同,按id降序排序
-- select * from student order by age desc, id desc
-- 如果SQL中既有条件、又有排序,必须先写条件
-- 查询所有的男同学,并按年龄升序排序
select * from student where sex='男' order by age asc
注意:如果SQL语句中,有where和order by,where一定要放到order by之前。
添加数据
语法: insert into 表名 set 字段=值, 字段=值, ......
-- insert into 表名 set 字段=值, 字段=值, ....
insert into student set age=30, sex='男', username='李青'
修改数据
语法:update 表名 set 字段=值, 字段=值,...... where 修改条件
不指定修改条件会修改所有的数据
-- 修改id为11的数据
update student set age=20, sex='女' where id=11
-- 没有指定条件,全部的数据都会修改
update student set age=25, sex='女'
删除数据
语法:delete from 表名 where 删除条件
不指定条件将删除所有数据
-- 删除一条数据
delete from student where id=11
-- 删除满足条件的数据
delete from student where id>6
-- 没有指定条件,删除全部数据
delete from student
SQL小结
SQL相当于是数据库中使用的编程语言。
可以通过SQL完成各项工作,比如查询数据,新增数据,删除数据,修改数据......
常用的增删改查语句:
- 查询 (
select 字段, 字段,... from 表名 [where 条件] [order by 字段 排序规则]
)- select * from student where id>5 order by age desc
- 新增(
insert into 表名 set 字段=值, 字段=值, ....
)- insert into student set username='张三', age=20, sex='男'
- 修改(
update 表名 set 字段=值, 字段=值, .... [where 条件]
)- update student set username='李四', sex='女' where id=4
- 删除(
delete from 表名 [where 条件]
)- delete from student where id=3
Node中的mysql模块
mysql模块的作用
数据在数据库中保存着呢?
但最终,用户应该在浏览器界面上看到数据。
这就需要使用 JS 代码,将数据库中的数据查询出来。
mysql模块是一个第三方模块,专门用来操作MySQL数据库。 可以执行增删改查操作。
安装mysql模块
初次安装第三方模块,只需要按照如下方式安装即可。后续会有详细的介绍。
# 注意,安装mysql的文件夹,不能用中文,不能叫mysql(不能和模块同名)
# 最好先执行下面这条命令,会帮你提高下载速度
npm config set registry https://registry.npm.taobao.org
# 初始化
npm init -y
# 执行下面的命令,下载安装mysql
npm i mysql
使用步骤
在Node中使用MySQL模块一共需要5个步骤:
-
加载 MySQL 模块
-
创建 MySQL 连接对象
-
连接 MySQL 服务器
-
执行SQL语句
-
关闭链接
// 1. 加载mysql模块
const mysql = require('mysql');
// 2. 创建连接对象(设置连接参数)
const conn = mysql.createConnection({
// 属性:值
host: 'localhost',
user: 'root',
password: '密码',
database: '数据库名'
});
// 3. 连接到MySQL服务器
conn.connect();
// 4. 完成查询(增删改查)
conn.query(SQL语句, (err, result) => {
err: 错误信息
result: 查询结果
});
// 5. 关闭连接,释放资源
conn.end();
练习增删改查
- 无论是查询、新增、修改、删除,都是相同的步骤。
- 不同的是,SQL不同,result结果也不同。
// 1. 加载mysql
const mysql = require('mysql');
// 2. 创建连接对象(填写连接参数)
const conn = mysql.createConnection({
host: 'localhost',
port: 3306,
user: 'root',
password: '',
database: 'yingxiong'
})
// 3. 连接到MySQL服务器
conn.connect();
// 4. 完成增删改查
let sql = 'select * from student where id<5';
let sql = 'insert into student set name="张三", age=20, sex="男"';
let sql = 'update student set age=30, sex="女" where id=3';
let sql = 'delete from student where id=3';
conn.query(sql, (err, result) => {
if (err === null) {
console.log(result);
} else {
console.log(err);
}
});
// 5. 关闭连接
conn.end();
增删改查小结
- 查询语句
- result -- 数组
- 数组的每个单元,就是查询到的一行数据
- 添加语句
- result -- 对象
- result.affectedRows -- 受影响的行数
- result.insertId -- 新增数据id
- 修改语句
- result -- 对象
- result.affectedRows -- 受影响的行数(满足条件的)
- result.changedRows -- 被改变的行数(真正发生变化的行数)
- 删除语句
- result -- 对象
- result.affectedRows -- 受影响的行数
封装MySQL
-
封装mysql,然后导出
module.exports = function (sql, callback) { const mysql = require('mysql'); const conn = mysql.createConnection({ host: 'localhost', user: 'root', password: '12345678', database: 'hahaha' }); conn.connect(); // 完成增删改查 conn.query(sql, callback); conn.end(); }
-
创建一个测试的文件,试试封装的函数
// 加载自定义模块 const db = require('./db'); // 调用函数 db('select * from student where id<5', (err, result) => { if (err) throw err; console.log(result); });
NPM(重点)
介绍
npm(node package manage)node 包 管理器。管理node包的工具。
包是什么?包就是模块。(包约等于模块,一个包可以包括一个或多个模块)
npm这个工具,在安装 node 的时候,就已经安装到你的计算机中了。
命令行中执行: npm -v
,如果看到版本号,说明安装成功了。
什么是第三方模块
非node自带的模块。也不是自定义的模块。
是别人写的模块,然后发布到npm网站,我们可以使用npm工具来下载安装别人写的模块。
第三方模块,都是在node核心模块的基础之上,封装了一下,实现了很多非常方便快速简洁的方法。
目前,npm网站收录了超过 150 万个第三方模块。
如果你想实现一个功能。那么请搜索第三方模块,没有做不到的事情,只有你搜不到。
npm的作用
npm的作用是:管理node模块的工具。
- 下载并安装第三方的模块
- 卸载第三方模块
- 发布模块
- 删除已发布的模块
- ....
npm 就是一个管理(下载安装、卸载...)第三方模块的工具
本地模块
初始化
安装本地模块,需要使用npm工具初始化。
npm init -y
# 或
npm init
# 然后一路回车
初始化之后,会在项目目录中生成 package.json 的文件。
安装卸载第三方模块的命令
初始化之后,就可以在当前文件夹中安装第三方模块了
建议在安装第三方模块之前,先执行如下命令。
下面的命令只需要执行一次即可(不管以后重启vscode还是重启电脑,都不需要执行第二次)
npm config set registry https://registry.npm.taobao.org
下载安装第三方模块
# 正常的下载安装
npm install 模块名
# 简写install为i
npm i 模块名
# 一次性安装多个模块
npm i 模块名 模块名 模块名
卸载模块
npm uninstall 模块名
npm un 模块名
npm un 模块名 模块名 模块名
上课演示的是 jquery、mysql、moment、cors、express、echarts
关于本地模块的说明
- 下载安装的模块,存放在当前文件夹的
node_modules
文件夹中,同时还会生成一个记录下载的文件package-lock.json
- 下载的模块,在哪里可以使用
- 在当前文件夹
- 在当前文件夹的子文件夹
- 在当前文件夹的子文件夹的子文件夹
- ......
- 翻过来讲,当查找一个模块的时候,会在当前文件夹的 node_modules 文件夹查找,如果找不到,则去上层文件夹的node_modules文件夹中查找,.....依次类推。
重要:代码文件夹不能有中文;代码文件夹不能和模块名同名。
怎样使用第三方模块
- 和使用内置模块一样,需要使用
require
加载模块 - 调用模块提供的方法完成工作
- 不用担心不会用,好的第三方模块都会用使用文档或者官方网站的。
- 有些模块没有官网,去 github 查找模块的使用文档,或者百度。
演示 moment 模块 的使用
// moment是一个专门处理时间日期的模块
// 使用模块之前,必须加载
const moment = require('moment');
// 设置语言环境
moment.locale('zh-cn', {
months: '一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月'.split('_'),
monthsShort: '1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月'.split('_'),
weekdays: '星期日_星期一_星期二_星期三_星期四_星期五_星期六'.split('_'),
weekdaysShort: '周日_周一_周二_周三_周四_周五_周六'.split('_'),
weekdaysMin: '日_一_二_三_四_五_六'.split('_'),
longDateFormat: {
LT: 'HH:mm',
LTS: 'HH:mm:ss',
L: 'YYYY-MM-DD',
LL: 'YYYY年MM月DD日',
LLL: 'YYYY年MM月DD日Ah点mm分',
LLLL: 'YYYY年MM月DD日ddddAh点mm分',
l: 'YYYY-M-D',
ll: 'YYYY年M月D日',
lll: 'YYYY年M月D日 HH:mm',
llll: 'YYYY年M月D日dddd HH:mm'
},
meridiemParse: /凌晨|早上|上午|中午|下午|晚上/,
meridiemHour: function (hour, meridiem) {
if (hour === 12) {
hour = 0;
}
if (meridiem === '凌晨' || meridiem === '早上' ||
meridiem === '上午') {
return hour;
} else if (meridiem === '下午' || meridiem === '晚上') {
return hour + 12;
} else {
// '中午'
return hour >= 11 ? hour : hour + 12;
}
},
meridiem: function (hour, minute, isLower) {
const hm = hour * 100 + minute;
if (hm < 600) {
return '凌晨';
} else if (hm < 900) {
return '早上';
} else if (hm < 1130) {
return '上午';
} else if (hm < 1230) {
return '中午';
} else if (hm < 1800) {
return '下午';
} else {
return '晚上';
}
},
calendar: {
sameDay: '[今天]LT',
nextDay: '[明天]LT',
nextWeek: '[下]ddddLT',
lastDay: '[昨天]LT',
lastWeek: '[上]ddddLT',
sameElse: 'L'
},
dayOfMonthOrdinalParse: /\d{1,2}(日|月|周)/,
ordinal: function (number, period) {
switch (period) {
case 'd':
case 'D':
case 'DDD':
return number + '日';
case 'M':
return number + '月';
case 'w':
case 'W':
return number + '周';
default:
return number;
}
},
relativeTime: {
future: '%s内',
past: '%s前',
s: '几秒',
ss: '%d秒',
m: '1分钟',
mm: '%d分钟',
h: '1小时',
hh: '%d小时',
d: '1天',
dd: '%d天',
M: '1个月',
MM: '%d个月',
y: '1年',
yy: '%d年'
},
week: {
// GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效
dow: 1, // Monday is the first day of the week.
doy: 4 // The week that contains Jan 4th is the first week of the year.
}
});
// console.log(moment().format("YYYY-MM-DD HH:mm:ss"))
// console.log(moment().format("L"))
// console.log(moment([2021, 0, 22, 09, 30, 25]).fromNow())
// console.log(moment(13432542333).fromNow())
console.log(moment('2020-11-10T15:49:05.000Z').fromNow())
演示jsonwentoken模块
jsonwebtoken模块的作用是生成token字符串。
https://github.com/auth0/node-jsonwebtoken
// 加载模块
const jwt = require('jsonwebtoken');
// console.log(jwt.sign(必填, 必填, 可选, 可选));
// Bearer 不属于token的内容,只是表示token的格式。
// jwt.sign()
// 1. 参数1:对象,要在token保存的数据
// 2. 参数2:加密的字符串,类似于一个钥匙。随便填;后续解密token的时候,需要使用它
// 3. 参数3:对象,配置项,比如配置一下过期时间
// 4. 参数4:生成token后的回调函数
// console.log('Bearer ' + jwt.sign(
// { id: 1, username: 'zs' },
// 'shhhhh',
// { expiresIn: '2h' },
// // (err, abc) => console.log(abc)
// ));
jwt.sign({ id: 1 }, 'sdfsdf', { expiresIn: '2h' }, (err, result) => {
if (err) throw err;
console.log('Bearer ' + result);
});
package.json文件
在初始化之后,会生成一个package.json文件
-
创建
package.json
npm init npm init -y
-
main
main 字段指定了模块的入口文件。
-
dependencies 依赖(复数)
-
dependencies指定了当前项目所依赖(需要)的包,使用
npm install
可以安装所有的依赖 -
软件的版本号 jQuery@3.3.1
- 大版本.次要版本.小版本
- 小版本:当项目在进行了局部修改或 bug 修正时,修正版本号加 1
- 次要版本:当项目在原有的基础上增加了部分功能时,主版本号不变,子版本号加 1
- 大版本:当项目在进行了重大修改或局部修正累积较多,而导致项目整体发生全局变化时,主版本号加 1
-
版本号前的
~
和^
- 指定版本:比如
1.2.2
,遵循“大版本.次要版本.小版本”的格式规定,安装时只安装指定版本。 - 波浪号(tilde)+指定版本:比如
~1.2.2
,表示安装1.2.x的最新版本(不低于1.2.2),但是不安装1.3.x,也就是说安装时不改变大版本号和次要版本号。 - 插入号(caret)+指定版本:比如ˆ1.2.2,表示安装1.x.x的最新版本(不低于1.2.2),但是不安装2.x.x
- 指定版本:比如
-
-
scripts
scripts
指定了运行脚本命令的 npm 命令行缩写,比如start指定了运行npm run start
时,所要执行的命令。"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node app.js", "t": "dir c:\\" }
运行
scripts
npm run t npm run start # 只有 start 可以简化调用 npm start
依赖的作用:
- 记录项目必须的包
- 发送给别人的时候,不需要发送比较大的
node_modules
文件夹。只需要发送给你package.json
即可,你只需要执行npm install
即可安装所有的包
require的加载机制
-
判断缓存中有没有,如果有,使用缓存中的内容
-
缓存中没有,那么表示第一次加载,加载完会缓存
-
判断模块名有没有带路径(./)
-
模块名中有路径,加载自定义模块(自己写的文件)
const xx = require('./xx')
- 优先加载同名文件,加载一个叫做 xx 的文件
- 再次加载js文件,加载 xx.js 文件
- 再次加载json文件,加载 xx.json 文件
- 最后加载node文件,加载 xx.node文件
- 如果上述文件都没有,则报错 “Cannot find module './xx'”
-
模块名没有路径,优先加载核心模块,如果没有核心模块,则加载第三方模块
-
加载第三方模块的查找方式
- 优先在当前文件夹的node_modules里面查找第三方模块
- 在当前文件夹的上级目录的node_modules里面查找第三方模块
- 继续向上层文件夹查找第三方模块
全局模块
和本地模块的差异
- 全局安装的模块,不能通过
require()
加载使用。 - 全局安装的模块,一般都是命令或者工具。
安装卸载命令
-
安装命令(多一个
-g
)npm i 模块名 -g # 或 npm i -g 模块名 ### mac 系统如果安装不上,使用下面的命令提高权限 sudo npm i -g 模块名
-
卸载命令(也是多一个
-g
)npm un 模块名 -g
-
全局安装的模块,在系统盘(C盘)
- 通过命令
npm root -g
可以查看全局安装路径
- 通过命令
全局安装nodemon模块
-
安装命令
npm i nodemon -g
-
nodemon的作用:
- 代替node命令,启动服务的工具
- 当更改代码之后,nodemon会自动重启服务。
-
解决办法是:
管理员
方式,打开命令行(powershell)窗口- 执行
set-ExecutionPolicy RemoteSigned;
- 在出现的选项中,输入
A
,回车。即可
-
如果报错如下
![
- 解决办法,重启vscode,win7可能要重启电脑。
- 如果上述问题都解决了,还是不能用nodemon,联系老师;
全局安装nrm
nrm 是作用是切换镜像源。
![
全局安装
npm i -g nrm (mac系统前面加 sudo)
使用nrm
nrm ls --- 查看全部可用的镜像源
nrm use taobao ---- 切换到淘宝镜像
nrm use npm ---- 切换到npm主站
全局模块和本地模块的对比
本地模块,在安装之前,必须先初始化;全局安装的模块不需要初始化。
![
开发属于自己的包
规范的包结构
在清楚了包的概念、以及如何下载和使用包之后,接下来,我们深入了解一下包的内部结构。
📂 - sy123
📃 - package.json (package.json包的配置文件)
📃 - index.js (入口文件)
📃 - README.md (说明文档)
一个规范的包结构,需要符合以下 3 点要求:
- 包必须以单独的目录而存在
- 包的顶级目录下要必须包含 package.json 这个包管理配置文件
- package.json 中必须包含 name,version,main 这三个属性,分别代表包的名字、版本号、包的入口。
- name 包的名字,我们使用 require()加载模块的时候,使用的就是这个名字
- version 版本,1.2.18
- main 入口文件。默认是index.js 。如果不是,需要使用main指定
注意:以上 3 点要求是一个规范的包结构必须遵守的格式,关于更多的约束,可以参考如下网址:
https://yarnpkg.com/zh-Hans/docs/package-json
开发属于自己的包
-
初始化 package.json
注意,JSON文件不能有注释,下面加注释,是为了理解。
{ "name": "sy123", // 包(模块)的名字,和文件夹同名。别人加载我们的包,找的就是这个文件夹 "version": "1.0.0", "description": "This is a package by Laotang", "main": "index.js", // 别人加载我们的模块用,require加载的就是这里指定的文件 "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ // 在npm网站中,通过关键字可以搜索到我们的模块,按情况设置 "laotang", "itcast", "test" ], "author": "Laotang", // 作者 "license": "ISC" // 开源协议 }
关于更多 license 许可协议相关的内容,可参考 https://www.jianshu.com/p/23e61804d81e
-
index.js 中定义功能方法
// 别人加载的就是我的 index.js // 所以,必须在 index.js 中导出内容 function a() { console.log('aaa') } function b() { console.log('bbb') } module.exports = { a, b }
-
编写包的说明文档
包根目录中的 README.md 文件,是包的使用说明文档。通过它,我们可以事先把包的使用说明,以 markdown 的 格式写出来,方便用户参考。
README 文件中具体写什么内容,没有强制性的要求;只要能够清晰地把包的作用、用法、注意事项等描述清楚即可。
注册npm账号
- 访问 https://www.npmjs.com/ 网站
- 点击 sign up 按钮,进入注册用户界面
- 填写账号相关的信息
- 点击 Create an Account 按钮,注册账号
- 注册完账号,需要到邮箱中认证一下
发布包
-
终端中
,切换镜像源为npm(不能发布到淘宝,所以必须切换镜像源为npm主站)- nrm use npm
-
终端中
,登录 npm 账号- 执行
npm login
命令 - 输入账号
- 输入密码(输入的密码是看不见的,正常)
- 输入邮箱
- 执行
-
发布
- 注意,执行命令的文件夹,必须是包的根目录。
- 运行
npm publish
命令,即可将包发布到 npm 上
-
常见错误
-
自己的模块名(文件夹名)不能和已存在的模块名同名,相似也不行。
-
没有切换镜像源,会提示如下错误。要发布到npm上,必须切换镜像源为npm
-
24小时内不能重复发布
-
新注册的账号,必须先邮箱(邮件可能是垃圾邮件)验证,然后才能发布
-
-
删除已发布的包
- 运行 npm unpublish 包名 --force 命令,即可从 npm 删除已发布的包。
-
注意:
- npm unpublish 命令只能删除 72 小时以内发布的包
- npm unpublish 删除的包,在 24 小时内不允许重复发布
- 发布包的时候要慎重,尽量不要往 npm 上发布没有意义的包!
更多关于npm的命令:https://www.npmjs.cn/
Express
express 介绍
- Express 是一个第三方模块,用于快速搭建服务器(替代http模块)
- Express 是一个基于 Node.js 平台,快速、开放、极简的 web 开发框架。
- Express保留了http模块的基本API,使用express的时候,也能使用http的API
- 使用express的时候,仍然可以使用http模块的方法,比如 res.end()、req.url
- express还额外封装了一些新方法,能让我们更方便的搭建服务器
- express提供了中间件功能,其他很多强大的第三方模块都是基于express开发的
- Express 官网
- Express 中文文档(非官方)
- Express GitHub仓库
- 菜鸟教程
- 腾讯云开发者手册
- 百度自行搜索
安装 express
项目文件夹中,执行 npm i express
。即可下载安装express。
注意:express不能安装在express文件夹中。否则安装失败。
使用Express构造Web服务器
使用Express构建Web服务器步骤:
-
加载 express 模块
-
创建 express 服务器
-
开启服务器
-
监听浏览器请求并进行处理
// 使用express 搭建web服务器
// 1) 加载 express 模块
const express = require('express');
// 2) 创建 express 服务器
const app = express();
// 3) 开启服务器
app.listen(3006, () => console.log('express服务器开始工作了'));
// 4) 监听浏览器请求并进行处理
app.get('GET请求的地址', 处理函数);
app.post('POST请求的地址', 处理函数);
express封装的新方法
express之所以能够实现web服务器的搭建,是因为其内部对核心模块http进行了封装。
封装之后,express提供了非常方便好用的方法。
比如前面用到的 app.get() 和 app.post()
就是express封装的新方法。
下面再介绍一个 res.send()
方法
- 该方法可以代替之前的 res.end 方法,而且比 res.end 方法更好用
- res.send() 用于做出响应
- 响应的内容同样不能为数字
- 如果响应的是JS对象,那么方法内部会自动将对象转成JSON格式。
- 而且会自动加Content-Type响应头
- 如果已经做出响应了,就不要再次做出响应了。
const express = require('express');
const app = express();
app.listen(3006, () => console.log('启动了'));
// 写接口
app.get('/api/test', (req, res) => {
// res.end('hello world,哈哈哈'); // 响应中文会乱码,必须自己加响应头
// res.end(JSON.stringify({ status: 0, message: '注册成功' })); // 只能响应字符串或者buffer类型
// express提供的send方法,可以解决上面的两个问题
res.send({ status: 0, message: '注册成功' }); // send方法会自动设置响应头;并且会自动把对象转成JSON字符串
});
请注意,在express中,我们仍然可以使用http模块中的方法和属性,比如req.url。
Express路由
-
路由:即请求和处理程序的映射关系。
-
使用路由的好处:
- 降低匹配次数,提高性能
- 分类管理接口,更易维护与升级
-
使用步骤:
/**
* 使用路由文件的步骤
* 1. 加载express模块
* 2. 创建 router 对象
* 3. 把接口挂载到 router 对象上
* 4. 导出 router 对象
*
* index.js 中
* 5. 加载路由模块,并注册成中间件
*/
- 注意事项:
- 路由文件如果没有导出 router,那么在 入口文件中不要注册中间件,否则报错
- 哪个路由文件中使用了db,自己加载(谁用谁加载)
![ima
login.js
// --------------------- 使用路由的步骤 ----------------------
// 1. 加载express
const express = require('express');
// 2. 创建路由对象,实则是个函数类型
const router = express.Router();
// 3. 写接口,把接口挂载到 router 上
router.post('/reguser', (req, res) => {});
router.post('/login', (req, res) => {});
// 一定要导出 router
module.exports = router;
index.js
// 三行必须的代码,启动服务
const express = require('express');
const app = express();
// 加载自定义的路由模块,注册中间件
let loginRouter = require('./routers/login');
app.use('/api', loginRouter);
app.listen(3006, () => console.log('启动了'));
Express中间件
中间件介绍
- 中间件(Middleware ),特指业务流程的中间处理环节。
- 中间件,是express最大的特色,也是最重要的一个设计
- 很多第三方模块,都可以当做express的中间件,配合express,开发更简单。
- 一个express应用,是由各种各样的中间件组合完成的
- 中间件,本质上就是一个函数
中间件原理
为了理解中间件,我们先来看一下我们现实生活中的自来水厂的净水流程。
![im
- 在上图中,自来水厂从获取水源到净化处理交给用户,中间经历了一系列的处理环节
- 我们称其中的每一个处理环节就是一个中间件。
- 这样做的目的既提高了生产效率也保证了可维护性。
express中间件原理:
![im
中间件的几种形式
// 下面的中间件,只为当前接口 /my/userinfo 这个接口服务
app.get('/my/userinfo', 中间件函数);
// 下面的几个中间件,是处理 /api/login 接口的
app.post('/api/login', 中间件函数, 中间件函数, 中间件函数, 中间件函数 .....);
// app.use 中的中间件,可以处理所有的GET请求和所有的POST请求,没有指定路径,那么处理所有接口
app.use(中间件函数);
// 下面的中间件函数,只处理 /api 开头的接口
app.use('/api', 中间件函数);
// 下面的中间件函数,处理 /abcd 、 /abd 这两个接口
app.use('/abc?d', 中间件函数);
app.get或者app.post表示写接口,必须写接口地址;
app.use() 参数1路由前缀,可以省略。另外无论是GET还是POST方式的请求,都会进入该中间件。
中间件语法
- 中间件就是一个函数
- 中间件函数中有四个基本参数, err、req、res、next
- 如果写两个参数,那么两个参数肯定是 req 和 res
- 如果写三个参数,那么三个参数肯定是 req, res 和 next
- 如果写四个参数,那么就是全部的参数,这个中间件叫做错误处理中间件。
- 把写好的中间件函数,传递给
app.get()
、app.post()
、或app.use()
使用
中间件的特点
![ima
- 每个中间件函数,共享req对象、共享res对象,即所有的req对象是一个对象;所有的res是一个对象
- 比如上述中间件1,为 req 对象添加了 body属性。中间件5中可以直接使用。
- 不调用
next()
,请求会卡在当前中间件;调用next()
表示将请求交给下一个中间件处理。- 上述中间件中的
next()
保证了 请求 ~ 响应 能够完整的进行完。 - 没有给
next()
传递参数,则正常进入下一个中间件。 - 有给
next(err)
传递参数,则直接进入错误处理中间件
- 上述中间件中的
- 所有请求都可以进入使用
app.use()
注册的中间件,但要注意前缀- 中间件1,给出接口前缀
/api
,所以只有/api
开头的接口才能进入 - 中间件2,给出接口前缀
/my
,所以只有/my
开头的接口才能进入 - 中间件3,没有给出接口前缀,所以任何接口都能进入
- 中间件1,给出接口前缀
- 错误处理中间件,必须传递 err、req、res、next四个参数,而且要放到所有接口的后面
- 一般用于同一处理错误信息。
- 错误处理中间件,也可以继续
next(错误)
,把错误交给后续的错误处理中间件处理。
中间件分类
- 应用级别的中间件(index.js 中的中间件,全局有效 )
- 路由级别的中间件(路由文件中的中间件,只在当前路由文件中有效)
- 错误处理中间件(四个参数都要填,一般放到最后)
- 内置中间件(express自带的,比如
express.urlencoded({ extended: true })
) - 第三方中间件(比如multer、express-jwt、express-session、....)
实际开发中,自己写中间件的机会并不大,一般都有对应的第三方中间件。
案例 - 登录注册接口
使用GIT管理项目
- bigevent-server
- index.js
- db.js
- .gitignore
- package.json
- package-lock.json ---- 被忽略
- node_modules ---- 被忽略
搭建好项目目录之后。使用Git初始化。
设置忽略文件(.gitignore
),这个忽略文件中记录的文件、文件夹不会添加到暂存区,不会提交到本地仓库,当然也就不会推送到远程仓库。
# git的忽略文件
# 忽略文件中指定的 文件、文件夹 不会被添加到暂存区,不会提交到本地仓库,不会推送到远程仓库
node_modules
package-lock.json
设置好忽略文件之后,下一步add、commit、push。即可。
忽略文件的语法
# 只忽略根目录里面的 node_modules
/node_modules
# 忽略所有叫做 node_modules 的文件夹
node_modules/
# 忽略所有的 .a 文件
*.a
# 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件
!lib.a
# 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO
/TODO
# 忽略任何目录下名为 build 的文件夹
build/
# 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt
doc/*.txt
# 忽略 doc/ 目录及其所有子目录下的 .pdf 文件
doc/**/*.pdf
创建数据表
字段 | 类型 | 长度 | 不是null | 主键 | 其他 |
---|---|---|---|---|---|
id | int | √ | 🔑 | √ 自动递增 | |
username | varchar | 10 | √ | ||
password | char | 32 | √ | ||
user_pic | longtext | ||||
nickname | varchar | 10 | |||
varchar | 30 |
使用ApiPost模拟注册请求
![ima
写接口
// 完成接口项目
// 前面三行启动服务
const express = require('express');
const app = express();
app.listen(3006, () => console.log('启动了'));
// 配置 + 写接口
// -------------------- 注册接口 ----------------------
// 请求体:username password
app.post('/api/reguser', (req, res) => {
// 1. 接口要接收数据
// 2. 判断账号是否已经被占用了
// 3. 如果没有被占用,把账号密码添加到数据库
});
服务端使用 req.body 接收请求体
请求体就是客户端提交的数据(username和password)。
// 完成接口项目
// 前面三行启动服务
const express = require('express');
const app = express();
app.listen(3006, () => console.log('启动了'));
// 配置 + 写接口
app.use(urlencoded({ extended: true }));
// -------------------- 注册接口 ----------------------
// 请求体:username password
app.post('/api/reguser', (req, res) => {
// 1. 接口要接收数据
console.log(req.body); // { username: 'laotang', password: '123456' }
// 2. 判断账号是否已经被占用了
// 3. 如果没有被占用,把账号密码添加到数据库
});
代码写完,一定要使用ApiPost发送请求,测试代码。
验证用户名是否存在
思路:根据用户名查询,看是否能够查到数据。
- 没有查询数据,说明这个用户名不存在,能够使用
- 如果查到数据库,说明这个用户名已经存在,不能使用
// 完成接口项目
// 前面三行启动服务
const express = require('express');
const app = express();
app.listen(3006, () => console.log('启动了'));
// 配置 + 写接口
app.use(urlencoded({ extended: true }));
// -------------------- 注册接口 ----------------------
// 请求体:username password
app.post('/api/reguser', (req, res) => {
// 1. 接口要接收数据
console.log(req.body); // { username: 'laotang', password: '123456' }
let { username, password } = req.body;
// 2. 判断账号是否已经被占用了
db('select * from user where username="${username}"', (err, result) => {
if (err) throw err;
// console.log(result); // 查到信息,result是非空数组;没有查到信息,result是空数组
if (result.length > 0) {
res.send({ status: 1, message: '用户名被占用了' });
} else {
// 没有被占用
// 3. 如果没有被占用,把账号密码添加到数据库
}
})
});
完成注册
如果用户名可用,则添加到数据表中,完成注册
// -------------------- 注册接口 ----------------------
// 请求体:username password
app.post('/api/reguser', (req, res) => {
// 1. 接口要接收数据
// console.log(req.body); // { username: 'laotang', password: '123456' }
let { username, password } = req.body;
// 2. 判断账号是否已经被占用了
db(`select * from user where username='${username}'`, (err, result) => {
if (err) throw err;
// console.log(result); // 查到信息,result是非空数组;没有查到信息,result是空数组
if (result.length > 0) {
res.send({ status: 1, message: '用户名被占用了' });
} else {
// 没有被占用
// 3. 如果没有被占用,把账号密码添加到数据库
db(`insert into user set username='${username}', password='${password}'`, (e, r) => {
if (e) throw e;
res.send({ status: 0, message: '注册成功' });
});
}
});
});
对密码进行md5加密
安全起见,数据表中不能存储明文密码。必须存储加密后的密码,而且应该使用一种不可逆的加密方案。
常用的加密方式是 md5。
-
下载安装第三方加密模块,并解构里面的 md5 方法
let { md5 } = require('utility')
-
对密码进行加密
password = md5(password)
客户端模拟登录请求
![ima
完成登录接口
- 接口要接收账号和密码(前面已经配置好 app.use(express.urlencoded({ extended: true }))),所以还是直接使用req.body
- 对密码进行加密
- 使用账号和加密的密码当条件,查询。
/**
* 登录接口
* 请求方式:POST
* 接口地址:/api/login
* 请求体:username | password
*/
app.post('/api/login', (req, res) => {
// console.log(req.body); // { username: 'laotang', password: '123456' }
let { username, password } = req.body;
password = md5(password);
// 使用username和加密的密码当做条件,查询。
let sql = `select * from user where username='${username}' and password='${password}'`;
db(sql, (err, result) => {
if (err) throw err;
// console.log(result); // 没有查到结果得到空数组; 查到结果得到非空数组
if (result.length > 0) {
res.send({ status: 0, message: '登录成功' })
} else {
res.send({ status: 1, message: '账号或密码错误' })
}
})
});
创建token
使用第三方模块 jsonwebtoken 创建token字符串。
-
下载安装 npm i jsonwebtoken
-
加载模块 const jwt = require('jsonwebtoken');
-
调用 jwt.sign() 方法创建token
- 参数1:必填,对象形式;希望在token中保存的数据
- 参数2:必填,字符串形式;加密的钥匙;后续解密token的时候,还需要使用。
- 参数3:可选,对象形式;配置项,比如可以配置token的有效期
- 参数4:可选,函数形式;生成token之后的回调
-
生成的token前面,必须拼接
Bearer
这个字符串。
if (result.length > 0) {
// 登录成功,生成token
// 在token中保存用户的id
// token前必须加 “Bearer ”,注意空格
let token = 'Bearer ' + jwt.sign({ id: result[0].id }, 'sfsws23s', { expiresIn: '2h' });
res.send({ status: 0, message: '登录成功', token })
} else {
res.send({ status: 1, message: '账号或密码错误' })
}
案例 - 服务端数据验证
- 客户端提交数据给服务器,服务器端也要进行验证。
- 开发领域,有一句话,叫做 “永远不要相信客户端的数据”
任务
验证,账号必须是2~10位,且只能使用数字、字母下划线组合,且必须是字母开头
验证,密码必须是6~12位,且不能有空格
验证流程
![ima
验证中间件
// 必须在这里,注册中间件,完成数据的验证
router.use((req, res, next) => {
// 获取username和password
let { username, password } = req.body;
// 验证用户名
if (!/^[a-zA-Z][0-9a-zA-Z_]{1,9}$/.test(username)) {
next('用户名只能包含数组字母下划线,长度2~10位,字母开头');
} else if (!/^\S{6,12}$/.test(password)) {
next('密码6~12位且不能出现空格');
} else {
next();
}
});
错误处理中间件
// 错误处理中间件
router.use((err, req, res, next) => {
// err 就是前面next过来的参数值
res.send({ status: 1, message: err })
});
服务端身份认证
JWT原理回顾
对于前后端分类模式的开发,大多使用 JWT(json web token)进行身份认证。
前面已经讲过JWT的原理了,使用下图回顾一下。
![ima
分析服务端该做什么
大事件中已经把前端该做的完成了,剩下的就是后端的任务了。
前端:
- 登录成功后,保存token
- 请求
/my/xxx
接口时,在请求头中加入Authorization
字段,值为token。
后端:
- 登录接口生成token
- 判断,如果客户端请求的是
/my/xxx
接口,要解析并验证token的真伪
![ima
代码实现认证
选择使用 express-jwt 第三方模块进行身份认证。从模块名可以看出,该模块是专门配合express使用的。
下载安装:npm i express-jwt
后端 index.js 中,当接收到一个请求后,先解析并验证token。
const jwt = require('express-jwt');
// app.use(jwt().unless());
// jwt() 用于解析token,并将 token 中保存的数据 赋值给 req.user
// unless() 完成身份认证
app.use(jwt({
secret: 'sfsws23s', // 生成token时的 钥匙,必须统一
algorithms: ['HS256'] // 必填,加密算法,无需了解
}).unless({
path: ['/api/login', '/api/reguser'] // 除了这两个接口,其他都需要认证
}));
上述代码完成后,当一个请求发送到服务器后,就会验证请求头中的 Authorization 字段了。
- 如果没有问题
- 将token中保存的 用户id 赋值给 req.user
- next()。
- 如果有问题,则 next(错误信息)。
所以,还需要在index.js 最后,加入错误处理中间件,来提示token方面的错误。文档中抄下来,修改。
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
// res.status(401).send('invalid token...');
res.status(401).send({ status: 1, message: '身份认证失败!' });
}
});
案例 - 个人中心接口
设计数据表
因为已经做过登录注册了,所以,这里不用再次创建了。
使用路由模块
/routers/user.js
// user路由文件
const express = require('express');
const router = express.Router();
// 导出
module.exports = router;
index.js中加载路由模块,注册中间件
index.js
app.use('/my/user', require('./routers/user'));
获取用户信息接口
ApiPost模拟请求
![ima
接口代码:
// *************************** 获取用户信息 ***************************/
/**
* 请求方式:GET
* 接口地址:/my/user/userinfo
* 参数: 无
*/
router.get('/userinfo', (req, res) => {
// console.log(req.user); // { id: 1, iat: 1611537302, exp: 1611544502 }
// return;
// 查询当前登录账号的信息,并不是查询所有人的信息
db('select * from user where id=' + req.user.id, (err, result) => {
if (err) throw err;
res.send({
status: 0,
message: '获取用户信息成功',
data: result[0]
})
});
});
更新用户信息接口
客户端模拟请求:
![ima
代码实现:
// *************************** 更新用户信息 ***************************/
/**
* 请求方式:POST
* 接口地址:/my/user/userinfo
* Content-Type: application/x-www-form-urlencoded
* 请求体:email | nickname | id
*/
router.post('/userinfo', (req, res) => {
// console.log(req.body); // { nickname: '老汤', email: '23323@qq.com', id: '1' }
let { id, nickname, email } = req.body;
if (id != req.user.id) return res.send({ status: 1, message: '无权更新' });
let sql = `update user set nickname='${nickname}', email='${email}' where id=${id}`;
db(sql, (err, result) => {
if (err) throw err;
res.send({ status: 0, message: '更新用户信息成功' });
})
});
更换头像接口
客户端模拟请求:
![ima
代码实现:
// *************************** 更换头像接口 ***************************/
/**
* 请求方式:POST
* 接口地址:/my/user/avatar
* Content-Type: application/x-www-form-urlencoded
* 请求体:avatar
*/
router.post('/avatar', (req, rebase64,iVBORw0KGgoAAAANSUhEUgAAAHg' }
let sql = `update user set user_pic='${req.body.avatar}' where id=${req.user.id}`;
db(sql, (err, result) => {
if (err) throw err;
res.send({ status: 0, message: '更换头像成功' })
})
});
重置密码接口
ApiPost认证
登录之后,把token字符串,保存到ApiPost的全局变量中。
![ima
登录之后,就会在ApiPost的全局变量中,保存上一个token。可以点击小眼睛查看:
![ima
其他需要认证的接口,选择认证,Bearer auth认证,填写 {{token}} 即可。
ApiPost发送请求的时候,会自动携带Authorization 这个请求头,并且会在token字符串前加上“Bearer ”,从而完成身份认证。
![ima
案例 - 类别管理接口
设计数据表并添加模拟数据
导入SQL。(user、category、article表中的数据有关联。必须全部导入,然后使用账号 admin、密码admin登录)
使用路由模块
/routers/category.js
// category路由文件
const express = require('express');
const router = express.Router();
// 导出
module.exports = router;
index.js中加载路由模块,注册中间件
index.js
app.use('/my/category', require('./routers/category'));
完成获取分类列表数据的接口
注意加载db.js
// ----------------- 获取分类的接口 ------------------
/**
* 请求方式:GET
* 接口地址: /my/category/list
* 参数:无
*/
router.get('/list', (req, res) => {
// 调用db函数,查询所有的分类
db('select * from category', (err, result) => {
if (err) throw err;
// 没有错误的话,做出响应
res.send({
status: 0,
message: '获取分类成功',
data: result
});
});
});
删除分类的接口
客户端发送请求,并且传递id参数
![ima
服务端代码:
// ----------------- 删除分类的接口 ------------------
/**
* 请求方式:GET
* 接口地址: /my/category/delete
* 请求参数: id(分类id),类型querystring
*/
router.get('/delete', (req, res) => {
// 获取id参数
var id = req.query.id;
// 删除数据表中的数据
db('delete from category where id=' + id, (err, result) => {
// 做出响应
if (err) throw err;
if (result.affectedRows > 0) {
res.send({status: 0, message: '删除分类成功'});
} else {
res.send({status: 1, message: '删除分类失败'})
}
});
});
添加分类的接口
![ima
服务端代码:
由于 index.js 中,有 app.use(express.urlencoded({ extended: true }))
,所以这里直接使用 req.body
接收请求体。
// ----------------- 添加分类的接口 ------------------
/**
* 请求方式:POST
* 接口地址: /my/category/add
* 请求参数: name(类别名称) | alias(类别别名)
* Content-Type: application/x-www-form-urlencoded
*/
router.post('/add', (req, res) => {
// 1. 接收客户端提交的数据(name和alias)
// console.log(req.body); // { name: '娱乐', alias: 'yule' }
let { name, alias } = req.body;
// 2. 添加到数据库
let sql = `insert into category set name='${name}', alias='${alias}'`;
db(sql, (err, result) => {
if (err) throw err;
// 3. 做出响应
res.send({ status: 0, message: '添加分类成功' })
});
});
更新分类接口
![ima
服务端代码:
// ----------------- 修改分类的接口 ------------------
/**
* 请求方式:POST
* 接口地址: /my/category/update
* 请求参数: name(类别名称) | alias(类别别名) | id(分类的id)
* Content-Type: application/x-www-form-urlencoded
*/
router.post('/update', (req, res) => {
// 1. 接收客户端提交的数据(id、name、alias)
// console.log(req.body); // { id: '1', name: '科技', alias: 'keji' }
let { id, name, alias } = req.body;
// 2. 执行update语句,修改数据
let sql = `update category set name='${name}', alias='${alias}' where id=${id}`;
db(sql, (err, result) => {
if (err) throw err;
if (result.affectedRows > 0) {
res.send({ status: 0, message: '修改分类成功' })
} else {
res.send({ status: 1, message: '修改分类失败' })
}
})
});
案例 - 文章相关接口
使用路由模块
/routers/article.js
// category路由文件
const express = require('express');
const router = express.Router();
// 导出
module.exports = router;
index.js中加载路由模块,注册中间件
index.js
app.use('/my/article', require('./routers/article'));
分页获取文章接口
注意引入 db.js
直接复制代码。因为这个接口中,涉及到 MySQL 连表查询和统计查询。
// ---------------- 分页获取文章列表 ----------------
// 接口要求:
/**
* 请求方式:GET
* 请求的url:/my/article/list
* 请求参数:
* - pagenum -- 页码值
* - pagesize -- 每页显示多少条数据
* - cate_id -- 文章分类的Id
* - state -- 文章的状态,可选“草稿”或“已发布”
*/
router.get('/list', (req, res) => {
// console.log(req.query);
// 设置变量,接收请求参数
let { pagenum, pagesize, cate_id, state } = req.query;
// console.log(cate_id, state);
// 根据cate_id 和 state制作SQL语句的条件
let w = '';
if (cate_id) {
w += ` and cate_id=${cate_id} `;
}
if (state) {
w += ` and state='${state}' `;
}
// 分页查询数据的SQL(该SQL用到了连表查询,并且使用了很多变量组合)
let sql1 = `select a.id, a.title, a.state, a.pub_date, c.name cate_name from article a
join category c on a.cate_id=c.id
where author_id=${req.user.id} and is_delete=0 ${w}
limit ${(pagenum - 1) * pagesize}, ${pagesize}`;
// 查询总记录数的SQL,查询条件和前面查询数据的条件 必须要一致
let sql2 = `select count(*) total from article a
join category c on a.cate_id=c.id
where author_id=${req.user.id} and is_delete=0 ${w}`;
// 分别执行两条SQL(因为db查询数据库是异步方法,必须嵌套查询)
db(sql1, (err, result1) => {
if (err) throw err;
db(sql2, (e, result2) => {
if (e) throw e;
res.send({
status: 0,
message: '获取文章列表数据成功',
data: result1,
total: result2[0].total
});
})
})
});
添加文章接口
// ---------------- 添加文章接口 --------------------
/**
* 接口地址:/my/article/add
* 请求方式:POST
* 请求体:title | content | cate_id | state | cover_img
* Content-Type: multipart/form-data
*/
这是我们遇到的第一个请求体为FormData类型的接口。
服务端获取FormData类型的数据,需要使用第三方模块 multer。
安装:npm i multer
加载:const multer= require('multer')
配置上传文件路径:const upload = multer({ desc: 'uploads/' })
接口中使用:
router.post('/add', upload.single('cover_img'), (req, res) => {
// upload.single() 方法用于处理单文件上传
// cover_img 图片字段的名字
// 通过 req.body 接收文本类型的请求体,比如 title,content等
// 通过 req.file 获取上传文件信息
});
只要客户端请求这个接口,就会自动创建 uploads
文件夹,并把文件上传到该文件夹。
此时,可以使用ApiPost测试:
![ima
注意,大事件接口文档规定,客户端只能提交 “title、content、cate_id、state、cover_img” 5个值。
而,article 数据表中,还要求添加 author_id
(用户id)、pub_date
(发布时间),这两个字段的值只能自己来添加了。
发布时间的处理,需要使用 moment
模块,自行安装。
var multer = require('multer')
var upload = multer({ dest: 'uploads/' }); // 配置上传文件的目录
const moment = require('moment');
router.post('/add', upload.single('cover_img'), (req, res) => {
// req.body 表示文本信息
// req.file 表示上传的文件信息
// console.log(req.file); // req.file.filename 表示上传之后的文件名
// 把数据添加到数据表中存起来
// req.body = { title: 'xx', content: 'xx', cate_id: 1, state: 'xx' }
let { title, content, cate_id, state } = req.body;
// 其他字段
let pub_date = moment().format('YYYY-MM-DD HH:mm:ss');
let cover_img = req.file.filename;
let author_id = req.user.id;
// console.log(obj);
// return;
let sql = `insert into article set title='${title}', content='${content}', cate_id=${cate_id}, state='${state}', pub_date='${pub_date}', cover_img='${cover_img}', author_id=${author_id}`;
db(sql (err, result) => {
if (err) throw err;
if (result.affectedRows > 0) {
res.send({ status: 0, message: '发布成功' })
} else {
res.send({ status: 1, message: '发布失败' })
}
})
});
删除文章接口
客户端模拟请求:
![ima
删除,可以做成 “软删除” 效果。即不是真的删除,而是把 is_delete 字段改为 1.
- is_delete = 0 ,正常的文章。获取文章的时候,只获取 id_delete=0 的文章。
- is_delete = 1, 表示已删除的文章。
// ---------------- 删除文件接口 --------------------
/**
* 请求方式:GET
* 接口地址:/my/article/delete/2
* 请求参数:id,url参数
*/
// router.get('/delete/:id/:age/:name', (req, res) => {
router.get('/delete/:id', (req, res) => {
let id = req.params.id;
let sql = `update article set is_delete=1 where id=${id} and author_id=${req.user.id}`;
db(, (err, result) => {
if (err) throw err;
if (result.affectedRows > 0) {
res.send({ status: 0, message: '删除成功' })
} else {
res.send({ status: 1, message: '删除失败' })
}
})
});
更新文章接口
router.post('/update', upload.single('cover_img'), (req, res) => {
// 和添加文章接口差不多,要注意,客户端多提交了文章id,这是我们修改文章的条件
// req.body = { title: 'xx', content: 'xx', cate_id: 1, state: 'xx', id: 6 }
let { title, content, cate_id, state, id } = req.body;
// 其他字段(发布时间,不是修改时间,所以不需要改了,用户id也不需要改)
let cover_img = req.file.filename;
// console.log(obj);
// return;
let sql = `update article set title='${title}', content='${content}', cate_id=${cate_id}, state='${state}', cover_img='${cover_img}' where id=${id}`;
db(sql, (err, result) => {
if (err) throw err;
if (result.affectedRows > 0) {
res.send({ status: 0, message: '修改文章成功' })
} else {
res.send({ status: 1, message: '修改文章失败' })
}
})
});
根据id获取一篇文章接口
// ---------------- 根据id获取一篇文章接口 -----------
router.get('/:id', (req, res) => {
// 怎么获取url中的参数,答,使用express提供的 req.params
// console.log(req.params.id);
db('select * from article where id='+req.params.id, (err, result) => {
if (err) throw err;
res.send({
status: 0,
message: '获取文章成功',
data: result[0]
})
})
});
小结Express接收客户端数据
![ima
req.user 是 express-jwt 解密token之后,把token里面保存的用户信息赋值给 req.user;
使用前端代码测试接口
同源策略
同宗同源:比如,你和你的亲兄弟、亲姐妹,叫叫做同源。
浏览器中,也有同源策略。指的是打开页面的URL和Ajax请求的URL比较,比较他俩是否同源。
比如,打开页面的URL:http://127.0.0.1:5501/login.html
发送Ajax请求的URL:http://localhost:3006/api/login
判断两个URL是否同源,查看协议、主机地址、端口号,如果这三项都相同,则称这两个URL同源,否则非同源。
如果非同源,则以下三种行为受到限制:
- DOM无法操作
- cookie不会自动携带
- Ajax请求无效
![ima
这就属性跨域请求,即违反了同源策略的请求,就是跨域请求。
解决跨域-CORS
CORS,叫做跨域资源共享,是XHR2.0中提出的一种新的解决跨域的方案。从IE10开始支持。
CORS方案的实现,是通过服务器的响应头来实现的。
服务器要设置:Access-Control-Allow-Origin: '*或者一个具体的源'
这里直接使用第三方模块 cors 来解决跨域。
- 安装 npm i cors
- 加载 const cors = require('cors');
- 使用 app.use(cors()); // 注意,必须把这个中间件,放到最前面。
解决跨域的第二种方案,叫做JSONP方案。目前知道。
补充
跨域
同源策略
编程中的同源,比较的是两个url是否同源。
主要看下面三个方面:
- 协议是否相同(http https file)
- 主机地址是否相同(www.xxx.com 127.0.0.1)
- 端口(0~65535)(http默认端口是80;https默认端口是443;MySQL默认端口3306)
协议、主机地址、端口组成一个“源”。
如果两个url的协议、主机地址、端口都相同,那么这两个url是同源的,否则就是非同源。
如果非同源,那么以下三种行为会受到限制:
- Cookie 无法操作
- DOM 无法操作
- Ajax请求无效(请求可以发送,服务器也会处理这次请求,但是响应结果会被浏览器拦截)
违反了同源策略的请求,叫做跨域请求。
解决跨域
主流的方案有两种:分别是JSONP和CORS.
-- 扩展
jsonjson是一种与语言无关的数据交换格式,作用:
(1)使用ajax进行前后台数据交换
(2)移动端与服务器的数据交换
Json的格式与解析:
json有两种格式:
(1)对象格式:{"key1":obj,"key2".obj,"key3":obj...}
(2)数据/集合格式:[obj,obj,obj...]
客户端与服务器常用数据交换格式xml、json、html
JSONP
是程序员被迫想出来的解决跨域的方案。
JSONP方案和Ajax没有任何关系
JSONP方案只支持GET请求
JSONP没有浏览器兼容问题,任何浏览器都支持。
-
原理
- 客户端利用 script 标签的 src 属性,去请求一个接口,因为src属性不受跨域影响。
- 服务端响应数据 (我们要把它转为字符串,让他做js代码运行如果响应其他就会返回一个语法错误ayntax错误,因为浏览器和服务器是通过json通信)
- 客户端接收到字符串,然后把它当做JS代码运行。
后端接口代码:
app.get('/api/jsonp', (req, res) => { // res.send('hello'); // res.send('console.log(1234)'); // res.send('abc()') // res.send('abc(12345)') // 接收客户端的函数名 let fn = req.query.callback; let obj = { status: 0, message: '登录成功' }; let str = JSON.stringify(obj); res.send(fn + `(${str})`); });
前端代码:
<script> // 提前准备好一个函数 function xxx(res) { console.log(res) } </script> <script src="http://localhost:3006/api/jsonp?callback=xxx"></script>
对于script而言最好给 字符串的执行函数,因为script标签最后接收到的数据无法直接进行赋值
- 流程
- 服务器直接把数据返回对于script无意义,因为无法直接赋值他不知道该返回给谁
- 于是就有了让服务器直接返回一个执行函数,但是只能传入字符串数字等简单数据类型,前提前端要定义函数
- 于是就有了可执行函数参数是json格式的字符串 用es6语法模板字符串 ,用JSON的stringify方法把对象转为字符串 res.send(反引号fn(${JSON.stringify(对象)})反引号)
- 函数名写死了 于是就有了 在url后查询字符串?键=值,值是客户端定义的函数名随便但是要和自定义函数一致,键是传给服务器的参数,不用更改,类似get请求添加参数,这样在服务器端通过req.query.键 问号后的变量 就能拿到
- jsonp需要前端和后端一起配合,而且后端发送的是可执行函数参数是json字符串,函数名还是req.query.前端查询字符串的键
- 但是创建的script标签不能自动删除,jquery的jsonp就比较好能自动删除
-
前端需要做什么?
- 如果使用jQuery,$.ajax({ dataType: 'jsonp' }),必须指定dataType选项为jsonp即可
-
后端需要做什么?
- 如果使用express,那么直接调用
res.jsonp(数据)
即可。
- 如果使用express,那么直接调用
-
自定义一个jquery的jsonp
<img src="C:\Users\浪客\AppDat alt="image-20211001221724919" style="zoom:80%;" />
CORS
由于XHR对象被W3C标准化之后,提出了很多XHR Level2的新构想,其中新增了很多新方法(onload、response....)和CORS跨域资源共享。浏览器升级后开始支持CORS方案,从IE10开始支持。
CORS方案,就是通过服务器设置响应头来实现跨域。
CORS才是解决跨域的真正解决方案。
- 前端需要做什么?
- 无需做任何事情,正常发送Ajax请求即可。
- 后端需要做什么?
- 需要加响应头 。或者使用第三方模块 cors 。
小结
方案 | 前端 | 后端 |
---|---|---|
CORS | × | 设置响应头 |
JSONP(原生) | 1. 准备一个函数;2. 使用script的src发送请求 | 响应函数调用 |
JSONP(jQuery) | 1. 还是调用$.ajax();2. 必须指定dataType: 'jsonp' | res.jsonp(数据) |
res.header("Access-Control-Allow-Origin", "*");//允许所有来源访问
防抖和节流
防抖和节流,作用类似,都是为了提高项目的性能。
防抖
当事件触发之后,约定单位时间(比如1s)之后,执行里面的代码;如果在单位时间只内再次触发了事件,那么要重新计时
,以保证事件里面的代码只执行一次。
每次触发事件取消掉原来的定时器开启一个新的定时器,用定时器的底层逻辑来提高性能,前提是在指定时间内如果没有时间约定那就毫无意义,如果重复触发要取消之前的定时器,开启新的定时器
!
!
<style>
* {
margin: 0;
padding: 0;
}
#box {
width: 500px;
margin: 20px auto;
}
ul,
li {
list-style: none;
}
input {
width: 100%;
height: 26px;
line-height: 26px;
}
li:hover {
background-color: beige;
}
ul {
display: none;
}
</style>
<div id="box">
<input type="text" id="ipt">
<ul>
</ul>
</div>
<script src="./jquery.js"></script>
<script>
let timer = null;
// 当输入框的键盘弹起的时候,发送请求,获取搜索建议
$('#ipt').on('keyup', function () {
// 清楚前面的定时器
clearTimeout(timer);
// 获取输入的值(搜索关键字)
let keywords = $(this).val();
if (keywords === '') {
return $('ul').empty().hide();
}
// 如果关键字不为空,则获取搜索建议
// 约定 1s 之后发送请求
timer = setTimeout(() => {
$.ajax({
url: 'https://suggest.taobao.com/sug',
data: { q: keywords, code: 'utf-8' }, // 加入code参数,能够搜索多个汉字
dataType: 'jsonp', // JSONP请求必须加这项
success: function (res) {
// console.log(res)
let arr = [];
res.result.forEach(item => {
arr.push(`<li>${result[0]}</li>`)
})
$('ul').html(arr.join('')).show();
}
});
}, 1000);
})
</script>
节流
当事件触发之后,约定单位时间之内,事件里面的代码最多只能执行 1 次
。
所以,节流减少了单位时间内代码的执行次数,从而提高性能。
!
!
使用 timer 当做开关(节流阀)。
- 开关 打开状态(timer = null),则允许执行代码。
- 开关是关闭状态(timer = 数字),则不允许执行代码。
代码:
<style>
html,
body {
height: 100%;
}
img {
position: absolute;
}
</style>
<img src="./angel.gif" alt="">
<script src="./jquery.js"></script>
<script>
let timer = null; // null,表示节流阀打开状态,允许执行事件里面的代码
let img = $('img');
$(document).on('mousemove', function (e) {
//
console.log(111)
// 当事件触发了,判断一下,节流阀的状态,如果是关闭状态,则不允许创建另一个定时器
if (timer !== null) return;
timer = setTimeout(() => {
console.log(222);
let x = e.pageX;
let y = e.pageY;
// 设置图片的css(left和top)
img.css({ left: x + 'px', top: y + 'px' });
// 当定时器执行完毕,重新打开节流阀
timer = null;
}, 16);
});
</script>
防抖以延时去除之前的以最后一次忽略之前的,节流是稀释频率有选择性的执行,多次触发同一事件,每个一段时间只会执行一次事件,(判断现在是否有逻辑有就不执行没就可以创建执行)触发了但是逻辑没执行 节流效果好 用于鼠标事件放大镜显示局部的,搜索联想,滚动事件 现在浏览器也有优化,
ES6模块化
模块化的好处
回顾nodejs中模块化的使用
node.js 遵循了 CommonJS 的模块化规范。其中:
- 导入其它模块使用
require()
方法 - 模块对外共享成员使用
module.exports
对象
模块化的好处:
- 模块化可以避免命名冲突的问题
- 大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用
- 只需关心当前模块本身的功能开发,需要其他模块的支持时,在模块内调用目标模块即可
模块化的分类
在 ES6 模块化规范诞生之前,JavaScript 社区已经尝试并提出了AMD
、CMD
、CommonJS
等模块化规范。
但是,这些由社区提出的模块化标准,还是存在一定的差异性与局限性、并不是浏览器与服务器通用的模块化
标准,例如:
AMD
和CMD
适用于浏览器端的 Javascript 模块化CommonJS
适用于服务器端的 Javascript 模块化
太多的模块化规范给开发者增加了学习的难度与开发的成本。因此,官方的ES6 模块化规范诞生了!
为什么要学习ES6 模块化规范
ES6 模块化规范是浏览器端与服务器端通用的模块化开发规范。它的出现极大的降低了前端开发者的模块化学
习成本,开发者不需再额外学习 AMD、CMD 或 CommonJS 等模块化规范。
ES6 模块化规范中定义:
- 每个 js 文件都是一个独立的模块
- 导入其它模块成员使用
import
关键字 - 向外共享模块成员使用
export
关键字
在nodejs中使用ES6模块化
node.js 中默认仅支持 CommonJS 模块化规范,若想基于 node.js 体验与学习 ES6 的模块化语法,可以按照
如下两个步骤进行配置:
- 确保安装了
v13.0.0
或更高版本的 node.js - 在 package.json 的根节点中添加
"type": "module"
节点
![image-202101010
ES6模块语法
ES6 的模块化主要包含如下 3 种用法:
- 默认导出与默认导入
- 按需导出与按需导入
- 直接导入并执行模块中的代码
默认导出与默认导入
默认导出的语法: export default 默认导出的成员
默认导入的语法: import 接收名称 from '模块路径'
- 导出
const a = 10
const b = 20
const fn = () => {
console.log('这是一个函数')
}
// 默认导出
// export default a // 导出一个值
export default {
a,
b,
fn
}
- 导入
// 默认导入时的接收名称可以任意名称,只要是合法的成员名称即可
import result from './xxx.js'
console.log(result)
注意点:
- 每个模块中,只允许使用唯一的一次
export default
- 默认导入时的接收名称可以任意名称,只要是合法的成员名称即可
按需导入与按需导出
按需导出的语法: export const s1 = 10
按需导入的语法: import { 按需导入的名称 } from '模块标识符'
export const a = 10
export const b = 20
export const fn = () => {
console.log('内容')
}
按需导入的语法
import { a, b as c, fn } from './xxx.js'
注意事项:
- 每个模块中可以有多次按需导出
- 按需导入的成员名称必须和按需导出的名称保持一致
- 按需导入时,可以使用as 关键字进行重命名
- 按需导入可以和默认导入一起使用
直接导入模块(无导出)
如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。
此时,可以直接导入并执行模块代码,示例代码如下:
import '模块的路径'
//xxx.js
for (let i = 0; i < 10; i++) {
console.log(i)
}
// 导入该模块
import './xxx.js'
Promise
Promise能够处理异步程序。
回调地狱
![ima
JS中或node中,都大量的使用了回调函数进行异步操作,而异步操作什么时候返回结果是不可控的,如果我们希望几个异步请求按照顺序来执行,那么就需要将这些异步操作嵌套起来,嵌套的层数特别多,就会形成回调地狱 或者叫做 横向金字塔。
下面的案例就有回调地狱的意思:
案例:有 a.txt、b.txt、c.txt 三个文件,使用fs模板按照顺序来读取里面的内容,代码:
// 将读取的a、b、c里面的内容,按照顺序输出
const fs = require('fs');
// 读取a文件
fs.readFile('./a.txt', 'utf-8', (err, data) => {
if (err) throw err;
console.log(data.length);
// 读取b文件
fs.readFile('./b.txt', 'utf-8', (err, data) => {
if (err) throw err;
console.log(data);
// 读取c文件
fs.readFile('./c.txt', 'utf-8', (err, data) => {
if (err) throw err;
console.log(data);
});
});
});
案例中,只有三个文件,试想如果需要按照顺序读取的文件非常多,那么嵌套的代码将会多的可怕,这就是回调地狱的意思。
Promise简介
- Promise对象可以解决回调地狱的问题
- Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大
Promise可以理解为一个容器,里面可以编写异步程序的代码
- 从语法上说,Promise 是一个对象,使用的使用需要
new
Promise简单使用
Promise是“承诺”的意思,实例中,它里面的异步操作就相当于一个承诺,而承诺就会有两种结果,要么完成了承诺的内容,要么失败。
所以,使用Promise,分为两大部分,首先是有一个承诺(异步操作),然后再兑现结果。
第一部分:定义“承诺”
// 实例化一个Promise,表示定义一个容器,需要给它传递一个函数作为参数,而该函数又有两个形参,通常用resolve和reject来表示。该函数里面可以写异步请求的代码
// 换个角度,也可以理解为定下了一个承诺
let p = new Promise((resolve, reject) => {
// 形参resolve,单词意思是 完成
// 形参reject ,单词意思是 失败
fs.readFile('./a.txt', 'utf-8', (err, data) => {
if (err) {
// 失败,就告诉别人,承诺失败了
reject(err);
} else {
// 成功,就告诉别人,承诺实现了
resolve(data.length);
}
});
});
第二部分:获取“承诺”的结果
// 通过调用 p 的then方法,可以获取到上述 “承诺” 的结果
// then方法有两个函数类型的参数,参数1表示承诺成功时调用的函数,参数2可选,表示承诺失败时执行的函数
p.then(
(data) => {},
(err) => {}
);
完整的代码:
const fs = require('fs');
// promise 承诺
// 使用Promise分为两大部分
// 1. 定义一个承诺
let p = new Promise((resolve, reject) => {
// resolve -- 解决,完成了; 是一个函数
// reject -- 拒绝,失败了; 是一个函数
// 异步操作的代码,它就是一个承诺
fs.readFile('./a.txt', 'utf-8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data.length);
}
});
});
// 2. 兑现承诺
// p.then(
// (data) => {}, // 函数类似的参数,用于获取承诺成功后的数据
// (err) => {} // 函数类型的参数,用于或承诺失败后的错误信息
// );
p.then(
(data) => {
console.log(data);
},
(err) => {
console.log(err);
}
);
三种状态
- 最初状态:pending,等待中,new的时候此时promise的结果为 undefined;
- 当 resolve(value) 调用时,达到最终状态之一:fulfilled,(成功的)完成,此时可以获取结果value
- 当 reject(error) 调用时,达到最终状态之一:rejected,失败,此时可以获取错误信息 error
- resolve和reject只能存在一个
当达到最终的 fulfilled 或 rejected 时,promise的状态就不会再改变了。
同步异步?
- new Promise是同步执行的,promise内部的代码默认是同步的
- 获取结果时(调用 resolve 触发 then/catch方法时)是异步的
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve();
console.log(3);
}).then(res => {
console.log(4);
})
console.log(5);
// 输出顺序: 1 2 3 5 4 ,因为只有 .then() 是异步的
then方法的链式调用
-
前一个then里面返回的字符串,会被下一个then方法接收到。但是没有意义;
-
前一个then里面返回的Promise对象,并且调用resolve的时候传递了数据,数据会被下一个then接收到
-
前一个then里面如果没有调用resolve,则后续的then不会接收到任何值
const fs = require('fs'); // promise 承诺 let p1 = new Promise((resolve, reject) => { fs.readFile('./a.txt', 'utf-8', (err, data) => { err ? reject(err) : resolve(data.length); }); }); let p2 = new Promise((resolve, reject) => { fs.readFile('./b.txt', 'utf-8', (err, data) => { err ? reject(err) : resolve(data.length); }); }); let p3 = new Promise((resolve, reject) => { fs.readFile('./c.txt', 'utf-8', (err, data) => { err ? reject(err) : resolve(data.length); }); }); p1.then(a => { console.log(a); return p2; }).then(b => { console.log(b); return p3; }).then(c => { console.log(c) }).catch((err) => { console.log(err); });
catch 方法可以统一获取错误信息
封装按顺序异步读取文件的函数
function myReadFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf-8', (err, data) => {
err ? reject(err) : resolve(data.length);
})
});
}
myReadFile('./a.txt')
.then(a => {
console.log(a);
return myReadFile('./b.txt');
})
.then(b => {
console.log(b);
return myReadFile('./c.txt');
})
.then(c => {
console.log(c)
})
.catch((err) => {
console.log(err);
});
- 用promise 对象承诺 封装一个异步程序 解决了回调地狱,打破异步操作的结果 必须在回调函数中
- 此时没有返回值 需要通过promise对象的then方法或者catch方法俩调用返回结果
- 此时执行多个异步操作要创建多个promise对象并调用then方法
- 此时不能按顺序执行异步操作,用then方法返回值,返回的是promise对象自身,需要return指定返回对象才能实现顺序操作异步,then方法返回的只能是promise对象返回字符串无意义,而且必须是调用了resolve有数据参数,才能正常通信 很像中间件的next
- 封装promise对象的创建 then的时候直接调用如读取文件函数传入参数
- promise开发:调用某个方法直接返回一个promise对象不需要手动创建
使用第三方模块读取文件
- npm init - y
- npm i then-fs 安装then-fs模块
- then-fs 将 内置的fs模块封装了,读取文件后,返回 Promise 对象,省去了我们自己封装
- 修改 package.json ,添加 "type": "module" 表示使用ES6的模块化语法
import fs from 'then-fs';
fs.readFile('./files/a.txt', 'utf-8')
.then(res1 => {
console.log(res1);
return fs.readFile('./files/b.txt', 'utf-8')
})
.then(res2 => {
console.log(res2);
return fs.readFile('./files/b.txt', 'utf-8')
})
.then(res3 => {
console.log(res3)
})
async 和 await 修饰符
ES6 --- ES2015
async 和 await 是 ES2017 中提出来的。
异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题。
从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底。它们都有额外的复杂性,都需要理解抽象的底层运行机制。
异步I/O不就是读取一个文件吗,干嘛要搞得这么复杂?异步编程的最高境界,就是根本不用关心它是不是异步。
async 函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案。
ES2017提供了async和await关键字。await和async关键词能够将异步请求的结果以返回值的方式返回给我们。
- async 用于修饰一个 function
- async 修饰的函数,总是返回一个 Promise 对象
- 函数内的所有值,将自动包装在 resolved 的 promise 中(return 返回值会被then方法获取 不用resolve了,then方法的参数由return返回值决定)
- await 只能出现在 async /异步函数内,后跟promise对象,,停止自身代码执行后面的代码,让await后的代码先暂停了函数的执行(第一个不出值后面的就不能获取到值,一致卡着,这不是同步而是截断),不会影响同步任务,虽然是同步书写但是核心是异步执行代码
- await 让 JS 引擎等待直到promise完成并返回结果
- 语法:let value = await promise对象; // 要先等待promise对象执行完毕,才能得到结果
- 由于await需要等待promise执行完毕,所以await会暂停函数的执行,但不会影响其他异步任务
- await返回的是promise对象中的then方法的回调函数参数
- 对于错误处理,可以选择在async函数后面使用
.catch()
或 在promise对象后使用.catch()
- async负责包装 await负责取值 对于捕获错误这一块,用try包裹异步代码踹他一下,catch函数在后做错误数据操作 ,还有就是 成功和失败都执行的是finally函数
const fs = require('fs');
// 将异步读取文件的代码封装
function myReadFile (path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf-8', (err, data) => {
err ? reject(err) : resolve(data.length);
});
}).catch(err => {
console.log(err);
});
}
async function abc () {
let a = await myReadFile('./a.txt');
let b = await myReadFile('./b.txt');
let c = await myReadFile('./c.txt');
console.log(b);
console.log(a);
console.log(c);
}
abc();
宏任务和微任务、事件循环
JavaScript是单线程的,也就是说,同一个时刻,JavaScript只能执行一个任务,其他任务只能等待。
为什么JavaScript是单线程的
js是运行于浏览器的脚本语言,因其经常涉及操作dom,所以设置为单线程操作。如果是多线程的,也就意味着,同一个时刻,能够执行多个任务。试想,如果一个线程修改dom,另一个线程删除dom,那么浏览器就不知道该先执行哪个操作。所以js执行的时候会按照一个任务一个任务来执行。
多线程实现多任务,异步也能实现多任务
为什么任务要分为同步任务和异步任务
试想一下,如果js的任务都是同步的,那么遇到定时器、网络请求等这类型需要延时执行的任务会发生什么?
页面可能会瘫痪,需要暂停下来等待这些需要很长时间才能执行完毕的代码
所以,又引入了异步任务。
- 同步任务:同步任务不需要进行等待可立即看到执行结果,比如console,性能快,不会有高计算,耗时操作
- 异步任务:异步任务需要等待一定的时候才能看到结果,比如setTimeout、网络请求,晚于同步任务但是 高计算,效率快
- 性能锁(了解)
宏任务和微任务
异步任务,又可以细分为宏任务和微任务。下面列举目前学过的宏任务和微任务。
任务(代码) | 宏/微 任务 | 环境 |
---|---|---|
script | 宏任务 | 浏览器 |
事件 | 宏任务 | 浏览器 |
网络请求(Ajax) | 宏任务 | 浏览器 |
setTimeout() 定时器 | 宏任务 | 浏览器 / Node |
fs.readFile() 读取文件 | 宏任务 | Node |
Promise.then() | 微任务 | 浏览器 / Node |
他们的执行过程是怎样的呢?
- 每次执行栈里的代码就是一个宏任务
- 同步任务和ajax 事件 定时器
比如去银行排队办业务,每个人的业务就相当于是一个宏任务;
比如一个人,办的业务有存钱、买纪念币、买理财产品、办信用卡,这些就叫做微任务。
微任务就是宏任务的一部分 在同步代码执行完才能执行,同步之前下一个微任务之后
promise.then() process.nextTick(Node.js环境)
微任务比宏任务节省资源,优先执行其他异步任务执行很快,小个子往前站
!
执行顺序
![image-202104181
事件循环(Event Loop)
事件循环比较简单,它是一个在 "JavaScript 引擎等待任务","执行任务"和"进入休眠状态等待更多任务"这几个状态之间转换的无限循环。
引擎的一般算法:
- 当有任务时:
- 从最先进入的任务开始执行。
- 休眠直到出现任务,然后转到第 1 步。
面试题分析
- 1
console.log(1)
setTimeout(function() {
console.log(2)
}, 0)
const p = new Promise((resolve, reject) => {
resolve(1000)
})
p.then(data => {
console.log(data)
})
console.log(3)
- 2
console.log(1)
setTimeout(function() {
console.log(2)
new Promise(function(resolve) {
console.log(3)
resolve()
}).then(function() {
console.log(4)
})
})
new Promise(function(resolve) {
console.log(5)
resolve()
}).then(function() {
console.log(6)
})
setTimeout(function() {
console.log(7)
new Promise(function(resolve) {
console.log(8)
resolve()
}).then(function() {
console.log(9)
})
})
console.log(10)
- 3
console.log(1)
setTimeout(function() {
console.log(2)
}, 0)
const p = new Promise((resolve, reject) => {
console.log(3)
resolve(1000) // 标记为成功
console.log(4)
})
p.then(data => {
console.log(data)
})
console.log(5)
- 4
new Promise((resolve, reject) => {
resolve(1)
new Promise((resolve, reject) => {
resolve(2)
}).then(data => {
console.log(data)
})
}).then(data => {
console.log(data)
})
console.log(3)
- 5
setTimeout(() => {
console.log(1)
}, 0)
new Promise((resolve, reject) => {
console.log(2)
resolve('p1')
new Promise((resolve, reject) => {
console.log(3)
setTimeout(() => {
resolve('setTimeout2')
console.log(4)
}, 0)
resolve('p2')
}).then(data => {
console.log(data)
})
setTimeout(() => {
resolve('setTimeout1')
console.log(5)
}, 0)
}).then(data => {
console.log(data)
})
console.log(6)
- 6
<script>
console.log(1);
async function fnOne() {
console.log(2);
await fnTwo(); // 右结合先执行右侧的代码, 然后等待
console.log(3);
}
async function fnTwo() {
console.log(4);
}
fnOne();
setTimeout(() => {
console.log(5);
}, 2000);
let p = new Promise((resolve, reject) => { // new Promise()里的函数体会马上执行所有代码
console.log(6);
resolve();
console.log(7);
})
setTimeout(() => {
console.log(8)
}, 0)
p.then(() => {
console.log(9);
})
console.log(10);
</script>
<script>
console.log(11);
setTimeout(() => {
console.log(12);
let p = new Promise((resolve) => {
resolve(13);
})
p.then(res => {
console.log(res);
})
console.log(15);
}, 0)
console.log(14);
</script>
ES6降级处理(了解)
因为 ES 6 有浏览器兼容性问题,可以使用一些工具进行降级处理,例如:babel
-
降级处理 babel 的使用步骤
- 安装 Node.js
- 命令行中安装 babel
- 配置文件
.babelrc
- 运行
-
项目初始化 (项目文件夹不能有中文)
npm init -y
-
在命令行中,安装 babel babel官网
npm install @babel/core @babel/cli @babel/preset-env
-
配置文件
.babelrc
(手工创建这个文件)babel 的降级处理配置
{ "presets": ["@babel/preset-env"] }
-
在命令行中,运行
# 把转换的结果输出到指定的文件 npx babel index.js -o test.js # 把转换的结果输出到指定的目录 npx babel 包含有js的原目录 -d 转换后的新目录
身份认证(了解)
开发模式
- 传统的服务端渲染模式
- 后端的接口和前端的代码在一起(服务器)
- 涉及不到跨域
- 有利于SEO
- 客户端(前端)不需要渲染数据,如果是手机,将会非常省电。运行速度非常快。
- 缺点是开发效率低。
- 适合使用 cookie 或 session 身份认证
- 新型的前后端分离模式
- 前端代码单独在一个文件夹(服务器)(自己的电脑上)
- 后端的接口在另外的文件夹(服务器)(刘龙宾老师的服务器上)
- 开发速度快,适合多人协作开发。
- 适合使用 JWT(json web token) 方式的身份认证。
PS:一个项目到底该使用哪种开发模式?
- 不能一概而论,比如有的网站,首页为了SEO采用传统的服务端渲染模式,其他页面采用前后端分离模式。
- 后台管理系统,涉及不到SEO,可以采用前后端分离模式。
- 小型企业网站,可以采用传统的服务端渲染模式。
演示传统的服务端渲染模式
-
最大的特点
-
服务端代码和前端代码在同一个服务器(文件夹)
-
搭建服务器
| - app.js (搭建服务器) | - public (public文件夹用于存放前端页面) | - index.html (一个前端的html页面)
-
app.js编写接口
/index.html
- 接口中,使用fs读取文件,并替换内容,最后响应给客户端
- 客户端请求
http://localhost:3006/index.html
// 接口,提供index.html 页面
app.get('/index.html', (req, res) => {
// 客户端发来请求,希望看到index.html 页面。
// 服务器,把html页面读取出来,把读取的结果响应给客户端即可
fs.readFile('./public/index.html', 'utf-8', (err, data) => {
if (err) throw err;
// console.log(data);
// 假设从数据库中查询到了标题和内容
data = data.replace('{{title}}', '咏鹅');
data = data.replace('{{content}}', '鹅鹅鹅,曲项向天歌')
res.send(data);
})
});
页面中的数据,是在服务端完成渲染的,客户端接收到的已经是一个包含数据的完整页面了,所以叫做服务端渲染模式。
Cookie
原理图
身份认证,要完成的是:不登录,不允许访问其他页面。
实现身份认证
- 搭建基础的服务器(或者直接使用前面的 传统服务端渲染模式 代码)
- 中间件配置 cookie-parser
app.use(cookieParser())
- 模拟一个登录接口
- 如果登录成功,设置cookie。
res.cookie('key', 'value', 配置项);
- 如果登录成功,设置cookie。
- /index.html 接口中,根据cookie判断是否登录,从而完成身份认证
详见代码
优缺点
- 优点
- 体积小
- 客户端存放,不占用服务器空间
- 浏览器会
自动
携带,不需要写额外的代码,比较方便
- 缺点
- 客户端保存,安全性较低。但可以存放加密的字符串来解决
- 只能存字符串,cookie的大小也是有限制的
- 可以实现跨域,但是难度大,难理解,代码难度高
- 不适合前后端分离式的开发
适用场景
- 传统的服务器渲染模式
- 存储安全性较低的数据,比如视频播放位置等
Session身份认证
原理图
要实现的效果:不登录,不允许访问其他接口
实现身份认证
-
搭建基础的服务器
- 下载安装第三方模块
express
和express-session
- 创建app.js
- 加载所需模块
const express = require('express');
const session = require('express-session');
- 下载安装第三方模块
-
中间件配置 session
app.use(session({ secret: 'adfasdf', // 这个随便写 saveUninitialized: false, resave: false }));
-
完成登录接口
-
如果登录成功,使用session记录用户信息。
req.session.isLogin = true; req.session.username = 'laotang';
-
-
/index.html 接口中,根据session判断是否登录,从而完成身份认证
详见代码
优缺点
- 优点
- 服务端存放,安全性较高
- 浏览器会自动携带cookie,不需要写额外的代码,比较方便
- 适合服务器端渲染模式
- 缺点
- 会占用服务器端空间
- session实现离不开cookie,如果浏览器禁用cookie,session不好实现
- 不适合前后端分离式的开发
适用场景
- 传统的服务器渲染模式
- 安全性要求较高的数据可以使用session存放,比如用户私密信息、验证码等
易错点
路由和中间件
-
// 接收post参数 用let 未来要加密 let {username,password}=req.body
-
验证登录 两种方式 ,根据用户名查询数据,差不到就是用户信息错误,查到了比对密码 第二种根据用户名和查询数据,查不到登录失败,查到了登录成功
-
基于Token的身份验证方法 客户端使用用户名和密码请求登录 服务端收到请求,验证登录是否成功 验证成功后,服务端会返回一个Token给客户端,反之,返回身份验证失败的信息 客户端收到Token后把Token用一种方式存储起来,如( cookie / localstorage / sessionstorage / 其他 ) 客户端每次发起请求时都会将Token发给服务端 服务端收到请求后,验证Token的合法性,合法就返回客户端所需数据,反之,返回验证失败的信息
-
Token的特点 · 随机性:每次的token都是不一样的 · 不可预测性:没有规律,无法预测 · 时效性: 可以设置token的有效时间 · 无状态、可扩展:由于只是一个算法,扩展起来非常方便
-
JWT标准的Tokens由三部分组成 header:包含token的类型和加密算法 payload:包含token的内容 signature:通过密钥将前两者加密得到最终的token
-
第一个参数 对象 要生成token的主题信息【这里可以包含用户的一些相关信息,content需要为一个对象,否则有可能会报错】
-
字符串 加密的key(密钥或私钥)
-
通常HTTP URL的格式是这样的:http://host[:port][path] http表示协议。host表示主机。port为端口,可选字段,不提供时默认为80。path指定请求资源的URI(Uniform Resource Identifier,统一资源定位符),如果URL中没有给出path,一般会默认成“/”(通常由浏览器或其它HTTP客户端完成补充上)。
-
Express里有个中间件(middleware)的概念。所谓中间件,就是在收到请求后和发送响应之前这个阶段执行的一些函数。
-
路由实质就是:如何处理HTTP请求中的路径部分 客户端和服务器的交互映射 app.get()、app.post()一条一条的配置,不过对于需要处理大量路由的网站来讲,这会搞出人命来的。所以呢,我们实际开发中需要结合路由参数(query string、正则表达式、自定义的参数、post参数)来减小工作量提高可维护性。路由有一级路由二级路由三级路由……
-
// app.use()不管路由操作是get和post(所有请求)都会触发app.use()通过app.use()挂载路由对象 忽略请求方式 指定访问路径(前缀为当前接口服务 ,没有前缀表示可以处理所有的请求,可以处理多个接口 /abcd 、 /abd 这两个接口app.use('/abc?d', 中间件函数);?表示有或者没有接口都行)本质是路由中请求req绑定了两个事件data和 end 事件,app.use()帮我们封装了 //app.use(express.urlencoded({ extended: false })) // 导入路由模块 const loginRouter = require('./router/login.js') // 挂载路由 他会忽略路由方式 可以设置前缀 进行拦截 app.use('/api', loginRouter) 这个参数挂载中间件 ,
-
// 通过缓存导入express
// 用到了加密密码模块和mysql模块
// 注意路径
// 不能创建express实例,会被覆盖,但是可以创建路由的实例对象
const router=express.Router()
// 向路由模块绑定接口 路径和处理函数
router.post('/reguser',(req,res)=>{
// 路由资源地址 url 有同一前缀 在app挂载的时候设置
module.exports = router
- 中间件路由 post 无校验时必须写个字符串填充数据
- 路由是最简单的中间件 无next 直接响应客户端
- 中间件必须是一个函数
- 中间件函数至少有 2 个参数,最多有 4 个参数
如果传递了 2 个参数,那么肯定是 req 和 res
如果传递了 3 个参数,那么肯定是 req、res、next
如果传递了 4 个参数,错误处理中间件那么肯定是 err、req、res、next 一般放到最后 - 中间件必须调用next()才能向下执行中间件或路由,所以中间件严格区分前后顺序需要加 小括号
- next(err) 调用时,传递了参数,这时会跳过中间所有中间件,直接进入第一个错误处理中间件(如果有的话)如果没有错误处理中间件,则会抛出这个错误(在控制台显示错误信息)
给next(err) 传递了参数,本身的意义就是表示这里发生了错误,需要把错误信息传递给错误处理中间件 - // app只能有一个 listen只能监听一个
- 模块化路由就是把路由操作写到一个单独的文件中,导入主文件进行使用,方便维护和复用和可读性,即定向抽离。不能express实例因为在主文件有了实例app,服务器无法监听多个app实例,所以在这里创建路由模块的实例, 暴露导入挂载。代码量不会少
- app.use(要挂载的路由模块)所有请求方式都会被中间的路由模块处理 通俗理解app.use()帮我们忽略所有的请求方式,是一个集合,但是可以指定访问路径的前缀,更好的管理映射,注册中间件并使用,自动调用,参数可以是中间件函数或者路由实例对象
- 一般是二级路由
- 路由是最后一个中间件,直接响应给客户
- 中间件(Middleware ),特指业务流程的中间处理环节,是一个函数
- // 想要让req.body接收到请求体参数,需要配置一行代码
app.use(express.urlencoded({ extended: false }));
express.urlencoded({ extended: false })才是中间件,解析post请求参数,挂载到body中 req.body,req的data事件和end事件,默认不配置解析请求体res.body返回undefined
extended: false:表示使用系统模块querystring来处理,也是官方推荐的
extended: true:表示使用第三方模块qs来处理
-
use对路径的要求是模糊匹配,只要书写的url路径以要求的路径开头,那么就算匹配,而get或post对url路径要求是,书写路径必须与要求的路径一致才算匹配
-
token认证通过会存储在req的user中是一个对象
-
app.use(中间件函数)定义一个全局生效的中间件,客户端发起任何请求都会触发
-
除了错误级别的中间件其他都写在所有路由之前,作用:抽离共有的功能上游中间件统一添加req和res属性和方法下游中间件可以使用,共享同一个req和res
-
// next(err)方式 必须加return
// return next(new Error('用户名必须是6到10位非空白字符'))
// 抛出错误 当错误抛出停止后面的代码
throw new Error('用户名必须是6到10位非空白字符') -
测试token验证成功的时候添加headers,Authorization 值为token 必须要有Bearer后面的空格 不要有引号
-
err错误中文 抛出的是err对象,对象才能使用errnamemessage
app.use((err, req, res, next) => {
// 错误返回中文 No authorization token was found err有name和message两个属性
console.log(err.name);
if (err.name === 'UnauthorizedError') {
err.message = '身份认证失败!'
}
res.send({
status: 0,
message: err.message
})
}) -
模块化路由就是把路由操作写到一个单独的文件中,导入主文件进行使用,方便维护和复用和可读性,即定向抽离。不能express实例因为在主文件有了实例app,服务器无法监听多个app实例,所以在这里创建路由模块的实例, 暴露导入挂载。代码量不会少
-
app.use(要挂载的路由模块)所有请求方式都会被中间的路由模块处理 通俗理解app.use()帮我们忽略所有的请求方式,是一个集合,但是可以指定访问路径的前缀,更好的管理映射,注册中间件并使用,自动调用,参数可以是中间件函数或者路由实例对象
-
一般是二级路由
-
路由是最后一个中间件,直接响应给客户
-
中间件(Middleware ),特指业务流程的中间处理环节,是一个函数
-
// 想要让req.body接收到请求体参数,需要配置一行代码
app.use(express.urlencoded({ extended: false }));
express.urlencoded({ extended: false })才是中间件,解析post请求参数,挂载到body中 req.body,req的data事件和end事件,默认不配置解析请求体res.body返回undefined
extended: false:表示使用系统模块querystring来处理,也是官方推荐的
extended: true:表示使用第三方模块qs来处理
-
use对路径的要求是模糊匹配,只要书写的url路径以要求的路径开头,那么就算匹配,而get或post对url路径要求是,书写路径必须与要求的路径一致才算匹配
-
token认证通过会存储在req的user中是一个对象 req.user.id一般用来获取当前用户的id
-
app.use(中间件函数)定义一个全局生效的中间件,客户端发起任何请求都会触发
-
除了错误级别的中间件其他都写在所有路由之前,作用:抽离共有的功能上游中间件统一添加req和res属性和方法下游中间件可以使用,共享同一个req和res
-
// next(err)方式 必须加return
// return next(new Error('用户名必须是6到10位非空白字符'))
// 抛出错误 当错误抛出停止后面的代码
throw new Error('用户名必须是6到10位非空白字符')
-
测试token验证成功的时候添加headers,Authorization 值为token 必须要有Bearer后面的空格 不要有引号
-
err错误中文
app.use((err, req, res, next) => {
// 错误返回中文 No authorization token was found err有name和message两个属性
console.log(err.name);
if (err.name === 'UnauthorizedError') {
err.message = '身份认证失败!'
}
res.send({
status: 0,
message: err.message
})
})
- throw new Error('用户名或者密码错误')抛出了一个错误信息为用户名或者密码错误的错误,默认无参数是Error
- delete results[0].password //删除密码不显示 delete 可以删除对象的某个属性
- data:results[0] //results返回的是数组,取出对象
- 参数 都是用来获取请求参数的
// 1. post请求体参数需要配置中间件 req.body 只适合post
// 2. 路由参数/动态参数在路由中/:id/:password req.params 是一个对象 和get和post没有关系写在了路由url中但。适合get和post
// 3.get/查询参数 ?id=1&&password='123' req.query 最基本的 适合post和get
-
sql语句and用于条件连接,逗号用于数据连接
-
定时器可以不做判断,不存在定时器清除定时器不报错
防抖和节流事件可以触发但是不会执行逻辑
定时器返回数值模块化是代码共享机制体现
模块化规范AMD 和 CMD 适用于浏览器端的 Javascript 模块化 CommonJS 适用于服务器端的 Javascript
node.js 遵循了 CommonJS 的模块化规范。其中:导入其它模块使用 require()方法 模块对外共享成员使用 module.exports 对象
es6 每个js文件都是一个独立的模块 导入import 导出 export node13版本之前不支持es模块化 (前缀和后缀都不能省略)
模块化要在package.json文件中添加 "type":"moduel" 配置之后,则只能使用ES6模块化语法,不能再使用CommonJS语法了
第一种 默认导入导出 import 接收名称 from “路径” 核心模块可以省略前后缀 导入时不支持解构赋值只能是一个接收名 export default 默认导出的成员 每个模块中,只允许使用唯一的一次 export default
第二种 按需导入与按需导出 按需导入可以和默认导入一起使用 必须是{} import {} from "路径" 支持结构 export 导出 fn as ss as起别名 但是fn就不能用了
第三种 如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员export 存不存在都无所谓import '模块的路径' 使用所有文件 css js less
尽量不要混用容易报错
nodejs不支持promise开发(fs)要封装 现在有axios mysql 和then-fs
new promise()对象 方法里默认的是同步代码 有异步定时器就是定时器就是异步
then/catch 方法里面默认的是异步代码
本文来自博客园,作者:jialiangzai,转载请注明原文链接:https://www.cnblogs.com/zsnhweb/articles/16204040.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异