2015 年终总结

年初的时候还在对 helpful 修修补补,添加手机页面支持、国际化、附件上传和邮件订阅的相关工作。这个时候的我已经会用各种库来实现想要的功能了,对项目的整体架构有了一些认识,能按功能来划分模块,但对面对对象编程的理解还不够透彻。虽然我将 mongoose 作为 ORM 库来使用,但并没有彻底的理解 ORM 的好处。我曾一度不理解 mongoose 返回的 document 除了给人添堵、不能直接取键值以外到底有什么用,对数据的操作全部是用 collection model 上自带的方法完成的。感觉异常的别扭,但又不知道哪里不对劲。

直到三月中下旬开始接手重构 cashier 项目,看到代码里面通过 sequelize 实现的对表的抽象和对数据的抽象感觉打开了一道大门。原来 ORM 提供的是数据的抽象,每个数据实例不单单包含数据内容信息还包括了你期望的这个实例所具有的所有功能,比如一个文章的实例可以自带取作者信息和更新标题的方法。这个数据实例可以在不同模块直接传递,也可以直接调用数据实例上的方法,这样用简直太方便了,代码看起来清晰也容易理解。

cashier 是一个订单管理系统,毕竟跟钱相关需要对数据的可靠性做出保证,所以引入了数据库外键和事务。通过 sequelize 来定义表的关联,数据实例也很容易拿到它的关联数据。虽然是第一次使用事务,但我之前对事务略有耳闻知道是怎么回事。事务一般是将一组 SQL 串行执行,每条语句会带有操作条件,如果其中某个语句执行失败,所有的操作都会回滚。如果在执行事务的过程中程序 crash,事务最终没有被 commit,那这个事务所做的修改也不会被持久化。这样在断电或、程序 crash 等极端情况下或者并发操作同一条记录的情况下都能保证数据的一致和可靠性。

之前我一直是用 async 来搞定流程控制上的问题,没有什么问题,但是用的越多越感觉 callback 在处理 error 的时候实在太繁琐,几乎每个回调函数的首行都是 if(err){callback(err)} ,这样将错误一层一层往上抛,最后被调用者获取并处理。当不同的模块间相互调用太多的时候,错误处理就变得很麻烦。后来在 @jysperm 的影响下开始调研在 cashier 中用 Promise 来处理所有的异步流程,Promise 带来的好处就是统一了回调和错误处理流程。Promise 理解起来并不复杂,每个异步任务都返回一个 Promise 对象,这个对象有三种状态 PendingFulfilledRejected,then 方法接收回调函数和错误处理回调,catch 方法接收错误处理回调,Promise 可以链式执行,也就避免了回调地狱,代码看起来会清晰很多。

现在遇到的一个问题是是在 Promise 链式调用中无法中断执行流程,只能抛出一个特殊的异常来中断,感觉还是有点别扭。另一个问题是 node.js 标准库还没有支持 Promise,需要自己封装一遍,不过现在社区也在讨论将标准模块 Promise 化的相关事情了。还值得注意的是 Promise 不同的实现在性能上有比较大的差别,可能换个 Promise 类库就能让你的程序速度快几倍。

当订单完成的时候需要给接入的网站发送异步通知和给用户发送订购邮件。这些异步任务的执行时长不定,很容易导致任务堆积和积压,如果执行失败要在合适的时间重试,如果重试失败也要通知开发者。如果不用队列的来处理,单独处理每个任务会很繁琐,也无法简单地横向拓展。最后根据 @orzfly 的建议最后选用了 resque 在 node.js 中的实现 node-resque,并通过 resque-web 来查看和管理异步任务。

五六月的时候梳理了 JavaScript 的原型继承相关概念,调研了 mongoose 里面的一些黑魔法,为了实践一下还写了一个简易的 ORM 原型 cado。ES6 中添加了 class 关键字作为构造函数的语法糖,比较像 CoffeeScript 了。

下半年大部分时间是花在了另一个项目 pomotodo-node 上了。项目虽说大一点,但是因为都模块化了所以整体的复杂度也没有很高,所用到的技术跟 cashier 差不太多,也没太多值得说的。

这一整年我都对网络相关很感兴趣,写了两个小东西 lyssaPowerProxy。PowerProxy 是参考 Surgeanyproxy 实现的网络调试工具,主体是一个 HTTP Proxy Server ,通过中间人攻击 (自制证书) 方法注入 HTTPS 来嗅探所有请求,打算采用 electron 制作跨平台应用。现在思路不是特别清晰,如果能把思路理顺应该会继续。


考虑到五一黄金周人山人海,专门挑了五一前几天跟女朋友去庐山玩了一趟。蜿蜒曲折的山路、险峰,确实也别有一番风味。我们去的时候没有看到山雾,听导游说前几天都有的,总之是错过了。

本来我是不想学驾照的,我对无人驾驶汽车的未来还是很有信心的,人的反应速度和操作能力终归是比不上电脑的。不过依旧是没有拗过,暑假的时候在家里的一再劝说下开始考驾照,暑假学了有一个多月再加上后来又补考了科目三,最后在十一月的时候拿到了驾照。

我们大四有两门选修课,我学分差不多实在不想继续呆学校了,室友同学也开始出去找实习。我就搬到校外跟女朋友租房子住,开始了一起买菜做饭的日子,经过几个月的练习,现在我也能差不多做一些菜了。之前在学校经常是凌晨三四点睡觉的,现在也开始纠正自己的作息习惯了。这样远程工作的日子还是挺舒心的,每天睡到自然醒然后起床吃点东西干点活,有时候跟朋友一起开开黑打游戏,不用打卡不用朝九晚五。

十一月尾的时候 @顾惜朝 姐姐在知乎私信上问我想不想去知乎实习,我的天然后我后来居然就顺利通过了面试,在年底的时候像做梦一样就来到了北京。除了空气,北京的一切都还不错,在北京的具体生活我以后再写吧。


感觉这一年像是分水岭,终于离开了学校,开始了新的生活。

拒绝外部修改的对象

document 是 mongoose 的数据实例,它拥有一些比较有意思的特性

  1. 实例拥有一些方法,能被正常调用
  2. console.log 显示的是纯数据,不包含方法
  3. 实例属性可以内部增加,但不能在外部增加
  4. 实例属性在外部不可被覆写,也不能被删除
  5. 实例在执行 update 方法后,属性也能被更新

看起来,1 和 2 比较冲突,3、4 和 5 也比较冲突。所以要怎么实现呢?

1、2 实现看起来比较简单,最简单的是把方法都添加到实例创建的原型上,这样创建的 document 实例的时候会自动继承原型上的方法,而在 console.log 实例的时候不会显示继承的属性,所以显示的结果会看起来非常干净。

后面的 3、4、5 统称起来就是 实例可内部修改但不能被外部修改 ,这个实现起来就麻烦了。

创建一个不可被修改也不能被删除的对象非常地容易,将 Object.defineProperty 中的 configurablewritable 都设为 false 就可以了。但是这样创建的对象是完全不能被修改的,更新属性的话没法搞。
要支持更新属性也行啊,把 configurablewritable 都设为 true … orz,这样就能外部修改了。

还有个办法就是用 getter,听起来不错,但是实际使用的时候发现 configurable 不能设为 false,不然的话不能更新 getter 里面的数据,设为 true 以后数据虽然不能被更新但又能被删除。
而且,设置了 getter 以后,用 console.log 打印实例的时候属性会显示成 [Getter],而不是实际值,这样跟 2 又冲突了。

还有个办法 Object.seal 可以密封对象。被密封后的对象不能添加新的属性,不能删除已有属性,以及不能修改已有属性的可枚举性、可配置性、可写性,但可以修改已有属性的值的对象。orz … 这里的修改并不能限制外部的。

似乎可以综合一下,Object.seal 允许修改,getter 可以限制修改来源。

而实际上 getter 的数据更新可以指定到一个内部变量,这样只用更改这个变量就可以更改这个属性了,而不用去直接改 getter,类似于做了一个映射。

class Model
  constructor: (record) ->
    _.extend @, record: record

    _.keys(record).map (key) =>
      Object.defineProperty @, key,
        enumerable: true
        configurable: false
        get: ->
          @record[key]

  update: (record) ->
    _.extend @, record: record

这样处理的话已经成功了一部分了,再还要不允许往实例上添加属性,在构造函数里面加上 Object.seal @ 就好了。

后面的问题处理完了,但是前面的 1、2 又冲突了。添加的 record 属性会在 console.log 的时候显示出来,而添加的 getter 也会显示成了 [Getter]

去研究了下 mongoose 的实现,发现它在 document 的原型里面添加了一个 inspect 方法。
我和逸川一起追溯了一下 console.log 的在 node.js 中的实现,整个的流程就是

  • console.log 调用了 util.format
  • 然后进而调用了 util.inspect
  • util.inspect 又调用了 formatValue
  • 然后是 formatValue 调用了传入 console.log 的参数的 inspect 方法

所以,console.log 的传入参数的 inspect 方法会直接影响 console.log 显示的内容

class Display
  constructor: ->

  inspect: ->
    return 'hello'

console.log new Display()  # 会显示 hello

那我们再给我们的 Model 加上 inspect 方法,里面返回 record 的数据即可。

这样我们就实现了一个 可内部修改但不能被外部修改 的对象了。

完整的示例参见 cado

Hour of code 讲稿

这是5月7日图书馆 Hour of code 活动的讲稿。讲稿大量参考了精子的博客,感谢~

各位学弟学妹好,我叫熊豹。现在大三,是建筑系土木工程专业的学生,同时也在一家互联网创业公司里面远程实习。
非常高兴能在这里跟大家分享一些经验和想法,希望能对你们有所帮助。

我开始接触编程的时间不算太早,高三左右开始写代码,到现在差不多四五年的样子。
现在主力语言 Node.js,以前用过一段时间 PHP,加起来写了十几万行的代码。
独立完成过一些大大小小的项目,写过 web 框架、DNS 服务器、反向代理、网盘、验证码识别、爬虫等等。
你们应该都有用我们学校的微信公众号查分吧?那个也是我写的。
接触的东西不少,也踩了了很多坑,走了很多弯路。


观念

在座的各位在学习编程的时候,我希望大家能先树立这样一些观念:

  1. 接入世界性互联网

    因为一些总所周知的原因,我们并不能自由地浏览一些网站。而我们在查找资料的时候,这将给我们带来非常大的困扰。
    你可以找人要个 VPN 或者尝试使用 GoAgent,或者选择购买曲径、GreenShadow。

  2. 版权意识

    你要认识到,如果你打算在计算机领域进行一些创造,那么维护版权,就是在维护你自己将来的利益,请试着去影响身边的更多人。
    可能你因为种种原因无法购买正版软件,但至少请认识到这是对作者利益的一种损害。

  3. 从官方渠道下载文件,获取信息

    也许是因为一直用盗版,很多人没有养成从官网下载软件的习惯,出了问题也想不到去求助官方客服。
    很多人查找资料不喜欢去官网,而喜欢看个人博客或总结,这样在信息的传递过程中可能会出现偏差。或者原作者对官方的文档做了一些更正而你的信息来源没有及时更正,也可能造成一些误差。

    不管你信不信,这是避免出现问题,解决问题的最方便,有效的手段。
    毫无疑问从官网得到的信息是最权威的,这在解决问题和与别人辩论的时候都很有用。

  4. 扔掉旧东西

    扔掉那些旧的,官方已经不再推荐或提供支持的东西,比如 IE6, WinXP, VC++6.0, VB6..
    一定会有新的更好用的替代品的,如果没有的话可以考虑自己创造一个。

  5. 使用 Google

    Google 会让官网出现在第一个,跟上上条观点相承,官方渠道更值得信赖。
    Google 会包含英文的搜索结果,这个时候不要害怕不要怂,即使你不懂英文,但很多问题只需一句话,一个词就能解决,你能搞定的。

  6. 学会Debug

    当你程序出BUG的时候最重要的是报错信息,读懂它将对你解决问题非常有帮助。你可以直接搜索报错信息,或者简要描述你的问题再搜索。有时候并不能搜到合适的结果,可以尝试用英文描述下你的问题再搜索。
    你用到的可能是一个再 Github 上的开源组件,这时候去搜一下它的 issues 可能会有所收获。如果找不到相关 issues,那你可以考虑去阅读你所用的开源组件的代码,断点调试一下。

  7. 学会提问

    如果无法通过搜索解决问题时,那么就需要去社区提问,在此之前你有必要学习一下提问的智慧。如果是 Github 上的开源项目,可以直接新开一个 issue。
    尽量不要在 QQ 或邮件上单独向别人提问,尤其是搜索就能解决的问题,因为这样的话,别人无法搜索到你最后解决问题的经验,而且对方欢迎不欢迎你的提问还说不定。

  8. 多阅读

    无论是在网上还是图书馆,你都应该在你研究的领域多读一些书和文章。书籍的内容是经过精心的安排的,它的目标就是供人阅读。
    不一定什么时候就会派上用场,你可能记不住文章的所有内容,但只需记住几个关键词,在要用到的时候你就可以重新找到这篇文章。

  9. 将自己的作品展示给大家看

    如果仅仅是练习的作品,没有什么商业机密,自己藏着不如发出来给大家看。你可以到 Github 上开源自己的代码,也给更多后来的人一些参考。

    同时对你自己也是一种压力,因为要展示给大家,所以你要让作品尽可能完美,不给自己丢脸。你也可以去参考代码质量较高开源项目不断完善自己的项目。一步一个脚印,也是自己成长的见证。
    如果你的业余项目代码质量很高,对你将来找工作也是一种帮助。


学习

我现在所想讲的不仅仅是编程,包括整个计算机和互联网领域。

现在互联网进入我们的生活,遍布世界的每一个角落,连接着世界上几十亿的设备,能让任何人在半秒之内,联结世界上任意两点。
当你在 QQ 上发出一条消息时,它会在光纤中穿过大半个中国,经过十几个路由的转发,调用若干台服务器,最后在不到一秒内被对方看到。

计算机和互联网可以算是由全人类共同构建的,最精密也是最庞大的工程。
每一个芯片都是在纳米级被雕刻而成的,容不得哪怕一粒灰尘的差错。
无数黑客、恐怖分子对互联网虎视眈眈,但它仍健壮地运作,支撑着整个世界的信息传递。

计算机还是一个几乎全新的学科,仅 60 年历史,而互联网更是仅仅 30 年。
硬件在以摩尔定律预测的那样每年翻一番,而软件方面也在不断更新迭代,没人知道计算机领域今后会发展成怎样。
相比于已经十分稳定的学科,计算机还在快速发展,有更多的变数,也有着更多可能取得突破的机会。

而我们就处在这样一个机遇与挑战并存的时代,一不小心就会被急剧发展的时代所抛弃。
为了跟上发展脚步,我们只有不断地学习,扔掉旧的过时的知识,拥抱新的变化。

现在老师在课堂上教的东西,可能是过去四五年流行过的工具或者语言,现在可能已经有了更好地东西来替代或者已经被替代好几代了。

我举个简单的例子,前端开发。

我之前上过几次我们学校的课,课堂上还停留在table布局的层次上。而现在你们知道前沿的前端发展成什么样子了吗?

div 布局?你们啊,还是 too young。

这几年浏览器大换血,Chrome 一统江湖(包括含 Chromium 内核的国产双核浏览器),新的 V8 引擎大幅度提高了前端 JavaScript 的运行效率。
前端中 JavaScript 占的比重也越来越大,之前还只是 JavaScript 作为辅助页面的交互语言存在,而现在则完全成了前端的主角。
甚至前端逐渐跟后端分离开来,也不需要后端来渲染页面,而只用提供数据,由 JavaScript 来完成渲染。

随着大家对审美和用户体验的要求越来越高,每个页面单独渲染的模式已经无法满足日益增长的需求了。
如果点个赞要刷新一次网页,打开评论要打开新的网页,关注一个人要打开一个新的网页,这样的交互你受得了么?

前端现在也发展出了 MVC、MVVM 等等概念,也出现了大量的 JavaScript 框架比如 angularjs、avalon、backbone、React 等等,甚至可以写独立应用。
有心的同学可以回去打开新浪微博首页的源代码看看,主体部分除了 script 标签以外已经没有其他标签了。
我们公司的产品番茄土豆的也是完全的前后端分离的典范。(插播一条广告,番茄土豆是番茄工作法和 todo list 结合一个效率类软件,客户端全平台覆盖,有兴趣的同学可以试用一下)
手机上的淘宝、美团外卖、知乎和手机 QQ 部分页面也都使用了前端技术构建。

这些都是近两年发生的变化。

我前面说了很多变化,那么我们的学习中就没有什么是不变的么?
当然也不是,程序是算法+数据结构,这些都是基本功。基本功还包括计算机原理、计算机网络、编译原理等等。
一般基础打好,学习新的东西也会很快,不用太过于担心。

我这里只是提醒你们知识是学不完的,技术也在不断地进化,要时刻保持学习的心态。


Node.js

接下来一部分我想讲一讲 Node.js。

Nodejs 自从 2009 年发布,因其出色的性能和开放的姿态一直备受关注。

阿里巴巴是应该是国内最先大规模使用 Node.js 的公司,在 2011 年的时候就开始在淘宝指数、数据魔方以及淘宝时光机等栏目上开始使用。
而在去年淘宝开始实时前后端分离计划,Node.js 作为数据中间层占了一个很重要的地位。现在我的淘宝、淘宝收藏夹、商品详情页、天猫首页、天猫会员、天猫电器城已经在使用 Node.js 了,未来还会有更多业务会迁移到 Node.js 上面。

除了淘宝以外,还有百度、网易以及一些创业公司比如知乎、teambition、花瓣网、番茄土豆(我司)在尝试用 Node.js 构建他们的产品。

听到到 Node.js 这个名字,初学者可能会误以为这是一个 JavaScript 应用。事实上, Node.js 采用 C++ 编写而成,是一个 JavaScript 的运行环境。

为什么采用C++语言呢?据 Node.js 创始人 Ryan Dahl 回忆,他最初希望采用 Ruby 来写 Node.js,但是后来发现 Ruby 虚拟机的性能不能满足他的要求,后来他尝试采用 V8 引擎,所以选择了 C++。

既然不是 JavaScript 应用,为何叫 .js 呢?因为 Node.js 是一个 JavaScript 的运行环境。提到 JavaScript,大家首先想到的是日常使用的浏览器,现代浏览器包含了各种组件,包括渲染引擎、JavaScript 引擎等,其中 JavaScript 引擎负责解释执行网页中的 JavaScript 代码。

作为 Web 前端最重要的语言之一,JavaScript 一直是前端工程师的专利。不过,Node.js 是一个后端的 JavaScript 运行环境(支持的系统包括 *unix、Windows),这意味着你可以编写系统级或者服务器端的 JavaScript 代码,交给 Node.js 来解释执行。

Node.js 采用了 Google Chrome 浏览器的 V8 引擎,性能很好,同时还提供了很多系统级的 API ,如文件操作、网络编程等。浏览器端的 JavaScript 代码在运行时会受到各种安全性的限制,对客户系统的操作有限。相比之下,Node.js 则是一个全面的后台运行环境,为 JavaScript 提供了其他语言能够实现的许多功能。

Node.js 的设计思想中以事件驱动为核心,它提供的绝大多数 API 都是基于事件的、异步的风格。以 Net 模块为例,其中的 net.Socket 对象就有以下事件:connect、data、end、timeout、error、close 等,使用 Node.js 的开发人员需要根据自己的业务逻辑注册相应的回调函数。这些回调函数都是异步执行的,这意味着虽然在代码结构中,这些函数看似是依次注册的,但是它们并不依赖于自身出现的顺序,而是等待相应的事件触发。

事件驱动、异步编程的设计,重要的优势在于,充分利用了系统资源,执行代码无须阻塞等待某种操作完成,有限的资源可以用于其他的任务。此类设计非常适合于后端的网络服务编程,Node.js 的目标也在于此。在服务器开发中,并发的请求处理是个大问题,阻塞式的函数会导致资源浪费和时间延迟。通过事件注册、异步函数,开发人员可以提高资源的利用率,性能也会改善。

Node.js 在设计上也是比较大胆,它以单进程、单线程模式运行,这和 JavaScript 的运行方式一致,事件驱动机制是 Node.js 通过内部单线程高效率地维护事件循环队列来实现的,没有多线程的资源占用和上下文切换,这意味着面对大规模的 http 请求,Node.js 凭借事件驱动搞定一切。

性能是 Node.js 的优势之一,还有一个很重要的优势是强大的社区支持,大家都非常愿意分享自己的代码和知识经验。

NPM 是 Node.js 的包管理器,这里我说的包是指别人封装好的独立的模块。
NPM 平台上的第三方模块超过 14W,在去年6月超越 Maven Central (Java) 和 gem (ruby) 成为了最大的包管理平台,这两个平台上的的模块数量现在刚过 10W 大关,还有一些其他语言的包管理器,比如 PHP、.Net、Go 上面的包都远不到 10W。
除了包的数量以外,包的下载量更是惊人,现在每天NPM平台上的下载量就有几千万,每个月更是有十几亿的下载量。

一般的 Node.js 项目都会依赖几个或者几十个第三方模块,大大降低了重复劳动,提高了工作效率。
而你在开发的过程中,有一些很好的封装也可以很简单得发布出来,回馈社区方便他人。

Node.js 现在发展势头正猛,有兴趣的同学可以回去自己再查查相关资料。

参考: