纯node.js搭建简单博客

纯node.js搭建简单博客

  • 说明
  • 环境搭建
  • 入口文件
  • 数据库链接
  • 用户逻辑
    • 用户集合
    • 渲染用户编辑界面
    • 用户新增
    • 用户登录
    • 用户修改
    • 管理员查看所有用户
    • 用户头像
    • 用户删除
    • 用户退出
    • 登录拦截
  • 文章逻辑
    • 文章新增
    • 文章修改
    • 文章展示
    • 文章删除
  • 主页逻辑
    • 首页显示
    • 文章显示
    • 评论/点赞
  • 总结

说明

使用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,//默认cookiecookie:{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 developmentres.locals.message = err.message;res.locals.error = req.app.get('env') === 'development' ? err : {};// render the error pageres.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不能直接携带messageres.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不能直接携带messageres.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){//删除sessionreq.session.destroy(()=>{//删除cookieres.clearCookie("connect.sid");req.app.locals.userInfo= null;//清空userinfores.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,//odref: "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.querylet 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:{//文章idtype : mongoose.Schema.Types.ObjectId,ref : "Article"//链接集合},uid:{//用户idtype : 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项目,包括所有的前后端逻辑:传送门


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部