奇丁有术
记我的第一次 - 开发篇

设备

  • Mac

    既有 Windows 的图形界面,又有 Unix 系 的 Shell,完美

  • Dell U2414H 显示器

    23.8 吋,窄边,三轴可调,价廉物美,爆款

  • 保友 Ergonor 金卓人体工学椅

    久坐不累,几乎已成为初创公司标配

  • 机械键盘

    对于 Mac,Matias 家的机械键盘可能最为适合,有线/无线都有

不要误会,资本家当然不是要讨好员工,资本家只是想让员工干活更有效率,创造舒服的工作环境束缚员工,让他们不想离开,从而最大化企业利益。

服务

主机

PaaS 太束手束脚,不考虑。国外服务器的话,基本上就只在 Linode 的 VPS 和 AWS 的 EC2 中做选择。 经过多次不严格的测试,Linode 的日本节点比 AWS 的日本节点连接我们业务中的主要客源国的速度要更均衡更稳定。

Linode 的功能也比较丰富,自带 Monitor / Node Balancer / Backup,用起来也很便捷省心,你只要交钱就是了。

有一款 VPS 测速 App 叫 Cloud Speed。

Linode 的 IP 池是被 GFW 墙的重灾区,当然我们使用 CDN,不担心国内用户无法访问,但是远程登录还是不大方便,只要向客服提出申请就可以更换 IP,也很方便。

测速时需注意,许多测速工具为 ping 工具,若想得到完整的 RTT 时间,还要从服务器 ping 一下 CDN 才能计算出来。当然如果使用的是 HTTP 工具就方便多了。

CDN

CDN 不仅可以在某种程度上对网站进行加速,还可以隐藏服务器的真实 IP,一定程度上增加了安全性。CDN 服务商太多了,我们考虑到易用性和价格,选择了 Incapsula 和 CloudFlare1。 Incapsula 的香港节点对于中国地区来说更快,而 CloudFlare 对于世界其他地区来说要比 Incapsula 好。于是我们使用 AWS Route 53 进行分区域解析,中国 IP 解析到 Incapsula 上去, 而其他 IP 解析到 CloudFlare 上去。

CloudFlare 的功能比 Incapsula 强大许多,有独有的图片加速 Mirage、Javascript 加速 Rocket Loader™,以及智能路由多层缓存 Argo 等功能,现在更是允许在节点部署 JavaScript Service Workers。 百度云加速和 CloudFlare 合作后,双方的节点资源共享,CloudFlare 国内的节点就是百度的,CloudFlare 也可以帮助要使用国内 CDN 节点的用户申请 ICP 备案。

根据我们实际使用的经验,无论是 Incapsula 还是 CloudFlare,一年之中总有那么几天里,有数分钟到数小时不等的不稳定时间,因此,对可用性要求极其高的用户,可能要另择方案。

CDN 以及各服务 Callback 的 IP 记得加入白名单。

DNS

我们选择了 AWS Route 532,功能十分强大,可以根据地理区域/延迟时间/加权/服务器心跳进行解析。

云存储

基本上没有什么好选的,国内七牛云,国外 AWS S3,我们是在后端根据用户地理位置加载更适合的文件 URL。

邮件

邮件服务商很多,我们综合考虑后觉得国外的 Mailgun 是性价比最高的,国内的 SendCloud 也很不错。Mailgun 上可以购买信誉度更高的独立 IP,使用越久的 IP 的信誉度也会更大,所以好的发件 IP 是需要 “养” 的。江湖上还流传着上主流客户端白名单的方法,我们势单力薄,无力一试。上线前务必先测试一下业务触发邮件会不会被各邮件客户端扔进垃圾邮件。有条件的话建议可以自觅方式提前预热发件 IP 和域名,而不用等到业务开始。经过玄学测量,对于某些客户端,某些邮件服务商发出的邮件会有天然的信誉加成,所以可以考虑混用,根据邮箱域名路由到不同的发送服务上去,还可以根据投递状态进行 round-robin。Mailgun 还提供验证邮箱地址是否真实的 API。

自动触发邮件、营销邮件、人工邮件最好使用不同的子域名

支付

初创公司自行整合多种支付 API 非常麻烦,Stripe3 会让问题迎刃而解,不仅无缝集成多种支付方式(包括储蓄卡、信用卡及 Alipay 等),交互动画更是独步天下,一级爽滑,体验极佳(甚至是其开发文档)。

多合一支付服务服务费较高

客户支持

Zendesk

国际化比较麻烦

短信服务

Nexmo

服务区域和价格都能让人满意

监控和分析

性能监控

New Relic

异常监控

Sentry(前端是 Raven.js)

行为分析

  • Google Analytics4
  • 百度统计

    虽然 GA 在国内实际上可以使用,但为了保险,还是用了百度

  • Inspectlet / Hotjar

    这类服务可以将用户的整段访问行为还原成录像进行展示,非常有趣,有一种窥探的快感,比冰冷的统计数据能更进一步地了解用户行为。有点像野生的可用性测试。

源代码托管 & Issue Tracking

Github5

Issue Tracking 的话,业内常用的还有自建 JIRA。

使用 Chrome 插件 ZenHub 可以在 Github Issue 系统中集成 Scrum 式项目管理功能

技术

语言

Web 后端开发的候选语言是 Ruby(on Rails)、Python、Java,PHP,当然还有当时特别火的 Javascript(Node.js)。

PHP 历史包袱沉重且风格丑陋,而 Node.js 当时极不成熟,其鼓吹的高并发对我们来说也没什么用;

Ruby 应该说在当时是 Web 开发的王者,具有超一流的元编程能力,我曾经看过有人这么形容,但是出处找不到了,大意就是说 Ruby 开发到底有多爽呢? 假如说使用 Python 就像是在干一位姑娘那么爽,那使用 Ruby 就像是对一个姑娘为所欲为那么爽。然而坏消息是,Ruby 职位在国内招人极其困难,基本上都需要自行培养。

再考虑到 Ruby、PHP、Javascript 几乎只专精于 Web,生态应用范围太窄,基本上候选就只剩 Java 和 Python。

Java 在生态和周边工具方面极其成熟,只是我个人总觉得 Java 特别笨重,干点什么事情都要写特别多的东西,感觉总是不得劲。Java 8 当时也还没发布,写起来确实没有 Python 舒服。 总体来说就是没什么毛病,但也很难让人眼前一亮。

Python 使用 “鸭子类型”,函数是一等公民,在语言层面具有部分诸如元类的元编程能力,灵活强大,语句简练易读,生态应用极其广泛也足够成熟,从日常脚本到 Web 开发到爬虫再到机器学习都能胜任。再考虑到可选的 Web 框架后, 最终我们选择 Python 作为后端开发语言。

心得体会

灵活 vs 安全

Python 这样的语言有些过于自由。比如说 Python 允许函数随便返回什么东西,输入不同时,返回来的东西是什么都可以,数量和类型都允许发生变化。序列也是什么都能装进去。 再比如说 Python 中的类与对象由于 “元编程” 能力,可以很容易实现出各种超出语言本身期待的行为。这是一种魔法, 经验丰富的开发者可以事半功倍,而经验不足的开发者很可能会玩出问题(没有人指望在不设置交通灯/隔离栏等障碍的情况下维持交通秩序)。

人们经常谈到 “动态一时爽,重构火葬场”,使用 Python 进行团队协作时该问题更加凸显。阅读他人源码时,经常会碰到一个形参完全不知道是什么东西,源码只能自上而下地理解,而无法自下而上地理解。 当然,现在也有 PEP 3107 Function Annotations 加辅助工具来缓解这类问题。

即使在文档完备的情况,这也会加重开发时考虑 context 的负担(在我看来,这属于一种信息压缩,只要达成共识,简单的语句可以蕴含丰富的含义),增加了团队协作时出错的概率。 更糟糕的是,这些错误只有在运行时才能被发现。

当然你可以说不犯错的话,这种自由带来的灵活强大就很棒棒。但我觉得这种 context 负担就类似于过程式语言赋值的副作用,没有人可以保证这种副作用一定会得到良好的处理。 函数式语言正是因为没有这种副作用的负担,使得程序紧凑,不易出错。

实际上每种语言的表达能力是等价的,Python 这种自由度很高的语言有太多方案去达成目标,但也很容易写出张牙舞爪的怪物。 严格的、看似不那么灵活强大的语言通常会加入许多约束,而这些约束也许会降低开发效率,但是也会强迫开发者去思考,并且通过限制统一开发风格。 使用这类语言开发会感到要进行很多冗余的表达,但恰恰是这些冗余的表达可以在某种方面确保该表达确实是你想要的语义,而精简的表达则缺乏冗余进行校验,容易出错。 Java 里很多严格麻烦啰嗦的事情都是在语言层面确保事情正确地进行,因此一般认为 Java 特别适合大型团队协作,开发高质量保证的大型项目。 约束导致有序,有序通向可靠。

正因如此,虽然 Python 这类语言有许多强大的高级特性,却一直也有声音警告反对过度使用高级特性,除非你很清楚自己在干什么。 当你发现很难将其改写为另一种语言时,那可能说明滥用高级特性已经病入膏肓了。

总之各种语言都有各自擅长的维度,就 Python 而言,开发效率(也可能是前期开发效率)是其最大的优点。 但有得必有失,选择某种语言必须是特别看重其优点,又要能坦然接受其缺点。 不要做语言的奴隶,最好语言之争毫无意义,世间没有万能药,只有适合的场景。

元编程 Metaprogramming

我认为编程如果只遵循一条原则的话就是 DRY(Don't Repeat Yourself),想要 DRY 就得尽可能重用代码,使其共享某种模式,一处定义,多处引用, 对变化的部分和重用的部分进行显式分离,保持正交。有时候模式本身还有模式,也就是模式的模式,那么元编程就有了用武之地。

元编程总是可以有其他非语言层面的方式实现,比如自己发明 DSL 编译到目标语言,这样反而运行速度更快。但从实用角度来说,在语言层面上实现元编程,确实很爽,一方面开发人员一般不会因为一个可能就用几次的元编程模式去写一个 DSL 编译器, 其次在语言中实现元编程可以将模式描述信息和相关业务功能的上下文代码物理上放置在一起,保持代码结构的紧凑。

但是毕竟,可以认为这也只是一个语法糖而已,语法糖很容易让人迷失在便利之中看不清事物的本质。我现在就不太喜欢 Vue 推崇的语法糖模式的阉割渲染逻辑(为什么要用 HTML 写逻辑,只是因为比较顺手吗?),反而倒是喜欢 React 重量级但完备的抽象,将渲染逻辑彻底地分离。

循环引用

通过一些简单的技巧,Python 实际上是允许循环引用的,这很容易让人不仔细思考依赖关系。 因此一旦遇见循环引用错误,一定不要绕过,此时应该仔细重构。

实际开发中,有些循环引用难以避免。这也是为何 Python 的 ORM 定义外键时一般允许使用字符串进行指定,一方面是允许 lazy loading,另一方面也是可以绕开难以避免的循环引用。 信息的双向流动会引入循环引用。比方说模型 A 一对多关系到模型 B,那么 B 到 A 有一个外键关系,B 必须依赖于 A;然后我们又希望能直接从 A 的方法中获得所有与其关联的 B, 这样 A 会依赖 B,出现循环引用。当然我们可以额外使用函数而不是方法来完成这个功能,但这样反而不容易让人觉得是一个好设计。即使从内聚和耦合方面思考,也很难明确 A 不应该 “知道” B 的存在。所以有时候为了某些特别的需求,应该允许有节制地反范式。 还有些不可避免的循环引用来自于第三方包。比方说某第三方包允许你自定义的模块/类对包的某部分功能进行定制,然而自定义的模块/类又必须依赖该第三方包以满足接口要求,这很容易隐含循环引用。

多继承

Python 中允许多继承,只要理解并正确利用 MRO(Python 的实现回避了菱形继承的歧义问题),Model Mixin 可以更便捷优雅地实现组合模式。 这是 “优先组合而非继承” 的一种实际用例。

GIL

Python 有个缺点是常见实现6 CPython 中的 GIL 全局解释锁,多线程无法利用多核能力,因此并发一般用多进程解决,造成内存占用较高。

风格规范与编码指南

Code is read much more often than it is written.

我认为应该严格区分所谓 “风格规范” 与 “编码指南”,前者对编译器或解释器来说是透明的,即不同的编码选择完全等价,而后者并非如此。

以下是综合 PEP 8、Google Python Style Guide 和 The Pocoo Style Guide 得出的风格规范和编码指南:

命名

"There are only two hard things in Computer Science: cache invalidation and naming things."

── Phil Karlton

命名真的是开发中最难的事,尤其是对于非英语母语的人来说。 好的命名需要描述精确,又要尽可能保持较短的长度,还要符合惯例。我们也得意识到命名的难度与 KISS 的贯彻程度成反比。

可以参考:

推荐一个变量命名的辅助神器:Codelf

Web 框架

几乎没有犹豫,除非做玩具,Django 几乎是最适合快速开发的选择。生态社区成熟,开发活跃,文档优异。文档厉害到从开发思想到最佳实践和实际部署甚至可能出现的实际开发问题都描述得一清二楚,我再也没见过第二个如此优异的文档。 Flask、Tornado 等 Micro Framework 难以望其项背。我也用过一些其他号称 Fullstack 的框架(当然不包括 Rails 和 Lavarel),但只有 Django 在很多细节问题上做到了较为完备的解决,如鉴权与授权、国际化、本地化和 URL Reverse 等等。做地理应用还专门有 GeoDjango,牛逼爆了。

很多人觉得 Django 学习曲线太高,其实 Django 主要的组成部分也就是 Model/ORM, URL dispatcher, Middleware, View, Template,无它。虽然现在 Django 可以替换 ORM 和模板引擎,但是我不推荐,除非你确定不需要 Django 随包附赠的可深度定制 CRUD 后台(使用 Django 的 ORM 和模板系统)。数据库 Migration 文件是个讨厌的东西,第三方 App 的 migration 默认不受主项目控制,而有些第三方 App 更新后可能不遵循 migration 的路径,而是直接改写,造成混乱。

Django 乃至 Python Web 框架长期为人诟病的主要原因是其运行速度,实际上在大部分场景下这并不是一个真正的问题。在留学租房领域,单机 Django 估计能撑到垄断全球, 而对于大多数其他业务领域来说,大多数团队甚至都活不到需要深入优化速度的阶段。通常制约 Web 应用的主要因素并非应用代码运行速度,而是 I/O 延迟。针对 I/O 优化的各级缓存,像多级漏斗一样可筛去大部分前往应用服务器的请求, 从而大大减少后端应用服务器的运行负担。就算你真的野心勃勃一心要搞个大家伙,那么 Instagram 也可以告诉你,Django 或 Python 不太可能成为你成功路上的绊脚石。相比之下,迭代速度才是创业团队更需要追求的。

一定要看一遍 Django Design philosophies

推荐书籍 Two Scoops of Django

ORM 查询优化

Django 最佳伴侣 django-debug-toolbar 是一款极为优秀的调试工具。 使用 django-debug-toolbar 可以很方便地查看一次请求的各部分处理上下文及耗时。 我们经常利用其信息消除 ORM 查询中的 SQL duplicates(就是一次请求中进行了冗余数据库查询,ORM 还没有智能到可以自动优化 SQL query),也就是所谓的 N+1 Query Probelem, Django ORM 中的解决方案7

  • 尽量使用 select_related
  • 善用 prefetch_related
  • 善用 Prefetch 对象

前后端分离

这里要稍微说一下前后端分离,我们想要的是,前后端在数据上得一致,在开发上分离。前端接收后端的数据,总得有个地方去集中统一指定传递的数据。 比如说表单验证,需要保证前后端规则一致。Django 的做法一般是后端写前端,是用 Python 编写某种 Specification,然后生成前后端代码,使其行为一致。

当前流行的 SPA 是将数据通过 Web API 进行传递, 而 Django 这类 MVC 框架则是将数据通过 Python 模板引擎传递,这相比于 SPA 模式主要问题在于调试痛苦,模板虽然是由前端人员编写(实际上后端也会参与),却又得由后端启动,前端人员难以独立便捷调试,可能需要安装一大套后端环境。 即使运行起来,前后端问题混合在一起,难以追踪。静态资源的管理也是个大问题,Django 生态的做派是交由后端处理,但实际上应该交由前端人员控制更科学。 最后我们的实践有些尴尬,是由前端人员编写某种 Specification,前端调试时用自己的构建工具,后端部署时用后端的构建工具,双方依靠那个 Specification 文件来保证行为一致。 反观 SPA,可以做到前后端分开独立部署,这样各自的代码和资源都是分开管理,职责划分从逻辑到物理都很明确。 我们之前就有前端人员反复抱怨这一点。当然如果有良好的 Mock 设施的话,其实此问题也可以得到缓解。 另一个小问题是前端人员需要学习特定的模板语言。

话虽如此,SPA 模式却有非常严重的 SEO 问题8(取巧使用 Cloaking 可能会被惩罚!),在我们的业务领域中是不可接受的,所以即使了解 SPA 前后端分离的好处,也必须继续使用传统后端 MVC Web 框架。 而调试部署问题、开发语言问题,对于小团队、小项目尤其是非前端丰富交互的应用也都不算什么大问题。另外,SPA 模式其他的问题还包括首屏加载问题及冗余逻辑问题等。

有时候我们甚至难以严格区分何为数据和业务逻辑(归后端),何为界面和渲染逻辑(归前端),又或者难以明确划分职能,如前文所提到的,实践中为了达成某种目的,有时我们必须反范式。 比如搜索结果排序,到底应该是后端排好交给前端,还是交给前端排?表单验证前端是否应该做?还是一股脑全扔给后端?这些其实根据不同场景会有不同的最佳实践。 因此 SPA 模式显然不是前后端分离的终点,前方等着我们的应该是一种处于中间的,融合两种模式特点,保留两者绝大部分优点的技术。这方面阿里早在 2014 年就已经做出了不错的初步尝试,即 Midway Framework。

数据库

PostgreSQL vs MySQL

从社区和生态来说,MySQL 还是比 PostgreSQL 要强,使用者广而多。并且一般认为在平凡情况下,MySQL 要比 PostgreSQL 快。 但是 PostgreSQL 拥有一些 MySQL 不具备的特点:

  • 对 SQL 标准和事务的完整支持
  • 良好的地理查询支持
  • 支持 HStore 和 JSON 字典存储
  • 自带全文搜索

因此最终我们选择了 PostgreSQL。

版本控制

数据库跟随源代码进行版本控制比较麻烦。 以使用 SQLite 数据库为例,加上一个钩子,切换分支后如果没有对应分支名的数据库文件,则复制祖先分支的数据库文件,以相应分支为关键字进行更名并执行 migration。 这种方式比较粗暴,有改进空间。 而使用 PostgreSQL 的话则大致类似,区别在于是为不同分支创建不同名称的数据库。

NoSQL

当时以 MongoDB 为代表的 NoSQL 跟着 Node.js 盛行,我觉得已经到了走火入魔的地步。很多人也不管什么场景, 上来就统统 MongoDB,要是本来用的 MySQL/PostgreSQL 没法迁移,就禁止使用外键。好在后来热度散去,终于拨乱反正。

但是除了大数据、高并发之外,有些场景也有反范式的需求,比如说订单关联的用户和房源不适合使用外键,否则一删全删。

提倡使用 软删除

内存数据库

Redis 经常拿来与 Memcached 作比较。 其实 Redis 的定位是键值对内存数据库,支持的数据类型要丰富得多,也可以作为 Memcached 的替代,用于缓存系统。 最重要的是,Redis 还具备定期持久化数据的功能。

App Server

  • uWSGI

    性能强大

  • Gunicorn

    Python 编写,配置简单

Web Server

  • Nginx
  • Openresty

    Nginx 加强版

我们使用的是 uWSGI + Openresty,还可以方便地选配各种 WAF。

消息队列

没得选,就是 Celery,使用 Redis 作为 Broker。务必使用 JSON 序列化,一定不要偷懒,安全第一。我们主要使用消息队列来处理异步任务。

Celery 中不能直接设置优先级,只能对不同的队列设置数量不同的 worker 来变相处理,拥有更多的 worker 就更有可能被优先处理。

Celery 也可以用来执行定时任务和周期任务

Celery 有一款监控工具 Flower

全文搜索

ElasticSearch 基本上已经是业内标准做法。使用广泛、易扩展、易部署。轻量级的全文搜索也可以交给 PostgreSQL。

我们场景中的难点来自于多语言,每篇文档都有三种语言版本。即使用户具有语言偏好,用户对搜索结果的期待不一定是用户界面的语言偏好, 一般的做法以某种语言设定去搜索是行不通的,而且也存在英文单词在中文文档中的可能。那么完善的多语言搜索应该是根据关键字对所有语言版本的文档进行搜索, 如果同一篇文档的多个版本都匹配,则要根据当前用户的语言偏好去重;如果一篇文档只有一个版本匹配且并非当前用户的偏好语言,则也只能返回匹配的版本, 否则。但是市面上较成熟的第三方库都不能支持这种需求,所以我们进行了较多的 Monkey Patch。

另外中文搜索需要分词,ElasticSearch 有第三方分词插件(官方的很鸡肋),但有版本限制,当然也可以自己写; 我们也可以自行对文档和 Query 进行预分词,这样比较麻烦,需要同时维护分词和未分词两种版本的文档。

自动补全 Autocomplete

自动补全的功能使用字典树 Trie 进行快速查询,再根据 Jaro-Winkler 或者 Levenshtein 距离在前端进行排序,以减轻后端压力。 我们选用的是一款性能极佳的 Trie 库:marisa-trie。 自动补全一定要快,要由 CDN 完整缓存。因为查询时间更长的反而是更简短的查询,可以自行生成常见请求进行缓存预热。

第三方库

  • Viewflow

    内部用后台系统同样不容忽视。我们使用了商业版的 Viewflow 开发工单系统,其为较完备的工作流第三方库,带状态机。 其业务逻辑基于 BPMN 模型,可以使用 专用作图工具 进行表达。

  • retrying

    专门给函数/方法增加失败后重新执行功能的 Python 装饰器

工具

翻墙

ShadowSocks

GFW

源代码管理

Git

我们参考的是 Vincent Driessen 的 分支模型

Commit 更新要勤,尽可能专一,保持最小粒度。 如果要 commit 自行修改的第三方代码,先 commit 第三方代码原版,再 commit 修改部分,方便追踪修改的部分及更新第三方代码。

commit 前的钩子工具:pre-commit

在线自动创建 .gitignore 文件的神器:gitignore.io

Commit Message

Fast is slow.

简述中的动词使用原型。 评价 commit message 的准则是:能仅根据该 commit message 还原出 commit 修改才是好的 commit message。

在 commit message 使用 #[No.] (方括号内为 issue 编号)可在 Github 中直接链接对应 issue。

更多参考:

集成测试 CI

  • Travis CI
  • Jenkins
  • Drone

运行环境

  • Vagrant
  • Pyenv

IDE

  • PyCharm
  • WebStorm

编辑器

  • Vim
  • Visual Studio Code

Shell

  • iTerm2
  • oh-my-zsh

以上这套几乎已成了 Mac 开发的标配。另外还有两个可选工具:

内网穿透

有时想在公网上调试服务。

  • ngrok
  • Pagekite

网页测速

网络线路

网页加载

浏览器兼容性

压力测试

爬虫

  • Scraper
  • Pyspider

视需求也可以直接购买数据或者商用采集器。

理想的爬虫

理想爬虫的硬要求当然是高效率易扩展,比如说支持分布式、高并发,还有智能反反爬虫机制,能完美模拟浏览器请求,有 IP 池,甚至能模拟人类浏览行为等等。 而软要求就看业务需求。我们主要使用爬虫来更新房源数据,一般监控页面即可,但需要细致处理并结构化原始数据,而非不定向的探索式爬虫。采集到不同信息源的数据后还要汇总合并,最终转为关系数据模型。 而各信息源的原始数据结构并不相同,合并困难。写爬虫是一个很脏的活,代码很容易失效或出错,变为一次性代码,非常不环保。原因防不胜防,有可能网页本身不遵从 HTML 规范,或者网页结构发生变更,又或者网页数据分情况有不同结构,不一而足。 需要重新思考,将我们业务中容易变化的部分抽取出来,不变的部分进行总结,变为基础设施。

于是我们基于 Scrapy 套了一个叫做 Sharingan 的自制小框架,主要想法是将数据采集严格分为数据选择和数据加工两个阶段。数据选择阶段通过 XPath/CSS 选择器/JSONPath/正则表达式/自定义选择器函数以尽可能小的粒度指定文档哪些部分有用,此阶段输出称为文档片段。 数据加工阶段定义片段需要进行的加工处理的 Pipeline,每个 Pipeline 过程可以数据选择阶段摘录的单个或多个片段为输入,输出则直接喂到最终模型的某字段中。 这样首先可以大大减轻网页变化带来的困扰,方便快速定位失效定义代码并进行更新;还可以明确跟踪关注点,确保页面更新是有效更新才进行处理;并且保证一开始就能以同一模型从不同数据源获取同构数据。 未来的构想是能做出可视化界面辅助编写爬虫就爽歪歪了。

网页技术栈分析

均为浏览器插件

  • Wappanalyzer
  • builtwith
  • WhatRuns
  • Lightbeam

元准则

当没有其他明确的开发指南时,遵循以下元准则:

  • DRY (Don't Repeat Yourself)
  • KISS (Keep It Simple, Stupid)

    "Simplicity is a virtue."

  • "Push relentlessly toward automation."

  • The Zen of Python

    • 显式优于隐式

更多原则可参考 Programming Principles

开发总结

前端

移动端开发

响应式设计

响应式不是万能的,只适用于一般的信息展示,如果涉及的交互比较丰富,则很难指望单纯的响应式布局能带来什么好处。 一开始响应式确实看上去很美好,也确实给我们带来了一定帮助,一次开发一份代码,两种客户端似乎都还能看得过去。 而当后期随着我们设计开发的进一步细致且独特,桌面端和移动端区别越来越大,很多组件实际上提供两份代码,由后端判断分发或前端判断运行。 这对桌面端负担不大,但是移动端会被迫接受很多无效代码,臃肿不堪。 因此如果有大量的排版和交互区别,还是应该及时单独设计移动站点。更何况移动端和桌面端的差别不仅仅在于排版和交互,运算性能、带宽、续航以及用户心理等等都会有所不同。

我们当时没有使用什么 “先进” 框架,就用了 Bootstrap 的布局和 jQuery 库,许多移动端的问题都要自己解决。

所以遇见了形形色色的移动端坑,包括但不限于:

  • 300 ms 点击延迟问题
    • 点击穿透问题
    • 解决方案:使用 fastclick
  • 移动端的 hover 样式无法还原问题
  • iOS 和 Safari 的坑
    • iOS 10+ Safari 强制自动缩放问题
    • iOS Safari modal 打开后视窗滚动问题
AMP&PWA

两者都是 Google 发起的增强移动端网页访问体验的项目。PWA 更狠,欲与原生 App 试比高。

浏览器兼容性问题

  • IE 的 Placeholder 兼容问题
  • Chrome 的历史记录问题

    Chrome 对前进后退的缓存判断是按照当时获取的数据来决定的,对于异步局部刷新,Chrome 在历史数据中缓存的只有部分数据。 FF 和 Safari 的行为更为科学,不知道现在 Chrome 这个问题是否依然存在。

    虽然我们不做 App,但大量使用了 Ajax 进行局部刷新以尽可能让体验更好

静态资源版本控制

主要好处是可以开启强制缓存,避免缓存验证请求。参见《前端工程精粹(一):静态资源版本更新与缓存》,讲得很清楚。

前端自动化测试

参见《前端自动化测试探索》,总结得很好。大厂就这点好,总是可以不断完善基础设施,无奈小作坊资源匮乏,这方面只能基本靠心灵手巧。

邮件模板

邮件的样式难以处理,不同邮件客户端支持的 HTML 标签和 CSS 均有所不同,渲染方式也五花八门9,又难以调试,因此要确保样式的一致难上加难。 推荐 Litmus,可以自动对邮件在不同客户端的呈现进行测试,还有一个 Email Builder 可以用来编辑邮件并预览客户端中的呈现效果。 CSS Support Guide for Email Clients 则类似于邮件版的 Can I Use。

和网页类似,邮件同样可以做成响应式10,相关的资源和工具很多,可参考这个列表

Gmail 提供结构化数据标记的额外支持,可对某些特定类型的邮件进行高亮及交互扩展。 但如果邮件数量不大此功能并不会被触发。

优化

Web 应用加速前端三板斧:

  • 减少延迟

    • 减少请求数
      • 缓存
        • 预请求 DNS
      • 合并请求
        • 合并资源
      • 按需请求外部资源
      • 减少重定向
    • 减少请求量

      • 缩小资源

        • 压缩资源

          记得加 sourcemap 方便生产调试

      • 缩小 cookie

        • 静态资源不使用 cookie
    • 减少请求距离

      • 使用 CDN 资源
  • 减少阻塞

    • 合理组织 CSS 与 Javascript
    • 异步加载
      • 预加载
    • 按需加载
  • 减少重排与重绘

    • 避免直接频繁的 DOM 操作
    • 指定图片大小

还有无法物理加速,只能优化体验的障眼法:预判/即时成功反馈,异步处理,成功静默,失败后回滚,提示重试。

可以参看 Best Practices for Speeding Up Your Web Site - Yahoo Developer Network译文)。

鉴权授权 Authn&Authz

Django 自带的模块及第三方库已经足够好。鉴权这部分的要点是密码要使用耗时较长的单向散列后进行存储,最好还要加 salt 甚至 pepper。相较于经典的 Cookie/Session 方案,JWT 方案无法主动撤销令牌,除非动用服务器存储。但如此这般则跟 Cookie/Session 的老办法没有太大区别,失去了 JWT 的轻巧,除非只用 OAuth2 鉴权。

用户 ID 和用户名的设定请参考:

鉴权功能容易忽略的点:

  • 不应提示登录失败具体是账号还是密码错误。
  • 隐私敏感的网站不应在注册或密码找回等环节泄露用户使用信息,无论用户是否存在,都应该视作存在/不存在。
  • 用户更换密码后,应使除当前会话以外之前所有关联会话失效。
  • 用户登录或注销的动作应跨标签页同步通信响应。

国际化和本地化 i18n&l10n

i18n 和 l10n 经常难以区分,简单来说就是 i18n 只做一次,而 l10n 需要每个目标区域做一次。 务必将国际化和本地化的工作交给专业框架,不要试图自行解决,低估其复杂性。 比如说复数形式就比一般人想象的复杂得多,事间存在的各语言复数形式远不止中英文这两种,还有各色奇形怪状的规则。

同一内容不同语言版本的页面需要有不同的 URL,有三种实现方式:子域名、语言代码前缀和语言参数。如果站点内容不够多,规模不够大,多语言子域名的方案有些过重,难以部署。而语言参数的方式过于松散,也不利于 SEO11,只要有可能,我们会尽量采取分层 URL 的模式。因此我们在 URL 中使用语言代码前缀标识语言版本。为保持 URL 简洁,我们没有单独为默认语言英语设置带语言代码前缀的 URL。即访问无语言代码前缀的 URL 将根据语言倾向跳转到有对应语言前缀的 URL,但如果语言偏好为默认语言则保持无语言前缀的 URL。 切换语言功能有不同方案,注意这里并不能仅仅跳转到对应语言 URL,还要更换语言偏好。一种方案是使用 POST,理论上也应该使用 POST(并非幂等请求),然而实践上并不太适合,因为还需跳转 URL,如此需要额外写 JS 进行控制。 使用 GET 的话会更方便,然后根据 HTTP 请求的 referer 返回跳转 URL。后来发现这种方案有较多问题。比如 referer 信息无法保证获得。当时用小米手机默认浏览器测试发现无法切换语言,后来和在小米的朋友一同确认了问题,原来当时版本的浏览器会丢失 HTTP referer 信息,造成切换语言无法顺利完成。再就是发现 Safari 会对跳转响应进行缓存(不清楚目前是什么情况),即使是 302 临时跳转。除非对 302 响应加缓存控制头部。 而用以切换语言的 URL 摆在页面上也不利于 SEO。

最后发现老大哥 Airbnb 的方案很值得学习,到底姜是老的辣,切换语言 URL 是在当前页面 URL 上加上切换语言参数来实现,完美回避了上边的问题,并保持了 GET 请求的便捷性。 刚看了一下,学旅家还在用我们抛弃的简易方式,异乡好居现在采用的是相较于 Airbnb 更为取巧的方式,直接立即跳转到对应语言的 URL,同时在 Cookie 中塞入切换语言的参数,这样一步到位,同时保持目标 URL 的整洁,可以符合国际化 SEO 的推荐准则12,缺点就是需要额外的 JS 进行控制。 但奇怪的是异乡好居并没有严格跳转无语言前缀 URL,只对首页 URL 进行语言偏好跳转。

另一类本地化工作,比如切换汇率的功能不需要这么麻烦,通常做法就是一个 POST 修改偏好,URL 不变,刷新本页即可。

后端

模型层 Model

  • 将逻辑封装进模型,View 层尽量减少逻辑(比如条件判断),尽可能直接使用模型提供的数据。
  • 设计模型时对数据残缺要有足够的容忍度。
  • 适时隔离数据存储与行为表现。比如说空值的字段,可以有默认值的表现。再比如子元素的字段为空时,可表现为父级元素字段的值。
Denormalization

Denormalization 区别于非范式,是在范式的基础上增加反范式,增加信息冗余牺牲写性能来提高读性能。

When is the denormalisation necessary?

  • Field as query index field

  • Field needs massive computation

在我们的业务里,denormalization 的典型适用场景是区域的房价范围,需要根据所有区域内房源记录计算,又不适合实时计算,需要缓存,又要适时更新。一开始我们是手动编写信号来实现, 直到发现一个更方便的第三方库 django-denorm13,可以用装饰器与方法方便地编写依赖计算规则,同时可以自动截停循环依赖的情况。

在我们的分层模型中,父级/子极模型某些字段允许进行设置,但为空时又应该能根据子级/父级模型相应字段得出一个可行的值,这种需求可以使用 property/setter method + denorm 字段的方式实现。 用 property method 包裹 denorm 字段也可以用来隔离跨模型计算依赖/耗时计算依赖,缓解 denormalization 的更新压力。对于同一模型中的字段依赖计算关系就没必要绕弯子了, 因为只要初始化好数据, 之后模型内的依赖会在保存时自动完成一致性计算, 除非需要利用 setter。

平滑部署

可参看 Deploying a Django App with No Downtime译文)、The Art of Graceful Reloading 以及 Django migrations without downtimes。 最需要注意的是不要直接使用 Django 自动生成的 migration,避免直接删除字段,手动编写 migration,尤其是失败回退的脚本。 数据库的删除操作一定要在部署完成后再进行。

更大规模的数据库迁移请参考:Online migrations at scale

优化

"Premature optimization is the root of all evil." ── Donald Knuth

应对高并发的常见手段除了缓存外,就是多开机器上 Load Balancer,做数据库连接池、读写分离14

读写分离的方式有:

  1. 数据库配好主从,自动一致性同步。Django 这边配好数据库读写路由。
  2. 透明数据库读写分离。不改 Django 代码,多一个中间层处理连接。

然鹅我们根本就没有活到必须优化的时期。

缓存

一个 HTTP 请求从客户端浏览器出发,将陆续经过 CDN Accelerator、Load Balancer、Web Server 抵达 App Server,还会向 Database 发起请求。 缓存优化的目标就是尽量减少请求的流动,让请求在更前端得到响应,从而减少 I/O 的时间,也减轻了服务器的负担。因此我们要在请求的每一站都设置缓存:

  • 浏览器缓存

    • HTTP 缓存15

      • 缓存控制

        HTTP 响应头中 Cache-Control / Expires

      • 缓存验证

        HTTP 响应头中 If-Modified-Since / Last-Modified / Etag / If-None-Match

    • DNS 缓存

      HTML 中 dns-prefetch16

  • CDN 缓存

  • Openresty/Nginx 缓存

  • Django 缓存

  • 数据库缓存

"There are only two hard things in Computer Science: cache invalidation and naming things."

── Phil Karlton

缓存主要的难点是何时更新与如何更新。 ORM 查询缓存有较多第三方库进行支持。 这篇《缓存更新的套路》总结得很好。 再就是小心惊群问题

动静分离

要高效利用缓存系统,需要动静分离,即对文档不变的部分和易变的部分进行分离。 比方说我们的首页除了 header 和 footer,对所有用户呈现的内容是一样的,那么这部分只用整站缓存一份(per site),而 footer 和 header 则应该对每个用户缓存一份(per user)。 在我们的场景里文档不同部分主要分为 per site、per user、per region 三种易变程度的数据,其他还可能有 per ip、per session 等。

至于分离数据最后的整合是由后端处理还是前端异步处理就看业务需求。一般情况下,强制缓存 per site 的数据,并缓存到 CDN 端。 Per user 的数据则用 AJAX 拿。但假如说页面上存在 per user 的高优先级脚本,这就可能不太合适,最好在后端提前组装后送出。 然而这样就无法利用更靠近客户端的 CDN 上的 per site 数据缓存,除非 CDN 是自建或者可以运行代码组装数据。

反爬虫

公开标识符一定不要使用自增 ID,尽量使用 UUID 和 UU Slug。一方面避免爬虫过于轻易进行抓取,其次也避免泄露业务规模的信息(当然如果就是想展示也无妨),另外 UU Slug 还有 SEO 加成。 如果一定要使用数字编号或者指定格式,使用双射可逆的 Pseudorandom Function 即 Pseudorandom Permutation 如 Format-Preserving Encryption17 可以做到利用自增 ID 生成外人无法猜透的与原 ID 同值域的加密 ID。

设置多级访问频率限制,我们使用的 CDN 服务对频繁访问有验证码触发,并且允许 ban IP。

市面上还有其他反爬虫技巧,最阴的莫过于并不阻止识别出的恶意爬虫,而是偷偷给它塞假数据。 还有放钓鱼链接甚至 zip 炸弹的手段,但对于定向型爬虫不太有效,只适合无脑探索的爬虫。

业务

  • 永远考虑服务降级处理。比如租期搜索精确匹配无结果就多层降级进行模糊匹配。
  • 单数库存与复数库存

    整合房源后,有两类不同库存模式:单数库存只有 Coming Soon, Available 和 Unavailable 三种确定状态,而复数库存则有五种确定状态: Coming Soon, Sold Out, Last Few, Limited Availability 和 Good Availability

  • 租期期限的完备设定

    房源的租期大致有固定租期(租期不可自定义),定长租期(租期长度不可自定义)和灵活租期(租期开始和结束日期可在范围内自由选择)三类。只存储租期开始和结束日期和租期长度范围四个字段来完备定义租期,并使用其他必要字段进行辅助方便运算。

文档

注释聊胜于无,如果感到不能一次及时解释清楚,也总该先写些什么,不用担心不够好,可以日后再补充完善。 Hack 一定要在原地详细注释,说清楚问题,方案的适用范围和解除条件,并附上相应 issue 链接。

其他参考:

其他

  • 继承/组合的层次不宜太深,复用和平坦要折中
  • 500 页面应该无外部依赖

SEO

不要轻信小道消息和旁门左道,最需要参考的是搜索引擎的官方文档,最需要改进的是网页自身的内容质量。

  • 首屏 SEO 友好(直到所有搜索引擎明确宣布完全使用 Javascript 渲染)。
  • 不同页面使用不同的标题和描述18
  • URL 中使用 slug,尽量使用分层结构,避免 URL 参数及提供面包屑导航有助于帮助搜索引擎进行理解19
  • 正确利用 HTML 标签的语义,尤其是 heading 标签。
  • 图片使用 alt 属性正确描述20
    • 甚至 SVG 图标也可以做 SEO。
  • 即使是弹出 modal 的注册登录,也要给独立页面的链接。
  • 重视内部链接上的文字贡献21
  • 非 SEO 功能链接/外链不使用用链接标签,转为使用 Javascript 跳转或使用 rel="nofollow"。

这里有一份 The Complete SEO Checklist For 2018,也可以看看 A technical guide to SEO

Google Search Console 允许使用抓取工具手动请求限量网页编入索引。

官方文档

Google 内容准则

百度

结构化数据标记

使用 Google 推荐的结构化数据标记可以突出内容在搜索结果中的展示效果。包括但不限于:

安全

网站安全

安全编码就不细说了,Python 本身和 Django 框架已经大大降低开发人员编码造成缓冲区溢出或 SQL 注入的可能。 我觉得最需要牢记的就是最小权限原则,扩展一点的说法就是一切够用就行,不要无谓地加大攻击面。

网站安全的具体保障手段可参考如下文档,覆盖已经比较全面:

个人安全

其实相比之下最不安全最不可控的是人而非机器。 除了提高自身防骗姿势水平以外,也要留心个人的软硬件安全,以防其成为渗透企业内部的跳板。千里之堤,常常溃于蚁穴。

  • 密码安全
    • 不使用弱密码,所有密码相异
      • 对于手握无数密钥的开发人员,用生命推荐使用 1Password,这钱花的值。
    • 使用二步验证,可以的话,使用设备验证而非短信验证,推荐 Authy
    • 不在陌生设备上输入密码,尽量使用设备验证
      • 必须使用时记得清除记录
  • 网络安全
    • 优先使用 Firefox,其次才是 Chrome
    • 慎用有访问浏览记录权限的浏览器插件
    • 使用浏览器插件 HTTPS Everywhere
    • 确认访问的 HTTPS 站点地址栏最左端是绿色标记
    • 不使用免费 VPN 或代理
    • 尽量不使用公共 WiFi,必须使用时最好挂 VPN
  • 系统安全
    • 所有软件勤更新
    • 不使用破解软件/注册机,替代方案为使用官方版本+某宝购买廉价注册码
      • 实在要使用请在虚拟机内操作
    • 优先使用 iPhone,其次才是 Android,说什么也别在日常用手机上 root
    • 所有加密选项尽量选择最强,并且不保留弱选项兼容

如果信息安全级别非常高:

  • 启用 BIOS 密码
  • 启用全盘加密
  • 邮件使用 PGP 加密
  • 使用专用机密软硬件即时通讯
  • 手机不使用数字或图形密码,启用密码保护措施
  • 文件下载后进行校验
  • 使用 Tor

匿名和防追踪是另一个话题,不在此探讨。

参考:隐私大爆炸

系列索引

推荐阅读


  1. 体量大,预算足的话可考虑 Akamai [return]
  2. DNSPod 也是创业公司常用的 DNS 解析服务,以前的话更推荐使用 DNSPod 国际版,可惜现在暂停使用了 [return]
  3. 国内可以选择 Ping++ [return]
  4. 博客的跳出率计算需要进行修正,方式是对停留时间进行阈值计算,参见 跳出率 - Google Analytics(分析)帮助 [return]
  5. 我们使用的是 Github 的付费私有仓库, 另一个可选服务 Bitbucket 上可以使用免费的私有仓库 [return]
  6. 目前其他的常见实现还有 PyPy 和 Jython,其实当年还有 Dropbox 雄心勃勃的 Pyston,当时被寄予厚望,现在已基本流产。 [return]
  7. 详见《实例详解 Django 的 select_related 和 prefetch_related 函数对 QuerySet 查询的优化》:(一) (二) (三) [return]
  8. SSR 至今依然没有那么美好,而且 SSR 到底应该算前端的职责还是后端的职责? [return]
  9. Things I've Learned About Building & Coding HTML Email Templates译文[return]
  10. Can Email Be Responsive? [return]
  11. 管理多区域和多语言网站 - Google [return]
  12. 将您网页的本地化版本告知 Google - Google [return]
  13. django-denorm 有已知 bug,不能响应 外键模型删除 和 bulk update [return]
  14. 5 Common Server Setups For Your Web Application | DigitalOcean [return]
  15. HTTP 缓存 | MDN [return]
  16. DNS 预读取 | MDN [return]
  17. 我们使用了 libffx 进行 FPE [return]
  18. 帮助 Google(和用户)了解您的内容 [return]
  19. 保持简单的网址结构 整理网站层次结构 [return]
  20. 优化图片 - 优化您的内容 [return]

最近修改于 2018-04-28 16:02