- html
- css
- JavaScript
- js核心语法(变量、数据类型、循环、分支、判断、函数、作用域、this、etc)
- WebAPI(DOM操作、BOM操作、基于XMLHttpRequest的Ajax操作、etc)
- 浏览器中有不同的JavaScript解析引擎
Chrome浏览器 => v8(最好) 火狐浏览器 => OdinMoney Safri浏览器 => JSCore ie浏览器 => chakra
- 每个浏览器都内置了DOM、BOM这样的API,因此浏览器中的JavaScript可以调用它们
- 运行环境是指代码正常运行的必要环境(内置API和v8解析引擎),v8引擎负责解析和执行JavaScript代码,内置API是由运行环境提供的特殊接口,只能在所属运行环境中调用
- 通过node.js可以进行后端开发
- node.js是一个基于chrome v8 引擎的JavaScript运行环境
- 官网
- 内置API和v8解析引擎 node.js无法调用DOM和BOM等浏览器内置API
- 基于Express框架,可以快速构建Web应用
- 基于Electron框架,可以构建跨平台的桌面应用
- 基于restify框架,可以快速构建API接口项目
- 读写和操作数据库、创建使用过的命令行工具辅助前端开发、etc..
- JavaScript基础语法+node.js内置API模块(fs,http,path等)+第三方API模块(express,mysql等)
5. Node.js环境的安装官网首页
- 在cmd面板中输入
node -v
- 专为开发人员设计的,用于人机交互的一种方式
- 打开终端
- 输入
node [要执行的js文件的路径]
tab键补齐路径
const fs = require('fs')
fs.readFile()
读取文件内容fs.writeFile()
写入文件内容
fs.readFile(path[,options],callback)
- path 必选,字符串,表示文件路径
- options 可选,表示以什么编码格式读取文件
- callback 必选,回调函数
const fs = require('fs')
fs.readFile('./file/1.txt', 'utf8', function (err, dataStr) {
console.log(err);
console.log('-------');
console.log(dataStr);
})
- 可以判读err对象是否为null,从而知晓文件的读取结果
fs.writeFile(file,data[,options],callback)
- 参数1:必选参数,需要指定一个文件的存放路径。
- 参数2:必选参数,表示要写入的内容
- 参数3:可选参数,表示以什么格式写入文件内容,默认值是utf8
- 参数4:必选参数,文件写入成功后的回调函数
- 导入fs文件系统模块
- 使用 fs.readFile() 方法,读取目录下的成绩.txt文件
- 判断文件是否读取失败
- 文件读取成功后,处理成绩数据
- 将处理完成的成绩数据,调用 fs.writeFile() 方法,写入到新的文件 成绩-ok.txt 中
- 在使用fs模块操作文件时,如果提供的操作路径是以 ./ 或 ../ 开头的 相对路径 时,容易出现路径动态拼接错误的问题
- 原因:代码在运行的时候,会执行node命令时所在的目录 ,动态的拼接出被操作文件的完整路径
- 如果要解决这个问题,可以直接提供一个完整的文件存放路径就行
- path模块是Node.js官方提供的,用来处理路径的模块
- 例如: path.join() 方法,用来将路径片段拼接成一个完整的路径字符串
- 语法格式:
path.join([...paths])
- ...paths <string> 路径片段的序列
- 返回值:<string>
- 使用 path.basename() 方法,可以获取路径中的最后一部分,经常通过这个方法获取路径中的文件名
- 语法格式:
path.basename(path[,ext])
- path / 必选参数,表示一个路径的字符串
- ext / 可选参数,表示文件扩展名
- 返回:/ 表示路径中的最后一部分
- 使用path.extname()方法,可以获取路径中的扩展名
- 语法格式:
path.extname(path)
- path / 必选参数,表示一个路径的字符串
- 返回:/ 返回得到的扩展名字符串
- 消费资源的电脑叫客户端
- 提供网络资源的电脑叫服务器
- http模块是Node.js官方提供的、用来创建web服务器的模块。通过http模块提供的http.createServer()方法能把一台普通的电脑,变成一台web服务器
- 语法:
const http = require('http')
- IP地址相当于互联网上每台计算机的唯一地址
- 点分十进制(192.168.1.1)
- 注意
- 互联网上每台web服务器,都有自己的IP地址
- 自己的电脑也是一台服务器(127.0.0.1)
- 域名(IP的别名)
- 域名服务器(DNS)用于存放IP和域名
- 127.0.0.1对应localhost
- 每一个端口号对应一个web服务
- 每个端口号不能被多个web服务所占用
- 80端口可以省略
- 导入http模块
const http = require('http')
- 创建web服务器实例
const server = http.createServer()
- 为服务器实例绑定request事件,监听客户端请求
server.on('request', (req, res) => { console.log('someone visit our web server') })
- 启动服务器
server.listen(80, () => { console.log('http server runnng at http://127.0.0.1') })
- 只要服务器接收到了客户端的请求,就会都用通过server.on()为服务器绑定的request事件处理函数,如果想在事件处理函数中,访问与客户端相关的数据和属性,可以使用如下方式:
server.on('request', (req) => { //req.url 是客户端请求的URL地址 const url = req.url //req.method 是客户端请求的method类型 const method = req.method const str = `Your request url is ${req.url},and request method is $ {req.method}` console.log(str) })
- 在服务器的request事件处理函数中,如果想要访问与服务器相关的属性和数据,可以使用如下方式
server.on('request', (req,res) => { //req.url 是客户端请求的URL地址 const url = req.url //req.method 是客户端请求的method类型 const method = req.method const str = `Your request url is ${req.url},and request method is ${req.method}` // 调用res.end()方法,向客户端响应一些内容 res.end(str) })
res.setHeader('content-Type','text/html;charset=utf-8')
- 获取 请求的url地址
- 设置 默认的响应内容 为404 Not found
- 判断用户请求的是否为 / 或 /index.html 首页
- 判断用户请求的是否为 /about.html 关于页面
- 设置 Content-Type响应头,防止中文乱码
- 使用 res.end() 把内容响应给客户端
- 提高了代码的复用性
- 提高了代码的可维护性
- 可以实现按需加载
- 使用什么样的语法格式类引入模块
- 在模块中使用什么样的语法向外暴露成员
- 内置模块(内置模块是由Node.js官方提供的,例如fs,path,http等)
- 自定义模块(用户自己创建的.js模块,都是自定义模块)
- 第三方模块(有第三方开发的模块,使用时需要下载)
- 内置模块
require('http')
- 用户自定义模块
require('./custom.js')
- 第三方模块
require('moment')
- 使用require()方法加载其他模块时,会执行加载模块的代码
- 和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域。
- 防止了全局变量污染的问题
- 在每个 .js 自定义模块中都有一个module对象,它里面存储了模块相关的信息
- 在自定义模块中,可以使用module.exports对象,将模块内的成员共享出去,供外界使用
- 外界用require() 方法导入自定义模块时,得到的就是module.exports所指向的对象
- 由于module.export单词写起来比较复杂,为了简化对外共享成员的代码,Node提供了 export 对象。默认情况下,module.export和export指向同一个对象。最终共享的结果,还是以module.export指向的对象为准。
- 时刻谨记,require()模块时,得到的永远时module.exports指向的对象。
- commonJS规范
- 每个模块内部,module变量代表当前模块
- module变量是一个对象,他的export属性(即nodule.exports)是对外的接口。
- 加载某个模块,其实是加载该模块的module.export属性,require()方法用于加载模块。
- Node.js 中的第三方模块又叫包
- 包是由第三方个人或者团队开发出来的
- 包都是免费和开源的
- 包都是内置模块封装出来的,提供了更高级,更方便的API,极大的提高了开发的效率
- node package manager 包管理工具
npm -v
查看自己npm的版本号
- 使用npm包管理工具,在项目中安装格式化时间的包moment
- 使用require()导入格式化时间的包
- 参考moment的官方api文档对时间进行格式化
npm install 包的完整名称
或npm i 包的完整名称
- 可以在包名称后面加入 @ 符号指定包的版本号
- 在项目文件夹下面多出了 node_modules 和 package-lock.json 的配置文件
- node_modules 文件夹用来存放所有已经安装到项目中的包,require()导入第三方包的时候,就是从这个目录中查找并加载包
- package-lock.json 配置文件用来记录node_modules目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等
- 项目名称、版本号、描述等
- 项目中都用到哪些包
- 哪些包只在开发期间会用到
- 哪些包在开发和部署时都需要用到
- 在项目目录中,创建一个叫做package.json的配置文件,即可用来记录项目中安装了哪些包,从而方便剔除node_modules目录后,在团队之间共享项目的源代码。
npm init -y
- 上面的命令只能在英文的目录下运行成功,不能有中文和空格。
- 运行npm install 命令安装包的时候,npm包管理工具会自动把包的名称和版本号记录到package.json中。
- 用来记录您使用npm install 命令安装了哪些包。
- 安装多个包之间用空格隔开
- 没有安装包会报 cannot find modules 包名称
- 可以运行
npm install
一次性安装所有的依赖包
- 可以运行
npm uninstall
卸载指定包(注意uninstall不能简写),命令执行成功后,会把卸载的包,自动从package.json中的dependencies中移除
- 如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到devDependencies节点中,与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到dependencies节点中。
- 可以使用
npm i 包名 -D
或npm i -D 包名
相当于npm install 包名 --save-dev
(可以在npm官网中查看是否需要这种安装方式)
- 查看当前的下包镜像源
npm config get registry
- 切换下包镜像源
npm config set registry=地址
- 淘宝镜像源 https://registry.npm.taobao.org/
- 为了更方便的切换下包的镜像源,可以安装nrm这个小工具
npm i nrm -g
nrm ls
查看所有可用的镜像源nrm use taobao
将下包的镜像源切换为淘宝镜像源
- 被安装到项目的node_modules中的包,都是项目包
- 开发依赖包(被记录到devDependencies节点的包,只有在开发时期会用到,安装的时候会用到 -D 命令)
- 核心依赖包(被记录到dependencies节点的包,在开发时期和项目上线之后都会用到)
- 安装全局包
npm i 包名 -g
- 卸载全局包
npm uninstall 包名 -g
- 只有工具性质的包,才有全局安装的必要性,因为他提供了终端命令
- 可以查看官方文档决定是否全局安装
- 模块在第一次被记载后会被缓存,require()不会导致模块的代码多次执行
- 所有的包都会优先从缓存中加载,从而提高模块的加载效率
- 内置模块是由Node.js 官方提供的模块,内置模块的加载优先级最高
- 加载自定义模块时,必须指定以 ./ 或者 ../ 开头的路径标识符,如果没有指定则node会把他当作内置模块或第三方模块进行加载。
- 在使用require()导入自定义模块时候,如果省略了文件的扩展名,则node.js会按顺序分别尝试加载以下的文件:
- 按照确切的文件名进行加载
- 补全.js扩展名进行加载
- 补全.json扩展名进行加载
- 补全.node 扩展名进行加载
- 加载失败,终端报错
- 如果传递给require()的模块标识符不是内置模块,也没有以 ./ 或../开头,则node.js会从当前的父目录开始,尝试从/node_modules 文件中加载第三方模块。如果没有找到对应的第三方模块,则移动到上一层父目录中,进行加载,直到文件系统的根目录
- 当把目录作为模块标识符,传递给require() 进行加载的时候,有三种加载方式
- 在被加载的目录下查找一个叫做package.json的文件,并寻求main属性,作为require()加载的入口
- 如果目录没有package.json文件,或者main 入口不存在或无法解析,则Node.js将会试图加载目录下的index.js文件
- 如果上两步都失败,则报错 Error:Cannot find module 'xxx'
- Express是基于Node.js平台,快速,开放,🤺的web开发框架。(和http模块类似)通俗理解就是专门用来创建web服务器的。
- web网站服务器:专门对外提供Web网页资源的服务器
- API接口服务器:专门对外提供API接口的服务器
npm i express
// 导入express
const express = require('express')
// 创建web服务器
const app = express()
//启动web服务器
app.listen(8080, () => {
console.log('8080端口启动!');
})
- 通过app.get()方法,可以监听客户端的GET请求
//监听get请求 app.get('/user', (req, res) => { //调用express 提供的 res.send()向客户端响应一个JSON对象 res.send({ name: 'ls', age: 18, gender:'男' }) })
- 通过app.post()方法,可以监听客户端的POST请求
//监听post请求 app.post('/user', (req, res) => { //调用express 提供的 res.send()向客户端响应一个文本字符串 res.send('请求成功') })
res.send({ name: 'ls', age: 18, gender:'男' })
- 通过req.query()对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数
app.get('/', (req, res) => { //通过req.query 可以获取到客户端发送过来的查询参数 // 默认情况req.query 是一个空对象 console.log(req.query); res.send(req.query) })
- 通过req.params对象,可以访问到URL中,通过:匹配的的动态参数
- 共享public目录下的资源
app.use(express.static('public))
- 注意Express在指定的静态目录中查找文件,并对外提供静态资源的访问路径,因此,存放静态文件的目录名不会出现在URL中。
- 多次调用express.static()
app.use(express.static('public')) app.use(express.static('files'))
- 访问静态资源文件时,express.static() 函数会根据目录的添加顺序查找所需的文件
- 如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用:
app.use('/public',express.static('public'))
- 在编写调式Node.js项目的时候,能监听修改的代码
npm i -g nodemon
- 由
node 名称.js
变为nodemon 名称.js
- 按键与服务之间的映射关系
- 在express中,路由指的是客户端与服务器处理函数之间的映射关系
- Express中的路由分3部分组成,分别是请求类型,请求的URL地址,处理函数
- 格式:
app.METHOD(PATH,HANDLER)
app.get('/', (req, res) => {
res.send('请求成功')
})
app.post('/', (req, res) => {
res.send('请求成功')
})
- 每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功,才会调用对应的处理函数,如果URL和请求方式同时匹配才会交给对应的处理函数。
- 把路由挂载到app上
//挂载路由
app.get('/', (req, res) => {
res.send('Hello world')
})
app.post('/', (req, res) => {
res.send('Hello world')
})
- 为了方便对路由进行模块化的管理,Express不建议将路由直接挂载到app上,而是推荐将路由抽离为单独的模块。
- 步骤
- 创建路由模块对应的.js文件
- 调用express.Router()函数创建路由对象
- 向路由对象上挂载具体额路由
- 使用modele.export向外共享路由对象
- 使用app.use()函数注册路由模块
- 类似与托管静态资源
- 中间件特指业务流程的中间处理环节
- 当一个请求到达Express服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理
- express的中间件,本质上就是一个function处理函数,express中间件的格式:
app.get('',(req,res,next)=>{
next()
})
- 路由形参列表必须含有next参数,而路由处理函数只包含req和res
- next函数就是实现多个中间件连续强调的关键,他表示把流转关系转交给下一个中间件或路由
//定义一个最简单的中间件函数
const mw = function (req, res, next) {
console.log('这是最简单的中间件函数');
//把流转关系,转交给下一个中间件或者路由
next()
}
- 客户端发起的任何请求,到达服务器之后,就会触发的中间件,叫做全局有效的中间件
- 通过app.use(中间件函数),即可定义一个全局生效的中间件
//定义一个最简单的中间件函数
const mw = function (req, res, next) {
console.log('这是最简单的中间件函数');
//把流转关系,转交给下一个中间件或者路由
next()
}
//将 mw 注册为全局生效的中间件
app.use(mw)
- 全局中间件简化形式
//全局中间件的简化形式
app.use((req, res, next) => {
console.log('这是最简单的中间件函数');
//把流转关系,转交给下一个中间件或者路由
next()
})
- 多个中间件之间,共享同一份req和res。基于这样的特征,我们可以在上游的中间件中,统一为req或res对象添加自定义的属性或者方法,供下游的中间件或者路由进行使用
- 可以使用app.use()选择定义多个全局中间件,客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用
app.use((req, res, next) => {
console.log('第一个全局中间件');
next()
})
app.use((req, res, next) => {
console.log('第二个全局中间件');
next()
})
- 不使用app.use()定义的中间件,叫做局部生效的中间件
//局部中间件一
const mw1 = (req, res, next) => {
console.log('调用了局部生效的中间件');
next()
}
//定义路由
app.get('/', mw1, (req, res) => {
res.send('Home page.')
})
app.get('/user', (req, res) => {
res.send('User page.')
})
- 可以在路由中,通过两种方式使用多个局部中间件
app.get('/', mw1, mw2, (req, res) => {
res.send('Home page.')
})
app.get('/user', [mw2, mw1], (req, res) => {
res.send('User page.')
})
- 一定要在路由之前注册中间件
- 客户端发送过来的请求,可以连续调用多个中间件进行处理
- 执行完中间件的业务代码后,不要忘记调用next()函数
- 为了防止代码逻辑混乱,不要在next()函数不要再写额外代码
- 连续调用多个中间件时,多个中间件之间,共享req和res对象的
为了方便大家理解和记忆中间件的使用,Express官方把常见的中间件用法,分成了5大类
- 应用级别的中间件
- 路由级别的中间件
- 错误级别的中间件
- Express内置的中间件
- 第三方的中间件
- 通过app.use()或app.get() 或app.post(),绑定到app实例上的中间件,叫做应用级别额中间件
app.use((req, res, next) => { next() })
app.get('/', mw1, (req, res) => { res.send('Home page.') })
- 通过绑定到express.Router()实例上的中间件,叫做路由级别的中间件,他的用法和应用级别的中间件没有任何区别,只不过,应用界别的中间件是绑定到app实例上,路由级别的中间件绑定到router实例上
router.use((req, res, next) => { next() })
- 错误级别的中间件的作用:专门来捕获异常错误,从而防止项目一场崩溃的问题
- 必须包含四个形参(err,req,res,next)
(err, req, res, next) => { next() }
- 错误级别中间件注册在所有路由之后
1. express.static 托管静态资源的内置中间件(无兼容性)
2. express.json 解析JSON格式的请求体数据(有兼容性)
`app.use(express.json())`
3. express.urlencoded 解析URL-eneoded格式的请求体数据(有兼容性)
`app.use(express.urlencoded({ extended: false }))`
- 非Express官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件,在项目中,大家可以按需下载并配置第三方中间件,从而提高项目的开发效率。
- body-parser中间件
- 运行
npm i body-parser
中间件 - 使用require导入中间件
- 调用app.use()注册并使用中间件
- 定义中间件
- 监听 req 的 data 事件
- 监听 req 的 end 事件
- 使用 querystring 模块解析请求体数据
- 将解析出来的数据对象挂载为req.body
- 将自定义中间件封装为模块
- 在中间件中,需要监听req对象的data事件,来获取客户端发送到服务器的数据,客户端会把数据切割后,分批发送到服务器,因此每次一次触发data事件时,接受到的可能是数据的一部分
- 当请求体数据接受完毕后,会自动触发req的end事件
- Node.js 内置了一个querystring 模块,专门用来处理查询的字符串,通过这个模块提供的parse()函数,就可以轻松把查询字符串,解析成对象的格式
- 上游的中间件和下游的中间件之间共享一份req和res,因此我们可以将解析出来的数据挂载为req的自定义属性,命名为req.body,供下游使用
- 刚才编写的GET和POST接口,存在一个很严重的问题,不支持跨域
- 解决方法
- cors 主流方法
- JSONP 只支持GFT请求
- 步骤
- 运行
npm i cors
安装中间件 - 使用
const cors = require('cors')
导入中间件 - 在路由之前调用
app.use(cors())
配置中间件
- 运行
- cors是由一系列的http响应头组成的,这些HTTP响应头决定浏览器是否阻止前端js代码跨域获取资源
- cors主要在服务端进行配置,客户端浏览器无需做任何额外的配置
- 有兼容性,只有支持XMLHttpRequest level2的浏览器才能使用
res.setHeader('Access-Control-Allow-Origin', 'http://bruceblog.io')
res.setHeader('Access-Control-Allow-Origin', '*')
- 默认情况下,CORS 仅支持客户端向服务器发送如下的 9 个请求头:Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type (值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)
- 如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败!
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')
- 默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods 来指明实际请求所允许使用的 HTTP 方法
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD')
res.setHEader('Access-Control-Allow-Methods', '*')
- 请求方式:GET、POST、HEAD 三者之一
- HTTP 头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type(只有三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain)
- 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
- 请求头中包含自定义头部字段
- 向服务器发送了 application/json 格式的数据 在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据
- 简单请求;服务器和客户端只会发生一次请求
- 预检请求;服务器和客户端只会发生两次请求,预检请求成功后,才会发生真正的请求
- 浏览器通过<script>标签的src属性,请求服务器上的数据,同时,服务器返回一个函数的调用
- 特点
- jsonp 不属于真正的ajax请求,因为它没有使用XMLHttpRequest这个功能
- jsonp仅支持POST、PUT、DELETE等请求
- 为了防止冲突必须在cors之前配置jsonp接口
app.get('/api/jsonp', (req, res) => {
//得到函数的名称
const funcName = req.query.callback
//定义要发送到客户端的数据对象
const data = { name: 'zs', age: 22,methos:'jsonp' }
//拼接出一个函数的调用
const scriptStr = `${funcName}(${JSON.stringify(data)})`
//把拼接好的字符串,响应给客户端
res.send(scriptStr)
})
- 安装操作数据库的第三方模块mysql
- 通过mysql模块连接到MySQL数据库
- 通过mysql模块执行SQL语句
npm i mysql
const mysql = require('mysql')
const db = mysql.createPool({
host: '127.0.0.1', //本机数据库
user: 'root', // 用户名
password: '123', // 密码
database: 'spring' // 数据库名
})
//测试mysql能否正常工作
db.query('select * from account', (err, res) => {
if (err) return console.log(err.message);
account = res
})
- 文件地址('./09.msql')
- 服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接动态生成的。因此客户端不需要使用 Ajax 额外请求页面的数据。
app.get('/index.html', (req, res) => {
const user = { name: 'Bruce', age: 29 }
const html = `<h1>username:${user.name}, age:${user.age}</h1>`
res.send(html)
})
- 优点:
- 前端耗时短。浏览器只需直接渲染页面,无需额外请求数据。
- 有利于 SEO。服务器响应的是完整的 HTML 页面内容,有利于爬虫爬取信息。
- 缺点:
- 占用服务器资源。服务器需要完成页面内容的拼接,若请求比较多,会对服务器造成一定访问压力。
- 不利于前后端分离,开发效率低。
- 前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。后端只负责提供 API 接口,前端使用 Ajax 调用接口。
- 优点:
- 开发体验好。前端专业页面开发,后端专注接口开发。
- 用户体验好。页面局部刷新,无需重新请求页面。
- 减轻服务器的渲染压力。页面最终在浏览器里生成。
- 缺点:
- 不利于 SEO。完整的 HTML 页面在浏览器拼接完成,因此爬虫无法爬取页面的有效信息。Vue、React 等框架的 SSR(server side render)技术能解决 SEO 问题。
- 企业级网站,主要功能是展示,没有复杂交互,且需要良好的 SEO,可考虑服务端渲染
- 后台管理项目,交互性强,无需考虑 SEO,可使用前后端分离
- 为同时兼顾首页渲染速度和前后端分离开发效率,可采用首屏服务器端渲染+其他页面前后端分离的开发模式
- 身份认证,又称;身份验证,鉴权,是指通过一种手段,完成对用户的确认。
- 确认当前所声称为某种身份的用户,确实是所声称的用户
- 服务器端渲染:推荐使用session认证机制
- 前后端分离:推荐使用jwt认证机制
- HTTP协议的无状态性指的是客户端的每次请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留HTTP请求的状态
- 通过cookie
npm install express-session
const session = require('express-session')
app.use(session({
secret: 'Bruce', // secret 的值为任意字符串
resave: false,
saveUninitalized: true,
}))
- 中间件配置成功后,可通过 req.session 访问 session 对象,存储用户信息
app.post('/api/login', (req, res) => {
req.session.user = req.body
req.session.isLogin = true
res.send({ status: 0, msg: 'login done' })
})
app.get('/api/username', (req, res) => {
if (!req.session.isLogin) {
return res.send({ status: 1, msg: 'fail' })
}
res.send({ status: 0, msg: 'success', username: req.session.user.username })
})
app.post('/api/logout', (req, res) => {
// 清空当前客户端的session信息
req.session.destroy()
res.send({ status: 0, msg: 'logout done' })
})
- Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
- 当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
- 当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制
- 前后端分离推荐使用 JWT(JSON Web Token)认证机制,是目前最流行的跨域认证解决方案
- 用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。
- Header、Payload、Signature
- Payload 是真正的用户信息,加密后的字符串
- Header 和 Signature 是安全性相关部分,保证 Token 安全性
- 三者使用 . 分隔
Header.Payload.Signature
- jwt字符串实例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTcsInVzZXJuYW1lIjoiQnJ1Y2UiLCJwYXNzd29yZCI6IiIsIm5pY2tuYW1lIjoiaGVsbG8iLCJlbWFpbCI6InNjdXRAcXEuY29tIiwidXNlcl9waWMiOiIiLCJpYXQiOjE2NDE4NjU3MzEsImV4cCI6MTY0MTkwMTczMX0.bmqzAkNSZgD8IZxRGGyVlVwGl7EGMtWitvjGD-a5U5c
- 客户端会把 JWT 存储在 localStorage 或 sessionStorage 中
- 此后客户端与服务端通信需要携带 JWT 进行身份认证,将 JWT 存在 HTTP 请求头 Authorization 字段中
- 加上 Bearer 前缀
格式:
Authorization: Bearer <token>
npm install jsonwebtoken express-jwt
- jsonwebtoken 用于生成 JWT 字符串
- express-jwt 用于将 JWT 字符串解析还原成JSON 对象
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
- 为保证 JWT 字符串的安全性,防止其在网络传输过程中被破解,需定义用于加密和解密的 secret 密钥
- 生成 JWT 字符串时,使用密钥加密信息,得到加密好的 JWT 字符串
- 把 JWT 字符串解析还原成 JSON 对象时,使用密钥解密
const secretKey = 'jwt No1 ^_^'
- 调用jsonwebtoken包提供的sign(),将用户的信息加密成jwt字符串,响应给客户端
app.post('/api/jwtlogin', (req, res) => {
const userinfo = req.body
if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
return res.send({ status: 400, msg: '登录失败' })
}
const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '120s' })
res.send({
status: 200,
msg: '登录成功',
token: tokenStr,
})
})
- 客户端访问有权限的接口时,需通过请求头的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证
- 服务器可以通过 express-jwt 中间件将客户端发送过来的 Token 解析还原成 JSON 对象
// unless({ path: [/^\/api\//] }) 指定哪些接口无需访问权限
app.use(
expressJWT({
secret: secretKey,
algorithms: ["HS256"],
}).unless({ path: [/^\/api\//] })
);
- 当 express-jwt 中间件配置成功后,即可在那些有权限的接口中,使用 req.auth 对象,来访问从 JWT 字符串中解析出来的用户信息
app.get('/admin/getinfo', (req, res) => {
console.log(req.auth)
res.send({
status: 200,
message: '获取信息成功',
data: req.auth,
})
})
- 当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行
- 通过 Express 的错误中间件,捕获这个错误并进行相关的处理
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
return res.send({ status: 401, message: '无效token' })
}
res.send({ status: 500, message: '未知错误' })
})