工程化demo

88 阅读23分钟

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
    
  • 如何配置路由

    • 中文官网:sme-fe.github.io/website-rou…

    • 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 &copy; 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 &copy; 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");
})

\