让你的代码讲出它的故事

2020 年 6 月 15 日

让你的代码讲出它的故事

现在用Hooks管理React函数式组件中的状态已经很容易了。我以前曾写过“用自定义Hooks作为服务”和“自定义Hooks中的函数式编程”(译文)。在本文中,我会分享自己做的一个相当简单的重构,通过重构带来了一种更整洁、可重用且更简单的实现。


本文最初发布于 Orizens 博客,经原作者Oren Farhi授权,由 InfoQ 中文站翻译并分享。


代码抽象


我认为代码应该是自解释的,并且能轻松到处移动和重用。有时,一种比较简单的方法是从基础的方法入手,一旦看到重复出现的模式,就可以将其抽象化。


我认为正确应用的代码抽象可以让很多事情一目了然。但抽象太多可能适得其反——很难找出实现的脉络,或者我喜欢称之为"糟糕的诗"。


我为ReadM™创建了 Speaker()组件,ReadM™是一款免费且易用的阅读 Web 应用,它可以激励孩子们通过实时反馈来练习、学习、阅读和讲出英语,并提供了很好的体验。



这一组件负责显示文本,且孩子读出句子或某个单词就可以用应用交互。为了改善用户体验,我决定在用户说话时添加单词高亮显示(很像卡拉 OK)。



React Speaker 组件布局


Speaker()组件会接收几个 props 来实现上述交互。


Speaker 的组件定义


以下是所有 props 的简要介绍:


  • text:Speaker所显示并“讲出来”的句子(或单词)

  • onSpeakComplete:念完之后,Speaker将调用这个回调

  • disable:禁用单击一个单词听它的发音的功能

  • verifiedtext中的一组单词,它们在当前会话期间已被念对

  • highlight:在text中之前已经念对的单词的布尔值数组

  • speed:一个数字,指示句子的读速


function Speaker({  text,  onSpeakComplete,  disable,  verified = [],  highlight = [],  speed,}: SpeakerProps) {  // code}
复制代码



Speaker 的行为和功能


接下来(该函数的主体),将定义高亮显示被念到的单词的状态,以及用于设置该单词的函数 handler。本节是很重要的一节,是本文要重点强调的内容。


const [highlightSpoken, setHighlightSpoken] = useState<{  word: string  index: number}>()const handleOnSpeak = useCallback(() => {  speak({    phrase: text,    speed,    onEndCallback: () => {      onSpeakComplete && onSpeakComplete(text)      setHighlightSpoken(null)    },    onSpeaking: setHighlightSpoken,    sanitize: false,  })}, [text, onSpeakComplete, setHighlightSpoken, speed])const handleOnSelectWord = (phrase: string) => {  speak({ phrase, speed, onEndCallback: noop })}
复制代码


Speaker 的显示:渲染


现在代码从 props 中获取值以准备显示属性,这些属性将传递到返回渲染值内的表示组件中。


const words = verified.length ? verified : createVerifiedWords(text, highlight)const rtlStyle = resolveLanguage(text).styleconst justify = rtlStyle.length ? "end" : "between"
复制代码


返回的渲染值是:


function Speaker(props) {  // all the above code commented  return (    <Column md="row" alignItems="center" justify={justify} className="speaker">      <Row        wrap={true}        className={`speaker-phrase bg-transparent m-0 ${rtlStyle}`}      >        {words.map((result, index) => (          <WordResult            key={`${text}-${index}`}            result={result}            disable={disable}            highlight={highlightSpoken && highlightSpoken.index === index}            onSelectWord={handleOnSelectWord}          />        ))}      </Row>      <ButtonIcon        data-testid="speaker"        icon="volume-up"        type="light"        size="4"        styles="mx-md-2"        disabled={disable}        onClick={handleOnSpeak}      />    </Column>  )}
复制代码


整合:使用 useSpeaker()这个自定义 Hook 来重构


虽然这个组件并不大,但它也能改得更有条理,更整洁一些。


Speaker 的行为和功能代码部分可以重用,并整合到它自己的可操作单元中。请注意,“speak()”函数在两种上下文中使用了两次——也许这里可以 DRY 它一下,换一种方法。


我们可以创建一个新的可重用挂钩——useSpeaker()。我们需要让这个 hook 接收当前念到的单词(一个状态)和 speak()功能。


然后我们才能抽象出整个行为的代码,并在 Speaker 的代码中使用这个好用的小代码段:


const { spokenWord, say } = useSpeaker({  text,  speed,  onEnd: onSpeakComplete,})
复制代码


useSpeaker()包括从 Speaker 组件中提取的代码。


import React from 'react';import { speak } from '../utils/speaker.util';type TextWord = {  word: string;  index: number;};export default function useSpeaker({ text, speed, onEnd }) {  const [spokenWord, setSpokenWord] = React.useState<TextWord>();  const say = React.useCallback(() => {    speak({      phrase: text,      speed,      onEndCallback: () => {        onEnd && onEnd(text);        setSpokenWord(null);      },      onSpeaking: setSpokenWord      sanitize: false,    });  }, [text, speed, onEnd]);  return { spokenWord, say };}
复制代码


现在有两个“speak()”函数调用。可以在 WordResult 组件内部重用新的 useSpeaker() hook。


我们需要在 WordResult 中更改的是——传递 speed 属性,而不是传递 onSelectWord()的函数 handler。使用 speed 和 result(包含“word”的对象)后,可以在 WordResult 内部重用 useSpeaker 的功能。


{  words.map((result, index) => (    <WordResult      key={`${text}-${index}`}      result={result}      disable={disable}      highlight={spokenWord && spokenWord.index === index}      speed={speed}    />  ))}
复制代码



使用上面的自定义 hook——useSpeaker()后,我们成功将 20 行代码缩减为可重用的 5 行代码。最重要的是,这段代码现在有更多的语义含义,并且目标非常明确。


代码如何发声


除了让代码实现"讲话"的功能外,useSpeaker()代码重构也体现了它的字面意思——只要使用正确的术语,代码就可能在你的脑海中发出声音。


我认为,编写好函数式代码后很快就应该考虑对其迭代。在阅读代码并尝试理解它时,你的脑海中可能会出现很多问题:


  • 为什么这段代码在这里?

  • 它有什么作用?

  • 用在哪里?

  • 它试图完成什么?


对于这些问题,我通常会添加一些目标,希望能带来更好的结果:


  • 哪些代码可以不要?

  • 可以将这里的代码合并为一个简短的函数名称吗?

  • 代码的哪些部分紧密耦合在一起,进而可以组合成一个“黑匣子”?

  • 怎样让代码像诗歌/书籍/日常对话那样讲一个故事?

  • 我可以让代码讲出自己的故事吗?


请查看我们的革命性应用ReadM™,这款程序能通过实时反馈树立儿童阅读和讲出英语的信心(更多语种正在开发中)。


我会基于ReadM™的开发经验,撰写更多有用的文章。


作者介绍


Oren Farhi是前端工程师和 JS 顾问。他的作品包括ReadM™Echoes Playerngx-infinite-scroll等。他撰写了《Angular和NgRx的响应式编程》一书。这里是他的开源项目列表


延伸阅读


https://orizens.com/blog/how-to-functional-programming-with-custom-react-hooks/


2020 年 6 月 15 日 17:121215

评论

发布
暂无评论
发现更多内容

面试官再问你Http请求过程,怼回去!

架构师修行之路

HTTP TCP/IP

JAVA,.NET项目开发难上手?Learun敏捷开发框架解君愁

Learun

第 0 期架构师训练营第 4 周作业 1

傅晶

区块链钱包应用开发,数字货币钱包源码

13530558032

《搞定1》读书笔记

超超不会飞

合约跟单交易系统开发,交易所一键跟单模式搭建

13530558032

OPPO互联网DevSecOps实践

OPPO安全

DevOps 安全

第 0 期架构师训练营第 3 周作业2---总结

傅晶

温故知新——Spring AOP

牛初九

spring aop ioc

解决数据指数级增长挑战,英特尔如何又快又好提供领导力产品?

飞天鱼2017

迎接物联网时代,区块链大有可为

CECBC区块链专委会

云计算 大数据 区块链技术

第 0 期架构师训练营第 4周作业 2--- 总结

傅晶

架构重构之禅

ninetyhe

Java 架构设计 代码重构

XSKY星辰天合助力中国五矿打造政企办公新标杆

XSKY融合存储

易观方舟Argo+CRM | 让企业数据发挥更大价值

易观大数据

Apache Pulsar 2.6.1 版本正式发布:2.6.0 功能增强版,新增 OAuth2 支持

Apache Pulsar

消息队列 Apache Pulsar 消息系统 消息中间件

月度工作汇报,为什么要全球直播?

赵新龙

TGO鲲鹏会 技术社区 开源社区

司法视频行为分析引擎

华宇法律科技

永续合约交易系统开发方案,合约交易所源码搭建

13530558032

爱技术爱折腾,想要编程到60岁--我的十年

盛安德软件

科普小知识:区块链与分布式系统

CECBC区块链专委会

区块链 分布式

Woman、man、camera、TV:如何做一个完整的深度学习应用

LeanCloud

学习 程序员 互联网 云容器引擎 LeanCloud

第 0 期架构师训练营第3周作业1

傅晶

组合模式

LeetCode题解:20. 有效的括号,栈,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

为什么中国出了这么多厉害的互联网公司,但没有自己设计过编程语言?

代码制造者

编程语言 低代码 企业信息化 零代码 编程开发

区块链是一个有去无返的奇幻旅程

CECBC区块链专委会

区块链

甲方日常2

句子

工作 随笔杂谈 日常

区块链承兑支付系统开发,USDT入金支付系统

13530558032

背景自由替换,远程办公好助手

华宇法律科技

前端智能化的加速时刻:华为机器视觉的创新方程式

脑极体

聊聊微服务

炜娓道来程序人生

架构 微服务 SOA

让你的代码讲出它的故事-InfoQ