1- 项目的介绍
-
做的是一个广告管理系统。
-
可以对项目有一个整体的认知。
-
demo说明
- backend:后端项目,提供一些接口
- frontend:前端项目,负责提供向用户展示的界面
- mongodb:用于存放数据库的。
-
启动demo
-
将mongodb server服务禁用
-
通过命令启动数据库,进入到mongo目录中,然后输入以下命令即可,启动成功之后,不要关闭控制台,将其最小化
mongod --dbpath ./ -
启动接口服务
-
配置host:可以通过该配置将指定的域名映射到指定的服务上
-
打开host目录:C:\Windows\System32\drivers\etc\hosts
127.0.0.1 api.zhangpeiyue.com
-
-
进入到接口项目根目录中,安装依赖
cnpm i
-
-
-
启动前端项目
-
进入到前端目录,执行以下命令
cnpm i -
启动项目
npm start -
账号:admin 密码: 11111111
-
2- 搭建前端项目
- 找一个目录,在目录中新建一个名字为
frontend目录,作为前端项目目录。
3- 通过webpack配置项目
-
安装需要的依赖模块
cd frontend cnpm install webpack webpack-cli html-webpack-plugin webpack-dev-server -D -
配置项目
-
在项目根目录新建一个src目录(源码),在src中新建一个名字为index.js的文件(入口文件)
-
在项目根目录中新建一个名字为public的目录,目录中新建一个入口界面(index.html)
- 注意:在打包或在dev server执行时,会将index.js打包后的文件在index.html中进行引入
-
在项目根目录(frontend)当中新建一个webpack.config.js文件(webpack配置文件)
const path = require("path"); // 用于提供模板页面的模块。 const HtmlWebpackPlugin = require("html-webpack-plugin"); // 导出配置信息 module.exports = { // 入口 entry: path.resolve(__dirname, "./src/index.js"), // 出口 output: { // 指定打包以后生成的文件名 filename: "js/app.js", // 指定打包文件所在的目录,必须为绝对地址 path: path.resolve(__dirname, "./build"), // 将上一次打包的内容进行清理 clean: true, // 指定文件引入的目录为站点根目录 publicPath: "/" }, // 插件,值的类型是数组,数组中的每一个元素即是一个插件的配置 plugins: [ new HtmlWebpackPlugin({ // 指定模板(入口页面)地址 template: path.resolve(__dirname, "./public/index.html"), // 指定js引入的位置 放置在body的尾部 inject: "body", // 去除缓存 hash: true, // 压缩处理 minify: { // 去除注释 removeComments: true, // 去除双引号 removeAttributeQuotes: true, // 折叠,去除空格换行 collapseWhitespace: true } }) ], devServer: { // 指定主机域名 hosts 127.0.0.1 zhangpeiyue.com host: "zhangpeiyue.com", // 端口号 port: 80, // 直接打开浏览器 open: true }, mode: "production" }
-
4- webpack配置拆分
-
在项目根目录中创建名字为config目录,用于生成webpack配置
-
config->webpack.prod.js(打包配置)
// 提供打包的配置信息 const path = require("path"); // 用于提供模板页面的模块。 const HtmlWebpackPlugin = require("html-webpack-plugin"); // 导出配置信息 module.exports = { // 入口 entry: path.resolve(__dirname, "../src/index.js"), // 出口 output: { // 指定打包以后生成的文件名 filename: "js/app.js", // 指定打包文件所在的目录,必须为绝对地址 path: path.resolve(__dirname, "../build"), // 将上一次打包的内容进行清理 clean: true, // 指定文件引入的目录为站点根目录 publicPath: "/" }, // 插件,值的类型是数组,数组中的每一个元素即是一个插件的配置 plugins: [ new HtmlWebpackPlugin({ // 指定模板(入口页面)地址 template: path.resolve(__dirname, "../public/index.html"), // 指定js引入的位置 放置在body的尾部 inject: "body", // 去除缓存 hash: true, // 压缩处理 minify: { // 去除注释 removeComments: true, // 去除双引号 removeAttributeQuotes: true, // 折叠,去除空格换行 collapseWhitespace: true } }) ], mode: "production" } -
config->webpack.dev.js(开发配置)
const prodConfig = require("./webpack.prod"); // 用于配置开发服务的 module.exports = { // 将prodConfig对象进行合并 ...prodConfig, // 配置开发服务 devServer: { // 指定主机域名 hosts 127.0.0.1 zhangpeiyue.com host: "zhangpeiyue.com", // 端口号 port: 80, // 直接打开浏览器 open: true }, mode: "development" }
-
-
在
package.json中增加脚本命令{ "scripts": { "start": "npx webpack serve --config ./config/webpack.dev.js", "build": "npx webpack --config ./config/webpack.prod.js " } } -
启动项目:
npm start -
打包项目:
npm run build
5- 设置网站的标题以及站标
-
更改标题,增加站标
<title>尚硅谷广告管理系统</title> <link rel="icon" href="/favicon.ico"> -
解决站标打包问题
-
下载插件
cnpm install copy-webpack-plugin -D -
使用
// 提供打包的配置信息 const path = require("path"); // 用于提供模板页面的模块。 const HtmlWebpackPlugin = require("html-webpack-plugin"); // 1- 引入复制插件 const CopyWebpackPlugin = require("copy-webpack-plugin"); // outDir 用于指定输出的目录 const outDir = path.resolve(__dirname, "../build"); // 导出配置信息 module.exports = { // 入口 entry: path.resolve(__dirname, "../src/index.js"), // 出口 output: { // 指定打包以后生成的文件名 filename: "js/app.js", // 指定打包文件所在的目录,必须为绝对地址 path: outDir, // 将上一次打包的内容进行清理 clean: true, // 指定文件引入的目录为站点根目录 publicPath: "/" }, // 插件,值的类型是数组,数组中的每一个元素即是一个插件的配置 plugins: [ new HtmlWebpackPlugin({ // 指定模板(入口页面)地址 template: path.resolve(__dirname, "../public/index.html"), // 指定js引入的位置 放置在body的尾部 inject: "body", // 去除缓存 hash: true, // 压缩处理 minify: { // 去除注释 removeComments: true, // 去除双引号 removeAttributeQuotes: true, // 折叠,去除空格换行 collapseWhitespace: true } }), // 2- 配置复制插件 new CopyWebpackPlugin({ patterns: [ { // 指定复制的目录文件 from: path.resolve(__dirname, "../public"), // 指定复制到哪 to: outDir, // 配置忽略 globOptions: { // **/ 代表的是from指定的目录,会将该目录中的index.html进行忽略 ignore: ["**/index.html"] } } ] }) ], mode: "production" } -
优化webpack.dev.js
const path = require("path");// 内置模块 const HtmlWebpackPlugin = require("html-webpack-plugin");// 第三方模块 const prodConfig = require("./webpack.prod");// 自定义模块 // 用于配置开发服务的 module.exports = { // 将prodConfig对象进行合并 ...prodConfig, // 配置开发服务 devServer: { // 指定主机域名 hosts 127.0.0.1 zhangpeiyue.com host: "zhangpeiyue.com", // 端口号 port: 80, // 直接打开浏览器 open: true }, // 1-由于开发环境中不需要对HTML文件进行压缩处理,所以需要对htmlWebpackPlugin进行再次配置。 plugins: [ new HtmlWebpackPlugin({ // 指定模板(入口页面)地址 template: path.resolve(__dirname, "../public/index.html"), // 指定js引入的位置 放置在body的尾部 inject: "body", // 去除缓存 hash: true }), ], mode: "development" }
-
6- 配置路由
-
路由:根据地址,决定使用哪一部分内容进行渲染。
-
下载:sme-router是一个模拟express的路由,通过它可以快速搭建一个前端展示路由。
cnpm install sme-router -
如何配置路由
-
src->public->index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>尚硅谷广告管理系统</title> <link rel="icon" href="/favicon.ico"> </head> <body> <!-- id为root的div元素会作为路由容器 --> <div id="root"></div> </body> </html> -
src->index.js
// 引入sme-router模块 import SMERouter from "sme-router"; // 对SMERouter进行实例化,需要指定一个参数(某元素的id) // 会将拥有该id的元素作为一个展示路由的容器。 // 默认为hash模块(带#),如果将第二个参数设置为html5,那么即为history模式 const router = new SMERouter("root", "html5"); // http://zhangpeiyue.com/login // 登陆路由,第一个参数为请求的地址,第二个参数是一个函数,用于指定渲染的内容 router.route("/login", (req, res) => { console.log("login"); res.render("登陆界面"); }) router.route("/index", (req, res) => { res.render("首页界面"); }) // sme-router:如果有两个地址相同,两个地址对应的函数均会执行,后者会将前者渲染的内容覆盖 router.route("/login", (req, res) => { console.log("login2"); res.render("登陆界面2") }) // 重定向:*代表所有地址均匹配 router.route("*", (req, res) => { res.redirect("/login");// 重定向到/login }) -
如果路由模式采用的是history,需要对webpack.dev.js进行调整
devServer: { // 指定主机域名 hosts 127.0.0.1 zhangpeiyue.com host: "zhangpeiyue.com", // 端口号 port: 80, // 直接打开浏览器 open: true, // 1-当找不到index.html时,会将index.html作为响应的界面 historyApiFallback: true },
7- webpack支持ejs
-
可以将每一个路由渲染的内容指定为一个ejs文件。
-
可以将ejs文件放置到目录views
-
需要配置webpack支持ejs类型文件
-
下载
cnpm install ejs-loader -D -
配置。打开src->config->webpack.prod.js
module: { rules: [ { test: /.ejs$/, loader: "ejs-loader", options: { variable: "data"// 可以用data接收参数 } } ] },
-
-
在views当中创建两个文件:index.ejs login.ejs
-
index.ejs
<div> <h3>首页</h3> <p>数组:<%=data.arr.join("") %> <p>数组:<%=data.arr.map(item=>"<span>"+(item+100)+"</span>").join("") %> </p> <p> <%=data.a%> </p> <p> <%=data.b%> </p> <p> <%=data.c%> </p> <p> <%=data.d%> </p> <hr /> <% for(let i=0;i<data.arr.length;i++){ %> <span> <%=data.arr[i] %> </span> <% } %> </div> -
login.ejs
<div> <h3>登陆界面<%=data %> </h3> <form action=""> <input type="text"> <input type="text"> <button>提交</button> </form> </div>
-
-
路由中引入模板,然后将模板的内容进行渲染
// 引入sme-router模块 import SMERouter from "sme-router"; // 1- 引入login import login from "./views/login.ejs"; // 2- 引入index import index from "./views/index.ejs"; const router = new SMERouter("root", "html5"); // console.log(login);// 通过impmort from 引入的ejs,得到的是一个函数,该函数接收的参数为data,返回的结果即是ejs的内容。 // console.log(login()); router.route("/login", (req, res) => { // 3会将login.ejs模板的内容进行渲染,并且传递了一个数字1 res.render(login(1)); }); router.route("/index", (req, res) => { // 4要将index.ejs模板的内容进行渲染,并且向ejs传递了一个对象 res.render(index({ a: 1, b: 2, c: 3, d: 4, arr: [1, 2, 3, 4] })); }); router.route("*", (req, res) => { res.redirect("/login");// 重定向到/login });
8- 省略扩展名以及路径 别名
-
src->config->webpack.prod.js
resolve: { // webpack默认支持.js .json文件的省略,如果要进行配置其它的扩展名省略,需要将.js,.json // 因为你的配置会将默认配置进行覆盖 // 以下配置支持.js,.json,.ejs的扩展名省略。 // 顺序:.js,.json,.ejs extensions: [".js", ".json", ".ejs"], // 通过alias指定别名 alias: { // 属性名即是别名,属性值即是别名指向的地址 // 地址别名一般以@开头。 "@v": path.resolve(__dirname, "../src/views") } },
9- 二级路由(子路由)
// 引入sme-router模块
import SMERouter from "sme-router";
import login from "@v/login";
import index from "@v/index";
import my from "./module/my"
const router = new SMERouter("root", "html5");
// 一级路由
router.route("/login", (req, res) => {
res.render(login(1));
});
// 一级路由
router.route("/index", (req, res, next) => {
// next(`
// <h3>首页</h3>
// ${res.subRoute()}
// `)
// ejs
next(index({
subRoute: res.subRoute()
}));
});
// 需求:当输入/index/adminList 标题为管理员列表
// 需求:当输入/index/advList 标题为广告列表
router.route("/index/adminList", (req, res) => {
res.render("管理员列表")
})
router.route("/index/advList", (req, res) => {
res.render("广告列表")
})
router.route("*", (req, res) => {
res.redirect("/login");// 重定向到/login
});
10- 管理员界面设计
-
adminLTE官网:adminlte.io
-
AdminLTE是一款建立在Bootstrap3和JQuery1.11+之上的开源的模板主题工具,它提供了一系列响应的、可重复使用的组件, 并内置了多个模板页面,同时自适应多种屏幕分辨率,兼容PC和移动端。通过AdminLTE,我们可以快速的创建一个响应式的Html5网站。
AdminLTE框架在网页架构与设计上,有很大的辅助作用, 尤其是前端架构设计师,用好AdminLTE 不但美观,而且可以免去写很大CSS与JS的工作量。
-
public当中引入样式,图片,JS
-
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>尚硅谷广告管理系统</title> <!-- Font Awesome Icons --> <link rel="stylesheet" href="/css/all.min.css"> <!-- Theme style --> <link rel="stylesheet" href="/css/adminlte.min.css"> <link rel="icon" href="/favicon.ico"> </head> <body> <!-- id为root的div元素会作为路由容器 --> <div id="root"></div> <!-- jQuery --> <script src="/js/jquery.min.js"></script> <!-- Bootstrap 4 --> <script src="/js/bootstrap.bundle.min.js"></script> <!-- AdminLTE App --> <script src="/js/adminlte.min.js"></script> </body> </html> -
src->views->index.ejs
<div class="wrapper"> <!-- Navbar --> <nav class="main-header navbar navbar-expand navbar-white navbar-light"> <!-- Left navbar links --> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a> </li> </ul> <!-- Right navbar links --> <ul class="navbar-nav ml-auto"> <li class="nav-item"> <button type="button" class="btn btn-block btn-danger">退出登陆</button> </li> </ul> </nav> <!-- /.navbar --> <!-- Main Sidebar Container --> <aside class="main-sidebar sidebar-dark-primary elevation-4"> <!-- Brand Logo --> <a href="index3.html" class="brand-link"> <img src="/img/logo.png" alt="AdminLTE Logo" class="brand-image img-circle elevation-3" style="opacity: .8"> <span class="brand-text font-weight-light">广告管理系统</span> </a> <!-- Sidebar --> <div class="sidebar"> <!-- Sidebar user panel (optional) --> <div class="user-panel mt-3 pb-3 mb-3 d-flex"> <div class="info"> <a href="#" class="d-block">欢迎您:admin</a> </div> </div> <!-- Sidebar Menu --> <nav class="mt-2"> <ul class="nav nav-pills nav-sidebar flex-column"> <!-- Add icons to the links using the .nav-icon class with font-awesome or any other icon font library --> <li class="nav-item"> <a href="#" class="nav-link active"> <i class="nav-icon fas fa-tachometer-alt"></i> <p>管理员列表</p> </a> </li> <li class="nav-item"> <a href="#" class="nav-link"> <i class="nav-icon fas fa-tachometer-alt"></i> <p>广告管理列表</p> </a> </li> </ul> </nav> <!-- /.sidebar-menu --> </div> <!-- /.sidebar --> </aside> <!-- Content Wrapper. Contains page content --> <div class="content-wrapper"> <!-- Content Header (Page header) --> <div class="content-header"> <div class="container-fluid"> <div class="row mb-2"> <div class="col-sm-6"> <h1 class="m-0">Starter Page</h1> </div><!-- /.col --> </div><!-- /.row --> </div><!-- /.container-fluid --> </div> <!-- /.content-header --> <!-- Main content --> <div class="content"> <div class="container-fluid"> </div><!-- /.container-fluid --> </div> <!-- /.content --> </div> <!-- /.content-wrapper --> <!-- Main Footer --> <footer class="main-footer"> <!-- To the right --> <div class="float-right d-none d-sm-inline"> Anything you want </div> <!-- Default to the left --> <strong>Copyright © 2022-2022 <a href="https://zhangpeiyue.com">zhangpeiyue.com</a>.</strong> All rights reserved. </footer> </div>
11- 将子路由内容指定位置进行呈现
- src->views->index.ejs
<div class="content">
<div class="container-fluid">
<%=data.subRoute %>
</div><!-- /.container-fluid -->
</div>
12- 通过点击进行路由的切换
- src->views->index.ejs
- 以下程序可以实现路由切换,但页面当中的所有的资源会重新加载。
<ul class="nav nav-pills nav-sidebar flex-column">
<!-- Add icons to the links using the .nav-icon class
with font-awesome or any other icon font library -->
<li class="nav-item">
<a href="/index/adminList" class="nav-link active">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>管理员列表</p>
</a>
</li>
<li class="nav-item">
<a href="/index/advList" class="nav-link">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>广告管理列表</p>
</a>
</li>
</ul>
13-利用router.go解决刷新加载的问题
-
创建一个配置文件:src->config->variable.js
import SMERouter from "sme-router"; // 公共常量:全部大写,多个单词之间使用_分割。 window.ROUTER = new SMERouter("root", "html5"); -
src->index.js
// 引入sme-router模块 import login from "@v/login"; import index from "@v/index"; // 引入配置文件 import "./config/variable"; // 一级路由 ROUTER.route("/login", (req, res) => { res.render(login(1)); }); // 一级路由 ROUTER.route("/index", (req, res, next) => { // ejs next(index({ subRoute: res.subRoute() })); }); ROUTER.route("/index/adminList", (req, res) => { res.render("管理员列表") }) ROUTER.route("/index/advList", (req, res) => { res.render("广告列表") }) ROUTER.route("*", (req, res) => { res.redirect("/index/adminList");// 重定向到/login }); -
src->views->index.ejs
<nav class="mt-2"> <ul class="nav nav-pills nav-sidebar flex-column"> <!-- Add icons to the links using the .nav-icon class with font-awesome or any other icon font library --> <li class="nav-item"> <a href="javascript:;" onclick="ROUTER.go('/index/adminList')" class="nav-link active"> <i class="nav-icon fas fa-tachometer-alt"></i> <p>管理员列表</p> </a> </li> <li class="nav-item"> <a href="javascript:;" onclick="ROUTER.go('/index/advList')" class="nav-link"> <i class="nav-icon fas fa-tachometer-alt"></i> <p>广告管理列表</p> </a> </li> </ul> </nav>
14- 实现高亮效果
-
views->index.ejs
<ul class="nav nav-pills nav-sidebar flex-column"> <!-- Add icons to the links using the .nav-icon class with font-awesome or any other icon font library --> <li class="nav-item"> <a href="javascript:;" onclick="ROUTER.go('/index/adminList')" class="nav-link <%=(data.url.toLowerCase()==='/index/adminList'.toLowerCase()?'active':'') %> "> <i class="nav-icon fas fa-tachometer-alt"></i> <p>管理员列表</p> </a> </li> <li class="nav-item"> <a href="javascript:;" onclick="ROUTER.go('/index/advList')" class="nav-link <%=(data.url.toLowerCase()==='/index/advList'.toLowerCase()?'active':'') %> "> <i class="nav-icon fas fa-tachometer-alt"></i> <p>广告管理列表</p> </a> </li> </ul> -
src->index.js
ROUTER.route("/index", ({ url }, res, next) => { // 路由地址是不区分大小写的。 // 比如请求的地址为:http://zhangpeiyue.com/index/adminlist // 那么req.url :/index/adminlist // console.log("index", req.url); // ejs next(index({ subRoute: res.subRoute(), url })); });
15- 将地址进行统一管理
-
src->config->variable.js
import SMERouter from "sme-router"; // 公共常量:全部大写,多个单词之间使用_分割。 window.ROUTER = new SMERouter("root", "html5"); // 主页 window.URL_INDEX = "/index"; // 管理员列表 window.URL_INDEX_ADMINLIST = "/index/adminList"; // 广告管理列表 window.URL_INDEX_ADVLIST = "/index/advList"; // 登陆 window.URL_LOGIN = "/login"; -
src->index.js
// 引入sme-router模块 import login from "@v/login"; import index from "@v/index"; // 引入配置文件 import "./config/variable"; // 一级路由 ROUTER.route(URL_LOGIN, (req, res) => { res.render(login(1)); }); // 一级路由 ROUTER.route(URL_INDEX, ({ url }, res, next) => { // 路由地址是不区分大小写的。 // 比如请求的地址为:http://zhangpeiyue.com/index/adminlist // 那么req.url :/index/adminlist // console.log("index", req.url); // ejs next(index({ subRoute: res.subRoute(), url })); }); ROUTER.route(URL_INDEX_ADMINLIST, (req, res) => { res.render("管理员列表") }) ROUTER.route(URL_INDEX_ADVLIST, (req, res) => { res.render("广告列表") }) ROUTER.route("*", (req, res) => { res.redirect("/index/adminList");// 重定向到/login }); -
src->views->index.ejs
<ul class="nav nav-pills nav-sidebar flex-column"> <!-- Add icons to the links using the .nav-icon class with font-awesome or any other icon font library --> <li class="nav-item"> <a href="javascript:;" onclick="ROUTER.go(URL_INDEX_ADMINLIST)" class="nav-link <%=(data.url.toLowerCase()===URL_INDEX_ADMINLIST.toLowerCase()?'active':'') %> "> <i class="nav-icon fas fa-tachometer-alt"></i> <p>管理员列表</p> </a> </li> <li class="nav-item"> <a href="javascript:;" onclick="ROUTER.go(URL_INDEX_ADVLIST)" class="nav-link <%=(data.url.toLowerCase()===URL_INDEX_ADVLIST.toLowerCase()?'active':'') %> "> <i class="nav-icon fas fa-tachometer-alt"></i> <p>广告管理列表</p> </a> </li> </ul>
16- 标题控制
-
src->views->index.ejs:增加上ID
<div class="col-sm-6"> <h1 class="m-0" id="pageTitle">页面标题</h1> </div> -
获取标题元素,更改内容
ROUTER.route(URL_INDEX_ADMINLIST, (req, res) => {
res.render("管理员列表");
// 1
document.querySelector("#pageTitle").innerHTML = "管理员列表"
})
ROUTER.route(URL_INDEX_ADVLIST, (req, res) => {
res.render("广告列表");
// 2
document.querySelector("#pageTitle").innerHTML = "广告列表"
})
ROUTER.route("*", (req, res) => {
res.redirect("/index/adminList");// 重定向到/login
});
17- 将路由逻辑进行抽离,抽离至控制器目录(controllers)
-
src->config->webpack.prod.js
resolve: { // webpack默认支持.js .json文件的省略,如果要进行配置其它的扩展名省略,需要将.js,.json // 因为你的配置会将默认配置进行覆盖 // 以下配置支持.js,.json,.ejs的扩展名省略。 // 顺序:.js,.json,.ejs extensions: [".js", ".json", ".ejs"], // 通过alias指定别名 alias: { // 属性名即是别名,属性值即是别名指向的地址 // 地址别名一般以@开头。 "@v": path.resolve(__dirname, "../src/views"), // 1增加 "@c": path.resolve(__dirname, "../src/controllers"), } }, -
src->controllers->index.js
import index from "@v/index"; export default () => { return ({ url }, res, next) => { next(index({ subRoute: res.subRoute(), url })) } } -
src->index.js
// 引入sme-router模块 import login from "@v/login"; // 引入indexController import indexController from "@c/index"; // 引入配置文件 import "./config/variable"; // 一级路由 ROUTER.route(URL_LOGIN, (req, res) => { res.render(login(1)); }); // 一级路由 // const indexController = ({ url }, res, next) => { // next(index({ // subRoute: res.subRoute(), // url // })); // } // const indexController = () => { // return ({ url }, res, next) => { // next(index({ // subRoute: res.subRoute(), // url // })) // } // } // 主页 ROUTER.route(URL_INDEX, indexController());// 函数柯里化 ROUTER.route(URL_INDEX_ADMINLIST, (req, res) => { res.render("管理员列表"); document.querySelector("#pageTitle").innerHTML = "管理员列表" }) ROUTER.route(URL_INDEX_ADVLIST, (req, res) => { res.render("广告列表"); document.querySelector("#pageTitle").innerHTML = "广告列表" }) ROUTER.route("*", (req, res) => { res.redirect("/index/adminList");// 重定向到/login });
18- 完成路由封装
-
src->config->webpack.dev.js
devtool: 'eval-cheap-module-source-map', -
src->components->SiderMenu
<nav class="mt-2"> <ul class="nav nav-pills nav-sidebar flex-column"> <% for(let i=0;i<data.routeArr.length;i++){ %> <li class="nav-item"> <a href="javascript:;" onclick="ROUTER.go('<%=data.routeArr[i].path%>')" class="nav-link <%=(data.url.toLowerCase()===data.routeArr[i].path.toLowerCase()?'active':'') %> "> <i class="nav-icon fas fa-tachometer-alt"></i> <p> <%=data.routeArr[i].title %> </p> </a> </li> <% } %> </ul> </nav> -
src->controllers->index.js
import index from "@v/index"; // 将导航组件引入至index.js中。组件:页面的一部分。 import sliderMenu from "../components/SiderMenu" // 路由配置文件 const routeArr = [ { title: "管理员列表",// 标题 path: URL_INDEX_ADMINLIST// 链接 }, { title: "广告列表", path: URL_INDEX_ADVLIST } ]; export default () => { let pageTitle = "";// 网页标题 return ({ url }, res, next) => { // 根据地址找到相关配置信息 const info = routeArr.find(v => v.path === url); // 如果找到了,那么将info.title作为页面标题 if (info) pageTitle = info.title; next(index({ subRoute: res.subRoute(), sliderMenu: sliderMenu({ url, routeArr }), pageTitle })) } } -
src->views->index.ejs
<div class="wrapper"> <!-- Navbar --> <nav class="main-header navbar navbar-expand navbar-white navbar-light"> <!-- Left navbar links --> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a> </li> </ul> <!-- Right navbar links --> <ul class="navbar-nav ml-auto"> <li class="nav-item"> <button type="button" class="btn btn-block btn-danger">退出登陆</button> </li> </ul> </nav> <!-- /.navbar --> <!-- Main Sidebar Container --> <aside class="main-sidebar sidebar-dark-primary elevation-4"> <!-- Brand Logo --> <a href="index3.html" class="brand-link"> <img src="/img/logo.png" alt="AdminLTE Logo" class="brand-image img-circle elevation-3" style="opacity: .8"> <span class="brand-text font-weight-light">广告管理系统</span> </a> <!-- Sidebar --> <div class="sidebar"> <!-- Sidebar user panel (optional) --> <div class="user-panel mt-3 pb-3 mb-3 d-flex"> <div class="info"> <a href="#" class="d-block">欢迎您:admin</a> </div> </div> <!-- Sidebar Menu --> <%=data.sliderMenu %> <!-- /.sidebar-menu --> </div> <!-- /.sidebar --> </aside> <!-- Content Wrapper. Contains page content --> <div class="content-wrapper"> <!-- Content Header (Page header) --> <div class="content-header"> <div class="container-fluid"> <div class="row mb-2"> <div class="col-sm-6"> <h1 class="m-0" id="pageTitle"> <%=data.pageTitle %> </h1> </div><!-- /.col --> </div><!-- /.row --> </div><!-- /.container-fluid --> </div> <!-- /.content-header --> <!-- Main content --> <div class="content"> <div class="container-fluid"> <%=data.subRoute %> </div><!-- /.container-fluid --> </div> <!-- /.content --> </div> <!-- /.content-wrapper --> <!-- Main Footer --> <footer class="main-footer"> <!-- To the right --> <div class="float-right d-none d-sm-inline"> Anything you want </div> <!-- Default to the left --> <strong>Copyright © 2022-2022 <a href="https://zhangpeiyue.com">zhangpeiyue.com</a>.</strong> All rights reserved. </footer> </div>
19- 管理员列表界面设计
-
src->views->adminList.ejs
<div class="card"> <div class="card-header"> <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#modal-default">添加管理员</button> </div> <!-- /.card-header --> <div class="card-body"> <table class="table table-bordered"> <thead> <tr> <th style="width: 10px">#</th> <th>Task</th> <th>Progress</th> <th style="width: 40px">Label</th> </tr> </thead> <tbody> <tr> <td>1.</td> <td>Update software</td> <td> <div class="progress progress-xs"> <div class="progress-bar progress-bar-danger" style="width: 55%"></div> </div> </td> <td><span class="badge bg-danger">55%</span></td> </tr> </tbody> </table> </div> </div> <div class="modal fade show" id="modal-default" style="padding-right: 17px;" aria-modal="true" role="dialog"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title">添加管理员</h4> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <form name="adminForm"> <div class="form-group"> <label for="adminName">管理员账号</label> <input type="email" name="adminName" class="form-control" id="adminName" placeholder="请输入管理员账号"> </div> <div class="form-group"> <label for="passWord">管理员密码</label> <input type="password" name="passWord" class="form-control" id="passWord" placeholder="请输入管理员密码"> </div> <div class="form-group"> <label for="rePassWord">管理员重复密码</label> <input type="password" name="repassWord" class="form-control" id="rePassWord" placeholder="请输入管理员重复密码"> </div> </form> </div> <div class="modal-footer justify-content-between"> <button type="button" class="btn btn-default" data-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" id="submitBtn">提交</button> </div> </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> -
src->controller->adminList.js
import adminList from "@v/adminList"; // 提交时的事件处理函数 const submitHandler = () => { console.log("事件处理函数", document.adminForm.adminName.value.trim()); console.log("事件处理函数", document.adminForm.passWord.value.trim()); console.log("事件处理函数", document.adminForm.rePassWord.value.trim()); } export default () => { return (req, res) => { res.render(adminList()); // 获取提交按钮元素,并指定点击事件 document.querySelector("#submitBtn").onclick = submitHandler; } }
20- 创建https服务
-
http服务的默认端口号为80,https默认端口号为443
-
https需要域名证书而http不需要证书。
-
首先要设置host:
127.0.0.1 api.zhangpeiyue.com -
搭建一个后端项目目录 backend
-
下载依赖模块
cnpm install express mongoose md5 -
backend当中创建服务文件:server.js
// 1- 引入https const https = require("https"); const fs = require("fs"); const path = require("path"); const express = require("express"); const utils = require("./utils"); const app = express(); // 指定响应体的数据格式,将响应体的数据放置在req.body中 app.use(express.json()); // 2- 通过https创建服务,第一个参数为证书信息,第二个参数app const httpsServer = https.createServer({ // 密钥 key: fs.readFileSync(path.resolve(__dirname, "./crt/api.zhangpeiyue.com.key")), // 证书 cert: fs.readFileSync(path.resolve(__dirname, "./crt/api.zhangpeiyue.com.crt")) }, app) // 指定https服务的端口号以及回调函数 httpsServer.listen(443, () => { console.log("https->success"); }) // 指定http服务的端口号以及回调函数 app.listen(8081, () => { console.log("http->success"); })
21- 两个概念
混合开发:前端与后端属于同一个项目。
当用户输入网址,按下回车,会请求服务,服务返回的内容即是渲染的内容。
前后端分离:
前端是一个项目,后端是一个项目。
前端负责调用接口,并对数据进行展现
后端负责提供接口,用于操作数据库。
22- 封装中间件
-
自定义一个函数
// 不允许省略的形参,放置最左侧 // 可以省略,但是又经常使用的次之 function sendJson(res, msg = "网络连接错误", ok = -1) { res.json({ ok, msg }) } app.get("/test", (req, res) => { sendJson(res, "账号已存在"); }) -
可以使用中间件
// 中间件是一个函数 app.use(function (req, res, next) { res.sendJson = function (msg = "网络连接错误", ok = -1) { res.json({ ok, msg }) } next(); }) app.get("/test", (req, res) => { res.sendJson("成功"); }) -
可以将中间件函数放置到外部
-
backend->module->middleware->index.js
module.exports = { sendJson() { return function (req, res, next) { res.sendJson = function (msg = "网络连接错误", ok = -1) { res.json({ ok, msg }); } next(); } } } -
backend->server.js 可以使用中间件
const middleware = require("./module/middleware"); app.use(middleware.sendJson()) app.get("/test", (req, res) => { res.sendJson("成功111"); })
-
23- 添加管理员接口
app.use(middleware.sendJson());
// 添加管理员接口
app.post("/adminList", async (req, res) => {
// console.log(req.body);// {adminName,passWord,rePassWord}
/*思路:
1- 接收数据
2- 验证数据是否合法
2-1:你的密码与重复密码是否一致
2-1:不一致,响应失败结果
2-2:一致,验证管理员账号是否已被占用
2-2-1:已占用,响应失败结果:您 的账号已经被占用
2-2-2:未占用,将接收的信息放置到数据库中
2-2-2-1:成功,响应成功信息
2-2-2-2:失败,响应失败信息
*/
try {
// 1 - 接收数据
const { adminName, passWord, rePassWord } = req.body;
if (passWord !== rePassWord) res.sendJson("两次输入的密码不一致")
else {
const count = await adminModel.count({ adminName });
if (count > 0) res.sendJson("您的账号已经被占用");
else {
await adminModel.insertOne({
adminName,
passWord: utils.getMd5(passWord),
regTime: moment().format("YYYY-MM-DD HH:mm:ss"),
lastLoginTime: moment().format("YYYY-MM-DD HH:mm:ss")
})
res.sendJson("添加管理员成功", 1);
}
}
} catch (msg) {
res.sendJson(msg);
}
})
24- 增加弹出提示框
- public->index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>尚硅谷广告管理系统</title>
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="/css/all.min.css">
<!-- Theme style -->
<link rel="stylesheet" href="/css/adminlte.min.css">
<!--1引入样式-->
<link rel="stylesheet" href="/css/toastr.min.css">
<link rel="icon" href="/favicon.ico">
</head>
<body>
<!-- id为root的div元素会作为路由容器 -->
<div id="root"></div>
<!-- jQuery -->
<script src="/js/jquery.min.js"></script>
<!-- Bootstrap 4 -->
<script src="/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="/js/adminlte.min.js"></script>
<!--2引入JS-->
<script src="/js/toastr.min.js"></script>
</body>
</html>
- 使用方法 src->constrollers->adminList.js
// 提交时的事件处理函数
const submitHandler = () => {
// console.log("事件处理函数", document.adminForm.adminName.value.trim());
// console.log("事件处理函数", document.adminForm.passWord.value.trim());
// console.log("事件处理函数", document.adminForm.rePassWord.value.trim());
let { adminName, passWord, rePassWord } = document.adminForm;
adminName = adminName.value.trim();
passWord = passWord.value.trim();
rePassWord = rePassWord.value.trim();
if (adminName.length < 1) {
toastr.error('请输入管理员账号.');
return;
}
if (passWord.length < 1) {
toastr.error('请输入管理员密码.');
return;
}
if (rePassWord.length < 1) {
toastr.error('请输入管理员重复密码.');
return;
}
if (passWord !== rePassWord) {
toastr.error('重复密码与密码的内容不一致');
return;
}
console.log("提交");
}
25- 使用axios发送请求,并解决跨域问题(重点)
-
下载
cnpm i axios -
使用代理:config->webpack.dev.js
devServer: { // 指定主机域名 hosts 127.0.0.1 zhangpeiyue.com host: "zhangpeiyue.com", // 端口号 port: 80, // 直接打开浏览器 open: true, // 当找不到index.html时,会将index.html作为响应的界面 historyApiFallback: true, // 增加内容: // 通过设置proxy属性增加代理服务,值的类型是一个对象,对象的每一个属性即是一个代理 // 属性名字即是代理的标识 proxy: { // 请求的地址如果以/api开头(使用了/api这个标识),该代理即会生效 // https://api.zhangpeiyue.com/adminList "/api": { // 代理的服务器目标 target: "https://api.zhangpeiyue.com", secure: false,// 支持https pathRewrite: { // 将/api的标识用空字符替换掉。 "^/api": "" } } } }, -
添加管理员中使用axios
// 提交时的事件处理函数 const submitHandler = async () => { // console.log("事件处理函数", document.adminForm.adminName.value.trim()); // console.log("事件处理函数", document.adminForm.passWord.value.trim()); // console.log("事件处理函数", document.adminForm.rePassWord.value.trim()); let { adminName, passWord, rePassWord } = document.adminForm; adminName = adminName.value.trim(); passWord = passWord.value.trim(); rePassWord = rePassWord.value.trim(); if (adminName.length < 1) { toastr.error('请输入管理员账号.'); return; } if (passWord.length < 1) { toastr.error('请输入管理员密码.'); return; } if (rePassWord.length < 1) { toastr.error('请输入管理员重复密码.'); return; } if (passWord !== rePassWord) { toastr.error('重复密码与密码的内容不一致'); return; } // 只有AJAX才会有跨域的问题。 // 如果是AJAX请求要求符合同源策略,如果不符合会造成跨域。 // 同源策略:同协议,同域名,同端口号,三者全部相同说明符合,有一个不相同说明不符合同源策略。 // 页面所在的服务地址:http://zhangpeiyue.com/index/adminList // 页面请求的服务地址:https://api.zhangpeiyue.com/adminList // 使用代理前要增加一个标识 /api const { data: { ok, msg } } = await axios.post("/api/adminList", { adminName, passWord, rePassWord }) if (ok === 1) { toastr.success(msg); // 清除表单的内容。 document.adminForm.reset(); // 触发取消按钮的单击行为,注意:要为你的取消按钮增加id==>cancelBtn document.querySelector("#cancelBtn").click(); } else { toastr.error(msg); } // const {ok,msg} = result.data; // console.log(result.data); } -
由于配置信息发生变化,所以要将服务重启
npm start
26- 封装axios
-
src->request->index.js
import axios from "axios"; // 创建axios的实例 const zhang = axios.create({ timeout: 5000, baseURL: "/api"// 自动增加标识/api }) // 请求拦截 zhang.interceptors.request.use(config => config); // 响应拦截 zhang.interceptors.response.use(res => { const { ok, msg } = res.data; if (ok === -1) { // 说明接口有异常 toastr.error(msg); return Promise.reject(msg); } else { return res.data;// 响应体数据 } }); export default zhang; -
src->controllers->adminList.js
import adminList from "@v/adminList"; // 1- 引入request import request from "../request"; // 提交时的事件处理函数 const submitHandler = async () => { let { adminName, passWord, rePassWord } = document.adminForm; adminName = adminName.value.trim(); passWord = passWord.value.trim(); rePassWord = rePassWord.value.trim(); if (adminName.length < 1) { toastr.error('请输入管理员账号.'); return; } if (passWord.length < 1) { toastr.error('请输入管理员密码.'); return; } if (rePassWord.length < 1) { toastr.error('请输入管理员重复密码.'); return; } if (passWord !== rePassWord) { toastr.error('重复密码与密码的内容不一致'); return; } // 2-如果成功可以得到响应体数据,将axios改为request const { msg } = await request.post("/adminList", { adminName, passWord, rePassWord }) toastr.success(msg); // 清除表单的内容。 document.adminForm.reset(); // 触发取消按钮的单击行为 document.querySelector("#cancelBtn").click(); } export default () => { return (req, res) => { res.render(adminList()); // 获取提交按钮元素,并指定点击事件 document.querySelector("#submitBtn").onclick = submitHandler; } }
27- 封装api
-
src->api->adminList.js (可以将与管理员相关的接口放置在该文件中)
// 所有与管理员相关的接口都在此处定义 import request from "../request"; // 添加管理员 export const addAdminList = function (body) { return request.post("/adminList", body); } -
src->controller->adminList.js
// 提交时的事件处理函数 const submitHandler = async () => { let { adminName, passWord, rePassWord } = document.adminForm; adminName = adminName.value.trim(); passWord = passWord.value.trim(); rePassWord = rePassWord.value.trim(); if (adminName.length < 1) { toastr.error('请输入管理员账号.'); return; } if (passWord.length < 1) { toastr.error('请输入管理员密码.'); return; } if (rePassWord.length < 1) { toastr.error('请输入管理员重复密码.'); return; } if (passWord !== rePassWord) { toastr.error('重复密码与密码的内容不一致'); return; } // 调用添加管理员接口 const { msg } = await addAdminList({ adminName, passWord, rePassWord }) toastr.success(msg); // 清除表单的内容。 document.adminForm.reset(); // 触发取消按钮的单击行为 document.querySelector("#cancelBtn").click(); }
28- 完成获取管理员接口
-
backend->module->DB.JS
// 获取数据集合列表内容 async find(options = {}) { await this._connect(); // whereObj const { whereObj = {}, sortObj = {}, limit = 0, skip = 0 } = options; return this.model.find(whereObj).sort(sortObj).limit(limit).skip(skip); } -
backend->server.js
app.get("/adminList", async (req, res) => { // 目标:将数据的内容进行响应 // 思路:连接数据库,获得对应数据,进行响应。 try { const adminList = await adminModel.find({ sortObj: { regTime: -1// 按照时间的倒序排列 } }); res.json({ ok: 1, msg: "成功", adminList }) } catch (err) { res.sendJson(err); } });
29- 支持LESS
-
在src->assets->less->common.less
.table tr, .table tr td { text-align: center; vertical-align: middle; } -
在src->index.js
import "./assets/less/common.less" -
下载
cnpm install less less-loader css-loader style-loader -D -
在config->webpack.prod.js
{ test: /.less$/, // cnpm install less less-loader css-loader style-loader -D use: [ // compiles Less to CSS "style-loader", "css-loader", "less-loader", ], }
30- 登陆界面的设计
-
src->views->login.ejs
<div class="login-box"> <!-- /.login-logo --> <div class="card"> <div class="card-body login-card-body"> <p class="login-box-msg">尚硅谷广告管理系统</p> <form name="loginForm" method="post"> <div class="input-group mb-3"> <input type="input" name="adminName" class="form-control" placeholder="请输入管理员账号"> <div class="input-group-append"> <div class="input-group-text"> <span class="fas fa-user"></span> </div> </div> </div> <div class="input-group mb-3"> <input type="password" name="passWord" class="form-control" placeholder="请输入管理员密码"> <div class="input-group-append"> <div class="input-group-text"> <span class="fas fa-lock"></span> </div> </div> </div> <div class="row"> <!-- /.col --> <div class="col-12"> <button type="submit" class="btn btn-primary btn-block">登陆</button> </div> <!-- /.col --> </div> </form> </div> <!-- /.login-card-body --> </div> </div> -
src->controllers->login.js
import login from "@v/login"; export default () => { return (req, res) => { res.render(login(1)); document.body.className = "login-page"; } } -
移除表单默认提交行为的两种方法
-
第一种方法,将按钮的类型设置为button
<div class="col-12"> <button type="button" class="btn btn-primary btn-block">登陆</button> </div> -
第二种方法,为表单增加onsubmit事件,在事件处理函数中移除默认行为
import login from "@v/login"; const loginHandler = function (e) { e.preventDefault(); // 管理员的账号 const adminName = this.adminName.value.trim(); // 管理员密码 const passWord = this.passWord.value.trim(); if (adminName.length < 1) { toastr.error("请输入管理员账号"); return; } if (passWord.length < 1) { toastr.error("请输入管理员密码"); return; } console.log(adminName, passWord, "提交"); } export default () => { return (req, res) => { res.render(login(1)); // 增加样式 document.body.className = "login-page"; document.loginForm.onsubmit = loginHandler; } }
-
31- 实现登陆的流程
-
在backend当中创建登陆的接口
// 实现方案一 app.post("/login", async (req, res) => { /*目标:实现登陆 思路: 1- 接收数据 2- 去数据库中判断账号和密码是否有符合的文档 2-1:没有找到符合条件的文档,响应登陆失败信息:您的账号或密码不正确 2-2:找到符合条件的文档,更新登陆的时间,然后响应:登陆成功 */ // 1 - 接收数据 const { adminName, passWord } = req.body; // 2- 去数据库中判断账号和密码是否有符合的文档 const count = await adminModel.count({ adminName, passWord: getMd5(passWord) }) if (count === 1) { await adminModel.updateOne({ adminName, passWord: getMd5(passWord) }, { lastLoginTime: moment().format("YYYY-MM-DD HH:mm:ss") }); res.sendJson("登陆成功", 1); } else { res.sendJson("您的账号或密码不正确") } }) // 方案二 app.post("/login", async (req, res) => { // 接收数据 try { const { adminName, passWord } = req.body; const info = await adminModel.findOneAndUpdate({ adminName, passWord: getMd5(passWord) }, { lastLoginTime: moment().format("YYYY-MM-DD HH:mm:ss") }) if (info) res.sendJson("登陆成功", 1) else res.sendJson("账号或密码错误") } catch (err) { res.sendJson(err); } }) -
前端当中调用接口
-
api->adminList.js中增加调用接口的方法
// 根据账号与密码进行登陆 export const postLogin = function(body){ return request.post("/login", body); } -
controllers->adminList.js当中调用api->adminList->登陆的方法
import login from "@v/login"; import { postLogin } from "../api/adminList"; const loginHandler = async function (e) { e.preventDefault(); // 管理员的账号 const adminName = this.adminName.value.trim(); // 管理员密码 const passWord = this.passWord.value.trim(); if (adminName.length < 1) { toastr.error("请输入管理员账号"); return; } if (passWord.length < 1) { toastr.error("请输入管理员密码"); return; } const { msg } = await postLogin({ adminName, passWord }); toastr.success(msg); } export default () => { return (req, res) => { res.render(login(1)); // 增加样式 document.body.className = "login-page"; document.loginForm.onsubmit = loginHandler; } } -
Db.js
// 1- 负责连接数据库 const mongoose = require("mongoose"); // 2- 可以对model的方法进行二次封装 class Db { constructor(model) { // 将model作为实例属性 this.model = model; } // 连接数据库 _connect() { // mongoose.connection.readState是连接数据库的状态:0 未连接,1已连接 // 如果连接成功,返回一个成功的promise if (mongoose.connection.readyState === 1) return Promise.resolve(); return mongoose.connect("mongodb://127.0.0.1:27017/atGuiguAdv", { serverSelectionTimeoutMS: 2000// 一旦连接的时间超过2秒,则认定失败 }) } // 添加一条数据,insertObj是要添加的数据 async insertOne(insertObj) { await this._connect(); return this.model(insertObj).save(); } // 查找数据的集合长度 async count(whereObj = {}) { await this._connect();// 连接数据库 return this.model.count(whereObj); } // 获取数据集合列表内容 async find(options = {}) { await this._connect(); // whereObj const { whereObj = {}, sortObj = {}, limit = 0, skip = 0 } = options; return this.model.find(whereObj).sort(sortObj).limit(limit).skip(skip); } // 根据_id删除数据 async deleteOneById(_id) { await this._connect(); return this.model.deleteOne({ _id }); } // 修改数据:whereObj:条件 upObj:修改的内容 async updateOne(whereObj, upObj) { await this._connect(); return this.model.updateOne(whereObj, upObj); } // 查找结果并修改 async findOneAndUpdate(whereObj, upObj) { await this._connect(); // 会根据条件进行修改,完成操作以后会将查找的内容进行返回,如果没有找到则返回null return this.model.findOneAndUpdate(whereObj, upObj); } } module.exports = Db;
-
32-使用 storage
-
登陆时login
import login from "@v/login"; import { postLogin } from "../api/adminList"; const loginHandler = async function (e) { e.preventDefault(); // 管理员的账号 const adminName = this.adminName.value.trim(); // 管理员密码 const passWord = this.passWord.value.trim(); if (adminName.length < 1) { toastr.error("请输入管理员账号"); return; } if (passWord.length < 1) { toastr.error("请输入管理员密码"); return; } const { msg } = await postLogin({ adminName, passWord }); toastr.success(msg); // 1- 如果登陆成功,增加一个名字为adminName的Storage localStorage.setItem("adminName", adminName); // 跳转至管理界面 ROUTER.go(URL_INDEX_ADMINLIST); } export default () => { return (req, res) => { res.render(login(1)); // 增加样式 document.body.className = "login-page"; document.loginForm.onsubmit = loginHandler; } } -
index
import index from "@v/index"; // 将导航组件引入至index.js中。组件:页面的一部分。 import sliderMenu from "../components/SiderMenu" // 路由配置文件 const routeArr = [ { title: "管理员列表",// 标题 path: URL_INDEX_ADMINLIST// 链接 }, { title: "广告列表", path: URL_INDEX_ADVLIST } ]; // 1- 退了登陆,注意:为退出登陆按钮增加上ID outLoginBtn const outLoginHandler = function () { localStorage.clear(); ROUTER.go(URL_LOGIN); } export default () => { let pageTitle = "";// 网页标题 return ({ url }, res, next) => { // 根据地址找到相关配置信息 const info = routeArr.find(v => v.path === url); // 如果找到了,那么将info.title作为页面标题 if (info) pageTitle = info.title; next(index({ subRoute: res.subRoute(), sliderMenu: sliderMenu({ url, routeArr }), pageTitle })) document.body.className = ""; document.querySelector("#outLoginBtn").onclick = outLoginHandler; } } -
views->index.ejs
<div class="sidebar"> <!-- Sidebar user panel (optional) --> <div class="user-panel mt-3 pb-3 mb-3 d-flex"> <div class="info"> <a href="#" class="d-block">欢迎您:<%=localStorage.getItem("adminName") %> </a> </div> </div> <!-- Sidebar Menu --> <%=data.sliderMenu %> <!-- /.sidebar-menu --> </div> -
controllers->index.js
export default () => { return (req, res) => { // 判断是否已经登陆过 if (!localStorage.getItem("adminName")) { ROUTER.go(URL_LOGIN);// 跳转至登陆界面 return; } res.render(adminList()); // 获取提交按钮元素,并指定点击事件 document.querySelector("#submitBtn").onclick = submitHandler; // 为包裹表格的元素,增加单击事件 document.querySelector("#adminTable").onclick = tableHandler; getAdminListAsync(); } }
33- token
token指的是令牌。可以对身份进行验证,具体的流程:
1- 当用户输入账号与密码以后,点击提交按钮,会将登陆信息提交至服务端接口(完成)
2-服务端接收到请求信息之后,会对账号以及密码进行验证(完成)
3- 服务端接收的账号以及密码如果正确,那么会生成token(token是在服务端生成的),token中会存储个人信息(账号,过期的时间),token是一个加密的串,可以以响应体或响应头的形式进行响应给前端。(完成)
4-前端可以将接收到的响应信息中的token通过storage进行保存。(完成)
5-登陆成功以后,向服务端发送请求时,会携带上token(请求头)(完成)
6-服务端相对应的接口得到token之后需要对接收的token进行验证,如果验证成功,则响应成功数据,如果验证失败,则响应token异常信息。(完成)
-
token的具体实现
-
下载jwt-simple模块
cnpm install jwt-simple -
基本使用:
// 熟悉token的生成与验证 const jwt = require("jwt-simple"); // 1- 如何生成token const adminName = "admin"; const KEY = "#$%^&*()%^&*()^&*()^&*("; const token = jwt.encode({ adminName, exp: Date.now() / 1000 + 2 // 过期时间,单位为秒 }, KEY) console.log(token); // 2- 如何验证token setTimeout(() => { const info = jwt.decode(token, KEY); console.log(info); }, 3000) -
-
utils->index.js封装了生成token的方法
const md5 = require("md5"); const jwt = require("jwt-simple"); // 盐料 const KEY = "if(1===1}{consoel.log('iloveyou')}" // 提供工具方法 module.exports = { getMd5(str) { return md5(str + KEY) }, // 生成token encode(payload, s = 60 * 30) { return jwt.encode({ ...payload,// adminName:xxxx exp: Date.now() / 1000 + s }, KEY) } } -
server.js
app.post("/login", async (req, res) => { // 接收数据 try { const { adminName, passWord } = req.body; const info = await adminModel.findOneAndUpdate({ adminName, passWord: getMd5(passWord) }, { lastLoginTime: moment().format("YYYY-MM-DD HH:mm:ss") }) if (info) { console.log(1111111111); // 增加:在响应体中增加token res.json({ ok: 1, msg: "成功", token: utils.encode({ adminName }) }) console.log(222222); } else res.sendJson("账号或密码错误") } catch (err) { console.log(333333, err); res.sendJson(err); } }) -
在前端中对token进行保存:前端->src->controllers->login.js
import login from "@v/login"; import { postLogin } from "../api/adminList"; const loginHandler = async function (e) { e.preventDefault(); // 管理员的账号 const adminName = this.adminName.value.trim(); // 管理员密码 const passWord = this.passWord.value.trim(); if (adminName.length < 1) { toastr.error("请输入管理员账号"); return; } if (passWord.length < 1) { toastr.error("请输入管理员密码"); return; } const { msg, token } = await postLogin({ adminName, passWord }); toastr.success(msg); localStorage.setItem("adminName", adminName); // 增加:保存token localStorage.setItem("token", token); // 跳转至管理界面 ROUTER.go(URL_INDEX_ADMINLIST); } export default () => { return (req, res) => { res.render(login(1)); // 增加样式 document.body.className = "login-page"; document.loginForm.onsubmit = loginHandler; } } -
前端发送请求通过请求拦截增加请求头token
-
src->request->index.js
// 请求拦截 zhang.interceptors.request.use(config => { // 增加上请求头token(如果请求头中需要携带token,请求头的名字一般叫做token或authorization) // 判断是否有token,如果有则请求头携带token if (localStorage.getItem("token")) { // 增加请求头token config.headers.token = localStorage.getItem("token"); } return config; });
-
-
后端在获取管理员列表接口,增加token的判断
app.get("/adminList", async (req, res) => { try { // 1- 得到请求头token console.log(req.headers.token); // 2- 验证token const info = await utils.decode(req.headers.token); console.log(11111, info); } catch (err) { // token解析失败 res.sendJson("token异常", -2); return; } // 目标:将数据的内容进行响应 // 思路:连接数据库,获得对应数据,进行响应。 try { const adminList = await adminModel.find({ sortObj: { regTime: -1// 按照时间的倒序排列 } }); res.json({ ok: 1, msg: "成功", adminList }) } catch (err) { res.sendJson(err); } }); -
src->request->index.js:在响应拦截中判断是否token异常
zhang.interceptors.response.use(res => { const { ok, msg } = res.data; if (ok === -1) { // 说明接口有异常 toastr.error(msg); return Promise.reject(msg); } else if (ok === -2) { toastr.error(msg); ROUTER.go(URL_LOGIN);// 跳转至登陆界面 return Promise.reject(msg); } else { return res.data;// 响应体数据 } }); -
使用多个回调
app.get("/adminList", async (req, res, next) => { try { // 1- 得到请求头token console.log(req.headers.token); // 2- 验证token await utils.decode(req.headers.token); next(); } catch (err) { // token解析失败 res.sendJson("token异常", -2); return; } }, async (req, res) => { // 目标:将数据的内容进行响应 // 思路:连接数据库,获得对应数据,进行响应。 try { const adminList = await adminModel.find({ sortObj: { regTime: -1// 按照时间的倒序排列 } }); res.json({ ok: 1, msg: "成功", adminList }) } catch (err) { res.sendJson(err); } }); -
将中间件函数进行封装
-
放置到module->middleware->index.js
const { decode } = require("../../utils"); module.exports = { sendJson() { return function (req, res, next) { res.sendJson = function (msg = "网络连接错误", ok = -1) { res.json({ ok, msg }); } next(); } }, author() { return async (req, res, next) => { try { // 1- 得到请求头token // console.log(req.headers.token); // 2- 验证token await decode(req.headers.token); next(); } catch (err) { // token解析失败 res.sendJson("token异常", -2); return; } } } // sendJson(req, res, next) { // res.sendJson = function (msg = "网络连接错误", ok = -1) { // res.json({ // ok, // msg // }) // } // next(); // } } -
使用server.js
const middleware = require("./module/middleware"); app.get("/adminList", middleware.author(), async (req, res) => { // 目标:将数据的内容进行响应 // 思路:连接数据库,获得对应数据,进行响应。 try { const adminList = await adminModel.find({ sortObj: { regTime: -1// 按照时间的倒序排列 } }); res.json({ ok: 1, msg: "成功", adminList }) } catch (err) { res.sendJson(err); } }); -
将获取当前时间封装至utils->index.js
// 获取当前时间 getNowTime() { return moment().format("YYYY-MM-DD HH:mm:ss") }
-
34- 广告列表的界面
- src->views->advList.ejs
<div class="card">
<div class="card-header">
<button type="button" id="addAdvBtn" class="btn btn-primary" data-toggle="modal"
data-target="#modal-default">添加广告</button>
<div class="card-tools">
<div class="input-group input-group-md">
<input type="text" class="form-control" id="keyword" placeholder="搜索关键字">
<div onclick="getAdvList()" class="input-group-append">
<div class="btn btn-primary">
<i class="fas fa-search"></i>
</div>
</div>
</div>
</div>
</div>
<!-- /.card-header -->
<div id="advTable" class="card-body">
<table id="advTable" class="table table-bordered">
<thead>
<tr>
<th>id</th>
<th>广告标题</th>
<th>广告图片</th>
<th>广告类别</th>
<th>广告链接</th>
<th>广告排序</th>
<th>添加时间</th>
<th>修改时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>62abd680bfc8dc02a05e1ce3</td>
<td>广告二</td>
<td><img height="80" src="http://127.0.0.1:3000/2022/06/17/1655428754546.png" alt=""></td>
<td>轮播图广告</td>
<td>http://www.baidu.com</td>
<td>111</td>
<td>2022-06-17 09:18:56</td>
<td>2022-06-17 09:19:14</td>
<td>
<button onclick="deleteAdv('62abd680bfc8dc02a05e1ce3')" type="button"
class="btn btn-danger btn-sm">删除</button>
<button type="button" onclick="openModel('62abd680bfc8dc02a05e1ce3')"
class="btn btn-success btn-sm">修改</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- /.card-body -->
<div class="card-footer clearfix" id="pageDiv">
<ul class="pagination pagination-sm m-0 float-right">
<li class="page-item"><a href="javascript:;" onclick="getAdvList(1)" class="page-link">«</a></li>
<li class="page-item active">
<a onclick="getAdvList(1)" class="page-link" href="javascript:;">1</a>
</li>
<li class="page-item"><a href="javascript:;" onclick="getAdvList(1)" class="page-link">»</a></li>
</ul>
</div>
</div>
<div class="modal fade show" id="modal-default" style="padding-left: 17px;" aria-modal="true" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">添加广告</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<form name="advForm" class="form-horizontal">
<input hidden="" type="text" name="id">
<div class="card-body">
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="advTitle">标题:</label>
<div class="col-sm-10">
<input id="advTitle" name="advTitle" type="text" class="form-control" placeholder="请输入广告标题">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="advType">类别:</label>
<div class="col-sm-10">
<select name="advType" id="advType" class="form-control">
<option value="1">轮播图广告</option>
<option value="2">轮播图底部广告</option>
<option value="3">热门回收广告</option>
<option value="4">优品精选广告</option>
</select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="advHref">链接:</label>
<div class="col-sm-10">
<input id="advHref" name="advHref" type="text" class="form-control" placeholder="请输入广告链接">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="advHref">排序:</label>
<div class="col-sm-10">
<input id="advOrder" name="advOrder" type="text" class="form-control"
placeholder="请输入广告排序,数字越大越靠前">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" for="advPic">图片:</label>
<div class="col-sm-10 custom-file">
<!-- multiple:可以选择多个文件
accept:指定可选择的文件类型
-->
<input accept="image/png,image/gif,image/jpeg" type="file" class="form-control"
name="advPic" id="advPic">
</div>
</div>
<div>
<!-- 预览图片的位置 -->
<img style="display:none;" id="preImg" height="100">
</div>
</div>
</form>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-default" id="adv-cancel" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="adv-save">添加</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
- src->controllers->advList.js
import advList from "../views/advList";
export default () => {
return (req, res) => {
res.render(advList());
}
}
35- 图片预览
import advList from "../views/advList";
let preImg;
// 当上传的图片发生改变后要执行的事件处理函数
const changeImg = function (e) {
// 目标:让选中的图片实现预览效果
// 1- 生成FileReader实例,赋值给常量reader
const reader = new FileReader();
// 2- 可以通过reader实例将图片转为base64
reader.readAsDataURL(e.target.files[0]);
// 3- onload来接收转换结果
reader.onload = function (event) {
// 转为base64之后要执行的函数
// console.log(event.target.result);// base64的内容
preImg.src = event.target.result;
preImg.style.display = "block";
}
}
export default () => {
return (req, res) => {
res.render(advList());
// 获取上传图片元素
document.querySelector("#advPic").onchange = changeImg;
preImg = document.querySelector("#preImg");// 获得预览图片的元素
}
}
36- FormData基本语法
// FormData
// 通过FormData生成一个实例
const formdata = new FormData();
// 追加数据
formdata.append("userName", "zhangsan");
formdata.append("age", 12);
// 读取名字为userName的数据
console.log(formdata.get("userName"));// zhangsan
console.log(formdata.get("age"));// 12
// userName再次追加数据
formdata.append("userName", "lisi");
console.log(formdata.get("userName"));// zhangsan
console.log(formdata.getAll("userName"));// ["zhangsan","lisi"]
// 通过set设置值
formdata.set("sex", "男");
console.log(formdata.get("sex"));// 男
formdata.set("sex", "女");
console.log(formdata.get("sex"));// 女
// 判断数据是否存在
console.log(formdata.has("sex"));// true
// 可以对数据进行删除
formdata.delete("sex");// 删除名字为sex的数据
console.log(formdata.has("sex"));// false
console.log(formdata.get("sex"));
37- FormData与表单结合使用
import advList from "../views/advList";
let preImg;
// 当上传的图片发生改变后要执行的事件处理函数
const changeImg = function (e) {
// 目标:让选中的图片实现预览效果
// 1- 生成FileReader实例,赋值给常量reader
const reader = new FileReader();
// 2- 可以通过reader实例将图片转为base64
reader.readAsDataURL(e.target.files[0]);
// 3- onload来接收转换结果
reader.onload = function (event) {
// 转为base64之后要执行的函数
// console.log(event.target.result);// base64的内容
preImg.src = event.target.result;
preImg.style.display = "block";
}
}
// 提交表单的事件处理函数
const saveHandler = function () {
// 在表单中使用FormData,将表单元素作为参数传递给FormData
const formdata = new FormData(document.advForm);
console.log("标题:", formdata.get("advTitle"));
console.log("类别:", formdata.get("advType"));
console.log("排序:", formdata.get("advOrder"));
console.log("链接:", formdata.get("advHref"));
console.log("图片:", formdata.get("advPic"));
}
export default () => {
return (req, res) => {
res.render(advList());
// 获取上传图片元素
document.querySelector("#advPic").onchange = changeImg;
preImg = document.querySelector("#preImg");// 获得预览图片的元素
// 获取提交按钮
document.querySelector("#adv-save").onclick = saveHandler;
}
}
38- 跑通接口
-
创建接口 backend-->server.js
// 5-添加广告接口 app.post("/advList", (req, res) => { res.sendJson("成功", 1); }) -
前端项目中src->api->advList.js
import request from "../request"; // 添加广告接口 export const postAdvList = function () { return request.post("/advList"); } -
提交表单:src->controllers->advList.js
const saveHandler = async function () { // 提交表单数据至接口中 const result = await postAdvList(); console.log(result); }
39- 前端提交表单数据至接口
-
前端项目如何提交
-
api->advList.js
import request from "../request"; // 添加广告接口 export const postAdvList = function (formdata) { // 将接收到的formdata作为请求体 return request.post("/advList", formdata); } -
提交表单
const saveHandler = async function () { // 提交表单数据至接口中 const formdata = new FormData(document.advForm); const result = await postAdvList(formdata); console.log(result); }
-
-
后端项目接口如何接收
-
下载模块formidable
cnpm install formidable -
接收并添加至数据库中
const formidable = require("formidable"); // 5-添加广告接口 app.post("/advList", (req, res) => { // 存放图片的目录 const uploadDir = path.resolve(__dirname, "./upload"); // 1- 在项目根目录中,创建一个用于保存图片的目录upload const form = new formidable.IncomingForm({ // 需要指定接收的图片存放的位置 uploadDir, keepExtensions: true,// 保留扩展名 }) // parse:可以将请求体中的非文件数据以及文件数据进行处理。 // parse:接收两个参数,第一个参数是请求对象,第二个参数是回调函数。 form.parse(req, async (err, params, files) => { // 回调函数会在处理完请求体数据之后执行。 // err:异常信息 // params:非文件数据 // files:文件数据 if (!err) { // console.log(params);// { advTitle: '1', advType: '2', advHref: '3', advOrder: '4' } const { advPic } = files; // 判断是否上传了图片 if (advPic.size > 0) { // 将图片放置到当天的目录中 2022/06/22 // console.log(moment().format("YYYY/MM/DD")); console.log(advPic); // upload目录下,年月日形式的目录地址 const saveDir = moment().format("YYYY/MM/DD"); const newPicName = saveDir + "/" + advPic.newFilename; // 判断地址是否存在,如果不存在,创建对应的目录 if (!fs.existsSync(path.join(uploadDir, saveDir))) fs.mkdirSync(path.join(uploadDir, saveDir), { recursive: true }); // 将图片移动到 2022/06/22 fs.renameSync(advPic.filepath, path.join(uploadDir, newPicName)); // 将接收的表单数据放置到数据库当中 await advModel.insertOne({ advTitle: params.advTitle, advType: params.advType / 1, advHref: params.advHref, advOrder: params.advOrder / 1, advPic: newPicName, createTime: utils.getNowTime(), upTime: utils.getNowTime() }) res.sendJson("成功", 1); } else { // 删除空图片 fs.unlinkSync(advPic.filepath); res.sendJson("请选择您要上传的图片"); } } else res.sendJson("上传失败"); }) }) -
module->model->advList.js
// 提供广告集合的模型 const mongoose = require("mongoose"); const Db = require("../Db"); const schema = new mongoose.Schema({ // 广告标题 advTitle: { type: String,// 类型为字符串 requried: true// 不允许为空 }, // 广告类型 advType: { type: Number,// 类型为数字 requried: true// 不允许为空 }, // 广告链接 advHref: { type: String,// 类型为字符串 requried: true// 不允许为空 }, // 广告排序 advOrder: { type: Number,// 类型为数字 requried: true// 不允许为空 }, // 广告图片地址 advPic: { type: String,// 类型为字符串 requried: true// 不允许为空 }, // 创建广告的时间 createTime: { type: String,// 类型为字符串 requried: true// 不允许为空 }, // 修改广告时间 upTime: { type: String,// 类型为字符串 requried: true// 不允许为空 } }, { versionKey: false// 可以省略,如果省略该配置,创建的文档会有版本的标识__v }) // 第一个参数是集合名,第二个参数是对集合数据状态的约束,第三个参数指定最终的集合名,如果第三个参数省略,那么最终生成的集合名为adminlists。 const model = mongoose.model("advList", schema, "advList"); module.exports = new Db(model);
-
40- 封装上传图片方法
-
utils->index.js
const path = require("path"); const fs = require("fs"); const formidable = require("formidable"); // 提供工具方法 module.exports = { // 封装上传图片 uploadPic(req, picName) { return new Promise((resolve, reject) => { // 存放图片的目录 const uploadDir = path.resolve(__dirname, "../upload"); const form = new formidable.IncomingForm({ uploadDir, keepExtensions: true,// 保留扩展名 }) form.parse(req, async (err, params, files) => { if (!err) { const picInfo = files[picName]; // 判断是否上传了图片 if (picInfo.size > 0) { const saveDir = moment().format("YYYY/MM/DD"); // 为params增加属性 params[picName] = saveDir + "/" + picInfo.newFilename; // 判断地址是否存在,如果不存在,创建对应的目录 if (!fs.existsSync(path.join(uploadDir, saveDir))) fs.mkdirSync(path.join(uploadDir, saveDir), { recursive: true }); // 将图片移动到 2022/06/22 fs.renameSync(picInfo.filepath, path.join(uploadDir, params[picName])); resolve({ params, msg: "上传成功", ok: 1,// 成功上传了图片 }) } else { // 删除空图片 fs.unlinkSync(picInfo.filepath); resolve({ params, msg: "请选择您 要上传的图片", ok: 2,// 无图片 }) } } else reject(err); }) }) } } -
server.js使用
app.post("/advList", async (req, res) => { try { const { params, ok, msg } = await utils.uploadPic(req, "advPic"); console.log(222, params, ok, msg); if (ok === 1) { params.advType = params.advType / 1; params.advOrder = params.advOrder / 1; // 插入图片 await advModel.insertOne({ ...params, createTime: utils.getNowTime(), upTime: utils.getNowTime() }) res.sendJson(msg, 1); } else res.sendJson(msg); } catch (err) { console.log(1111, err); res.sendJson(err); } }
41- 查询广告
42- 删除广告
43- 修改广告
- 点击修改按钮获取要修改的内容,呈现出来
- 用户进行修改操作,修改完毕之后提交
- 服务端进行更新
44- 打包服务器配置
const path = require("path");
const express = require("express");
const {createProxyMiddleware} = require("http-proxy-middleware");
const history = require("connect-history-api-fallback");
const app = express();
// node支持单页面应用
// cnpm install connect-history-api-fallback
// app.use(history());// 默认是渲染index.html
app.use(history({
index:"home.html",// 指定默认的页面
}))
app.use(express.static(path.resolve(__dirname,"./build")));
// node中使用代理服务
// cnpm install http-proxy-middleware
app.use("/api",createProxyMiddleware({
target:"https://api.zhangpeiyue.com",
secure:false,
pathRewrite: {
"^/api":""
}
}))
app.listen(8091,()=>{
console.log("success");
})
\