Express
express中文网连接: https://www.expressjs.com.cn/
1.基于node平台的web开发框架
- 特性
- 提供了简便的路由定义方式
- 对获取的http请求参数进行了简化处理,将参数做成了请求对象的属性
- 对模板引擎支持程度高
- 提供了中间件机制控制http请求
- 有大量第三方中间件对功能进行扩展
2.使用步驟
// 引入express框架
const express = require('express')
// 创建网站服务器
const app = express()
app.get('/',(req,res)=>{
// send方法内部检测响应内容的类型 自动设置http状态码 自动设置响应的内容类型及编码
res.send() // 可以传递json对象
})
app.listen(3000)
3.核心概念:中间件
-
中间件就是一堆方法,接收请求,可以对请求作出响应,也可以将请求继续交给下一个中间件处理
-
中间件由express提供,负责拦截请求,请求处理函数由开发人员自己提供
-
默认情况下,从上到下匹配中间件,一旦匹配成功,终止匹配,next方法决定是否允许请求向下走
-
一般分为应用层中间件、路由中间件、内置中间件、错误处理中间件、第三方中间件
-------------------------应用层中间件-------------------------
app.use((req,res,next)=>{
console.log(‘所有请求先过我这里’);
next(); // 必须要向后放行,否则后面的代码不会执行
})
-------------------------路由中间件-------------------------
// 创建路由对象
var router = express.router()
router.use(’/user’,(req,res,next)) => {
console.log(‘匹配的地址’:‘req.originalurl’)
},(req,res) => {
res.send(‘请登录’)
})
app.use(’/’,router)
---------------引申,利用路由中间件创建二级路由----------------
home.js
const express = require(‘express’)
const home = express.Router() // 创建路由对象
home.get(’/index’,()=>{ // home路由下创建二级路由
// 路由为/home/index
res.send(‘home页面’)
})
module.exports = home;app.js
const home = require(’./home’)
// 路由和请求路径匹配
app.use(’/home’,home)
----------------错误处理中间件(有四个参数)----------------------
// 集中处理错误的地方
app.use((err,req,res,next)=>{
res.sendStatus(err.httpStatusCode).json(err);
})
// 一般错误处理放到最后,匹配不到路径就返回404,注意下Koa是放到最前
app.use(function(req,res){
res.status(404).send(“您查找的页面不存在”);
});
-----------------引申,抛错误方式-------------------------------
throw new Error(‘程序发生未知错误’) //同步方式,throw出node的内置错误对象
fs.readFile(’./…’,(err,data)=>{
if(err!=null) next(err) //异步方式,把错误对象传到next()方法中
})
try{ //异步方式同步写法,使用async await语法糖
await User.find()
}catch(err){
next(err);
}…
-------------------内置中间件---------------------------------
// 4.x版本开始,express.static()成了唯一内置中间件
app.use(express.static(path.join(__dirname,‘public’)[,options])
app.use(’/static’,express.static(path.join(__dirname,‘public’)) // 写第一个参数,还可以增加虚拟路径
options配置项:
var options = {
dotfiles: ‘ignore’, //是否对外输出文件名以点(.)开头的文件
etag: false, //启用或禁用 etag 生成
extensions: [‘htm’, ‘html’], //用于设置后备文件扩展名
index: false, //发送目录索引文件
maxAge: ‘1d’, //设置 Cache-Control 头的 max-age 属性
redirect: false,//当路径名是目录时重定向到结尾的’/’
setHeaders: function (res, path, stat) { //设置随文件一起提供的HTTP头的函数
res.set(‘x-timestamp’, Date.now());
}
}
--------------------第三方中间件------------------------------
例如下文中的body-parser中间件
4.express路由
app.methods(path,callback) // express路由方法
// methods方法有get、post、put、head、delete、options、trace、copy、lock、mkcol、move、purge、propfind、proppatch、unlock、report、mkactivity、checkout、merge、m-search、notify、subscribe、unsubscribe、patch、search、connect
// path包含三种 1.完整字符串路径 2.字符串模式路径 3.正则表达式路径 /^home/
-----------------------------------------------------
app.get('/user/:id',callback) //动态路由
路由请求参数获取
a.获取get请求参数
app.get(...,(req,res)=>{log(req.query)}) //输出一个对象 {name:'张三',age:18}
req.query直接把url地址中query的部分转换为对象格式输出
b.获取post请求参数
-----------1.第三方模块 body-parser---------------------
// 下载安装 引入模块
const bodyParser = require('body-parser');
// 配置body-parser模块,解析json格式数据
const jsonparser = bodyParser.json();
app.use(jsonparser);
// 或者 解析x-www-form-urlencoded格式数据,false表示使用express内置的模块解析
urlendcodedparser = bodyParser.urlencoded({ extended:false });
// 拿到参数,如果没有使用app.use(..),那么传入请求中使用
app.post('/user',urlendcodedparser,(req,res) => {
console.log(req.body) //输出一个对象 {name:'张三',age:18}
})
第三方模块的本质其实是内部返回了一个类似function(req,res,next){....;next();}的函数
------------2. 第三方模块 connect-multiparty------------------
// 多用于文件上传,但也可以访问到post请求的数据,尽量不使用这个
npm i connect-multiparty
cosnt multipaty = require('connect-multiparty');
const multipartyMiddleware = multiparty();
app.post('/',multipartMiddleware,function(req,res){
console.log(req.body);
});
------------3. 第三方模块 formidable--------------------------
// 用于接收post传来的formdata
(前端要么new Formdata()然后append('file',file.files[0]),要么设置enctype='multipart/form-data')
(前端预览文件?
方式1:
input的change事件const readr = new FileReader();
reader.readAsDataURL(this.files[0]);
reader.addEventListener('load',()=>{ img.src = reader.result; })
方式2:
img.src = window.URL.createObjectURL(this.files[0])
方式3:
img.src = 后端传过来文件上传到服务器上的临时地址
监听load事件完毕,再把img设置为display=''
)
const formidable = require("formidable");
const fS = require('fs');
const path = require("path");
app.post('/upload', (req,res) => {
const form = new Formidable.IncomingForm(); //创建解析对象
form.uploadDir = Path.join(__dirname, './Public/'); //设置上传路径
form.keepExtensions = true; // 保存文件后缀名
// 解析表单对象,fields对象为字段名,files对象为上传的文件
form.parse(req,function (err, fields, files) {
if (err) throw err;
const FilePath = files.Content.path; //Content为formdata append文件的对应字段
const NewPath = path.join(Path.dirname(FilePath), files.Content.name);
fS.rename(FilePath, NewPath, function (err) {
if (err) throw err;
let msg = {
errno: 0,
data:['http://localhost:8080/' + files.Content.name]
};
res.json(msg); //返回前台json数据,包含一个errno和data(包含文件的地址)
});
});
});
c.路由参数 (restful风格+动态路由)
url: localhost:3000/find/123
app.get('/find/:id',(req,res)=>{
log(req.params) // {id:123}获取路由参数
})
d.重定向
res.redirect('/')
e.cookie
// 存储量小 一般4k 正常情况不加密,实际使用需要加密
npm i cookie-parser
const cookieParser=require("cookie-parser");
app.use(cookieParser());
// 设置cookie
res.cookie("name",'zhangsan',{maxAge: 900000, httpOnly: true}); //参数:名称,值,{配置信息}
参数说明:
domain: 域名
name=value:键值对,可以设置要保存的 Key/Value,注意这里的 name 不能和其他属性项的名字一样
Expires: 过期时间(秒),在设置的某个时间点后该 Cookie 就会失效
maxAge: 最大失效时间(毫秒),设置在多少后失效 。
secure: 值为true时,cookie在HTTP中是无效,在HTTPS中才有效 。
Path: 表示在哪个路由下可以访问到cookie。
httpOnly:微软对COOKIE 做的扩展。如果设置了“httpOnly”属性,则通过脚本无法读取到COOKIE信息
singed:是否签名cookie,设为true会对cookie签名,需要用 res.signedCookies而不是 res.cookies访问cookie;且被篡改的签名cookie会被服务器拒绝,会重置为它的原始值
// 获取cookie
req.cookie.name //得到'zhangsan'
------------------------多个二级域名共享cookie-------------------------
// 实现相应路由下多个二级路由的cookie共享
domain: "abc.com" abc.com这个顶级域名下的二级域名都可以访问
------------------------cookie加密-------------------------------------
// 1.设置cookie时进行加密,设置 signed:true 获取是使用req.signedCookies.name
// 原理:signed设置为true后,底层会将cookie的值与“secret”进行hmac加密
// 2.直接对cookie值加密,使用node的crypto模块
*****************md5摘要算法(加签名,防篡改)***************
定义md5.js
const crypto=require('crypto');
module.exports={
MD5_SUFFIX:'FIFJOSDSXMJVRO039292MKK3J5NO2J', // 提高安全性的做法,加入随机字符串
md5:function(str){ // str为需要加密的字符串,utf-8避免出现中文加密不一致情况
return crypto.createHash('md5').update(str,'utf-8').digest('hex');
}
}
****************使用****************
const crypto =require('./md5')
const str = '123456';
console.log(crypto.md5(str+crypto.MD5_SUFFIX));
****************封装一个AES对称加密解密模块****************
/* 写在前面:
createCipheriv方法接受三个参数:
algorithm用于指定加密算法,如aes-128-ecb、aes-128-cbc等;
key是用于加密的密钥;
iv参数可选,用于指定加密时所用的向量
注意这里的密钥必须是8/16/32位,如果加密算法是128,则对应的密钥是16位,如果加密算法是256,则对应的密钥是32位
例如:ase-128-cbc 加密算法要求key和iv长度都为16
*/
const crypto = require('crypto');
const secretkey = Buffer.from('123456789abcdefg', 'utf8'); //设置唯一(公共)秘钥
const serectIv = Buffer.from('abcdefg123456789', 'utf8'); // 16位
// AES对称加密函数
function encrypt(data, key = secretkey, iv = serectIv) {
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv); //使用aes-128-cbc加密
let enc = cipher.update(data, 'utf8', 'hex'); //编码方式从utf-8转为hex
return (enc += cipher.final('hex'));
}
// AES对称解密函数
function decrypt(data, key = secretkey, iv = serectIv) {
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);//使用aes-128-cbc解密
let dec = decipher.update(data, 'hex', 'utf8');//编码方式从hex转为utf-8
return dec += decipher.final('utf8');
}
module.exports = { encrypt, decrypt }
调用的时候直接导入文件,然后encrypt(data) decrypt(data)
f.session
npm i express-session
const session=require("express-session");
// 配置session中间件
app.use(session({
secret: 'secret key', // 用来加密信息的参数
resave: false,
saveUninitialized: false,
cookie: (
'name',
'value',
{
maxAge: 24 * 60 * 60 * 1000, //设置客户端cookie有效期限
secure: false,
resave:false
}
)
}));
session(options)的配置主要有
name - cookie的名字(原属性名为 key)(默认:’connect.sid’)
store - session存储实例
secret - 用它来对session cookie签名,防止篡改
genid - 生成新sessionID的函数 (默认使用uid2库)
rolling - 在每次请求时强行设置cookie,这将重置cookie过期时间(默认:false)
resave - 强制保存session即使它并没有变化 (默认: true, 建议设为:false)
proxy - 当设置了secure cookies(通过”x-forwarded-proto” header )时信任反向代理。当设定为true时,”x-forwarded-proto” header 将被使用。当设定为false时,所有headers将被忽略。当该属性没有被设定时,将使用Express的trust proxy。
saveUninitialized - 强制将未初始化的session存储。当新建了一个session且未设定属性或值时,它就处于未初始化状态。在设定一个cookie前,对于登陆验证减轻服务端存储压力,权限控制是有帮助的(默认:true)
unset - 控制req.session是否取消(例如通过 delete,或者将它的值设置为null)。这可以使session保持存储状态但忽略修改或删除的请求(默认:keep)
//设置session
app.use('/login',function(req,res){
req.session.userinfo.name='lisi';
res.send("登陆成功!");
});
//获取session
app.use('/',function(req,res){
if(req.session.userinfo){
res.send('welcome' + req.session.userinfo.name + '!');
}else{
res.send("请登陆");
}
});
//重新设置cookie的过期时间 单位ms
req.session.cookie.maxAge=1000;
// 删除session
app.use('/logout',function(req,res){
req.session.destroy(function () {
res.clearCookie('connect.sid');// 删除cookie
res.redirect('/admin/login');// 重定向到用户登录页面
req.app.locals.userInfo = null;// 清除公共信息
}
});
------------------------引申 express-mysql-session----------------------
// 将session上传存储到mysql数据库
const express=require("express");
const mysql=require("mysql");
const cors=require("cors"); // 处理跨域
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const options = {
host: 'localhost',
port: 3306,
user: 'root',
password: 'root',
database: 'session_store'
}
const sessionConnection = mysql.createConnection(options); // 数据库连接
const sessionStore = new MySQLStore({
expiration: 1000*60*60*3,
createDatabaseTable: true, // 创建表
schema: { // 表规则
tableName: 'session', // 表名
columnNames: { // 列
session_id: 'session_id',
expires: 'expires',
data: 'data'
}
}
},sessionConnection);
const app = express();
app.use(session({
key: 'sessione_name', // 自行设置的签名
secret: '123456', //密匙
store: sessionStore, //存储管理器***
resave: false,
saveUninitialized: false,
cookie:('name', 'value',{ maxAge: 12*60*1000,
secure: false,
name: "seName",
resave: false}))
}));
app.use(cors());
.....后面存取session是一样的操作
扩展1:express中的模板引擎(了解)
ejs https://ejs.bootcss.com/
express-art-template
需要同时下载 art-template express-art-template
使用方式
// 当渲染后缀名为art的模板文件时,使用express-art-template模板引擎
app.engine('art',require('express-art-template'))
// 设置模板存放目录
app.set('views',path.join(__dirname,'views'); //第一个参数固定,views
// 设置默认模板后缀,自动帮我们拼接
app.set('view engine','art');
// 配置完之后的好处:使用express提供的rend()方法
app.get('/',(req,res)=>{
res.rend('index') // 渲染模板
})
app.locals对象
-
将变量设置到app.locals对象下,所有的模板中都可以获取到,公共数据存取很方便
// app.locals对象下添加一个自定义users属性,属性的值可以自己任意设置
app.locals.users = [
{name:‘张三’,age:18}
{name:‘李思’,age:40}
]
// 模板中使用时,可以直接写users拿到 app.locals对象下添加的users属性值
{{each users}}
- {{KaTeX parse error: Expected 'EOF', got '}' at position 11: value.name}̲}---{{value.age}}
{{/each}}res.app.locals 通过res也可以设置和获取app.locals对象
扩展2:Express脚手架
npm i express-generator -g
// 创建一个名称为app的express应用并使用ejs模板引擎
express --view=ejs app
目录结构基本大同小异
扩展3:项目技能
1.密码加密bcrypt
哈希加密,单程加密方式,加入随机字符串增加密码被破解的难度
bcrypt依赖环境 1.Python2.x 2.node-gyp (-g) 3.windows-build-tools (--global --production)
// 导入bcrypt
const bcrypt = require('bcrypt');
// 返回生成的随机字符串
const salt = await bcrypt.genSalt(10);
// 对密码进行加密,返回值是加密后的密码,参数是密码和随机字符串
const result = await bcrypt.hash('123456', salt);
// 密码比对 返回值true或false
let isValid = await bcrypt.compare(明文密码, 加密密码);
2.登录拦截
// 写在需要拦截的路由之前或者所有路由之前,搭配session使用
// 拦截请求 判断用户登录状态
app.use('/admin', require('./middleware/guard'));
const guard = (req, res, next) => {
if (req.url != '/login' && !req.session.username) {
res.redirect('/admin/login');
} else {
// 如果用户是登录状态 并且是一个普通用户
if (req.session.role == 'normal') {
return res.redirect('/home/')
}
// 用户是登录状态 将请求放行
next();
}
}
3.Joi验证器,node服务端好用的验证工具
// 定义时写在model的构造函数js文件中
// 引入joi模块
const Joi = require('joi');
// 验证器
const validateUser = user => {
// 定义对象的验证规则
const schema = {
username: Joi.string().min(2).max(12).required().error(new Error('用户名不符合验证规则')),
email: Joi.string().email().required().error(new Error('邮箱格式不符合要求')),
password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/).required().error(new Error('密码格式不符合要求')),
role: Joi.string().valid('normal', 'admin').required().error(new Error('角色值非法')),
state: Joi.number().valid(0, 1).required().error(new Error('状态值非法'))
};
// 实施验证
return Joi.validate(user, schema);
}
// 使用时 引入集合构造函数和验证器
const { User, validateUser } = require('../../model/user');
// 使用验证
try {
await validateUser(req.body)
}catch (e) {
// 验证没有通过
return next(JSON.stringify({path: '/admin/user-edit', message: e.message}))
}
4.数据分页
-
分页功能核心要素:当前页,总页数(向上取整)
和mongoose/mongodb配合的示例
let current = req.body.current || 1 // 当前页码
let pagesize = req.body.pagesize //每页条数
let count = await User.countDocuments({查询条件}) // 查询数据总数
let total = Math.ceil(count / pagesize) // 计算总页数
// 数据开始查询位置=(current-1)*pagesize
let start = (current-1)*pagesize
User.find({查询条件}).limit(pagesize).skip(start)