22年五月毕设

毕业设计(学术垃圾):
「软件icestone_毕业设计」https://www.aliyundrive.com/s/scWhCZQFj57
点击链接保存,或者复制本段内容,打开「阿里云盘」APP .


毕业设计(论文)任务书

个人博客系统搭建
(基于前后端的内容管理系统)
作者:icestone
专业名称:软件技术
指导老师:***
完成时间:2022年5月1日

目录
第一章:绪论 3
1.1 blog系统的设计缘由 3
1.1.2blog的设计方向 3
1.1.3本次blog系统的期望 3
第二章:摘要 4
2.1摘要 4
第三章:系统总体设计 5
3.1开发环境的选择与配置 5
3.2后端服务的选择 5
3.2.1前端框架与渲染插件 5
3.3远程开发的选择 6
3.3.1服务器端配置 6
3.3.1远程开发的ide及其他开发工具 6
3.4.1后端框架 7
3.4.2后端数据处理及层级结构 7
3.4.3监听配置 7
3.5前端 7
3.5.1前端框架的选择 7
3.5.2移动端适配的处理 8
3.5页面的ui美化 8
第四章:系统的具体实现及细节 9
4.1后端接口的实现 9
4.1.1数据库及服务端口配置 9
4.1.2后端文件的层级结构: 11
4.1.3对应页面返回的实现 13
4.1.4index页面渲染并返回的实现 13
4.1.5markdown页面的查找并渲染返回 17
4.1.6person接口的数据校对及渲染返回 22
4.1.7wallpaper界面的接口实现 24
4.1.7通用API数据的处理并返回 27
4.1.8用户所有文章的返回实现 28
4.1.9文章修改的实现 29
4.1.10文章点赞的自增 31
4.1.11文章的下架,发布与删除的实现 31
4.2前端技术栈的使用与细节实现 32
4.2.1页面的ui风格统一 32
4.2.1页面逻辑和数据的交互 33
4.2.2页面数据的渲染 33
第五章:小结 35

第一章:绪论
1.1 blog系统的设计缘由
Blog 是Weblog的简称 Weblog 其实是Web和Log的组合词 Web 指World Wide Web 当然是指互连网了;Log的原义则是"航海日志" 后指任何类型的流水记录合在一起来理解。
它通常是由简短且经常更新的帖子(Post)所构成 这些张贴的文章都按照年份和日期倒序排列 Blog的内容和目的有很大的不同 Blog的内容和目的有很大的不同 从对其他网站的超级链接和评论。有关公司、个人、构想的新闻到日记、照片、诗歌、散文 甚至科幻小说的发表或张贴都有。
许多Blogs记录着blog个人所见、所闻、所想,还有一些Blogs则是一群人基于某个特定主题或共同利益领域的集体创作 撰写这些Weblog或Blog的人就叫做Blogger或Blog writer。
博客存在的方式:一般分为三种类型:一是托管博客无须自己注册域名、租用空间和编制网页,博客们只要去免费注册申请即可拥有自己的博客空间 是最"多快好省"的方式。二是自建独立,网站的博客有自己的域名、空间和页面风格 需要一定的条件。三是附属博客,将自己的博客作为某一个网站的一部分(如一个栏目、一个频道或者一个地址) 这三类之间可以演变 甚至可以兼得一人拥有多种博客网站。
1.1.2blog的设计方向
由于作者在学习技术的路上,需要记载很多bug,技术的使用知识等笔记,所以选择开发博客系统用以记载笔记,并实现网站功能的自定义与网站的扩展。博客系统是一个共享学习、生活经验以及分享个人心得的社交平台,通过博客人们可以在相互交流中提升个人能力,也可以通过欣赏他人文章开阔个人视野。随着博客系统的发展,越来越多具有写作热情或者想要记录生活经验的人不仅仅要求博客系统的功能完善,更是对博客系统的稳定性、安全性、美观性、响应速度等方面提出了较高的要求;同时,随着移动设备的迅猛发展,当前时代下的博客系统也应该能良好的适应移动平台。
1.1.3本次blog系统的期望
而此次的项目我选择的是第二种,自建独立,使用第一种的话对个人而言是很方便的.但是在学习技术的路上,我记载了很多笔记,在第三方网站查看时又需要忍受他们的条条框框,如:看广告,开会员实现某些功能等等,这是让人难以忍受的.第三种的附属博客可能会麻烦到他人来帮助我挂载自己的博客,舍弃。如果自己开发网站,可以轻松实现各种功能的自定义,方便日后查看自己记载的一些技术栈,更有利于实践自己的开发技术。所以,作为一名开发人员,此次我选择自己搭建前后端并部署在服务器端实现在线编辑,发表,查看,评论等功能的博客网站。
第二章:摘要
2.1摘要
该博客网站设计的目的旨在建立以express和mongo dB数据库为工具,功能简洁、结构灵活且精致、轻巧的个人博客网站系统。其中网络日志的管理作为本系统的主要目标,且同时可以满足用户对于博客文章,留言,个人信息等的管理和展示,其他浏览者评论,并且管理员用户可以设置博客的背景等功能。
在编写中更追求博客系统功能的后续扩展与日常维护与代码的易懂。

第三章:系统总体设计

3.1开发环境的选择与配置
  为了方便在测试期间的使用,网站部署在服务器端,部署的系统是centos8。
由于网站对带宽要求不大,仅需求一定的存储即可,所以我选择的是1核,2G内存40G系统盘的轻量级应用服务器。

3.2后端服务的选择
在数据持久化中我选择mongo dB作为后端数据库,前端使用轻量级的jquery进行代码逻辑开发,使用express作为路由接管。关于页面渲染,有后端渲染的,也有前端渲染的,以达到搜索引擎的seo优化。在主页面,也就是/index页面采用的是前端渲染,这样,在百度/必应或者google收录时会收录主页的文章列表,用以减轻服务器端的压力;在文章的具体页面,由于渲染的数据较少,所以采用服务器端渲染。采用两种方式渲染,是为了方便在网页返回中,有些组件,例如标题菜单,有时会更改,需要从数据库中获取数据并渲染返给客户端。

3.2.1前端框架与渲染插件
由于jquery的轻量化,简便,又有大量稳定的插件与其配合,所以前端选择了jquery来进行逻辑代码的编写。这里还使用了jquery-template作为前端的渲染插件,它易于使用,渲染速度可靠。
Markdown渲染模块是重中之重,这里选择的是marked.js,它不依赖于第三方库,又可以配合heightlight.js实现代码的高亮,易于使用,api接口强大,因此选择它作为markdown渲染模块。在前端渲染中,使用的是隐藏的div来填充markdown文件的value,并使用marked.js实例,将value渲染为html,这样大大减少了文件渲染的难度。更有利的是,它可以使用自定义的less来实现markdown文件的样式覆盖
3.3远程开发的选择
3.3.1服务器端配置
服务器端使用域名绑定host,国内主机,使用时已经备份了域名。远程使用密钥链接,确保了个人使用的安全性,防火墙暂时只开启了80和一些必要使用的端口,其中有22和mongo dB远程连接的27017端口等。
3.3.1远程开发的ide及其他开发工具
远程连接使用命令行的话,作者选择的是xshell,学生免费而且受用人群广,配合xftp使用起来还是很方便的。在远程ide上作者选择的是webstrom来部署服务器,ide支持的插件丰富,快捷键齐全,补全功能强大。
为了方便开发,在远程工作时使用的时root账户,配置mongo dB和NodeJS环境变量,在xshell中开启一个mongo dB的screen,将mongo dB作为后台服务常驻运行;在另一个screen中运行网络服务。
3.4.1后端框架
后端备选的框架有koa和express,都是基于nodejs和webpack的非传统后端框架,两者各有优缺点。由于koa适合接口api较多的项目,而此次blog系统所需的api并不算多;koa框架本身所挂载的对象不如express多,如果要获取request,发送result等功能都需要依赖其他js包。express并没有比koa那么依赖middleware,在这个blog系统中所需的middleware验证并不多,且express自身支持的api比koa更多,所以本次选择的是express作为使用框架。
3.4.2后端数据处理及层级结构
另一方面,express使用的是回调函数来return和render数据,该blog系统中并没有必要区分出controller,service,model等层级结构,编写起来更为快捷。
这里的层级设计了schema,来用以构建数据库的表模型;router,用以负责各个页面的路由挂载和后端增删改查的逻辑处理和数据返回的处理;views层级作为前端的页面模板,其中将共同的部分代码作为公共template来复用。前端页面所需的image资源存放在/images中,less,svg,js,js库等所需的静态资源存放在开放的静态目录public中。
主要的服务器监听文件为,main.js,将所需的路由,配置等通过use引入在main.js中,来实现代码的简洁。
3.4.3监听配置
在监听方面,监听了前端页面请求的同时,对于错误资源的请求也监听并返回404页面。对于一些他人恶意的请求同时记载在了数据库中,例如他人对服务器资源结构进行扫描,或者对主机80端口下开启服务的请求,都会写入到数据库中,在查看时,可以通过admin账户进行查看。

3.5前端
3.5.1前端框架的选择
前端的预备选择有vue和jquery来进行开发,但是由于项目已经使用jquery开发近半,逻辑代码较多,并不那么容易更换框架。前端页面所需渲染的数据大部分都是不变的,并不是那么依赖数据驱动来进行页面更新,更多的是逻辑交互和单一页面数据的渲染。Vue更依赖于vueCli来进行编写,且依赖一个个组件,显然不如jquery来的轻便,所以blog系统使用jquery来进行开发。

3.5.2移动端适配的处理
这里选择的是bootstrap来进行页面的适配,在页面模板中的header中引入,并使用的是bootstrap官方推荐的页面结构,禁止缩放。为了适应页面的各个元素大小,以及全局字体大小,这里选择使用rem来配置元素大小,并为body设置统一的fontsize大小。对于一些页面高度可能会发生变化的页面,这里选择使用隐藏侧边的滚动条来解决页面宽度可能发生变化而不美观。
对于一些特定的自己编写的元素,在移动端与pc端显示的可视宽度和高度不同而导致它的缩放比例不同,作者这里选择为其编写不同的less样式,并使用less的媒体查询来匹配样式。对于一些list,在pc端时,是横向排列;但是在移动端可能是没有足够的宽度,那么就需要使用竖向排列,这里没有为其编写不同的less样式,而是使用的flex布局来实现自动适应宽度。
3.5页面的ui美化
对于用户而言,页面的是否美观与富有设计感是对网站的第一感受。想要获得有设计感的网页,但是介于自身并没有设计相关的知识,所以在此项目中,我选择使用第三方的ui框架,大量减少了我在项目中的页面细节设计。
页面的ui这里有一些统一的配置,例如div元素的radio统一设置为0.5rem,页面中所有的button元素,统一设置box-shadow与hover样式。项目中使用的div,大部分使用bootstrap中的卡片,页面中可以重复使用的样式,都做了封装,在需要使用时,直接调用类名即可复用。
对于所有页面公共的less样式,使用一个单独的less文件封装,在所有页面的公共模板中引用。同时在公共模板中也引入了reset.css,作出样式的初始化,避免默认样式影响到所需样式的调试。
对于页面一些元素的出现效果,这里使用的是animate和scrollmagic来进行配置,最初考虑使用jquery自带的动画,但是由于jquery默认的动画过于单调;所以这里使用的是scrollmagic在主页使用,绑定元素出现在页面时执行出现动画,对于一些不需要固定高度的元素使用的是animate。

第四章:系统的具体实现及细节
4.1后端接口的实现
4.1.1数据库及服务端口配置
在/etc/profile中配置即可,这里把/根目录作为mongo dB的data和log存储:

  1. mongod环境变量

  2. export MONGODB_HOME=/usr/local/mongodb
  3. export PATH= P A T H : PATH: PATH:MONGODB_HOME/bin

在开发过程中,有许多数据库插入,删除的post,在本地查看时,为了方便,使用的是Datagrip进行远程连接,使用ssh通道。在这里使用27017端口的如果没有设置密码,那么可能会被他人端口扫描后删除数据。
该项目使用一个数据table,文章数据,用户数据,admin用户,评论数据存储在不同的collection中。在项目开发中,并不需要自己使用命令行去创建一个个collections,这里使用的是mongoose的schema,在使用时去create,例如文章的schema:

  1. //markdown文章的schema
  2. const mongoose = require(‘mongoose’);
  3. //新建一个schema
  4. // import mongoose from “mongoose”;
  5. //markdown模型
  6. var Schema = mongoose.Schema({
  7.  markdownId: Number,  
    
  8.  filename: String,  
    
  9. filepath: String,  
    
  10. useremail: {  
    
  11.     type: String,  
    
  12.     default: '[email protected]'  
    
  13. },  
    
  14. //在index中显示在div上的文章描述  
    
  15. category: {  
    
  16.     type: String,  
    
  17.     default: '没有'  
    
  18. },  
    
  19. author: {  
    
  20.     type: String,  
    
  21.     default: 'icestone'  
    
  22. },  
    
  23. authorId: Number,  
    
  24. //文章的标签,一般不止一个,为了给文章细分  
    
  25. tag: {  
    
  26.     type: String,  
    
  27.     default: ''  
    
  28. },  
    
  29. tag2: {  
    
  30.     type: String,  
    
  31.     default: ''  
    
  32. },  
    
  33. tag3: {  
    
  34.     type: String,  
    
  35.     default: ''  
    
  36. },  
    
  37. fileData: {  
    
  38.     type: String,  
    
  39.     default: '这是文章内容'  
    
  40. },  
    
  41. description: String,  
    
  42. //头像  
    
  43. avatar: {  
    
  44.     type: String,  
    
  45.     default: '/public/images/usericons/avatar-default.png'  
    
  46. },  
    
  47. createTime: {  
    
  48.     type: Date,  
    
  49.     default: Date.now  
    
  50. },  
    
  51. praise: {  
    
  52.     type: Number,  
    
  53.     default: 0  
    
  54. },  
    
  55. //最后一次点赞的时间,防止有人无聊一直刷赞  
    
  56. praiselasttime: {  
    
  57.     type: Date,  
    
  58.     default: Date.now  
    
  59. },  
    
  60. status: {  
    
  61.     type: Number,  
    
  62.     //默认发布status为1  
    
  63.     //未发布为-1  
    
  64.     //删除为-2  
    
  65.     default: 1  
    
  66. },  
    
  67. //文章权重  
    
  68. weight: {  
    
  69.     type: Number,  
    
  70.     default: 0  
    
  71. },  
    
  72. //浏览量  
    
  73. views: {  
    
  74.     type: Number,  
    
  75.     default: 0  
    
  76. }  
    
  77. });
  78. module.exports = mongoose.model(‘file’, Schema, ‘markdownFile’);
    这里定义字段时只用提供字段的type,default Value,并导出schema对象即可,在后端使用时,连接数据库后,require导入对象即可使用查询语句进行增删改查。

为了后端api开发过程中方便调试,开发和线上运行时一直使用的是dev模式,这里使用nodemon 达到效果,nodemon是nodejs的一个第三方库,用以在服务启动时,监听除了node_modules文件夹之外的js文件的改动。例如router文件或是页面的js文件每一次改动,都会触发服务重启,例如:

  1. ^C[root@kirin nodeserver]# nodemon app.js
  2. [nodemon] 2.0.15
  3. [nodemon] to restart at any time, enter rs
  4. [nodemon] watching path(s): .
  5. [nodemon] watching extensions: js,mjs,json
  6. [nodemon] starting node app.js

使用screen将mongo dB和nodejs挂在后台长时间运行:

  1. [root@kirin nodeserver]# screen -ls
  2. There are screens on:
  3.  2898.nodetest   (Attached)    
    
  4.  1365.easeblog   (Attached)    
    
  5.  1270.mongodb    (Detached)    
    
  6. 3 Sockets in /run/screen/S-root.
    为了方便pc端使用浏览器进行测试访问,这里使用的是80端口。

4.1.2后端文件的层级结构:
在开发文件中,使用webpack进行包管理并开发,在该项目中服务器启动文件在最外层,其他文件加为页面模板或者是静态资源文件夹:

  1. [root@kirin nodeserver]# ll
  2. total 1228
  3. -rw-r–r-- 1 root root 6430 Apr 2 13:32 app.js
  4. drwxr-xr-x 2 root root 6 Dec 14 21:33 attachment
  5. -rw-r–r-- 1 root root 201 Dec 19 21:29 err.txt
  6. -rw-r–r-- 1 root root 165662 Apr 1 09:29 favicon.ico
  7. drwxr-xr-x 2 root root 36864 Nov 22 23:25 images
  8. drwxr-xr-x 630 root root 20480 May 12 21:11 node_modules
  9. -rw-r–r-- 1 root root 959 Apr 2 13:27 package.json
  10. -rw-r–r-- 1 root root 573997 May 12 21:11 package-lock.json
  11. drwxr-xr-x 11 root root 122 Apr 1 09:32 public
  12. -rw-r–r-- 1 root root 50 Apr 1 09:29 robots.txt
  13. drwxr-xr-x 5 root root 4096 Apr 1 09:33 router
  14. -rw-r–r-- 1 root root 1120 Apr 1 09:29 server.js
  15. drwxr-xr-x 4 root root 53 Nov 6 2021 source
  16. drwxr-xr-x 2 root root 4096 Apr 1 09:39 views
  17. -rw-r–r-- 1 root root 327 Apr 1 09:29 webpack.config.js
  18. -rw-r–r-- 1 root root 180939 Apr 1 09:29 yarn-error.log
  19. -rw-r–r-- 1 root root 199227 May 12 21:11 yarn.lock
    其中主要的文件夹及文件:
    node_modules,package-lock.json,package.json,webpack.config.js,yarn.lock和yarn-error.log是webpack的第三方包和包管理配置。
    public,用于存放第三方cdn资源,字体,图片,网页模板会引用到的服务器端js或是less文件等静态资源文件;router文件夹是用来存放后端api接口的文件夹;images用来存放markdown所引用的图片;views存放的是前端会用到的页面模板或者是开发中使用到的测试页面。app.js是服务器的监听端口文件,引用router文件并一些后端的开发插件配置。server.js用来测试后端api代码,attachment是用来存放前端post的测试文件。
    由于在app.js中引用的有不同的层级结构,所以为了方便开发与辨别,这里对不同的router,存放在不同的文件夹下:
  20. [root@kirin nodeserver]# cd router/
  21. [root@kirin router]# ll
  22. total 132
  23. -rw-r–r-- 1 root root 188 Apr 1 09:32 blackPage.js
  24. -rw-r–r-- 1 root root 14610 Apr 3 23:26 edit.js
  25. -rw-r–r-- 1 root root 3602 Apr 1 09:32 getcopy.js
  26. -rw-r–r-- 1 root root 5353 Apr 1 10:40 index.js
  27. -rw-r–r-- 1 root root 2645 Dec 19 20:55 indexRouter.js
  28. -rw-r–r-- 1 root root 1636 Apr 1 09:32 logDelete.js
  29. -rw-r–r-- 1 root root 6719 May 13 16:49 loginAndRegister.js
  30. -rw-r–r-- 1 root root 2713 Apr 1 09:32 markdownEvents.js
  31. -rw-r–r-- 1 root root 8269 May 13 17:37 markdown.js
  32. -rw-r–r-- 1 root root 1453 Apr 1 09:32 moongooseDBQuery.js
  33. -rw-r–r-- 1 root root 6173 Apr 1 09:32 newtab.js
  34. drwxr-xr-x 3 root root 304 Apr 4 09:36 node_modules
  35. -rw-r–r-- 1 root root 4279 Apr 1 11:34 router.js
  36. drwxr-xr-x 2 root root 239 Apr 1 09:32 schemaTest
  37. -rw-r–r-- 1 root root 699 Apr 2 13:15 session.js
  38. -rw-r–r-- 1 root root 1605 Apr 1 09:32 tags.js
  39. -rw-r–r-- 1 root root 912 Apr 1 09:32 test.js
  40. drwxr-xr-x 2 root root 4096 Apr 1 10:21 tools
  41. -rw-r–r-- 1 root root 2138 Apr 1 09:33 UserFileList.js
  42. -rw-r–r-- 1 root root 19703 Apr 1 09:32 user.js
  43. -rw-r–r-- 1 root root 6965 May 13 17:03 wallpaper.js

每个js文件对应的是不同页面的后端api接口配置,其中有部分属于公共页面的接口配置。schemaTest是不同的数据表的schema,在实现数据库增删改查时用到。

4.1.3对应页面返回的实现
网站的页面有:index,person,markdown页面等,他们使用get请求请求不同的路由,例如:/,/person,/markdown等等,他们请求的是对应的页面,后端处理的代码分别写在对应的router文件中。
对于后端页面的渲染,这里使用的是express-art-template来进行渲染:

  1. const template = require(‘express-art-template’)
  2. //模板引擎的选项
  3. template.minimize = true
  4. template.htmlMinifierOptions = {
  5.  collapseWhitespace: true,  
    
  6.  minifyCSS: true,  
    
  7.  minifyJS: true,  
    
  8.  // 运行时自动合并:rules.map(rule => rule.test)  
    
  9.  ignoreCustomFragments: []  
    
  10. }
  11. app.engine(‘html’, template)
    在模板引擎中配置了压缩渲染的js,css文件,并指定渲染的模板文件后缀是html。
    4.1.4index页面渲染并返回的实现
    下面是对应返回首页:
  12. indexRouter.get(‘/’, function (req, res) {
  13.  res.render('index.html', {  
    
  14.      // indexDate: doc,  
    
  15.      pageName: `icestone's blog`  
    
  16.  })  
    
  17. })
    此处的indexRouter是会挂载在app.js中,当前端get请求网站的/时,后端使用render,渲染index.html这个模板,并将render函数后面中传入的对象 渲染;这里pagename对应的就是index页面的页面名称,由于index页面在请求时url中并没有其他参数,这里只需要返回渲染页面即可。
    对于前端页面的请求数据并渲染,是通过inedx.js来请求首页默认的20条数据:
  18. //indexrouter首页刷新时请求的二十条数据
  19. indexRouter.get(‘/getIndexList’, function (req, res) {
  20.  //数据查询  
    
  21.  markdownFile.find({  
    
  22.      status: {$nin: ['-1', '-2']},  
    
  23.      // weight: [0, 1]  
    
  24.  }, {  
    
  25.      fileData: 0,  
    
  26.      praiselasttime: 0,  
    
  27.     __v: 0,  
    
  28.     // _id: 0,  
    
  29.     filepath: 0,  
    
  30.     weight: 0  
    
  31. }, function (err, doc) {  
    
  32.     if (err) {  
    
  33.         res.status(201).json({  
    
  34.             err_code: - 1,  
    
  35.             inf: 'data query error'  
    
  36.         })  
    
  37.     } else {  
    
  38.         res.send(doc)  
    
  39.     }  
    
  40. }).sort({_id: - 1}).limit(20)  
    
  41. })
    这里通过get请求/getIndexList获取状态非-1或-2的20条数据并直接返回doc,前端接收doc数据并将其渲染,如果查询失败,则返回对应的status和err_code。
    由于在前端界面,用户可以点击”下一页” 来获取后面的数据,这里使用的是前端返回页面上最后一个文章list的id,后端根据此id查询后面的20条合法数据:
  42. //获取根据markdownid后面的20条数据:
  43. indexRouter.post(‘/getIndexList’, function (req, res) {
  44.  //连接数据库:  
    
  45.  if (mongoose.connection.readyState == 0) {  
    
  46.      //连接数据库  
    
  47.      mongoose.connect('mongodb://127.0.0.1:27017/markdownFile', { 
    
  48.          useNewUrlParser: true,  
    
  49.          useUnifiedTopology: true  
    
  50.      })  
    
  51.     return  
    
  52. }  
    
  53. //请求的文章id  
    
  54. const item = req.body.num  
    
  55. if (isNumber(item)) {  
    
  56.     markdownFile.find({  
    
  57.         'markdownId': {$gt: item - 21, $lt: item}  
    
  58.     }, {  
    
  59.         //不显示某个字段  
    
  60.         fileData: 0,  
    
  61.         praiselasttime: 0,  
    
  62.         __v: 0,  
    
  63.         _id: 0,  
    
  64.         filepath: 0,  
    
  65.     }, function (err, doc) {  
    
  66.         if (err) {  
    
  67.             res.static(201).json({  
    
  68.                 err_code: - 1,  
    
  69.                 inf: 'data query error'  
    
  70.             })  
    
  71.         } else {  
    
  72.             res.send(doc)  
    
  73.         }  
    
  74.     }).sort({_id: - 1})  
    
  75. } else {  
    
  76.     res.static(201).json({  
    
  77.         err_code: - 2,  
    
  78.         inf: 'Parameter error'  
    
  79.     })  
    
  80. }  
    
  81. })
    后端接受post提交的数据,这里对接受的数据进行校验,如果数据非法,则返回传参错误的status,前端根据不同的err_code来做出不同的反应。由于mongo dB默认的顺序是可以自定义的,这里使用的是_id倒序来获取数据。在20条数据中排除status为-1或-2的数据。
    Index页面在每次请求时会获取一条随机诗句,这里的诗句是存放在后端的数据库中。先请求第三方api获取一定数量的诗句,存储在数据库后再开始使用。关于请求第三方的诗句并写入到数据库:
  82. //获取诗句
  83. const poem = require(‘…/schemaTest/poem’)
  84. const request = require(‘request’)
  85. const mongoose = require(‘mongoose’)
  86. //连接数据库
  87. mongoose.connect(‘mongodb://127.0.0.1:27017/markdownFile’, {
  88.  useNewUrlParser: true,  
    
  89.  useUnifiedTopology: true  
    
  90. })
  91. let time = 100
  92. setInterval(function () {
  93. request('https://v1.jinrishici.com/all.json', function (error, response, data) {  
    
  94.     data = eval('(' + data + ')')  
    
  95.     console.log(data)  
    
  96.     let apoem = new poem({  
    
  97.             content: data.content,  
    
  98.             origin: data.origin,  
    
  99.             author: data.author,  
    
  100.             category: data.category  
    
  101.     })  
    
  102.     apoem.save(function (err, doc) {  
    
  103.             console.log(data)  
    
  104.             if (err) {  
    
  105.                     console.log(time + 'error!')  
    
  106.             } else  
    
  107.                     console.log(time + 'success!')  
    
  108.     })  
    
  109. })  
    
  110. time ++  
    
  111. }, 500)
    这里请求了第三方的json数据后解析,获取有用数据并json格式化,格式化后存入数据库,这里防止数据写入过快,设置了setInterval,在每隔几毫秒后就会再次执行插入语句。
    关于返回随机诗句:
  112. //首页随机返回一句诗句
  113. indexRouter.get(‘/rendomPoem’, function (req, res) {
  114.  poem.aggregate([  
    
  115.      {  
    
  116.          $project: {  
    
  117.              _id: 0,  
    
  118.              __v: 0  
    
  119.          }  
    
  120.      }, {  
    
  121.         $sample: {  
    
  122.             size: 1  
    
  123.         }  
    
  124.     }  
    
  125. ]).limit(1).exec(function (err, aggregateResult) {  
    
  126.     if (err) {  
    
  127.         res.send(err)  
    
  128.         return  
    
  129.     } else  
    
  130.         res.send(aggregateResult)  
    
  131. })  
    
  132. })
    这里主要是使用聚合函数来举例获取一条随机数据并返回。
    在index页面文章的最前面会放入置顶文章,这里的只当文章是在前端页面加载完后,请求后端的api接口,获取置顶文章的信息:
  133. //获取weight前置文章
  134. indexRouter.get(‘/getWeightIndexList’, function (req, res) {
  135.  markdownFile.find({  
    
  136.      status: {  
    
  137.          $nin: [- 1, - 2]  
    
  138.      },  
    
  139.      weight: {  
    
  140.          $gt: 0  
    
  141.          // $ne: [0, 1]  
    
  142.     }  
    
  143. }, {  
    
  144.     fileData: 0,  
    
  145.     praiselasttime: 0,  
    
  146.     __v: 0,  
    
  147.     _id: 0,  
    
  148.     filepath: 0,  
    
  149. }, function (err, doc) {  
    
  150.     if ( ! err) {  
    
  151.         res.send(doc)  
    
  152.     } else {  
    
  153.         res.static(201).json({  
    
  154.             err_code: - 1,  
    
  155.             inf: 'data query error'  
    
  156.         })  
    
  157.     }  
    
  158. }).sort({weight: - 1})  
    
  159. })
    这里获取weight不为0的文章数据并返回,由于置顶文章可能会有先后顺序的区别,这里用weight的数值来代表置顶文章的权重。
    4.1.5markdown页面的查找并渲染返回
    不同的是markdown页面的渲染,get请求时,会在url中携带请求的markdownId,这里后端会去获取id,并在数据库中查看该id的状态,如果状态为发布且可查看的状态,则渲染并返回该页面,否则会返回该文章不存在或已删除的页面。
    markdown页面返回的实现代码:
  160. markdownRouter.get(‘/markdown’, function (req, res) {
  161.  let querystring = req.url.split('?')[1]  
    
  162.  //%形式解析为markdown文件名  
    
  163.  //连接数据库  
    
  164.  if (mongoose.connection.readyState == 0) {  
    
  165.      mongoose.connect('mongodb://127.0.0.1:27017/markdownFile', {  
    
  166.          useNewUrlParser: true,  
    
  167.          useUnifiedTopology: true  
    
  168.      })  
    
  169.     return  
    
  170. }  
    
  171. markdownFile.find({  
    
  172.     markdownId: querystring  
    
  173. }, {}, function (err, doc) {  
    
  174.     try {  
    
  175.         //如果文章不存在  
    
  176.         if (err) {  
    
  177.             res.render('404', {  
    
  178.                 errinf: '该文章不存在或已删除!'  
    
  179.             })  
    
  180.             //如果文章状态为-1(未发布)或者未-2(已删除)  
    
  181.         } else if (doc[0].status == - 1 || doc[0].status == - 2) {  
    
  182.             try {  
    
  183.                 if (Boolean(req.session.user.email)) {  
    
  184.                     //文章处于下架状态且登录用户为该文章的拥有者时可以查看,views自增  
    
  185.                     if (req.session.user.email == doc[0].useremail) {  
    
  186.                         //浏览量自增  
    
  187.                         markdownFile.findOneAndUpdate({  
    
  188.                             markdownId: querystring  
    
  189.                         }, {  
    
  190.                             $inc: {  
    
  191.                                 views: + 1  
    
  192.                             }  
    
  193.                         }, function (err, doc) {  
    
  194.                             if (err) {  
    
  195.                                 console.log('views自增失败')  
    
  196.                             } else  
    
  197.                                 console.log('views自增success')  
    
  198.                         })  
    
  199.                         res.render('markdown', {  
    
  200.                             markdownFile: doc[0],  
    
  201.                             fileData: doc[0].fileData,  
    
  202.                         })  
    
  203.                         //不是文章的发布者时,不返回文章数据  
    
  204.                     } else {  
    
  205.                         res.render('err', {  
    
  206.                             errlog: '该文章不存在或已删除!'  
    
  207.                         })  
    
  208.                     }  
    
  209.                 } else {  
    
  210.                     res.render('err', {  
    
  211.                         errlog: '该文章不存在或已删除!'  
    
  212.                     })  
    
  213.                 }  
    
  214.             } catch (e) {  
    
  215.                 res.render('err', {  
    
  216.                     errlog: '该文章不存在或已删除!'  
    
  217.                 })  
    
  218.             }  
    
  219.         } else {  
    
  220.             //浏览量自增  
    
  221.             markdownFile.findOneAndUpdate({  
    
  222.                 markdownId: querystring  
    
  223.             }, {  
    
  224.                 $inc: {  
    
  225.                     views: + 1  
    
  226.                 }  
    
  227.             }, function (err, doc) {  
    
  228.                 if (err) {  
    
  229.                     console.log('views自增失败')  
    
  230.                 } else  
    
  231.                     console.log('views自增success')  
    
  232.             })  
    
  233.             res.render('markdown', {  
    
  234.                 markdownFile: doc[0],  
    
  235.                 fileData: doc[0].fileData,  
    
  236.             })  
    
  237.         }  
    
  238.     } catch (e) {  
    
  239.         console.log(e)  
    
  240.     }  
    
  241. })  
    
  242. })
    使用 req.url.split(‘?’)[1] 来解析url中的id参数,用以判断文章状态,并查询文章数据。这里文章每一次请求都会自增一次浏览量,由于mongoose数据库中自带的_id是结合了时间戳的格式,但是在使用中并不想用,所以这里我设置了个文章创建时的顺序id;这里的顺序id将其视为文章的身份id,在增删改查时都会用到,该id是在首页和markdown页面获取数据时都会查询并返回数据。
    对于数据库查询时成功与失败的回调,这里都会在后端输出;在页面返回时,使用render渲染查询出的数据并使用对应的模板渲染。
    在markdown页面中,有些文章会有评论,文章在渲染完成后,会发送post请求/getFileComments来获取文章的评论数据:
  243. //获取文章的评论
  244. markdownRouter.post(‘/getFileComments’, function (req, res) {
  245.  const fileId = req.body.fileId;  
    
  246.  (async () => {  
    
  247.      let result = await comments.find({  
    
  248.          markdownFileId: fileId  
    
  249.      }, {  
    
  250.          _id: 0,  
    
  251.          commentDara: 1,  
    
  252.         createTime: 1,  
    
  253.         posterEmail: 1,  
    
  254.         status: 1,  
    
  255.         registrationStatus: 1  
    
  256.     }, {limit: 6})  
    
  257.     res.status(200).json({  
    
  258.         err_code: 1,  
    
  259.         data: result,  
    
  260.         inf: '获取文章评论...'  
    
  261.     })  
    
  262. })()  
    
  263. })
    这里使用的是立即执行函数和async来返回数据,减少了代码的回调层数。有些文章到的评论可能过多,这里限制返回前六条评论数据。
    对于前端评论的接收api:
  264. //接收文章的评论
  265. markdownRouter.post(‘/markdownComments’, function (req, res) {
  266.  // console.log(req.body);  
    
  267.  const fileId = req.body.fileId  
    
  268.  const commentData = req.body.data  
    
  269.  try {  
    
  270.      //发布评论的人的邮箱  
    
  271.      const posterEmail = req.session.user.email;  
    
  272.      //获取该文章的所有者  
    
  273.     (async () => {  
    
  274.         const result = await markdownFile.find({  
    
  275.             markdownId: fileId  
    
  276.         }, {  
    
  277.             _id: 0,  
    
  278.             useremail: 1  
    
  279.         })  
    
  280.         const ownerEmail = result[0].useremail  
    
  281.         console.log(posterEmail)  
    
  282.         const comment = new comments({  
    
  283.             markdownFileId: fileId,  
    
  284.             markdownFileUserEmail: ownerEmail,  
    
  285.             commentDara: commentData,  
    
  286.             posterEmail: posterEmail,  
    
  287.             status: 1,  
    
  288.             registrationStatus: 1  
    
  289.         })  
    
  290.         comment.save((err) => {  
    
  291.             if (err) {  
    
  292.                 res.status(200).json({  
    
  293.                     err_code: - 1,  
    
  294.                     inf: '存入错误,请重试或者放弃'  
    
  295.                 })  
    
  296.             } else {  
    
  297.                 res.status(200).json({  
    
  298.                     err_code: 1,  
    
  299.                     inf: 'comment save success'  
    
  300.                 })  
    
  301.             }  
    
  302.         })  
    
  303.     })()  
    
  304. } catch (e) {  
    
  305.     //发布评论的人的邮箱  
    
  306.     const posterEmail = '不愿意透露姓名的人';  
    
  307.     //获取该文章的所有者  
    
  308.     (async () => {  
    
  309.         const result = await markdownFile.find({  
    
  310.             markdownId: fileId  
    
  311.         }, {  
    
  312.             _id: 0,  
    
  313.             useremail: 1  
    
  314.         })  
    
  315.         const ownerEmail = result[0].useremail  
    
  316.         const comment = new comments({  
    
  317.             markdownFileId: fileId,  
    
  318.             markdownFileUserEmail: ownerEmail,  
    
  319.             commentDara: commentData,  
    
  320.             posterEmail: posterEmail,  
    
  321.             status: 1,  
    
  322.             registrationStatus: - 1  
    
  323.         })  
    
  324.         comment.save((err) => {  
    
  325.             if (err) {  
    
  326.                 res.status(200).json({  
    
  327.                     err_code: - 1,  
    
  328.                     inf: '存入错误,请重试或者放弃'  
    
  329.                 })  
    
  330.             } else {  
    
  331.                 res.status(200).json({  
    
  332.                     err_code: 1,  
    
  333.                     inf: 'comment save success'  
    
  334.                 })  
    
  335.             }  
    
  336.         })  
    
  337.     })()  
    
  338. }  
    
  339. })
    这里会先获取post请求中的session,如果存在session,则评论的发布者赋予对应的email,用以辨别身份;如果没有session,则存入指定的评论者信息。由于此阶段有些信息可能并不存在,则使用try,catch来进行异常捕获。

4.1.6person接口的数据校对及渲染返回
对于person界面的返回与数据处理,需要登陆后才能查看自己的后台文章,如果密码错误或是传参数据非法,则会返回对应的res,前端获取到res再处理对应的信息:

  1. //登录
  2. loginAndRegisterRouter.post(‘/login’, function (req, res) {
  3.  const {email, password} = req.body  
    
  4.  //数据查询  
    
  5.  if (mongoose.connection.readyState == 0) {  
    
  6.      //连接数据库  
    
  7.      mongoose.connect('mongodb://127.0.0.1:27017/markdownFile', {useNewUrlParser: true, useUnifiedTopology: true})  
    
  8.      return  
    
  9.  }  
    
  10. User.find({'email': email}, function (err, doc) {  
    
  11.     //账户或密码错误  	
    
  12.     //用户存在时  
    
  13.     if (Boolean(doc[0])) {  
    
  14.         //查询数据库发现存在 那么就到个人页面  
    
  15.         // 创建session并为其设置session  
    
  16.         const user = {  
    
  17.             email: email,  
    
  18.             nickname: doc[0].nickname,  
    
  19.             icon: doc[0].avatar  
    
  20.         }  
    
  21.         req.session.user = user  
    
  22.         //密码输入正确:  
    
  23.         if (md5(md5(password)) == doc[0].password) {  
    
  24.             const user = {  
    
  25.                 email: doc[0].email,  
    
  26.                 nickname: doc[0].nickname,  
    
  27.                 icon: doc[0].avatar  
    
  28.             }  
    
  29.             res.status(200).json({  
    
  30.                 err_code: 0,  
    
  31.                 message: 'login success!',  
    
  32.                 session: user  
    
  33.             })  
    
  34.             //    密码输入不正确:  
    
  35.         } else {  
    
  36.             //密码错误,返回err_code为1  
    
  37.             res.status(201).json({  
    
  38.                 err_code: 1,  
    
  39.                 message: '密码错误!'  
    
  40.             })  
    
  41.         }  
    
  42.     } else {  
    
  43.         res.status(200).json({  
    
  44.             err_code: 3,  
    
  45.             message: '账户或密码错误'  
    
  46.         })  
    
  47.     }  
    
  48. })  
    
  49. })
    由于是post接口,这里可以使用res.body来解析并获取前端提交的数据,由于前端对于登录成功与否的判断只需要一些简单的flag作为判断,这里直接使用res.status为其设置返回的状态以及err_code作为前端判断账户状态的依据;并对不同的err_code作出不同的反应。
    用户的密码这里使用的是blueimp-md5第三方包来进行安全加密,它使用的是md5摘要加密。为了用户的安全性,在用户创建时,对其密码进行加盐并二次md5加密,以此以来,就算是数据库拥有者也无法解读用户密码。
    用户登录之后可能在很长一段时间内使用登录后的功能,这里为了用户的体验考虑,使用的是session来进行验证用户登录:
  50. //挂载session
  51. app.use(session({
  52.  //resave : 是指每次请求都重新设置session cookie,假设你的cookie是10分钟过期,每次请求都会再设置10分钟  
    
  53.  resave: true,  
    
  54.  saveUnitialized: true, //无论有没有session,都默认给你分配一把钥匙  
    
  55.  secret: 'kannimadesession', //在生成的session后面添加的字符串,和在md5密码后面加上一个字符串防止别人对比出来的同理  
    
  56.  saveUninitialized: true,  
    
  57. }))
    上面的挂载需要在app.js上挂载来使用,设置了session的有效期和session自带的盐;这里的加盐也是为了一些用户获取session字符串后解密其中存储的数据。
  58. const user = {
  59.  email: email,  
    
  60.  nickname: doc[0].nickname,  
    
  61.  icon: doc[0].avatar  
    
  62. }
  63. req.session.user = user
    上面是在登录时查询到了用户的doc文档之后,创建session并将其分发给客户端,客户端存储了用户的email,nickname,avater信息。session是一种用户凭证,session安全是十分重要的;session经过一定的时间会消失,用户在自己的person页面可以通过点击退出来清除session,同时服务器在重启时也会清除之前分发的所有session,为了避免第三方使用这里session不可以跨域使用。
    这里退出session使用get请求即可:
  64. loginAndRegisterRouter.get(‘/logout’, function (req, res) {
  65.  req.session.destroy(function (err) {  
    
  66.      res.redirect('/person')  
    
  67.  })  
    
  68. })

前端页面发送get请求,如果存在user的session,则清除session并重定向到登录页面。服务器端此时会忘掉对应的session,前端存储的session也会被摧毁。

4.1.7wallpaper界面的接口实现
在该接口中,admin可以提交图片,更改admin数据库中对应的数据,返回wallpaper界面中的壁纸文件。

  1. wallpaperRouter.get(‘/wallpaper’, function (rqe, res) {
  2.  //返回前十张图:  
    
  3.  if (mongoose.connection.readyState == 0) {  
    
  4.      //连接数据库  
    
  5.      mongoose.connect('mongodb://127.0.0.1:27017/markdownFile', {useNewUrlParser: true, useUnifiedTopology: true})  
    
  6.      return;  
    
  7.  }  
    
  8.  // res.send('测试期间,不予开放')  
    
  9.  wellpaperSchema.find({  
    
  10.     status: {$eq: 1}  
    
  11. }, {}, function (err, doc) {  
    
  12.     // console.log(doc)  
    
  13.     res.render('wallpaper', {  
    
  14.         wellpaperlist: doc  
    
  15.     })  
    
  16. }).sort({  
    
  17.         '_id': -1  
    
  18.     }  
    
  19. // ).limit(10);  
    
  20. );  
    
  21. })
    为了方便用户管理壁纸信息,这里是将所有指定的壁纸信息存储在数据库中,其中包括但不限于壁纸的文件名,上传日期,文件路径等。这里仅仅查询数据库中指定数量的壁纸信息给前端,前端页面再进行页面路径的填充。
    图片上传只有admin用户才能使用,这里的api对post请求的用户信息进行校验。将文件名,文件保存路径等数据保存在后端对应的文件夹,保存成功,则返回成功的res:
  22. //wellpaper上传:
  23. wallpaperRouter.post(‘/wellpaperUpLoader’, function (req, res) {
  24.  /* console.log('session:---'); 
    
  25.   console.log(req.session) 
    
  26.  if (mongoose.connection.readyState == 0) {  
    
  27.      mongoose.connect('mongodb://127.0.0.1:27017/markdownFile', { 
    
  28.          useNewUrlParser: true,  
    
  29.          useUnifiedTopology: true  
    
  30.      })  
    
  31.     return;  
    
  32. }  
    
  33. webinf.find({}, function (err, doc) {  
    
  34.     // console.log(doc[0].email)  
    
  35.     let adminemail = doc[0].email;  
    
  36.     try {  
    
  37.         //是admin用户才上传  
    
  38.         if (req.session.user.email == adminemail) {  
    
  39.             let form = new multiparty.Form();  
    
  40.             var path = require('path');  
    
  41.             //存储路径  
    
  42.             form.uploadDir = path.resolve(__dirname, '../public/images/plus');  
    
  43.             form.keepExtensions = true; //是否保留后缀  
    
  44.             form.autoFiels = true; //启用文件事件,并禁用部分文件事件,如果监听文件事件,则默认为true。  
    
  45.             form.parse(req, function (err, fields, files) {  
    
  46.                 console.log('文件大小:')  
    
  47.                 console.log(files.imgUploader[0].size)  
    
  48.                 let pictureSize = files.imgUploader[0].size;  
    
  49.                 if (err) {  
    
  50.                     res.send('图片存储失败')  
    
  51.                     //    图片存储成功  
    
  52.                 } else {  
    
  53.                     //图片路径与图片名称  
    
  54.                     let filepath = files.imgUploader[0].path  
    
  55.                     //兼容windows和linux获取文件名  
    
  56.                     let filebasename = path.basename(path.basename(filepath));  
    
  57.                     let filename = path.basename(filepath)  
    
  58.                     let wellpaperpath = path.join('/public/images/plus', filename)  
    
  59.                     let wellpaperdoc = new wellpaperSchema({  
    
  60.                         email: adminemail,  
    
  61.                         wellpapername: filename,  
    
  62.                         wellpaperpath: wellpaperpath,  
    
  63.                         created_time: getNowFormatDate(),  
    
  64.                         category: '',  
    
  65.                         status: 1  
    
  66.                     });  
    
  67.                     wellpaperdoc.save(function (err, doc) {  
    
  68.                         if (err) {  
    
  69.                             res.send('save error')  
    
  70.                         } else {  
    
  71.                             res.send("save success")  
    
  72.                         }  
    
  73.                     })  
    
  74.                 }  
    
  75.             })  
    
  76.         } else {  
    
  77.             res.send("you don't have permissions to upload wellpaper!")  
    
  78.         }  
    
  79.     } catch (e) {  
    
  80.         res.send("you don't have permissions to upload wellpaper!")  
    
  81.     }  
    
  82. })  
    
  83. })
    对于用户身份的校验,这里使用的是将admin用户存储在单独的collection中,前端发送请求时,根据请求中的session获取email,如果时admin的email,则执行后续代码,否则直接return。对于文件的校验,这里只对文件的后缀名进行限制,不可以依赖前端对post数据的限制,这是不可靠的。
    其中保存数据的时间,使用的是自定义返回时间的方法getNowFormatDate(),code:
  84. function getNowFormatDate() {
  85.  var date = new Date();  
    
  86.  var seperator1 = "-";  
    
  87.  var seperator2 = ":";  
    
  88.  var month = date.getMonth() + 1;  
    
  89.  var strDate = date.getDate();  
    
  90.  if (month >= 1 && month <= 9) {  
    
  91.      month = "0" + month;  
    
  92.  }  
    
  93. if (strDate >= 0 && strDate <= 9) {  
    
  94.     strDate = "0" + strDate;  
    
  95. }  
    
  96. var currentdate = date.getFullYear() + seperator1 + month + seperator1 + strDate  
    
  97.     + " " + date.getHours() + seperator2 + date.getMinutes()  
    
  98.     + seperator2 + date.getSeconds();  
    
  99. return currentdate;  
    
  100. }
    这里仅仅对data获取的时间进行简单的格式化,通过data对象获取当前的时间戳,对时间戳格式化并获取有用数据作为时间数据存入数据库,在前端获取wallpaper数据时会返回它的时间。
    4.1.7通用API数据的处理并返回
    由于整个网站有很多页面会随机获取一张图片,返回相同数据等api接口,这里写了一个统一的router文件来返回数据。
    随机返回一张头图的实现:前端post请求/getimage接口,在url中使用?后面跟上请求的图片类型。后端对其请求的类型进行解析,这里的类型有质量高的图片和质量较低的图片。由于指定的图片都存放在不同的文件夹,这里对不同类型图片的请求,随机获取不同的图片。先是读取对应文件夹下的图片名称:
    var backgroundPath = path.join(path.resolve(__dirname, ‘…’), ‘/public/images/highPX’)

将其中读取的文件写入一个list中,获取随机数,再从list中随机获取一张图片文件并返回:

  1. var backgroundPath = path.join(path.resolve(__dirname, ‘…’), ‘/public/images/highPX’)
  2. // var backgroundPath = path.join(path.resolve(__dirname, ‘…’), ‘/public/images/ipadImageHigh’);
  3. let filelist = fs.readdirSync(backgroundPath)
  4. let image = path.join(backgroundPath, ‘/’ + filelist[randomNum(0, filelist.length)])
  5. // console.log('返回的图片路径; ’ + image)
  6. res.sendFile(image)

这里文件夹的图片一般是不需要修改的,仅仅需要随机获取一张,所以并没有对其编写post上传接口。
网站再开发中,有许多别他人代码对服务器文件进行扫描,对此开发者需要及时注意并记录,这里我对于所有非法的访问路径记录在服务器中,在前端页面中,开发者也可以单独查看这些访问数据:

  1.  logWrite.find({}, {}, function (err, doc) {  
    
  2.      // console.log(doc)  
    
  3.      if (err) {  
    
  4.          res.send(err.message)  
    
  5.      }  
    
  6.      res.render('err', {  
    
  7.          // data: req.url  
    
  8.          data: doc  
    
  9.      })  
    
  10. }).sort({  
    
  11.     _id: - 1  
    
  12. }).limit(50)  
    
  13. }
    上面是对日志记录的查询,查询成功后直接发送数据文档,前端再对其进行处理。如果是非法路径,这里会返回数据不存在的页面。
    4.1.8用户所有文章的返回实现
    对于用户个人页面的文章数据的获取,这里后端通过/blogList中的?后面的查询参数,这里的查询参数是用户的Email,后端通过find查询email来返回合法的文章信息;这里合法的文章是没有被用户下架且属于发布状态的。
  14. let fileRes = await markdownFile.find({
  15.          useremail: req.url.split('?')[1],  
    
  16.          status: {$eq: 1}  
    
  17.      }, {  
    
  18.          filename: 1,  
    
  19.          tag: 1,  
    
  20.          tag2: 1,  
    
  21.          tag3: 1,  
    
  22.          category: 1,  
    
  23.         markdownId: 1,  
    
  24.         createTime: 1  
    
  25.     }).sort({_id: -1});  
    

对于用户email的合法验证,会先在用户的collection中查询该用户是否存在,且用户状态是否合法,如果合法则继续查询该用户的文章数据。

  1. let userRes = await User.find({
  2.          email: req.url.split('?')[1]  
    
  3.      }, {  
    
  4.          email: 1,  
    
  5.          nickname: 1,  
    
  6.          avatar: 1,  
    
  7.          gender: 1,  
    
  8.          synopsis: 1,  
    
  9.          motto: 1,  
    
  10.         created_time: 1,  
    
  11.         _id: 0  
    
  12.     }).limit(1); 
    
    在后继数据的返回中,使用if判断文章的文档数据和用户的文档信息,只有两者都存在时才会渲染并返回页面。如果用户不存在,则会直接返回err页面,对于一些用户可能并没有创建文章,这里返回的数据会是[]空数组,在判断时为true,并不影响页面的返回。

4.1.9文章修改的实现
文章在修改时,主要存储的是文章的内容:fileData,文章标题:filename,文章tag:tag等等。在接受post提交的修改数据时会先验证用户session中的email数据是否属于该文章的作者。这里对与fileData的长度并没有作出限制,只有存储时间超时就会报错并返回err_code。

  1. if (verifyEmail == userDoc[0].useremail) {
  2.              // console.log('用户验证ok')  
    
  3.              markdownFile.findOneAndUpdate({  
    
  4.                  _id: id  
    
  5.              }, {  
    
  6.                  fileData: markDownfile,  
    
  7.                  filename: filename,  
    
  8.                  tag: tags,  
    
  9.                  tag2: tags2,  
    
  10.                 tag3: tags3,  
    
  11.                 category: category,  
    
  12.             }, function (err, data) {  
    
  13.                 if (err) {  
    
  14.                     console.log('数据存入报错: ' + err)  
    
  15.                     res.status(200).json({  
    
  16.                         err_code: 1,  
    
  17.                         message: '数据存入错误' + err  
    
  18.                     })  
    
  19.                 } else  
    
  20.                     res.status(200).json({  
    
  21.                         err_code: 0,  
    
  22.                         message: '保存成功!'  
    
  23.                     })  
    
  24.             })  
    
  25.         }  
    

上面是获取文章的作者和session中存储的拥护email进行对比,只有相同才会继续保存。如果不是该文章的作者session,则会返回对应的res,其中message中保存着报错原因。
部分文章是有图片的,对于图片上传的实现。
用户上传图片时,目前使用粘贴,前端即可捕获到粘贴板上的图片并上传,后端在接受时,同样会验证session,如果session通过,则会存储图片到指定文件夹并返回该文件名在markdown文档中的格式:

  1. form.parse(req, function (err, fields, files) {
  2.  if (err) {  
    
  3.      console.log(err)  
    
  4.      res.send(err)  
    
  5.  } else {  
    
  6.      // console.log(files)  
    
  7.      console.log('文件名及其路径:')  
    
  8.      let filepath = files.imageFile[0].path  
    
  9.      console.log(filepath)  
    
  10.     //获取路径的文件名:  
    
  11.     let filebasename = path.basename(path.basename(filepath));  
    
  12.     console.log('文件名;')  
    
  13.     console.log(filebasename)  
    
  14.     console.log('当前文件夹:' + __dirname)  
    
  15.     const oldpath = path.join(__dirname, path.join('..', '/images/' + filebasename))  
    
  16.     console.log('olldpath=    ' + oldpath)  
    
  17.     const newpath = path.join(__dirname, path.join('..', '/images/' + filebasename + '.png'))  
    
  18.     //给图片修改名称  
    
  19.     try {  
    
  20.         fs.renameSync(oldpath, newpath, function (e) {  
    
  21.             console.log('rename err')  
    
  22.             console.log(e)  
    
  23.         });  
    
  24.     } catch (e) {  
    
  25.         console.log(e)  
    
  26.     }  
    
  27.     const returnpath = path.join('/images/' + filebasename + '.png')  
    
  28.     // 新建  
    
  29.     new imageSchema({  
    
  30.         path: returnpath,  
    
  31.         createtime: sd.format(new Date(), 'YYYY-MM-DD HH:mm')  
    
  32.     }).save((err) => {  
    
  33.         if (err) {  
    
  34.             res.send('图片存入数据库出错')  
    
  35.         }  
    
  36.         console.log('SUCCESS')  
    
  37.         res.status(200).json({  
    
  38.             filepath: '[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PJi2aBse-1654436633334)(' + returnpath + ')]'  
    
  39.         })  
    
  40.     })  
    
  41. }  
    
  42. });
    这里使用的是multiparty和formidable来对post请求中form表单文件数据的解析并保存。保存时并没有使用原来的文件名,而是使用随机生成的是个字符串组成的文件名,避免文件名重复。
    4.1.10文章点赞的自增
    对文章点赞时,前端会发送一个post请求,仅提交点赞文章的id,后端获取后将markdown文件的collection中的praise自加1。数据update成功后,会返回一个res,其中存储着成功或者失败的状态和message。这里的返回数据前端并不会判断成功与否。
    4.1.11文章的下架,发布与删除的实现
    文章的下架,发布,删除的api接口都是先在接受post时进行session的验证。只用在该session中存储的email与要修改的文章用户邮箱相同时,才会修改,否则报错。在修改文章状态时,只需修改对应的状态码即可:
  43. markdownFile.findOneAndUpdate({
  44.  _id: fileId  
    
  45. }, {
  46.  //删除状态  
    
  47.  //默认为1,发布状态  
    
  48.  status: '1'  
    
  49. }, function (err, doc) {
  50.  if (err) {  
    
  51.      res.status(201).json({  
    
  52.         err_code: - 1,  
    
  53.         data: 'post err! please try again'  
    
  54.     })  
    
  55. }  
    
  56. res.status(200).json({  
    
  57.     err_code: 0,  
    
  58.     data: 'post success'  
    
  59. })  
    
  60. })
    删除所使用的并不是remove查询语句,修改对应的状态码,代表删除文章,使用的是假删除,防止用户删错文章错误。文章的状态码在任何查询文章collection时都会用到并过滤。

4.2前端技术栈的使用与细节实现
4.2.1页面的ui风格统一
作为一个前端开发人员,对个人所编写的前端页面美观有一定的要求,虽然没有学过相关的ui设计,但这里使用并借鉴了一些统一的风格化设计:页面的卡片元素使用统一的style,div根据大小使用统一的redio和hover的风格化效果,按钮使用统一的style。
字体字号:文字在设计中根据其意义分为标题、副标题、正文等等,作为统一风格,最基础的首先要统一字体字号、颜色、字重。同样的排版和字号不仅让用户觉得规范整齐,更使不同的页面看起来规范统一。
对于icon,ICON即是图标,图标具有很多种特性,从构成上分为线性、面性、正负形等,还有颜色、圆角还是尖角,是以圆形、方形、或者圆角方形等等包裹图标,还是有共同的图标特点,比如线条断点,双色图标,等等。统一风格的UI设计,在图标这个范畴内,同类型图标会运用统一的特性,比如主导航几个图标同一风格,甚至点击触发的当前状态也是同一风格。这是项目内统一风格的重要一步。为了样式和风格的统一,这里使用的是bootstrap自带的icon,为了方便cdn引用,将icon字体文件存放在static的文件夹下。
控件样式,页面中的控件是设计中重要的组成部分,控件中圆角与直角,线条粗细,元素的排版布局间距,色调的明暗变化,元素的特性(比如按钮)都要保持一致,这样会使风格更加统一。该项目中对于一些卡片,分割线,卡片等元素,使用bootstrap来进行排布。
间距及分割方式,在控件里提到了间距问题,对于整个项目的设计而言,间距问题也十分重要。页面中,左右离边缘的间距,相邻控件的横向间距和纵向间距都需要统一。元素之间的间距,甚至是字与字之间的字间距都需要统一。
对于页面中使用的公共style,这里放在的单独的less文件中:

  1. [root@kirin css]# ll
  2. total 172
  3. -rw-r–r-- 1 root root 3721 Apr 1 09:29 404.css
  4. -rw-r–r-- 1 root root 1585 Apr 1 09:29 admin.less
  5. -rw-r–r-- 1 root root 0 Apr 1 09:29 blackPage.less
  6. -rw-r–r-- 1 root root 3353 Apr 1 09:29 contentLimte.less
  7. -rw-r–r-- 1 root root 6013 Apr 1 09:29 edit.less
  8. -rw-r–r-- 1 root root 1043 Apr 1 09:29 err.less
  9. -rw-r–r-- 1 root root 37414 Apr 1 09:29 font-awesome.css
  10. -rw-r–r-- 1 root root 1468 Apr 1 09:29 footer.less
  11. -rw-r–r-- 1 root root 3065 Apr 1 09:29 header.less
  12. -rw-r–r-- 1 root root 39 Apr 1 09:29 index2.less
  13. -rw-r–r-- 1 root root 38 Apr 1 09:29 indexImageList.css
  14. -rw-r–r-- 1 root root 10247 Apr 1 09:29 index.less
  15. -rw-r–r-- 1 root root 470 Apr 1 09:29 loading.css
    这里是统一写在contentLimte.less文件中,引用在header.html的最头部less文件,方便后面less文件中的复用。
    页面中部分使用动画,这里使用的是animate来实现动画的加载与退出。由于它使用简便,仅需add或remove类名即可实现动画的加载与切换,同时也是为了实现动画效果的统一。在主页使用的是scrollmagic,由于主页有一页的高度是留白处理,只用滚动到下一页才会显示主页文章的list,所以使用绑定高度并执行动画。
    4.2.1页面逻辑和数据的交互
    对于页面中逻辑交互,没有使用大型框架的项目中最适合使用的可能就是jquery了。在该项目中,最多的就是按钮的点击与页面互动行为的数据交互。这里使用类名绑定function即可:$(‘.rightPointer’).click(function () {})。
    页面中的交互更多的还有类名的切换,是用toggleClass即可,由一些click动作触发。这里对于部分优先级较高的样式,使用的是绑定id进行切换。
    对于数据的交互,由于是不断开发,不断更改,这里使用的axios和ajax来传输数据。Axios对于异步函数很友好,不必一层一层添加回调;ajax层级结构明晰,对于一些代码层级结构并不是很多的逻辑代码很友好,两者的使用方式都是相似的。前端接受的数据具有统一的结构,一般有err_code,message,result组成返回的result。在对返回数据进行解析时,使用异步函数,防止函数堵塞而跳过了某些函数的执行。
  16. $.ajax({
  17.  data: formData,  
    
  18.  method: 'POST',  
    
  19.  // 告诉jQuery不要去处理发送的数据  
    
  20.  processData: false,  
    
  21.  contentType: false,  
    
  22.  url: '/getcopy',  
    
  23.  success: function (data) {  
    
  24.      $('.display').attr('src', data.filepath);  
    
  25.     let oldhtml = $('#blog').val()  
    
  26.     $('#blog').val(oldhtml + data.filepath)  
    
  27. },  
    
  28. error: function (data) {  
    
  29.     console.log('返回的错误数据:')  
    
  30.     console.log(data);  
    
  31. }  
    
  32. })

4.2.2页面数据的渲染
在markdown的文章页面,这里使用的是marked来渲染。页面加载完后会执行将文章数据的value,也就是markdown·转为对应的html内容并填充到container元素中。为了页面中部分代码高亮,使其更为美观,在实例化marked之前,为其挂载了heightlight语法,将html内容中被code包裹起来的内容自动识别需要高亮的代码块。

  1. hljs.initHighlightingOnLoad();

  2. const md = window.markdownit({

  3.  breaks: true,  
    
  4.  typographer: true,  
    
  5.  html: true,  
    
  6.  highlight: function (str, lang) {  
    
  7.      if (lang && hljs.getLanguage(lang)) {  
    
  8.          try {  
    
  9.              return '<pre class="hljs"><code>' +  
    
  10.                 hljs.highlight(lang, str, true).value +  
    
  11.                 '</code></pre>';  
    
  12.         } catch (__) {  
    
  13.         }  
    
  14.     }  
    
  15.     return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';  
    
  16. }  
    
  17. });
    由于文章内容可能在使用时经常被刷新并获取最新的文章内容,这里将文章内容的获取方法进行模块化,使用时,只需分别调用对应的function。

  18. //源代码文件:

  19. //进入页面执行一次渲染

  20. render();

  21. //页面上文章内容的渲染

  22. function render() {

  23.  // $('.preView').empty()  
    
  24.  // const data = $('.sourceData').html();  
    
  25.  const data = $(".sourceData").val()  
    
  26. var result = md.render(data);  
    
  27. $('.preView').empty().html(result)  
    
  28. console.log("调用了render")  
    
  29. }
    页面上数据的渲染大致是相似的,在edit文章的页面,这里将源码内容放在左边,右边是渲染之后的内容:

    数据每一次提交后,会从后端获取的数据中更新页面。在该页面监听了键盘上的保存数据的组合键:ctrl+s。为了方便save数据方法的调用,这里单独作出了function的抽离,也是为了方便后期代码的维护与更新。
    第五章:小结
    作为一个完整的博客系统,本系统的主要结构有:阅读博客,后台管理,系统管理。其中后台包括随笔管理,文章管理,评论管理,链接管理,密码管理。系统管理包括账户管理和密码管理。从而使得博客用户能够展现自我和互相交流。
    在详细设计和编码中,运用最基本的开发技术,代码简单易懂,系统里间接跳转比较清晰,完成了普通用户和系统管理员两者的功能分离,而且各自的共能操作都能得到正确的数据并存入数据库中保存。另外系统利用MVC模式极大的提高了系统的灵活性,复用性,开发效率,适应性和可维护性。
    系统的开发以及论文的研究由于时间的关系,有一些地方做的还不是很完美。对于设计模式的研究时间不长,所以在写代码的时候还没有做到真正的得心应手,是在查询一些帮助的文档的情况下才完成的。
    由于时间仓促,一些技术和设备所限,该系统存在一些不足之处,有待进一步改进和完善。
    感谢学院领导以及何老师给我们创造的良好的学习环境和此次实践的机会。

猜你喜欢

转载自blog.csdn.net/ice_stone_kai/article/details/125137313
今日推荐