Node.js
模块化
什么是模块化?
模块化是指解决一个复杂问题时,自顶向下把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元。
模块化的好处
- 提高代码的复用性
- 提高代码的可维护性
- 可以实现按需加载
node.js中模块的分类
- 内置模块(由node.js官方提供的模块,如fs、path、http等)
- 自定义模块(用户创建的每个.js文件)
- 第三方模块(由第三方开发出来的模块,使用需要下载)
加载模块
使用require()方法来加载所需模块
注意:
- 使用require()方法加载其他模块时,会执行被加载模块中的代码
- 可以省略.js后缀名
模块作用域
- 和函数作用域相似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问
- 好处:
- 防止全局变量污染问题
向外共享模块作用域中的成员
- module 对象
- module.exports 对象
- 在自定义模块中,可以使用module.exports对象将模块内的成员共享出去
- 外界用require方法导入自定义模块时,得到的就是module.exports所指向的对象
- 共享成员时的注意点:使用require()导入模块时,得到的结果永远以module.exports指向的对象为准
- exports和module.exports指向同一个对象
模块的加载机制
优先从缓存中加载
模块第一次加载后会被缓存,多次调用require()不会导致模块的代码被执行多次,从而提高模块的加载效率
内置模块的加载优先级最高
若遇到同名的模块,内置模块优先级较高
自定义模块加载机制(省略扩展名情况)
- 按照确切文件名进行加载
- 补全.js扩展名进行加载
- 补全.json扩展名进行加载
- 补全.node扩展名进行加载
- 加载失败,终端报错
第三方模块加载机制
从当前模块的父目录开始,若找不到,则移动到上一层父目录,进行加载,直到文件系统的根目录
以目录为模块传递给require
- 先寻找目录下package.json文件,查找main属性,作为require()加载的入口
- 若没有package.json文件或main属性入口不存在或无法解析,则node.js会加载目录下的index.js
- 若都失败了,则在终端打印错误信息,报告模块的确实:Error:Con not find module ‘xxx’
fs文件系统模块
- 利用require导入fs模块
const fs = require('fs');
fs.readFile()
读取文件- 参数1:读取文件的存放路径(必选)
- 参数2:读取文件的编码格式,一般默认为utf8(可选)
- 参数3:回调函数,失败和成功的结果(
function(){失败,成功}
)(必选)
fs.writeFile()
写入内容- 参数1:读取文件的存放路径(必选)
- 参数2:要写入的内容
- 参数3:读取文件的编码格式,一般默认为utf8(可选)
- 参数4:回调函数,失败和成功的结果(
function(){失败,成功}
)(必选)
- 使用terminal出现路径拼接错误的问题,是因为使用了./ 或 …/开头的相对路径,使用绝对路径可以解决此问题
__dirname
表示当前文件所处的目录
- 注:
fs.writeFile()
写入同一文件时会覆盖
path路径模块
path.join()
将多个路径片段拼接- 各参数用’,'隔开
path.basename()
从路径字符串解析出文件名- 参数1:路径(必须)
- 参数2:后缀名(可选),选择后会移除
path.extname()
获取扩展名
http模块
- IP地址:互联网上每台计算机的一个唯一标识
- IP地址格式通常为“点分十进制”表示成(a,b,c,d)的形式,都是为0-255之间的十进制整数
- 互联网中每台web服务器都有属于自己的IP地址
- 域名和域名服务器
- IP地址和域名是一一对应的关系
- 端口号:
- 每个端口号不能同时被多个web服务占用
- 在实际应用中,url的80端口可以省略
- 创建基本的web服务器步骤:
- 导入http模块
const http = require('http');
- 创建web服务器实例
const server = http.createServer();
- 为server绑定request事件,监听服务器请求
server.on('request', function(req, res){})
- 启动服务器
server.listen(端口号, function(){})
- 解决中文乱码的问题:
- 设置内容的编码格式
res.setHeader('Content-Type', 'text/html; charset=utf-8');
- 向服务端发送内容
res.end('发送的内容')
npm与包
创建自己的npm包
- 新建文件夹
- 设置package.json文件,index.js文件和README.md文档
- index.js放主要信息
- package.json放配置信息
- 例子:
{ "name": "", // 所起的名字 "version": "1.0.1", // 版本号 "main": "index.js", // 主要文件 即前面设置的index.js "description": "", // npm包的描述 "keywords": [], // 关键词 "license": "ISC" // 开源协议 }
- 可以将所创建的js文件放在新建的src文件夹下,每个js文件分别要
module.exports
- 在index.js中
module.exports={}
所有src里的js文件 - 使用时通过require根文件夹引入
发布包
- 在cmd中cd至发布文件的根文件夹
- 运行命令:
npm publish
删除已发布的包
- cmd中运行:
npm unpublish 包名 --force
- 注意:
- npm unpublish 命令只能删除72小时内发布的包
- 删除的包在24小时内不允许重复发布
Express
了解Express
- 本质为npm上的第三方包,提供了快速创建Web服务器的便捷方法
- 可以方便快速创建Web网站服务器或API接口的服务器
使用Express创建服务器
基本使用
const express = require('express');
const app = express();
app.listen(80, () => {
console.log('server running at http://127.0.0.1');
})
监听get请求
app.get('请求url',function(req,res){
res.send(); //向客户端响应的内容
})
req.query
req.query默认是一个空对象,客户端使用?name=a&age=30这种查询字符串的形式发送到服务器的参数,可以用req.query获取
例:在地址栏输入127.0.0.1/?username=test&age=20
app.get('/',function(req,res)=>{
res.send(req.query); //{"username":"test","age":"20"}
})
req.params
URL地址中,可以通过:参数名的形式匹配动态参数值,req.params默认是一个空对象,里面存放着通过:动态匹配到的参数值(参数名不固定)
例:在地址栏输入127.0.0.1/user/1
app.get('/user/:id', (req, res) => {
res.send(req.params); //{"id":"1"}
})
监听post请求
app.post('请求url',function(req,res){
res.send();//向客户端响应的内容
})
托管静态资源
-
app.use(express.static())
注意:Express在指定静态目录中查找文件,并对外提供资源的访问路径,因此,存放静态文件的目录名不会在url中
若要托管多个目录,则多次调用express.static()函数
app.use('/public', express.static('./public'))
挂载路径前缀
Express路由
- 路由指的是客户端的请求与服务器处理函数之间的映射关系
- Express中的路由分3部分,分别是请求的类型、请求的url地址、处理函数
app.METHOD(PATH, HANDLER)
最简单的挂载方法
app.get('url', function(req, res)=>{})
app.post('url', function(req, res)=>{})
模块化路由
- 创建路由模块对应的js文件
- 调用
express.Router()
创建路由对象 - 向路由对象上挂载具体的路由
- 使用
module.exports
向外共享路由对象 - 在路由文件下使用
require()
引入路由模块 - 使用
app.use()
函数注册路由模块- 在use中可以给路由模块添加统一的访问前缀
Express中间件
什么是中间件
中间件特指业务流程中的中间处理环节
中间件的格式
本质上是一个function处理函数格式如下:(有包含next则为中间件处理函数,没有则为路由处理函数)
const express = require('express');
const app = express();
app.get('/', function(req, res, next){
next();
});
app.listen(80);
全局中间件
- 使用
app.use()
定义的中间件函数,为全局中间件,next()函数可以共享给后面的路由 - 若定义了多个全局中间件就依次写下去
局部生效中间件
- 使用
app.use()
定义的中间件,为局部生效中间件 - 使用方式:
const mw = (req, res, next)=>{
next();
}
app.get('/', mw, (req, res)=>{
res.send()
})
- 定义多个局部中间件
//以下两种写法完全等价
app.get('/', mw1, mw2, (req, res)=>{res.send()});
app.get('/', [mw1, mw2], (req, res)=>{res.send()});
- 监听req的data事件
//监听req的data事件,获取客户端发送到服务器的数据,若数据量比较大,客户端会把数据切割后分批发送到服务器,所以data事件有可能会触发多次,需要对接收到的数据进行拼接
let str = '';
req.on('data', (data)=>{
str += data;
});
- 监听req的end事件
//监听req的end事件(请求体发送完毕后自动触发),可用于获取完整请求体数据
req.on('end',()=>{
console.log(str);
});
中间件的分类
- 应用级别的中间件
- 路由级别的中间件
- 错误级别的中间件
- Express内置的中间件
- 第三方的中间件
应用级别的中间件
通过app.use()或app.get()或app.post绑定到app实例上的中间件
路由级别的中间件
绑定到express.Router()实例上的中间件,用法和应用级别的没有区别
错误级别的中间件
- 作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
- 格式:错误级别的中间件有4个形参,(err, req, res, next)
- 注意:错误级别的中间件必须注册在所有路由之后
- 例:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
throw new Error('服务器内部发生了错误!');
res.send('Home page');
})
app.use((err, req, res, next) => {
console.log('wrong!');
res.send('Error' + err.message);
})
app.listen(80, () => {
console.log('http://127.0.0.1');
})
Express内置的中间件
- express.static 快速托管静态资源的内置中间件,如:html文件、图片、css样式等((无兼容性)
- express.json解析JSON格式的请求体数据(有兼容性,在4.16.0+版本可用)
app.use(express.json())
//在服务器可以使用req.body来接收客户端发送的请求体数据,若没有配置解析的中间件,req.body默认为{}
app.post('/user', (req, res) => {
console.log(req.body);
res.send('ok');
})
- express.urlencoded解析URL-encoded格式的请求体数据(有兼容性,在4.16.0+版本可用)
app.use(express.urlencoded({
extended: false
}))
//在服务器可以使用req.body来接收客户端发送的请求体数据,若没有配置解析的中间件,req.body默认为{}
app.post('/book', (req, res) => {
console.log(req.body);
res.send('ok');
})
第三方中间件
- 运行npm install xxx
- 使用require导入
- 调用app.use()注册并使用
- 例:(express.urlencoded和express.json基于body-parser封装而来)
const express = require('express');
const app = express();
const parser = require('body-parser');
app.use(parser.urlencoded({
extended: false
}))
app.post('/user', (req, res) => {
console.log(req.body); //req.body是请求体,包含了发送请求的内容
res.send('ok');
})
app.listen(80, () => {
console.log('http://127.0.0.1');
})
使用Express写接口
什么是CORS
CORS(Cross-Origin Resource Sharing,跨域资源共享)由一系列HTTP响应头组成,这些http响应头觉得浏览器是否阻止前端js代码跨域获取资源
浏览器的同源安全策略默认会阻止网页跨域获取资源,如果有接口服务器配置了CORS相关的HTTP响应头,就可以解除浏览器端的跨域访问限制
CORS的注意事项
- CORS主要在服务器端进行配置,客户端无须做额外配置
- CORS在浏览器中有兼容性,只有支持XMLHttpRequest Level2的浏览器,才能正常访问(如:IE10+、Chrome4+、FireFox3.5+)
CORS跨域资源共享
npm install cors
安装中间件require('cors')
导入中间件app.use(cors())
配置中间件
CORS响应头部
- 默认情况下,CORS仅支持客户端向服务器发送以下9个请求头:
Accept、Accept-Language、Content-Language、DPR、DownLink、Sava-Data、ViewPort-Width、Width、Content-Type(值仅限于text/plain、multipart/form-data、application/x-www-form-urlencoded三者之一) - 默认情况下,CORS仅支持客户端发起GET、POST、HEAD请求
Access-Control_Allow-Origin
origin参数的值指定了允许访问该资源的外域URL
res.setHeader('Access-Control_Allow-Origin','<origin>')
*表示允许来自任何域的请求
res.setHeader('Access-Control_Allow-Origin','*')
Access-Control_Allow-Headers
若客户端需要向服务器发送额外的请求头信息,需要在服务器端通过 Access-Control_Allow-Headers对额外的请求进行声明,否则请求会失败
res.setHeader('Access-Control_Allow-Header','Content-type, X-Custom-Header')
Access-Control_Allow-Methods
如果客户端希望通过PUT、DELETE等其他方式请求服务器资源,需要通过Access-Control_Allow-Methods指明所允许使用的HTTP方法
res.setHeader('Access-Control_Allow-Methods','POST, GET, PUT, DELETE')
//允许所有的HTTP方法
res.setHeader('Access-Control_Allow-Methods','*')
JSONP
- 若要配置jsonp接口,必须在配置cors前配置jsonp,否则jsonp接口会被处理成开启了cors的接口
- 例:(服务器端)
app.get('/api/jsonp', (req, res) => {
//获取客户端发送的回调函数的名字
const funcName = req.query.callback;
//得到要通过JSONP发送给客户端的数据
const data = {
name: 'a',
age: 20
};
//根据前面的数据拼接函数调用的字符串
const str = `${funcName}(${JSON.stringify(data)})`;
//将字符串响应给客户端的<script>标签进行解析执行
res.send(str);
})
数据库使用
SQL基本使用
select语句
select关键字对大小写不敏感
select * from 表名称
select 列名称 from 表名称
insert into语句
insert into 语句用于向数据表中插入新的数据行
insert into table_name(列1,列2...) values(值1,值2...)
update语句
update用于修改表中的数据
update 表名称 set 列名称=新值 where 列名称=值
delete语句
delete用于删除表中的数据
delete from 表名称 where 列名称=值
count(*)函数
用于查询结果的总数据条数
select count(*) as total from 表名称
在项目中操作MySQL
- 安装第三方模块mysql
- 通过mysql模块连接到数据集
- 通过mysql模块执行sql语句
- 基本使用:
建立连接
const mysql = require('mysql');
//建立连接关系
const db = mysql.createPool({
host: '127.0.0.1', //数据库的IP地址
user: 'root', //登录数据库的账号
password: '', //密码
database: '' //要操作的数据库
})
//测试mysql模块能否正常工作
db.query('select 1', (err, results) => {
//查询失败
if (err) {
return console.log(err.message);
}
//查询成功
//如果执行的是select查询语句,执行的结果是数组
console.log(results);
})
插入数据
//向users表中插入数据,可以通过affectedRows属性来判断是否插入成功
const user = {
username: 'newUser',
password: 'newPsd',
status: '0'
}
//插入数据的简易写法
//const sqlStr = 'insert into users set ?';
const sqlStr = 'insert into users (username, password, status) values (?,?,?)';
//db.query(sqlStr, user, (err, results)=>{})
db.query(sqlStr, [user.username, user.password, user.status], (err, results) => {
if (err) {
return console.log(err.message);
}
//执行后结果是一个对象,可以通过affectedRows判断是否成功
if (results.affectedRows === 1) {
console.log('插入成功!');
}
})
更新数据
//更新用户信息
const user = {
id: 3,
username: 'sb',
password: '54sb',
}
//更新数据简便写法
//const sqlStr = 'update users set ? where id=?';
const sqlStr = 'update users set username=?, password=? where id=?';
//db.query(sqlStr, [user, user.id], (err, results) => {})
db.query(sqlStr, [user.username, user.password, user.id], (err, results) => {
if (err) {
return console.log(err.message);
}
if (results.affectedRows === 1) {
console.log('更新成功!');
}
})
标记删除
先将欲删除的行进行标记,避免因误删而导致数据丢失的悲惨后果
const sqlStr = 'update users set status=1 where id=?';
db.query(sqlStr, 4, (err, results) => {
if (err) {
return console.log(err.message);
}
if (results.affectedRows === 1) {
console.log('标记删除成功!');
}
})
Web开发模式
服务器渲染的Web开发模式
- 概念:服务器发送给客户端的html页面,是在服务器通过字符串的拼接动态生成的,客户端不需要使用ajax这样的技术额外请求页面的数据
- 优点:
- 前端耗时少
- 有利于SEO。服务器的响应是完整的html页面内容,爬虫更容易爬取获得信息,更有利于SEO
- 缺点:
- 占用服务器端资源
- 不利于前后端分离,开发效率低
前后端分离的Web开发模式
- 概念:依赖ajax技术,后端只提供api接口,前端使用ajax调用接口的开发模式
- 优点:
- 开发体验好
- 用户体验好
- 减轻服务器端的渲染压力
- 缺点:
- 不利于SEO(可以利用Vue、React等前端框架的SSR(Server side render)技术解决)
如何选择Web开发模式
- 如果网站的主要功能是展示而没有复杂的交互,并且需要良好的SEO,可以使用服务器渲染的开发模式
- 如果网站的交互性比较强,不需要考虑SEO,可以使用前后端分离
- 若为了兼顾首页的渲染速度和前后端分离的开发效率,也可以采用首屏服务器渲染和其他页面前后端分离的开发模式
身份认证
不同开发模式的身份认证
- 服务器端渲染推荐使用Session认证机制
- 前后端分离推荐使用JWT认证机制
Session认证机制
Http协议的无状态性
客户端的每次http请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次http请求的状态
Cookie
- Cookie是存储在用户浏览器的一段不超过4kb的字符串,有一个名称(Name)、一个值(Value)和其他几个用于控制Cookie有效期、安全性、使用范围的可选属性组成
- Cookie的几大特性:
- 自动发送
- 域名独立
- 过期时限
- 4kb限制
- 过程:
- 客户端第一次请求服务器时,服务器通过响应头的形式,向客户端发送一个身份认证的Cookie,客户端将Cookie保存在浏览器中,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的Cookie通过请求头的形式发送给服务器,服务器即可验证客户端的身份
- Cookie不具备安全性,不要使用Cookie存储重要且隐私的数据
Express中使用Session认证
配置session中间件
const session = require('express-session');
app.use(session({
secret: 'xxx',
resave: false,
saveUninitialized: true
}))
向session中存放数据
express-session中间件配置后,可以通过req.session来访问和使用session对象,从而存储用户的相关信息
req.session.user = req.body; //存储用户信息
req.session.isLogin = true; //存储登录状态
清除session中的数据
req.session.destroy();
session认证的局限性
Session认证需要配合Cookie才能实现。Cookie默认不支持跨域访问,当涉及前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域Session认证
JWT认证机制
认识JWT
- JWT(JSON Web Token)是目前最流行的跨域认证解决方案
- 工作原理:用户的信息通过Token字符串的形式,保存在客户端浏览器中,服务器通过还原Token字符串的形式来认证用户身份
- JWT组成部分:Header(头部)、Payload(有效荷载)、Signature(签名),三者之间用英文的“.”分割,格式:
Header.Payload.Signature
- Payload部分存放真正的用户信息,是用户信息经过加密之后生成的字符串
- Header和Signature是安全性相关的部分,为了保护Token的安全
Signature
客户端收到服务器返回的JWT后,通常会存储在localStorage或sessionStorage中,客户端之后与服务器的通信中,可以将JWT放在HTTP请求头的Authorization字段中,来进行身份验证
Authorization:Bearer <token>
Express中使用JWT认证
安装jsonwebtoken和express-jwt包
- jsonwebtoken用于生成JWT字符串
- express-jwt用于将JWT字符串解析还原成JSON对象
定义secret密钥
- 保证JWT字符串的安全性,防止JWT字符串在网络传输过程中被别人破解
- 当生成JWT字符串时,需要使用secret密钥对用户信息进行加密,最终得到加密好的字符串
- 把JWT字符串解析还原成JSON对象时,需要使用secret密钥进行解密
生成JWT字符串
- 调用jsonwebtoken包提供的sign()方法,将用户信息加密成JWT字符串,响应给客户端
- jwt.sign()方法包含3个参数:
- 参数1:用户的信息对象
- 参数2:加密的密钥
- 参数3:配置对象,配置当前token的有效期(expiresIn:‘3s’ && expiresIn:‘3h’)
const tokenStr = jwt.sign({
username: userInfo.username
}, secretKey, {
expiresIn: '30s'
});
res.send({
//为了方便客户端使用token,在服务器端直接拼上前缀
token: 'Bearer ' + tokenStr
})
将JWT字符串解析为JSON对象
注意:
- 只要配置成功了express-jwt这个中间件,就可以把解析出来的用户信息挂载到
req.user
属性上 - 不要把密码加密到token中
// expressJWT({secret: secretKey}) 用来解析token的中间件
// .unless用来指定哪些接口不需要访问权限
app.use(expressJWT({
secret: secretKey,
algorithms: ['HS256']
}).unless({
path: [/^\/api\//]
}))
捕获解析JWT失败后产生的错误
app.use((err, req, res, next)=>{
// token解析失败导致的错误
if(err.name === 'UnauthorizedError'){
return res.send({
status: 401,
message: '无效token'
})
}
//其他原因导致的错误
res.send({
status: 500,
message: '未知错误'
})
})
对密码进行加密处理
-
可以使用bcryptjs对要存放进数据库的密码进行加密
-
优点:
- 加密之后的密码无法被逆向破解
- 同一密码多次加密,得到的加密结果不相同
-
步骤:
- 安装bcryptjs包
- 导入bcryptjs
- 在处理函数中调用bcrypt.hashSync(明文密码,随机的长度)方法进行加密处理,示例:
const bcrypt = require('bcryptjs'); password = bcrypt.hashSync(password, 8);
- 调用bcrypt.compareSync(用户提交的密码,数据库中的密码)方法比较密码是否一致,返回值是布尔值,示例:
const compareResult = bcrypt.compareSync(xx,xx); if(!compareResult){ return res.send({ status: 1, message: '密码错误!' }) }
表单验证
- 前端验证为辅,后端验证为主
- 适当使用第三方的包来辅助验证(如:joi包、@escook/express-joi包)
- 例:
const joi = require('joi')
/**
* string() 值必须是字符串
* alphanum() 值只能是包含 a-zA-Z0-9 的字符串
* min(length) 最小长度
* max(length) 最大长度
* required() 值是必填项,不能为 undefined
* pattern(正则表达式) 值必须符合正则表达式的规则
*/
// 用户名的验证规则
const username = joi.string().alphanum().min(1).max(10).required()
// 密码的验证规则
const password = joi
.string()
.pattern(/^[\S]{6,12}$/)
.required()
解析表单数据
-
注:使用
express.urlencoded()
无法解析multipart/form-data
格式的请求体数据 -
可以使用multer来解析
multipart/form-data
格式的表单数据 -
步骤:
- 安装multer包
- 导入并配置multer,创建multer的实例对象,通过dest属性指定文件的存放路径
const multer = require('multer'); const path = require('path'); const upload = multer({ dest: path.join(__dirname, 'xxx') }) // upload.single() 是一个局部生效的中间件,用来解析 FormData 格式的表单数据 // 将文件类型的数据,解析并挂载到 req.file 属性中 // 将文本类型的数据,解析并挂载到 req.body 属性中 router.post('/add', upload.single('cover_img'), article_handler.addArticle)