[Flask/React/MongoDB]BlogReworkIII-如何搭建一个动态Blog

少女dtysky

世界Create

时刻2016.03.12

项目源在这里
这是此Blog迎来的第三次重构了,使用Flask、React、Mongo、Express等将原先的静态网站完全替换为了单页应用,当然是响应式设计。重构的理由非常简单—这种简单也是源于我习惯性的随性而为。当然,虽然理由简单,一开始设想得也很简单,但实际做起来却又是另外一番景象了。通过这次重构,我最大的收获或许并不是技术上的提升,而是对工程周期的评估和把握能力的提升—作为唯一的管理和执行者,亲眼见证了一个工程是如何从两天的预想周期膨胀为一周、随后膨胀为半个月、最后膨胀为一个月的。作为一个能力平平的平常人综合征患者,或许只有这点坚持的精神还能拿的出手了吧(笑


Blog发展历程

建博客,也是要按照基本法的,你不能说建,你就建,总是想搞个大新闻,又没什么干货,这样,还不如纯粹装个逼,你说是不是?当年的我啊,还是太年轻,图样图森破,上台拿衣服,没什么审美,也没什么技术,所以,搞出的那个东东啊,自己觉得一颗赛艇,其实,在删的时候,比谁都快,搞得连个底子都没有留,到时候后悔了,我还是要负责的吧?

2014.05

由于未来找工作的考虑以及当时泛滥的助人之心,用初步可用的Python功底加上刚上手的Html + Css技术,在Pelican框架的帮助下用十天左右首次将Blog搭建起来。此次使用的是默认主题中的一个基于bootstrap的主题,嘛,标准的技术博客风格吧。当时还没有服务器和域名等等的概念,所以这个Blog只是被简单地托管到了dtysky.github.io上(现在已经无效)。

2014.06

对HTML+CSS有了更加深入的认识,第一次的重构开始。由于当时的审美还停留在一个比较low的水准,加上当时的中二还没有经过否定之否定这个螺旋上升的过程,所以无论是主题还是分类都比较不堪入现在的目,而且当时对很多东西还是不了解,基本所有的布局都是用静态图像完成的。不过值得一提的是,在这一次,我初步使用了JS—我对这个语言的初步影响并不好,觉得他的语法就是SHIT。
原始设计已经遗失,唯一还存在的就是这一篇当时的记录233:世界线启动
看看当时的分类www(其实现在看来也蛮带感的,我还是挺喜欢的:

  1. 经验而谦逊的现实
  2. 雅致而耗散的诗人
  3. 矛盾而真诚的世界
  4. 纯粹而残缺的歌姬
  5. 平凡而美丽的生活

2015.02

半年后的春节期间完成。随着技术的进一步提升和更进一步的审美,加上中二的否定之否定的螺旋上升(不要在意期间发生了什么),便有了这一次的重构。此次重构主要是样式上的变更,首先将分类转换,然后修改了整体的布局,并基于jquery实现了一些动画。此次重构后的界面和现在的界面十分接近,不同的只有左侧动画(原先是先滑出左侧,再划回来)、播放器和一些载入动画,当然,最大的不同自然还是静态网站和动态网站的区别了。
不仅使界面上的修改,在此次重构中我注册了现在使用的dtysky.moe这个域名,同时将网站搬移到了DigitalOcean的VPS上,用NGINX作为服务器。此外还加入了Disqus的评论系统、bshare的分享系统以及各个搜索引擎的SEO。
这次做好了保留工作,老样式的网站保留在这里:
old.dtysky.moe

2016.02

又是春节期间,也是毕业后的第一个春节。在毕业的这半年内,我完成了一次离职,并从FPGA工程师转为了软件工程师(现在是WEB)。
某日,我发现我还是想要加一个音乐播放器在网站上的,所以也就需要将Blog变成动态网站,这就是为什么我花了一个月的业余时间的理由。。。

To(Never www)Do list

Server端的Cache

在现在的设计下,前端每一次的请求都会导致后端的一次数据库查询操作,并且对于那些有分页的、列举文章的页面,Client端会先从服务端拉取所有的文章简介,然后存储在前端的Cache中。这其实是不太好的,首先这可能会浪费一些带宽(当然,在GZIP的压缩下,对比JS和CSS文件这些数据量都是小CASE),其次比较重要的一点是会造成后端端数据库的查询压力,对于性能不那么强的VPS,这对并发还是有比较大的影响的。考虑到Blog的文章更新还是相对较慢的,所以一个简单Cache的存在还是可以允许的。
具体的设计,无非就是在某些数据被请求时先去找Cache,如果没有就查询数据库并将其加入Cache,然后在文章有更新时将Cache清空即可。

用Redux和ES7对前端再次重构

现在的设计对SEO并不友好(虽然我已经使用了React-router的服务端渲染(由于有AJAX存在,所以单独使用对SEO并没什么用)并且使用了fragment这个meta tag提示搜索引擎这是一个ajax页面(但这毕竟只能对谷歌有效)),所以我需要一个方法来解决React服务端渲染和客户端的数据同步问题,这个可能只能看看REDUX这样的框架了。
至于ES7么。。。我觉得我暂时无法用ES5写出干净的前端代码,而下一次重构的时候ES7也差不多出来了。。。

本次设计

后端

架构

用Python实现。
作为Blog的后端,无非由六个方面需要考虑,其一是对原始文章的解析,其二是数据库管理,其三是文件监控,其四是Sitemap和RSS的生成,其五是WEB服务器,最后一个就是作为辅助的日志管理了,以下是一张基本的设计框架图,然后根据这张图来简单介绍我此次的设计和用到的技术。 //src.dtysky.moe FlowChart1

文章解析

手写HTML文章是笨蛋才会去做的事情(虽然我的确手写过一段时间233),现在一般Blog文章的编写都是使用Markdown或者Restructuredtext,冰鞋比起常常用于Documents编写语言的后者,前者由于语法简洁等原因使用范围更广,所以本Blog的文章使用也是Markdown。
Markdown固然足够满足文章正文的需求,但有一些meta元素,比如文章标题、作者、分类、时间等要素还是需要自己去想办法添加的,我这里借鉴、或者说是延续了Pelican的设计,将所有的meta元素添加到了文章头,和正文用空行隔开,分别对meta和正文进行解析,最后汇总,所以最后编写的文章实际上是这样的:

Title:test
Authors:dtysky,x
Tags:test1,test2

#h1
...

要注意的是这里并没有Category这个标签,这是因为为了延续之前的设计,并且为了简化工作(其实就是懒...),所以直接将该文章所在的目录作为了分类;除此之外,Authors这个标签也不是必须的,如果没有,解析器会使用配置文件中的default_author代替。

Meta解析

Meta解析的实现是十分简单的,无非就是将meta的tag和value作为键值对写入一个hashtable中,单考虑到日后的可扩展性,如上面的结构图所示,我将其分为了两个部分:

  1. MetaParsers:用于将原始的以字符串的形式存在的meta文本解析为一个hashtable或者任何需求的数据形式,所有parsers都继承自MetaDataParser父类,文章解析程序在初始化的时候会载入每一个子类并创建一个对象,并在解析时调用该对象的parse方法,对该对象对应的meta元素的值进行解析,并返回被解析过的数据。
  2. SlugWrappers:用于生成已经解析过的metadata的url,也就是会在最终的url里出现的、表征该metadata的唯一定位符,这个可以按照喜好自己来,我大部分是默认为不转换(也就是url和原始数据一致),诚然这可能会带来一些SEO问题,但为了防止冲突和便于日后管理,我舍弃了原先Pelican自带的大写转小写、汉字转拼音的做法。这一步会在parse完成后执行,所有子类对应对象的warp方法会被调用,将输入的每一个单个的meta元素转换为url。

通过这两部的解析,原始的meta文本就被转换为了可以供后续流程处理的数据,在我自身的设计中,可能会出现在url中的metadata都会有viewslug两个元素,一个用于显示,一个用于定位。
得益于这种设计,meta都是可扩展的,如果我想添加一个meta,只需要分别从MetaDataParserSlugWrapper继承一个子类,简单重写一下其中的某些成员函数便可以实现。

Markdonw解析

这里直接使用了Python的Markdown库,但另外加入了Pygments语法高亮插件,有需要的时候也可以加入其他插件(比如表格)。

数据库管理

文章解析之后便需要存到地方,这个地方自然就是数据库了(当然静态页面也可以,不过那并不是此Blog的做法),这里选取的是MongoDB,原因么...最近用得比较多,顺手就这么选了。这个数据库本身没有什么说的,用起来还是很简单,这部分主要难度还是在于如何管理数据库的写入(这可后面要说到的文件监控有很大关系),考虑解耦的原则,这一部分只负责接受数据和指令对数据库进行操作,不负责决策。
和上面的设计一样,我设计了一个DatabaseWriter父类,所有的Meta和正文的写入类都继承自这个父类,之后可以重写父类的方法来完成逻辑,在最基本的设计中,每个子类都拥有三个对外的接口函数insertupdatedelete,分别用于想数据库中插入一篇文章、更新一篇文章或者删除一张文章,这是一种增量式的设计,保证每次只会对北改动过的文章进行解析和写入。这一步在文章解析后被执行,最终被调用的就是上面提到的那几个方法,接受解析过的完整文本,将数据写入子类对应的表中。
这里提一句,被写入数据库中的记录是包含文章文件的路径的,这会在后面用到。数据库的设计在此:
Database desigin

文件监控

文件解析和数据库管理都搞定之后,缺少的就是提供输入激励的东西了。这里采用的数据驱动的设计,程序监听文件的状态,在状态发生变化时响应这些变化并作出响应的处理。
这里选取的是Watch dog这个系统状态监听库,其中的FileSystemEventHandler这个模块中提供了良好文件状态事件支持,只需要指定一个监听的文件夹并去重写它预设的on_create等方法便可以实现逻辑(这里需要注意,在OSX和LINUX下事件触发的状况不同,所以配置文件中才会有is_linux这个选项),设计如下:

  1. 当文件新建时,触发on_create事件,解析文章并将其insert入数据库。
  2. 当文件被删除时,触发on_delete事件,通过文件名找到数据库中该文章对应的记录,反向找到正文和所有meta,随后调用所有writerdelete 方法。
  3. 当文件被修改时,触发on_modified事件,解析文章并将其update入数据库。
  4. 当文件被移动时,等价于一次删除和一次新建。

如此,文件的解析部分基本就完成了。

Sitemap和RSS

这两个基本是Blog的标配了,一个用于SEO一个用于用户订阅,其实也没什么难度,就是每次文本解析完毕后读取数据库中所有的文章根据格式生成几个XML文件,稍微研究一下二者的各市标准就行,不过这其中还是稍微有点坑的:
在XML中插入HTML需要转义:

entity_ref = {
    "<": "&lt;",
    ">": "&gt;",
    "&": "&amp;",
    "'": "&apos;",
    '"': "&quot;"
}

考虑到这二者实在没有什么扩展性的必要,我就直接写死了,RSS生成的有全站、作者和分类。

WEB服务器

数据源准备好之后就是想前端给数据的事了,所以需要开一个WEB服务器。这里选取了Flask作为服务器框架,路由规则也比较简单不再赘述。和上面大多数设计一样,每一条路由规则都对应一个继承自WebHandler的父类,这个父类又继承自Flask中的View类,父类通过重写View类的dispatch_request方法来响应请求(采用这种方式来做路由而不是装饰器的方式是为了便于后面路由的统一管理)。
程序初始化时,程序首先会找到WebHandler的所有子类并注册路由,之后接收到请求时,程序首先会找到该条路由规则对应的handler对象,然后调用它的dispatch_request方法,dispatch_request方法又会调用_handle方法进行最终响应的实现。

日志管理

在主体功能之外还有一个重要的功能是被需求的——日志管理。系统需要有全局的logger来记录关键的信息同步打印并输出到文件,由于这个比较简单我就自己造轮子了:

  1. logger输出的信息有三个等级—error、info和warnning,分别打印为不同的颜色并输出到不同的文件。
  2. 每天新建一个log文件便于分析。

至此,后端的实现基本完成。

前端

Node.js实现。 前端主要分为两部分—Client和Server,其中Client是大头,负责和用户进行交互,Server负责将Client端需求的数据传送到用户设备。在本设计中Server还有服务端渲染和SEO的职能。
当然,一时由于经验,而是由于。。。ES5也没规范到哪去,感觉JS这个语言还是有很多缺陷的,ES6似乎好了很多,嘛。。。先凑合着吧,code style的确很烂我承认。

工具链

前端开发最好还是有一个用于开发测试的工具链,这对于效率的提升是立竿见影的。这里我选择了GruntWebpack,前者用于自动化事务管理,后者用于打包和测试项目。我的设计如下:
//src.dtysky.moe FlowChart2

其中debugserver-buildclient-build三个事务分别用于测试、打包Server端程序和打包Client端程序,build事务包含了前两者,简化操作。

Client

Client负责的是用户交互,使用了ReactReact-router作为View和前端路由,具体的设计如下:

View

基础显示,包括整体视图的布局和一些动画效果,页面属于响应式设计,为PC和移动端分别走了适配,页面组件包括:

  1. Home,显示所有文章的列表。
  2. Authors, 显示所有作者的列表。
  3. Tags, 标签云。
  4. Author,显示某个作者所有文章的列表。
  5. Tag,显示某个标签所有文章的列表。
  6. Category,显示某个分类所有文章的列表。
  7. Article,显示文章。
  8. Title,标题栏,显示所有分类。
  9. Left-image,左侧图像滑窗。
  10. Menu和Menu-phone,前者在PC端的右下角,后者在移动端的上面。
  11. Footer,移动端的最下方。
  12. Music-player,音乐播放器。

动画效果包括:

  1. 元素色彩和基本位移操作,使用CSS中的transition实现。
  2. 元素旋转和特殊运动,使用animationkeyframe实现。
  3. 左侧的图像滑动,使用Velocity-react库实现。

在页面资源方面,将原来的png图标全部封装成了字体和svg文件,找不到线程的就自己手撸...最终素材大小和请求次数的确降低了许多,封装工具在这里

等待、错误和404

这些细节的地方也是需要关注的,具体是什么效果就自己试吧www:

  1. 等待界面,自己的log加上半圆边框不断旋转,“少女祈祷中”是什么梗就不说了,这里其实是想搞一个抽象的、机械齿轮转动的效果。
  2. 一般错误界面,未知错误的界面,一般看不到的,基本就是机械齿轮卡壳的感觉。
  3. 404界面,直接在这看吧
Meta修改

虽说是单页应用,但每个不同的页面有自己的meta tag还是很重要的,比如titlekeywords之类的,这里我是用了React-helmet组件对他们进行了修改(当然,本质上应该还是DOM操作就是,图个方便。)

分享组件

在每篇文章的右上角都会有一个分享的图标,本次重构去掉了对bshare的依赖,自己写了个分享组件,觉得设计的还成,九大社交网站+本页面二维码。

音乐播放器

这也是导致本次重构的元凶了,这个播放器基于APlayer,并做了些许的定制性修改。播放源需要自己在配置文件中进行配置,在当前设计中每次修改完音乐列表就必须重新打包发布一遍Client端的代码,不过把它提出来也不是什么难事,有时间吧。。。
在我自己的设计中,这个播放器还和文章meta中的music标签关联,当文章中存在这个标签时,播放器会重新载入文章中制定的音乐列表进行播放,否则就会载入配置中默认的列表。

前端路由

这个是用React-router实现的,也是单页应用的精髓,有了它,伪静态的页面便是轻而易举,他会抓取浏览器的历史并完成响应的路由,页面每次只会更新路由指定的区域。

缓存

为了避免请求过多,这里建立了一个Cache用于缓存之前向后端请求过的数据,在页面每次加载之前程序会首先从Cache里取数据,如果没有才会请求并将获得的数据加入Cache中。

其他

Google analysis和Mathjax的支持以及......彩蛋。
我认为彩蛋是一个网站必须的东西,它可以出现在Meta里,也可以出现在Code的Comments里,而我,则是将其放在了Console...

Server

Server框架是Express,也算是从主流了,主要完成:

静态资源

Express自带的static中间件实现,此条规则用于访问静态资源。

服务端渲染

对所有的正常页面请求,使用ReactrenderingToString方法在服务端进行首页渲染,遗憾的是对于本设计这并不能解决SEO问题(前面说到过),所以,就当是提升用户体验吧。

针对搜索引擎的页面

每一条正常的url都对应着一条特殊的url -> (/xxx -> /jade/xxx),这个页面是为搜索引擎(或者说就是谷歌)提供的,具体原理是在正常页面的meta中加入一个fragment的tag,谷歌找到这个tag后就会在原始url后加上?_escaped_fragment_,我们可以在Server捕捉它并将准备好的另外一个页面提交给谷歌。我所准备的页面是用jade作为模板的,并用requests向后端发起请求来获取数据。这些页面去掉了动画和播放器,基本可以看做纯粹由HTML+CSS构成的静态页面了。

日志管理

这里同样需要一个Logger,我选取的是Tracer这个库,将其封装为了一个中间件。

到此,前端基本完成。

其他

NGINX作为反向代理服务器,Forever.js做进程守护。

如果不是自己的创作,少女是会标识出来的,所以要告诉别人是少女写的哦。