我开发了一个基于 Beancount 的账本托管服务 HostedBeans,欢迎大家来了解纯文本复式记账并试用我的服务。
标签 #Node.js

与精子同款的博客主题 Simple Block 现已发布!

今年年初,因为我已经很久不写 PHP 了,所以我将 我的博客 从 WordPress 换到了基于 Node.js 的 Hexo, 顺便自己编写了一个专用于我的博客的主题。

一周前我用 Github 开源的 Primer 重写了这个博客主题,将与我相关的个人信息修改为可配置项,然后将这个主题正式发布为了 Simple Block, 它实现了:

  • 支持使用 Jade 和 Markdown 向边栏添加小部件,或向正文前添加横幅。
  • 响应式设计,支持移动设备直接浏览,首次加载(gzip)仅 15k.
  • 为 Mac, Windows 以及 Linux 挑选了恰当的字体。
  • 正文使用 Github 开源的 Markdown 样式。

这个主题掺入了非常多我的个人偏好,比如我使用了 Jade 和 CoffeeScript, 使用了 Gulp, 以及页面中一些奇怪部分(链接到源文件什么的)。

在设计上,我参考了我原来使用的 WordPress 2012 默认主题 Twenty Twelve, 我将各个部分放到了单独的白色区块上,并使间隙透明,于是就有了 Simple Block.

我平时很少设计前端页面,自知设计水平很差,这款主题基本上代表了我近期前端设计的最高水平了。

实现细节

Hexo 支持用 Node.js 编写插件或者主题的 helper, 这是非常大的一个亮点。

我看了官网上列出的几个主题,它们都没有 npm 依赖,在说明中建议直接将 Git 仓库克隆下来,就算安装完成了。

而我的主题依赖了 coffee-script, jade 以及 marked, 所以需要一个安装依赖的过程,Hexo 并不会自动为主题安装依赖,所以需要用户自行运行 npm install.

另一方面我发现官网上的主题都直接将前端依赖放到了 Git 仓库中,我觉得这样是不恰当的,于是我使用 bower 和 gulp 来构建前端样式,然后将编译好的版本上传到 Github 的 Release 中,让用户下载编译好的版本,而不是直接克隆 Git 仓库。其实 Hexo 提供了对 CSS 方言的自动编译,但功能较弱,没有办法完全替代 gulp 等构建工具的功能。

以上是我在开发这个主题的过程中发现的 Hexo 主题机制的一些问题,我还没探索出合适的解决方案,各位在安装的过程中如果遇到问题,请向我提出。

Mabolo: 轻量级的 MongoDB ORM

一开始我像很多人一样使用 Mongoose 作为 ORM, 但时间长了我发现了 Mongoose 的一些不理想的地方。

Mongoose 通过定义 Setter 的方式记录了对文档的每一次修改,以便可以用 save 方法将文档无冲突地储存在数据库中。但我在实际使用中发现,我很少会使用这个功能,每当对文档进行更新的时候,几乎都是直接使用 MongoDB 的原子性操作符($set 等)。Mongoose 在这个功能上下了很大功夫,也增加了很多额外的约束。例如它 使用了一些黑科技 来阻止用户修改从数据库查出的文档。而我希望从数据库中查出文档后进行一些加工,向文档上储存一些额外的数据来供渲染页面时使用(但不储存到数据库),本来我们在 JavaScript 这样的语言中是期待一个对象是可以被随意修改的,但在 Mongoose 中却不可以。

我发现我其实只需要 Mongoose 的一小部分功能,于是我自己编写了 Mabolo, 我对它的定位是一个轻量级、无黑科技的 ORM. 它完成于 2015 年初,目前已被使用到了我的大部分个人项目中。

Mabolo 用 300 行代码实现了一个 ORM 最核心的一些功能:为数据集合定义字段的类型、验证文档字段的合法性、定义类方法和实例方法、嵌入式的文档和数组、原子性地更新整个文档、同时兼容 Promise 和 callback 风格的 API.

Mabolo 几乎没有使用什么黑科技,每个 Model 都是一个普通的 JavaScript 构造函数,而每个文档则都是由这个构造函数生成的实例 —— 除了几个用来保存内部状态的不可枚举属性之外和普通的对象没有任何区别。

接下来我来谈一谈 Mabolo 中的几个实现细节。

定义 Model

在 Mongoose 中,要先创建 Mongoose 实例(即代表一个数据库连接)才能根据它来创建 Model, 但这样会造成 Model 定义依赖于这个全局的数据库连接。而在 Mabolo 中,可以先创建与实例无关的 Model, 然后再将其绑定到 Mabolo 实例上:

User.coffee:

Mabolo = reuqire 'mabolo'
module.exports = Mabolo.model 'User',
  name: String

app.coffee

Mabolo = reuqire 'mabolo'
mabolo = new Mabolo 'mongodb://localhost/test'
User = mabolo.bind require './User'

即使 Model 还没被绑定到 Mabolo 实例上,也是可以执行查询的,这些查询会被阻塞,直到 Model 被绑定到一个数据库连接上。

实现上,Mabolo.model 会创建一个继承(CoffeeScript 的 extends)自 AbstractModel 的类,作为 Model 来使用。在绑定时,Mabolo.bind 会调用 Model 上的 bindCollection 函数,这个函数会 resolve 一个内部的 Promise, 让对数据库的操作开始执行。

嵌入式文档

Mabolo 中 Model 的字段定义,既可以是基本类型,也可以是另一个 Model, 还可以是基本类型或 Model 的数组。

Token = mabolo.model 'Token',
  code: String

User = mabolo.model 'User',
  tokens: [Token]

在保存文档到数据库时,Mabolo 会调用 Model::transform 构造字段定义中的嵌入式文档,这样才可以运行定义在嵌入式文档上的字段验证。而在从数据库取出文档时,也会构造字段定义中所描述的嵌入文档, 以便用户调用嵌入文档上的实例方法。

下一步会支持在嵌入文档上运行 update 和 remove 方法。主要实现方法是 Model::transform 会在构造出的嵌入文档上储存父文档和在父文档中的位置,以便 update 时为查询和更新中的字段名加上前缀。

再之后会支持文档中的引用关系,在从数据库中取出文档时,Mabolo 会自动取出被引用到的文档,这个过程被称为「填充」,用户也可以自己定义更复杂的填充规则。

原子性地更新文档

Mabolo 使用了和 Mongoose 类似的技术来原子性地更新整个文档,即在每次更新时都为文档设置一个版本号(在 Mabolo 中是一个随机的字符串),在进行原子更新时会将当前版本号作为一个查询条件来运行更新,如果没有成功(版本号被另一个操作修改了),会从数据库中查出最新的文档,重放修改然后再一次尝试提交。

user.modify (user) ->
  Q.delay(1000).then ->
    user.age = 19
.then ->

Mabolo 使用了一种更简单的方式实现重放修改 —— 即要求用户传入一个无副作用的修改函数,这个函数会在每次重放修改的时候被调用一次。应该说这只是一种不推荐大量使用的备选方案,更好的做法是直接使用 MongoDB 的原子操作符:

user.update
  $set:
    age: 19

Cichorium: 基于 Promise 的中间件路由框架

Cichorium 的代码仅有 130 行,用 CoffeeScript 风格实现了一个简单的基于中间件的路由框架,其中的异步操作都是以 Promise 风格提供的。

Cichorium 有一个路由表和一个错误路由表。路由表是一个数组,其中每个元素可以是一个子路由表(数组),或一个中间件(函数)。

中间件会被按顺序地执行,中间件可以返回一个 Promise 表示这是一个异步中间件,Cichorium 会等待这个 Promise 被 resolve 再执行下一个中间件。在中间件中,可以用 nextRoute 来跳过当前路由表上的其他路由,直接进入父级的下一个路由,条件路由(例如匹配 HTTP 方法或 URL 前缀)就是这么实现的。

如果执行过程中抛出了异常(或 Promise 被 reject),就会进入错误处理,错误路由表中的路由会按照同样的规则被逐个调用,如果错误处理过程中抛出了新的异常,那么新的异常会替换掉之前的异常,错误处理中间件可以用 errorResolved 来解决这个异常,剩余的中间件就不会再执行了(但如果有多个异常则下个中间件会以之前的一个异常被继续调用)。


一开始创建这个项目只是想造一个 express 的轮子,后来学习了 Promise 之后觉得 express 和 Promise 的配合有一些麻烦,express 不能识别中间件返回的 Promise, 而必须手动调用 next.

express 使用 next('route') 来跳过剩余的中间件,但我发现用抛出一个特殊的异常(Cichorium 的 nextRoute 就是这么实现的)效果会更好,而且因为有 Promise, 在回调函数中抛出的异常也会被正确地传递回 Cichorium.

精子生于 1995 年,英文 ID jysperm.

订阅推送

通过 Telegram Channel 订阅我的博客日志、产品和项目的动态:

王子亭的博客 @ Telegram


通过邮件订阅订阅我的博客日志、产品和项目的动态(历史邮件):

该博客使用基于  Hexo  的  simpleblock  主题。博客内容使用  CC BY-NC-ND  授权发布。最后生成于 2024-04-08.