slack使用react重写了web客户端。在这篇文章中,他们以重写emoji选择器为例,展示了react在性能和代码可维护性上给他们带来的巨大好处,以及给用户带来的体验升级。查看英文原文: rebuilding slack’s emoji picker in react。
slack正在将web客户端迁移到react。在最开始,我们的前端使用了jquery和handlebars。后来,社区开发出更好的方案用于创建可伸缩的、基于数据驱动的用户界面。jquery的“渲染后修改”模式直截了当,但无法与底层的模型保持同步。不同的是,react的“渲染后再渲染”模式可以保证渲染和模型的一致性。slack也紧跟业界的步伐,不断改进前端的性能和可靠性。
我们认为,要引入react,最好的办法就是先用react重写现有产品里的某个特性——这样我们就可以比较出新的开发流程和结果与原先的有什么不同。我们需要重写一个组件,这个组件必须具备交互性,能够自包含,并能够体现react在性能方面的优势。我们很快就找到了一个绝佳的组件——被重度使用且极度复杂的emoji选择器。
virtual dom的优势
阅读这篇文章要求对react有一定的了解,如果你不熟悉react,建议先阅读一下react的官方文档。简单地说,react是一个javascript代码库,可以用它方便地开发声明式的、基于数据驱动的用户界面。它的api很简单,主要由一个组件类组成,这个类包含了一些生命周期方法。组件本身不会生成html,相反,它们会生成类似dom的树,叫作virtual dom。react会比较两个virtual dom,并使用最少的操作将其中的一棵树转换成另一棵树。例如,你可以告诉react基于新的模型数据重新渲染整个视图,它就会以最快的速度帮你更新文本节点,就好像它有一个精灵军团在帮你完成dom的更新操作一样。
react擅长于将组件在单个模板上的各种行为合并在一起。举个例子,假设一个slack频道变为未读状态时,你通过javascript来更新频道的边栏:
找出这个频道的id在dom里查找这个频道对应的节点将节点状态切换成未读(应用css类)
这个过程很简单,不过你还得为其他事件编写不同的处理逻辑,比如“create”、“join”、“leave”和“rename”。相反,react把这5中情况合并在一起统一处理:
使用新的模型数据重新渲染频道边栏。
我们不需要为每一种dom操作编写代码,而是重新渲染整个组件,react会为我们完成这个过程。react通过让代码变得更通用(一刀切的模板)来简化开发。
emoji选择器
emoji是slack ui的一个组成部分,是最理想的react组件。它动态、离散,只需要少量的输入——一组emoji、默认皮肤和用户的emoji使用历史。刚好现有的emoji选择器需要进行性能调优,因为现在不管emoji会不会出现在视图里都需要进行渲染。在查找emoji时需要切换每个emoji的可见性,在重度使用时性能很成问题。新的slack团队准备了1374个默认emoji,这还不包括自定义emoji(在写这篇文章的时候,slack团队总共有3126个emoji,有些团队甚至更多)。重写emoji选择器将会对slack的日常使用产生重大影响。
我们选择在storybook里开发新的组件,storybook自称是一个“会让你喜欢上它的ui开发环境”。它不要求你改变开发方式,但会让开发、测试和代码审查变得更有趣。你可以在storybook里通过指定不同的属性来定义不同版本的组件。我们为emoji选择器增加了一个新皮肤和几种emoji查找方式。
组件布局
react emoji选择器的根组件是有状态的,而子组件则是无状态的。我们按照惯例把每个组件导出到单独的文件里。结构如下所示:
header
分类选项卡:列出了emoji的类别,每个类别都有一个“jump to”链接。搜索框:通过emoji的名称或别名过滤emoji。
body
固定的头部:显示当前类别选项卡的名称。emoji列表:所有类别的emoji虚拟列表。
footer
emoji预览:当前选择的emoji大图预览。皮肤选择器:显示当前的皮肤,并可以切换到其他皮肤。快捷动作(可选的):emoji的子集,用于快速回复消息。
react为编写无状态组件提供了两种方式:purecomponent类和function。function更为简单一些,不过它们在每次合并时都会进行渲染,会影响性能。react团队计划对function进行优化,不过目前最好还是避免使用它们。于是我们选择了purecomponent,它预定以了shouldcomponentupdate方法,这个方法可以防止在遇到相同属性时进行更新操作。
react是一个视图层,把它与自己开发的应用集成要比把它与标准的框架集成直截了当得多。我们不应该破坏emoji选择器的封装性,这样才能很好地与slack现有的模式集成在一起——我们希望这个组件就像是从一个端到端的react应用里拿出来的一样。为了保持选择器的纯净,我们在现有的模块系统里创建了一个轻量级的适配器。适配器挂载选择器组件,抽取模型数据,并监听来自外部的信号。采用这种模式,我们可以在开发新功能的同时逐步地迁移代码库。
新的开发流程
虽然使用react进行开发是一件很愉悦的事情,但将它集成到我们已有的开发流程里却不是那么一回事——至少在一开始不是那么令人愉快。在那个时候,slack使用的是自己开发的前端构建管道,没有所谓的导入、依赖或者复杂的转换(比如transpilation)。我们决定采用jsx语法和es2015+,并使用babel和webpack在本地构建emoji选择器的资源。
我们预期签入本地编译的代码会很痛苦,但我们低估了接连发生的合并冲突和依赖管理问题是多么令人抓狂。最后,我们尝试将webpack集成到我们的开发和staging环境里,目标是无缝地替代已有的工作流。为此,我们做了如下的工作。
基于webpack-dev-server开发了一个服务,当相关资源和依赖发生变更时,自动编译本地开发服务器上的资源。支持将webpack资源加载到单元测试里(这样就有可能为react组件编写测试用例)。重构生产环境的构建流程,将webpack资源推送到我们的cdn。
通过重写emoji选择器,迫使我们反思我们的构建管道如何能够以一种更健壮、更具伸缩性的方式打包资源。
性能
我们在少量的团队里部署了新的组件,并观察结果。我们观察了emoji选择器在用户使用不同的5种交互方式下的渲染速度,对于大部分的操作,react表现出了显著的速度提升。以下列出了选择器在正常规模团队里的不同渲染时间。
第一次挂载:-270毫秒(减少了85%)第二次挂载:-158毫秒(减少了91.3%)搜索(多个结果):+27毫秒(增加了259%)搜索(一个结果):-25毫秒(减少了53.2%)重置搜索:-68毫秒(减少了70.1%)
最大的改进来自“第一次挂载”,从318毫秒到48毫秒,减少了270毫秒,也就是85%。这要极力归功于react-virtualized——一个虚拟列表代码库——减少重新渲染emoji的数量。在默认视图上,react emoji选择器比dom少渲染了85%。
或许最让人感到吃惊的变化来自“搜索(多个结果)”,时间从17毫秒增加到了44毫秒,增加了27毫秒。旧选择器只是把不匹配的emoji隐藏起来,也就是说,当匹配到大部分emoji时会相对较快。但它的缺点也是显而易见的,“搜索(一个结果)”和“重置搜索”就让它的缺点原形毕露,因为此时它需要隐藏更多的emoji。
未来
使用react重写emoji选择器加快了渲染速度,同时简化了代码,让代码更容易维护。我们正在使用react重写剩余的代码。我们还有很多工作要做,这次重写将为用户的日常体验带来积极的影响,为此我们感到非常兴奋。与此同时,我们积累了react的实践经验,可以帮助平台更进一步。
本文转自d1net(转载)