Node.js
一、Node.js基础
1. 认识Node.js
Node.js是一个javascript运行环境。它让javascript可以开发后端程序,实现几乎其他后端语言实现的所有功能,可以与PHP、Java、Python、.NET、Ruby等后端语言平起平坐。
Nodejs是基于V8引擎,V8是Google发布的开源JavaScript引擎,本身就是用于Chrome浏览器的js解释部分,但是Ryan Dahl 这哥们,鬼才般的,把这个V8搬到了服务器上,用于做服务器的软件。
可以使用
node 路径文件
指令来执行给定路径下的指定js文件
01, nodejs的特性
- Nodejs语法完全是js语法,只要你懂js基础就可以学会Nodejs后端开发
- NodeJs超强的高并发能力,实现高性能服务器
- 开发周期短、开发成本低、学习成本低
- 可以作为后端,向前端返回
html、json数据、jsonp数据
等,还可以作为中间代理服务器,发送请求返回数据
02 ,使用 Node.js 需要了解多少 JavaScript
http://nodejs.cn/learn/how-much-javascript-do-you-need-to-know-to-use-nodejs
03 ,浏览器环境vs node环境
五大浏览器内核:
- IE浏览器——
Trident
内核- FirFox火狐——
Gecko
内核- Safari苹果——
webkit
内核- Chrome谷歌——
Blink
内核(webkit
分支)- Opera欧朋——
Blink
内核
谷歌浏览器的内核和引擎
V8
引擎 ==> 解析JSblink
内核 ==> 解析HTML/CSS- node摘除了谷歌浏览器的
HTML/CSS
和Blink
引擎,是一个专门运行Js
代码的运行环境
- 因为node脱离了浏览器、html、css,所以是不可以进行相关操作的,否则会报错
console.log(document)
,打印dom节点,报错
Node.js 可以解析JS代码(没有浏览器安全级别的限制)提供很多系统级别的API,如:
-
文件的读写 (File System)
const fs = require('fs') fs.readFile('./ajax.png', 'utf-8', (err, content) => { console.log(content) })
-
进程的管理 (Process)
function main(argv) { console.log(argv) } main(process.argv.slice(2))
-
网络通信 (HTTP/HTTPS)
const http = require("http") http.createServer((req,res) => { res.writeHead(200, { "content-type": "text/plain" }) res.write("hello nodejs") res.end() }).listen(3000)
2. 开发环境搭建
http://nodejs.cn/download/
3. 模块、包、commonJS
为什么要进行模块化?
- 把所有代码写在一个
js
文件中可以吗?- 如果在一个页面中引入
a、b、c
三个js
文件,它们有同名的方法或变量怎么办?会不会冲突或覆盖?- 如果a文件中有一个方法,
a
和b
可以用,但是不想让c
用怎么办?- 如果
b
用了a
的方法,b.js
文件是不是要在a.js
文件后面引入?
如果不使用模块化:
- 将代码全部写入一个js文件中,代码臃肿,耦合度高,不方便维护
- 多个js文件中,可能会用相同名称的方法或变量,会造成命名冲突,覆盖
- 多个js文件之间如果有依赖关系,例如
b.js
使用了a.js
文件中的fn
方法,则b
文件就一定要在a
文件引入后才能引入,否则找不到使用的fn
方法
模块化开发:将js代码分为多个模块,一个js文件就是一个模块,然后将这些js文件代码组合起来使用
- 将js代码放在多个js文件中,避免代码臃肿,耦合度低,方便维护
- 各个模块的变量方法之间不会有命名冲突
- 在模块中
定义
变量或方法- 然后导出
暴露
出去- 谁需要使用,谁就
引入
,不引入无法使用
01 CommonJS规范
CommonJS
规范是一套规范,我们常用的只是CommonJS
中的一个子规范node.js
和webpack
遵循了CommonJS
的规范,所以我们可以在node.js、webpack
中使用CommonJS
规范- 浏览器就没有遵循
CommonJS
规范,所以不可以在浏览器中使用CommonJS
规范
02 modules模块化规范写法
导出语法:
module.exports
导出:
module.exports = X
,导出一个变量或方法module.exports = {X,Y}
,导出对象,内置N个变量或方法module.exports
导出方式,每个文件中只能使用一次,否则会造成覆盖exports
导出:
exports.X = X
,导出对象,内置N个变量或方法exports
导出方式,在一个页面可以使用多次,用于导出多个变量或方法require('path')
导入:
- 如果目标路径的文件,导出对象,那我们引入的就是一个对象,要以对象的形式使用它内部的变量或方法
- 如果目标路径的文件,导出一个值,那我们就可以直接把它当成变量或方法使用
- 总结:
exports
导出的一定是对象module.exports
导出的可能是对象,可能是变量或方法,取决于等号后面的值
我们可以把公共的功能 抽离成为一个单独的 js 文件 作为一个模块,默认情况下面这个模块里面的方法或者属性,外面是没法访问的。如果要让外部可以访问模块里面的方法或者属性,就必须在模块里面通过 exports
或者 module.exports
暴露属性或者方法。然后谁需要使用,通过 require('path')
引入后才能使用。
m1.js:
const name = 'gp19'
const sayName = () => {
console.log(name)
}
console.log('module 1')
// 接口暴露方法一:
module.exports = {
say: sayName
}
// 或直接
module.exports = sayName;
// 接口暴露方法二:
exports.say = sayName;
// 错误!
exports = {
say: sayName
}
main.js:
const m1 = require('./m1')
m1.say()
03 在Node中使用ES6模块语法
- 我们知道,正常情况下Node.js中是无法执行ES6的模块化语法的,但是通过修改
package.json
文件中的type
属性可以实现。 package.json
文件中的type
属性默认为CommonJS
,修改为module
即可- 然后就可以在
Node
环境下执行使用了ES6
语法的js
代码 - 注意,这种情况下,引入时,路径中文件的
.js
文件后缀不可省略 - 但是这种情况下,就只能使用
ES6
了,不存在两种规范混合使用
4. Npm&Yarn
模块分类:
- 自定义模块,就是我们自己定义的js文件,通过导入导出使用
- Node内置模块,可以直接使用
- 第三方模块,别人上传到网络库中的模块
- 使用
npm
或yarn
包管理工具来下载它们并使用npm
包管理工具是Node
内置的,有了Node
就可以直接使用
01 npm的使用
- 初始化:
npm init
,初始化package.json
文件,用来记录我们都下载了那些模块包
package.json
文件,记录了我们的初始化信息,和模块下载记录package-lock.json
文件,版本锁定,记录了
- 下载模块包的版本
- 下载的模块包还依赖了哪些模块包,以及它们的版本
- 因为模块包版本一旦变化,兼容性和功能就可能出现变化,锁定各个依赖包的版本,保证每个参与者的下载的模块包版本一致
- 下载:
npm i 模块名 --save
,下载指定的模块包
- 保存在生产环境
--S
npm i 模块名@版本
,模块名后跟@版本号
下载指定版本- 下载的模块包会记录在
dependencies
下,代表保存在生产上线版本下的依赖- 简写:
npm i 模块名
或npm i 模块名 --S
npm i 模块名 --save--dev
- 保存在开发环境
--D
- 下载指定的模块包,并记录在
devDependencies
下,代表保存在开发版本下的依赖,例如测试框架、构建工具等工具,只在开发时需要,上线后不需要,就使用--save--dev
- 如果该模块包已经记录在
dependencies
下,执行该命令就会被切换到devDependencies
下- 简写
npm i 模块名 --D
npm i
,下载项目所需的所有目录- 卸载:
npm uninstall 模块名
,卸载该模块包- 更新:
npm update 模块名
,更新模块包版本- 查看:
npm list
,显示该目录下,下载的所有模块包
npm list -g
,显示电脑上下载的所有的模块包npm info 模块名
,展示该模块的信息
npm init
npm install 包名 –g (uninstall,update)
npm install 包名 --save-dev (uninstall,update)
npm list -g (不加-g,列举当前目录下的安装包)
npm info 包名(详细信息) npm info 包名 version(获取最新版本)
npm install md5@1(安装指定版本)
npm outdated( 检查包是否已经过时)
"dependencies": { "md5": "^2.1.0" } ^ 表示 如果 直接npm install 将会 安md5
2.*.* 最新版本
"dependencies": { "md5": "~2.1.0" } ~ 表示 如果 直接npm install 将会 安装md5 2.1.* 最新版本
"dependencies": { "md5": "*" } * 表示 如果 直接npm install 将会 安装 md5 最新版本
02 全局安装 nrm
nrm
:用于切换模块库的下载地址,使用速度快的资源库,减少下载时间
- 全局安装:
npm install -g nrm
- 查看地址:
nrm ls
- 切换地址:
nrm use 地址名
- 测试速度:
nrm test
NRM (npm registry manager)是npm的镜像源管理工具,有时候国外资源太慢,使用这个就可以快速地在 npm 源间切换。
手动切换方法: npm config set registry https://registry.npm.taobao.org
安装 nrm
在命令行执行命令,npm install -g nrm,全局安装nrm。
使用 nrm
执行命令 nrm ls 查看可选的源。 其中,带*的是当前使用的源,上面的输出表明当前源是官方源。
切换 nrm
如果要切换到taobao源,执行命令nrm use taobao。
测试速度
你还可以通过 nrm test 测试相应源的响应时间。
nrm test
npm install -g cnpm --registry=https://registry.npmmirror.com
03 yarn使用
yarn
和npm
比较:
- 都是包管理工具
- 速度更快,
yarn
缓存过的包不需要重新下载,而且是并行下载,所以下载多个包时,速度更快- 安全性好,
yarn
在执行代码前,会先验证包的完整性,不怕丢包yarn
在包下载好后,就会锁定版本,npm
就是学的yarn
使用
yarn
:
- 初始化:
yarn init
- 生成
package.json
文件记录下载的模块包- 生成
yarn.lock
锁定下载的模块包的版本,并锁定下载模块包依赖的其它模块包的版本- 下载包:
yarn add 模块名
yarn add 模块名@版本
,模块名后跟@版本号
下载指定版本- 默认记录在
dependencies
,代表生产版本依赖yarn add 模块名 --dev
,记录在devdependencies
,代表仅在开发版本使用的依赖yarn install
,下载项目所需的所有依赖包,注意:yarn
的install
不能缩写为i
- 升级包:
yarn uograde 模块名@版本
- 卸载包:
yarn remove 模块名
- 注意:要在
dependencies
和devDependencies
之间切换记录位置
- 使用
npm
时,只需要再通过后缀--D
和--S
的方式直接再下载一次,就可以完成切换记录位置- 使用
yarn
时,就需要现将已有的模块包卸载掉,再重新下载记录到指定位置
npm install -g yarn
对比npm:
速度超快: Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。
超级安全: 在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。
开始新项目
yarn init
添加依赖包
yarn add [package]
yarn add [package]@[version]
yarn add [package] --dev
升级依赖包
yarn upgrade [package]@[version]
移除依赖包
yarn remove [package]
安装项目的全部依赖
yarn install
npm 和 yarn 的 保存环境
- 默认都是
--save
保存至生产环境--save--dev
,保存在开发环境- 带
dev
的是保存在开发测试环境,代表仅是在开发过程中需要的依赖,比如一些测试、构建工具- 反之,则是保存在生产环境
--save--dev
==>--D
,开发环境--save
==>--S
,生产环境
5. 内置模块
1,http模块
http模块可以用来创建服务器
要使用 HTTP 服务器和客户端,则必须require('http')
。
- 首先引入
http
内置模块
var http = require('http')
- 利用
http
模块创建服务器
- 创建服务器
var server = http.createServer()
- 监听服务器请求事件
- 监听服务器请求
server.on('request',(req,res)=>{})
req
就是请求传递的参数res
就是返回渲染的内容- 与创建服务器简写在一起:
http.createServe((req,res)=>{...})
- 监听服务器状态
server.listen(3000,()=>{})
http
的res
类中 的方法和属性
res.statusCode
, 设置响应状态码,200、404、500
res.statusCode = 200
res.statusMessage
,设置状态码信息 ,OK、Not Found
,会根据statusCode自动设置。不能赋值中文
res.statusMessage = 'Not Found'
res.setHeader(name,value)
,设置响应头
res.setHeader("content-type":"text/html;charset=utf-8")
res.write()
方法,响应体,响应正文,给浏览器发送响应体,可以调用多次,从而提供连续的响应体
res.write("响应内容")
- 可以响应普通文本,
json
数据,也可以响应html
片段
res.writeHead(statusCode,statusMessage,headers)
,可以同时设置:状态码、状态信息、响应头
- 有三个参数,分别是响应状态码
number
,响应状态信息string
,响应头object
statusCode
,状态码,是一个数字,用来区分标识响应状态,例如200、404
statusMessage
,状态信息,字符串,用来描述响应状态,例如200
对应的ok
,404
对应的Not found
headers
,响应头,例如{"content-type":"text/html;charset=utf-8","access-control-allow-origin":"*"}
, 可以设置响应内容的数据格式、编码格式、跨域等,res.setHeader()
也可以设置响应头,但是res.writeHead
设置的响应头优先级更高
res.end(data)
方法,通知服务器,所有响应头和响应主体都已被发送,即服务器将其视为已完成,结束请求。
- 如果包含了参数:结束请求,并且响应一段内容,相当于
res.write(data) + res.end()
注意:必须先设置状态码,再设置响应头,最后设置响应主体,顺序不能乱。
原生的
node.js
返回数据时,通常是要先转成json
格式,返回数据的格式要和请求头匹配,比较麻烦。后期使用框架的时候会方便一些。
const http = require('http');
// 创建本地服务器来从其接收数据
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!'
}));
});
server.listen(8000);
//引入http模块
const http = require('http');
// 创建本地服务器来从其接收数据
const server = http.createServer();
// 监听请求事件
server.on('request', (request, res) => {
//定义响应体格式
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: 'Hello World!'
}));
});
server.listen(8000);
2,url模块
url模块主要用来解析操作路径
在request
请求对象中,我们可以通过 req.url
获取到请求的 路径和参数,但是路径和参数是拼接在一起的,有时候我们需要获取 单独的路径和参数 (例如路由判断),这个时候我们就可以使用 url
模块 处理路径,得到我们想要的路径 或 参数。
2.1 parse
let url = require('url')
url
模块的parse
方法:
url.parse(req.url,true)
url
模块的parse
方法可以把路径解析为一个对象
url.parse('/zxf?age=10&gender=boy')
{...,path:'/zxf?age=10&gender=boy',pathname:'/zxf',query:'age=10&gender=boy'}
- 其中的
pathname
属性,是解析后的页面路径,不包含域名和参数
- 可以用来获取页面路径
pathname:'/zxf'
- 其中的
query
属性,是解析后的参数,当第二个参数为true
时,会将参数解析为对象
- 第二个参数默认为
false
,query : key=value&name=zxf
- 第二个参数为
true
时,query:{ key:value , name:'zxf' }
- 可以用来获取页面参数
总结:
url.parse()
方法
- 可以将
url
中的路径解析出来- 可以将
url
中的参数解析出来
const url = require('url')
const urlString = 'https://www.baidu.com:443/ad/index.html?id=8&name=mouse#tag=110'
const parsedStr = url.parse(urlString)
console.log(parsedStr)
2.2 format
url
的format
方法和parse
方法正好相反,parse
方法是将一个url
解析为一个对象,而format
是将一个对象转换为一个url
地址。
const url = require('url')
const urlObject = {
protocol: 'https:',
slashes: true,
auth: null,
host: 'www.baidu.com:443',
port: '443',
hostname: 'www.baidu.com',
hash: '#tag=110',
search: '?id=8&name=mouse',
query: { id: '8', name: 'mouse' },
pathname: '/ad/index.html',
path: '/ad/index.html?id=8&name=mouse'
}
const parsedObj = url.format(urlObject)
console.log(parsedObj)
2.3 resolve
url.resolve(baseUrl,addUrl)
方法的主要作用就是拼接路径
- 两个参数,第一个是基础路径,第二个是追加路径,
url.resolve()
最终会根据它们拼接得到一个新的路径url.resolve('http://zxf.com/a/b/c','d')
,参数二前面不加/
http://axf.com/a/b/d
- 基础路径中最后一个
/
后面的路径会被第二个参数替换url.resolve('http://zxf.com/a/b/c','/d')
,参数二前面加/
http://zxf.com/d
- 基础路径中域名后面的路径会全部被参数二替换
const url = require('url')
var a = url.resolve('/one/two/three', 'four') ( 注意最后加/ ,不加/的区别 )
var b = url.resolve('http://example.com/', '/one')
var c = url.resolve('http://example.com/one', '/two')
console.log(a + "," + b + "," + c)
2.4,url 模块的新方法
上面三种方法apiparse、format、resolve
其实都是旧版的api,下面介绍新版本的api用法
myURL = new URL(req.url,baseUrl)
- 通过
new URL()
得到一个实例对象,类似于旧版api中url.parse(req.url)
得到的对象- 在实例化时,传入两个参数,参数1是
req.url
可以理解为相对路径,参数2是基础路径,如服务器地址- 得到的对象中,常用的属性有
href
:完整路径origin
:协议、域名、端口号port
:端口号pathname
:路径地址searchParams
:解析后的参数对象,并且可以使用一些api方法操作对象
.append(key,value)
,添加参数.delete(key)
,移除参数.entries()
,转为数组,返回一个迭代器Iterator
.keys()
,获取所有的key
,返回一个迭代器Iterator
.values()
,获取所有键值对的值,返回一个迭代器Iterator
.forEach((value,key,searchParams)=>{})
,循环遍历对象.get(key)
,获取第一个该key
的值.getAll(key)
,获取所有该key
的值,返回一个数组.has(key)
,检测是否包含指定key
,返回一个布尔值.set(key,value)
,设置键值对,没有就追加,有就清除之前所有的同名key
再追加.sort()
,对键值对进行排序,无返回值toString()
,返回序列化字符串的参数,表单编码格式,用&
符号分隔- 旧版的
parse
方法可以帮我们拿到解析后的路径和参数,而新版new URL()
得到的对象也可以帮助我们拿到解析后的路径、参数- 旧版的
resolve
方法可以帮助我们拼接路径,但新版的new URL()
在实例化的时候,已经帮助我们完成了拼接
3, querystring模块
require('querystring')
querystring
模块主要用于对url
参数进行格式化 和 对特殊字符进行转义
3.1,parse || decode
querystring.parse('a=1&b=2')
, 表单编码格式字符串=转=>
对象
- 可以将
url
中,表单编码格式的参数转换为对象querystring.parse('a=1&b=2')
=>{a:1,b:2}
parse
接口等同于decode
接口
const querystring = require('querystring')
var qs = 'x=3&y=4'
var parsed = querystring.parse(qs)
console.log(parsed)
3.2, stringify || encode
querystring.stringify({a:1,b:2})
,与parse
方法相反
- 可以将对象, 转换为
url
中表单编码格式的参数querystring.stringify( {a:1,b:2})
=>'a=1&b=2'
stringify
接口等同于encode
接口
const querystring = require('querystring')
var qo = {
x: 3,
y: 4
}
var parsed = querystring.stringify(qo)
console.log(parsed)
querystring
的parse、stringify
,类似于JSON的两个方法,只是querystring
的两个方法是在 表单编码 和 对象 之间转换
03.3 escape/unescape
有些时候,我们的参数中可能存在某些特殊符号,传递拼接后可能会影响代码的正常执行,我们可以使用
escape
先进行转义,使用时还可以使用unescape
反向转义回来
querystring.escape('asd//-$-//123')
==>'asd%2F%2F-%24-%2F%2F123'
querystring.unescape('asd%2F%2F-%24-%2F%2F123')
==>'asd//-$-//123'
const querystring = require('querystring')
var str = 'id=3&city=北京&url=https://www.baidu.com'
var escaped = querystring.escape(str)
console.log(escaped)
const querystring = require('querystring')
var str = 'id%3D3%26city%3D%E5%8C%97%E4%BA%AC%26url%3Dhttps%3A%2F%2Fwww.baidu.com'
var unescaped = querystring.unescape(str)
console.log(unescaped)
04 http模块补充
04.1 接口:jsonp
jsonp
解决跨域,是一个非官方的方式,需要前后端配合,相对麻烦一些
- 前端:页面动态生成
script
标签,src
请求接口地址,后面要跟一个回调函数?callback=getdata
作为参数,回调函数是提前要准备好的- 后端:在接收到请求后,通过解析拿到前端传过来的回调函数,将数据拼接到回调函数的参数中,返回一个调用的函数,前端就可以通过回调函数拿到数据
const http = require('http')
const url = require('url')
const app = http.createServer((req, res) => {
let urlObj = url.parse(req.url, true)
switch (urlObj.pathname) {
case '/api/user':
res.end(`${urlObj.query.callback}({"name": "gp145"})`)
break
default:
res.end('404.')
break
}
})
app.listen(8080, () => {
console.log('localhost:8080')
})
04.2 跨域:CORS
cors
是通过配置响应头来设置跨域
- 后端设置响应头时,通过
access-control-allow-origin
选项来控制哪些地址可以访问access-control-allow-origin
值为*
时,代表任意地址都可访问- 值为
http://127.0.0.1:5500
时,代表该指定地址可以访问
const http = require('http')
const url = require('url')
const querystring = require('querystring')
const app = http.createServer((req, res) => {
let data = ''
let urlObj = url.parse(req.url, true)
res.writeHead(200, {
'content-type': 'application/json;charset=utf-8',
'Access-Control-Allow-Origin': '*'
})
req.on('data', (chunk) => {
data += chunk
})
req.on('end', () => {
responseResult(querystring.parse(data))
})
function responseResult(data) {
switch (urlObj.pathname) {
case '/api/login':
res.end(JSON.stringify({
message: data
}))
break
default:
res.end('404.')
break
}
}
})
app.listen(8080, () => {
console.log('localhost:8080')
})
04.3 模拟get
node.js
不光可以帮助我们向前端返回 html片段、返回数据
,还可以作为中间服务区帮助我们转发请求,解决跨域问题的同时,扮演 客户端的角色,帮我们请求其他服务器的数据
- 我们直接请求别人的服务器上的数据的话,可能会产生跨域问题。
- 这个时候,我们可以创建一个自己的服务器,并开放访问,对浏览器就不存在跨域问题了
- 然后对自己服务器发送请求,服务器接收到给定路径的请求后,就作为客户端去请求我们想要的数据,服务器之间没有跨域,在请求到数据后,再返还给我们自己的浏览器
node
发送请求
http/https
模块的get
方法可以发送请求- 参数1是请求路径
- 参数2是一个回调函数,
- 内部可以通过
res
的data
事件监听,监测数据的返回- 通过
res
的end
事件获取返回完毕的数据- 数据相应的过程中,数据是以流的形式返回的,所以
data
回调中的参数都是数据的一个片段end
回调中的参数,是已经返回完毕的完整数据http.get(url,(res)=>{ let data //ondata,数据以流的形式返回,chunk是数据的一部分 res.on('data',(chunk)=>{ data += chunk }) //onend,数据返回完毕,此时data已经将数据收集完成 res.on('end',()=>{ cnosole.log(data) } ) })
var http = require('http')
var https = require('https')
// 1、接口 2、跨域
const server = http.createServer((request, response) => {
var url = request.url.substr(1)
var data = ''
response.writeHeader(200, {
'content-type': 'application/json;charset=utf-8',
'Access-Control-Allow-Origin': '*'
})
https.get(`https://m.lagou.com/listmore.json${url}`, (res) => {
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
response.end(JSON.stringify({
ret: true,
data
}))
})
})
})
server.listen(8080, () => {
console.log('localhost:8080')
})
04.4 模拟post请求:服务器提交(攻击)
node.js
的 post
请求
http
发送post
请求需要通过http.request()
方法发起
http.request(options,()=>{})
- 参数
options
是一个对象,里面描述了请求的路径信息和请求头等
host
属性,请求的域名port
属性,请求的端口,https
默认端口443
,http
默认端口80
path
属性,具体路径地址method
属性,请求方式headers
属性,请求头,可以设置请求数据类型,例如"content-type":"application/json"
- 第二个参数是一个回调函数,和
get
请求一样,可以监听数据流的返回和结束- 该方法会返回一个对象,代表正在进行的请求
post
请求是要传递参数的,而传递参数(请求正文),需要通过http.request()
返回的对象req
来编写
req.write(请求体)
最后再通过
req.end()
来完成请求
- 如果
req.end(请求体)
中携带了请求体,则相当于先执行req.write(请求体)
再执行req.end()
//发起post请求 let req = http.request(options,(res)=>{ let data //ondata,数据以流的形式返回,chunk是数据的一部分 res.on('data',(chunk)=>{ data += chunk }) //onend,数据返回完毕,此时data已经将数据收集完成 res.on('end',()=>{ cnosole.log(data) } ) }) //post传递参数 req.write({args1:1 , args2:2}) //结束post请求 res.end()
const https = require('https')
const querystring = require('querystring')
const postData = querystring.stringify({
province: '上海',
city: '上海',
district: '宝山区',
address: '同济支路199号智慧七立方3号楼2-4层',
latitude: 43.0,
longitude: 160.0,
message: '求购一条小鱼',
contact: '13666666',
type: 'sell',
time: 1571217561
})
const options = {
protocol: 'https:',
hostname: 'ik9hkddr.qcloud.la',
method: 'POST',
port: 443,
path: '/index.php/trade/add_item',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData)
}
}
function doPost() {
let data
let req = https.request(options, (res) => {
res.on('data', chunk => data += chunk)
res.on('end', () => {
console.log(data)
})
})
req.write(postData)
req.end()
}
// setInterval(() => {
// doPost()
// }, 1000)
04.5 爬虫
const https = require('https')
const http = require('http')
const cheerio = require('cheerio')
http.createServer((request, response) => {
response.writeHead(200, {
'content-type': 'application/json;charset=utf-8'
})
const options = {
// protocol: 'https:',
hostname: 'i.maoyan.com',
port: 443,
path: '/',
method: 'GET'
}
const req = https.request(options, (res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
filterData(data)
})
})
function filterData(data) {
// console.log(data)
let $ = cheerio.load(data)
let $movieList = $('.column.content')
console.log($movieList)
let movies = []
$movieList.each((index, value) => {
movies.push({
title: $(value).find('.movie-title .title').text(),
detail: $(value).find('.detail .actor').text(),
})
})
response.end(JSON.stringify(movies))
}
req.end()
}).listen(3000)
05 event模块
典型的发布者——订阅者模式,可以用来解耦,避免嵌套
- 引入:
let event = require('events')
- 实例:
let myEvent = new event()
- 发布:
myEvent.on('eventName',(data)=>{...})
- 订阅:
myEvent.emit('eventName',data)
- 注意:使用
event
模块发布订阅事件,注意要避免事件的重新发布
- 有些情况下,需要重新实例化
event
对象,使用新的实例对象 来发布事件,否则相同的事件发布可能叠加,导致一次触发多个重复的事件
let event = require('events')
// events模块是典型的 发布者-订阅者模式
// 通过on发布事件,emit订阅事件
let eventA = new event()
setInterval(() => {
//每次都是用一个原有的eventA对象发布事件,每两秒发布一个,所以发布的数量会积累,导致一次触发多个
eventA.on('playA', () => {
console.log('playA————A触发');
})
eventA.emit('playA')
}, 2000)
let eventB = null
setInterval(() => {
//每次发布事件的都是一个重新赋值的全新eventB对象,所以它身上只会有新发布的事件,不会有以前积累,每次只触发一个
eventB = new event()
eventB.on('playB', () => {
console.log('playB————B触发');
})
eventB.emit('playB')
}, 2000)
const EventEmitter = require('events')
class MyEventEmitter extends EventEmitter {}
const event = new MyEventEmitter()
event.on('play', (movie) => {
console.log(movie)
})
event.emit('play', '我和我的祖国')
event.emit('play', '中国机长')
event.on('name',(args)=>{})
有点像声明一个函数- 而
event.emit('name',args)
有点像调用函数 - 但是 事件发布可以重复发布并积累 ,所以一次订阅,可能对应多个发布
06 fs文件操作模块
fs
模块可以帮助我们执行 文件夹目录、文件 的 写入、删除、读取操作
let fs = require("fs")
- 1,写入目录,
fs.mkdir(path,callback)
,创建文件夹
path
是写入的地址和目录名callback
是一个回调函数,用于捕捉错误,参数err
为错误对象err == null
,说明没有错误err.code == "EEXIST"
,代表目录已存在- 2,重命名目录或文件,
fs.rename(path,newPath,callback)
,
path
和newPath
是新旧目录路径或新旧文件路径err.code == "ENOENT"
,代表目录不存在- 3,删除目录,
fs.rmdir(path,callback)
- 只能删除空目录,如果内部有文件要先删除全部文件
- 4,读取目录,
fs.readdir(path,callback)
- 读取目标路径的目录
- 回调有两个参数,第一个参数是
err
,err-first
参数风格——错误优先- 第二个参数是
data
,一个包含目录下所有文件名/目录名的数组- 5,写入文件,
fs.writeFile(path,content,callback)
- 三个参数,文件路径、写入内容、回调函数
- 在目标文件上写入内容,如果没有该文件,则创建该文件
- 只能重新写入,会替换之前的内容,不能追加写入
- 6,追加写入 ,
fs.appendFile(path,content,callback)
- 类似于
writeFile
,但是不会覆盖之前的内容,会在原来内容的基础上追加写入- 7,删除内容,
fs.unlink(path,callback)
- 删除指定路径的文件
- 8,读取文件,
fs.readFile(path,callback)
- 读取指定路径的文件
callback
有两个参数,第一个是err
,第二个是data
- 9,读取文件/目录信息,
fs.stat(path,chatset,callback)
- 读取指定路径的目录或文件
- 第二个参数是编码格式,如
utf-8
,可以帮助我们编译读取到的数据callback
有两个参数,err
和data
data
身上有两个方法
data.isFile()
,判断是否为文件,返回一个 布尔值data.isDirectory()
,判断是否为目录,返回一个布尔值- 文件操作属于异步操作
上面说到,文件操作是属于异步操作的,但是这就容易出现一些问题。
例如,我们要删除一个文件夹,就得先保证它是一个空文件夹,就需要先读取它的文件,然后循环删除,然后在删除文件夹
但是,如果执行了循环删除它内部的所有文件,然后开始删除文件夹,还是可能出错,因为删除所有文件是一个异步操作,执行删除文件夹的时候,内部文件可能还没有删除完成。
即:上面的所有文件循环删除这个操作执行完成前,不可以执行下面的删除文件夹操作。
这是我们就可以使用 fs
模块对应操作的 同步api
fs
模块的同步写法
fs.apiSync(path)
- 写法差异: 同步
api
的写法就是在原来异步api
的后面加了一个Sync
- 错误捕捉: 同步
api
不需要写回调函数,所以自己没有办法捕捉错误,错误会直接被系统捕捉,影响服务器运行
- 为了避免错误上升到系统,我们可以使用
try{}catch(err){}
捕捉错误try{ fs.rmdir(path) } catch (err){ console.log(err) }
- 数据获取: 原来的数据获取是在回调的
data
,同步操作方法是以 返回值的形式获取数据- 它会使
fs
操作变为同步操作,自上而下,依次执行- 缺点: 使用同步,会导致代码阻塞,服务器暂时无法响应,这是万万不可的,所以我们通常不会在服务器正常运行期间使用
fs
模块的同步操作(服务器初始化和结束时没关系)
重点:
fs
模块的promise
版本的Api
let fs = require("fs").promises
- 只需要在引入时,后面加一个
promises
就可以啦,这样fs
模块操作的返回值就是一个promise
对象fs.rmdir(path)
- 不需要写回调
- 捕捉错误,在
catch
方法中捕捉错误- 获取数据,在
then
方法中获取数据fs.readdir('./test').then(async data => { await Promise.all(data.map(item => fs.unlink(`./test/${item}`))) await fs.rmdir('./test') })
const fs = require('fs')
// 创建文件夹
fs.mkdir('./logs', (err) => {
console.log('done.')
})
// 文件夹改名
fs.rename('./logs', './log', () => {
console.log('done')
})
// 删除文件夹
fs.rmdir('./log', () => {
console.log('done.')
})
// 写内容到文件里
fs.writeFile(
'./logs/log1.txt',
'hello',
// 错误优先的回调函数
(err) => {
if (err) {
console.log(err.message)
} else {
console.log('文件创建成功')
}
}
)
// 给文件追加内容
fs.appendFile('./logs/log1.txt', '\nworld', () => {
console.log('done.')
})
// 读取文件内容
fs.readFile('./logs/log1.txt', 'utf-8', (err, data) => {
console.log(data)
})
// 删除文件
fs.unlink('./logs/log1.txt', (err) => {
console.log('done.')
})
// 批量写文件
for (var i = 0; i < 10; i++) {
fs.writeFile(`./logs/log-${i}.txt`, `log-${i}`, (err) => {
console.log('done.')
})
}
// 读取文件/目录信息
fs.readdir('./', (err, data) => {
data.forEach((value, index) => {
fs.stat(`./${value}`, (err, stats) => {
// console.log(value + ':' + stats.size)
console.log(value + ' is ' + (stats.isDirectory() ? 'directory' : 'file'))
})
})
})
// 同步读取文件
try {
const content = fs.readFileSync('./logs/log-1.txt', 'utf-8')
console.log(content)
console.log(0)
} catch (e) {
console.log(e.message)
}
// 异步读取文件:方法一
fs.readFile('./logs/log-0.txt', 'utf-8', (err, content) => {
console.log(content)
console.log(0)
})
console.log(1)
// 异步读取文件:方法二
const fs = require("fs").promises
fs.readFile('./logs/log-0.txt', 'utf-8').then(result => {
console.log(result)
})
在fs
模块中,提供同步方法是为了方便使用。那我们到底是应该用异步方法还是同步方法呢?
由于Node环境执行的JavaScript代码是服务器端代码,所以,绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码,否则,同步代码在执行时期,服务器将停止响应,因为JavaScript只有一个执行线程。
服务器启动时如果需要读取配置文件,或者结束时需要写入到状态文件时,可以使用同步代码,因为这些代码只在启动和结束时执行一次,不影响服务器正常运行时的异步执行。
07 stream流模块
stream
流模块,以数据流的模式,读取、写入、传输 数据
let fs = require("fs")
let readStream = fs.createReadStream(filePatn,charset)
,创建读取流
readSteam
.on("data",callback)
ondata
事件可以监听数据流的读取,callback
的参数是正在读取的数据readStream
.on("end,callback)
onend
事件可以监听数据流读取结束readStream
.on("error",callback)
onerror
事件可以监听数据流读取过程中的错误let writeStream = fs.createWriteStream(filePath)
,创建写入流
writeStream
.write("content")
,写入内容writeStream
.end()
,结束写入readStream.pipe(writeStream)
,文件流传输
- 读取流身上 有一个
.pipe()
方法,可以把读取流和写入流串联起来,把读取的数据直接写入其他文件- 读取A文件内容别写入到B文件
FileA
==>
FileB
let fs = require('fs') //创建针对A文件的可读流 let rs = fs.createReadStream('FileAPath','utf-8') //创建针对B文件的可写流 let ws= fs.createWriteStream('FileBPath') //将A的可读流与B的可写流连接 rs.pipe(ws)
- 注意:如果可写流指定路径的文件不存在,会先创建再写入
stream
是Node.js提供的又一个仅在服务区端可用的模块,目的是支持“流”这种数据结构。
什么是流?流是一种抽象的数据结构。想象水流,当在水管中流动时,就可以从某个地方(例如自来水厂)源源不断地到达另一个地方(比如你家的洗手池)。我们也可以把数据看成是数据流,比如你敲键盘的时候,就可以把每个字符依次连起来,看成字符流。这个流是从键盘输入到应用程序,实际上它还对应着一个名字:标准输入流(stdin)。
如果应用程序把字符一个一个输出到显示器上,这也可以看成是一个流,这个流也有名字:标准输出流(stdout)。流的特点是数据是有序的,而且必须依次读取,或者依次写入,不能像Array那样随机定位。
有些流用来读取数据,比如从文件读取数据时,可以打开一个文件流,然后从文件流中不断地读取数据。有些流用来写入数据,比如向文件写入数据时,只需要把数据不断地往文件流中写进去就可以了。
在Node.js中,流也是一个对象,我们只需要响应流的事件就可以了:data
事件表示流的数据已经可以读取了,end
事件表示这个流已经到末尾了,没有数据可以读取了,error
事件表示出错了。
var fs = require('fs');
// 打开一个流:
var rs = fs.createReadStream('sample.txt', 'utf-8');
rs.on('data', function (chunk) {
console.log('DATA:')
console.log(chunk);
});
rs.on('end', function () {
console.log('END');
});
rs.on('error', function (err) {
console.log('ERROR: ' + err);
});
要注意,data
事件可能会有多次,每次传递的chunk
是流的一部分数据。
要以流的形式写入文件,只需要不断调用write()
方法,最后以end()
结束:
var fs = require('fs');
var ws1 = fs.createWriteStream('output1.txt', 'utf-8');
ws1.write('使用Stream写入文本数据...\n');
ws1.write('END.');
ws1.end();
pipe
就像可以把两个水管串成一个更长的水管一样,两个流也可以串起来。一个Readable
流和一个Writable
流串起来后,所有的数据自动从Readable
流进入Writable
流,这种操作叫pipe
。
在Node.js中,Readable
流有一个pipe()
方法,就是用来干这件事的。
让我们用pipe()
把一个文件流和另一个文件流串起来,这样源文件的所有数据就自动写入到目标文件里了,所以,这实际上是一个复制文件的程序:
const fs = require('fs')
const readstream = fs.createReadStream('./1.txt')
const writestream = fs.createWriteStream('./2.txt')
readstream.pipe(writestream)
08 zlib
浏览器客户端在向服务器请求一个文件或数据后,如果请求资源太大,响应传输就会耗时较久,且时间主要花费在传输过程中。
为了节约时间,我们可以压缩资源,传输过去,在解压,这样在传输过程中就可以节约时间成本
zlib
内置模块可以帮助我们压缩资源。
let fs = require('fs')
let zlib = require('zlib')
,引入zlib
模块let gzip = zlib.createGzip()
,利用zlib
模块创建gzip
对象let rs = fs.createReadStream('./A.txt')
let ws = fs.createWriteStream('./B.txt')
rs.pipe(gzip).pipe(ws)
- 先将要传输的数据传递给
gzip
对象压缩,然后再开始向外 输出
const fs = require('fs')
const zlib = require('zlib')
const gzip = zlib.createGzip()
const readstream = fs.createReadStream('./note.txt')
const writestream = fs.createWriteStream('./note2.txt')
readstream
.pipe(gzip)
.pipe(writestream)
09 crypto
crypto模块的目的是为了提供通用的加密和哈希算法。用纯JavaScript代码实现这些功能不是不可能,但速度会非常慢。Nodejs用C/C++实现这些算法后,通过cypto这个模块暴露为JavaScript接口,这样用起来方便,运行速度也快。
MD5是一种常用的哈希算法,用于给任意数据一个“签名”。这个签名通常用一个十六进制的字符串表示:
const crypto = require('crypto');
const hash = crypto.createHash('md5');
// 可任意多次调用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');
console.log(hash.digest('hex'));
update()
方法默认字符串编码为UTF-8
,也可以传入Buffer。
如果要计算SHA1,只需要把'md5'
改成'sha1'
,就可以得到SHA1的结果1f32b9c9932c02227819a4151feed43e131aca40
。
Hmac算法也是一种哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac还需要一个密钥:
const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', 'secret-key');
hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');
console.log(hmac.digest('hex')); // 80f7e22570...
只要密钥发生了变化,那么同样的输入数据也会得到不同的签名,因此,可以把Hmac理解为用随机数“增强”的哈希算法。
AES是一种常用的对称加密算法,加解密都用同一个密钥。crypto模块提供了AES支持,但是需要自己封装好函数,便于使用:
const crypto = require("crypto");
function encrypt (key, iv, data) {
let decipher = crypto.createCipheriv('aes-128-cbc', key, iv);
// decipher.setAutoPadding(true);
return decipher.update(data, 'binary', 'hex') + decipher.final('hex');
}
function decrypt (key, iv, crypted) {
crypted = Buffer.from(crypted, 'hex').toString('binary');
let decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
return decipher.update(crypted, 'binary', 'utf8') + decipher.final('utf8');
}
key,iv必须是16个字节
可以看出,加密后的字符串通过解密又得到了原始内容。
6. 路由
何为路由?路由就是根据不同的 路径,返回不同的页面或者不同的接口内容
根据不同的路径,做出不同路径的处理,就是路由
不同路径 => 不同处理
01 基础
var fs = require("fs")
var path = require("path")
function render(res, path) {
res.writeHead(200, { "Content-Type": "text/html;charset=utf8" })
res.write(fs.readFileSync(path, "utf8"))
res.end()
}
//把不同路径,映射为函数,根据参数的不同,返回对应页面或者相应数据
const route = {
"/login": (req, res) => {
render(res, "./static/login.html")
},
"/home": (req, res) => {
render(res, "./static/home.html")
},
"/404": (req, res) => {
res.writeHead(404, { "Content-Type": "text/html;charset=utf8" })
res.write(fs.readFileSync("./static/404.html", "utf8"))
}
}
02 获取参数
get请求
get 请求可以利用
new URL(path,baseUrl)
来解析路径和参数
get
请求的参数存放在解析后对象的searchParams
对象中myUrl.searchParams.get(paramsName)
可以获取指定参数的值myUrl.searchParams
是一个具有迭代器接口的对象
"/api/login":(req,res)=>{
const myURL = new URL(req.url, 'http://127.0.0.1:3000');
console.log(myURL.searchParams.get("username"))
render(res,`{ok:1}`)
}
post请求
post 请求需要监听
req
对象的ondata
事件来获取参数,且参数数据可能过多,所以是以流的形式获取的
req.on('data' , (chunk)=>{})
,post
请求的参数是流的形式接受,可以用一个变量接收req.on('end',()=>{})
,当参数接收完成时,会触发end
事件,但end
事件的回调没有参数,也无法获取post
请求的参数- 在
onend
中,参数已经接收完成,这个时候的data
是完整的参数
"/api/login": (req, res) => {
var post = '';
// 通过req的data事件监听函数,每当接受到请求体的数据,就累加到post变量中
req.on('data', function (chunk) {
post += chunk;
});
// 在end事件触发后,通过querystring.parse将post解析为真正的POST请求格式,然后向客户端返回。
req.on('end', function () {
post = JSON.parse(post);
render(res, `{ok:1}`)
});
}
03 静态资源处理
浏览器在收到服务器返回的HTML内容之后,就要开始从上到下依次解析,当在解析的过程中,如果发现:link、script、img、iframe、video、audio
等 带有src
或者link
的href
属性 标签的时候,浏览器会自动对这些静态资源发起一个新的请求。我们要对这些新的请求进行处理。
处理静态资源时,浏览器发起的静态资源请求,既不是
api
又不是route
路由,所以就会跑到404
的路由接口中
我们可以在404
的路由接口中,对请求进行判断,看它是否为静态资源,是静态资源就返回资源,否则再按404
处理
判断是否为静态资源
- 1,先获取请求路径,
let myUrl = new URL(req.url,'http://127.0.0.1:3000')
- 2,按照静态资源的路径,拼接完整路径
path
是node
的一个内置模块,用来处理路径path.join
是用来拼接路径的,相对路径是/
符分隔的,绝对路径是\
符分隔的,不同操作系统的分隔符也不一样,而path.join
会把传进去的路径参数,根据系统进行自动拼接出来,并统一分隔符__dirname
是当前path
的目录路径,它可以看作是node
的一个全局变量- 我们把
当前目录
,和当前目录下的静态文件目录
,和请求路径
拼接在一起,看我们静态资源目录中,是否存在拼接路径的资源,就知道本次请求的是不是静态资源- 3,使用
fs
模块的fs.existsSync(filePathname)
方法,可以得出目标路径资源是否存在,返回一个布尔值- 4,如果存在该路径,我们就像返回
html
片段一样,返回资源,如果没有该路径,就按照404
路由处理- 5,
mime
第三方模块,是一个自动得出响应头中,响应数据的类型的第三方模块
mime.getType(fileType)
,参数fileType
是文件的后缀名,根据后缀名自动得出返回数据的响应头类型
function readStaticFile(req, res) {
//获取请求路径
const myURL = new URL(req.url, 'http://127.0.0.1:3000')
//按照静态资源的路径,拼接完整路径
var filePathname = path.join(__dirname, "/static", myURL.pathname);
//使用fs.existsSync(path) ,判断给定路径是否存在
if (fs.existsSync(filePathname)) {
// 存在就返回资源
res.writeHead(200, { "Content-Type": `${mime.getType(myURL.pathname.split(".")[1])};charset=utf8` })
res.write(fs.readFileSync(filePathname, "utf8"))
res.end()
return true
} else {
//不存在,按照404响应
return false
}
}
二、Express
https://www.expressjs.com.cn/
基于 Node.js 平台,快速、开放、极简的 web 开发框架。
1.特色
express
是node.js
的一个框架,就像是vue
是JavaScript
的一个框架一样,使用框架可以帮助我们提高效率,因为他已将帮我们封装好了一些接口,帮助我们做了好多事,让我们使用起来更简洁,更方便。
例如:
- 不需要再写请求头
res.writeHead()、res.write()、res.end()
,先写请求头,再写响应体,在结束响应,合并为一个res.send()
res.send()
,包含了请求头,默认状态码是200
,要修改可以:res.status(xxx).send()
- 原来响应数据要先转成
json
数据,现在可以响应对象object
数据- 原来不能直接响应汉字,要先在响应头中设置响应数据类型,编码格式,而现在可以直接响应中文
res.send('中文内容')
- ……等等,总之就是:
简洁方便,小而精
2.安装
$ npm install express --save
3.路由
路由是指如何定义应用的端点(URIs)以及如何响应客户端的请求。
路由是由一个 URI、HTTP 请求(GET、POST等)和若干个句柄组成,它的结构如下: app.METHOD(path, [callback…], callback)。
express
定义路由的语法
app.method(path,[callback...])
app
是express
对象的一个实例
METHOD
是一个HTTP
请求方法方式
,如get、post、delete、put
path
是服务器上的路径
callback
是当路由匹配时要执行的函数
下面是一个基本的路由示例:
var express = require('express');
var app = express();
// respond with "hello world" when a GET request is made to the homepage
app.get('/', function(req, res) {
res.send('hello world');
});
路由路径和请求方法一起定义了请求的端点,它可以是字符串、字符串模式或者正则表达式。
// 匹配根路径的请求
app.get('/', function (req, res) {
res.send('root');
});
// 匹配 /about 路径的请求
app.get('/about', function (req, res) {
res.send('about');
});
// 匹配 /random.text 路径的请求
app.get('/random.text', function (req, res) {
res.send('random.text');
});
使用字符串模式的路由路径示例:
/ab?cd
?
前面的那个字符可有可无- 可以用
?
标记多个可选字符,/ab?c?d
/ab/:id
- 可以声明占位符,声明后必须使用
- 可以声明多个占位符,
/ab/:id/:name
/ab+cd
+
前面的字符可以是一个或多个/ab*cd
*
符号位置,可以插入其他字符/ab(cd)?e
?
前面括号内的片段可有可无- 类似于第一种
/ab?cd
,只不过该方式标记的是片段
// 匹配 acd 和 abcd
app.get('/ab?cd', function(req, res) {
res.send('ab?cd');
});
// 匹配 /ab/******
app.get('/ab/:id', function(req, res) {
res.send('aaaaaaa');
});
// 匹配 abcd、abbcd、abbbcd等
app.get('/ab+cd', function(req, res) {
res.send('ab+cd');
});
// 匹配 abcd、abxcd、abRABDOMcd、ab123cd等
app.get('/ab*cd', function(req, res) {
res.send('ab*cd');
});
// 匹配 /abe 和 /abcde
app.get('/ab(cd)?e', function(req, res) {
res.send('ab(cd)?e');
});
使用正则表达式的路由路径示例:
// 匹配任何路径中含有 a 的路径:
app.get(/a/, function(req, res) {
res.send('/a/');
});
// 匹配 butterfly、dragonfly,不匹配 butterflyman、dragonfly man等
app.get(/.*fly$/, function(req, res) {
res.send('/.*fly$/');
});
可以为请求处理提供多个回调函数,其行为类似 中间件。唯一的区别是这些回调函数有可能调用 next(‘route’) 方法而略过其他路由回调函数。可以利用该机制为路由定义前提条件,如果在现有路径上继续执行没有意义,则可将控制权交给剩下的路径。
路由回调(中间件)
在express
框架中,路由的回调函数可以是多个,类似于中间件
可以避免我们将所有的逻辑代码写在一个函数中太臃肿
三种写法
app.method(path , fn1(){} , fn2(){})
,函数放在内部,依次编写app.method(path , [fn1 , fn2])
,函数放在外部,将所有函数名放在一个数组中app.method(path , [fn1] , fn2(){})
,混合使用向下执行
- 这些中间件回调函数有三个参数,
req、res、next
,分别是:请求对象、响应对象、中间件next()
,上一个函数调用了next()
下一个函数才能继续执行- 如果上一个函数执行了
res.send()
,则响应结束,后面的函数不会再执行中间件传递参数
- 多个中间件传递参数,可以利用他们共有的
res
对象res.args
,把数据定义在res
对象上,下一个函数就可以通过res拿到参数
app.get('/example/a', function (req, res) {
res.send('Hello from A!');
});
使用多个回调函数处理路由(记得指定 next 对象):
app.get('/example/b', function (req, res, next) {
console.log('response will be sent by the next function ...');
next();
}, function (req, res) {
res.send('Hello from B!');
});
使用回调函数数组处理路由:
var cb0 = function (req, res, next) {
console.log('CB0')
next()
}
var cb1 = function (req, res, next) {
console.log('CB1')
next()
}
var cb2 = function (req, res) {
res.send('Hello from C!')
}
app.get('/example/c', [cb0, cb1, cb2])
混合使用函数和函数数组处理路由:
var cb0 = function (req, res, next) {
console.log('CB0')
next()
}
var cb1 = function (req, res, next) {
console.log('CB1')
next()
}
app.get('/example/d', [cb0, cb1], function (req, res, next) {
console.log('response will be sent by the next function ...')
next()
}, function (req, res) {
res.send('Hello from D!')
})
4.中间件
Express 是一个自身功能极简,完全是由路由和中间件构成一个的 web 开发框架:从本质上来说,一个 Express 应用就是在调用各种中间件。
中间件(Middleware) 是一个函数,它 可以访问请求对象、响应对象, 和 web 应用中处于请求-响应循环流程中的中间件,一般被命名为
next
的变量。
在中间件中,调用next()
,其实就是调用的下一个中间件。
中间件的功能包括:
- 执行任何代码。
- 修改请求和响应对象。
- 终结请求-响应循环。
- 调用堆栈中的下一个中间件。
如果当前中间件没有终结请求-响应循环,则必须调用 next() 方法将控制权交给下一个中间件,否则请求就会挂起。
Express 应用可使用如下几种中间件:
- 应用级中间件
- 路由级中间件
- 错误处理中间件
- 内置中间件
- 第三方中间件
使用可选则挂载路径,可在应用级别或路由级别装载中间件。另外,你还可以同时装在一系列中间件函数,从而在一个挂载点上创建一个子中间件栈。
(1)应用级中间件
应用级中间件,都是绑定到应用上的
绑定方式app.use()、app.method()
app.use(path , 中间件)
- 利用
use
绑定中间件时,有两个参数,第一个是路径,如果有值,就只对指定路径的路由生效,如果没有值(可以省略),则对所有路由生效。- 第二个参数就是要绑定的中间件
app.use('/home' , homeRouter)
,绑定一个路由中间件,只对'/home'
路径下的路由生效app.use((req,res)=>{})
,绑定一个错误处理中间件,没有路径,对所有路径的路由生效app.get('/home',(req,res)=>{})
,绑定一个针对'/home'
路径的中间件,只对'/home'
路径生效- 中间件的绑定挂载时机很重要,例如检测
token
的中间件,只有在绑定后才会生效,在它后面的路由会受影响,而在它前面的路由不受影响
应用级中间件绑定到 app 对象 使用 app.use() 和 app.METHOD(), 其中, METHOD 是需要处理的 HTTP 请求的方法,例如 GET, PUT, POST 等等,全部小写。例如:
var app = express()
// 没有挂载路径的中间件,应用的每个请求都会执行该中间件
app.use(function (req, res, next) {
console.log('Time:', Date.now())
next()
})
// 挂载在 '/home' 路径下的中间件, 只对'/home' 下的请求或路由生效
app.use('/home',homeRouter)
(2)路由级中间件
路由级中间件和应用级中间件一样,只是它绑定的对象为 express.Router()
。
应用中间件 是绑定在
express()
生成的app
上,而路由中间件是绑定在express.router()
生成的router
上
let express = require('express') let router = express.Router() router.get('/',[fn]) router.get('/list',[fn]) router.get('/hot',[fn]) module.exports = router // 这里的fn就是一个路由级中间件
我们可以把路由级中间件挂载到app上面
let express = require('express') let app = express() let homeRouter = require('homeRouter') //将路由级中间件挂载到app上,该中间件只对/home路径下的路由生效 //在这里,/home是作为一级路径,而上面路由中间件中的/、/hot、/list,是该路径下的二级路径 app.use('/home' , homeRouter)
var app = express()
var router = express.Router()
// 没有挂载路径的中间件,通过该路由的每个请求都会执行该中间件
router.use(function (req, res, next) {
console.log('Time:', Date.now())
next()
})
// 一个中间件栈,显示任何指向 /user/:id 的 HTTP 请求的信息
router.use('/user/:id', function(req, res, next) {
console.log('Request URL:', req.originalUrl)
next()
}, function (req, res, next) {
console.log('Request Type:', req.method)
next()
})
// 一个中间件栈,处理指向 /user/:id 的 GET 请求
router.get('/user/:id', function (req, res, next) {
// 如果 user id 为 0, 跳到下一个路由
if (req.params.id == 0) next('route')
// 负责将控制权交给栈中下一个中间件
else next() //
}, function (req, res, next) {
// 渲染常规页面
res.render('regular')
})
// 处理 /user/:id, 渲染一个特殊页面
router.get('/user/:id', function (req, res, next) {
console.log(req.params.id)
res.render('special')
})
// 将路由挂载至应用
app.use('/', router)
(3)错误处理中间件
错误处理中间件和其他中间件定义类似,只是要使用 4 个参数,而不是 3 个,其签名如下: (err, req, res, next)。
app.use(function(err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})
(4)内置的中间件
express.static 是 Express 唯一内置的中间件。它基于 serve-static,负责在 Express 应用中提托管静态资源。每个应用可有多个静态目录。
app.use(express.static('public'))
app.use(express.static('uploads'))
app.use(express.static('files'))
(5)第三方中间件
安装所需功能的 node 模块,并在应用中加载,可以在应用级加载,也可以在路由级加载。
下面的例子安装并加载了一个解析 cookie 的中间件: cookie-parser
$ npm install cookie-parser
var express = require('express')
var app = express()
var cookieParser = require('cookie-parser')
// 加载用于解析 cookie 的中间件
app.use(cookieParser())
5. 获取请求参数
- get
获取
get
参数,只需要使用req.query
req.query
,req
对象的query
属性,是一个对象,他是解析后的参数对象
req.query
//{ arg1:xx, arg2:xx, ...}
- post
获取
post
参数,需要先用express
框架的 内置中间件 解析,再用req.body
获取
app.use(express.urlencoded({extended:false}))
- 该中间件用来解析
url
表单编码格式 的参数app.use(express.json())
- 该中间件用来解析
json
格式 的参数- 注意:一定要 在路由调用前 挂载中间件
- 解析后在路由中间件中,通过
req.body
获取解析后的参数对象
app.use(express.urlencoded({extended:false}))
app.use(express.json())
req.body
- delete
获取
delete
参数,只需要使用req.params.xx
req.params.xx
,req
对象的params
属性,是一个对象,他是解析后的 参数对象delete
请求方式,是通过动态路由传参的方式进行传参axios.delete("/admin/user/delete/:id")
6.利用 Express 托管静态文件
在以前没有使用
express
框架的时候,我们处理静态文件,要:
- 准备好静态资源目录
- 先判断该请求是不是静态资源请求
- 判断是否存在该资源
- 然后再返回资源
而使用
express
,只需要:
- 准备好静态资源目录
- 挂载绑定
app.use(express.static('public'))
中间件,并把所在目录名作为参数传给它- 就可以提供静态资源的访问了
express.static( directoryName )
中间件是express
内置中间件,可以更方便的帮助我们托管静态文件
- 可以绑定多个静态目录
- 因为所有静态资源都在静态资源目录中,所以路径中不存在静态目录名
- 如果我们想让静态资源都放在一个虚拟路径下,可以
app.use('/vName',express.static('public'))
,此时再访问该目录下的静态资源文件,就需要在路径中加上/vName
的前缀
通过 Express 内置的 express.static 可以方便地托管静态文件,例如图片、CSS、JavaScript 文件等。
将静态资源文件所在的目录作为参数传递给 express.static 中间件就可以提供静态资源文件的访问了。例如,假设在 public 目录放置了图片、CSS 和 JavaScript 文件,你就可以:
app.use(express.static('public'))
现在,public 目录下面的文件就可以访问了。
http://localhost:3000/images/kitten.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/images/bg.png
http://localhost:3000/hello.html
所有文件的路径都是相对于存放目录的,因此,存放静态文件的目录名不会出现在 URL 中。
如果你的静态资源存放在多个目录下面,你可以多次调用 express.static 中间件:
app.use(express.static('public'))
app.use(express.static('files'))
访问静态资源文件时,express.static 中间件会根据目录添加的顺序查找所需的文件。
如果你希望所有通过 express.static 访问的文件都存放在一个“虚拟(virtual)”目录(即目录根本不存在)下面,可以通过为静态资源目录指定一个挂载路径的方式来实现,如下所示:
app.use('/static', express.static('public'))
现在,你就可以通过带有 “/static” 前缀的地址来访问 public 目录下面的文件了。
http://localhost:3000/static/images/kitten.jpg
http://localhost:3000/static/css/style.css
http://localhost:3000/static/js/app.js
http://localhost:3000/static/images/bg.png
http://localhost:3000/static/hello.html
7.服务端渲染(模板引擎)
SSR(Server-Side Rendering,服务器端渲染)是一种 Web 应用渲染方式,在服务器端将页面的 HTML、CSS 和 JavaScript 生成并返回给客户端,客户端浏览器接收到已经渲染好的页面内容,无需再进行额外的渲染。这与传统的客户端渲染(Client-Side Rendering, CSR)相对应,客户端渲染是在浏览器中进行页面的渲染。
服务器渲染,也就是我们说的
SSR
CSR
,(Client-Side Rendering, 客户端渲染)
- 前端:获取数据,渲染数据,这种模式称为 客户端渲染
- 后端:只负责返回数据即可
SSR
,(Server-Side Rendering,服务器端渲染)
- 在 服务器端将页面 的 HTML、CSS 和 JavaScript 生成并 返回给客户端
- 客户端浏览器接收到已经渲染好的页面内容,无需再进行额外的渲染。
- 它是根据我们前端的页面做一个模板,然后替换模板中的数据,来实现服务器渲染页面
- 优点:
- SEO 友好:服务器端渲染的页面可以被搜索引擎更好地抓取和索引,因为搜索引擎爬虫可以直接获取到完整的 HTML 内容。这对于需要搜索引擎优化的网站来说非常重要。
- 首屏加载速度快:SSR 可以在服务器端将页面渲染好,客户端接收到的是已经渲染好的 HTML,因此首屏加载速度较快,提高了用户体验。
- 缺点:
- 服务器负载较大:服务器端需要处理每个页面请求的渲染工作,这将增加服务器的负担。对于高并发的应用,可能需要更多的服务器资源来支撑。
- 开发复杂度高:SSR 需要考虑客户端和服务器端的同构问题,为了保持代码的一致性,需要在客户端和服务器端共享部分代码。这增加了开发和维护的复杂度。
- 运行环境限制:SSR 通常需要在服务器端运行 JavaScript,因此可能会受到运行环境的限制。例如,某些浏览器 API 可能在服务器端无法使用。
在node中使用
ssr
- 首先要下载
ejs
中间件,npm i ejs
- 配置中间件
app.set('views', './views')
,指定模板路径app.set('view engine', 'ejs')
,指定模板引擎- 当路由被访问时,我们不再是返回数据让前端渲染,而是直接渲染模板
- 在路由中,
res.render('viewName')
,渲染指定模板- 跳转重定向:
res.redirect('path')
,根据当前模板的路径,选出目标模板的路径,重定向渲染目标路径- 渲染时,可以跟第二个参数,是传递给模板的数据,
res.render(path , { key : value })
ejs
中间件的语法:
- 整体类似于
html
<% %>
,中间可以是:字符串、变量、if else 、for循环、三元表达式
<%= %>
,加个=
代表 不解析html
,类似于vue
的v-text
<%- %>
,加个-
代表 解析html
,类似于vue
的v-html
<%# %>
,在ejs
模板文件中,可能存在注释内容,而正常的注释会被渲染到源码中,<%# %>
该种形式的注释不会被渲染到源码中<%- include(path , argsObject)%>
,include()
可以引入其他模板,
- 类似于
vue
的组件一样,path
是模板路径,第二个参数是一个对象,是传给引入模板的参数,- 被引入的模板可以根据使用者的参数做一些条件判断展示
// 假如服务端渲染时,传入的参数list是是一个数字数组 <ul> <% for(let i =0; i<list.length; i++){ %> // for 循环头部 <% if(list[i]%2) {%> // if 头部 <li><%- list[i] %></li> // 解析内容 <% } %> // if 结尾 <% } %> // for 结尾 </ul> <%- include('./footer.ejs',{isShow:false}) %>
- 除了使用
ejs
渲染模板之外,其实我们还可以通过修改ejs
配置,想页面 直接渲染html
app.set("views", "./views")
app.set("view engine", "html")
app.engine("html", require("ejs").renderFile)
- 把原来的中间件配置改为现在这样,然后把
html
页面放入views
文件夹中- 这样就可以直接向页面渲染
html
npm i ejs
需要在应用中进行如下设置才能让 Express 渲染模板文件:
- views, 放模板文件的目录,比如: app.set(‘views’, ‘./views’)
- view engine, 模板引擎,比如: app.set(‘view engine’, ‘ejs’)
8. express项目生成器
通过应用生成器工具 express-generator 可以快速创建一个应用的骨架。它的骨架可以帮助我们:
- 创建项目目录,包含
入口文件、路由目录、静态资源目录、ssr模板目录
- 创建
App
应用、引入中间件、注册中间件…等等
使用
express 生成器
:
- 全局安装:
npm i -g express--generator
- 创建项目:
express projectName
- 但是默认创建的项目使用的ssr引擎是
jade
引擎,我一般是使用ejs
引擎,创建项目时是express projectName --view=ejs
- 项目创建后,我们需要进入项目目录,
npm i
下载依赖包- 我们通常使用
nodemon
或node-dev
启动项目,所以在package.json
文件中,将启动方式node
修改为前面的方式- 最后使用
npm start
来启动项目
三、MongoDB
1.关系型与非关系型数据库
关系型数据库 和 非关系型数据库
- 关系型数据库
- 内部数据像一张表格
- 表内的每条数据,都有相同数量的字段,且字段名都是相同的
- 非关系型数据库
- 每条数据就像一个对象
- 每条数据的字段数量不一定相同,甚至字段名字也可以不相同
关系型数据库:
非关系型数据库:
2.安装数据库
https://docs.mongodb.com/manual/administration/install-community/
3.启动数据库
- 启动数据库服务端
mongod --dbpath 要存放的路径
- 启动数据库客户端
mongo
(1)windows
mongod --dbpath d:/data/db
mongo
(2)mac
mongod --config /usr/local/etc/mongod.conf
mongo
4.在命令行中操作数据库
-
库的增删改查操作
-
表的增删改查操作
表内数据的增删改查操作
5.可视化工具进行增删改查
Robomongo Robo3T adminMongo
6.nodejs连接操作数据库
node
连接数据库,操作数据
- 想要使用
node
连接数据库,首先开启数据库服务端- 下载
mongoose
依赖包- 可以创建一个文件夹、文件,用来连接数据库
const mongoose = require("mongoose")
, 引入模块mongoose.connect("mongoose://127.0.0.1:27017"/isName)
,连接数据库,并为数据库起一个名字- 然后将该文件引入到项目初始配置中
bin/www
中,项目启动,就会自动执行该脚本,创建连接数据库- 创建集合(表)
- 一个集合,就对应着一个表,我们可以用一个表,对应项目中的一个模块的数据
let mongoose = require("mongoose");
,引入模块const schema = mongoose.Schema;
,因为mongo
数据库在过于自由,我们可以利用Schema
模块对数据进行限制
const userType = { username: String, password: String, age: Number};
- 如上:就是一个数据的规则,不符合规格的字段不会被添加进去,其余字段不受影响
const userModel = mongoose.model("user", new schema(userType));
,创建一个集合表,可以用它存放数据,操作数据module.exports = userModule
,导出表,在使用的地方引入,然后对它内部数据进行增删改查- 在路由中间件内部引入表,在接到请求后,按需求对表内部增删改查数据
增
:model.create(dataObj).then(res=>{})
,添加数据,成功回调参数为 添加后的本条数据(多了id
主键)删
:model.deleteOne({key:value})
,根据删除条件删除数据
- 一般传入
_id
主键作为条件,根据主键匹配删除删除、更新
等操作,后面跟One
则是对找到的第一条进行操作,Many
是对筛选到的全部数据操作改
:model.updateOne({key:value},{newKey:newVlue})
,传入 查找条件,第二个参数是修改后的 新数据查
:model.find({} , [str1,str2]).sort({str1:1}).skip((pagenum-1)* pagesize).limit(pagesize).then(data=>res.send(data))
model.find({} , ["str"])
,查询数据
,第一个参数{}
是查询筛选条件,第二个参数是数组,指定返回数据的哪些字段.sort({str:1})
,数据排序
,根据所给字段对返回的数据排序,1
为正序,-1
为倒序.skip((pagenum - 1)*pagesize).limit(pagesize)
,分页查询
,skip()
决定跳过多少条数据,limit()
决定取多少条数据- 查询后的返回值是数据组成的数组
- 获取动态路由传递的参数
req.params
/api/userl/delete/:id
,这种动态路由传参,我们可以在req.params
中拿到传递的参数{ id:xxxx }
连接数据库
const mongoose = require("mongoose")
mongoose.connect("mongodb://127.0.0.1:27017/company-system")
创建模型
const mongoose = require("mongoose")
const Schema = mongoose.Schema
const UserType = {
username:String,
password:String,
gender:Number,
introduction:String,
avatar:String,
role:Number
}
const UserModel = mongoose.model("user",new Schema(UserType))
module.exports = UserModel
增加数据
UserModel.create({
introduction,username,gender,avatar,password,role
})
查询数据
UserModel.find({username:"zxf"},["username","role","introduction","password"]).sort({createTime:-1}).skip(10).limit(10)
更新数据
UserModel.updateOne({
_id
},{
introduction,username,gender,avatar
})
删除数据
UserModel.deleteOne({_id})
四、接口规范与业务分层
1.接口规范
RESTful
接口规范
- 在以往的接口中,有很多的接口是用来操作同一块数据
- 例如:添加用户、删除用户、修改用户、查询用户,这些接口都是针对用户的增删改查
- 针对用户的增删改查可以有四个接口:
GET/addUser
、GET/deleteUser
、GET/updateUser
、GET/getUser
,通过不同的动词来创建针对同一数据的不同接口- 而
RESTful
接口规范主张:url
地址中只包含名字代表数据资源,使用http
动词(请求方式)表示动作进行操作数据资源- 例如以下:
GET/addUser
==>POST/user
,post请求 表示 添加 数据GET/deleteUser
==>DELETE/user
,delete请求 表示 删除 数据GET/updateUser
==>PUT/user
,put请求 表示 修改 数据GET/getUser
==>GET/user
,get请求 表示 获取 数据
2.业务分层
业务分层,我们可以将代码处理为
MVC
和Router
M
: 可以理解为model
数据层,存放和操作数据V
: 可以理解为view
视图层,展示渲染数据C
: 可以理解为controller
业务控制层,控制业务,将数据从model
取出交给view
Router
: 路由层,根据请求做出相应逻辑操作,开展业务- 将代码分开处理:
Router
:接收到请求后,调用相对的controller
开展业务controller
:调用对应的model
取出数据,取出数据或让model
操作数据,然后返回数据或结果model
:按照需求,对数据库中的数据进行增删改查view
:展示渲染数据- 对业务分层其实就是对
MVC
架构原理的一个体现,同时让代码逻辑清晰,避免代码臃肿混乱
// Router 路由层 ——————————————————————————————————————————
var express = require("express");
var router = express.Router();
let { userModel } = require("../dbmodel/userModel");
// 引入controller 层逻辑,接到请求后,调用controller层代码
let userController = require("../controllers/UserController");
// 添加用户
router.post("/user", userController.addUser);
// 获取用户
router.get("/user", userController.getUser);
// 删除用户
router.delete("/user", userController.deleteUser);
// 修改用户
router.put("/user", userController.updateUser);
module.exports = router;
// controller 业务逻辑层 从model取数据 向view响应数据
// 引入对应的model层 根据对应的业务逻辑,调用对应的model层操作数据
const userService = require("../services/UserService");
const userController = {
addUser: async (req, res) => {
await userService.addUser(req, res);
res.send({ add: "ok" });
},
deleteUser: async (req, res) => {
await userService.deleteUser(req.body);
res.send({ delete: "ok" });
},
updateUser: async (req, res) => {
await userService.updateUser({ username: req.body.username }, req.body);
res.send({ update: "ok" });
},
getUser: async (req, res) => {
let data = await userService.getUser(req, res);
res.send(data);
},
};
module.exports = userController;
// model 数据层 对数据增删改查
// 引入数据库模型(表) ,对数据 增删改查
let { userModel } = require("../dbmodel/userModel");
const userService = {
addUser: (req, res) => {
return userModel.create(req.body).then((data) => {
console.log(data);
});
},
deleteUser: (query) => {
return userModel.deleteMany(query).then((data) => {
console.log(data);
});
},
updateUser: (searchParams, value) => {
return userModel.updateOne(searchParams, value).then((data) => {
console.log(data);
});
},
getUser: (req, res) => {
return userModel
.find({}, ["username", "age"])
.sort({ age: 1 })
.skip(0)
.limit(100);
},
};
module.exports = userService;
五、登录鉴权
1. Cookie&Session
Cookie & Session
,是用来进行权限控制的- 例如,用户登录后才能跳转到应用主页
- 但是如果用户不登录,而是直接在地址栏通过输入
url
地址,也可以跳转到主页,这个时候我们就要进行后端的权限控制,检测用户登录后,才能让他去某些页面,否则就重定向到登录页- 而
Cookie & Session
就是用来进行权限控制的一个标记状态,某些页面必须携带有效的Cookie
才被允许访问- 而有了
Cookie
还要session
是因为:Cookie
很容易被伪造,所以在cookie
中存储了一个与session
相对于的sessionId
,登录后根据Cookie
中的sessionId
能匹配到相应的session
才算Cookie
有效,否则视为无效Cookie
Cookie
中的SeesionId
如同一把钥匙,而Session
就是一个房间,两者是一一对应的,访问页面或发送请求时,会自动根据Cookie
去匹配对应的Session
看它是否存在- 用户登陆成功后,就会 在服务端存储一个
session
,并 在客户端存储一个Cookie
,Cookie
中存储的是一个与Session
相匹配的SessionId
,根据Cookie
中的SessionId
能匹配到相对应Session
,才能访问某些需要权限的页面或接口express-session
会自动在客户端设置cookie
,不需要我们操作Cookie
也会伴随着http
请求自动发送给后端,也不需要我们操作- 用户如果手动退出,服务端就会摧毁
session
,导致cookie
找不到对应的session
,从而失效- 代码操作:
- 下载
express-session
中间件,自动处理cookie
和session
- 下载
connect-mongo
中间件,将session
存储带服务器,减轻内存压力,避免服务器重启刷新丢失session
- 在应用入口文件中,引入、注册 中间件,同时在注册
express-session
中间件时,在内部配置connect-mongo
,把session
存在服务器- 用户登录后,设置
session
- 在入口文件中,路由中间件前面,设置
Cookie-Seesion
校验中间件,如果是登录相关的放行next()
,否则匹配session
,
- 匹配成功, 重新设置
session
,让Cookie
重新计时,然后放行- 匹配失败,是路由请求就重定向到
login
,是api
请求就返回错误状态码- 逻辑流程:
- 登录
- 成功 => 在服务端存
session
,自动在客户端种Cookie
- 失败 => 重新登录
- 发送请求
- 应用中间件拦截
- 登录请求,
next()
- 路由请求,
session
匹配,成功重设session
,next()
,失败重定向api
请求,session
匹配,成功重设session
,next()
,失败返回401
错误码
- 引入
express-session
中间件 - 注册
express-session
中间件 - 注册中间件 校验匹配
session
// 引入express-session
var session = require("express-session");
// 引入 connect-mongo ,将session存在数据库
let MongoStore = require("connect-mongo");
// 注册session中间件
app.use(
session({
// 设置cookie名
name: "zxf_session",
// 设置加密
secret: "sjdfkjsdlfksjd",
// 设置cookie过期时间
cookie: {
// 一小时
maxAge: 1000 * 60 * 60,
// http可以访问,true为仅https能访问
secure: false,
},
// 再重新设置session时,自动重新计时过期时间
resave: true,
// 初始化cookie,一开始就给客户端一个cookie,只不过是未激活无效的
saveUninitialized: true,
// 超时前刷新,cookie重新计时;false表示无论怎么刷新,都是按照第一次开始计时
rolling: true,
// 将session存到数据库,避免内存开销过大,以及服务器刷新=>导致内存释放,session丢失
// 创建数据库
store: MongoStore.create({
// 数据库地址
mongoUrl: "mongodb://127.0.0.1:27017/zxf_session",
// 过期时间 过期或退出,会自动操作session
ttl: 1000 * 60 * 60,
}),
})
);
// 注册中间件 校验 cookie-session
app.use((req, res, next) => {
if (req.url.includes("login")) {
next();
return;
}
if (req.session.user) {
// session校验通过,重新设置,刷新Cookie时间
req.session.date = Date.now();
next();
} else {
if (req.url.includes("api")) {
res.status(401).json({ ok: 0 });
} else {
res.redirect("/login");
}
}
});
- 登录后,生成
session
保存在服务端,同时express-session
会自动向客户端种入Cookie
login: async (req, res) => {
let data = await userService.login(req, res);
if (data.length === 0) {
// 登陆失败,返回错误码
res.send({ ok: 0 });
} else {
// 登陆成功,在服务端存session,客户端种Cookie,然后拿着有效Cookie才能执行请求
req.session.user = true;
res.send({ ok: 1 });
}
},
- 退出时,直接 销毁服务端的
session
quit: async (req, res) => {
// req.session.destory() 方法销毁服务端session
req.session.destroy(() => {
res.send({ quit: "ok" });
});
},
「HTTP 无状态」我们知道,HTTP 是无状态的。也就是说,HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。但有的场景下,我们需要维护状态。最典型的,一个用户登陆微博,发布、关注、评论,都应是在登录后的用户状态下的。「标记」那解决办法是什么呢?
const express = require("express");
const session = require("express-session");
const MongoStore = require("connect-mongo");
const app = express();
app.use(
session({
secret: "this is session", // 服务器生成 session 的签名
resave: true,
saveUninitialized: true, //强制将为初始化的 session 存储
cookie: {
maxAge: 1000 * 60 * 10,// 过期时间
secure: false, // 为 true 时候表示只有 https 协议才能访问cookie
},
rolling: true, //为 true 表示 超时前刷新,cookie 会重新计时; 为 false 表示在超时前刷新多少次,都是按照第一次刷新开始计时。
store: MongoStore.create({
mongoUrl: 'mongodb://127.0.0.1:27017/zxf_session',
ttl: 1000 * 60 * 10 // 过期时间
}),
})
);
app.use((req,res,next)=>{
if(req.url==="/login"){
next()
return;
}
if(req.session.user){
req.session.garbage = Date();
next();
}else{
res.redirect("/login")
}
})
2. JSON Web Token (JWT)
(1)介绍
我为什么要保存这可恶的session呢, 只让每个客户端去保存该多好?
当然, 如果一个人的token 被别人偷走了, 那我也没办法, 我也会认为小偷就是合法用户, 这其实和一个人的session id 被别人偷走是一样的。
这样一来, 我就不保存session id 了, 我只是生成token , 然后验证token , 我用我的CPU计算时间获取了我的session 存储空间 !
解除了session id这个负担, 可以说是无事一身轻, 我的机器集群现在可以轻松地做水平扩展, 用户访问量增大, 直接加机器就行。 这种无状态的感觉实在是太好了!
缺点:
- 占带宽,正常情况下要比 session_id 更大,需要消耗更多流量,挤占更多带宽,假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。听起来并不多,但日积月累也是不小一笔开销。实际上,许多人会在 JWT 中存储的信息会更多;
- 无法在服务端注销,那么久很难解决劫持问题;
- 性能问题,JWT 的卖点之一就是加密签名,由于这个特性,接收方得以验证 JWT 是否有效且被信任。对于有着严格性能要求的 Web 应用,这并不理想,尤其对于单线程环境。
注意:
CSRF攻击的原因是浏览器会自动带上cookie,而不会带上token;
以CSRF攻击为例:
cookie:用户点击了链接,cookie未失效,导致发起请求后后端以为是用户正常操作,于是进行扣款操作;
token:用户点击链接,由于浏览器不会自动带上token,所以即使发了请求,后端的token验证不会通过,所以不会进行扣款操作;
(2)实现
//jsonwebtoken 封装
//引入中间件
const jsonwebtoken = require("jsonwebtoken")
const secret = "zxf"
const JWT = {
//创建生成token的方法
generate(value,exprires){
//中间件的 sign 方法用来生成 token,参数1是要加密的字段信息,参数2是秘钥,参数3是过期时间
return jsonwebtoken.sign(value,secret,{expiresIn:exprires})
},
verify(token){
try{
// 中间件的 verify 方法是用来解密校验token的,参数1是token,参数2是秘钥
return jsonwebtoken.verify(token,secret)
}catch(e){
return false
}
}
}
module.exports = JWT
//node中间件校验
app.use((req,res,next)=>{
// 如果token有效 ,next()
// 登陆成功前,没有 token ,所以登录不需要校验
if(req.url==="/login"){
next()
return;
}
// 获取token
const token = req.headers["authorization"].split(" ")[1]
//有 token 是axios请求,需要校验
if(token){
var payload = JWT.verify(token)
// console.log(payload)
if(payload){
//校验成功,刷新 token ,放行
const newToken = JWT.generate({
_id:payload._id,
username:payload.username
},"1d")
res.header("Authorization",newToken)
next()
}else{
// 如果token过期了, 返回401错误
res.status(401).send({errCode:"-1",errorInfo:"token过期"})
}
}
})
// 登陆成功 生成token 放在响应头中返回给前端
const token = JWT.generate({
_id: result[0]._id,
username: result[0].username
}, "1d")
res.header("Authorization", token)
import axios from 'axios'
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// 请求时,把 token 放在请求头 发给后端
const token = localStorage.getItem("token")
config.headers.Authorization = `Bearer ${token}`
return config;
}, function (error) {
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// 响应成功后,有返回新 token 就存在本地刷新 token
const {authorization } = response.headers
authorization && localStorage.setItem("token",authorization)
return response;
}, function (error) {
const {status} = error.response
if(status===401){
// 如果请求失败,失败原因是 token 过期,那就去重新登陆
localStorage.removeItem("token")
window.location.href="/login"
}
return Promise.reject(error);
});
Cookie-session
,是把用户的session
存储在一起,放在内存或数据库中,比较占资源,且可能受到CSRF
跨站攻击- 而
JWT
,是 把一段用户信息通过加密后生成一段字符串,加密的秘钥在服务端保存着
使用
JWT
鉴权
- 下载
jsonwebtoken
中间件- 创建一个工具文件在
utils
目录中,创建一个JWT
对象,它有两个方法,一个用来 生成token
,一个用来验证token
- 将对象引入到入口文件中,注册一个校验中间件
- 在用户登录成功后,马上在生成
token
,并在响应头中设置token
- 因为
token
被携带在axios
请求头中,所以只有api
请求需要校验,路由请求不会携带token
,而用户登录成功后才会有token
,因此:
- 如果是
登录接口
,直接放行next()
- 如果是
路由请求
,直接放行next()
,没有携带token
的就是路由请求- 如果是
接口请求
,接口请求都携带了token
,拿到token
进行解密
- 解密失败,说明过期无效,向前端返回错误状态码
- 解密成功,刷新
token
,放在响应头中返回,然后next()
- 前端处理
token
- 在每次请求时,在
axios
请求拦截器中设置,将本地token
取出,放入请求头中- 每次有了响应结果后,在响应拦截器中,检测结果
- 如果又返回
新token
,存在本地刷新token
- 如果报错,处理错误
六、文件上传管理
Multer 是一个 node.js 中间件,用于处理 multipart/form-data
类型的表单数据,它主要用于上传文件。
注意: Multer 不会处理任何非 multipart/form-data
类型的表单数据。
npm install --save multer
//前后端分离-前端
const params = new FormData()
params.append('zxffile', file.file)
params.append('username', this.username)
const config = {
headers: {
"Content-Type":"multipart/form-data"
}
}
http.post('/api/upload', params, config).then(res => {
this.imgpath = 'http://localhost:3000' + res.data
})
Multer 会添加一个 body
对象 以及 file
或 files
对象 到 express 的 request
对象中。 body
对象包含表单的文本域信息,file
或 files
对象包含对象表单上传的文件信息。
//前后端分离-后端
router.post('/upload', upload.single('zxffile'),function(req, res, next) {
console.log(req.file)
})
七、APIDOC - API 文档生成工具
apidoc 是一个简单的 RESTful API 文档生成工具,它从代码注释中提取特定格式的内容生成文档。支持诸如 Go、Java、C++、Rust 等大部分开发语言,具体可使用 apidoc lang
命令行查看所有的支持列表。
apidoc 拥有以下特点:
- 跨平台,linux、windows、macOS 等都支持;
- 支持语言广泛,即使是不支持,也很方便扩展;
- 支持多个不同语言的多个项目生成一份文档;
- 输出模板可自定义;
- 根据文档生成 mock 数据;
npm install -g apidoc
##
注意:
(1) 在当前文件夹下 apidoc.json
{
"name": "****接口文档",
"version": "1.0.0",
"description": "关于****的接口文档描述",
"title": "****"
}
(2)可以利用vscode apidoc snippets 插件创建api
八、Koa2
1.简介
koa 是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套,并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。
2. 快速开始
2.1 安装koa2
# 初始化package.json
npm init
# 安装koa2
npm install koa
2.2 hello world 代码
const Koa = require('koa')
const app = new Koa()
app.use( async ( ctx ) => {
ctx.body = 'hello koa2' //json数据
})
app.listen(3000)
2.3 启动demo
node index.js
3. koa vs express
通常都会说 Koa 是洋葱模型,这重点在于中间件的设计。但是按照上面的分析,会发现 Express 也是类似的,不同的是Express 中间件机制使用了 Callback 实现,这样如果出现异步则可能会使你在执行顺序上感到困惑,因此如果我们想做接口耗时统计、错误处理 Koa 的这种中间件模式处理起来更方便些。最后一点响应机制也很重要,Koa 不是立即响应,是整个中间件处理完成在最外层进行了响应,而 Express 则是立即响应。
3.1更轻量
- koa 不提供内置的中间件;
- koa 不提供路由,而是把路由这个库分离出来了(koa/router)
3.2 Context对象
koa增加了一个Context的对象,作为这次请求的上下文对象(在koa2中作为中间件的第一个参数传入)。同时Context上也挂载了Request和Response两个对象。和Express类似,这两个对象都提供了大量的便捷方法辅助开发, 这样的话对于在保存一些公有的参数的话变得更加合情合理
3.3 异步流程控制
express采用callback来处理异步, koa v1采用generator,koa v2 采用async/await。
generator和async/await使用同步的写法来处理异步,明显好于callback和promise,
3.4 中间件模型
express基于connect中间件,线性模型;
koa中间件采用洋葱模型(对于每个中间件,在完成了一些事情后,可以非常优雅的将控制权传递给下一个中间件,并能够等待它完成,当后续的中间件完成处理后,控制权又回到了自己)
//同步
var express = require("express")
var app = express()
app.use((req,res,next)=>{
console.log(1)
next()
console.log(4)
res.send("hello")
})
app.use(()=>{
console.log(3)
})
app.listen(3000)
//异步
var express = require("express")
var app = express()
app.use(async (req,res,next)=>{
console.log(1)
await next()
console.log(4)
res.send("hello")
})
app.use(async ()=>{
console.log(2)
await delay(1)
console.log(3)
})
function delay(time){
return new Promise((resolve,reject)=>{
setTimeout(resolve,1000)
})
}
//同步
var koa = require("koa")
var app = new koa()
app.use((ctx,next)=>{
console.log(1)
next()
console.log(4)
ctx.body="hello"
})
app.use(()=>{
console.log(3)
})
app.listen(3000)
//异步
var koa = require("koa")
var app = new koa()
app.use(async (ctx,next)=>{
console.log(1)
await next()
console.log(4)
ctx.body="hello"
})
app.use(async ()=>{
console.log(2)
await delay(1)
console.log(3)
})
function delay(time){
return new Promise((resolve,reject)=>{
setTimeout(resolve,1000)
})
}
app.listen(3000)
4. 路由
4.1基本用发
var Koa = require("koa")
var Router = require("koa-router")
var app = new Koa()
var router = new Router()
router.post("/list",(ctx)=>{
ctx.body=["111","222","333"]
})
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)
4.2 router.allowedMethods作用
4.3 请求方式
Koa-router 请求方式: get
、 put
、 post
、 patch
、 delete
、 del
,而使用方法就是 router.方式()
,比如 router.get()
和 router.post()
。而 router.all()
会匹配所有的请求方法。
var Koa = require("koa")
var Router = require("koa-router")
var app = new Koa()
var router = new Router()
router.get("/user",(ctx)=>{
ctx.body=["aaa","bbb","ccc"]
})
.put("/user/:id",(ctx)=>{
ctx.body={ok:1,info:"user update"}
})
.post("/user",(ctx)=>{
ctx.body={ok:1,info:"user post"}
})
.del("/user/:id",(ctx)=>{
ctx.body={ok:1,info:"user del"}
})
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)
4.4 拆分路由
list.js
var Router = require("koa-router")
var router = new Router()
router.get("/",(ctx)=>{
ctx.body=["111","222","333"]
})
.put("/:id",(ctx)=>{
ctx.body={ok:1,info:"list update"}
})
.post("/",(ctx)=>{
ctx.body={ok:1,info:"list post"}
})
.del("/:id",(ctx)=>{
ctx.body={ok:1,info:"list del"}
})
module.exports = router
index.js
var Router = require("koa-router")
var router = new Router()
var user = require("./user")
var list = require("./list")
router.use('/user', user.routes(), user.allowedMethods())
router.use('/list', list.routes(), list.allowedMethods())
module.exports = router
entry入口
var Koa = require("koa")
var router = require("./router/index")
var app = new Koa()
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)
4.5 路由前缀
router.prefix('/api')
4.6 路由重定向
router.get("/home",(ctx)=>{
ctx.body="home页面"
})
//写法1
router.redirect('/', '/home');
//写法2
router.get("/",(ctx)=>{
ctx.redirect("/home")
})
5. 静态资源
const Koa = require('koa')
const path = require('path')
const static = require('koa-static')
const app = new Koa()
app.use(static(
path.join( __dirname, "public")
))
app.use( async ( ctx ) => {
ctx.body = 'hello world'
})
app.listen(3000, () => {
console.log('[demo] static-use-middleware is starting at port 3000')
})
6. 获取请求参数
6.1get参数
在koa中,获取GET请求数据源头是koa中request对象中的query方法或querystring方法,query返回是格式化好的参数对象,querystring返回的是请求字符串,由于ctx对request的API有直接引用的方式,所以获取GET请求数据有两个途径。
- 是从上下文中直接获取 请求对象ctx.query,返回如 { a:1, b:2 } 请求字符串 ctx.querystring,返回如 a=1&b=2
- 是从上下文的request对象中获取 请求对象ctx.request.query,返回如 { a:1, b:2 } 请求字符串 ctx.request.querystring,返回如 a=1&b=2
6.2post参数
对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到ctx.request.body中
const bodyParser = require('koa-bodyparser')
// 使用ctx.body解析中间件
app.use(bodyParser())
7. ejs模板
7.1 安装模块
# 安装koa模板使用中间件
npm install --save koa-views
# 安装ejs模板引擎
npm install --save ejs
7.2 使用模板引擎
文件目录
├── package.json
├── index.js
└── view
└── index.ejs
./index.js文件
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()
// 加载模板引擎
app.use(views(path.join(__dirname, './view'), {
extension: 'ejs'
}))
app.use( async ( ctx ) => {
let title = 'hello koa2'
await ctx.render('index', {
title,
})
})
app.listen(3000)
./view/index.ejs 模板
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1><%= title %></h1>
<p>EJS Welcome to <%= title %></p>
</body>
</html>
8. cookie&session
8.1 cookie
koa提供了从上下文直接读取、写入cookie的方法
- ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
- ctx.cookies.set(name, value, [options]) 在上下文中写入cookie
8.2 session
-
koa-session-minimal 适用于koa2 的session中间件,提供存储介质的读写接口 。
const session = require('koa-session-minimal') app.use(session({ key: 'SESSION_ID', cookie: { maxAge:1000*60 } }))
app.use(async (ctx, next) => { //排除login相关的路由和接口 if (ctx.url.includes("login")) { await next() return } if (ctx.session.user) { //重新设置以下sesssion ctx.session.mydate = Date.now() await next() } else { ctx.redirect("/login") } })
9. JWT
app.use(async(ctx, next) => {
//排除login相关的路由和接口
if (ctx.url.includes("login")) {
await next()
return
}
const token = ctx.headers["authorization"]?.split(" ")[1]
// console.log(req.headers["authorization"])
if(token){
const payload= JWT.verify(token)
if(payload){
//重新计算token过期时间
const newToken = JWT.generate({
_id:payload._id,
username:payload.username
},"10s")
ctx.set("Authorization",newToken)
await next()
}else{
ctx.status = 401
ctx.body = {errCode:-1,errInfo:"token过期"}
}
}else{
await next()
}
})
10.上传文件
https://www.npmjs.com/package/@koa/multer
npm install --save @koa/multer multer
const multer = require('@koa/multer');
const upload = multer({ dest: 'public/uploads/' })
router.post("/",upload.single('avatar'),
(ctx,next)=>{
console.log(ctx.request.body,ctx.file)
ctx.body={
ok:1,
info:"add user success"
}
})
11.操作MongoDB
const mongoose = require("mongoose")
mongoose.connect("mongodb://127.0.0.1:27017/zxf_project")
//插入集合和数据,数据库zxf_project会自动创建
const mongoose = require("mongoose")
const Schema = mongoose.Schema
const UserType = {
username:String,
password:String,
age:Number,
avatar:String
}
const UserModel = mongoose.model("user",new Schema(UserType))
// 模型user 将会对应 users 集合,
module.exports = UserModel
九、MySQL
1.介绍
付费的商用数据库:
- Oracle,典型的高富帅;
- SQL Server,微软自家产品,Windows定制专款;
- DB2,IBM的产品,听起来挺高端;
- Sybase,曾经跟微软是好基友,后来关系破裂,现在家境惨淡。
这些数据库都是不开源而且付费的,最大的好处是花了钱出了问题可以找厂家解决,不过在Web的世界里,常常需要部署成千上万的数据库服务器,当然不能把大把大把的银子扔给厂家,所以,无论是Google、Facebook,还是国内的BAT,无一例外都选择了免费的开源数据库:
- MySQL,大家都在用,一般错不了;
- PostgreSQL,学术气息有点重,其实挺不错,但知名度没有MySQL高;
- sqlite,嵌入式数据库,适合桌面和移动应用。
作为一个JavaScript全栈工程师,选择哪个免费数据库呢?当然是MySQL。因为MySQL普及率最高,出了错,可以很容易找到解决方法。而且,围绕MySQL有一大堆监控和运维的工具,安装和使用很方便。
2.与非关系数据库区别
关系型和非关系型数据库的主要差异是数据存储的方式。关系型数据天然就是表格式的,因此存储在数据表的行和列中。数据表可以彼此关联协作存储,也很容易提取数据。
与其相反,非关系型数据不适合存储在数据表的行和列中,而是大块组合在一起。非关系型数据通常存储在数据集中,就像文档、键值对或者图结构。你的数据及其特性是选择数据存储和提取方式的首要影响因素。
关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织
优点:
1、易于维护:都是使用表结构,格式一致;
2、使用方便:SQL语言通用,可用于复杂查询;
3、复杂操作:支持SQL,可用于一个表以及多个表之间非常复杂的查询。
缺点:
1、读写性能比较差,尤其是海量数据的高效率读写;
2、固定的表结构,灵活度稍欠;
3、高并发读写需求,传统关系型数据库来说,硬盘I/O是一个很大的瓶颈。
非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合,可以是文档或者键值对等。
优点:
1、格式灵活:存储数据的格式可以是key,value形式、文档形式、图片形式等等,文档形式、图片形式等等,使用灵活,应用场景广泛,而关系型数据库则只支持基础类型。
2、速度快:nosql可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘;
3、高扩展性;
4、成本低:nosql数据库部署简单,基本都是开源软件。
缺点:
1、不提供sql支持;
2、无事务处理;
3、数据结构相对复杂,复杂查询方面稍欠。
3.sql语句
插入:
INSERT INTO `students`(`id`, `name`, `score`, `gender`) VALUES (null,'zxf',100,1)
//可以不设置id,create_time
更新:
UPDATE `students` SET `name`='tiechui',`score`=20,`gender`=0 WHERE id=2;
删除:
DELETE FROM `students` WHERE id=2;
查询:
查所有的数据所有的字段
SELECT * FROM `students` WHERE 1;
查所有的数据某个字段
SELECT `id`, `name`, `score`, `gender` FROM `students` WHERE 1;
条件查询
SELECT * FROM `students` WHERE score>=80;
SELECT * FROM `students` where score>=80 AND gender=1
模糊查询
SELECT * FROM `students` where name like '%k%'
排序
SELECT id, name, gender, score FROM students ORDER BY score;
SELECT id, name, gender, score FROM students ORDER BY score DESC;
分页查询
SELECT id, name, gender, score FROM students LIMIT 50 OFFSET 0
记录条数
SELECT COUNT(*) FROM students;
SELECT COUNT(*) zxfnum FROM students;
多表查询
SELECT * FROM students, classes;(这种多表查询又称笛卡尔查询,使用笛卡尔查询时要非常小心,由于结果集是目标表的行数乘积,对两个各自有100行记录的表进行笛卡尔查询将返回1万条记录,对两个各自有1万行记录的表进行笛卡尔查询将返回1亿条记录)
SELECT
students.id sid,
students.name,
students.gender,
students.score,
classes.id cid,
classes.name cname
FROM students, classes; (要使用表名.列名这样的方式来引用列和设置别名,这样就避免了结果集的列名重复问题。)
SELECT
s.id sid,
s.name,
s.gender,
s.score,
c.id cid,
c.name cname
FROM students s, classes c; (SQL还允许给表设置一个别名)
联表查询
SELECT s.id, s.name, s.class_id, c.name class_name, s.gender, s.score
FROM students s
INNER JOIN classes c
ON s.class_id = c.id; (连接查询对多个表进行JOIN运算,简单地说,就是先确定一个主表作为结果集,然后,把其他表的行有选择性地“连接”在主表结果集上。)
注意:
- InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
- InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;
外键约束
CASCADE
在父表上update/delete记录时,同步update/delete掉子表的匹配记录
SET NULL
在父表上update/delete记录时,将子表上匹配记录的列设为null (要注意子表的外键列不能为not null)
NO ACTION
如果子表中有匹配的记录,则不允许对父表对应候选键进行update/delete操作
RESTRICT
同no action, 都是立即检查外键约束
4.nodejs 操作数据库
const express = require('express')
const app = express()
const mysql2 = require('mysql2')
const port = 9000
app.get('/',async (req, res) => {
const config = getDBConfig()
const promisePool = mysql2.createPool(config).promise();
// console.log(promisePool)
let user = await promisePool.query('select * from students');
console.log(user)
if (user[0].length) {
//存在用户
res.send(user[0])
} else {
//不存在
res.send( {
code: -2,
msg: 'user not exsit',
})
}
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
function getDBConfig() {
return {
host: '127.0.0.1',
user: 'root',
port: 3306,
password: '',
database: 'zxf_test',
connectionLimit: 1 //创建一个连接池
}
}
查询:
promisePool.query('select * from users');
插入:
promisePool.query('INSERT INTO `users`(`id`,`name`,`age`, `password`) VALUES (?,?,?,?)',[null,"zxf",100,"123456"]);
更新:
promisePool.query(`UPDATE users SET name = ? ,age=? WHERE id = ?`,["xiaoming2",20,1])
删除:
promisePool.query(`delete from users where id=?`,[1])
十、Socket编程
1.websocket介绍
应用场景:
-
弹幕
-
媒体聊天
-
协同编辑
-
基于位置的应用
-
体育实况更新
-
股票基金报价实时更新
WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。我们来看看WebSocket连接是如何创建的。
首先,WebSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求,格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13
该请求和普通的HTTP请求有几点不同:
- GET请求的地址不是类似
/path/
,而是以ws://
开头的地址; - 请求头
Upgrade: websocket
和Connection: Upgrade
表示这个连接将要被转换为WebSocket连接; Sec-WebSocket-Key
是用于标识这个连接,并非用于加密数据;Sec-WebSocket-Version
指定了WebSocket的协议版本。
随后,服务器如果接受该请求,就会返回如下响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
该响应代码101
表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket
指定的WebSocket协议。
版本号和子协议规定了双方能理解的数据格式,以及是否支持压缩等等。如果仅使用WebSocket的API,就不需要关心这些。
现在,一个WebSocket连接就建立成功,浏览器和服务器就可以随时主动发送消息给对方。消息有两种,一种是文本,一种是二进制数据。通常,我们可以发送JSON格式的文本,这样,在浏览器处理起来就十分容易。
为什么WebSocket连接可以实现全双工通信而HTTP连接不行呢?实际上HTTP协议是建立在TCP协议之上的,TCP协议本身就实现了全双工通信,但是HTTP协议的请求-应答机制限制了全双工通信。WebSocket连接建立以后,其实只是简单规定了一下:接下来,咱们通信就不使用HTTP协议了,直接互相发数据吧。
安全的WebSocket连接机制和HTTPS类似。首先,浏览器用wss://xxx
创建WebSocket连接时,会先通过HTTPS创建安全的连接,然后,该HTTPS连接升级为WebSocket连接,底层通信走的仍然是安全的SSL/TLS协议。
浏览器支持
很显然,要支持WebSocket通信,浏览器得支持这个协议,这样才能发出ws://xxx
的请求。目前,支持WebSocket的主流浏览器如下:
- Chrome
- Firefox
- IE >= 10
- Sarafi >= 6
- Android >= 4.4
- iOS >= 8
服务器支持
由于WebSocket是一个协议,服务器具体怎么实现,取决于所用编程语言和框架本身。Node.js本身支持的协议包括TCP协议和HTTP协议,要支持WebSocket协议,需要对Node.js提供的HTTPServer做额外的开发。已经有若干基于Node.js的稳定可靠的WebSocket实现,我们直接用npm安装使用即可。
2.ws模块
服务器:
const WebSocket = require("ws")
WebSocketServer = WebSocket.WebSocketServer
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('message', function message(data, isBinary) {
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
});
});
ws.send('欢迎加入聊天室');
});
客户端:
var ws = new WebSocket("ws://localhost:8080")
ws.onopen = ()=>{
console.log("open")
}
ws.onmessage = (evt)=>{
console.log(evt.data)
}
授权验证:
//前端
var ws = new WebSocket(`ws://localhost:8080?token=${localStorage.getItem("token")}`)
ws.onopen = () => {
console.log("open")
ws.send(JSON.stringify({
type: WebSocketType.GroupList
}))
}
ws.onmessage = (evt) => {
console.log(evt.data)
}
//后端
const WebSocket = require("ws");
const JWT = require('../util/JWT');
WebSocketServer = WebSocket.WebSocketServer
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws, req) {
const myURL = new URL(req.url, 'http://127.0.0.1:3000');
const payload = JWT.verify(myURL.searchParams.get("token"))
if (payload) {
ws.user = payload
ws.send(createMessage(WebSocketType.GroupChat, ws.user, "欢迎来到聊天室"))
sendBroadList() //发送好友列表
} else {
ws.send(createMessage(WebSocketType.Error, null, "token过期"))
}
// console.log(3333,url)
ws.on('message', function message(data, isBinary) {
const messageObj = JSON.parse(data)
switch (messageObj.type) {
case WebSocketType.GroupList:
ws.send(createMessage(WebSocketType.GroupList, ws.user, JSON.stringify(Array.from(wss.clients).map(item => item.user))))
break;
case WebSocketType.GroupChat:
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(createMessage(WebSocketType.GroupChat, ws.user, messageObj.data));
}
});
break;
case WebSocketType.SingleChat:
wss.clients.forEach(function each(client) {
if (client.user.username === messageObj.to && client.readyState === WebSocket.OPEN) {
client.send(createMessage(WebSocketType.SingleChat, ws.user, messageObj.data));
}
});
break;
}
ws.on("close",function(){
//删除当前用户
wss.clients.delete(ws.user)
sendBroadList() //发送好用列表
})
});
});
const WebSocketType = {
Error: 0, //错误
GroupList: 1,//群列表
GroupChat: 2,//群聊
SingleChat: 3//私聊
}
function createMessage(type, user, data) {
return JSON.stringify({
type: type,
user: user,
data: data
});
}
function sendBroadList(){
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(createMessage(WebSocketType.GroupList, client.user, JSON.stringify(Array.from(wss.clients).map(item => item.user))))
}
});
}
3.socket.io模块
服务端:
const io = require('socket.io')(server);
io.on('connection', (socket) => {
const payload = JWT.verify(socket.handshake.query.token)
if (payload) {
socket.user = payload
socket.emit(WebSocketType.GroupChat, createMessage(socket.user, "欢迎来到聊天室"))
sendBroadList() //发送好友列表
} else {
socket.emit(WebSocketType.Error, createMessage(null, "token过期"))
}
socket.on(WebSocketType.GroupList, () => {
socket.emit(WebSocketType.GroupList, createMessage(null, Array.from(io.sockets.sockets).map(item => item[1].user).filter(item=>item)));
})
socket.on(WebSocketType.GroupChat, (messageObj) => {
socket.broadcast.emit(WebSocketType.GroupChat, createMessage(socket.user, messageObj.data));
})
socket.on(WebSocketType.SingleChat, (messageObj) => {
Array.from(io.sockets.sockets).forEach(function (socket) {
if (socket[1].user.username === messageObj.to) {
socket[1].emit(WebSocketType.SingleChat, createMessage(socket[1].user, messageObj.data));
}
})
})
socket.on('disconnect', reason => {
sendBroadList() //发送好用列表
});
});
function sendBroadList() {
io.sockets.emit(WebSocketType.GroupList, createMessage(null, Array.from(io.sockets.sockets).map(item => item[1].user).filter(item=>item)))
}
//最后filter,是因为 有可能存在null的值
客户端:
const WebSocketType = {
Error: 0, //错误
GroupList: 1, //群列表
GroupChat: 2, //群聊
SingleChat: 3 //私聊
}
const socket = io(`ws://localhost:3000?token=${localStorage.getItem("token")}`);
socket.on("connect",()=>{
socket.emit(WebSocketType.GroupList)
})
socket.on(WebSocketType.GroupList, (messageObj) => {
select.innerHTML = ""
select.innerHTML = `<option value="all">all</option>` + messageObj.data.map(item => `
<option value="${item.username}">${item.username}</option>`).join("")
})
socket.on(WebSocketType.GroupChat, (msg) => {
console.log(msg)
})
socket.on(WebSocketType.SingleChat, (msg) => {
console.log(msg)
})
socket.on(WebSocketType.Error, (msg) => {
localStorage.removeItem("token")
location.href = "/login"
})
send.onclick = () => {
if (select.value === "all") {
socket.emit(WebSocketType.GroupChat,{
data: text.value
})
} else {
socket.emit(WebSocketType.SingleChat,{
data: text.value,
to:select.value
})
}
}
十一、mocha
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如对函数abs(),我们可以编写出以下几个测试用例:
输入正数,比如1、1.2、0.99,期待返回值与输入相同;
输入负数,比如-1、-1.2、-0.99,期待返回值与输入相反;
输入0,期待返回0;
输入非数值类型,比如null、[]、{},期待抛出Error。
把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。
如果单元测试通过,说明我们测试的这个函数能够正常工作。如果单元测试不通过,要么函数有bug,要么测试条件输入不正确,总之,需要修复使单元测试能够通过。
单元测试通过后有什么意义呢?如果我们对abs()函数代码做了修改,只需要再跑一遍单元测试,如果通过,说明我们的修改不会对abs()函数原有的行为造成影响,如果测试不通过,说明我们的修改与原有行为不一致,要么修改代码,要么修改测试。
这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
mocha是JavaScript的一种单元测试框架,既可以在浏览器环境下运行,也可以在Node.js环境下运行。
使用mocha,我们就只需要专注于编写单元测试本身,然后,让mocha去自动运行所有的测试,并给出测试结果。
mocha的特点主要有:
- 既可以测试简单的JavaScript函数,又可以测试异步代码,因为异步是JavaScript的特性之一;
- 可以自动运行所有测试,也可以只运行特定的测试;
- 可以支持before、after、beforeEach和afterEach来编写初始化代码。
1.编写测试
const assert = require('assert');
const sum = require('../test');
describe('#hello.js', () => {
describe('#sum()', () => {
it('sum() should return 0', () => {
assert.strictEqual(sum(), 0);
});
it('sum(1) should return 1', () => {
assert.strictEqual(sum(1), 1);
});
it('sum(1, 2) should return 3', () => {
assert.strictEqual(sum(1, 2), 3);
});
it('sum(1, 2, 3) should return 6', () => {
assert.strictEqual(sum(1, 2, 3), 6);
});
});
});
2.chai断言库
var chai = require('chai')
var assert = chai.assert;
describe('assert Demo', function () {
it('use assert lib', function () {
var value = "hello";
assert.typeOf(value, 'string')
assert.equal(value, 'hello')
assert.lengthOf(value, 5)
})
})
var chai = require('chai');
chai.should();
describe('should Demo', function(){
it('use should lib', function () {
var value = 'hello'
value.should.exist.and.equal('hello').and.have.length(5).and.be.a('string')
// value.should.be.a('string')
// value.should.equal('hello')
// value.should.not.equal('hello2')
// value.should.have.length(5);
})
});
var chai = require('chai');
var expect = chai.expect;
describe('expect Demo', function() {
it('use expect lib', function () {
var value = 'hello'
var number = 3
expect(number).to.be.at.most(5)
expect(number).to.be.at.least(3)
expect(number).to.be.within(1, 4)
expect(value).to.exist
expect(value).to.be.a('string')
expect(value).to.equal('hello')
expect(value).to.not.equal('您好')
expect(value).to.have.length(5)
})
});
3.异步测试
var fs =require("fs").promises
var chai = require('chai');
var expect = chai.expect;
it('test async function',async function () {
const data =await fs.readFile('./1.txt',"utf8");
expect(data).to.equal('hello')
});
4.http测试
const request = require('supertest')
const app = require('../app');
describe('#test koa app', () => {
let server = app.listen(3000);
describe('#test server', () => {
it('#test GET /', async () => {
await request(server)
.get('/')
.expect('Content-Type', /text\/html/)
.expect(200, '<h1>hello world</h1>');
});
after(function () {
server.close()
});
});
});
5.钩子函数
describe('#hello.js', () => {
describe('#sum()', () => {
before(function () {
console.log('before:');
});
after(function () {
console.log('after.');
});
beforeEach(function () {
console.log(' beforeEach:');
});
afterEach(function () {
console.log(' afterEach.');
});
});
});