纯node.js搭建简单博客

甄志
2023-12-01

说明

使用node.js搭建简单的网站。
这是鄙人第一次做网站,网站逻辑也非常简单,主要是根据黑马程序员的公开课程制作的,但是进行了小范围的逻辑修改。
本文主要介绍后端逻辑。

数据库:mongoDB
node.js web应用框架:Express
前端框架:Bootstrap

环境搭建

安装node.js , 使用express-generator生成器创建脚手架(当然也可以不用脚手架)

入口文件

app.js是入口文件,进行一些配置

var createError = require('http-errors');//错误处理
var express = require('express');//express
var path = require('path');//path模块
var cookieParser = require('cookie-parser');//cookie
var logger = require('morgan');//日志模块
const session =require("express-session");//session
var moment = require('moment');
    moment.locale('zh-cn');
const dataformat = require("dateformat");
const template = require("art-template");
template.defaults.imports.dateformat=dataformat;//模板导入
//数据库连接
require("./model/connect")

//下面是路由模块的导入
var indexRouter = require('./routes/home');
var usersRouter = require('./routes/users');
//建立一个express客户端
var app = express();

//添加的第三方模块处理post强求
const parser =require("body-parser");
app.use(parser.urlencoded({extended :false}))
//处理post请求,所有的post都被parser拦截了,这样的话所有的post请求都会多出来一个body项目

app.use(session({secret:"secret key",    //加密秘钥
                 saveUninitialized:false,//默认cookie
                cookie:{
                     maxAge:24*60*60*1000//过期时间毫秒
                }
         }))//配置session,实现用户识别
//托管静态资源
//需要使用完全路径,因此使用了path模块
//第一个参数是绝对根目录,第二个参数是之后的目录
//一定要在路由器之前托管静态资源!!!注意,html页面是视图模板,不是静态资源
app.use(express.static(path.join(__dirname,"public")));

// view engine setup 视图引擎设置
app.set('views', path.join(__dirname, 'views'));
//指示模板框架的位置
app.set('view engine', 'art');
//提供模板的默认后缀,让系统可以识别你的模板
app.engine("art",require("express-art-template"))
//需要先安装这个模板引擎,然后调用他

//下面是自动调用的几个中间件
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
//负责登录拦截
app.use("/users",require("./middleware/loginGuard.js"));
//使用的路由器
app.use('/', indexRouter);//对首页的访问
app.use('/users', usersRouter);//对用户已控制的访问

// catch 404 and forward to error handler 错误处理程序
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler 错误处理中间件
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error.jade');
});

//测试

module.exports = app; //让这个文件可以被使用

数据库链接

使用mongoose管理数据库

//链接数据库
const mongoose =require("mongoose");
mongoose.connect("mongodb://localhost/blog",{useNewUrlParser:true})
.then(()=>console.log("mongoose 链接成功!"))
.catch(()=>console.log("数据库链接失败"))

这里的代码非常简单,只是进行了简单的链接,链接参数都是默认的,如果没有这个blog文件夹,数据库会自动创建文件夹。

用户逻辑

在app.js中,将用户信息的处理交给“/users”路由器处理
var usersRouter = require('./routes/users');
app.use('/users', usersRouter);//对用户已控制的访问
新建路由器文件,初始化一个路由器
var express = require('express');
var router = express.Router();
用户路由包括以下几个路由

//下面的是渲染用户登录页面的路由 
router.get('/login', function(req, res, next) {
  res.render("users/login")//提供相应的模板引擎渲染
});
//下面是相应用户登录请求的路由
router.post("/login" , require("./users/login.js"))
//实现用户退出的路由
router.get("/logout",require("./users/logout.js"))
//文章管理的实现
//下面的是文章修改页面渲染的路由
router.get('/article-edit', require("./users/article-edit-render"));
//文章列表渲染的路由
router.get("/article",require("./users/article-render"))
//文章添加路由
router.post("/article-add",require("./users/article-add"))
//文章删除路由
router.get("/article-delete",require("./users/article-delete"))
//文章修改路由
router.post("/article-edit",require("./users/article-edit"))
/用户管理的实现
//下面的是用户列表的路由
router.get('/user', require("./users/userpage"));
//下面是用户编辑页面渲染的路由
router.get("/user-edit",require("./users/user-edit-render"))
//下面是用户添加功能的路由相应的是用户编辑页面的post请求
router.post("/user-edit",require("./users/user-edit.js"))
//下面是用户信息修改功能的路由 响应对user-change 的post请求
router.post("/user-change",require("./users/user-change.js"))
//下面是用户删除的路由
router.get("/delete",require("./users/delete"))
用户首页路由
router.get("/home",require("./users/home-render.js"))
//修改头像
router.post("/head-edit",require("./users/head-edit"))

用户集合

用户是存储在数据库中的,所以先建立用户集合

    const mongoose =require("mongoose");
    const userSchema=new mongoose.Schema({
    username :{//用户名
        type : String,//注意大写!!
        require:true,
        minlength:1,//最小长度 //maxlength:最大长度
    },
    email:{//邮箱名
        type : String,
        unique:true,//查重
        require:true
    },
    password:{//密码
        type : String,
        require:true
    },
    role:{//账户类型
        type : String,
        repuire:true
    },
    state:{//账户状态
        type:Number,
        default:0//默认值
    },header:{//头像
        type :String,
        default :"/home/images/logo.png"
    }
})

//获得一个集合的构造函数
const User = mongoose.model("User",userSchema);

这些代码是数据库处理代码。最好放在单独的模块里

渲染用户编辑界面

用户编辑界面不仅需要显示需要编辑的信息,还要渲染来自服务器的错误信息
如果是编辑页面,还要渲染用户的原有信息

const {User}=require("../../model/user")
module.exports=async function(req,res,next){
   req.app.locals.current="user";//标记,保持高亮的
    const {message , id }= req.query//如果出现了错误,会使用message提示
    if(id){//如果有id,证明是用户修改
       let user=await User.findOne({_id:id})//传递两个参数 错误信息 user信息
       res.render("users/user-edit", {message : message ,user:user } );
    }else{
       res.render("users/user-edit", {message : message });//传递一个错误信息
    }
  }

用户新增

用户新增的逻辑是
1.接受服务器传来的表单
2.对信息进行合法性检查
3.操作数据库新增用户

在添加用户之前需要对用户信息进行合法性检查
这里使用了joi第三方模块 网站https://joi.dev/api/?v=17.6.0

const validateUser=(user)=>{
    //定义对象验证规则
    const schema=joi.object({
        username:joi.string().min(1).error(new Error("用户名不符合规则!")).required(),
        email: joi.string().email().error(new Error("没有填写正确的邮箱格式!")).required(),
        password: joi.string().regex(/^[a-zA-Z0-9]{3,30}$/).error(new Error("密码必须由字母或者数字开头!")).required(),
        role:joi.string().valid("normal","admin").error(new Error("角色严重错误")).required(),
        state :joi.number().valid(0,1).error(new Error("状态非法")).required()
   })
   return schema.validate(user);
}

用户的密码需要加密,这里使用了bcryptjs模块,具体的用法请自行查阅
下面是这个路由处理函数的完整代码

//添加修改用户的路由
const joi = require("joi")//引入joi模块
///从数据库模块导入用户集合构造函数
const {User ,validateUser}=require("../../model/user");
//导入数据加密库bcriptjs 这个不需要任何依赖 用来比较密码
const bcrypt = require('bcryptjs');

module.exports =async function(req,res,next){
    const newUser=validateUser(req.body);//使用验证函数验证,产生错误参数
   if(newUser.error){//通过查询字符串的方式携带错误信息,redirect不能直接携带message
        res.redirect(`/users/user-edit?message=${newUser.error.message}`)
        return;//防止报错,终止程序 
    }else{
      //基础验证通过了,验证重复
      const user = await  User.findOne({email: req.body.email})
      if(user){res.redirect(`/users/user-edit?message=邮箱已经被注册!`); return;}//防止报错,终止程序
      const salt=await bcrypt.genSalt(10);//产生“盐"加密密码
      const password = await bcrypt.hash(req.body.password,salt);//哈希加密
      req.body.password=password;
      if(await User.create(req.body)){//添加用户
          res.redirect("/users/user");
      }else{
        res.status(400).render("users/error",{msg:"服务器出现了未知的错误!"})
      }
   }
}

用户登录

用户登录的时候,需要使用bcrypt比较密码
同时需要向session中存储信息,保证在次访问时不需要重新登录

const {User}=require("../../model/user");///从数据库模块导入用户集合构造函数
const bcrypt = require('bcryptjs');//导入数据加密库bcriptjs 这个不需要任何依赖 用来比较密码
module.exports=async (req,res)=>{
    //接受请求参数
   const {email,password}=req.body;
   if(email.trim().length===0||password.trim().length===0){
     return res.status(400).render("users/error",{msg:"邮件地址或密码错误!3S后跳转到原来页面,如果没有,请手动跳转"})
   }//对信息进行初步验证
   //查询用户信息 使用了await 语法 查到了user中就会有信息
   let user=await User.findOne({email : email})//使用findOne查询唯一的信息,只要集合中有这个信息项就会返回这个对象
   if(user){
     //查询到了用户
      if(user.state===0){
          let key=await bcrypt.compare(password,user.password);//实现比对密码
          if(key){
             req.session.username=user.username;//登录成功,向session中存储一些信息
             req.session.role=user.role;
            req.app.locals.userInfo = user;//在模板文件中存储一些信息,显示用户信息
             res.redirect("/") 
          }else{
             res.status(400).render("users/error",{msg:"用户名或者密码错误!"})
          }
      }else{
         res.status(400).render("users/error",{msg:"你的账号被封号了!!!"})
      }
   }else{ 
      res.status(400).render("users/error",{msg:"用户名或者密码错误!"}) 
  }
}

用户修改

用户修改和用户添加是差不多的,但是用户修改时,密码是用来验证的而不是用来修改的

   const {User , validateUser} = require("../../model/user");//引入模块
//导入数据加密库bcriptjs 这个不需要任何依赖 用来比较密码
const bcrypt = require('bcryptjs');
module.exports=async function(req,res,next){
    const userID = req.query.id;//获取信息
    const userMessage=req.body; //获取信息
    let user = await User.findOne({_id:userID});//查询用户
    if(await bcrypt.compare(userMessage.password , user.password)){
      //正确的密码
      const changeUser=validateUser(userMessage);//使用验证函数验证
      if(changeUser.error){//通过查询字符串的方式携带信息,redirect不能直接携带message
        res.redirect(`/users/user-edit?id=${userID}&message=${changeUser.error.message}`)
        return;//防止报错,终止程序 
      }else{//正确的密码 正确的信息格式
        await User.updateOne({_id:userID},{//一定要使用await接受,否则会有问题!写的是要修改的,不用修改的不写
            username:userMessage.username,
            email:userMessage.email,
            role:userMessage.role,
            state:userMessage.state
        })
        res.redirect("/users/user");
      }
    }else{
      //错误的密码
      res.redirect(`/users/user-edit?id=${userID}&message=密码验证错误`)
    }
    
}

管理员查看所有用户

//所有用户信息展示页面渲染路由
const {User}=require("../../model/user.js");//导入用户集合

module.exports=async function(req, res, next) {
    req.app.locals.current="user";//标记高亮
    let page = req.query.page || 1;//获得页数
    //一共有几页?
    let usernum=await User.countDocuments({})//数据的总数
    let total=Math.ceil(usernum/20);//向上取整计算总页数
    let start=(page-1)*20;//计算查询开始的位置
    let users=await User.find({}).limit(20).skip(start);//查询信息
    res.render("users/user",{
          users:users,//用户信息
          page:page,  //当前页面
          total:total,//页面总数
          usernum:usernum//用户总数
    })//提供相应的模板引擎渲染
}

用户头像

用户头像需要文件模块这里使用了formidable 模块

const form = new formidable.IncomingForm();
    //配置服务器文件夹,将客户端上传的文件保存到这里这里必须写绝对路径 __dirname 是这个文件的文件夹
   form.uploadDir = path.join(__dirname,"../","../","public/","upload")
   //解系表单
   form.keepExtensions = true;//保留拓展名
   form.parse(req,async function (err,fields,files){//错误对象 正常信息 文件信息
    //fs.rename(path.normalize(files.cover.filepath),path.normalize(files.cover.filepath)+path.parse(files.cover.originalFilename).ext,(error)=>{if(error) {console.log(error)}}) //如果拓展名保留出现蜜汁问题可以使用原生fs方法改名
     let thepath;
     //下面的代码可以为用户添加默认封面
     //如果表单提交是时候没有携带文件,formidable也会生成一个空文件,我们要把他删除
    if(path.parse(files.cover.originalFilename).ext===""){
        fs.unlink(path.normalize(files.cover.filepath),(err)=>{if(err)console.log("head-edit err in line 17")});
        thepath="/home/images/logo.png";//更改的路径
     }else{
        thepath= (path.normalize(files.cover.filepath)+path.parse(files.cover.originalFilename).ext).split("public")[1];//我不知道为什么文件拓展名保留失败了才这么写,如果保留成功,files.cover.filepath 就是路径了。但是路径要经过裁剪再保存
     }
        await User.updateOne({_id:id},{//更新用户信息
          header:thepath
        })
        req.app.locals.userInfo.header = thepath;//更新一下渲染
    if(err){return console.log(err)}
      res.redirect("/users/home")//更改之后重定向到用户首页 
      })
   }

用户删除

不仅要删除用户信息,还要删除用户的评论,文章,点赞,否则可能会因为读取到undifiend而出现错误

//删除用户的路由
//导入数据库操作
const {User}=require("../../model/user");
const {Comment} = require("../../model/comment")
const {Good}=require("../../model/good")
const {Article}=require("../../model/artical")//文章集合构造函数
module.exports =async function(req,res,next){//使用get请求,信息会被携带在查询字符串中。通过req解析
   Comment.remove({uid : req.query.id },(err)=>{if(err)console.log(err)})//用户的所有评论
   Good.remove({uid : req.query.id },(err)=>{if(err)console.log(err)})//用户的所有点赞
   Article.remove({author: req.query.id },(err)=>{if(err)console.log(err)})//用户的所有文章
   await User.findOneAndDelete({_id:req.query.id});
   res.redirect("/users/user");
}

用户退出

用户退出最简单,删除cookie就可以了

const router = require("../users")
module.exports=function(req,res){
    //删除session
    req.session.destroy(()=>{
    //删除cookie
    res.clearCookie("connect.sid");
    req.app.locals.userInfo= null;//清空userinfo
    res.redirect("/")//重定向
    })
}

登录拦截

用户在没有登录的时候是不能访问管理页面的,所以需要登录拦截
另外普通用户也不能访问用户管理界面

const Guard = (req,res,next)=>{//拦截请求 从session中读取的
if(req.url!="/login"&&req.url.split("?")[0]!="/user-edit"&&!req.session.username){
          res.redirect("/users/login");//不是登录 不是新增用户的
}else{
       if( req.session.role==="normal"&&req.url==="/user")//普通用户不能访问显示所有用户的路由
          return res.redirect(`/users/user-edit?id=${req.app.locals.userInfo._id}`)//res.send()}
        next();//必须"放行!!!
}}
module.exports=Guard;

文章逻辑

文章逻辑基本和用户逻辑是一样的,甚至更简单
建立文章集合

const mongoose =require("mongoose");
//建立集合规则 操作文章数据库
const articalSchema=new mongoose.Schema({
    title:{
        type : String,
        required:[true, "没有文章标题" ]
    },author:{
        type : mongoose.Schema.Types.ObjectId,//od
        ref: "User",//关联连个数据库,数据库的名字一定要写对!!!
        required:[true, "没有作者" ]
    },publishDate:{
        type : Date,
        required:[true,"没有时间"]
    },cover : {
        type :String,
        default :"/home/images/logo.png"
    },content:{
        type: String
    },view:{
        type :Number,
        default : 0
    }
}) 
const Article = mongoose.model("Article",articalSchema);//建立构造函数,第一个传入的参数将作为这个数据库集合的名字
 module.exports={Article}//暴露项目

文章新增

文章有封面,所以需要对文件进行处理

//文章请求
//引入第三方模块
const formidable = require("formidable")
const path = require("path");
const fs = require('fs');
const {Article}=require("../../model/artical")//文章集合构造函数
module.exports=async function(req,res,next){
    //建立表单解释对象
    const form = new formidable.IncomingForm();
    //配置服务器文件夹,将客户端上传的文件保存到这里这里必须写绝对路径 __dirname 是这个文件的文件夹
   form.uploadDir = path.join(__dirname,"../","../","public/","upload")
   //解系表单
   form.keepExtensions = true;
   form.parse(req,async function (err,fields,files){//错误对象 正常信息 文件信息
       fs.rename(path.normalize(files.cover.filepath),path.normalize(files.cover.filepath)+path.parse(files.cover.originalFilename).ext,(error)=>{if(error) {console.log(error)}})
       //由于保留文件后缀的操作蜜汁失灵,只能使用传统方法更改名字
     //res.send((path.normalize(files.cover.filepath)+path.parse(files.cover.originalFilename).ext).split("public")[1])
     let thepath;
     if(path.parse(files.cover.originalFilename).ext===""){
        thepath="/home/images/logo.png";
     }else{
        thepath= (path.normalize(files.cover.filepath)+path.parse(files.cover.originalFilename).ext).split("public")[1];
     }
      await Article.create({
        title :fields.title,
        author :fields.author,
        publishDate :fields.publishDate,
        content :fields.content,
        cover: thepath
      })
      if(err){return console.log(err)}
      res.redirect("/users/article")//重定向  
   })
}

文章修改

和前面的代码基本一致

const formidable = require("formidable")
const path = require("path");
const fs = require('fs');
const {Article}=require("../../model/artical")//文章集合构造函数
module.exports=async function(req,res,next){
    //建立表单解释对象
    const {id} = req.query;
    const form = new formidable.IncomingForm();
    //配置服务器文件夹,将客户端上传的文件保存到这里这里必须写绝对路径 __dirname 是这个文件的文件夹
   form.uploadDir = path.join(__dirname,"../","../","public/","upload")
   //解系表单
   form.keepExtensions = true;
   form.parse(req,async function (err,fields,files){//错误对象 正常信息 文件信息
       fs.rename(path.normalize(files.cover.filepath),path.normalize(files.cover.filepath)+path.parse(files.cover.originalFilename).ext,(error)=>{if(error) {console.log(error)}})
       //由于保留文件后缀的操作蜜汁失灵,只能使用传统方法更改名字
     //res.send((path.normalize(files.cover.filepath)+path.parse(files.cover.originalFilename).ext).split("public")[1])
     let thepath;
     if(path.parse(files.cover.originalFilename).ext===""){
        thepath="/home/images/logo.png";
     }else{
        thepath= (path.normalize(files.cover.filepath)+path.parse(files.cover.originalFilename).ext).split("public")[1];
     }
      await Article.updateOne({_id:id},{
        title :fields.title,
        author :fields.author,
        publishDate :fields.publishDate,
        content :fields.content,
        cover: thepath
      })
      if(err){return console.log(err)}
      res.redirect("/users/article")//重定向  
   })
}

文章展示

向用户展示所有文章,管理员可以看见所有文章,用户只能看见自己的文章

//文章集合的查询
const {Article}=require("../../model/artical")
const pagenation = require("mongoose-sex-page")
module.exports=async function(req,res,next){
    req.app.locals.current="art";//标记,让标签可以高亮
    //查询所有文章数据
    //let articles = await Article.find({}).populate("author").lean();//这个项目关联了其他的数据库集合,所以可以使用这个方法进行多集和联合查询
    let select;
    if(req.app.locals.userInfo.role==="admin"){
        select={};
    }else{
        select={author  : req.app.locals.userInfo._id }
    }
    const {page} = req.query;
    let articles = await pagenation (Article).find(select).page(page).size(10).display(6).populate("author").exec(); 
    articles = JSON.stringify(articles);//不转化的话会报错,使用这种转换方法解决了报错问题,但是不知道为什么
    articles = JSON.parse(articles);
    res.render("users/article",{articles:articles});
}

文章删除

//文章删除
const {Article}=require("../../model/artical")//文章集合构造函数
const {Comment} = require("../../model/comment")
const {Good}=require("../../model/good")
module.exports=async function(req,res,next){
    Comment.remove({aid : req.query.id },(err)=>{if(err)console.log(err)})//文章的所有评论
    Good.remove({aid : req.query.id },(err)=>{if(err)console.log(err)})//文章的所有点赞
    await Article.findOneAndDelete({_id:req.query.id});
    res.redirect("/users/article");
}

主页逻辑

主页路由比较简单

var express = require('express');//引入express
var router = express.Router();//建立理由器
//博客的首页路由
/* GET home page. */
router.get('/', require("./home/index"));//主页
router.get('/article',require("./home/article-render"))//文章
router.post("/good",require("./home/good"))//点赞
router.post("/comment",require("./home/comment"))//评论
module.exports = router;
//暴露路由模块

首页显示

首页显示所有文章,使用了上面已经使用过的分页工具mongoose-sex-page

const {Article}=require("../../model/artical");//文章集合构造函数
//显示所有文章的界面
const pagination = require("mongoose-sex-page")
module.exports =async function(req,res,next){
    const {page} = req.query
    let result = await pagination(Article).find({}).page(page).size(4).display(3).populate("author").exec();
    result = JSON.stringify(result);//使用这种转换方法解决了报错问题,但是不知道为什么
    result = JSON.parse(result);
    res.render("home/default",{result:result})
}

文章显示

通过读取查询字符串的信息确定文章并查找

const {Article} = require("../../model/artical")
const {Comment} = require("../../model/comment")
const {Good}=require("../../model/good")
module.exports =async function(req,res,next){
    const {id} = req.query;
    const article = await Article.findOne({_id:id}).populate("author").lean();//文章
    await Article.updateOne({_id:id},{view : article.view+1})//更新浏览量
    const comments = await Comment.find({aid:id}).populate("uid").lean();//评论
    const goods = await Good.countDocuments({aid:id});//评论
    let gooded = false;
    if( req.app.locals.userInfo){
        const find= await Good.countDocuments({aid:id , uid :req.app.locals.userInfo._id})
        if(find===1){
             gooded=true;
        }
    }
    //res.send(id);
    res.render("home/article.art",{article ,comments,goods,gooded})
}   

评论/点赞

评论点赞的代码基本一致,只是评论有内容,点赞没内容
构建集合

const {Schema, default: mongoose , model } = require("mongoose")
const commentSchema=s=new Schema({
    aid:{//文章id
        type : mongoose.Schema.Types.ObjectId,
        ref : "Article"//链接集合
    },uid:{//用户id
        type : mongoose.Schema.Types.ObjectId,
        ref: "User"
    },content:{//时间
        type:String
    },time:{//内容
        type : Date
    }
});
const Comment = model("Comment",commentSchema);
module.exports={
    Comment
}

添加评论

const{Comment}=require("../../model/comment")
module.exports = async function(req,res,next){
      const {content , aid ,uid} = req.body;
      await Comment.create({
           content : content,
           uid :uid,
           aid :aid,
           time:new Date()
      })
      res.redirect("/article?id="+aid);
}

总结

这是一个非常简单的网站,只是实现了简单的增 删 改 查
所有项目文件请访问我的github项目,包括所有的前后端逻辑:传送门

 类似资料: