在掘金,简书,微信公众号盛行的今天,构建一个博客系统似乎有点多余。所幸的是开发周期并非太长,秉持着不管三七二十一先发布再说的信念,这个系统总算是顺利上线了。至于后期它能运营多久,有多少的访问量,似乎不是现阶段该考虑的事情。

这篇文章主要讲述博客系统开发过程中的技术选型,对于一些抉择会进行相对深入的探讨,也会对当初为什么选择了这门技术而不是那门技术做出解释。除了开发之外还会涉及一些设计,测试,部署相关的话题。

背景

该博客系统是在Ruby On Rails这套框架的基础上构建的,项目源代码托管于Github,项目名称为Stone。起名为Stone原因有二

  1. 最喜爱的乐团五月天里头刚好有一名团员的艺名为石头。
  2. 希望这个项目能够如磐石般坚挺(Be strong like the stone),而不会像从前的个人项目那般,开发到一半便删库跑路。

此外,网站的名称为Step By Step,恰好是五月天一首十分好听的曲子《步步》的英文翻译。

Step By Step

设计障碍

作为一名开发者,本人并没有任何网页设计的经验。所幸在前端岗位工作多年,倒是有着大量把各种设计图转换成代码的经验。虽说不能够做出一个有着酷炫设计的网站,但做出一个中规中矩的网站还是没什么问题的。凭着低于平均水平的审美能力,并照着《写给大家看的设计书》里面所提及的设计原则先设计出了一个版本。总之把网站上线了再说,以后有哪里看不顺眼再慢慢进行调整。

从事Web开发工作这些年,只攒下了搬不上台面的切图,以及编码经验,要运用诸如Sketch,Photoshop这些重量级软件来做设计其实还真不如用CSS来得顺手。于是决定抛弃市面上流行的设计工具,仅以CSS为画笔,浏览器为画板,一步步地构造出页面的基本结构及其相关配色。如此作业,当碰到了看着比较顺眼的网页设计素材的时候也能够直接通过CSS来调整页面,而不需要再另外出一版设计图了。这似乎在某种程度上来说也有点敏捷开发的味道。

前端样式与交互

Stone并不是一个前后端分离的项目,一方面是为了节约开发时间,另一方面是觉得真的没必要。于是只是单纯地采用了Rails默认的开发脚手架,并没有引入React,Vue这类流行的前端框架。其中比较大的好处就在于几乎是零配置,不需要让自己成为一个Webpack配置工程师了。

虽然新版的Rails已经引入了Webpacker,但默认情况下它仅仅会打包JavaScript资源,CSS资源依然会用通过Sprockets来处理。如果你有特殊需求可以自己对编译流程进行调整,每个人都有自己的喜好,对懒得折腾的人来说采用默认规则就好。

页面的样式代码采用了Sass来进行预处理。为了更容易地实现页面的交互还引入了JQuery作为工具库使用(2020年还用JQuery似乎有点不合时宜了,然而工具这种东西顺手即可)。此外,作为个人网站仅仅有普通的样式和交互似乎还不太够看,往往需要一些可点击的社交Icon,让访问者可以跳转到对应的社交网站。比如,微博,Twitter,Facebook等等。我采用的是Font Awesome这个图标库所提供的字体图标。一些常用的Icon它都能免费提供支持,对个人来说免费的已经足够了。

PS: 在页面交互方面,Rails默认引入了Turbolinks,它能够加快Rails应用中页面间的跳转速度,能达到单页面应用的跳转效果。

后台开发,测试与部署

一. 开发

Stone并不是一开始就用Ruby On Rails开发的,最开始采用的技术是Jekyll。Jekyll是一个十分热门的静态网站生成引擎,后来JavaScript跟Go也出了类似的东西,分别是HexoHugo(不知道我的小组长Hugo会不会怪Go社区盗用了他的名字)。

Jekyll的部署也比较简单,具体可以参考Jekyll Deployment。我当时采用的部署策略是:自己在服务器构建一个私有的git仓库,并设定一个相关的钩子脚本,每次推送代码到这个私有的git仓库都会自动运行钩子脚本,自动生成静态页面。最后这堆静态页面则通过Nginx来托管,并接受外界的访问。具体可参考Automated Deployment

不过运营一段时间后觉得效果差强人意。文章资源也是托管到代码库的,相当于数据即代码,这也导致了每次更改数据都需要推送一遍源代码,这对我来说有点麻烦。另外,CSS这些静态资源缺乏指纹,导致了有时候更改了CSS文件推送代码到远端并重新生成页面之后,在一些开着缓存的浏览器中会看不到更新之后的效果,需要清空缓存。当然这个问题可以通过引入第三方库jekyll-assets或者更改缓存策略来解决。

思来想去,与其渐渐地把一个系统堆砌成Rails那样功能全面的东西,倒不如干脆一开始就用Rails来写.于是花了大概三天的时间(空闲时间)把整个系统重写了一遍(Rails的生产力体现出来了)。

二. 测试

既然用了Rails那就应该好好写写测试了。平时工作中总是能找到各种不写测试的借口,导致了一些开发周期比较长的项目到了后期每改一行代码都如履薄冰。如今做个人项目何不趁机享受测试的乐趣?(当看到东西变绿的时候心情还是蛮好的)。

1. 测试框架

Rails默认就有一个很强大的测试系统,不过现在社区的人似乎都更推荐采用另外一个行为测试框架Rspec。它是一个用来构造Ruby测试用例的框架,语法优雅,还包含了丰富的断言语句。

不过要想更好地跟Rails项目集成的话,可以考虑直接采用前人已经实现了的rspec-rails,它包含了大量的生成器,你可以根据需求针对不同的Rails构件生成对应的测试。它的文档囊括了比较丰富的Demo,这里就不详细展开了。此外为了更好地测试HTML页面,推荐使用rspec-html-matchers,它提供了许多选择器,可以用来测试页面的DOM节点的生成情况。

2. 数据构造

编写测试另外一个比较麻烦的地方就在于数据构造,其实很多时候不愿意写测试就是因为构造数据太麻烦了。为了一个测试用例能够通过,不得不用许多行代码来构造出特定场景的数据。

为了更好地构造数据,其中一种方法是采用fixtures,所谓fixtures其实就是测试时用的种子数据。也就是说每次跑测试之前都会预加载种子数据,以保证测试用例不需要再自己构造数据了。不过这种方法有一个比较大的问题就是缺乏灵活性。当数据比较多的时候,可能你加一份数据或者修改一些原来种子数据字段的值都影响之前那些测试用例的测试结果。

往往我们可能更需要用完即弃的数据。Thoughtbot公司开发了一套比较趁手的用于构造测试数据的工具factory_bot。为了更方便在Rails项目中使用可以直接采用factorybotrails

与直接管理数据不同,factory_bot更像是数据模板的管理者,想象一下你有一堆模板,然后你拿出来改几个字段就能够建立一份新的数据。通过恰当的配置,这些临时构造的数据可以在每个测试用例完成之后立即被销毁,降低了测试用例之间的耦合性,配合Rspec用起来会十分方便。可以用JavaScript来简单想象这个过程

// database
database = {}

// data template
const product_template = {
  name: "Product Name",
  price: 22.4
}

function test_framework (callback) {
  // testing, construct data
  database.p1 = Object.assign({}, product_template, { name: 'Product1' })
  database.p2 = Object.assign({}, product_template, { name: 'Product2' })

  // make some tests by database.p1, database.p2
  console.log(database.p1.name === 'Product1')
  console.log(database.p2.name === 'Product2')

  // callback for destroy the data for test
  callback()
}

test_framework(() => {
  // after test, delete some useless data
  delete database.p1
  delete database.p2
})

factory_bot就相当于date template的管理者,它提供了许多简便的方法用于construct data。而Rspec则提供特殊的语法来组装测试用例,并提供大量的断言语句(由于js本身没有断言语句我先用console.log代替)。测试用例的定义就相当于上面的test_framework函数的定义。调用函数即运行测试。测试运行完毕之后,还能够自动回收那些临时构造的数据,便类似示例代码的回调函数。

PS: 上述的示例代码是可以跑过的,简单起见就先用JavaScript来说明整个测试过程了,可能会有点粗糙。Rspec + FactoryBot的功能比这强大得多。

三. 部署方案

在Rails项目中最方便的部署工具莫过于Capistrano。它是一套Ruby写就的用于操控远程计算机的工具,得益于Ruby的优雅语法,你能够以更为模块化且美观的方式来组织部署策略。之所以说Capistrano用来部署Rails项目较为方便,归根结底还是在于插件的支持。前人已经写好了许多丰富的插件,基本上不需要太多的改动便能够实现一套Rails项目的部署策略。以下是一些较为常用的插件列表

  • capistrano/bundler,引入该库之后部署流程能够自动运行bundler来安装相关的项目依赖。
  • capistrano/rails,通过恰当的配置,部署流程会自动运行db:migrate以及assets:precomplie两个任务,在正式环境启动Rails之前这两个准备工作必不可少。
  • capistrano/rbenv,主要用来操控远程电脑的rbenv软件,为部署流程选择特定的Ruby版本。当然需要事先在远程电脑安装好rbenv软件以及对应的Ruby版本,引入该库之后,部署流程则会自动选取Ruby版本。
  • capistrano/rvm,功能跟rbenv相似,只不过它所操作的Ruby版本控制系统是rvm,在它和rbenv之间,两者择其一即可。
  • capistrano-puma,这个仓库不是官方维护的,它提供了一些命令让你可以远程操作puma服务(Rails应用的服务)。

基本上安装并引入以上插件之后就能够拥有一个比较完整的Rails项目部署流程了。采用这种方式一个比较麻烦的地方在于需要前期就把服务器的一些依赖软件给安装好,比如数据库,nginx服务,rbenv等等,不过对于服务迁移情况不多的场景这种方式也够用了。秉持着简单,粗暴,够用的原则,便没有采用类似于Docker,Ansible这样的大杀器。

结语

开发一个博客系统的想法大概萌生于几年前,却迟迟没有实现。除了因为技术栈繁多(是否需要单页面应用?React还是Vue?Rails,Django还是Express?)无从选择之外,最主要原因还是拖延症(千万别小看了人的惰性)。居然把短短一两周的工作拖了几年,现在回想起来也真是匪夷所思。