一、技术栈
想到什么记录什么,比较乱
express+mongodb+ejs
以及一堆第三方插件
效果总览:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XWARaNMk-1659142148445)(总览.PNG)]
目录介绍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-unYPNrUl-1659142148448)(目录.PNG)]
config
配置文件,主要配置了数据库访问路径
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w5YBebYs-1659142148449)(配置文件.PNG)]
model
存放了 数据库每个集合(表)的 module 以及工具类
core.js 链接数据库
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K6LTZLTr-1659142148450)(连接数据库.PNG)]
node_modules 第三方插件存放的地方
routers 路由配置
三个大模块 admin后台路由 index 前台路由 api 接口路由
其中admin文件夹下的都是admin后台的路由配置
admin下的main.js 是公共的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDWQnOPn-1659142148451)(路由.PNG)]
static 静态资源存放
-- admin 后台页面使用的css,js等资源
-- bootstrap
-- css
--images
-- js
-- upload 文件上传所保存的位置
-- wysiwyg-editor 富文本编辑器所使用的静态资源
views 界面文件(html)
和路由的配套
-- admin
-- public 为公共的模块,抽离出来的
比如:公共的头部,导航栏,以及成功,失败页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yp4rpztk-1659142148453)(视图.PNG)]
app.js 项目入口 启动 node app.js
配置了所有共有使用的模块
package.json 依赖文件
一些
async 和await
由于 操作数据库是异步的,会出现代码执行了,而数据库还没操作完成的情况,会出现很多错误
因此 将方法定义为 async方法 操作数据库前 添加 await
二、第三方插件介绍
ejs
模板引擎,用来渲染后端的数据
const ejs = require("ejs");
// 配置模板引擎
app.engine('html',ejs.__express);
app.set('view engine', 'html');
//配置静态web目录
app.use(express.static("static"));
body-parser
用来接收表单提交的数据
const bodyParser = require("body-parser");
// 配置body-parser
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// req.body 就可以获取表单穿的数据
express-session
配置session 用来拦截未登录的用户
const session = require("express-session");
// 配置session
app.use(session({
secret: 'this is session', //服务器端生成 session 的签名
name:"liu", //修改 session 对应 cookie 的名称
resave: false, //强制保存 session 即使它并没有变化
saveUninitialized: true, //强制将未初始化的 session 存储
cookie: {
maxAge:1000*60*30,
secure: false // true 表示只有 https 协议才能访问 cookie
},
rolling:true //在每次请求时强行设置 cookie,这将重置 cookie 过期时间(默认:false)
})
);
mongoose
连接数据库
const mongoose = require("mongoose");
multer
文件上传
const multer = require('multer');
let tools={
multer(){
var storage = multer.diskStorage({
//配置上传的目录
destination: async (req, file, cb)=>{
//1、获取当前日期 20200703
let day=sd.format(new Date(), 'YYYYMMDD');
// static/upload/20200703
let dir=path.join("static/upload",day)
//2、按照日期生成图片存储目录 mkdirp是一个异步方法
await mkdirp(dir)
cb(null, dir) //上传之前目录必须存在
},
//修改上传后的文件名
filename: (req, file, cb)=> {
//1、获取后缀名
let extname= path.extname(file.originalname);
//2、根据时间戳生成文件名
cb(null, Date.now()+extname)
}
})
var upload = multer({ storage: storage })
return upload;
}
}
// 使用 括号里的字段根据前端的属性来
tools.multer().single("focus_img")
silly-datetime
获得时间
const sd = require('silly-datetime');
var d=new Date()
return d.getTime()
svg-captcha
验证码
const svgCaptcha = require('svg-captcha');
router.get('/verify', function (req, res) {
var captcha = svgCaptcha.create();
// 将验证码的文字信息存在session中
req.session.captcha = captcha.text;
res.type('svg');
res.status(200).send(captcha.data);
});
三、路由挂载
app.js
访问localhost:3000/ 跳转 index.js的路由
访问localhost:3000/admin 跳转 admin.js的路由
访问localhost:3000/api 跳转 api.js的路由
const admin = require("./routes/admin");
const index = require("./routes/index");
const api = require("./routes/api");
const app = express();
// 配置外部路由模块
app.use("/admin",admin);
app.use("/api",api);
app.use("/",index);
admin.js
访问localhost:3000/admin/ 跳转 main.js的路由
访问localhost:3000/admin/login 跳转 login.js的路由
const express = require("express");
var router = express.Router();
// 引入模块
const user = require("./admin/user");
const login = require("./admin/login");
const nav = require("./admin/nav");
const manager = require("./admin/manager");
const main = require("./admin/main");
const focus = require("./admin/focus");
const articleCate = require("./admin/articleCate");
const article = require("./admin/article");
const setting = require("./admin/setting");
// 挂载路由
router.use("/",main);
router.use("/user",user);
router.use("/login",login);
router.use("/nav",nav);
router.use("/manager",manager);
router.use("/focus",focus);
router.use("/articleCate",articleCate);
router.use("/article",article);
router.use("/setting",setting);
module.exports = router;
四、登录
1.配置前端页面
views/admin/login/login.html 提交路径为/admin/login/doLogin 方式 post
静态资源在 static/admin/
2.编写路由处理
routes/admin/login.js (先把这个路由挂载在admin.js中,admin.js又挂载在app.js)
// 1. 加载登录界面
// localhost:3000/admin/login
router.get("/", async(req,res)=>{
res.render("admin/login/login.html");
})
// 2.验证码
router.get('/verify', function (req, res) {
var captcha = svgCaptcha.create();
// 将验证码的文字信息存在session中
req.session.captcha = captcha.text;
res.type('svg');
res.status(200).send(captcha.data);
});
验证码的html
<dd>验 证 码:
<input id="verify" type="text" name="verify">
<img id="verify_img" src="/admin/login/verify"
title="看不清?点击刷新"
onclick="javascript:this.src='/admin/login/verify?mt='+Math.random()">
</dd>
3.处理登录请求
router.post("/doLogin",async (req,res)=>{
// 获取登录表单的信息
let verify = req.body.verify;
let username = req.body.username;
let password = req.body.password;
// 输入的验证码小写与 正确的验证码小写 是否相等
if(verify.toLocaleLowerCase()!= req.session.captcha.toLocaleLowerCase()){
// 输入不正确
res.render("admin/public/error.html",{
"redirectUrl":"/admin/login", // 回到登录页面
"message": "验证码错误"
});
}
// 判断用户名,密码是否正确
// 根据表单提交的信息去数据库查询
let result = await ManagerModel.find({"username":username,"password":md5(password)});
// 如果存在该用户(用户名密码正确)
if(result.length>0){
// 保存用户信息在session中
req.session.userinfo = result[0];
// 输入正确
res.render("admin/public/success.html",{
"redirectUrl":"/admin", // 回到登录页面
"message": "正在前往登录"
});
}else {
res.render("admin/public/error.html",{
"redirectUrl":"/admin/login", // 回到登录页面
"message": "用户名或密码错误"
});
}
})
4.退出登录
// 退出登录
router.get("/loginOut",(req,res)=>{
// 清空session
req.session.userinfo = null;
// 重定向到登录页面
res.redirect("/admin/login");
})
5.配置拦截器
规定 /admin 下的所有路由都要经过登录后才能访问
在admin.js中配置 中间件 拦截器
原理: 路由请求前会先经过中间件 判断session是否存在用户,存在就放行,不存在就跳转到登录页面
// 配置拦截器
// 为了方便进行代码调试,先注释,需要再打开
// router.use((req,res,next)=>{
// // 用url模块处理路由,防止get请求携带参数
// let pathname = url.parse(req.url).pathname;
// // 如果session中存在用户,放行
// if(req.session.userinfo && req.session.userinfo.username){
// next();
// }else {
// // 这些请求放行
// if(pathname=="/login"||pathname=="/login/doLogin"||pathname=="/login/verify"){
// next();
// }else {
// // 拦截剩余请求
// // 重定向到登录页面
// res.redirect("/admin/login");
// }
// }
// });
五、模块的增删查改
只记录一个,其他的都是一样的流程,只是绑定的属性,操作的集合不同
focus模块
1.准备前端页面
views/admin/focus/index.html
views/admin/focus/add.html
views/admin/focus/edit.html
2.配置路由
routes/admin/focus.js
查
router.get("/",async (req,res)=>{
// 数据库查询所有数据
let result = await FocusModel.find({});
// 渲染到 admin/focus页面,携带数据list
res.render("admin/focus",{
list: result
})
});
index.html 使用ejs模板引擎渲染
<%- include ("../public/page_header.html")%>
<div class="panel panel-default">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr class="th">
<th>名称</th>
<th>分类</th>
<th>图片</th>
<th>跳转地址</th>
<th class="text-center">排序</th>
<th class="text-center">状态</th>
<th class="text-center">操作</th>
</tr>
</thead>
<tbody>
<%for(var i=0;i<list.length;i++){%>
<tr>
<td><%=list[i].title%></td>
<td>
<%if(list[i].type==1){%>
网站
<%}else if(list[i].type==2){%>
app
<%}else if(list[i].type==3){%>
小程序
<%}%>
</td>
<td>
<%if(list[i].focus_img){%>
<img src="/<%=list[i].focus_img%>" width="80" />
<%}%>
</td>
<td><%=list[i].link%></td>
<td class="text-center">
<span class="chSpanNum" data-id="<%=list[i]._id%>" data-model="Focus" data-field="sort"><%=list[i].sort%></span>
</span></td>
<td class="text-center">
<%if(list[i].status==1){%>
<!-- 自定义属性 model和字段以及id -->
<img src="/admin/images/yes.gif" class="chStatus" data-id="<%=list[i]._id%>" data-model="Focus" data-field="status" />
<%}else{%>
<img src="/admin/images/no.gif" class="chStatus" data-id="<%=list[i]._id%>" data-model="Focus" data-field="status" />
<%}%>
</td>
<td class="text-center">
<a href="/admin/focus/edit?id=<%=list[i]._id%>">修改</a>
<a class="delete" href="/admin/focus/delete?id=<%=list[i]._id%>">删除</a></td>
</tr>
<%}%>
</tbody>
</table>
</div>
</div>
增
// 先渲染到增加页面
router.get("/add",(req,res)=>{
res.render("admin/focus/add.html");
});
// 处理登录请求
// 调用tools中封装好的上传文件方法
// focus_img 是前端页面的 name值
router.post("/doAdd",tools.multer().single('focus_img'), async(req,res)=>{
// 存在图片则将路径前面的 static/ 去除后存储在数据库
// 因为 访问static里的文件不需要带static/前缀,app.js配置了可以访问
// 不存在则置为空
let focus_img = req.file ? req.file.path.substr(7) : "";
// 表单的其他数据都在req.body中, 拼接上focus_img
let result = new FocusModel(Object.assign(req.body,{"focus_img":focus_img}));
await result.save();
res.render("admin/public/success.html",{
"redirectUrl":"/admin/focus", // 回到列表页
"message": "增加数据成功"
});
});
注意点:
使用multer上传图片,需要在表单form 添加一个属性
enctype="multipart/form-data"
改
// 先获取到需要更改的那一个数据渲染到更改页面
router.get("/edit",async (req,res)=>{
// req.quert.id 获取 get传值
let id = req.query.id;
// 去数据库查询数据
let result = await FocusModel.find({"_id":id});
// 也可以用findOne 这样传递的就是result
// 渲染到 edit页面
res.render("admin/focus/edit.html",{
list: result[0]
});
});
// 处理更改请求
router.post("/doEdit",tools.multer().single("focus_img"),async (req,res)=>{
// console.log(req.body);
// console.log(req.file);
// 先判断是否更新了 图片
if(req.file){ // 更新了 图片
// 去掉 static/
let focus_img = req.file.path.substr(7);
// Object.assign是用来拼接对象的
await FocusModel.updateOne({"_id":req.body.id},
Object.assign(req.body,{"focus_img":focus_img}));
}else { // 没有更新图片直接更新body里的数据就可以了
await FocusModel.updateOne({"_id":req.body.id},req.body);
}
res.render("admin/public/success.html",{
"redirectUrl":"/admin/focus", // 回到列表页
"message": "修改数据成功"
});
});
删
删除数据库数据的同时,删除上传到服务器的图片
router.get("/delete",async (req,res)=>{
// 先获取数据
let id = req.query.id;
let result =await FocusModel.findOne({"_id":id});
// console.log(result);
// 获取路径名 存在就赋值,不存在置为空
let focus_img = result.focus_img;
// 根据id删除
let delResult = await FocusModel.deleteOne({"_id":id});
// console.log(delResult);
// 判断是否删除成功
if(delResult.deletedCount==1){
// 存在图片
if(focus_img){
// 删除上传到服务器的图片
// console.log("/static/"+focus_img);
fs.unlink("static/"+focus_img, (err) => {
console.log(err);
})
}
}
res.render("admin/public/success.html", {
"redirectUrl": "/admin/focus",
"message": "删除数据成功"
})
})
六、界面
view/admin/main/index.html
配置iframe
<%- include ("../public/page_header.html")%>
<nav class="navbar navbar-inverse" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<img src="/admin/images/node.jpg" height="44px;" />
</div>
<div class="collapse navbar-collapse" id="example-navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li><a>欢迎您,admin</a>
</li>
<li><a href="/admin/login/loginOut">安全退出</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-sm-2">
<%- include ("../public/page_aside.html")%>
</div>
<div class="col-sm-10">
<iframe name="rightMain" style="border:none;" id="rightMain" src="/admin/welcome" frameborder="false" scrolling="auto" width="100%" height="100%">
</iframe>
</div>
</div>
</div>
</body>
</html>
page_aside.html
配置 target=“rightMain”
<ul class="aside">
<li>
<h4>管理员管理</h4>
<ul>
<li class="list-group-item"> <a href="/admin/manager" target="rightMain"> 管理员列表</a></li>
<li class="list-group-item"> <a href="/admin/manager/add" target="rightMain">增加管理员</a></li>
</ul>
</li>
<li>
<h4>分类管理</h4>
<ul>
<li class="list-group-item"> <a href="/admin/nav" target="rightMain"> 导航管理</a></li>
<li class="list-group-item"> <a href="/admin/nav/add" target="rightMain">增加导航</a></li>
</ul>
</li>
<li>
<h4>轮播图管理</h4>
<ul>
<li class="list-group-item"> <a href="/admin/focus" target="rightMain"> 轮播图管理</a></li>
<li class="list-group-item"> <a href="/admin/focus/add" target="rightMain">增加轮播图</a></li>
</ul>
</li>
<li>
<h4>内容管理</h4>
<ul>
<li class="list-group-item"> <a href="/admin/articleCate" target="rightMain"> 分类管理</a></li>
<li class="list-group-item"> <a href="/admin/article" target="rightMain">内容管理</a></li>
</ul>
</li>
</ul>
这样通过侧边导航栏访问的页面就会出现在ifram框里面
上边导航栏
<ul id="myTab" class="nav nav-tabs">
<li class="active">
<a href="#index" data-toggle="tab">基本设置</a></li>
<li>
<a href="#info" data-toggle="tab">
内容详情
</a>
</li>
<li>
<a href="#seo" data-toggle="tab">
Seo设置
</a>
</li>
</ul>
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade in active" id="index">
</div>
<div class="tab-pane fade" id="info">
<textarea name="content" id="content" cols="30" rows="10"></textarea>
</div>
<div class="tab-pane fade" id="seo">
</div>
七、点击图标修改状态,点击数量修改数量
1.前端定义自定义属性和类
<span
class="chSpanNum"
data-id="<%=list[i]._id%>"
data-model="Focus"
data-field="sort">
<%=list[i].sort%>
</span>
<%if(list[i].status==1){%>
<!-- 自定义属性 model和字段以及id -->
<img src="/admin/images/yes.gif"
class="chStatus"
data-id="<%=list[i]._id%>"
data-model="Focus"
data-field="status" />
<%}else{%>
<img src="/admin/images/no.gif"
class="chStatus"
data-id="<%=list[i]._id%>"
data-model="Focus" data-field="status" />
<%}%>
可以看到定义了两个类chStatus chSpanNum
static/admin/js/base.js中 编写触发事件和请求
var app = {
init(){
this.changeStatus();
this.changeNum();
},
// 鼠标点击 改变状态
changeStatus: function (){
// 绑定
$(".chStatus").click(function(){
// 获取 自定义属性的值
var id = $(this).attr("data-id");
var model = $(this).attr("data-model");
var field = $(this).attr("data-field");
var el = $(this);
// 异步请求
$.get("/admin/changeStatus", // 请求路径
{
// 携带参数
id:id,
model:model,
field:field
},function(response){ // 回调函数
//console.log(response)
if(response.success){
// 后端数据库更新成功
// 如果原来是 yes的图片 改为no
if(el.attr("src").indexOf("yes")!=-1){
// 更改 src属性的值
el.attr("src", "/admin/images/no.gif");
}else{
el.attr("src", "/admin/images/yes.gif");
}
}
}
)
})
},
// 鼠标点击 更改数量
changeNum: function(){
// 1. 绑定
$(".chSpanNum").click(function(){
// 2. 获取自定义属性的值
var id=$(this).attr("data-id");
var model=$(this).attr("data-model");
var field=$(this).attr("data-field");
var spanEl=$(this);
// 获取 span框里的数值
//.html 和.html()是不一样的 切记切记
var spanNum=$(this).html();
// alert(spanNum)
// 编写输入框,加到span框体里
var input=$("<input value='' style='width:60px'/> ");
$(this).html(input);
// console.log("执行了83行");
// 此处jquery报错
// 获得焦点,将原先的数值添加到输入框
$(input).trigger('focus').val(spanNum);
// console.log("执行了85行");
// 停止冒泡
$(input).click(function(e){
e.stopPropagation();
})
// 输入框失去焦点时触发事件
$(input).blur(function(){
// 获取输入框的新值
var inputNum = $(this).val();
// 简单的数据校验
// 如果值是正数,添加到span标签里,负数或空置为零
if(inputNum>0){
spanEl.html(inputNum);
}else {
spanEl.html(0);
}
// 异步请求
$.get("/admin/changeNum", // 请求路由
// 携带的参数
{id:id,model:model,field:field,num:inputNum},
// 回调函数
function(response){
if(!response.success){
console.log(response);
}
}
)
})
})
}
}
后台处理请求
router/admin/main.js
// 引入所有model
const FocusModel = require("../../model/focusModel");
const NavModel = require("../../model/navModel");
const ManagerModel = require("../../model/managerModel");
const ArticleCateModel = require("../../model/articleCateModel");
const ArticleModel = require("../../model/articleModel");
// 配置 所有的model
let appModel = {
FocusModel: FocusModel,
NavModel: NavModel,
ManagerModel: ManagerModel,
ArticleCateModel:ArticleCateModel,
ArticleModel:ArticleModel
}
// 处理所有的 状态更改请求
/**
* 前提知识 es6里面的属性名表达式
* var aaa="name"
* var obj = {
* [aaa]:"张三"
* }
* console.log(obj)
* 结果为 {"name":"张三"} 可以用字符串去匹配预先的对象
*/
router.get("/changeStatus",async (req,res)=>{
// 获取get传值
let id = req.query.id;
// 传过来的是数据库的表名 需要拼接成model的形式
let model = req.query.model + "Model";
let field = req.query.field; // 要修改的字段 status
let json; // 需要更新的数据
// 去数据库查询此id 信息
// model为拼接好的串,在appModel 里匹配到相应的Model
let result = await appModel[model].find({"_id":id});
// 结果存在
if(result.length>0){
// result是一个对象,获得第一个对象 用field去匹配相应的字段
let tempField = result[0][field];
//字段值是否为一 封装需要更新的数据对象
tempField == 1?json={[field]:0} : json={[field]:1};
// 数据库更改
await appModel[model].updateOne({"_id":id},json);
res.send({
success: true,
message: "修改状态成功"
});
}else {
res.send({
success: false,
message: "修改状态失败"
});
}
});
router.get("/changeNum",async(req,res)=>{
// 获得get传值
let id = req.query.id;
let model = req.query.model + "Model";
let field = req.query.field; // 要修改的字段sort
let num = req.query.num;
// 先查询是否存在
let result = await appModel[model].find({"_id":id});
//结果存在
if(result.length>0){
let json = {
// 例如 字段sort 的值是 num
[field]:num
};
await appModel[model].updateOne({"_id":id},json);
res.send({
success: true,
message: "修改数量成功"
});
}else {
res.send({
success: false,
message: "修改数量失败"
});
}
})
八、富文本编辑器
wysiwyg-editor
https://www.froala.com/wysiwyg-editor/docs/options
node中使用 建议参考官方文档
<!-- Include Editor style. -->
<link href="https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_editor.pkgd.min.css" rel="stylesheet" type="text/css" />
<!-- Create a tag that we will use as the editable area. --><!-- You can use a div tag as well. -->
<textarea></textarea>
<!-- Include Editor JS files. -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/froala-editor@latest/js/froala_editor.pkgd.min.js">
</script>
<!-- Initialize the editor. -->
<script> new FroalaEditor('textarea');</script>
汉化
https://www.froala.com/wysiwyg-editor/languages
1、引入 zh_cn 的语言包
2、配置 language: ‘zh_cn’
自定义导航条
https://www.froala.com/wysiwyg-editor/docs/options#toolbarBottom
new FroalaEditor('#content',{
height: 300,
language: 'zh_cn',
toolbarButtons: [ ['bold', 'strikethrough', 'subscript', 'superscript', 'outdent', 'indent', 'clearFormatting', 'insertTable', 'html'] ,['undo', 'redo']],
toolbarButtonsXS: [ ['bold', 'strikethrough', 'subscript', 'superscript', 'outdent', 'indent', 'clearFormatting', 'insertTable', 'html'] , ['undo', 'redo']] });
上传图片
https://www.froala.com/wysiwyg-editor/docs/options#imageUploadURL
new FroalaEditor('#content',
{ height: 300,
language: 'zh_cn',
imageUploadURL: '/admin/goods/doUpload', })
router.post("/doUploadImage", multer().single('file'), async (req, res) => { var article_img = req.file ? req.file.path.substr(7) : "";
res.send({link: "/"+article_img});
//注意:后台返回数据格式:{link: 'path/to/image.jpg'} })
九、分页
使用分页插件
1、首先引入jQuery和jqPaginator
2、定义一个空的div 让这个div的class pagination
https://v3.bootcss.com/components/#pagination
3、初始化
http://jqpaginator.keenwon.com/
$('#id').jqPaginator({
totalPages: 100,
visiblePages: 10,
currentPage: 1,
onPageChange: function (num, type) {
$('#text').html('当前第' + num + '页');
}
});
*db.表名.find().skip((page-1)pageSize).limit(pageSize)
<script>
$('#pagination').jqPaginator({
totalPages: <%=totalPages%>,
visiblePages: 5,
currentPage: <%= page %>,
onPageChange: function (num, type) {
if(type=="change"){
location.href="/admin/article?page="+num+"&keywords=<%=keywords%>";
}
// console.log(type);
// console.log('当前第' + num + '页')
}
})
</script>
十、配置全局变量
app.locals.adminPath=”experss_admin”
或者
req.app.locals.adminPath=”experss_admin”