让你的代码讲出它的故事

现在用 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 :一个数字,指示句子的读速

复制代码

functionSpeaker({
text,
onSpeakComplete,
disable,
verified = [],
highlight = [],
speed,
}: SpeakerProps){
// code
}

Speaker 的行为和功能

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

复制代码

const[highlightSpoken, setHighlightSpoken] = useState()
consthandleOnSpeak = useCallback(()=>{
speak({
phrase: text,
speed,
onEndCallback:()=>{
onSpeakComplete && onSpeakComplete(text)
setHighlightSpoken(null)
},
onSpeaking: setHighlightSpoken,
sanitize:false,
})
}, [text, onSpeakComplete, setHighlightSpoken, speed])
consthandleOnSelectWord =(phrase:string) =>{
speak({ phrase, speed, onEndCallback: noop })
}

Speaker 的显示:渲染

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

复制代码

constwords= verified.length? verified : createVerifiedWords(text, highlight)
const rtlStyle = resolveLanguage(text).style
const justify = rtlStyle.length?"end":"between"

返回的渲染值是:

复制代码

function Speaker(props) {
// all the above code commented
return (


{words.map((result, index) => (

))}



)
}

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

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

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

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

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

复制代码

const { spokenWord,say} = useSpeaker({
text,
speed,
onEnd: onSpeakComplete,
})

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

复制代码

import Reactfrom'react';
import { speak }from'../utils/speaker.util';
type TextWord = {
word: string;
index: number;
};
exportdefaultfunction useSpeaker({ text, speed, onEnd }) {
const [spokenWord, setSpokenWord] = React.useState();
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) => (

))
}

使用上面的自定义 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/