34

让你的代码讲出它的故事

 3 years ago
source link: https://www.infoq.cn/article/ZmcUNS7ezTwKjL0wUCLI
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

代码抽象

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

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

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

Iv6bayu.png!web

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

YrE3Ubz.gif

React Speaker 组件布局

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

Speaker 的组件定义

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

  • text :Speaker 所显示并“讲出来”的句子(或单词)
  • onSpeakComplete :念完之后,Speaker 将调用这个回调
  • disable :禁用单击一个单词听它的发音的功能
  • verifiedtext 中的一组单词,它们在当前会话期间已被念对
  • highlight :在 text 中之前已经念对的单词的布尔值数组
  • speed :一个数字,指示句子的读速

复制代码

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

beieai6.png!web

Speaker 的行为和功能

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

复制代码

const[highlightSpoken, setHighlightSpoken] = useState<{
word:string
index:number
}>()
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 (
<Columnmd="row"alignItems="center"justify={justify}className="speaker">
<Row
wrap={true}
className={`speaker-phrasebg-transparentm-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 Reactfrom'react';
import { speak }from'../utils/speaker.util';
type TextWord = {
word: string;
index: number;
};
exportdefaultfunction 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}
/>
))
}

VvYraeN.png!web

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK