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

从被误解到最流行:论 JavaScript 如何完成华丽转身

有人说「JavaScript 是花了 10 天时间匆忙被设计出来的语言」,也有人说「凡是能用 JavaScript 写出来的,最终都会用 JavaScript 写出来」。写这篇文并非要对 JavaScript 做一个全面的优劣分析,而是想与大家分享一些存在于 JavaScript 及其生态系统中的、在我看来比较有趣的闪光点。

插件化的语言特征

JavaScript 曾经是一门兼容性最糟糕、升级最困难的语言。开发者们要苦等到所有用户都升级了浏览器,才敢使用新版本的特征。然而在最近几年,随着 Babel 等编译器的兴起,越来越多的 JavaScript 开发者们都放开了手,开始在生产环境中使用那些尚未被纳入标准的语言特征了。

使用了 Babel 的项目需要在发布之前引入一个「构建」的步骤,将使用了较新的语言特征的源代码转译为兼容性更好、被所有浏览器所支持的早期版本的 JavaScript,所以开发者就不必再去关心用户的浏览器是否支持这项新特征了。

Babel 是一个开源的、插件化的编译器框架,JavaScript 的每个语言特征(包括那些还未被纳入标准的)都被实现成了一个插件,插件可以遍历和替换 AST,进而对编译的结果施加影响。令人兴奋的一点是 Babel 让语言的特征形成了模块化,也就是说开发者可以在构建脚本中来配置要使用的语言特征。

Babel 的出现大大加速了 JavaScript 的进化。因为一旦有人希望在 JavaScript 中加入一个新特征,他首先会去实现一个 Babel 插件,然后很快就会有开发者去使用这个插件(这个过程不过是修改一下构建脚本)。这样新特征会得到来自一线开发者的验证和反馈,并有效地得以改进,如此形成一个良性循环。对比来看,某一些语言的新特征在设计和普及阶段进展非常缓慢。因为如果一个特征无法成为标准,就不会有开发者使用,而没有开发者使用,标准的制定者又无法得到足够的反馈,进而推迟进入标准的时间。

总有一种适合你的方言

除了对 JavaScript 本身的增强,社区中还有着上百种编译成 JavaScript 的「方言」。创造一种 JavaScript 的方言并不难,你只要编写一个从源代码到 ES AST 的词法和语法分析器,后续的步骤交给 Babel 就好。社区中比较知名的几种方言有:

这些方言有着各自的风格,从外观来看语法完全不同,但它们最终都会编译成标准的 JavaScript,这意味着它们之间是可以互操作的,你可以在一个 TypeScript 的项目中使用 CoffeeScript 编写的库,反之亦然。你甚至可以在一个项目中混用不同的方言。

开发者很少需要担心新特征或方言带来的不稳定性,因为代码最终会被编译成标准的 JavaScript,只要编译的过程没有错误,最后都是交由 JavaScript 引擎来执行,这并没有为 JavaScript 引擎带来新的复杂度。一旦有一天你决定不再使用某个特征或方言时也不要紧,直接使用编译后的 JavaScript 就好了。

这样一来,可以说 JavaScript 不再是一门语言,而是一个 JVM(JavaScript Virtual Machine)了。同时因为浏览器厂商(它们是这个世界上最大的科技巨头)之间的竞争和合作,JavaScript 有着几乎是所有虚拟机语言中最好的性能。

精简而灵活的语言核心

JavaScript 的标准库仅包含了非常有限的功能,某种程度上来说这也是件好事 —— 精简的标准库给第三方库留出了充分的竞争空间,真正得到大家认可的库才会被广泛使用,而不仅仅因为它被包含在了标准库中。

JavaScript 语言本身并没有定义得非常好的「范式」,你可以使用函数式的风格,比如函数作为参数和返回值、闭包、lodash 等函数式工具Immutable.js 提供的不可变数据类型(ES2015 甚至还包括了尾递归优化);你还可以使用面向对象的风格,比如使用原型 prototype 构造出具有静态成员和实例成员、支持继承和多态的类(ES2015 也添加了 class 这个关键字来更加方便直观地定义类)。

正是 JavaScript 的这种灵活性,赋予了类库的设计者很大的施展空间。很多知名的类库可以说是创造了一种新的编程范式:

  • Backbone:面向对象的 ORM,通过事件模型来通知对象的变化。
  • Express:通过定义串联的「中间件」来处理 HTTP 请求。
  • React:每当状态发生变化便重新渲染整个页面,减少用户界面状态管理的复杂度。

不止于浏览器环境

JavaScript 不仅可以在浏览器中运行,因为它精简的语言核心(甚至不包括任何 IO 相关的功能),现在已经被移植到了其他很多平台:

  • Node.js:提供了访问文件系统、进行网络操作的 API,用于构建 Web 后端等服务器程序。
  • Ionic / Cordova:提供访问移动设备的 API,使用 Web 技术来构建移动应用。
  • Electron:让 JavaScript 可以同时访问 Web 和 Node.js 的 API,以便用 Web 技术来构建桌面应用。
  • React Native:用 JavaScript 去操作原生 UI 组件来构建移动应用。

这些环境下有着和浏览器中完全不同的 API,但运行的都是同样的 JavaScript 代码,你的业务逻辑代码可以在这些环境间共用。JavaScript 社区中大部分已有的、不依赖具体运行环境的工具库都可以不加修改地运行在这些新环境中。

异步单线程是把双刃剑

无论在浏览器还是 Node.js 中,JavaScript 都采用了异步单线程的并发模型,所有的 IO 操作都采取异步执行,并通过「回调函数」来接收结果。以 Node.js 为例,引擎内部使用了一个固定数量的线程池,通过操作系统的「IO 多路复用」功能来进行 IO 操作,这样即使有大量并发的 IO 操作,也不过是多花了一点内存来维护相关的数据结构,并不会创建新的线程。这也是为什么大家都说 Node.js 适合高并发场景的原因了。同时 JavaScript 暴露给开发者的线程只有一个,只有这个线程会执行 JavaScript 代码,所以开发者不必象其他一些多线程语言那样去关心线程同步和线程安全的问题。JavaScript 开发者对于异步任务的接受程度也更高,他们会尽可能地让没有依赖关系的操作并行执行,让无谓的等待时间最小化

作为代价,JavaScript 中所有的 IO 操作都需要通过传递 回调函数 的方式来获取结果,初学者会为此非常苦恼 —— 编写循环、处理异常时会束手束脚,异步回调的写法也非常繁琐,一不留神回调函数的嵌套就会失去控制。为此社区创造了很多语言特性和工具来试图解决这个问题,包括 EventEmitter、async.js、Promise、co/generator、async/await 等。虽然基本可以认为 Promise 是未来的趋势,但目前还并没有普及到所有的 JavaScript 开发者,而且在这几种异步流程控制方案之间互相调用也很令人头痛。此外因为只有一个 JavaScript 线程在运行,所以如果在一个函数中有 CPU 密集的计算任务,它就会阻塞整个事件循环的处理,此时需要开发者手工让出线程,来处理事件循环中其他的事件。

好了,怕篇幅再长反而会分散大家对内容的理解和印象,就此收笔。我这儿还有些其他相关的内容,感兴趣的朋友可以继续读下去。

semver:语义化版本规范在 Node.js 中的实现

精子又开了一个新系列!我计划在这个系列中每篇文章介绍一个 NPM(Node Package Manager)上的包,来向大家分享一些我在使用这个包的过程中的经验,同时也会延伸到一些相关的技术,例如如果介绍 redis 这个包,那么我也会顺便介绍一下 Redis. 因为对于一些使用广泛的包我可能需要更多的时间来搜集资料,所以一开始会从一些比较小而专的包开始。

semver

我们先从 semver 这个不起眼的包开始,它是 语义化版本(Semantic Versioning)规范 的一个实现,目前是由 npm 的团队维护的,实现了版本和版本范围的解析、计算、比较,在 NPM 的被依赖(Most depended-upon)榜单中排名 34.

Semantic Versioning 是由 GitHub 的联合创始人 Tom Preston-Werner 建立的一个有关如何命名软件和库(包)版本的规范,用以解决在大型项目中对依赖的版本失去控制的问题(例如你可能因为害怕不兼容而不敢去更新依赖)。现在 Semantic Versioning 已经在开源社区中得到了广泛的认同,Node.js 的包管理工具 npm 也完全基于 Semantic Versioning 来管理依赖的版本。

semver 定义了两种概念:

  • 版本是指例如 0.4.11.2.71.2.4-beta.0 这样表示包的特定版本的字符串。
  • 范围则是对满足特定规则的版本的一种表示,例如 1.2.3-2.3.41.x^0.2>1.4.

在这两种概念上可以进行很多种计算,例如比较两个版本的大小、判断一个版本是否满足一个范围、判断一个版本是否比范围中的任何版本都大等。

显然这个包的使用场景就是与版本号打交道。例如你有一个客户端工具,需要在每次启动时向服务器发起一个查询来检查更新,那么用 semver 去比较版本将会是一个很好的选择:

console.log(semver.gt(lastestVersion, currentVersion) ? 'New Update available' : 'Already lastest version');

基于 Semantic Versioning 规范,我们还可以计算出两个版本之间的差异程度:

console.log(semver.diff(lastestVersion, currentVersion) == 'major' ? 'Major Release' : 'Minor or patch Release');

再比如你在实现一个插件化的系统,每个插件(在 package.json 中)都会声明一个所兼容的主程序的版本范围,而主程序在加载插件时需要检查这个版本并使当地给出警告:

plugins.forEach(function(plugin) {
  if (!semver.satisfies(platformVersion, plugin.engines.platform))
    console.log(plugin.name, 'require', plugin.engines.platform, 'but unable to meet');
});

在你使用 express 设计一个支持多种版本的 API 服务器时,你可以这样做:

app.get('/', apiVersion('<0.6'), function(req, res) {
  res.send('Less than 0.6');
});

app.get('/', apiVersion('1.2.3 - 2.3.4'), function(req, res) {
  // ...
});

app.get('/', apiVersion('*'), function(req, res) {
  res.send('Unsupported version');
});

其中 apiVersion 中间件的一个简单实现:

function apiVersion(range) {
  return function(req, res, next) {
    if (semver.satisfies(req.headers['accept-version'], range))
      next();
    else
      next 'route'; // skip this route
  };
}

也许你用字符串计算再配合一点正则也可以完成上述的场景,但你很难做到对 Semantic Versioning 的完备支持,在之后发布新版本后撰写版本号的时候也会受到限制,例如 semver 可以正确地比较 0.9.00.10.0 以及 0.9.0-beta.1,但自行实现这些支持将会非常繁琐。

所以其实选择 semver 的理由很简单 —— 让专业的包去完成专业的工作,相信这也是在 Node.js 社区得到了广泛认同的观点。

2015 年度小结(技术方面)

从 2014 年末开始开发的一个互联网金融项目终于在今年三月份上线了,这是一个 Node.js 编写的 Web 服务,但上线仅仅是个开始,之后的半年时间我们仍在这个项目上进行着密集地开发。

就像 2014 年度的技术小结 中提到的,2014 一整年我都在进行有关自动测试的实践,经过几个项目的积累,这个项目从头至尾都有着覆盖完整的自动测试,我所有的调试工作也都是借助自动测试完成的,我甚至没有在自己的电脑上运行过这个项目的前端页面。因为路由层面受业务影响很大,经常修改一些功能的行为,所以后来大部分测试都是针对 Model 层面的单元测试。

这个项目使用了一种「以数据结构为核心」的设计,所谓数据结构就是一个 JavaScript 的 Object, 对应着数据库中数据表的各个字段,这些代表着业务实体的 Object 在项目中的各个函数之间传递。绝大部分函数的参数和返回值都是这种 Object, 它们在这些 Object 上获得或修改数据,并将这些 Object 与数据库同步,即使需要传递额外的数据,也是将数据作为属性附加到相关的 Object 上。可以说这是一种非常 JavaScript 的风格,因为这些 Object 非常近似于数据库中的一行记录,所以在单元测试中很容易构造,非常大地简化了单元测试中「构造特定环境」的这个步骤 —— 函数的输入和输出都是特定结构的 Object, 这对于 JavaScript 来讲太简单了。

随着功能的添加,业务逻辑变得越来越复杂,因为 Node.js 强制 IO 操作异步的这个特征,异步流程控制变成了一个令人头痛的问题 —— 直到我发现了 Promise。Promise 是 对异步任务的一种抽象,当我深入地理解了它的工作原理后,才认识到我在学习 Node.js 上走的最大的弯路就是很晚才开始了解和使用 Promise.

相比于编写 Callback 风格的异步代码,使用 Promise 意味着一种思路上的转变,虽然 Promise 的原理简单,但在具体的使用场景上还是需要自己做很多尝试的,例如具有分支的异步逻辑、循环地处理数据、逐级传递异常等。

在这个实践的过程中,我逐步地将自己的项目中的异步代码改成基于 Promise. 在和 Express 的配合中,我发现因为 Express 没有对 Promise 的支持,所以 Express 的路由定义实际上变成了 Promise 的「边界」,所有的 Promise 都要在这里进行一次转换,改成 Express 的错误处理机制。于是我想如果有一个支持 Promise 的路由框架将会是一件很有趣的事情,于是我花了几天的时间设计并实现了 Cichorium, 这是一个代码只有一百来行,基于 Promise 来提供异步中间件和错误处理的路由框架。

在使用 Promise 的过程中,也让我对「异常」有了更加深入的认识,异常是现代语言所提供的非常强大的流程控制机制,让本来唯一一条通常的、正确的执行路径变得可以从任何一处中断,并进入一个所谓的异常处理流程。异常可能包括「预期到的情况」和「非预期的情况」,如果在自己的代码中抛出了异常,那么通常是属于可以预期到的情况,例如参数错误、前提条件不满足等,抛出异常的目的是为了中断正常流程,并通知调用者;而非预期的情况则可能是所依赖的库抛出的异常,或因运行时错误 JavaScript 引擎抛出的异常。

异常会被调用栈上离异常被抛出处最近的处理程序捕捉到,一旦异常处理程序「解决」了这个异常,其他的异常处理程序就不会再得到通知。所以处理程序应当只去处理已知的、必须在此处理的异常,然后将其他的异常继续向其他处理程序抛出,最后到达一个「边界」,例如作为 HTTP 相应发给客户端,或打印一条日志。

这个项目在上线初期时间赶得比较紧,加上经验不足,在上线后的前几个月时间一直都在遭遇性能问题。中间出现过几次因为并发请求过多,多个请求修改同一条数据进而出现的数据不一致的情况。本来是有一个通过简单的 Redis 锁限制一个用户同时只能有一个写入数据的请求的机制的,但毕竟不是根本的解决方案。于是我开始尝试使用 MySQL 的事务,将一组相关的 SQL 查询放入一个事务中执行,对于有前提条件的更新操作(例如扣余额后余额不能为负数),将前提条件作为一个更新条件,如果执行后发现并没有行被更新,就说明前提条件不满足,然后回滚这个事务,向客户端报告失败。借助于数据库提供的原子性和一致性,即使并发很高,或者程序崩溃,都不会出现数据不一致。

使用事务只是解决了在高并发情况下的数据一致性的问题,但并没有解决性能问题。这个项目中的数据主要是财务记录,用户的每一次操作都会生成财务记录,这些数据被用来追踪每一笔资金的流向,会被聚合起来用于给用户展示统计信息,这个过程需要对数据进行筛选、分组、排序等复杂的计算。

显然这些计算如果在数据库中计算会有更好的性能,因为不需要在程序和数据库之间传输大量的数据,而且 MySQL 应该会对这类计算有更好的优化。于是我开始补习 SQL, 将几乎全部的筛选、分组、排序逻辑都在 MySQL 中完成。同时我开始学习如何分析 MySQL 的性能瓶颈,最简单的就是慢查询日志,曾经一度有一些查询需要 300 秒的执行时间。至于解决方案,除了优化查询条件之外最主要的就是加索引了,我也花了一些时间来了解索引背后的原理和最佳实践。

这些统计数据和时间是强相关的,过去的数据通常来讲就不会再修改了,所以如果能够将这些数据的统计结果缓存起来,将会显著地提高性能。其实本来也有一个简单的缓存机制,用户访问统计信息后会被缓存,但一旦用户执行任何财务操作都会使整个缓存刷新。所以很容易想到的是进行更细粒度的缓存,即在时间的维度上应用所谓的「套娃娃缓存」,在 Redis 中以天为单位缓存发生的财务变动、当日结束时各项统计指标的值。如果某一天的财务数据发生变动,只需以前一天的数据为基础去计算之后的数据,大多数情况下历史数据是不会改变的,只会刷新当天的缓存。

这项修改花费了不少时间,因为需要重写所有生成统计数据的代码,在前一天的计算结果的基础上计算出当天的统计数据,并连同一些中间结果一起缓存起来,供下一天的计算使用。相当于将原来一个简单明了的计算过程被拆分成了若干个小步骤,步骤之间还需要通过 Redis 来交换数据,看似复杂,但减少了很多不必要的重复计算,上线之后将性能提高了差不多一个数量级。

这个项目大概是我这一年完成的最满意的项目了,我参与到了绝大部分的设计工作中,也完成了差不多一半的编程工作,从头至尾都有着完整的自动测试覆盖,借助 Promise 实现健壮的异步流程控制和异常处理,在高并发的情况下实践了事务、缓存、索引相关的知识。


我从年初 开始使用 Atom 完成我的全部工作,选择 Atom 并不是因为它已经有多么好用了,而是因为 Atom 有着优良的设计和活跃的社区。最近两年我工作都是使用 Node.js 来完成的,而 Atom 也基于 Node.js 和 Web 技术构建起来的,甚至 Atom 也是用 CoffeeScript 实现的,这种相同的技术栈,令我非常有「安全感」。我也在了解和学习 Atom 的实现,它有着完全插件化的架构和设计良好的 API, 对我后来重构 RootPanel 都非常有帮助。

在我了解 Atom 的过程中,我发现中文网络上对 Atom 的讨论非常分散,于是我创建了 Atom 中文社区,到年末已经有 800个注册用户和 1000 个帖子了。说实话,中文技术社区的氛围并不好,因为可能技术能力较强或英语水平较高的人会直接选择去参与官方的社区,目前也基本上是我一个人在回答问题、翻译官方博客和文档、汇总一些资料,不过既然我还在用 Atom, 就会一直将这个社区维护下去。


RootPanel 在 2015 年上半年依然在缓慢地进行着,因为通过阅读 Atom 的代码学习到了大量有关插件化设计的方法,所以我这半年并没有向 RootPanel 中添加新功能,而是一直在反复地重构 RootPanel 的架构。

首先是为其中的重要概念建立抽象,例如服务组件(MySQL 数据库、Nginx 站点之类)、计费方案(计费周期、价格、限制)、支付渠道、控制台上的控件等。之前虽然也有针对这些概念进行抽象,但基本上是写到哪里、需要什么接口,就添加一个相应的接口,缺乏一个全局性的规划。进而导致抽象出的概念不够简洁、不够彻底(有一些插件的逻辑仍散落在核心代码中)。

JavaScript 本身是一个很灵活的语言,对象本身是「无模式」的,属性和方法都可以随意地修改,也提供了「原型链」来支持对象之间的继承关系。为概念建立抽象的一种有效途径就是「面向对象」风格的设计,Atom 就采用了这样的设计,我觉得面向对象对于 RootPanel 可能同样很合适。

面向对象首先统一了「数据」和「行为」,让数据可以带有行为,而在执行这些行为的时候又不必显式地传递数据;对象本身也是一个抽象层级,只要两个对象有相同的属性和方法(而不论背后的行为),就可以被当作同一种对象操作,即所谓的「鸭子类型」,这对于插件化的系统而言十分便利。

于是我用了一部分面向对象的风格来重构 RootPanel, 将其中很多概念抽象为了类,为每个模块起一个恰当的名字,减少不同模块之间的依赖;为模块划分「级别」,建立层级一致的抽象 —— 即在任何一个层级来看,抽象都是完整的,让同层级的类来打交道,而不是将层次不一的类混在一起。


在 2014 年我就一直对 Mongoose 有很多不满,一直想自己造一个轮子,在 RootPanel 的开发过程中也遇到了 Mongoose 的一些坑和一些难以实现的需求,于是今年终于行动起来了,然后就有了 Mabolo —— 一个轻量级的 MongoDB ORM

我对 Mabolo 的定位是一个简单的、「没有魔法」的 ORM, 每个 Model 都是一个普通的 JavaScript 构造函数,而每个文档则都是由这个构造函数生成的实例 —— 除了几个用来保存内部状态的不可枚举属性之外和普通的对象没有任何区别。Mabolo 不去追踪数据被改变的情况,而是鼓励使用 MongoDB 的原子操作符进行数据更新,Mabolo 仅在更新后帮你将最新的数据同步到这个对象上。

嵌套对象是 MongoDB 的特色之一,在实际项目中也经常会用到这样的设计,于是我也为 Mabolo 添加了嵌入式对象的支持,允许将 Model 中某个字段的类型设置为另一个 Model. 在储存到数据库前会运行所有子 Model 的验证方法,在从数据库取出结果后会为每个子 Model 字段构造相应的对象,以便在这些子 Model 上运行更新和删除等方法。


五月初的时候和 Yeechan 等人参加了 SegmentFault D-Day 上海站 的活动,主要听了有关 Docker 和 React 的主题分享。

因为我开发 RootPanel 的经验,对 Docker 这种性能损耗极低的虚拟化技术自然十分感兴趣,在参加这次活动之前就去简单地了解过 Docker, 当时我对 Docker 的不解主要在于 Image 只能单继承,这样就不太容易像「搭积木」一样去组合自己想要的环境,这可能是因为文档上面那个搭积木的示意图对我的误导比较大。

经过这次的主题分享,我才比较全面地了解到基于 Docker 去部署应用的思路,即既然创建容器的成本是极低的,那么可以为系统中的每个部分去创建单独的 Image, 运行单独的容器,然后通过 Docker Compose 这类工具去组合容器。Dockerfile 描述了应用的运行环境和依赖项,而 docker-compose.yml 描述了如何将一个系统中所需要的各个部分组合起来,完成了关于一个系统的完整描述。在实际运行时,因为容器之间的联系非常少,通常只暴露几个网络端口,所以给整个系统带来了非常好的横向拓展的能力,系统的每个部分都可能会运行多个容器,甚至这些容器可能会分布在不同的物理服务器上,同时提供一致的服务。

因为 Docker 是内核级别的虚拟化,对系统调用的抽象代价很低,而因为使用了 AUFS 对文件系统进行抽象、需要建立虚拟网卡进行端口转发,所以磁盘和网络 IO 的抽象开销相对较大。所以 Docker 更适合计算密集型、依赖复杂(这样才能发挥 Docker Image 的优势)的程序,就是通常 Web 项目中负责处理请求的「应用」这部分,而将数据库等 IO 密集、部署简单、不频繁升级的程序直接部署在物理机上。

现在 Web 后端程序面临的主要挑战就是高并发,保证单个程序的稳定性,倒不如采用分布式的架构,将一个处理能力强的实例拆分为若干个处理能力较弱的实例,转而保证一旦有实例失效,可以立刻重新创建一个实例接替它继续工作。但如果在实例中储存了一些全局的状态(例如锁)就无法通过启动多个实例的方式来横向拓展。所以比较理想的实践就是将应用实现为「无状态」的,即容器中的应用只根据来自网络的请求进行计算,对数据库、缓存和文件系统的调用同样通过网络去请求容器外部的服务。这样才可以进一步利用 Docker 的优势 —— 容器可以根据规模需要随时去在不同的物理机上创建和销毁而不需要同步数据。

随着对 Docker 了解的深入,我开始意识到 Docker 对 RootPanel 这类 PaaS 平台是一个「杀手级」的应用,像 RootPanel 那样笨拙地使用一系列 Linux 的机制和工具去隔离用户和直接使用 Docker 相比毫无优势,让我很有将 RootPanel 改为基于 Docker 的架构的冲动。但想来想去还是放弃了这个想法,因为一方面这个改动可能会非常大,另一方面其实已经有了很多非常优秀的基于 Docker 的开源 PaaS 程序了。

后来我加入 LeanCloud 负责云引擎的开发工作,云引擎实际上就是一个基于 Docker 的 PaaS 平台,各方面都和 RootPanel 非常相似。既然日常的工作已经是这样一个项目了,所以进一步促使我中止了 RootPanel 的开发。但说实话我对 PaaS 还依然有兴趣,也许有一天我会根据我在 RootPanel 和 LeanCloud 的经验,重新设计一个最简架构的 PaaS 来纪念 RootPanel.

随着在工作中深入地了解 Docker, 在年末的时候我将我的服务器上应用全部换成了基于 Docker 来运行,这样的好处就是每个应用都可以有自己的环境,而且每个服务的环境和服务之间的依赖关系都被描述在了 Dockerfile 和 compose.yml 中,彻底解决了以前服务器上各种应用「乱七八糟」的现象,以后若要迁移服务器或重新部署将会变得非常容易。


过去一年我花了不少时间断断续续地将「JavaScript 权威指南」和「计算机程序的构造和解释」看完了,对 JavaScript 的了解也进了一步,其实 JavaScript 对函数式风格的代码还是有很不错的支持的。按我在 JavaScript 中对函数式编程的实践,最有价值的的两点就是「无状态」和「无副作用」。

随着前端应用越来越复杂,所展现的数据之间的逻辑关系也越来越复杂,也出现了很多框架来解决前端 UI 和数据(即状态)之间的同步问题,其中之一的 React 从一个非常有趣的角度来入手 —— UI 可以是应用状态的一个函数,给定一组状态就有一个确定的 UI. 如果每次状态发生变化都重新渲染整个 UI, 便可以极大地降低管理 UI 和 状态的复杂度。

React 还在浏览器提供的 DOM 上建立了一层抽象,在每次重新渲染 UI 时,React 操作的都是 Virtual DOM, 而后再去与真正的 DOM 进行对比,更新必要的部分。我觉得这种抽象还是非常有价值的,Virtual DOM 限制了很多操作,但它提供了优化性能的空间,也为将 React 程序迁移到非 Web 平台提供了可能性,例如后来我就尝试过在服务器端使用 React 来渲染 HTML.

后来我在 RootPanel 和其他一些项目上实验性地使用了 React, 我也使用了官方推荐的 JSX 来编写代码,React 这种将 JavaScript 作为应用主体的做法很不同于一些将 HTML 作为应用主体的框架。有一些人批评 JSX 将这些年好不容易才分开的 HTML 和业务逻辑(JavaScript 代码)又重新混在了一起。而我则认为「模板语言」的出现一方面是因为部分语言表现能力较弱,需要模板语言将 HTML 和琐碎的语法细节分离;另一方面则是试图在数据和冗长的 HTML 表现之间建立一层抽象。JavaScript 本来已有很不错的表现能力,JSX 又添加了一些与 HTML 相融合的语法;React 通过引入「组件」的概念来拓展 HTML 的标签,让用户可以自己创建包含内部逻辑和状态的标签,进而让 HTML 表现不再冗长,所以分离就变得不必要了。

总体上来讲我对 React 很有好感,因为我觉得 React 很好地实现了一些函数式编程的风格,来简化 UI 编程中对状态的管理,React 鼓励将组件设计为无状态的,同时将渲染过程设计为无副作用的,这样无论何时,只要状态发生改变就重新渲染整个 UI 即可。

在我后来编写 LeanEngine Snipper 的时候,需要在前端进行大量数据处理以便根据用户的筛选来展示图表。一开始没有考虑太多,部分函数是会修改其参数(往往是一个包含大量对象的数组)的,在后来支持用户修改筛选条件时就遇到了问题 —— 原始数据在绘图的各个环节中都有可能被修改,不得不在开始绘图之前对原始数据进行一次 clone, 在后来的性能分析中发现 98% 的时间都花费在了 clone 上面。

于是我不得不重构代码,让大部分函数不修改参数,而是在参数的基础上返回一个新的对象,将需要 clone 的数据减少到了最小,经过这次的优化,筛选的性能提高了 40 倍以上。从直观感受上来看,每个函数返回新的对象会消耗更多的资源,但在 JavaScript 中,返回新对象实际上只是在拷贝它的属性的引用,并不会花费多少时间,反倒是在 clone 对象时需要遍历所有的属性,才需要花费大量的 CPU 时间。


因为最近两年都在使用 Node.js, 我希望也使用 Node.js 来驱动我的博客,我最后选择了插件化架构的 Hexo —— 一个静态博客生成器,我自己编写了 主题,并将博客的数据也托管在 GitHub 上。后来我将 RP 主机博客粉丝团主页 也都迁移到了 Hexo, 后来新建的 皮蛋豆腐的博客 也使用了 Hexo.


今年我作为 HackPlan 的成员,参与了几次招聘,后来我也作为求职者参加了几次面试。

国外的一些职业,包括医生、律师,也包括工程师,都普遍地去打造自己的个人品牌,目的是为了找到更好的工作。确实在过去两年中这种个人品牌对我的工作是很有帮助的,在我面试的过程中,我去的几乎所有公司的面试官都表示曾经听说过我。虽说技术岗位以能力为先,但至少如果混个脸熟,双方会有一个基本的信任。

我当时说在找到工作之后会和大家分享一下参加面试的经验,但后来想了一下,写出来的话应该都是关于我没有选择的那些公司的负面评价,大家都是同行,这样不是很好,所以后来只写了 加入 LeanCloud 的过程。


说实话,现在使用 Node.js 的公司依然是少数,因此在求职时我也将 PHP 纳入了考虑。在我离开 PHP 之后,社区发生了许多变化,出现了像 Laravel 这样设计优良的一站式框架,composer 这个包管理器也被越来越多的人接受。为了重新捡起 PHP 这个技能,我花了一些时间用 Laravel 做了一个最简单的论坛系统的轮子 —— labbs-laravel.

在之前,无论是 PHP 还是 Node.js 中,我都没有使用过像 Laravel 这种重量级的框架。Laravel 不同于国内一些粗制滥造的重量级框架,虽然它提供了很多功能,但却并不显得臃肿。首先 Laravel 并没有选择造轮子而是构建在 Packagist 中已有的包之上,它有着一个非常精简的核心架构,除了经典的 MVC 支持外,其他的各类功能(认证、缓存、队列)都被抽象成了「服务」,这些服务可以独立为单独的包发布在 Packagist 上,且同类的服务是可以互相替换的。

Laravel 对我来讲最大的亮点是 ORM 部分(Eloquent),我之前用过的 ORM 比较少,在实现 Mabolo 的过程中一直在纠结如何实现对象之间的引用关系。Eloquent ORM 将关系本身也抽象为了一个类,当你访问一个对象的关系字段时,得到的是一个「关系对象」,你可以在这个对象上进行筛选和查询等操作。其实这样的设计还是非常直观的,但因为我之前闭门造车,一直没能「独立发现」,在新的一年中我会用这样的思路去给 Mabolo 添加关系支持。


最后如果做个总结的话,我这一年依然主要在编写 Node.js 代码,也写过少量的前端代码,对 JavaScript 的了解越来越深入。这一年的我在关注基于 Promise 的异步流程控制和错误处理、深入了解关系型数据库和 SQL、探索函数式风格的 JavaScript、探索和学习插件化架构的设计、借助 Docker 来管理应用的部署和拓展。

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

订阅推送

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

王子亭的博客 @ Telegram


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

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