使用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项目,包括所有的前后端逻辑:传送门