Orta therox   cover

构建属于自己的工具集

所有人都希望用最少���代码来快速、高效地构建应用程序。这就需要累积多年的经验,才能够找到正确的抽象 (abstraction) 来实现这一点。Artsy 移动端团队用 Swift 构建了多款应用,但是他们却说 Swift 并不是应用的未来选择。在本次讲演中,将会讨论如何使用 Swift 来构建应用,以及致使我们使用 React Native 的讨论是如何引起的。


概述 (0:00)

我是 Orta Therox,我目前在一家名为 Artsy 的公司工作。我们公司致力于让艺术同音乐一样,随时随地都可即兴欣赏。我们将艺术作品放到线上,用应用程序和网站来实现这一点。我是在五年半之前加入这家公司的,然后我组织成立了 Artsy 的移动端团队。我还帮忙构建了 Artsy 的默认开源协议,这让我能够在业余时间内,为我所依赖的开源项目贡献代码和做出改进。

过去五年来,我一直在研究 CocoaPods。这是一个相当庞大的项目,因此它占用了我大量的时间。去年,我还一直在构建一款名为 Danger 的项目。这个项目可以让繁琐的代码审查部分变得自动化,如果您是开发团队的一份子的话,那么我建议您应该去看一眼它。

我在这里将谈一谈我们为何要摈弃 Swift,更具体地说法是为什么要摈弃原生代码。我们现在有四款应用,其中两个是用 Swift 构建的,还有一个是用 Objective-C 构建的,最后一个则是被称为 “Eigen” 的 Artsy 核心应用。我知道在 Swift 大会上谈论不要用 Swift 是一个很奇怪的想法,但是我并不是夸夸其谈。构建应用的替代方法有很多种,比如说有 Ruby 的 Rubymotion 框架、C# 的 Mono 框架、Web 的 Cordova,以及 Java 的 J2ObjC。这些技术都可以与 Objective-C 或者 Swift 相抗衡。

在我深入此话题之前,我想先声明一点:我认为 Swift 是一门很棒的语言,就像我觉得 Objective-C 同样也很棒一样。同样地,我觉得 Rust、Go、Ruby、Elixir 和 JavaScript 都是非常棒的语言。它们都有各自的优点,并且这些语言我都使用过。在这里,我将把 Swift 这个词的语义予以扩展,它将表示您在进行原生开发时所用的工具。这里可以是任意一种代码,也可以是 LLVM、LLDB 或者 UIKit、Foundation 之类的工具。

为什么要摈弃 Swift 呢? (2:50)

我们为什么要一开始就谈论这个话题呢?

因为我们发现,目前我们构建应用的方式,并没有随着团队人员和应用代码的增长而得以改善。在 Artsy 核心应用 Eigen 中构建的任何东西,都很少重用现有的原生代码,因此我们构建功能所花费的时间也越来越长。这使得我们构建和测试应用所花费的时间在逐步增加,直到最后,我们需要用两个 iOS 工程师,才能够花费相同的时间,完成一名 Web 工程师所做的功能。

我们于 2012 年 11 月启动了 Eigen 项目。从那以后,我们就一直在完善这个项目,团队中通常有三个人,并且都是全职工作的。Eigen 需要展示 Artsy 当中的所有资源,这意味着我们需要大量用于浏览的自定义视图控制器。

我们在内部将这种做法称之为”既要搭建浏览器,还要搭建网页“。到了 2015 年 3 月,我们放弃了追随 Web 端的步伐,因为我们已经疲于追命了。我们得出了这个令人失望的结论之后,就开始反思,我们该怎么做才能解决这个问题呢?

对于 iOS 应用开发而言,其本质上不应该有导致开发速度缓慢的因素。在 2015 年冬假期间,我们探索了如何编写更多的可重用代码,从而让开发新功能更快。我们提出了很多代码重用的想法。每个新的视图控制器都需要相当多的新代码,并且我们还要尽力去实现设计当中的特殊部分。

我们对复用寄予了很高的期望,因为我们希望这个改变可以让我们的开发时间不再是一个问题。现在回想起来,我们那个时候并没有得出这样一个结论:导致这个问题的原因确实是因为原生开发比 Web 开发要慢很多,这才是关键所在。

原生设计模式 (5:20)

因此,让我们来讨论下可以解决这个问题的原生设计模式,一种是可能有用的工具修复模式,而另一种才是我们真正的想法。

Receive news and updates from Realm straight to your inbox

我们所提出的第一个设计模式,是构建自己的组件基础架构。我们使用了大量适用于 Swift 风格应用的 Swift 范例来完成此操作。我们探讨了构建 JSON-Powered 组件的这个想法。比如说 Spotify 的 Hub,或者 Hyperslow 的 Spot。我们很喜欢这个模式,因为它用一种非常独特的方式来描述数据应该如何组织,以及数据的行为应该如何体现。它也可以处理实时加载;通过更改 JSON,就可以立即在 UI 中反映出来。这个解决方案看起来非常优雅。

因此,我们考虑构建一个类似于 React 的组件结构。其中一个例子便是 Bending Spoons 的一个名为 Katana 的项目。当时移动端团队从 Web 团队中了解到,他们对响应式编程范例好评如潮,并且还大大提升了开发人员的生产力。

如果您发现应用程序的构建变得很慢,那是因为您需要将部分或者整个应用进行编译,此外还有签名过程,因此解决这个问题的方法就是要尽可能少地进行编译。我们可以迁移到浏览器的架构,然后将应用项目分解成不同的子模块,也就是通过脚本为每个控制器制作一个 Xcode 项目。这种想法听上去不错,但是从长远来看,要对其进行维护的话,无疑是非常脆弱和复杂的。

有一位工程师提出,要不要尝试一下 React Native?因此我们打算做一个实验,看一看将其集成进来后应用将会变成什么样,因此我们将一个视图控制器用 JavaScript 进行了重写。

探索解决方案 (7:45)

我们只有四名工程师,并且我们各自手上都有项目要忙;因此我们没有太多时间让我们的所有想法全部成为现实。

我和上次在这演讲的 Ash Furrow 两个人,对我们的测试策略进行了探讨,尝试用 Swift 构建更好的抽象表述,这将作为我们代码基础库的一个重要的全新部分。由于我们的 Swift 代码在每个域当中都是非常特殊的,这使得所有复用的可能性全被否决。此外开发时间也长得可怕。由于移动端的上线时间与 Web 端高度重合,这使得对移动端团队而言,开发出一模一样的功能是十分艰难的,同时也不利于 Web 团队的交付,更别提移动端团队还得同时实现多个 Sketch 设计界面。

我们实验 React Native 的效果非常好,对既有视图控制器进行重写的代码少了很多。这使得移动端的构建速度加快了不少,开发人员非常享受它带来的友好体验。这归功于我们所做的两个操作:

  • 原生代码
  • Swift 还需要大量的改进,才能让它符合我们既有的代码库。人们对 Swift 感到万分兴奋,每天都花费很多精力来改进它。Swift 是一门还在不断变化的语言,看到这些变化其实是非常有意义的,因为您完全可以影响到 Swift 的演进!这是一条捷径,就像显而易见的是:Apple 希望您立马就开始使用 Swift,但是替代品终究只是替代品,这也是 Swift 的缺点所在。

对于我们来说,Swift 内部的机制是一个未知领域。我们非常了解 Swift 的整个生态系统,因为我们参与了 CocoaPods 的开发,我们也参与并构建了自己的库。但是这也有很多缺点,原生代码对开发人员的经验要求相当严格。对于增量更新而言,我们应用的编译时间长达数十秒。而一年之前,我还在把玩一个名为 “Injection” 的 Xcode 项目,这真的让我深刻感受到了原生代码的发展速度。

通过代码注入 (injection),我们便可以进行二次更改,但是这势必需要将更多的代码迁移到 Swift 当中,由于缺少运行时的支持,这使得我们应用了代码注入之后,编译的时间实际上增加了多得多。Swift 不适合我们这个由 API 驱动的应用,也就是由 API 来控制视图控件的表示方式,并且我们应用内部需要大量用于展示数据的代码,这些代码非常重要,并且非常样板化 (boilerplate)。UIKit 希望尽可能将网络和 API 划分开,以便您能够基于 UIKit 本身来构建 UI。这使得您最终会为每一款应用构建一个完全自定义的网络栈、对象模型以及接口绑定。

React Native (11:20)

那么,���用 React Native 有什么缺点呢?

接入 React Native 使我们新增了 593 个依赖。是的没错,593 个依赖。这使得我们根本没有办法真正了解依赖关系树。React Native 已经有几年的历史了,但是如今仍然只有几家大公司在使用它,并且它演进的速度也非常快。最近,它还每两周发布一个新的版本。这多半是实用主义的问题,也就是意味着 React Native 的开发人员只会先修复 Facebook 遇到的问题。这换句话说也只不过是另一次炒作的开始罢了。这意味着大家看到的演进不过就是”修复某个问题“。这本不应该发生的。另一方面,React 对我来说,只不过是由 API 驱动应用的一个很好的抽象层而已。当我们将 React 与 Relay 混合使用时,就可以让我们客户端的基础代码只需专注于展示 API 结果。

用了 React Native 一年之后,我们仍然还在编写原生代码,并且您仍然可以随时回到 Objective-C 或者 Swift 的怀抱。更重要的是,每当您所构建的东西需要使用 React Native 的时候,我们发现编写闭包代码是非常困难的。

所有的 JavaScript 代码都是在单独的线程中运行的;布局和 UI 的更改最终会传回主线程,从而脱离了您所构建的组件。React Native 只有一个简单的布局系统,并没有自动布局。React Native 使用了一个简单和经过充分测试的 FlexBox 模型。自 2008 年以来,关于 FlexBox 的实现多如牛毛,并且很多公司都为客户端类似的界面构建了自己的一套 FlexBox。JavaScript 的测试目前也不如原生语言的测试,如果要比较的话就是 50 年代的汽车和目前的汽车的区别。即便理念相同,但是执行的层级也大相径庭。我们可以与这些项目的创作者沟通,帮助他们理清未来的发展方向。

这就是与原生开发的区别所在。举个例子,Radar 是 Apple 内部所使用的系统,其中外部开发人员只能对外部的贡献代码进行写入访问,也就是说 Radar 并不透明。对我来说,使用它感觉就是在浪费时间。我从别的地方遇到了一个问题,我完全没必要等着项目维护者去修复它,我可以让别人去响应我提出的这个问题,并且我们也可以通过公开索引 (publicly indexed) 获得很多好处。

在 25 人的开发团队中,有 5 名是移动端团队的开发人员。我们现在与 Web 团队分享同样的细节。开发人员可以、并且会在原生和 Web 当中同时发布新功能。迁移到 React Native 着实让我们的移动端团队减轻了很大的负担。

最后,所有的代码都是可建立分支的 (forkable)。我们对依赖的所有主要子系统建立了分支,并对其做出了改进,其中包括了 React Native、Relay 以及我们的文本编辑器。这个问题非常关键,我们这样就可以依赖自己的工具来构建代码了。我们可以迅速修复所遇到的每一个问题,唯一的阻碍只有时间以及自身的水平而已。

JavaScript (15:41)

让原生开发人员非常骄傲的一件事便是,JavaScript 是一门很可怕的语言。他们指出这门语言中有一些非常严重的缺陷,由于这些缺陷,使得它所有的好处都消失殆尽,就好像您所花的时间写出的完全是最糟糕的代码。对于 10 天就创造出来的语言而言,它已经经历了 22 年的改进,同时也日趋成熟。如今,写 JavaScript 是一个很快乐的事情,特别是我们有很好的工具可以来改善代码质量。

我们使用了 TypeScript,这是 Microsoft 为 JavaScript 添加了类型的一个变体。它还有可选类型,从而您可以在不需要类型的时候将类型忽略,然后在需要使用类型的时候再来使用。这无疑是非常务实的做法。

我们最终便能够得出结论,对于我们所创建的项目类型而言,React Native 是有意义的。我们认为我们已经有足够的知识来深入到我们所依赖的重点项目中。这也意味着,如果这些项目今后弃坑的话,我们仍然可以为它们提供支持,直到我们也弃坑为止。虽然看起来我们并不会弃坑,但是我们仍然还是考虑了未来五年的发展,而不仅仅只考虑五个月。我们将一直在 Artsy 上线各种应用。

React (17:09)

让我们来谈论点抽象的内容。React 是什么呢?

React 是 Facebook 的一个提供单向组件模型 (directional component model) 的项目,数据只能够单向传输。这可以将整个前端应用中的 MVC 栈完全替代掉。React 为抽象 Web 视图层次结构而构建了一个名为 “Dom” 的东西,从而方便对所有视图进行更改,然后 React 就可以处理这些视图状态之间的差异。

React Native 则是 React 的一个本地化实现,它并不是对网页中 Dom 的抽象实现。请注意的是,React Native 并不包括视图控制器。因为 Apple Cocoa 框架的 MVC 模型并不能直接映射到 React Native。您所编写的 JavaScript 代码,只是让 React Native 与所创建的视图层级进行交互而已。它并不会将您的 JavaScript 转换为原生代码,而是与原生代码进行交互。因此这个视图层级就可以位于任何地方。React Native 是跨平台的,因此它可以用在 Samsung TV、Windows Phone、Android 手机甚至网页当中。

示例 (18:30)

那么它在实践当中会是什么样子呢?我们将在 Artsy 应用中重新创建一个组件,然后我们再来查看一些我们应用中用到的真实组件。

为了创建一个组件,我们需要一个艺术家的名字以及年份。我们将从一个新的组件开始,我们将要使用 React.component 的子类创建一个对象。您也可以将它们称之为函数。我们希望这有一个默认的 export,这样当别人引用这个文件时,就可以得到一个类的定义。我们将需要 import React,这样才能够引用 React 当中的内容。在 JavaScript 中,文件中的每一个符号都需要引用。

接下来,我们将定义一个 render 函数,来描述该组件所输出的 Dom 内容。这个函数表示它只会返回一个视图组件。在这种情况下,它只会向 UIViewHierachy 中输出一个 UIView

我们可以通过一些 Label 来更好地表示我们的数据。现在我们的组件拥有两个 Label。由于我们使用的是 FlexBox,所以这两个 Label 实际上只是并列而已。

此外我们还希望这个组件能够复用,所以我们需要使用 ArtsyHeader 组件,现在它将在整个应用中拥有一个一致的编号。每个组件就相当于一个 UIView 和一个 UIViewController,您可以用 JavaScript 编写控制器的相关代码,在 JSX 中(JavaScript 版本的 HTML)编写视图的相关代码。这种分离模式的感觉非常好,就像我们用 Storyboard 或者 IB 来构建 UI 一样,然后我们通过代码让它们协同工作即可。

最后,我们需要对数据进行处理。这里我们需要使用名为 props 的东西。props 是有父类所设置的变量。这就是我之前所说的单向数据流。props 是由父类所配置的,对于当前组件而言是一种不可变的数据片段,如果 props 发生了变化,那么组件将被重新渲染。而在这个组件之下的整个组件树也会被重新渲染。这很有可能会让您的 UI 视图结构发生重组,这具体取决于重新渲染的组件是何种地位。您只需要这样处理即可:借助这些数据,我就可以填充这个组件结构了。


	<ArtistHeader artist={{ name: "Josef Albers", year: "b.1888" }} />

	import React from "react"
	import { View, Label } from "react-native"

	import Header from ".../components/header"

	export default class ArtistHeader extends React.Component {
		render() {
			const artist = this.props.artist
			return (
				<View>
					<Header>{artist.name}</Header>
					<Label>{artist.year}</Label>
				</View>
			);
		}
	}

Relay (21:10)

那么这位艺术家的数据是从哪里来的呢?这就需要我们在某个地方,发出请求并将数据处理后放到应用当中。我们用 Relay 来实现。我无法用言语表达我对 Relay 的喜爱之情。Relay 改变了我们整个客户端的编程范例。

那么 Relay 是什么呢?

Relay 是一个库,它允许组件描述其所需要的一小块网络请求,以便能完全渲染其当中的所有内容。Relay 会依据您的组件层次结构,然后自动将所有的网络片段组合起来,从而导出一个完整的 API 请求。

回到我们原来的代码当中,我们需要将 export 删除掉,因为我们在使用 Relay 的时候,这个东西会影响使用,但是也无法被自动移除。我们同时还要导入 Relay。现在这个实际上就类似于 Swift 中的一个私有类。这样它就能为我们处理所有的网络操作。


	import React from "react"
	import { View, Label } from "react-native"
	import Relay from "react-relay"
	import Header from ".../components/header"

	class ArtistHeader extends React.Component {
		render() {
			const artist = this.props.artist
			return (
				<View>
					<Header>{artist.name}</Header>
					<Label>{artist.year}</Label>
				</View>
			);
		}
	}

	export default Relay.createContainer(ArtistHeader, { fragments: {
		artist: () => Relay.QL`
			fragment on Artist {
				name
				year
			}
		`,
		}
	})

我应该要解释一下它是如何工作的。首先,我们需要导出一个 Relay.create 的容器,而不是导出类定义。然后在这个容器当中,我们需要声明:当您请求 API 以获取艺术家的信息时,需要包含有姓名和年份。Relay 随后会将所有这些片段放在一起,并创建一个隐含的组件树,之后 Relay 再将这些片段组合在一起,以准确地获得全部所需的数据。Relay 还会检查本地所缓存的数据,如果本地缓存不存在,那么它将会调用 API 来获取未缓存的结果。

这里有一些我们应用的真实代码,它展示了我们是如何处理艺术家的人物介绍,并通过组件显示出来的:


	class Biography extends React.Component {

		render() {
			const artist = this.props.artist
			if (!artist.blurb && !artist.bio) { return null }

			const bio = this.props.artist.bio.replace('born', 'b.')

			return (
				<View style={{ marginLeft: sideMargin, marginRight: sideMargin }}>
					<Headline style={{ marginBottom: 20 }}>Biography</Headline>
					{ this.blurb(artist) }
					<SerifText style={styles.bio} numberOfLines={0}>{bio}</SerifText>
				</View>
			)
		}
	blurb(artist) {
		return artist.blurb ? <SerifText style={styles.blurb} numberOfLines=
				: null
		}
	}
  

首先,如果人物介绍不存在的话,那么整个函数就会返回 null。这就意味着没有任何东西可以展示出来,所以这完全没问题。这是我们组件树的根视图。它配置了正确的边距。然后这里有一个标题,用于显示”人物介绍”这个词。下面就是我们要显示的简介了,其中的数据也可能来自一个会返回 null 的函数。随后在屏幕外面我们也有需要展示的人物介绍信息。在此下面我们定义了样式表和 Relay 片段。

整个视图控制器就是这些。有些控制器可能要复杂一些,但是也没有复杂到哪里去。您可以定义从 API 中所需获取的数据,然后再在相关的组件中使用这些数据。在 Apple 的工具中,您很可能会编写出一大堆东西,这就是不同所在。

工具集 (24:18)

让我们回到主题:工具集当中。在探索完原生代码这套沙箱之后,我们发现还有其他更好的工具,更适合我们构建 Artsy 的相关应用。

Apple 的工具集对 Apple 想要人们所制作的那类应用要更为友好,这并不奇怪。我们可以看一看 2016 年的设计奖。获得大奖的应用由 Complete Anatomy、Auxie(一个音视频创作应用)、DJ Pro、Ulysses(一个很棒的文本编辑器)、Streaks(一个基于习惯的待办列表应用)、Frame.io(一个查看视频的应用)以及 Zova(私人教练应用)。只有两款是真正需要联网才能使用的,其他绝大多数都是完全本地化的。我所说的本地化程序,我的意思指的是绝大部分输入和输出都是在应用内部进行的,这和 Apple 做的其他应用没什么两样。

例如:Garageband、Keynote、Notes、Photos、Calendar、Reminders 虽然都可以将数据同步到服务器,但是这并不是应用的核心功能。这只是利用 Swift 和 Xcode 所构建的应用。它已经是操作系统的一部分了。这就是 Apple 所编写、销售的应用的本质所在。

开发团队显然很关心这项工作,包括外部开发人员也是如此,但是他们都优先考虑 Apple 的内部需求,随后才会考虑我们的需求。这也是他们如何能成为实际上最有价值的公司之一的原因。而且,对于他们而言,并没有什么要做出改变的动力。我们这些正在构建完全由 API 驱动应用的人而言,最终都会与这些工具集做斗争。我们显然可以做到这一点,不过 Apple 已经意识到了这一点。现在我们有了一个新的强类型语言:比如说只提供准系统网络 (barebone networking) API 的高样本抽象 (high boilerplate abstractions)。

每年我们都会看到一个新的库出现,但是如何让编写应用更容易、更快速,对于这个核心问题而言,自 Storyboard 推出之后就再也没能得到解决。Swift 只是能使应用构建变得更加安全,但是我并不需要这样的应用。我完全可以理解 Apple。他们很少发布新东西,这样才能保持价值的稳定。我们最终决定不想继续沿着这条路走到黑了,一年之后,我们目前的工具集是完全开放、抽象更好的,这样我们便可以专注于应用的逻辑构建,不过这仅适用于应用的一个子集。

我已经多次强调过,这是我们整个团队和产品所做的决定。如果您正在制作一个完全由 API 驱动的应用,并且您觉得从 API 向屏幕输出数据的这个过程不对的话,那么就像我们所做的这样,或许答案就在 Xcode 之外。

问答时间到 (27:24)

问:只使用 React Native 的话,构建复杂动画和转场效果,或者其他自行创建的复杂行为的话,困不困难?

Orta:说实话,我并不完全相信 React Native 对于这一类应用而言很好用。有很多地方您都可以回到原生工具集当中来实现。我们的应用中是存在动画的,但是没有一个是看起来非常复杂的。因此这对我们来说从来都不是一个问题。有很多其他的库视图在 Android 和 iOS 中抽象出动画工具,但是在制作这些抽象文件的时候,绝对是有损耗的。如果我需要制作一个精美的动画应用,那么我就可能不会考虑 React Native 了。或许您可以强行这么做,但是总会感觉怪怪的。

问:假设您有一个特定的功能区域,这个区域是由 API 驱动的,但是此外您可能还需要复杂的原生动画效果。React Native 是否适用于构建这种混合的应用架构呢?

Orta:我们将 React Native 构建到应用中的方式是:我们构建了一个包含所有 React Native 库的 Pod;然后我们在一个完全独立的应用中对其进行处理,然后再通过 CocoaPods 进行整合,主应用并不知道 React Native 视图控制器和原生视图控制器之间的区别,因此如果您事先考虑这种做法的话,效果会很好的。不过这并不是 React Native 所推荐的方式,不过如果您正在构建一个向我们这样复杂的大型应用的话,那么这种做法是非常可行的,而且用起来感觉还不错。我们将其视作库来使用。这样就可以用在不同的应用当中,这样不同的应用就可以更快地完成构建,因为它只需要关注组建构建和实际的主应用即可,应用只需要关注应用本身即可。

About the content

This talk was delivered live in March 2017 at try! Swift Tokyo. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Orta Therox

Orta is the lead iOS developer at Artsy, building beautiful portfolio apps for some of the biggest Art galleries in the world. Encouraged by Artsy’s awesome commitment to open source, he regularly devotes time to working on and around the CocoaPods ecosystem, building tools like CocoaDocs, maintaining the Specs repository, and pruning documentation. If the CocoaPods team had fancy titles, he’d probably be called a community manager.

4 design patterns for a RESTless mobile integration »

close