@Lenciel

好的引导流程胜过10个新功能

SNS的有趣之处就在于,经常你会看到一些话,让你对着屏幕点头不已。比如下面这句:

Vhost threshold

Joshua Porter的这段话看起来是吐槽,但又实实在在的发生在参与创业大潮的你我身边。在2000年互联网泡沫破灭之前,那些最终幻灭的高科技公司的发展轨迹无非是:

  1. 用一个美好的想法或者概念融资
  2. 花9-12个月的时间推出一个产品
  3. 花大把的PR费用去做推广和宣传
  4. 发展无法满足预期
  5. 花6-9个月推出产品2.0版本
  6. 重复#1-#5,直到烧完所有的融资

在2000年之后,经过这十来年的发展,业界总结了很多的经验教训。我们在团队里面推行了敏捷、TDD等各种各样的方法论,我们在架构上进行了从monolithic到microservice的演进,我们的部署开始容器化并且都是Devops来完成了。

所有的人都充满信心的宣布,我们现在通过迭代,能够以较低的成本快速推出新版本了。然后,当你观察身边那些高科技公司的时候你发现:

  1. 用一个美好的想法或者概念融资
  2. 花3-6个月推出一个应用(web app、手机app或者干脆是基于微信开发的app)
  3. 提交到各种应用市场然后发动PR攻势
  4. 发展无法满足预期
  5. 买关键字、买量、买推广
  6. 发展无法满足预期
  7. 花3-6个月推出应用2.0版本
  8. 重复#1-#7,直到烧光融资

Hmmm…所以也难免有人会说这其实没有什么不同嘛:

Vhost threshold

然而无论预警者的声音再大声,作为创业者这种"启动-失败-再启动"的反复试错的精神都已经成为了我们的信条。的确,就跟你打开一张刮刮乐发现没奖时一样,如果我们做出来的项目没有人用,那么再来一次无疑是最轻松最诱人的选择。

然而如果你仔细看数据的话,会发现下一次失败是那样的必然。

数据:冷启动后的留存

下面这个曲线是从业者们最不愿意面对的曲线,展示了从流量导入到一个月后可怜的留存数据:

Vhost threshold

  • 1000个UV访问
  • 200个用户注册(20%)
  • 160个用户完成注册(80%)
  • 次日上线40%
  • 次周上线20%
  • 次月上线10%

也就是说30天后,日活用户大概是2%左右:而且你还不要觉得这数据很惨。如果去搜集真实产品的数据来看,除开IM类产品,大多数的产品甚至完不成这样的数据。

所以,做互联网产品首先要接受一个现实:你多半比拿破仑派去入侵俄罗斯的大军要崩得更快

加新功能?

当悲观的数据被端到面前的时候,决策层首先要明白,大多数时候加入任何新功能都不能把曲线扳回来。最容易出现的错误就是:

  1. 加入的功能不是服务于大多数人:特别是如果加入的功能仅仅服务于已经注册或者已经使用过产品的用户,而不是目前还不是用户的或者刚刚注册为用户的人群
  2. 加入的功能没有体现在大多数人能感知的地方:特别是如果加入了新功能,但是在用户进行注册或者使用的流程里面没有提示他们,让他们根本感知不到

这些错误之所以很容易犯,是因为我们作为产品设计和开发的人,很愿意把时间花在"很高级"或者"很酷"的功能上。而这些所谓的增加用户"粘性"的高级功能,如果你看看上面那条曲线,你会发现大多数是没有任何意义的:一个在用户使用产品第七天才会用到的功能,对于在第四天之后就不再出现的用户来说,等于没有。

其实在产品设计里面有一个概念是所谓的"engagement wall"。那些需要用户关联支付渠道或者累积经验值才能解锁的功能,或者是深度使用才能挖掘出来的功能,被认为是藏在墙后面的。比如微信上面的拍摄分享视频,发送红包等等。那些可以通过简单的投入就能让用户体验到价值的功能则被认为是在这堵墙外面的。比如微信上浏览好友的朋友圈,点个赞等等。把什么样的功能作为吸引用户的糖果放在面上,把什么样的功能放在墙里面藏着并激励用户来解锁它们,是需要经过精心设计的,但有一个规则不会变:那些放在墙里面的功能,是没法改变曲线的走势的。

如何选择下一个新加的功能?

首先想好要不要挑。

如果产品还在初期,你可能需要做的是精细打磨现有的功能,而不是加入新功能。新功能,特别是我们自认为可以"扭转乾坤"的新功能,往往意味着巨大的风险,巨大的投入,巨大的预期,以及极大的失败可能性。你需要掂量自己和团队是否能够承受得住。

决定好要挑?那就先去全面深入地了解你面对的问题域和你的用户的所思所想,搜集那些真正能够带来转化率的功能,然后:

  1. 做那些能够影响最多人的功能:成功的产品会把最多的时间花在那些非用户或者是随便来玩玩的用户使用的功能上,比如Slack的引导流程,比如Medium的访客评论
  2. 做那些能够带来转化的功能:特别是在精力和人力有限的时候,做影响曲线前半段走势的功能:注册、登录、引导等等,特别是首次登录后的引导。不同的产品类型,需要引导的方向是不一样的。测试平台上,你要引导用户顺畅的完成一次测试;SNS你要引导用户添加好友,完成第一次对话或者分享;云服务,你要引导用户完成机器创建、带宽选择、域名解析等等动作,让他可以使用你的服务。根据你的业务,给用户一个舒适度极高的引导流程,带来的转化率的提升是非常高的。

然后?

Let’s keep our fingers crossed and God bless us…

Django其实不是MVC

Vhost threshold

很多时候Django都被称为是一个MVC框架 — Model-View-Controller。这样说的人要么就是已经熟悉过其他的MVC框架,所以看到Django有自己的Template系统和views.py来放各种逻辑代码,就想当然的认为Django也是;要么就是其实没有真正实践过Django,从各种其他的错误文档里面看到或者是听说的。

MVC框架,是针对状态的。为了明白这个,我们假设你是在编辑一个图片:

  • 你得在内存里面保存这张图片 (Model)
  • 你得在屏幕上显示这张图片 (View)
  • 你得有办法让用户改变图片 (Controller)
  • 当用户改变图片后,你得更新显示:Controller通知Model更新状态,然后Model通知View刷新显示(最好是通过某种pub/sub机制,让View和Model之间是没有耦合的)

MVC框架主要是管理状态,让MVC三者是同步的:这三部分同时在内存(可能跑在不同的线程甚至进程)里,有各自的状态,相互之间进行交互,让变动同步到各方。

Django的Model-View-Template有很大的不同。

首先是没有状态。大多数的HTTP GET请求,拿到的数据库里面的数据,都被当成是immutable的不可重入的输入,而没有状态。而在一般的Web应用开发中,HTTP上有状态的交互可以通过:

  1. 修改保存在服务器端数据库里的数据
  2. 修改保存在客户端的数据(比如cookies)

共同来完成。因此一次状态的变化并不是在一个page的view里面保持的:状态一半放在当前的page和cookies里面,一半放在session数据库里面。

但是处理HTTP请求的时候,Django的MVT是完全无状态的。这里首先要说明的是,views.py这个名字本身是有一点儿误导的,因为给人的感觉是它只做"读"操作而不去"写"数据库(也就是说只是处理GET请求而不是POST请求)。但实际上GET/POST请求都会被放在views.py里面处理,所以更好的名字其实应该是handler.py:大多数Django的REST框架都是这样命名的。

处理GET请求的时候,如前面分析,本身就没有状态,而只是对输入的请求和服务器返回的数据进行展示。其次,当涉及数据修改的POST请求时,Django的处理其实是非常类似于老式的Web应用的。

所谓的老式的Web应用是指,过去的网站上当后台的数据发生变化的时候,其实是需要用户在前端自己点击刷新按钮来刷新的(最典型的刷新按钮就是浏览器里面那个刷新按钮)。这个动作背后发生的事情其实是:

  1. 除开标识当前是哪个用户在浏览哪部分数据的信息(当前的url,用户的identity等等cookies里面的数据),把浏览器里面其他的状态都丢弃
  2. 发起一个全新的请求,获取所有的数据,再次重建页面

说Django和老派的Web应用类似,是指一旦数据变更(比如一次SQL的INSERT或者是UPDATE),你需要返回一个redirect再做一次GET:"有数据的状态变化了,让我们重头再来一次"。

这也是为什么Django的ORM里面是没有一个"identity mapper"的Model处理状态变化的办法就是完全的无视它:当你觉得数据改变了时,直接重新获取一次数据重建页面。

这和大多数经典的MVC框架(比如AngularJS)是和这完全相反的套路:在设计上做了很多事情来避免"从头再来",而是通过建立MVC之间的消息机制,来通知各方的状态变化,做到同步。

MVC还有一部分是关于如何分隔代码。如果你把MVC当成:"把存储数据,显示数据和处理数据的代码分离",那Django的设计的确是符合这个模式的。

但是实际上这是一个粒度非常粗的描述,因此就把Django说成是MVC的其实会带来很多误会。

比如,Django是基于HTTP的,所以理解它的MVT,最好的办法就是实践它:看它的view里面是如何处理一个HTTP的请求并返回一个HTTP的response。如果你脑子里面有其他的不是基于HTTP这层次的MVC框架,用来类比学习Django,你大概会哭…

其次,Django的框架,它的app里面文件的组织和使用,和很多别的MVC框架也是不同的。

最近有个特别火的日志是Hynek Schlawack的Know Your Models。它是基于经典的MVC框架来假设,实际上Django并不是适用于这套假设的。

比如他觉得应该有pure的models,从而把M、V、C分离开做到可以独立进行处理。

但其实Django里面很多app都仅仅是数据库的简单wrapper。这种情况下其实没必要有pure的M,然后再加上一堆V和C。其实这是Django的美好之处:以admin这个app为例,它的设计初衷就是要在数据库上面封装一个足够简单的编辑层,以致于95%的代码都是可以自动生成的。

Model通过API暴露出来给View用,当然也是正确的思路。但我自己写代码的时候,就很喜欢把所有的直接调用.filter()的代码都放到models.py里面,这样一来models.py就是独立可测的。

并且,如果你写了一个pure的Model,而把逻辑代码从Model里面抽取出来放到别的地方去,那你在admin和其他ModelForms里面就没法重用了。

总的来说,在Django里面,model在创建的时候,就是有业务逻辑贯穿在里面的。如果你每个customer只能有一个email,那么你的model就得包含这个限制。如果你要改变这个规则,那么就不仅仅是MVT的某一方要改,而是从上到下都得修改。

甚至我个人认为MVC里面说的"逻辑和数据分离"这种思路本来就挺奇怪的。除非你把数据存储当成key-value这样的东西,那么你怎么可能在一个不是为了某个业务逻辑设计的数据库上开发出一个应用呢?

数据就是数据,是gloabl data而不是gloabal state。在整个HTTP请求被处理的过程中,它被认为是没有变化的:如果有,就应该再发一条请求来取最新的数据再去重画。

当我们开发Django的应用时,为了满足实际上的业务逻辑的需要,数据库的schema一般一直在变。这样Django的model就可以作为API的一个良好的基石,把往上走的事情做得尽量简单。

这涉及到软件开发里最基本的一个设计要点:你把数据库仅仅当成应用里面的持久化层,还是当成应用的一部分,甚至是最重要的一部分。

我其实一直偏向于后者:喂,把像Postgres这样RDMS当成一个持久层未免也太不尊重了吧!所以在使用Django设计app的时候,不但要思考"model layer",还要综合考虑其他数据库可以做的事情:比如contraint checking, transactions, triggers等等。同样,在测试的时候也不仅仅是测试那些字段和model之间的关系,而是要考虑对业务逻辑的测试。