在过去对框架的设计中我收到过的最有用的建议是“不要一开始就根据现有的技术去整合和改进。而是先搞清楚你觉得最理想的框架应该是怎样的再根据现在的技术去评估的确实现不了时再妥协。这样才能做出真正有意义的框架。”在这篇文章里就让我们按照这样一条建议来探索一下现在的 web 框架最终可以进化成的样子你绝对会被惊艳到。前端还是从前端说起。前端目前的现状是随着早期的 Backbone近期的 Angular、React 等框架的兴起前端在模块化、组件化两个方向上已经形成了一定的行业共识。在此基础上React 的 FLUX、Relay 则是进一步的对前端应用架构的探索。这些技术在目前国内的大公司、大团队内部实际上都落地得非常好因为很容易和公司内部已有的后端技术栈结合。而且这些纯前端框架的配套技术方案一般比较成熟例如在支付宝确定使用 React其实有一部分原因是它兼容 IE8并且有服务器端渲染方案来加速首屏。相比之下像 Meteor 这类从前到后包办的框架就较难落地。虽然能极大地提高开发效率整体架构非常先进但架构的每一个层级往往不容易达到行业内的顶尖标准。特别是在服务器端对大公司来说通常都有适合自己业务的服务器集群、数据库方案并且经受过考验。因此当一个团队一上手就要做面向十万级、甚至百万级用户的产品时是不太愿意冒风险去尝试的。反而是个人开发者、创业型的团队会愿意去用因为确实能在短时间内高效地开发出可用的产品出来。包括像 Leancloud 提出的这类型的服务也是非常受欢迎的。这种现状就是理想和现实的一个争论。Meteor 的方式能满足我对开发效率的理想而团队已有的技术方案能保障稳定。能否整合其中的优势不妨让我们进一步来细化一下对框架的希望- 有强大的前后端一致的数据模型层- 代码可以可以复用。例如我有一个 User 模型当我创建一个新的 user 时user 上的字段验证等方法是前后端通用的由框架自动帮我区别前后端环境。- 数据模型和前端框架没有耦合但可以轻松结合。这样在前端渲染型的框架进一步升级时不影响我的业务逻辑代码。- 由数据模型层提供自动的数据更新机制。例如我在前端要获取 id 为 1 的用户并且如果服务器端数据有更新的话就自动帮我更新不需要我自己去实现轮询。我希望的代码写法是:var user new User({id:1}); user.pull(); user.watch();实际上Meteor已经能实现绝大部分上述功能。但这不是软文。我要强调两点我不希望的- 我不希望这个数据模型层去包含业务逻辑也就是我创建的user对象我不希望它提供 login、logout 等 api。- 我也不希望数据模型层自动和任何ORM框架绑定提供任何 SQL 或 NoSQL 的数据支持。看到这两点你可能心中大打问号这两点不正是高效的精髓吗前后端逻辑复用屏蔽数据库细节。别急让我们重新用“理想的方式”来思考一下“逻辑”和“数据持久化”这两件事。数据与逻辑我们以这样一个问题开头任何一个应用我们的代码最少能少到什么程度这算半个哲学问题。任何人想一想都会得到同一个答案最少也就少到和应用本身的描述一一对应而已了。什么是应用描述或者说什么是应用我们会这样描述一个博客“用户可以登录、退出。用户登录后可以发表文章。发表文章时可以添加相应的标签。”抽象一下描述答案很简单数据和逻辑。如果你在一个流程要求严格的公司应用描述就是prd或系分文档。应用的数据就是数据字典应用的逻辑就是流程图的总和流程图那么代码最少能怎么写呢数据很简单参照数据字典我们来用一种即使是产品经理都能掌握的伪码来写//描述字段 User : { name : string } Post : { title : string, content : text } Tag : { name : string } //描述关系 User -[created]- Post Post -[has]- Tag这里为了进一步帮助读者从已有的技术思维中跳出来我想指出这段伪码和数据库字段描述有一个很大的区别那就是我不关心 User 和 Post 中间的关联关系到底是在两者的字段中都创建一个字段来保存对方的id还是建立一个中间表。我只关心我描述它时的逻辑就够了。数据描述的代码最简也就简单到这个程度了。那么逻辑呢我们先用按常规方式试试class User{ createPost( content, tags[] ){ var post new Post({content:content}) post.setTags( tags.map(tagName{ return new Tag(tagName)} ) ) return post } }好像还不错如果今天产品经理说我们增加一个 功能如果文章里 某个用户那么我们就发个站内信给他。class User{ createPost( content, tags[] ){ var post new Post({content:content}) post.setTags( tags.map(tagName{ return new Tag(tagName)} ) ) if( at.scan(content) ){ at.getUser(content).forEach( atUser { system.mail( atUser ) }) } return post } }你应该意识到我要说什么了像互联网这种可以快到一天一个迭代的开发速度如果没有一个好的模式可能用不了多久新加的功能就把你的 createPost 搞成了800行。当然我也并不是要讲设计模式。代码中的设计模式完全依赖于程序员本人我们要思考的是从框架层面提供最简单的写法。让我们再回到哲学角度去分析一下业务逻辑。我们所谓的逻辑其实就是对一个具体过程的描述。在上面这个例子里过程无非就是添加标签全文扫描。描述一个过程有两个必备点- 干什么- 顺序顺序为什么是必备的某天上面发了文件说标题里带 XXX 的文章都不能发于是你不得不在函数一开始时就进行检测这时就必须指定顺序。如果我们用左右表示会互相影响的顺序从上下表示互不相干的顺序把上面的最初的流程图重画一下这是一棵树。如果我们再加个功能添加的标签如果是某个热门标签那么我们就把这篇文章放到网站的热门推荐里。这棵树会变成什么样子呢是的事实上人类思维中的任何过程都可以画成一棵树。有条件的循环可以拆解成递归最终也是一棵树。但重点并不是树本身重点是上面这个例子演化的过程从一开始最简单的需求到加上一点新功能再到加上一些恶心的特殊情况这恰恰就是真实世界中 web 开发的缩影。真实世界中的变化更加频繁可怕。其中最可怕的是很多时候我们的程序结构、用到的设计模式都是适用于当前的业务模型的。而某天业务模型变化了代码质量又不够好的话就可能遇到牵一发动全身大厦将倾的噩梦。几乎每个大公司都有一个“运行时间长维护的工程师换了一批又一批”的项目。Amazon曾经有个工程师描述维护这种项目的感觉“climb the shit mountain”。回到之前的话题在逻辑处理上我们的理想是写出的代码即短又具有极高的可维护性和可扩展性。更具体一点可维护性就是代码和代码结构能最大程度地反映业务逻辑。最好我的代码结构在某种程度上看来和我们流程图中的树一样。这样我读代码就几乎能理解业务逻辑。而可扩展性就是当出现变化时我能在完成变化时能尽量少地去修改之前的代码。同样的如果我们能保障代码和代码结构能和流程图尽量一致那么在修改时图上怎么改我们代码就怎么改。这也就是理论上能达到的最小修改度了。综上我们用什么样的系统模型能把代码变得像树形结构一样很简单事件系统就可以做到。我们把都一个业务逻辑当做事件来触发而具体需要执行的操作单做监听器那么上面的代码就可以写成// emitter 是事件中心 emitter.on(post.create, function savePost(){...}) emitter.on(post.create, function createTags(){...}, {before:savePost}) emitter.on(post.create, function scanSensitiveWords( post ){ if( system.scanSensitiveWords( post ) ){ return new Error(you have sensitive words in post.) } }, {block:all}) emitter.on(post.create, function scanPopTags(){...})//执行创建文章操作 emitter.fire(post.create, {...args})这样看来每个操作的代码变得职责单一整体结构也非常工整。值得注意的是在这段伪码里我们用了 {before:savePost} 这样的参数来表示操作的顺序看起来也和逻辑本身的描述一致。让我们回到可维护性和可扩展性来检查这种写法。首先在可维护性上代码职责变得很清晰并且与流程描述一致。不过也有一个问题就是操作的执行顺序已经无法给人宏观上的印象必须把每个监听器的顺序参数拼起来才能得到整体的顺序。在可扩展性上无路是新增还是删除操作对应到代码上都是删除或新增相应的一段不会影响到其他操作代码。我们甚至可以把这些代码拆分到不同的文件中当做不同的模块。这样在增减功能时就能通过增删文件来实现这也为实现一个文件级的模块管理器提供了基础技术。至此除了无法在执行顺序上有一个宏观印象这个问题似乎我们得到了理想的描述逻辑的方式。那我们现在来攻克这最后一个问题。拿目前的这段伪码和之前的比较不难发现之前代码需要被执行一遍才能较好地得到其中函数的执行顺序才能拿到一个调用栈。而现在的这段代码我只要实现一个简单的 emitter将代码执行一遍就已经能得到所有的监听器信息了。这样我就能通过简单的工具来得到这个宏观的执行顺序甚至以图形化的方式展现出来。得到的这张图不就是我们一模一样的流程图吗不知道你有没有意识到我们已经打开了一扇之前不能打开的门在之前的代码中我们是通过函数间的调用来组织逻辑的这和我们现在的方式有一个很大的区别那就是用来封装业务逻辑的函数和系统本身提供的其他函数没有任何可以很好利用的区别即使我们能得到函数的调用栈这个调用栈用图形化的方式打印出来也没有意义因为其中会参杂太多的无用函数信息特别是当我们还用了一些第三方类库时。打印的结果可能是这样而现在我们用来表述业务的某个逻辑就是事件。而相应的操作就是监听器。监听器无论是触发还是注册都是通过 emitter 提供的函数那么我们只需要利用 emitter就能打印出只有监听器的调用栈。而监听器的调用栈就是我们的流程图。代码结构可图形化并且是有意义的可图形化这扇大门一旦打开门后的财富是取之不尽的。我们从 开发、测试、监控 三个方面来看我们能从中获得什么。在开发阶段我们可以通过调用栈生成图那通过图来生成代码还会难吗对于任何一份流程图我们都能轻易地直接生成代码。然后填空就够了。在调试时、我们可以制作工具实时地打印出调用栈甚至可以将调用时保存的传入传出值拿出来直接查看。这样一旦出现问题你就可以直接根据当前保存的调用栈信息排查问题而再无需去重现它。同理繁琐的断点四处打印的日志都可以告别了。测试阶段既然能生成代码再自动生成测试用例也非常容易。我们可以通过工具直接检测调用栈是否正确也可以更细致地给定输入值然后检测各个监听器的传入传出值是否正确。同样很容想到监控我们可以默认将调用栈的数据建构作为日志保留再用系统的工具去扫描、对边就能自动实现对业务逻辑本身的监控。总结一下上述用事件系统去描述逻辑、流程使得我们代码结构和逻辑能达到一个非常理想的对应程度。这个对应程度使得代码里的调用栈信息就能表述逻辑。而这个调用栈所能产生的巨大价值一方面在于可图形化另一方面则在于能实现测试、监控等一系列工程领域的自动化。到这里我们已经得到了两种理想的表达方式来分别表述数据和逻辑。下面真正激动人心的时刻到了我们来关注现实中的技术看是否真的能够做出一个框架让我们能用一种革命性的方式来写应用理想到现实首先来看数据描述语言和和数据持久化。你可能早已一眼看出 User -[create]- Post 这样的伪码是来自图数据库 Neo4j 的查询语言 cypher 。在这里我对不熟悉的读者科普一下。Neo4j 是用 java 写的开源图数据库。图数据本身是以图的方式去存储数据。例如同样对于 User 这样一个模型在 关系型数据库中就是一张表每一行是一个 user 的数据。在图数据库中就是一堆节点每个节点是一个 user。当我们又有了 Post 这个模型时如果要表示用户创建了 Post 这样一个关系的话在关系型数据库里通常会建立一个中间表存上相应 user 和 post 的 id。也或者直接在 user 或 post 表里增加一个字段存上相应的id。不同的方案适用于不同的场景。而 在图数据库中要表达 user 和 post 的关系就只有一种方式那就是创建一个 user 到 post 的名为 CREATED 的关系。这个关系还可以有属性比如 {createdAt:2016,client:web} 等。你可以看出图数据和关系型数据库在使用上最大的区别是它让你完全根据真实的逻辑去关联两个数据。而关系型数据库则通常在使用时就已经要根据使用场景、性能等因素做出不同的选择。我们再看查询语言在 SQL 中我们是以SELECT ... FROM 这样一种命令式地方式告诉数据怎样给我我要的数据。语句的内容和存数据的表结构是耦合的。例如我要找出某个 user 创建的所有 post。表结构设计得不同那么查询语句就不同。而在 Neo4js 的查询语句 cypher 中是以 (User) -[CREATED] -(Post) 这样的模式匹配的语句来进行查询的。这意味着只要你能以人类语言描述自己想要的数据你就能自己翻译成 cypher 进行查询。除此之外图数据当然还有很多高级特性。但对开发者来说模式匹配式的查询语句才是真正革命性的技术。熟悉数据库的读者肯定有这样的疑问其实很多 ORM 就能实现 cypher 现在这样的表达形式但在很多大公司里你会发现研发团队仍然坚持手写 SQL 语句而坚决不用 ORM。理由是手写 SQL 无论在排查问题还是优化性能时都是最快速的。特别是对于大产品来说一个 SQL 就有可能节约或者损失巨额资产。所以宁愿用 “多人力、低效率” 去换 “性能和稳定”也不考虑 ORM。那么 cypher 如何面对这个问题确实cypher 可以在某种程度上理解成数据库自带的 ORM。它很难通过优化查询语句来提升性能但可以通过其他方式。例如对耗时长的大查询做数据缓存。或者把存储分层图数据库变成最底层中间针对某些应用场景来使用其他的数据库做中间层。对有实力的团队来说这个中间层甚至可以用类似于智能数据库的方式来对线上查询自动分析自动实现中间层。事实上这些中间技术早就已经成熟结合上图数据库和cypher是可以把传统的“人力密集型开发”转变为“技术密集型开发”的。扯得略远了我们重新回到模式匹配型的查询语句上为什么说它是革命性的因为它刚好满足了我们之前对数据描述的需求。任何一个开发者只要把数据字典做出来。关于数据的工作就已经完成了。或者换个角度来说在任何一个已有数据的系统中只要我能在前端或者移动端中描述我想要的数据就能开发出应用不再需要写任何服务器端数据接口。Facebook 在 React Conf 上放出的前端 Relay 框架和 GraphQL 几乎就已经是这样的实现。再来看逻辑部分无论在浏览器端还是服务器端用什么语言实现一个事件系统都再简单不过。这里我们倒是可以进一步探索除了之前所说的图形界面调试测试、监控自动化我们还能做什么对前端来说如果前后端事件系统可以直接打通并且出错时通过图形化的调试工具能无需回滚直接排查那就最好了。例如在创建 post 的前端组件中//触发前端的 post.create 事件 var post {title: test, content: test} emitter.fire(post.create).then(function(){ alert(创建成功) }).catch(function(){ alert(创建失败) })