- 原文地址:Designing very large (JavaScript) applications
- 原文做者:Malte Ubl
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:Shery
- 校对者:Starrier Allen
这是我在 JavaScript 澳大利亚开发者大会(JSConf AU)上演讲内容的文字编辑记录。在 YouTube 上观看整个演讲视频。javascript
幻灯片文本:你好,我曾经构建过很是大型的 JavaScript 应用。前端
你好,我曾经构建过很是大型的 JavaScript 应用。我再也不那么作了,因此我认为如今是个好时机来回顾并分享我学到的东西。昨天我在会议聚会上喝啤酒时,有人问我:“嘿,马尔特,到底是什么赋予了你权利和权威,来说这个话题?”我想这个问题的答案实际上就是这个演讲的主题,尽管我一般以为谈论本身有点奇怪。大概是由于,我在谷歌构建了这样一个 JavaScript 框架。它被 Google 照片,Google 协做平台,Google+,Google 云端硬盘,Google Play,搜索引擎,全部这些网站使用。其中一些项目很是大,你可能已经使用了其中的一些。java
幻灯片文本:我认为 React 很好。react
这个 Javascript 框架不是开源的。它不是开源的缘由是它与 React 同时出现,我想“世界是否真的须要另外一个 JS 框架来作选择?”。谷歌已经拥有了一些 JS 框架,Angular 和 Polymer,而且我以为再有一个会让人们感到困惑,因此我只是认为咱们应该把它留给咱们本身。但除了不是开源的,我认为仍是有不少东西能够从中学习,值得分享咱们一路上学到的东西。android
一张人山人海的图片.webpack
因此,咱们来谈谈很是大型的应用,以及他们之间的共同点。固然可能会有不少开发者参与其中。可能有几十人甚至更多,他们都有本身的情感和人际问题,你必需要考虑到这一点。ios
即便你的规模不大,也许你已经在这个领域工做了一段时间,也许你甚至不是第一个维护它的人,你可能不了解项目的全部结构或者内容,可能有些东西是你不太明白的,你的团队中可能还有其余人不了解应用程序的全部信息。这些都是咱们在构建很是大型的应用程序时,必须考虑的事情。git
推特: 一个没有初级工程师的高级工程师团队是一个工程师团队。github
我想在这里作的另外一件事是以咱们的职业生涯说明下背景。我想咱们不少人会认为本身是高级工程师。或者是还差一点点,但咱们想成为一个高级工程师。我认为高级的意思是我几乎能够解决其余人可能抛出的任何问题。我熟悉个人工具,我熟悉个人领域。而这项工做的另外一个重要部分是我让初级工程师最终成为高级工程师。web
幻灯片文本:初级 -> 高级 -> ?
可是会发生什么呢?在某种程度上,咱们可能会怀疑“下一步多是什么?”。当咱们达到这个高级阶段时,咱们接下来要作什么?对于咱们中的一些人来讲,答案多是作管理,但我认为这不该该成为每一个人的答案,由于不是每一个人都应该成为管理者,对吗?咱们中有些人是很是优秀的工程师,为何咱们不该该在咱们的余生中也这样作?
幻灯片文本:“我知道我会如何解决问题”
我想提出一种方法来升级到高级水平。我把本身看成高级工程师的方式是,我会说:“我知道如何解决这个问题”,而且由于我知道如何解决这个问题,因此我也能够教别人去解决它。
幻灯片文本:“我知作别人怎么解决这个问题”
个人理论是,下一个层次是我能够对本身说:“我知道别人会如何解决这个问题”。
幻灯片文本:“我能够预测 API 选择和抽象如何影响其余人解决问题的方式。”
让咱们更具体一点。你说了这样一句话:“我能够预见我作出 API 选择时,或者我往项目中引入抽象时,它们如何影响其余人解决问题。”我认为这是一个强大的概念,可让我思考我所作的选择对应用程序的影响。
幻灯片文本:同理心的应用。
我会称之为同理心的应用。你在和其余软件工程师一块儿思考,你在思考你所作的事情以及你给他们的 API 是怎么样的,以及它们如何影响其余工程师编写软件。
幻灯片文本:对简易模式感同身受。
幸运的是,这是对简易模式感同身受。同理心一般很难,并且这仍然很是困难。但至少与你有同感的人,他们也是软件工程师。尽管他们可能与你大相径庭,但他们至少同你同样也在开发软件。当你得到更多的经验时,你能够很擅长的运用这种类型的同理心。
幻灯片文本:编程模型。
考虑到这些话题,我想谈谈一个很是重要的术语,那就是编程模型,这个词我会用不少次。它表明“给定一套 API,库,框架或工具,人们如何在这种背景下编写软件”。我演讲的真正内容是关于 API 等细微变化对编程模型的影响。
幻灯片文本:影响编程模型的示例:React,Preact,Redux,Date picker,npm。
我想举几个影响编程模型的例子:假设你有一个 Angular 项目,而且你说:“我将把它移植到 React 中”,这显然会改变人们编写软件的方式,对吧?可是接下来你想:“啊哈,60 KB 就为了使用一点虚拟 DOM 操做,让咱们切换到 Preact”,这是一个 与 React API 兼容的库,即便你作出了这个选择,它也不会改变人们编写软件的方式。或许随着项目的进展,你会以为“单单 React 自有的状态管理还不够,应用会变得很复杂,我应该有一些东西来管理应用状态,我会引入 Redux”,这将改变人们编写软件的方式。而后又来了个新需求“咱们须要一个日期选择器”,你到 npm 上进行搜索,有 500 个结果,你选了一个日期组件。你挑选哪个真的很重要吗?它绝对不会改变你编写软件的方式。可是,npm 以及它的庞大生态集合,绝对会改变你编写软件的方式。固然,这些只是可能影响人们如何编写软件的几个例子。
幻灯片文本:代码分割.
如今我想谈谈全部大型 JavaScript 应用在将它们交付给用户时的一个共同点:它们最终变得很是大,以致于你不但愿一开始就把整个应用一次性传输给用户。为此,咱们引入了这种称为代码分割的技术。代码分割意味着你为应用程序定义了一组打包。因此,你会说“有些用户只使用个人应用程序的这一部分,有些用户使用另外一部分”,所以,当用户实际使用应用程序时,只有使用到的部分才被下载执行。这是咱们全部人均可以作到的。像许多事情同样,它是由闭包编译器实现的 —— 至少在 JavaScript 世界中。但我认为使用 webpack 进行代码分割是最流行的方式。若是你使用的是 RollupJS,这是超棒的,他们最近也增长了对代码分割的支持。代码分割绝对是大家应该作的事情,可是当你将它引入到应用程序中时有一些事情须要考虑,由于它确实对编程模型有影响。
幻灯片文本:同步 -> 异步。
你有过去是同步如今成为异步的东西。你的应用程序在没有代码分割时,简单美好。整个项目只有一件大事。它启动,而后它很稳定,你了解它的前世此生,你没必要等待资源加载。有了代码分割后,有时候你可能会说“哦,我须要那个打包文件”,因此你如今须要利用网络来获取所需的文件,这也使得你必须考虑网络可能出现异常状况,因此应用程序也变得更加复杂。
幻灯片文本:人性化。
此外,咱们须要有人介入,由于代码拆分须要你定义如何打包,须要你考虑什么时候加载它们,因此,那些在大家团队的工程师们如今必须决定哪些文件打包到一块儿,何时加载那些打包文件。每次有人介入时,都会明显影响编程模型,由于他们必须考虑这些问题。
幻灯片文本:基于路由的代码分割。
有一种很是成熟的方法能够解决这个问题,它能够将咱们从进行代码分割的混乱中解脱出来,它被称做基于路由的代码分割。若是你尚未使用代码分割,那它多是你初次进行代码分割的方式。路由将应用程序以 URL 粒度进行分割。例如,你的产品页面可能在 /product/
上,而且你的分类页面可能在其余地方。你只需将每一个路由用的文件打包到一块儿,而后你的应用程序将根据路由自动进行代码分割。不管什么时候用户访问路由,路由都会加载相关的打包文件,有了路由以后,你能够忘记代码分割的存在。再从编程模型上来看,这几乎与将全部东西都打包到一块儿同样。这是一种很是好的代码分割方法,绝对是个好的开始。
可是这个演讲的主题是设计很是大型的 JavaScript 应用程序,而且这类应用程序很快会变得巨大无比,路由自己也会随之变大,以致于基于路由的代码分割再也不适用。实际上我有一个关于这类应用程序的好例子。
“public speaking 101”的谷歌搜索查询截图。
我正在弄清楚如何成为这场演讲的公众演讲者,而且我获得了一个很好的蓝色连接列表。你彻底能够设想这个页面很是适合将全部文件打包到一个路由里。
“weath”的谷歌搜索查询截图。
但后来我对天气感到疑惑,由于加州有一个严峻的冬天,忽然间有了这个彻底不一样的模块。因此,这个看似简单的路由比咱们想象的更为复杂。
“20 usd to aud”的谷歌搜索查询截图。
后来我被邀请参加此次会议,我查看了 1 美圆是多少澳元,那时出现了这个复杂的货币转换器。很显然,这些专用模块大约有 1000 多个,将它们放在同一个打包文件中是不可行的,由于打包文件的大小会有几兆字节,用户将会真的变得不高兴。
幻灯片文本:组件级别的懒加载?
因此,咱们不能只使用基于路由的代码分割,咱们必须想出一个不一样的方式来作代码分割。基于路由的代码拆分很不错,由于你将应用程序进行了最粗略级别的拆分,而当应用程序进一步增加时,它能起到的做用就微乎其微了。由于我喜欢直截了当,那么作超级细粒度而不是超级粗粒度拆分怎么样。让咱们想象若是咱们网站的每个组件都懒加载,会发生什么。当你只考虑带宽时,从效率的角度来看,这彷佛很是好。从延迟等其余观点来看,这多是很是糟糕的,但它确定是值得考虑。
幻灯片文本:React 组件同他们的子组件是静态依赖关系。
但让咱们想象一下,例如,你的应用程序使用 React。而且在 React 中,组件们同他们的子组件是静态依赖关系。这意味着若是你懒加载你的子组件,就会改变你的编程模型,而且事情会变得不那么美好,这让你只好叫停这种策略。
ES6 导入示例。
假设你有一个货币转换器组件,你想把它放在你的搜索页面上,你能够导入它,是这样的吧?这是在 ES6 模块中使用的普通方式。
Loadable 组件示例。
可是若是你想延迟加载它,你会获得这样的代码,你把它包装在 Loadable 组件中,你还使用一种懒加载 ES6 模块的新方式动态导入。固然有成千上万种方法能够作到这一点,我不是 React 专家,但全部这些方式都会改变你编写应用程序的方式。
幻灯片文本:静态 -> 动态。
事情再也不那么美好了 —— 一些静态的东西如今变成了动态的,这是编程模型改变的另外一个警示。
幻灯片文本:谁来决定什么时候对什么东西进行懒加载?
你不得已忽然想知道:“谁来决定什么时候对什么东西进行懒加载”,由于这会影响到应用程序的等待时间。
幻灯片文本:静态仍是动态?
人类再次出现,他们必须思考“有静态导入,有动态导入,何时该用哪个?”。弄错就很是糟糕了,当一个静态导入的文件忽然变成动态导入的时候,可能会把某些东西错误的打包进文件。随着时间的推移,同时你又有不少工程师在这个项目上开发,恐怕就会出错。
幻灯片文本:分割逻辑和渲染。
接下来我会分享 Google 如何作到保证良好编程模型的前提下,又有不错的性能的。咱们经过渲染逻辑和应用逻辑来分割组件,好比当你按下货币转换器上的按钮时发生的状况。
幻灯片文本:仅在渲染时加载是惟一的加载逻辑。
因此,如今咱们有两件独立的事情,而且咱们只在渲染时才加载组件的应用程序逻辑。事实证实,这是一个很是简单的模型,由于你能够简单地在服务端渲染页面,而后由实际呈现的内容,触发下载关联的应用程序打包文件。由于加载是经过渲染自动触发的,这使得人得以脱离系统。
幻灯片文本:搜索结果页面上的货币转换器。
这个模型看起来不错,但它确实有一些折中。若是你知道一般服务端渲染在 React 或 Vue.js 等框架中如何工做,这个过程被称为 hydration。hydration 是这样的,你服务端渲染的一些东西,而后在客户端再次渲染它,这意味着你必须加载代码来渲染一些已经在页面上的东西,这在加载代码和执行代码方面都是巨大的浪费。这么作既浪费带宽,又浪费 CPU —— 但它确实很好,由于你在客户端忽略了服务端渲染的东西。咱们在 Google 使用的方法不是那样的。因此,若是你设计这个很是大型的应用程序,你就会想:我是采用那种更复杂的超快速方法,仍是采用效率较低的 hydration 方式,但这样能有个良好的编程模型?你将不得不作出这个决定。
幻灯片文本:2017 新年快乐。
个人下一个话题是我最喜欢的计算机科学问题 —— 它不是命名问题,尽管我极可能给它起了个糟糕的名字。这是“2017 年假期特别问题”。过去有人写过一些代码,如今再也不须要它们了,但它仍然在你的代码库中?...这种状况时常发生,我认为 CSS 的问题尤其突出。你有一个大型 CSS 文件。里面有不少样式选择器。谁真的知道哪些样式选择器是否仍然对应着你应用中的内容?因此,你最终只能把那些代码留在那里。我认为 CSS 社区处于变革的最前沿,由于他们意识到这个问题,而且他们建立了诸如 CSS-in-JS 之类的解决方案。由于你的组件能够放到一个单独的文件里,2017 年假期特别问题组件,你能够说“它再也不是 2017 问题”,你能够删除整个组件,而且全部相关文件一并消失。这使得删除代码很是容易。我认为这是一个很是好的想法,它不只仅适用于 CSS。
幻灯片文本:不惜一切代价避免中央配置。
我想举几个例子,说明为何你想不惜一切代价避免在你的应用程序中采用中央配置,由于中央配置(好比大型 CSS 文件)使得代码难以删除。
幻灯片文本:routes.js。
我以前在你的应用程序中谈论过路由。许多应用程序都会有一个相似“routes.js”的文件,其中包含全部路由信息,这些路由将本身映射到某个根组件。这是一个中央配置的例子,你不会但愿在大型应用程序中这么作。由于有了这种中央配置,工程师会说:“我还须要那个根组件吗?我须要更新其余文件,那是其余团队负责的文件。我不肯定是否被容许修改它。也许我该明天再作“。以后,这些文件只会愈来愈大。
幻灯片文本:webpack.config.js。
这种反模式的另外一个例子是 webpack.config.js 文件,在这里你能够假设你经过它构建了整个应用程序。刚开始可能没什么问题,但随着时间的推移,这份配置再也不适用,你须要知道其余团队在应用程序中作了什么,这样才能对配置文件作出兼容性的调整。再一次,咱们须要一个模式来展示如何分散咱们构建过程的配置。
幻灯片文本:package.json。
这有一个很好的例子:npm 使用的 package.json。每一个软件包都会说“我有这些依赖关系,这就是你如何运行我,如何构建个人方式”。显然,对于全部的 npm,都不能有一个巨大的配置文件。这对于成千上万的文件来讲不起做用。这确定会让你在 git 操做中遇到不少合并冲突。固然,npm 很是大,但我认为咱们的许多应用程序已经变得足够大,让咱们不得不担忧一样的问题,而且必须采用相同的模式。我没有全部的解决方案,但我认为 CSS-in-JS 的想法将会涉及咱们应用程序的其余方面。
幻灯片文本:依赖关系树。
更抽象地说,我会描述这个想法,即咱们负责如何抽象地设计咱们的应用程序,如何组织它,做为承担塑造咱们的应用程序的依赖树的责任。当我说“依赖”时,个人意思是很是抽象的。它多是模块依赖关系,多是数据依赖关系,服务依赖关系,还有不少不一样的类型。
幻灯片文本:由路由和 3 个根组件构成的依赖关系树示例。
显然,咱们都有超复杂的应用程序,但我会用一个很是简单的例子。它只有 4 个组成部分。它有一个路由,知道如何从应用程序的一个路由到下一个路由,它有几个根组件:A、B 和 C。
幻灯片文本:中心导入问题。
正如我以前提到的那样,这具备中心导入问题。
幻灯片文本:由路由和3个根组件构成的依赖关系树示例。路由导入根组件。
由于路由如今必须导入全部的根组件,若是你想删除其中的一个,你不得不进入路由文件,删除引用,删除路由,并最终你有了 2017 假期特别问题。
幻灯片文本:导入 -> 加强。
咱们在谷歌已经为此提出了一个解决方案,我想向大家介绍一下,我想咱们历来没有谈过这件事。咱们提出了一个新概念。它被称为加强。这是你用来代替导入的东西。
幻灯片文本:导入 -> 加强。
实际上,这与导入是相反的。这是一个逆向依赖。若是你加强一个模块,你会让这个模块对你有依赖性。
幻灯片文本:由路由和3个根组件构成的依赖关系树示例。根组件加强了路由。
看看依赖关系图,它发生了什么,仍然是相同的组件,但箭头指向相反的方向。所以,不是路由导入根组件,根组件宣布本身加强了路由的功能。这意味着我能够经过删除文件来删除根组件。由于它再也不加强路由,因此这是删除组件的惟一操做。
幻灯片文本:谁来决定什么时候使用加强?
这真的很棒,若是它不是再次涉及人性化。他们如今必须考虑“我是该导入它,仍是使用加强?我在哪一种状况下使用哪种方式?”。
图片:危险。危险化学品。
这是这个问题的特别糟糕的状况,由于加强模块的能力,可以使系统中的全部其余东西都依赖于你是很是强大的,若是出错的话,就会很是危险。很容易想象这可能会致使很是糟糕的状况。因此,在谷歌咱们认为这是一个好主意,但咱们也认为它是非法的,没有人可使用它 —— 有一个例外:生成的代码。它实际上很是适合于生成的代码,它解决了生成代码的一些固有问题。有了生成的代码,你有时必须导入你甚至看不到的文件,必须猜想他们的名字。可是,若是生成的文件刚好不可见,并加强了它所需的任何内容,那么你就没有这些问题。你根本不须要知道这些文件。他们只是神奇地加强了中央注册表。
幻灯片文本:单文件组件指向其加强路由的组件。
咱们来看一个具体的例子。咱们这里有个单文件组件。咱们在其上运行代码生成器,并从中提取这个小的路由定义文件。那个路由文件只是说“嘿,路由,我在这里,请导入我”。显然,你能够将这种模式用于各类其余事情。也许你正在使用 GraphQL,你的路由应该知道你的数据依赖关系,那么你可使用相同的模式。
幻灯片文本:基本打包文件。
不幸的是,这不只仅是咱们所须要知道的。第二个我最喜欢的计算机科学问题,我称之为“基础垃圾打包文件”。在应用程序的打包逻辑中的基本打包文件老是会被加载,而与用户与应用程序的交互方式无关。因此,这一点尤为重要,由于若是它很大,那么全部进一步深刻的东西都会很大。若是它很小,那么依赖文件也有可能变小。一个小故事:在某个时候,我加入了 Google Plus JavaScript 基础架构团队,而且我发现他们的基础打包文件包含 800 KB 的 JavaScript。因此,我对你的警告是:若是你想比 Google Plus 更成功,就不要让你的 JS 基础打包文件超过 800 KB,但不幸的是你的文件体积很难维持在理想状态。
幻灯片文本:指向 3 个不一样依赖关系的基础打包文件。
这有一个例子。你的基础打包文件须要依赖于路由,由于当你从 A 到 B 时,你须要知道 B 的路由,因此它老是在周围。可是你真正不想要的是将任何形式的 UI 代码打包进基础打包文件,是由于取决于用户如何进入你的应用程序,可能会有不一样的用户界面。因此,例如日期选择器绝对不该该放在你的基础打包文件中,结帐流程也不该该。但咱们如何防止这种状况?不幸的是导入很是脆弱。你可能在无心中导入那个很酷的工具包,由于它有一个函数来生成随机数。如今有人说“我须要一种自动驾驶汽车的实用工具”,而且忽然将自动驾驶汽车的机器学习算法导入到你的基础打包文件中。相似这样的事情很容易发生,由于导入是传递性的,因此问题每每会随着时间的推移而累积起来。
幻灯片文本:禁止依赖测试。
咱们找到的解决方案是禁止依赖测试。禁止依赖测试是一种断言,例如你的基础打包文件不依赖于任何 UI。
幻灯片文本:断言基本打包文件不依赖于 React.Component。
咱们来看一个具体的例子。在 React 中,每一个组件都须要继承自 React.Component。所以,若是你的目标是基本打包文件中没有 UI,只需添加一个测试来肯定 React.Component 不是你基本打包文件的传递依赖。
禁止的依赖关系被删除。
再看一下前面的例子,当有人想添加日期选择器时,只会出现测试失败。而这些测试失败一般很容易就能很好地解决,由于一般这我的并非真的想要添加依赖关系 —— 它只是经过一些传递路径进入。比较这一点,当这种依赖关系已经存在了 2 年,由于你没有测试。在这些状况下,一般很难经过重构代码来摆脱依赖关系。
幻灯片文本:最天然的路径。
理想状况下,你会发现最天然的路径。
幻灯片文本:最直接的方式必须是正确的。
你想要达到这样一个状态,不管你的团队中的工程师作什么,最直接的方式也是正确的方式 —— 这样他们就不会离开这条道路,因此他们天然而然地作了正确的事情。
幻灯片文本:不然添加一个确保正确的测试。
这可能不老是可行的。在那种状况下,只需添加一个测试。但这不是不少人认为有权作的事情。可是,为确保你的基础架构保持不变,请为你的测试程序添加测试的受权。测试不只仅是为了测试你的数学函数是否正确。它们也用于基础架构和应用程序的主要设计特性。
幻灯片文本:避免在应用领域以外进行人为判断。
尽量避免在应用领域以外进行人为判断。在开发应用程序时,咱们必须了解业务,可是并不是团队中的每位工程师都能理解代码拆分的原理。并且他们不须要那样作。在不是每一个人都能理解它们的时候,试着将这些东西以一种友好的方式引入到你的应用程序中,并保持其复杂性。
幻灯片文本:能够轻松删除代码。
真的,让删除代码简单点。个人演讲题为“构建很是大型的 JavaScript 应用程序”。我能够给出的最佳建议:不要让你的应用程序变得很是大。最好的办法是在还来得及的时候开始删除东西。
幻灯片文本:没有抽象比错误的抽象更好。
我想再谈一点,那就是人们有时会说,没有抽象比错误的抽象要好。这实际上意味着错误的抽象代价很是高,因此要当心。我认为这有时会被误解。这并不意味着你不该该有抽象。这只是意味着你必须很是当心。
咱们必须善于找到正确的抽象。
幻灯片文本:同理心和经验 -> 正确的抽象。
正如我在演讲开始时所说的:实现目标的方式是使用同理心,并与团队中的工程师一块儿思考他们将如何使用你的 API 以及他们将如何使用抽象。你如何随着时间的推移充实这种同理心会成为经验。综上所述,同理心和经验使你可以为你的应用程序选择正确的抽象
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。