人生小事——我的故事

快过年了,才突然发现,即将过去的一年是个有趣的年份:工作八年,学编程十六年,虚岁三十二。前者各占后者一半。

人生充满了巧合。

都说“人生大事”,回忆往昔,对我有重大影响的,却偏偏是一些小事。

在这个寒冷的冬夜,躲在被窝里码字,给大家讲讲——我的故事。

Read More

编程熟手入门Go语言

Introducing Go

2018年看完的第一本书,《Introducing Go》是一本介绍 Go 语言的书,针对人群是已经有编程经验的读者。全书,算上封面、目录、附录、索引,总共才 124 页,实际阅读时间(包括手打一遍示例代码并完成每章后面的习题),不到15个小时。我是一次一小时左右断断续续看完的,但是需要的话,完全可以在一个周末看完。

总体评价是短小精悍,有编程经验的读者入门 Go 语言的理想书籍。

Read More

npm scripts

代码两周前就完成了,但是上周工作忙,这周又小小沉迷了泰拉瑞亚,现在才来进行总结。

这次主要讲讲返璞归真,用 npm 取代 Gulp 对前端项目进行一些处理。

这篇文章涉及内容过多,似乎应该拆成多个主题

Read More

加密货币

to the moon, banner of /r/dogecoin

很久以前就知道加密货币这回事,但一直停留在听闻的阶段,虽然也从技术角度对其基本原理进行了了解,但从没真正尝试通过挖矿或购买去拥有一枚。

但是最近加密货币实在太火了,开车听个广播也都是相关的新闻,以至于你根本没办法不去注意它。所以,上个周末,我尝试着进一步了解了一下,发现围绕着加密货币,已经产生了一系列有意思的新事物和商业模式,无论加密货币是不是泡沫,这些商业模式却有真实的参考价值。

Read More

聚沙成塔

一个平面设计的作业,形状勉强像塔

今天初步把以前的博客从时光机搬了回来。勉强能用RSS文件导回一些,更多是靠手动搬运,部分文章的排版仍是混乱的,有空再慢慢整理吧。

没有这些博客,我都忘记自己是怎么过来的了,早期的文章记录的都是些细碎的东西,解决的都是一个个非常具体的问题。虽然,好多解决方法现在看来都不完善,同样的问题其实也可能是其他原因导致的,那时却根本无法看到,只知道某个方法奏效了,那就记录下来。

因为简单,产量就很高,最多的时候半个月能写5篇,简直难以想象。光光解决各种乱码问题就能写上3篇,毕竟,写第一篇的时候想不到还会需要另两种方法来解决一个看似相同的问题。

但正是这些细小的沙子慢慢往上垒,我今天才有机会从更高的视角来俯视曾经的稚嫩吧。

时光机和回忆之旅

时光机和回忆之旅

今天又用时光机找回以前的博客看了看,终于下定决心要将这些逝去的记忆碎片拼凑回来。
这个随着阿荡的服务器到期丢失的,我的最初的博客。
第一次通过邮件和老外交流;第一次翻译英文文档;第一次使用安卓手机;让电脑通过手机的无线流量套餐上网。

现在看看,还真有一种奇妙的感觉。

归来的技术栈——无状态的服务端

本系列文章都是以方便开发和维护为出发点,在可能的时候,尽量兼顾性能方面的需求

本文是 归来的技术栈 的第三篇,必要时请阅读链接中的内容了解背景。

如果拿 Node.js 和 PHP 比较,我向来是喜欢 Node 多一点。但有一点是 PHP 天然具备, 而 Node 需要付出一些努力才能实现的。

默认情况下,PHP 针对每个请求会使用独立的进程来处理,完成后即销毁。虽然从性能上来说,每次执行都要重复加载文件和数据会带来负面影响,但在另一些方面,却有着先天优势。

比如某个请求发生了错误,不会对另一个请求造成影响;内存泄漏的负面效果无法累积起来影响系统的长期稳定运行;在一定程度上只要堆服务器就可以横向扩展来提高并发能力;不需要针对服务器启停编写额外的代码来初始化和扫尾;服务器意外关闭一般也不会造成什么影响(虽然也不总是如此,比如用了APC之类)。

这些都是无状态带来的好处,可以降低开发和维护的成本,并通过堆硬件提高性能。抛弃一些东西,在其他方面就可以获得更多。

完全无状态的服务端类似一个巨大的纯函数,每个参数相同的请求都会获得相同的结果,也不对外部环境产生影响。但是如果真做到这个程度,服务端就变成静态了,失去其应有的作用。

因此我们还是需要一种手段,在外部保存数据,最好是基于文件的数据库。数据库用内存加速可以接受,但是完全依赖内存就会失去对意外宕机的抵抗力。其他临时性的数据,能省则省。包括 session,虽然 session 也可以用文件或数据库保存,但是我们可以通过使用 json web token ,把它也省了。服务器上的状态越少、维护越容易。

当然,在实际工作中,我也使用缓存,但仅限于特别关键的地方。缓存必须独立于自己开发的软件存在,并且往往是以单向的、即时计算的形式来生成。独立可以确保能在不同进程间共享;单向是指只有一种方法来计算缓存,如果要更新缓存,只能用相同的方法重新计算一次,而不允许对当前的缓存进行加减等操作,这样就算出现意外的并发,也不会导致缓存中的内容出现错误;即时计算是指在第一次需要某个值时,才对其进行计算,并将结果存入缓存,直到过期。如此一来,缓存也成了“无状态”的,缓存的内容不受计算次数的影响,宕机重启也不会影响软件的整体运行,缓存会在需要时各自重建。虽然这种做法没有将缓存的价值发挥到最大,但由于开发和维护成本极低,有很高的性价比。

Node.js 毕竟不是 PHP,要在 Node.js 和 Express 环境中做到完全的无状态有点费力不讨好,但是基本的思想可以借鉴,尽量减少服务端要维护的状态,并在模块的级别实现这一目标。

要做到这一点,最基本的一个要求,就是别让变量活过一个请求,Express 的每个请求最终都会对应到一个回调函数,这个函数里不能出现其他来自外部的变量(必要的第三方库和数据库连接池会出现在其中,但从整个模块角度看,它们应该算是不可变量)。如果你在整个模块中,都能只使用 const,不用 var 来完成所有功能,并且没有作弊(把 const 定义的对象下的属性当变量用),那么就基本达成无状态的目标了。

当你的模块实现了无状态。在性能方面,就可放心的使用 Cluster 来启动子进程扩展并发能力,请求无论落入哪个子进程,都不会影响返回的结果,如果在前面加上一道负载均衡,还可以进一步扩展到多台服务器;在开发和维护方面,你可以放心的使用热加载,通过监测文件变化,在不停止服务的情况下更新软件版本。粒度比使用 nodemon 或者 supervior 等工具更细。

归来的技术栈——正确模块化,express 的 app.use

在 express 4 中,app.use 有如下用法:

1
2
3
4
5
6
7
const express = require('express');
const app = express();
const subapp = express();

app.use('/subpath', subapp);

app.listen(3000);

subapp 做为 express 的一个实例,本身也是 middleware ,可以被 app.use “挂载”到指定路径。

这种用法给我们项目中功能模块的可移植性进一步增加了保证,以之前用到的目录结构为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── app.js
└── src
├── admin
│ ├── stylesheets
│ ├── javascripts
│ ├── index.html
│ ├── client.js
│ ├── riot-tags
│ └── server.js
└── app
├── stylesheets
├── javascripts
├── index.html
├── client.js
├── riot-tags
└── server.js

两个 server.js 文件分别是 admin 和 app 的入口文件。这时我们可以这样做:

/app.js

1
2
3
4
5
6
7
8
const express = require('express');
const app = express();

const admin = require('./src/admin/server.js');

app->use('/admin', admin);

app->listen(3000);

/src/admin/server.js

1
2
3
4
5
6
7
8
const express = require('express');
const app = express();

app->get('/login', ...)
app->post('/api/login', ...)
...

module.exports = app;

这样,我们可以通过GET /admin/loginPOST /admin/api/login 来访问 admin 模块中的方法了。 在这种设计下,admin 目录中的内容自成一体,不依赖 /app.js 向它传入参数。可移植性就更好了。

由于 /src/admin/server.js 中的 app 是一个完整的 express 实例,因此它本身也可以通过 use 方法来使用 middleware。并且这个 middleware 的有效范围也会被控制在 admin 中,而不会干扰其他模块。所以这也是一种控制 middleware 有效范围的简便方法。

这个方法将模块和主程序完全解偶,如果你再进一步,将 admin 模块做成无状态的,那么就可以安全的实现热更新了,这将大大提高开发效率。

归来的技术栈

所有的设计都是从开发者面对的问题出发。

我要用有限的人手(就当是我一个人吧)进行全栈开发,中小型项目。从开发角度讲,要同时保证开发效率和业务模块的可移植性;从运行的角度讲,需要较高的可靠性但对性能要求不高。

向来是个全栈程序员。自从上次换了工作,到现在三年多。后端一直在用 PHP 和 Slim 框架,Composer 进行包管理,MySQL 做数据库,开发 restful 风格的接口,四平八稳;前端页面做成 SPA 形式,因缘际会用了 riot.js,理念和现在当红的 react 和 vue 相似,功能稍弱,但学习成本也低。

这个理念,正是这次想要更换技术栈的主要原因,上个月和朋友聊天时,说起了以前为什么会选择 riot.js,为了 riot.js 官网里的两句话:

We should focus on reusable components instead of templates
Templates separate technologies, not concerns

这个观点其实是 react 的开发者提出来的,我们要关注的是可重用的组件而不是模版,模版的概念是从技术角度出发的,而真正需要我们关心的是业务。第一次看到这个观点时,我是震撼的,因为那时我正在用 Angluar 1,老是要在文件(模型、视图、控制器)之间来回切换和定位,总觉得现状有问题,却又不知道如何改变。

那天聊完之后,我突然意识到,同样的理念可以走的更远一点,不用限定在组件的级别,用在业务模块的级别上也是可行的(尤其是对全栈开发者和中小型项目来说)。我们之前的项目根据前后端来划分目录结构,也是一种从技术角度出发的选择,而这让我遇到了一些问题。

先来看看以前的项目结构(隐藏了一些不必要的细节)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── app
│ ├── lib
│ │ ├── admin
│ │ └── app
│ └── router.php
├── lib
├── pages
│ ├── admin.html
│ └── app.html
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
└── riot-tags
├── admin
└── app

/app/lib 目录下是后端的逻辑,/pages 目录下是前端入口页面(模版,通过PHP调用展示),/riot-tags 下是前端文件,其他前端需要用到的图片、公共样式和库都在 /public 下面。

假设另一个项目需要用到 admin 模块,我只能分别将 /app/lib/admin/pages/admin.html/riot-tags/admin 拷贝到新项目里去。但是 /public 目录下的内容就比较麻烦了,不同模块用到的东西都混在一起,已经很难分清哪个被谁用了。如果再在这些目录下按模块分目录,又会变得非常繁琐。

这样分配目录还会对开发者的心态产生潜移默化的影响,两个模块的后端文件之间靠的太近,会增加相互调用,写出高耦合度代码的倾向。

再之前的工作是开发一个长期维护的平台,没有这方面的考量,而现在经常遇到将之前开发的系统中的功能挑几个出来,做成一个新系统的需求,因此模块的独立性和可移植性变得非常重要。

新的目录结构类似这种形式(隐藏了一些不必要的细节):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── app.js
└── src
├── admin
│ ├── stylesheets
│ ├── javascripts
│ ├── index.html
│ ├── client.js
│ ├── riot-tags
│ └── server.js
└── app
├── stylesheets
├── javascripts
├── index.html
├── client.js
├── riot-tags
└── server.js

除了目录之外,现在的技术栈也存在其他方面的问题,由于同时用了 PHP 和 JavaScript 两种主要的开发语言,在小公司难招人的情况下,招两个都会的人更难;需要用到两个包管理工具,开发环境、运行环境、开发工具配置什么也都要搞两种;虽然已经把服务端搞得很瘦,常用逻辑的代码生成器也做了;但同时要维护两套东西确实不够DRY的。说到底吧,还是人不够。

所以,只能尽可能把技术栈缩小,另外通过把粒度做粗一点、牺牲一定的性能来换取开发时的便利和运行时的稳定。

于是,在国庆假期的时候,时隔三四年,再次拿起 node.js 和 express,是为“归来的技术栈”(不知道为什么突然想起了《归来的奥特曼》,挺应景的,所以取了这么个标题)。

这次把整个结构从头梳理了一遍,保留之前的成功之处,再引入一些新鲜血液。整个过程下来,积攒了不少东西,在此和大家分享。当然,由于各自面对的问题和环境不同,你不一定认同我的某些观点,但我仍希望能带给你一些有意思的东西。

先丢几个我非常认同的观点:

代码是写给人看的,其次才是给机器执行
软件的维护成本非常重要,软件生命周期越长越重要,直到变成最重要的
开发人员的时间比机器的更宝贵
写博客比写代码更花时间

这个系列会包含以下内容:

  • 正确模块化,express 的 app.use
  • 无状态的服务端
  • json web token
  • 一个小坑 - app.use 和 unless 的 useOriginalUrl
  • 无状态的客户端
  • 单页面应用和路由 - navigo
  • 如何保存和校验密码 - bcrypt
  • 打通前后端,简化操作 - pouchdb
  • 打通前后端,简化操作 - restlike
  • 简化开发流程,后端自动重启和热更新
  • 简化开发流程,前端热更新 - webpack