Skip to content
This page has been auto-translated and may contain errors.View in English

什么是框架? ​

假设你在做一个小型 Web 应用:账户系统、动态流、设置页面。在你自己的想法能出现在屏幕上之前,你需要一堆代码:把 URL 转换成页面的代码、和数据库通信的代码、保护密码安全的代码,还有在数据变化时重新渲染画面的代码。这些都不是你这个应用独有的创意,每个应用都需要它们,而且从一个项目到另一个项目,这部分代码基本都是重复的。

这些底层管道你当然可以自己写,事实上过去很多年人们就是这么干的。但这样做很慢,各种细微的 bug 也往往藏在这里,而且每个团队最后都会造出一套大同小异、又都有点小毛病的同类机制。框架就是大家积累出来的答案:把这些共用的底层管道写一次,经过成千上万个项目的打磨、变得足够可靠,再打包成一套结构,告诉你自己的代码该往哪里放。你负责写出让项目与众不同的那部分,框架负责处理每个项目都要重复的那部分。

框架不是 Web 开发的专利。凡是有程序被开发出来的地方,都会有框架的身影,这一章讲的正是"框架"这个概念本身,跟你用什么语言、做什么领域没有关系。

什么是框架(顺带说说什么是库) ​

"框架"这个词经常被随便乱用,所以我们直接从代码层面看一个真正管用的区分:

js
// 库:你的代码说了算,需要的时候才去调用库。
const label = dateLibrary.format(order.createdAt, "MMM D");

// 框架:框架说了算,它会调用你插进去的代码。
export default function OrdersPage() {
  return listOfOrders();
}

第一行是你在掌控全局:你的程序自己运行,只是在需要的时候借用一个工具来完成某件事。第二行则完全不同。你自己永远不会去调用 OrdersPage,你只是把它写好,放在框架要求的位置上,框架会在合适的时机——也就是访客打开订单页面的那一刻——去调用它。

这种反转有个专门的名字:控制反转(inversion of control)。用库的时候,你的代码掌控着程序的运行流程,需要时借用一些辅助工具;用框架的时候,框架掌控着运行流程,你的代码只是填进框架给你留出的空位。一句话概括:库是你去调用的,框架是主动来调用你的。

同样的模式出现在编程的各个角落。Django 接收 Web 请求,然后调用你写的视图函数。Flutter 运行应用本身,然后询问你的 widget 该画些什么。Unity 运行游戏循环,每一帧都会调用你的脚本。pytest 找到你写的测试函数,然后帮你把它们跑一遍。领域各不相同,思路却是同一个:框架掌控着整台引擎,你负责提供它预留位置要用到的零件。

如果想找个画面来理解:库就像一个放在你身边的工具箱,你搭建东西的过程中,需要用到哪个工具就伸手去拿。框架更像是一栋楼的框架结构,你到场的时候它已经立在那里了。墙体、线路、管道都已经各就各位,你的工作是把自己的东西填进那些让这栋楼真正属于你的房间里。两者都能替你省事,区别在于谁来决定整个建筑的形状。

这种结构会带来一些约定要求,这其实是它的一个特点。大多数框架都遵循**约定优于配置(convention over configuration)**的原则:把文件放在框架期望的位置,按框架期望的方式命名,一切就会自动接通,不需要额外写配置代码。Rails 让这个说法广为人知,如今大多数现代框架都在遵循某种版本的这个原则。这样做的好处是,任何熟悉这个框架的开发者,打开任何用它构建的项目都知道该去哪里找东西。代价是这些约定本身又是一件你得先学会才能让一切正常运转的东西,而且如果你想跟这些约定对着干,几乎总比顺着它们走要痛苦得多。

控制反转会改变你阅读和调试程序的方式。在一个普通脚本里,调用栈从你自己的代码开始,栈上的一切都是你的。而在框架内部,调用栈从框架内部深处开始,你写的函数只是框架选择去调用的一些条目:钩子、处理函数、生命周期方法。那句老笑话说得再准确不过:"别来找我们,我们会来找你的(don't call us, we'll call you)"。实际意义在于,学习一个框架,与其说是学它的 API 表面,更重要的是学它的时机安排——它会调用你哪些函数、什么时候调用、又期望你返回什么。当程序表现出乎你意料时,答案通常就藏在这个时机安排里,框架的生命周期文档就是那份值得随时翻开的地图。

Juno框架和库 库就是一个工具箱:你的程序自己掌控全局,需要用工具的时候才去拿一个。框架更像是一栋楼的框架结构:结构已经立好了,你把自己的房间往里面搭。真正让我一下子想明白的说法是:库是你去调用的,而框架是主动来调用你的。
Juno框架和库 库是你去调用的,框架是主动来调用你的,这个反转就叫控制反转。框架还会搭配一套约定:把代码放在框架期望的位置,它就会自动接通。顺着约定走,而不是跟它对着干,这基本就是用好一个框架最关键的技巧。
Juno框架和库 控制反转意味着调用栈从框架开始,你的代码是以框架调用的钩子形式出现的。所以真正要学的是时机:你的哪些函数会被调用、什么时候被调用、框架又期望拿到什么返回值。调试一个基于框架的应用,本质上就是在读它的生命周期,越早不再觉得这是"魔法",你就学得越顺。

框架能带来什么,又要付出什么代价 ​

支持使用框架的理由是很实在的。那些原本可能要花你几周时间才能搞定的问题,早就被别人解决好了,而且是被那些最早撞上各种棘手边界情况的人解决好的:密码处理、表单验证、路由、渲染。你的项目会有一套结构,新来的同事几分钟就能看明白,因为它和这个框架下的其他所有项目结构都一样。你还会继承一整个生态:插件、教程、已经有人问过并解答过的问题,以及一大批已经熟悉这套东西的可招募人才。

代价同样是实实在在的,值得同样直白地正视。框架是一个你无法掌控的庞大依赖,它有自己的 bug、自己的节奏、自己的一套看法。在你的第一个页面渲染出来之前,会有一段学习曲线要爬,而且你学到的一部分知识只是关于这个框架的,不是关于编程本身的。你的代码会被框架的形状所塑造,你待得越久,之后想脱身就越难。而且框架是会变的:大版本会不断推出,设计模式会被重新思考,跟上这些变化是一项持续存在的工作量,这在纯手写代码的情况下是不需要的。

这两份清单谁也压不倒谁。取舍完全取决于具体项目,所以对任何框架真正该问的问题是"它能不能让这个项目变得更简单"。一个框架能不能站得住脚,就看它能不能让你的项目变简单。如果能,就大方地用它。如果不能,接下来两章讲的正是如何尽早看出这一点。

生态效应是加分项里被低估的一项。在一个成熟的框架上,你遇到的问题几乎不可能是全新的:认证、文件上传、发邮件、部署,每一项都已经有人打包好或写好文档了。这就把原本要花好多天去搭建的工作,变成了只要几个小时去组装。对应的减分项是,这种熟练度是以框架为单位计价的:你知道的一部分东西是"Django 是怎么做这件事的",而不是"Web 服务器本身是怎么运作的"。要留意每一个便利背后那个更普适的原理,这也正是这套文档存在的意义。

有两项代价只会在生产环境里才显现出来。第一,抽象是会漏的:框架那层方便好用的表面下面藏着真实的底层机制,而当有一天"魔法"失灵时,你最终还是得去调试那套底层机制,只不过现在多隔了一层你自己没写过的代码。要为理解框架底层到底在做什么预留时间,因为迟早你会用得上。第二,升级的跑步机是实打实要付出的成本:一个大型代码库的大版本迁移可能要吃掉你几周时间,而悄悄跳过升级,往后就会悄悄变成没打补丁的安全隐患。把这两项都拿来跟纯手写代码的方案比一比:纯手写代码的可动部件更少,也没有什么要迁移的,但它会把每一个已经被解决的问题重新摆到你面前,让你自己承担风险去重新解决。这个权衡正是选择和学习框架这一章要讨论的内容。

Juno框架的收益与代价 框架把已经解决好的问题、一套一看就懂的结构,还有一整个帮你的生态,都交到你手上。作为交换,你要背上一个庞大的依赖、一段学习曲线,以及它自己那套做事方式。这两方面都是真实存在的,所以真正要问的问题是:它能不能让你手头这个具体项目变简单。有时候答案是令人开心的"能",有时候纯手写代码反而是更让人安心的路!
Juno框架的收益与代价 生态是这笔交易里被低估的另一半:在一个成熟的框架上,你几乎遇不到全新的问题。不过要一直留意每个便利背后那个更普适的原理,不然你的知识就会变成"这个框架是怎么做的",而不是"这件事本身是怎么运作的"。框架该做的就是让项目变简单,这才是唯一的检验标准。
Juno框架的收益与代价 有两项代价是后账:抽象是会漏的,所以总有一天你得隔着一层自己没写过的代码去调试框架的底层机制;大版本迁移是真实的工作量,不可能永远躲着不做。要提前把这些成本算进去,跟纯手写代码那种"要重新解决框架早就解决过的问题"的成本放在一起权衡。这两笔账我都交过,都不好受,但这不是搞教条主义的理由。

接下来往哪儿走 ​

弄清楚这个概念之后,自然而然会想问:市面上到底都有哪些框架?框架的种类会带你逛一圈主要的框架家族,从 Web 到游戏再到测试,涵盖每个领域里最热门的选择。之后,选择和学习框架会讲一些实用的东西:怎么挑一个框架、怎么学一个框架,以及什么时候你其实根本不需要框架。另外,如果上面那些 JavaScript 例子让你觉得陌生,JavaScript 课程会讲这门语言本身,在学任何基于它的框架之前,先打好这个基础才是正确的第一步。