art-template+Express+MongoDB小项目

前言 为了体验前后端交互的内容,我找了art-template+Express+MongoDB(代码Github->https://github.com/molifenge/selfblog)这个简单的小项目练练手,页面极其简陋,不喜勿喷。本项目是使用前端引擎art-template+Express+MongoDB为主开发的项目,本文将对项目使用的技术和一些细节处进行讲解。
一、效果图 前台界面:文章列表页面+文章详情页面
1.前台页面(含分页功能)

前台文章列表
2.文章详情页面+评论功能实现
在这里插入图片描述
后台界面:登录页面+用户列表页面+文章列表页面
1.用户登录:
art-template+Express+MongoDB小项目
文章图片
后台登陆页面
2.用户增加删除(分页功能)
art-template+Express+MongoDB小项目
文章图片
用户添加删除功能
3.用户修改功能(密码比对失败不能修改)
art-template+Express+MongoDB小项目
文章图片
用户修改
4.文章添加功能(含图片上传)
art-template+Express+MongoDB小项目
文章图片
文章添加功能
5.文章修改删除功能
文章修改删除
二、功能 已经完成功能

  • 用户登录
  • 用户添加、修改、删除(包含密码验证、数据分页功能)
  • 文章添加、修改、删除(包含图片上传、数据分页功能)
  • 前台文章列表页面显示
  • 文章详情页面
  • 文章评论功能
  • 期待优化或实现
    • 用户注册功能
    • 文章修改时,日期默认原始日期显示
    • 前台文章列表页面自适应布局实现
    • 前台文章列表页面美化
    • 评论功能编辑器转Markdown编辑器
    • 三、主要使用技术
      • 前端引擎art-template+express-art-template
      • Express
      • MongoDB
      • body-parser插件(处理post参数)
      • formidable插件(解析上传文件数据)
      • bcrypt插件(对密码进行加密)
      • mongoose-sex-page模块(分页功能实现)
      • Joi插件(数据校验)
      • express-session(用来存储登录用户的sessionId)
      • 四、快速上手Express+MongoDB Express的使用 4.1 Express
        Express是一个基于Node平台的web应用开发框架,它提供了一系列强大的特性,来帮助我们创建各种Web应用。
        首先,我们要先安装Express。
npm i express

然后,我们利用express来创建网站服务器,以便后面进行页面交互:
//引入express框架 const express = require('express'); //创建网站服务器 const app = express(); //监听端口,这边监听的是8080端口,即可以通过localhost:8080/访问该项目中的文件 app.listen(8080):

4.2 Express的中间件
中间件就是一堆方法,Node提供中间件可以拦截请求,从而对请求做出响应,或者是将请求交给下一个中间件。
以下是我看见过的一个关于中间件的示意图,表示的很清楚:

art-template+Express+MongoDB小项目
文章图片
中间件
而中间件主要由两个部分构成,即 中间件方法和 请求处理函数。中间件方法由Express提供,用来拦截请求;请求处理函数由程序员编写,用来处理请求。
比如 app.use(path,function(req,res,next))。第一个参数 path是要请求的路径,第二个参数是对应的请求处理函数,这个 function又包括三个参数,即 reqresnextreq是请求; res是响应; next是一个方法,因为我们在程序中可能不止使用一个中间件, next方法就是用来放行请求,跳到下一个中间件的,这个参数在要用到的时候写,如果没用到可写可不写。
在这次项目中,我主要使用了几个中间件:app.use()app.get()app.port(),还有内置中间件express.static(),这几个中间件的作用大都有些不同。
app.use(path,function(req,res,next))——向页面path发出请求时(无论是什么请求),调用后面那个请求处理函数。
app.get(path,function(req,res,next))——向页面path发出get请求时,调用后面那个请求处理函数。
app.port(path,function(req,res,next))——向页面path发出get请求时(表单提交数据),调用后面那个请求处理函数。
app.use(express.static(path.join(__dirname,'public)))——开放静态资源文件,这样就可以用url访问项目中的图片、css等静态资源了(public页面用来存放静态资源)。
4.3 项目中的路由文件
在项目中,我把所有的前端页面放在了views文件夹中,所有的路由处理逻辑放在了route文件夹中。而后,根据项目前后台,进一步区分admin(后台)和home(前台)两个文件夹。

art-template+Express+MongoDB小项目
文章图片
目录
为了使文件彼此之间依赖关系更加清晰,将所有的路由处理函数都封装成一个路由处理模块,然后将这个模块一整个代入。根据前后台功能,将路由处理分成两个主要文件, admin.js负责处理对后台系统页面的请求, home.js负责对前台页面的请求。于是在 app.js中这样写:
//导入两个主要的路由处理逻辑 const admin = require('./route/admin'); const home = require('./route/home'); //当访问localhost:8080/admin时会向admin发出请求,home同上 app.use('/admin',admin); app.use('/home',home);

而后,admin.js中通过express.Router()创建路由,随后用中间件来拦截页面请求并对应相应的路由处理模块。最后,不要忘记了将一整个模块导出,即module.exports = admin。文件中这样写:
// 后台管理 //引用express框架 const express = require('express'); //创建博客展示页面路由 const admin = express.Router(); //渲染登录页面 admin.get('/login',require('./admin/loginPage')); //接收登录页面的数据并进行判断 admin.post('/login',require('./admin/login')); //创建用户列表路由 admin.get('/user',require('./admin/userPage')); //实现退出功能 admin.get('/logout',require('./admin/logout')); // 1.用户管理 // 创建用户编辑页面路由 admin.get('/user-edit',require('./admin/user-edit')); //创建实现用户添加功能路由 admin.post('/user-edit',require('./admin/user-edit-fn')); // 用户修改路由 admin.post('/user-modify',require('./admin/user-modify')); // 删除用户功能路由 admin.get('/delete',require('./admin/delete')); // 2.文章管理 // 文章列表页面路由 admin.get('/article',require('./admin/article')); // 文章编辑页面路由 admin.get('/article-edit',require('./admin/article-edit')); // 文章添加功能路由 admin.post('/article-edit',require('./admin/article-add')); //用户修改路由 admin.post('/article-modify',require('./admin/article-modify')); // 删除文章功能路由 admin.get('/article-delete',require('./admin/article-delete')); module.exports = admin;

home.js就不在这里列出了,可自行到Github查看。
MongoDB的使用 4.5 MongoDB
MongoDB就是一个数据库,选择它的好处很多,其中之一就是:MongoDB不需要显式创造数据库,如果正在使用的数据库不存在,MongoDB会自动创造。
操作MongoDB需要第三方模块mongoose,很多要使用的函数(包括常规的增删查改)都在mongoose里面。
首先,我们要先安装MongoDB。在命令行输入:
npm i mongoose

然后启动数据库:
net start mongodb

停止数据库连接是:
net stop mongodb

最后,连接数据库。
const mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/playground') .then(() => console.log('数据库连接成功')) .catch(err => console.log('数据库连接失败',err));

注意!!! MongoDB默认是没有账户和密码登入的。如果想知道如何有账户和密码地访问数据库,指路-->MongoDB设置用户名密码https://www.jianshu.com/p/237a0c5ad9fa。
4.5 项目中使用的数据库表
我将本次项目所需的创建数据库表以及连接数据库等逻辑放在model文件夹中。根据需求,本次项目需要建三张表,分别是用户user、文章article、评论comment,对应user.jsarticle.jscomment.js。对应字段可直接前往代码查看。
4.6 MongoDB的创建集合和增删查改
4.6.1 创建集合 创建集合分两步:1.对集合设定规则;2.创建集合。
这里拿user.js来举例:
//1.创建集合规则 const UserSchema = new mongoose.Shema({ username:{ type:String,//type——值类型 required:true,//require——为true为必填项 minlength:2,//最小字符串长度 maxlength:20//最大字符串长度 },//用户名 email:{ type:String, // unique为true是为了保证邮箱地址不重复,因为要用这个作为登录名 unique:true, required:true },//邮箱 password:{ type:String, required:true },//密码 role:{ type:String, required:true },//角色,admin是超级管理员,normal是普通用户 state:{ type:Number, default:0//默认值为0 }//状态,0——启用,1——禁用 }); //2.创建集合(表)User const User = mongoose.model('User',userSchema); //最后记得导出该集合,否则其他路由模块不能使用 module.exports = User;

这边注意一点!!创建集合后,我们在数据库中不一定能看见它。只有当集合里面有数据时,集合才能显式被创建。
4.6.2 增删查改 以下是项目中用到的一些数据库的增删查改的语句:
function createUser(){ //增加一条文档。 const salt = await bcrypt.genSalt(); const pass = await bcrypt.hash('123456',salt); const user = awaitUser.create({ username:'iteheima', email:'123456@qq.com', password:pass, role:'admin', state:0 }); User.find().then(result => console.log(result)); //查询User中所有文档 User.findOne({_id:id}); //查询User表中_id值为id(条件)的文档。 User.findOneAndDelete({_id:id}); //删除一条_id值为id的文档 User.updateOne({_id:id},{ //更新_id值为id的文档,更新数据见第二个参数 username:username, email:email, role:role, state:state });

五、快速上手art-template 前端渲染就是把数据渲染到前端页面上去,一般来说,有以下三种方法:
  1. JS原始语法渲染
  2. art-template等前端渲染引擎
  3. Vue模板语法。
【art-template+Express+MongoDB小项目】这里我使用的是art-template模板引擎。
art-template 是一个简约、超快的模板引擎,采用作用域预声明的技术来优化模板渲染速度,从而获得接近 JavaScript 极限的运行性能,并且同时支持 NodeJS 和浏览器。
首先,是安装art-template,为了更好地支持art-template在Express中的使用,我也安装了express-art-template。
npm i art-template express-art-template

然后,进行一些配置:
//导入art-template const template = require('art-template'); const path = require('path'); //告诉浏览器当渲染后缀为art的模块时所使用的模板引擎 app.engine('art',require('express-art-template')); //告诉express框架模板所在的位置 app.set('views',path.join(__dirname,'views')); //告诉express框架模板的默认后缀 app.set('view engine','art');

这样,我们就可以使用art-template模板语法进行前端渲染了,接下来将讲一些art-template在本项目中的运用。
首先,要知道,在项目中是有一些公共模块的,比如头部、尾部、导航栏等等。为了减少代码的冗余,也为了减少我们编写的代码量,art-template提供了模板继承和子模板套用两种方法,然后我把一些公共模块放在了对应的common文件夹里。
common文件夹目录如下:

art-template+Express+MongoDB小项目
文章图片
common目录
layout.art:页面骨架,用来放置页面公共部分,比如一些 等文件。
Blog - Content Manager {{block 'link'}}{{/block}}{{block 'main'}} {{/block}}{{block 'script'}} {{/block}}

header.artaside.art:页面公共部分,是头部和侧边导航栏。

art-template+Express+MongoDB小项目
文章图片
页面布局
然后,在对应页面中继承 layout.art并插入子模板 header.artaside.art
例如, user.art(具体内容自见代码):
{{extend './common/layout.art'}}{{block 'main'}}{{include './common/header.art'}}{{include './common/aside.art'}}用户{{userInfo.username}} 找到{{count}}个用户 新增用户 ...
    ...
...{{/block}}{{block 'script'}}{{/block}}

六、相关插件 6.1 body-parser body-parser也是一个Express的中间件,它主要是用来解析post提交过来的普通请求参数的。
它的使用如下:
//引入body-parser模块 const bodyParser = require('body-parser'); //配置body-parser模块,记得写在所有中间件前面 app.use(bodyParser.urlencoded({extended : false}));

这样就可以在post提交请求以后在req.body访问到表单提交的数据了。
但是,body-parser只能解析普通表单数据,向项目中有文件上传功能的,body-parser是没法解析的,req.body会是一个空对象。所以我们要使用formidable第三方模块。
6.2 formidable formidable第三方模块也是用来解析表单的,它支持get、post请求参数,和文件上传功能。
首先,先安装formidable,在命令行输入:
npm i formidable

然后,在需要使用文件上传的表单数据的路由模块中:
//1.引入formidable const formidable = require('formidable'); //2.创建表单解析对象 const form = new formidable.IncomingForm(); //3.设置文件上传路径 form.uploadDir = path.join(__dirname,'../','../','public','uploads'); //4.保留表单上传文件的扩展名 form.keepExtensions = true; //5.解析表单 form.parse(req,async (err,fields,files) => { //fields用来访问普通表单对象,比如fields.title //files用来访问上传文件,files.cover.path就是上传文件的绝对路径 ... });

读取文件:
//1.创建文件读取对象 var reader = new FileReader(); //2.读取文件 reader.readAsDataURL(this.files[0]); reader.onload = funtion(){ //将文件读取结果保存在页面中,以便显示 preview.ssrc = https://www.it610.com/article/reader.result; }

6.3 bcypt bcypt是用来给密码加密的,数据库中的密码原是按照明文存储的,这是极其不安全的。因此我们使用bcypt来对密码进行加密然后存储到数据库中。
首先,安装bcypt,在命令行中输入:
npm i bcyptjs

我们这个项目中需要使用到密码加密的地方就是添加用户,当我们在表单中填写要添加用户的信息后点击提交,表单数据就会被保存在req.body传递给相对应的路由处理模块。路由处理模块中,我们将加密后的密码替换req.body.password并在后面保存到数据库中去。
可见/route/user-edit-fn.js
//1.导入bcypt const bcypt = require('bcryptjs'); //2.生成随机字符串 const salt = bcrypt.genSalt(10); //3.使用随机字符串对密码进行加密 const password = bcypt.hash(req.body.password,salt); //把加密后的密码替换给req.body.password req.body.password = password;

在登录页面中,也有使用到bcrpt的地方:我们填入账户密码以后,要对密码进行比对,这时调用bcypt中的compare方法将输入的密码req.body.password和数据库中的密码password进行比对:
const bcrypt = rquire('bcrypt'); const password = req.body.password; let isValid = bcypt.compare(password,user.password);

6.4 mongoose-sex-page 这个插件是用来实现数据分页功能的,导入这个插件以后生成的集合构造函数包括以下内容(json格式):
{ "page":1,//当前页 "size":2,//每页显示数据条数 "total":8,//总共的数据条数 "records":[ //这里面是查询出来的具体数据 { "_id":"5c...", "title":"ceshi" } ], "pages":4,//总共多少页 "display":[1,2,3,4]//客户端显示的页码 }

本项目中的具体使用可见/route/article.js路由模块和/views/article.art
//article.js //1.导入mongoose-sex-page const pagination = require('mongoose-sex-page'); //2.导入数据库 const {Article} = require('../../model/article'); module.exports = async (req,res) => { // 客户端传递过来的当前页码 const page = req.query.page; //locals这个对象可以把添加的属性传递到客户端 req.app.locals.currentLink = 'article'; // page方法指定当前页面 // size方法指定每页显示的数据条数 // display方法指定客户端要显示的页码数量 // exec方法向数据库中发送查询请求 // 查询所有文章数据 let count = await Article.countDocuments({}); let articles = await pagination(Article).find().page(page).size(2).display(3).populate('author').exec(); //res.send(articles.records); //return; res.render('admin/article.art',{ articles:articles, count:count }); }

此时传递到article.art页面上的数据articles,具体分页好以后的数据在article.records里面。
6.5 Joi
Joi是JS对象的规则描述和验证器。
在项目中我用它来验证数据库的数据是否符合要求。安装也很简单,命令行输入npm i Joi就可以了。
然后我创建一个验证器,并且将它默认导出,这样在需要验证数据的地方我就可以直接导入这个验证器来使用了,如果验证没有通过会出现一个错误,我们只要判断是否捕捉到错误就可以了。
//model/user.js const Joi = require('Joi'); ... const validateUser = user => { //定义验证规则 const schema = { username:Joi.string().min(2).max(12).required().error(new Error('用户名不符合验证规则')), email:Joi.string().email().required().error(new Error('邮箱格式不符合要求')), // regex(正则表达式),范围在a-z、A-Z、0-9中,最短3,最长30 password:Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/).required().error(new Error('密码格式不符合要求')), // valid方法告诉客户端只传递normal和admin,除此之外都是不合法的 role:Joi.string().valid('normal','admin').required().error(new Error('角色值非法')), state:Joi.number().valid(0,1).required().error(new Error('状态值非法')) }; //实施验证 return Joi.validate(user,schema); } ... module.exports = { User, validateUser }

6.6 express-session 在登录的时候有这样一个问题,我们在login.art输入账户和密码,然后根据代码,会自动跳转到用户列表页面。然而,我们是否真的登录了呢?其实,即使我们登录成功了,但是下一次访问服务器的时候,服务器依然不认得。因此,这里涉及到关于cookiesession的知识点。
cookie是浏览器为页面开辟的存储空间,session是服务器为访问的用户开辟的一个存储空间。当用户访问服务器的时候,需要服务器生成一个sessionId来唯一标识用户身份,并把这个sessionId存储在客户端cookie里面。然后,在下一次访问服务器的时候,带着这个sessionId去访问服务器,然后服务器才响应该用户登录后才能获取的信息。在这里,我们使用express-session来实现session功能。
//app.js // 导入express-session const session = require('express-session'); //配置session app.use(session({ secret:'secret key', saveUnitialized:false, cookie:{ maxAge:24*60*60*1000 } }));

七、踩坑记 7.1 安装bcrypt后,require(bcypt)出错。 在使用bcrypt的时候出错【笑哭】因为我原来是这样写地,安装是npm i bcrypt然后导入的是require('bcrypt')。但其实在window下要安装bcryptjs,然后require('bcryptjs')暂时还不懂其中的原理,可能由于node.js版本不同?
7.2 文章修改,没有上传文件点击修改后图片不显示。 在文章编辑页面中,预期的功能是,如果我没有重新上传封面图片,图片就默认是第一次创建文章数据时上传的图片。但是在formidable中,不管原来是否有图片,只要我没有上传图片,他就默认files为空,然后自己会生成1个0kb的图片,导致我不重新上传一张图片就点击提交文章封面图片就会为空。
因此我在路由模块article-modify.js中添加了一个判断:如果我没有重新上传图片,files.cover.name就会为空,那我就直接取数据库中的cover然后更新。
let cover = ""; if(!files.cover.name){ let article = await Article.findOne({_id:id}); cover = article.cover; } else { cover = files.cover.path.split('public')[1]; } // res.send(cover); await Article.updateOne({_id:id},{ title:fields.title, author:fields.author, publishDate:fields.publishDate, cover:cover, content:fields.content });

项目地址 项目地址:Github->https://github.com/molifenge/selfblog
运行项目:
nodemon app.js

然后,在浏览器输入localhost:8080/admin/login即可访问。

    推荐阅读