基本结构搭建
npm i -g express-generator
: 安装 npm 包 (已经安装就第二步)
express -e accounts
: 快速创建文件夹, 添加ejs
模板引擎的支持
在views
中通过 ejs 写好页面 (关于页面的显示这里就不多介绍)
将所需要的 js 和 css 文件存放到 public 文件夹下
在routes/index.js
中写好增删改查
npm i
: 安装依赖
npm start
: 运行项目 (在 bin / www 文件, 端口用的 3000)
package.json 中的 start 命令可以改为 nodemon
app.js代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 var createError = require ('http-errors' );var express = require ('express' );var path = require ('path' );var cookieParser = require ('cookie-parser' );var logger = require ('morgan' );var indexRouter = require ('./routes/index' ); var app = express (); app.set ('views' , path.join (__dirname, 'views' )); app.set ('view engine' , 'ejs' ); ❗ app.use (logger ('dev' )); app.use (express.json ()); app.use (express.urlencoded ({ extended : false })); app.use (cookieParser ()); app.use (express.static (path.join (__dirname, 'public' ))); app.use ('/' , indexRouter); app.use (function (req, res, next ) { next (createError (404 )); }); app.use (function (err, req, res, next ) { res.locals .message = err.message ; res.locals .error = req.app .get ('env' ) === 'development' ? err : {}; res.status (err.status || 500 ); res.render ('error' ); });module .exports = app;
Mongodb 连接 下载 mongdb — 省略
启动服务
记得在 cmd 中先启动服务 (两个 cmd 窗口分别启动)
运行命令 mongod
运行命令 mongosh 连接本机的 mongodb 服务
使用 mongoose
npm i mongoose
: 先安装 mongoose
Mongoose 是一个对象文档模型库 , 为了方便使用代码操作 mongodb数据库
Mongoose 代码模块化
我们将 mongoose 的基本使用封装为一个 db 文件, 里面装一个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 module .exports = function (success, error ) { if (typeof error !== 'function' ) { error = () => { console .log ('连接失败' ); } } const mongoose = require ('mongoose' ); mongoose.connect ('mongodb://127.0.0.1:27017/weirdo' ) mongoose.connection .once ('open' , () => { success (); }) mongoose.connection .once ('error' , () => { error () }) mongoose.connection .once ('close' , () => { console .log ('连接关闭' ); }) }
这样我们就可以在需要使用的地方使用db函数连接数据库即可
导入db文件
导入模型对象
我们还准备把数据库的一些信息单独拿出来, 做一个配置文件config, 那么以后信息需要修改的时候也比较方便
1 2 3 4 5 6 module .exports = { DBHOST : '127.0.0.1' , DBPORT : '27017' , DBNAME : 'weirdo' }
然后到db.js文件中使用即可
在www文件中导入并调用db函数
1 2 3 4 5 6 7 8 const db = require ("../db/db" ); db (() => { ... ... });
创建AccountModel.js
模型对象
我们在根目录下创建一个modules文件夹, 存放一些数据库需要的模型对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const mongoose = require ('mongoose' )let AccountSchema = new mongoose.Schema ({ title : { type : String , require : true , }, time : Date , type : { type : Number , default : -1 , }, account : { type : Number , require : true }, remarks : { type : String , } })let AccountModel = mongoose.model ('accounts' , AccountSchema );module .exports = AccountModel ;
这里由于我们的时间time
在最后获取到的是一个字符串, 我们要将它转换为Date
型, 就要用到一个工具包
插入数据
routes/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 router.post ('/account' , (req, res ) => { AccountModel .create ({ ...req.body , time : moment (req.body .time ).toDate () }).then (() => { res.render ('success' , {msg : '添加成功hhh' , url : '/account' }) }).catch ((err ) => { res.status (500 ).send ('插入失败' ) }) })
读取数据
routes/index.js
这里用到了格式化时间, 同样需要导入moment
, 然后这里传递moment
到ejs
去
1 2 3 4 5 6 7 8 9 router.get ('/account' , function (req, res, next ) { AccountModel .find ().then ((data ) => { res.render ('list' , {accounts : data, moment : moment, url : '/account/create' }) }).catch (() => { res.status (500 ).send ('读取失败' ) }) });
然后使用格式化时间
另外, 由于数据库里面的id
是_id
, 所以我们将list.ejs
的相关字段改一下
其实不改也行, mongodb默认也会帮我们操作
删除数据
1 2 3 4 5 6 7 8 9 10 11 12 router.get ('/account/:id' , (req, res ) => { let id = req.params .id ; AccountModel .deleteOne ({ _id : id, }).then (() => { res.render ('success' , {msg : '删除成功hhh' , url : '/account' }) }).catch (() => { res.status (500 ).send ('删除失败' ) }) })
为了使前端不要误操作, 我们给删除按钮添加一个验证
1 2 3 4 5 6 7 8 9 10 11 let delBtns = document .querySelectorAll ('.delBtn' ); delBtns.forEach ((item ) => { item.addEventListener ('click' , (e ) => { if (confirm ('您确定要删除该条文档吗?' )) { return true ; } else { e.preventDefault () } }) })
添加接口功能 为了将来我们在app, 安卓, ios中开发完程序后, 到时候直接和接口对接, 那么整个功能就完成了
首先先启动 mongod 服务, 然后本地连接 mongosh
npm start 打开我们通过 express-generator 做的记账本案例
在routes下添加以下文件
这里的web/index.js就是原先的index.js
另外, 在app.js中修改相关代码
1 2 3 4 5 6 const indexRouter = require ("./routes/web/index" );const accountRouter = require ("./routes/api/account" ); ... ... app.use ("/" , indexRouter); app.use ("/api" , accountRouter);
这样, 我们通过api
访问到的就是接口返回的信息了
api/account.js 代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 var express = require ("express" );var router = express.Router ();const moment = require ("moment" );const AccountModel = require ("../../modules/AccountModel" ); router.get ("/account" , function (req, res, next ) { AccountModel .find () .then ((data ) => { res.json ({ code : '0000' , msg : '读取成功' , data : data, }) }) .catch (() => { res.json ({ code : '1001' , msg : '读取失败' , data : null }) }); }); router.post ("/account" , (req, res ) => { AccountModel .create ({ ...req.body , time : moment (req.body .time ).toDate (), }) .then ((data ) => { res.json ({ code : '0000' , msg : '添加成功' , data : data }) }) .catch ((err ) => { res.json ({ code : '1002' , msg : '添加失败' , data : null }) }); }); router.delete ("/account/:id" , (req, res ) => { let id = req.params .id ; AccountModel .deleteOne ({ _id : id, }) .then (() => { res.json ({ code : '0000' , msg : '删除成功' , data : {} }) }) .catch (() => { res.json ({ code : '1003' , msg : '删除失败' , data : null }) }); }); router.get ('/account/:id' , (req, res ) => { let {id} = req.params ; AccountModel .findById (id).then (data => { res.json ({ code : '0000' , msg : '获取成功' , data : data }) }).catch (err => { res.json ({ code : '1004' , msg : '获取失败' , data : null }) }) }) router.patch ('/account/:id' , (req, res ) => { let {id} = req.params ; AccountModel .updateOne ({_id : id}, req.body ).then (() => { AccountModel .findById (id).then (data => { res.json ({ code : '0000' , msg : '更新成功' , data : data }) }).catch (err => { res.json ({ code : '1004' , msg : '获取失败' , data : null }) }) }).catch (err => { res.json ({ code : '1005' , msg : '更新失败' , data : null }) }) })module .exports = router;
只是将之前返回的数据变为了json, 之前是 res.render 渲染 ejs 页面, 现在是返回 json 数据
这样我们就可以在apipost中进行各种操作了
会话控制 cookie
如果是通过 express-generator 进行创建项目的话就不用自己搭建了, 这里使用的是原生express
npm i express : 注意要安装获取 cookie 的包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const express = require ('express' );const cookieParser = require ('cookie-parser' );const app = express (); app.use (cookieParser ()); app.get ('/set-cookie' , (request, response ) => { response.cookie ('username' ,'wangwu' ); response.cookie ('email' ,'23123456@qq.com' , {maxAge : 5 *60 *1000 }); response.send ('Cookie的设置' ); }); app.get ('/get-cookie' , (request, response ) => { console .log (request.cookies ); console .log (request.cookies .username ) response.send ('Cookie的读取' ); }); app.get ('/delete-cookie' , (req, res ) => { res.clearCookie ('username' ); res.send ('删除成功' ) }) app.listen (3000 , () => { console .log ('服务已经启动....' ); });
session
session 是保存在 服务器端的一块儿数据 ,保存当前访问用户的相关信息
它可实现会话控制,可以识别用户的身份,快速获取当前用户的相关信息
运行流程
填写账号和密码校验身份,校验通过后服务端创建 session
信息 ,然后将 session_id
的值通过响应头返回给浏览器
在登录(请求)了一次此网站后, 下次发送请求时会自动携带 cookie
,服务器通过 cookie
中的session_id
的值确定用户的身份
session 的代码操作
安装 npm i express-session
它可在 express
中对 session
进行操作
安装 npm i connect-mongo
, 可以连接 mongodb
数据库, 也可以做一些操作, 这样就可以将session存到数据库
记得要先把 mongodb 跑起来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 const express = require ('express' );const session = require ("express-session" );const MongoStore = require ('connect-mongo' );const app = express (); app.use (session ({ name : 'sid' , secret : 'atguigu' , saveUninitialized : false , resave : true , store : MongoStore .create ({ mongoUrl : 'mongodb://127.0.0.1:27017/weirdo' }), cookie : { httpOnly : true , maxAge : 1000 * 300 }, })) app.get ('/login' , (req, res ) => { if (req.query .username === 'admin' && req.query .password === 'admin' ) { req.session .username = 'zhangsan' ; req.session .email = 'zhangsan@qq.com' res.send ('登录成功' ); } res.send ('登录失败' ); }) app.get ('/home' , (req, res ) => { console .log ('session的信息' ); console .log (req.session .username ); if (req.session .username ) { res.send (`你好 ${req.session.username} ` ); } else { res.send ('登录 注册' ); } }) app.get ('/logout' , (req, res ) => { req.session .destroy (() => { res.send ('成功退出' ); }); }); app.listen (3000 , () => { console .log ('服务已经启动, 端口 ' + 3000 + ' 监听中...' ); });
session 和 cookie 的区别
存在的位置
cookie:浏览器端
session:服务端
安全性
cookie 是以明文的方式存放在客户端的,安全性相对较低
session 存放于服务器中,所以安全性 相对 较好
网络传输量
cookie 设置内容过多会增大报文体积, 会影响传输效率
session 数据存储在服务器,只是通过 cookie 传递 id,所以不影响传输效率
存储限制
浏览器限制单个 cookie 保存的数据不能超过 4K ,且单个域名下的存储数量也有限制
session 数据存储在服务器中,所以没有这些限制
token
token 是服务端生成并返回给 HTTP 客户端的一串加密字符串, token 中保存着 用户信息
它可以实现会话控制,可以识别用户的身份,主要用于移动端 APP
工作流程
填写账号和密码校验身份,校验通过后响应 token,token 一般是在响应体中返回给客户端的
后续发送请求时,需要手动将 token 添加在请求报文中,一般是放在请求头中
特点
服务端压力更小
相对更安全
数据加密
可以避免 CSRF(跨站请求伪造)因为不能自动携带
扩展性更强
和 cookie 的区别
jwt
JWT(JSON Web Token )是目前最流行的跨域认证解决方案,可用于基于 token 的身份验证
JWT 使 token 的生成与校验更规范
我们可以使用 jsonwebtoken 包 来操作 token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 npm i jsonwebtokenconst jwt = require ('jsonwebtoken' );let token = jwt.sign ({ username : 'zhangsan' }, 'zhaoweirduohaoshuai' , { expiresIn : 60 }) jwt.verify (token, 'zhaoweiduohaoshuai' , (err, data ) => { if (err){ console .log ('校验失败~~' ); return } console .log (data); })
项目完善 我们先在 views 文件夹下创建 req.ejs 文件用来存放注册页面
然后我们在web文件夹下新建一个文件 auth.js 用来存放登录和注册所要用到的路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 var express = require ("express" );var router = express.Router ();const UserModel = require ('../../modules/UserModel.js' )const md5 = require ('md5' ) router.get ('/reg' , (req, res ) => { res.render ('reg' ) }) router.post ('/reg' , (req, res ) => { UserModel .create ({ ...req.body , password : md5 (req.body .password ), }).then (() => { res.render ("success" , { msg : "注册成功hhh" , url : "/login" }); }).catch ((err ) => { res.status (500 ).send ("注册失败" ); }); })module .exports = router;
这里你会发现我们对数据库存储数据, 我们需要一个UserModel
我们在这个文件中将数据存储到数据库中, 我们就还需要一个model
所以我们在 modules 文件夹下新建一个 UserModel.js
接收两个数据, 用户名和密码, 并将数据存放在 users 数据库集合中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const mongoose = require ('mongoose' )let UserSchema = new mongoose.Schema ({ username : { type : String , required : true , }, password : { type : String , required : true }, })let UserModel = mongoose.model ('users' , UserSchema );module .exports = UserModel ;
然后我们在 app.js 中导入登录和注册的全局中间件
1 2 3 4 ...const authRouter = require ("./routes/web/auth" ); ... app.use ("" , authRouter);
同样我们在 views 下添加 login.ejs 页面
由于登录时我们也是需要拿到加密后的密码和数据库中的密码进行比对, 也是需要md5加密
然后我们在 auth.js 中写入登录的相关操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 router.get ('/login' , (req, res ) => { res.render ('login' ) }) router.post ('/login' , (req, res ) => { let {username, password} = req.body UserModel .findOne ({ username : username, password : md5 (password) }).then ((data ) => { if (!data) res.send ('账号或者密码错误' ) else { req.session .username = data.username ; req.session ._id = data._id res.render ("success" , { msg : "登录成功hhh" , url : "/account" }); } }).catch (() => { res.status (500 ).send ("登录失败" ); }); })
查询数据库时, 如果data没有值才说明账号或者密码错误, 如果登录成功那还要写入session
写入session我们就需要用到 两个包, 第一个是用express进行session操作, 第二个是连接mongdb数据库
1 npm i express-session connect -mongo
然后我们在app.js中进行导入session的中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const session = require ("express-session" );const MongoStore = require ("connect-mongo" ); ...const {DBHOST , DBNAME , DBPORT } = require ('./config/config' )var app = express (); app.use ( session ({ name : "sid" , secret : "atguigu" , saveUninitialized : false , resave : true , store : MongoStore .create ({ mongoUrl : `mongodb://${DBHOST} :${DBPORT} /${DBNAME} ` , }), cookie : { httpOnly : true , maxAge : 1000 * 60 * 60 * 24 * 7 , }, }) );
另外我们这里的mongoUrl不能写死, 所以我们导入了配置项
用户登录检测
我们访问 account 页面, 无痕模式发现都能进行操作, 这样是不行的, 只有登录了并且返回了session才能进行操作
所以我们在web/index.js
中的每一个接口前加入以下判断
1 2 3 if (!req.session .username ) { return res.redirect ('/login' ); }
我们发现代码冗余, 于是我们将其封装为一个中间件
在根目录下创建 middlewares 文件夹存放中间件, 并将这段代码放进去
1 2 3 4 5 6 7 8 9 10 module .exports = (req, res, next ) => { if (!req.session .username ) { return res.redirect ('/login' ); } next () }
然后在index.js
中导入即可
1 2 3 4 5 6 let checkLoginMiddleware = require ('../../middlewares/checkLoginMiddleware' ) ... router.get ("/account" , checkLoginMiddleware, function (req, res, next ) {... router.get ("/account/create" , checkLoginMiddleware, function (req, res, next ) {... ...
退出业务
我们在auth.js中写退出相关接口
1 2 3 4 5 6 7 router.post ('/logout' , (req, res ) => { req.session .destroy (() => { res.render ('success' , {msg : '退出成功' , url : '/login' }); }) })
主要的操作就是销毁session
然后在回调函数中跳转页面
然后我们在 list.ejs 中添加一个按钮实现点击退出
这里为什么不用 a 标签 然后里面href 写 ‘/logout’ ?
因为如果在其它端口通过link/script/image
标签发送请求, 则也会导致退出登录 => 即XSRF(跨站请求伪造)
所以我们尽量改为post请求, 但是这样还远远不够哈, 具体的解决办法可以看自己的八股
优化路由和首页
1 2 3 4 5 router.get ('/' , (req, res ) => { res.redirect ('/account' ); })
我们创建一个 404.ejs
在url路径错误的情况下放一个公益页面
1 2 3 <body> <script src ="//volunteer.cdn-go.cn/404/latest/404.js" > </script > </body>
然后在app.js 中它帮我们写的位置修改一下, render 404页面
1 2 3 4 app.use (function (req, res, next ) { res.render ('404' ) });
JWT引入
目前还是存在bug, 因为我们的接口端(api那个)不受任何约束
我们在 api 文件夹下创建一个 auth.js 来限制我们的登录
我们先安装 jsonwebtoken : npm i jsonwebtoken
然后在 auth.js 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 var express = require ("express" );var router = express.Router ();const UserModel = require ("../../modules/UserModel.js" );const md5 = require ("md5" );const jwt = require ("jsonwebtoken" ); router.post ("/login" , (req, res ) => { let { username, password } = req.body ; UserModel .findOne ({ username : username, password : md5 (password), }) .then ((data ) => { if (!data) { res.json ({ code : "2002" , msg : "用户名或密码错误" , data : null , }); } else { let token = jwt.sign ( { username : data.username , _id : data._id , }, "weirdo" , { expiresIn : 60 * 60 * 24 * 7 , } ); res.json ({ code : "0000" , msg : "登录成功" , data : token, }); } }) .catch (() => { res.status (500 ).send ("登录失败" ); res.json ({ code : "2001" , msg : "数据库读取失败" , data : null , }); }); }); router.get ("/logout" , (req, res ) => { req.session .destroy (() => { res.render ("success" , { msg : "退出成功" , url : "/login" }); }); });module .exports = router;
对于api相关的接口进行token限制, 至于token如何在客户端进行的保存, 这就不知道了
这样我们在登录成功后就可以拿到token, 那么我们在访问关于account的相关接口时就要去校验
同理我们将校验token写为一个中间件, 放在middlewares文件夹中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const jwt = require ('jsonwebtoken' ) module .exports = (req, res, next ) => { let token = req.get ("token" ); if (!token) { return res.json ({ code : "2003" , msg : "token 缺失" , data : null , }); } jwt.verify (token, "weirdo" , (err, data ) => { if (err) { return res.json ({ code : "2004" , msg : "token校验失败" , data : null , }); } else { next (); } }); };
然后在api/account.js
中的每个接口使用即可
我们现在感觉 ‘weirdo
‘ 这个密钥在创建和校验都使用了, 以后改起来不太方便, 于是我们将其放到配置文件中
1 2 3 4 5 6 7 module .exports = { DBHOST : "127.0.0.1" , DBPORT : "27017" , DBNAME : "weirdo" , secret : "weirdo" , };
另外我们可以在校验token成功后保存用户的信息
然后我们就可以在记账本其它的接口访问这个用户数据(req.user)了
比如
后面的部署上线就自己去操作吧.
文件(图片)上传 注意 这时候已经使用了express-generator
文件上传也是在发送http
请求报文
npm i formidable
: 拿到formidable
对象处理文件上传
routes/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 var express = require ("express" );var router = express.Router ();let {formidable} = require ("formidable" ); router.get ("/" , function (req, res, next ) { res.render ("index" , { title : "Express" }); }); router.get ("/portrait" , (req, res ) => { res.render ("portrait" ); }); router.post ("/portrait" , (req, res, next ) => { let form = formidable ({ uploadDir : __dirname + '/../public/images' , keepExtensions : true }); form.parse (req, (err, fields, files ) => { if (err) { next (err); return ; } let url = '/images/' + files.portrait [0 ].newFilename ; console .log (files); res.send (url); }); });module .exports = router;
portrait.ejs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 文件上传</title > </head > <body > <form action ="/portrait" method ="post" enctype ="multipart/form-data" > 用户名: <input type ="text" name ="username" > <br > 头像 : <input type ="file" name ="portrait" > <button > 点击提交</button > </form > </body > </html >
get请求获取的表单
点击提交后 (即 post 请求 => 进行文件上传)