Skip to content

express图书馆项目

01 产品需求分析

统计信息:字数 9717 阅读20分钟

需求:做一个文章管理系统,技术栈为 express + nodejs + mongdb。整体项目是后端项目,前端界面实现功能为主,没有太多 CSS 实现。

原型图中有五个界面:注册、登录、首页、文章编辑、文章详情界面

  • 注册:一个表单:用户名,密码,确认密码,注册按钮,跳转登录
  • 登录:一个表单:用户名,密码,登录按钮,跳转注册
  • 首页:上边 header:左侧是产品 LOGO,右边是用户头像(下拉菜单中有:设置,退出)中间是文章列表(序号,作者,标题,发布时间,编辑,删除等)底部是跳转(前一页,后一页,首页,尾页,共多少页等)
  • 文章编辑:输入文章标题,文章内容,右下方是发布的按钮(也支持编辑已有的小说)头部的组件不变 Header 通用
  • 文章详情:只读的界面,上面是题目,中间是作者和发布时间,下面是具体的内容。头部的组件不变 Header 通用

02 初始化 express + mongodb

数据库设计:包括两个数据库表,用户表;书籍表

使用 express-generator CLI 初始化项目结构(全局安装),需要全局安装 nodemon 可以保证 node 服务端热更新,创建使用 ejs 模板。

需要安装 mongoDB 服务器,在项目中建立链接(这里根据实际安装的数据库操作)。类似 mysql 链接的步骤。

model.js

var MongoClient = require('mongodb').MongoClient;
var url = 'mongodb://localhost:27017';
var dbName = 'project';

// 封装数据库的连接方法
function connect(cb) {
  MongoClient.connect(url, (err, client) => {
    if (err) {
      console.log(err);
    } else {
      let db = client.dn(dbName);
      cb && cb(db);
      client.close();
    }
  })
}

module.exports = { connect };

Routers.js 调用连接的方法

var express = require('express');
var model = require('../model');
var router = expres.Router();

router.get('/', (req, res, next) => {
  model.connect((db) => {
    db.collection('users').find().toArray((err, res) => {
      console.log(res);
      res.render('index', { title: 'Demo' });
    });
  });
});

module.exports = routers;

课时3 注册功能

ejs 模板抽取公共部分,head.ejs

<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/stylesheets/style.css" />

在页面模板中,使用 include 语法,即可使用公共部分代码

regist.ejs

<html lang="en">
  <head>
    <title>注册</title>
    <%- include head %>
  </head>
  <body>
    <h3>
      注册页面
    </h3>
    <div>
      <form action="/user/regist" method="post">
        <input type="text" name="username" placeholder="请输入用户名">
        <input type="submit" name="" value="注册"/>
      </form>
    </div>
  </body>
</html>

路由部分增加

// 注册页面
router.get('/regist', function(req, res, next) {
  const { username, password } = req.body;
  var data = {
    username, password
  };
  model.connect(function(db) {
    db.collection('users').insertOne(data, function(err, ret) {
      if (err) {
        console.log('注册失败')
        res.redirect('regist');
      }
    })
  });
  // 返回成功
  res.send(data);
});

课时4 登录功能

// routers.js
router.post('./login', function(req, res, next) {
  var data = {
    username: req.body.username,
    password: req.body.password,
  };
  // TODO:数据有效性验证
  model.connect(function(db) {
    db.collection('users').fund(data).toArray(function(err, docs) {
      // 登录错误,重定向到登录页
      if (err) {
        logger.err(err);
        res.redirect('./login');
      } else {
        // 用户信息存在,重定向到首页
        if (docs.length > 0) {
          res.redirect('/');
        } else {
          // 用户信息不存在,重定向到登录页
          res.redirect('./login');
        }
      }
    }
  });
})

课时5 登录拦截 session

使用 express-session 保存登录信息(类似的是 token)

在入口中,添加中间件

var session = require('express-session');

// 中间件:官方默认配置即可
app.use(session({
  secret: 'xzxzxz',
  resave: false,
  saveUninitialized: true,
  cookie: {
    secure: true,
    maxAge: 1000 * 60 * 30, // 设置过期时间
  },
});

// 登录拦截
app.get('*', function(req, res, next) {
  // 从 session 中拿到用户名
  var username = req.ression.username;
  // 如果拿不到用户名,重定向到登录页
  if (path !== '/login' && path !== '/regist' && !username) {
    res.redirect('/login');
  }
  next();
})

然后,当用户成功登录后,增加 session

router.post('./login', function(req, res, next) {
  var data = {
    username: req.body.username,
    password: req.body.password,
  };
  model.connect(function(db) {
    db.collection('users').fund(data).toArray(function(err, docs) {
      if (err) {
        logger.err(err);
        res.redirect('./login');
      } else {
        if (docs.length > 0) {
          // 新增:用户信息存储到 session 
          req.session.username = data.username;
          res.redirect('/');
        } else {
          res.redirect('./login');
        }
      }
    }
  });
})

课时6 退出登录

首先完成一个前端公共部分的 nav 导航栏

<div>
  <a href="/">
    <img src="/images/home.png" alt="首页">
  </a>
  <span>{% username %}</span>
  <a href="/write">写文章</a>
  <a href="/users/logout">退出</a>
</div>

首页引入导航栏(需要传参)

<html>
    <head>
    <title>首页</title>
    <%- include head %>
  </head>
  <body>
    <%- include('nav', { username:username }) %>
  </body>
</html>

退出登录

router.get('/login', function(req, res, next) {
  req.session.username = null;
  res.redirect('./login');
});

课时7 文章发布功能

Article.js

// api 增加文章
router.post('/add', function(req, res, next) {
  const { title, content, username } = req.body;
  var data = {
    title,
    content,
    id: Date.now(),
    username: username || 'unknown',
  }
  model.connect(function(db) {
    db.collection('articles').insertOne(data, function(err, ret) {
      if (err) {
        console.log('publish error', err);
        res.redirect('/write');
      } else {
        res.redirect('/');
      }
    })
  })
});

对应页面模板

<head>
  <title>写文章</title>
  <%- include head %>
</head>
<body>
  <%- inlcude('bar', { username: username }) %>
  <div class="article">
    <form action="/article/add">
      <input type="text" name="title" placeholder="请输入文章标题" value=""/>
      <textarea name="content"></textarea>
      <input type="submit" value="发布"/>
    </form>
  </div>
  <!-- 引入第三方编辑器插件 xheditor -->
  <script>
    $('#elm1').xheditor({
      tools: 'full',
      skin: 'default',
    });
  </script>
</body>

课时8 文章列表

正常的情况是,前端获取某一个页面的数据,不应该获取全部的文章

<div class="list">
  <% list.map(function(item, index)) { %>
    <div class="row">
      <span>序号</span>
      <span>作者</span>
      <span>标题</span>
      <span>时间</span>
      <span>
        <a href="/edit">编辑</a>
        <a href="article/delete?id=<%-item.id%>&page=<%data.currrentPage%>">删除</a>
      </span>
    </div>
  <% }); %>

  <!-- 分页器 -->
  <div class="pages">
    <% for(let i = 0; i < data.total; i++) { %>
      <a href=""><%- i %></a>
    <% } %>
  </div>
</div>
router.get('/', function(req, res, next) {
  var username = req.session.username || '';

  var data = {
    total: 0,
    curPage: 1,
    list: [],
  };
  var pageSize = 2;

  model.connect(function(db) {
    // 1 查询所有文章
    db.collection('articles').find().toArray(function(err, docs) {
      console.log('文章列表', err);
      var list = docs;
      list.map(function(ele, index) {
        ele['time'] = moment(ele.id).format('YYYY_MM_DD HH:mm:ss')
      })
      data.total = Math.ceil(docs.length / pageSize);
      // 2 查询当前页的文章列表
      model.connect(function(db) {
        db.collection('artilces').find().sort({ _id: -1 }).limit(pageSize).skip((page - 1) * pageSize)
      })
      data.list = docs;
      res.render('index', { username, data: data });
    })
  })
})

// 阅读文章,编辑文章
// 如果已有文件ID,就是编辑文章,如果没有文件 ID 那就是预览文章
router.get('/write', function(req, res, next) {
  // username, id, page
  model.connect(function(db) {

  })
  res.render('write', { username })
})

课时9 分页查询

课时10 删除文章

课时11 修改文章

// 写文章
router.get('/write', function(req, res, next) {
  res.render('write', {});
});

课时12 文章详情页

课时13 文件上传

// 文件上传,借用了第三方的工具实现前端上传
// 后端需要增加

router.post('/upload', function(req, res, next) {
  let form = new multiparty.Form();
  form.parse(req, function(err, fields, files) {
    console.log(files);
    var file = files.filedata[0];
    // 这里把上传路径获取到
    var rs = fs.createReadStream(file.path);
    // 这是服务端存储的路径
    var newPath = '/uploads/' + file.originalFilename;
    var ws = fs.createWriteStream('./public' + newPath);
    // 然后把本地文件写入服务器?
  })
})

课时14 项目总结


Last update: November 9, 2024