基于vue2、eggjs、mysql的个人博客(更新中)
简单做一个博客。功能点:注册、登录、cookie、权限控制、文章列表、文章详情、文章目录、点赞、评论、分页加载整体架构:前端使用vue-cli3创建项目npminstall-g@vue/clivuecreatehello-world为了避开烦人的eslint,我选择了手动选择features:同时没有选择Linter/Formatter,使用vscode中的插件prettier和vetur对代码进行格式化。使用axios封装http请求的方法参考了vue中axios的封装和API接口的管理,对网络请求进行封装。网络请求放在/src/request,config.js包含基本配置信息:http.js封装了请求拦截,响应拦截,统一错误处理。api.js包含网站的所有接口。导出后挂载到vue.prototype.$api上,这样就可以全局使用this.$api.xxx来使用接口了:然后在main.js中导入挂载:cookie/session博客使用cookie/session方法记录会话状态。在app.vue创建的生命周期函数中,通过checkLogin()接口查询用户登录状态。具体逻辑是:首页页面加载letdocumentEle=document.documentElementletneedLoadMore=documentEle.scrollTop+documentEle.clientHeight+50>documentEle.scrollHeight;如果(needLoadMore&&!this.nomore){this.loadingMore=true;//暂时不能再滚动加载数据this.nomore=true;this.loadMoreArticle();}document.documentElement.scrollTop:文档滚动距离document.documentElement.clientHeight:可见范围内的文档高度document.documentElement.scrollHeight:文档总高度判断思路是:视口高度+文档滚动距离>=文档总高度可以预留一个预加载的距离为50。前端每次判断触底后,都会向后端请求下一页数据,返回nomore字段,表示是否有未加载的文章。这里有一个细节:每次触底的时候手动设置nomore为true,等后台返回nomore后再修改它的值。这样做的目的是防止在请求返回之前再次发送请求。参考:了解clientHeight、offsetHeight、scrollHeight、offsetTop和scrollTop如何显示博客文章。在真正做博客之前,我以为每篇博文都是通过单独写html标签来排版的。。。后来通过别人的项目找到了正确的做法。实现方式是:使用markdown/富文本编辑器编辑文章,生成html片段,使用v-html语法将html片段插入到vue中,文章会以html标签的形式呈现。博客使用的markdown编辑器是mavonEditor,代码高亮使用highlightjs。
提取目录extractCatalog(){letcontentElementRef=Array.from(this.$refs.content.querySelectorAll("h1,h2,h3,h4,h5,h6"));contentElementRef.forEach((item,index)=>{item.id=item.localName+"-"+index;this.catalogList.push({tagName:item.localName,href:`#${item.localName}-${index}`,文本:item.innerText});});}上一节将文章的html片段渲染到div容器元素中,然后就可以使用querySelectorAll("h1,h2,h3,h4,h5,h6")函数找到所有的titleDom节点在文中。querySelectorAll()的好处是会按照传入的参数顺序进行搜索,不用担心乱序。获取目录后,对各级目录进行编号,生成跳转的锚点。判断对应section是否出现for(letindex=0;index
0){this.firstVisibleElemetHref=this.catalogList[index].href;休息;}}}window.addEventListener("滚动",this.watchPageScrollFunc);判断方法:元素顶部到视口顶部的距离+元素本身的高度<=视口高度&&元素顶部到视口顶部的距离+元素的高度元素本身>0其中,获取元素到视口上面的距离用到了:getBoundingClientRect(),某个每个元素相对于窗口的位置集合中有top、right、bottom、left等属性:rectObject=object.getBoundingClientRect();rectObject.top//元素顶部到窗口顶部的距离;rectObject.right//元素右侧到窗口左侧的距离;rectObject.bottom//元素底部到窗口顶部的距离;rectObject.left//元素左侧到窗口左侧的距离;在整篇文章的dom结构中,使用querySelectorAll("h1,h2,h3,h4,h5,h6")搜索title类的所有dom节点,每次滚动动态检查这些title元素与top的距离依次搜索视口,并在找到出现在视口中的第一个Dom元素时返回。此外,还可以添加节流功能来优化性能。参考:如何判断一个元素是否在可视区ViewPortComment实现评论数据使用的数据结构如下,这里只实现了二级评论,数据结构确定后具体实现比较简单。letcomments=[{author:"admin",content:"评论1",articleId:"9",time:"2020-04-1210:59",id:"0",replyList:[{author:"ghm",content:"回复消息1",articleId:"9",replyTo:"admin",time:"2020-04-1210:59",id:"0-0"}]},{author:"admin",content:"消息2",articleId:"9",time:"2020-04-1210:59",id:"1",replyList:[{author:"ghm",content:"回复消息2",articleId:"9",replyTo:"admin",time:"2020-04-1211:00",id:"1-0"}]}];第一版可以添加表情的输入框实现方法:直接在中插入emoji的unicode,显示效果如下:这个的问题是显示效果不如图片,在不同的浏览器中会显示出不同的效果。果断换成图片显示表情,但是不能插入
标签,所以参考掘金的实现,使用css属性contenteditable将普通Dom元素变成可编辑的Dom元素,这样就可以使用appendChild()插入
,最终的显示效果明显比直接用emoji要好。 后台数据库表1设计表:文章列表:表二:文章详情:表三:用户详情eggjs遵循约定而非配置,具有以下优势:统一目录结构统一分层设计:router-controller-service-model全套安全、日志、测试解决方案高度可扩展的插件机制初始化npminitegg--type=simplenpmidirectory结构化路由框架约定在app/router.js文件中统一配置所有路由//app/router.jsmodule.exports=app=>{const{router,controller}=app;router.get('/user/:id',controller.user.info);};完整原因定义如下:router.verb('router-name','path-match',middleware1,...,中间件N,app.controller.action);注:在Router的定义中,可以支持多个Middleware串联执行。控制器必须在app/controller目录中定义。一个文件还可以包含多个控制器定义。定义路由时,可以通过${fileName}.${functionName}指定对应的Controller。控制器支持子目录。在定义路由时,可以使用${directoryName}.${fileName}.${functionName}来制定对应的Controller。框架中一些常见的路由用法:获取查询参数//curlhttp://127.0.0.1:7001/search?name=eggctx.query.name获取路径参数//app/router.jsmodule.exports=app=>{app.router.get('/user/:id/:name',app.controller.user.info);};//app/controller/user.jsexports.info=asyncctx=>{ctx.body=`user:${ctx.params.id},${ctx.params.name}`;};//curlhttp://127.0.0.1:7001/user/123/xiaomingpostrequestboby'sgetctx.request.body重定向//内部路由重定向app.router.redirect('/','/home/index',302);//外部路由重定向ctx.redirect(\`http://cn.bing.com\`);控制器(Controller)简单的说就是Controller负责解析用户的输入,处理后返回相应的结果。框架建议Controller层主要对用户的请求参数进行处理(校验转换),然后调用相应的服务方法处理业务。获取业务结果后封装返回:获取用户通过HTTP传递的请求参数。校准和组装参数。调用Service进行业务处理,必要时对Service的返回结果进行处理和转化,使其适应用户的需求。通过HTTP将结果响应给用户。定义:所有Controller文件必须放在app/controller目录下,可以支持多级目录,访问时可以通过目录名进行级联。//app/controller/post.jsconstController=require('egg').Controller;classPostControllerextendsController{asynccreate(){const{ctx,service}=this;constcreateRule={title:{type:'string'},content:{type:'string'},};//验证参数ctx.validate(createRule);//程序集参数constauthor=ctx.session.userId;constreq=Object.assign(ctx.request.body,{author});//调用Service进行业务处理constres=awaitservice.post.create(req);//设置响应内容和响应状态码ctx.body={id:res.id};ctx.status=201;}}module.exports=PostController;上面定义的PostController的方法可以通过文件名和方法名来使用。//app/router.jsmodule.exports=app=>{const{router,controller}=app;router.post('createPost','/api/posts',controller.post.create);}定义了Controller类,每次请求访问服务器时都会实例化一个新的对象,项目中的Controller类继承自egg.Controller,并且会在this上挂上以下属性:this.ctx、this.app、this。service,this.config,this.logger服务(Service)服务是一个抽象层,用于复杂业务场景下的业务逻辑封装。提供这种抽象有以下优点:使Controller中的逻辑更加简洁。为了保持业务逻辑的独立性,抽象出来的Service可以被多个Controller重复调用。将逻辑与表示分离可以更轻松地编写测试用例。使用场景:复杂的数据处理,比如需要展示的信息需要从数据库中获取,必须按照一定的规则计算后才能返回给用户展示。或者计算完成后更新到数据库。调用第三方服务,如GitHub信息获取等。服务文件必须放在app/service目录下,可以支持多级目录。访问时可以通过目录名级联访问app/service/biz/user.js=>ctx.service.biz.user因为它继承自egg。service,所以它有如下属性方便我们开发:this.ctx,this.app,this.service,this.config,this.loggerMySQLegg提供了访问MySQL数据库的egg-mysql插件。安装插件:$npmi--saveegg-mysqlenableplugin://config/plugin.jsexports.mysql={enable:true,package:'egg-mysql',};最后在config/config.${env}.js//config/config.${env}.jsexports.mysql={//单个数据库信息配置client:{//hosthost中配置各个环境的数据库连接信息:'mysql.com',//端口号port:'3306',//用户名user:'test_user',//密码password:'test_password',//数据库名database:'test',},//是否toloadonapp,defaultopenapp:true,//是否加载agent,默认关闭agent:false,};SequelizeSequelize是nodejs社区广泛使用的ORM框架。它将关系型数据库的表结构映射为对象,允许开发者使用js语法完成数据库操作。此外,Sequelize提供了Migrations来帮助开发者管理数据库中的每一个变化,因此数据库表的每一次变化都应该通过Migrations来实现。egg为集成Sequelize提供了脚手架:npminitegg--type=sequelize在线配置服务器环境1.一台linux服务器笔者购买了阿里云最便宜的ECS云服务器,配置为1核2G,1M带宽,学习使用是完全够了,操作系统是CentOS。然后参考阿里云给的教程在服务器上部署NodeJs环境,部署mysql。2.购买域名在阿里云购买域名后,按照新手指南设置域名解析,同时设置www和@,即可通过www.xxx.com和xxx访问网站.com。同时,如果要使用域名访问网站,还需要注册域名。3.将本地数据库迁移到云服务器。笔者使用的数据库可视化工具是navicat。在navicat中将选中的数据库dump成一个sql文件,然后在云服务器的mysql中新建一个数据库。运行这个SQL文件后,数据库就完成了。移民。上传文件1.下载文件传输工具Xftp2。部署前端代码npmrunbuild打包后,使用Xftp上传dist文件夹到服务器3.部署后端代码,gitclone后端代码到服务器npminpmstart配置nginx1.安装nginxyuminstallnginx安装的nginx会在/etc/nginx2.使用Xftp修改nginx配置在/etc/nginx目录下找到nginx.conf文件,用记事本打开编辑:userroot;worker_processes1;events{worker_connections1024;}http{包括mime.types;default_type应用程序/八位字节流;发送文件;keepalive_timeout65;服务器{听80;服务器名称本地主机;root/root/project/dt_blog/frontend/dist/;索引index.html;add_header访问控制允许来源*;位置/api{proxy_passhttp://127.0.0.1:7001;proxy_set_headerHOST$host;}location/{try_files$uri$uri/@router;索引index.html;}location@router{重写^.*$/index.htmllast;}}}这两个配置很重要:location/{try_files$uri$uri/@router;indexindex.html;}location@router{重写^.*$/我ndex.htmllast;}网站部署好后可以正常访问,但是打开二级页面刷新后会出现404。原因是:v-router设置的路径不是真正的路由,项目中的路由跳转都是通过js实现的,在服务器部署前端打包的dist目录后,访问根路径在浏览器中会默认为index.html,但是直接访问其他路径,并没有对应的真实路径(参考:https://www.cnblogs.com/kevingrace/p/6126762.html)完成以上配置后,可以通过服务器ip:80访问部署好的网站,但是通过ip地址访问网站一直不行。太合理了,那就需要给网站配置域名了。配置域名