如何设计 React 代码结构?
怎样设计一个项目的文件和组件的结构、甚至是某个组件的内部结构?这个问题永远没有正确答案。
以下为译文:
像许多话题一样,许多人都持有不同的观点,而且解决方案也有很多。
可能你自己的意见会受到个人经历的影响,比如你对于何谓易读、何谓易解析,甚至何谓漂亮都有自己的看法。当你加入一个团队时,通常你们都需要在这些结构问题上达成一致,这并不是一件易事。
这类的讨论就好像两个人争论哪种颜色最好看,是蓝色还是红色。他们都会主张自己的意见,阐明自己的立场,然后固执己见。他们永远也不会折衷,比如认为紫色最好看。
因此,我并不期望能够在这个问题上达成任何共识。在本文中,我只会介绍在设计React代码结构方面的个人喜好,以及其中的原因。希望你能从中借鉴一二,或者至少可以从不同的角度理解这个问题。
拼图
首先,我想简单地介绍一下我是哪种程序员。在写代码时,我会需要构建的东西看成基本的组成部分,就像拼图一样。我编写的每个功能、每个函数、每个组件都是一副拼图中的一部分。
我很喜欢将我做的东西可视化。我喜欢用画图的方式解释问题时,我希望能够将用户真正使用的UI功能反映到代码中。
对于我而言,还有一件重要的事情,那就是能迅速看到整体样貌,并尽可能简单地理解组件的功能。这两个目的都可以通过选择正确的命名方式以及可视化结构来实现。
圣诞老人数字化
在这篇文章中我假想了一个项目,名叫圣诞老人的数字愿望单。所有人都可以使用这个项目,创建用户,添加愿望,还可以查看个人信息。
下面是组件 EditMyInformationToSanta 的代码,显然它需要重构。我们可以通过这个组件添加你希望获得的圣诞礼物。
import React, { useState } from 'react';
import { saveMyInformationToSanta } from '../../api/santa-api';
const EditMyInformationToSanta = () => {
const [name, setName] = useState('');
const [age, setAge] = useState('');
const [gender, setGender] = useState(null);
const [address, setAddress] = useState('');
const [hasFireplace, setHasFireplace] = useState(null);
const [naughtyOrNice, setNaughtyOrNice] = useState(null);
const [letterToSanta, setLetterToSanta] = useState('');
const [wish, setWish] = useState('');
const [wishList, setWishList] = useState([]);
const submitMyInformationToSanta = async event => {
event.preventDefault();
await saveMyInformationToSanta({
name,
age,
gender,
address,
hasFireplace,
naughtyOrNice,
letterToSanta,
wishList,
});
};
return (
<div>
<h1>Hi, Santa! This is me</h1>
<form>
<h2>About me:</h2>
<label>
<span>My name is:</span>
<input
type="text"
value={name}
placeholder="Write your name"
onChange={event => setName(event.target.value)}
/>
</label>
<label>
<span>My age is:</span>
<input
type="text"
value={age}
placeholder="Tell Santa your age"
onChange={event => setAge(event.target.value)}
/>
</label>
<fieldset>
<legend>I am a...</legend>
<label>
<input
type="radio"
value="boy"
checked={gender === 'boy'}
onChange={event => setGender(event.target.value)}
/>
Boy
</label>
<label>
<input
type="radio"
value="girl"
checked={gender === 'girl'}
onChange={event => setGender(event.target.value)}
/>
Girl
</label>
</fieldset>
<label>
<span>My address is:</span>
<input
type="text"
value={address}
placeholder="Where do you live?"
onChange={event => setAddress(event.target.value)}
/>
</label>
<fieldset>
<legend>I have a fireplace?</legend>
<label>
<input
type="radio"
value={true}
checked={hasFireplace}
onChange={event => setHasFireplace(event.target.value)}
/>
Yes
</label>
<label>
<input
type="radio"
value={false}
checked={hasFireplace === false}
onChange={event => setHasFireplace(event.target.value)}
/>
No
</label>
</fieldset>
<fieldset>
<legend>This year I have been naughty or nice?</legend>
<label>
<input
type="radio"
value="naughty"
checked={naughtyOrNice === 'naughty'}
onChange={event => setNaughtyOrNice(event.target.value)}
/>
Naughty
</label>
<label>
<input
type="radio"
value="nice"
checked={naughtyOrNice === 'nice'}
onChange={event => setNaughtyOrNice(event.target.value)}
/>
Nice
</label>
</fieldset>
<div>
<h2>My wishes this year:</h2>
<label>
<span>I want:</span>
<input
type="text"
value={wish}
placeholder="Write a wish"
onChange={event => setWish(event.target.value)}
/>
</label>
<button
type="button"
value="Add wish"
onClick={() => {
setWishList(wishList.concat(wish));
setWish('');
}}
/>
<h3>My wish list:</h3>
<ul>
{wishList.map(wish => (
<li>{wish}</li>
))}
</ul>
</div>
<div>
<h2>Santa, I also want to tell you...</h2>
<textarea
placeholder="Do you want to say something to Santa?"
onChange={event => setLetterToSanta(event.target.value)}
value={letterToSanta}
/>
</div>
<button type="submit" onClick={submitMyInformationToSanta} />
</form>
</div>
);
};
export default EditMyInformationToSanta;
有几个问题很明显,比如重复的输入框和单选按钮的布局,所以我们可以将此作为切入点。如下便是重构后的代码:
import React, { useState } from 'react';
import { saveMyInformationToSanta } from '../../api/santa-api';
import TextInputWithLabel from './TextInputWithLabel';
import RadioToggle from './RadioToggle';
const EditMyInformationToSanta = () => {
const [name, setName] = useState('');
const [age, setAge] = useState('');
const [gender, setGender] = useState(null);
const [address, setAddress] = useState('');
const [hasFireplace, setHasFireplace] = useState(null);
const [naughtyOrNice, setNaughtyOrNice] = useState(null);
const [letterToSanta, setLetterToSanta] = useState('');
const [wish, setWish] = useState('');
const [wishList, setWishList] = useState([]);
const submitMyInformationToSanta = async event => {
event.preventDefault();
await saveMyInformationToSanta({
name,
age,
gender,
address,
hasFireplace,
naughtyOrNice,
letterToSanta,
wishList,
});
};
return (
<div>
<h1>Hi, Santa! This is me</h1>
<form>
<h2>About me</h2>
<TextInputWithLabel
label="My name is:"
placeholder="Write your name"
value={name}
onChange={event => setName(event.target.value)}
/>
<TextInputWithLabel
label="My age is:"
placeholder="Tell Santa your age"
value={age}
onChange={event => setAge(event.target.value)}
/>
<RadioToggle
question="I am a..."
label1="Boy"
toggleValue1="boy"
label2="Girl"
toggleValue2="girl"
value={gender}
onChange={event => setGender(event.target.value)}
/>
<TextInputWithLabel
label="My address is:"
placeholder="Where do you live?"
value={address}
onChange={event => setAddress(event.target.value)}
/>
<RadioToggle
question="I have a fireplace?"
label1="Yes"
toggleValue1={true}
label2="No"
toggleValue2={false}
value={hasFireplace}
onChange={event => setHasFireplace(event.target.value)}
/>
<RadioToggle
question="This year I have been naughty or nice?"
label1="Naughty"
toggleValue1="naughty"
label2="Nice"
toggleValue2="nice"
value={naughtyOrNice}
onChange={event => setNaughtyOrNice(event.target.value)}
/>
<div>
<h2>My wishes this year:</h2>
<TextInputWithLabel
label="I want:"
placeholder="Write a wish"
value={wish}
onChange={event => setWish(event.target.value)}
/>
<button
type="button"
value="Add wish"
onClick={() => {
setWishList(wishList.concat(wish));
setWish('');
}}
/>
<h3>My wish list:</h3>
<ul>
{wishList.map(wish => (
<li>{wish}</li>
))}
</ul>
</div>
<div>
<h2>Santa, I also want to tell you...</h2>
<textarea
placeholder="Do you want to say something to Santa?"
onChange={event => setLetterToSanta(event.target.value)}
value={letterToSanta}
/>
</div>
<button type="submit" onClick={submitMyInformationToSanta} />
</form>
</div>
);
};
export default EditMyInformationToSanta;
保持组件分离!
许多开发人员喜欢将类似上例中的局部组件放到一个文件中,但我倾向于一个文件最多只保存一个组件。对于我来说,在理解代码全貌的时候,一个文件只包含一个组件的形式比一个文件中包含多个组件更容易。
在本例中,差别也许不是太大,由于我要重构的组件并不是太大,但组件规模增大后问题就会浮现。几个星期后你就不得不在文件中来回上下滚动才能找到“根”组件,这会非常麻烦。
我喜欢简单的规则,这样就可以避免重构时的内部讨论,也不需要讨论什么时候应该将组件放到单独的文件中。
按照域提取
回到我们的例子。尽管我们将重复的代码提取到了可重用的组件中,我认为依然还有可以重构的地方。我想按照代码的功能,重构同一功能的代码。在下面的例子中,我把表单每一部分的组件都提取出来,分别是AboutMe、LetterToSanta 和 MyWishes。
import React, { useState } from 'react';
import { saveMyInformationToSanta } from '../../api/santa-api';
import AboutMe from './AboutMe';
import MyWishes from './MyWishes';
import LetterToSanta from './LetterToSanta';
const EditMyInformationToSanta = () => {
const [me, setMeState] = useState({
name: '',
age: '',
address: '',
gender: null,
hasFireplace: null,
naughtyOrNice: null,
});
const [letterToSanta, setLetterToSanta] = useState('');
const [wish, setWish] = useState('');
const [wishList, setWishList] = useState([]);
const submitMyInformationToSanta = async event => {
event.preventDefault();
await saveMyInformationToSanta({
...me,
letterToSanta,
wishList,
});
};
return (
<div>
<h1>Hi, Santa! This is me</h1>
<form>
<AboutMe me={me} onMeChange={updatedMeState => setMeState(updatedMeState)} />
<MyWishes wish={wish} wishList={wishList} onWishChange={setWish} onWishListChange={setWishList} />
<LetterToSanta letterToSanta={letterToSanta} onLetterChange={setLetterToSanta} />
<button type="submit" onClick={submitMyInformationToSanta} />
</form>
</div>
);
};
export default EditMyInformationToSanta;
现在,这个目录中除了文件 EditMyInformationToSanta.jsx 之外,还有一堆简单的组件文件。每个文件都很小,很容易单独理解。
表单每个部分对应的组件都仅限于 EditMyInformationToSanta.jsx 文件使用,因此我将它们放到了同一个目录下。重构的目的是为了让主文件看起来干净、容易理解。我们也可以将其他相关的文件放在这个目录中,比如样式、图像、文本、工具函数或其他资源等。
域 vs 组件
应用程序会不断增长,例如利用 SantaLocation API 来实时跟踪圣诞老人的位置,或者显示前十个最期待的礼物,总有一天你会遇到重用表现层组件的情况。我喜欢将代码分成两个目录:/components 和 /domain,目的是将表现层组件分离开来。
/domain 包含应用程序的域逻辑。EditMyInformationToSanta 以及相关的文件就在这个目录下。
/components 包含整个项目中所有可重用的组件。典型的例子就是 TextInputWithLabel,我们将它从 /domain/edit-my-information-to-santa 目录移动到 /components/text-input-with-label。与 /domain 目录一样,这个目录也可以包含相关文件,如文本、样式等。
保持整洁
我们刚才一直在讨论怎样设计React代码的布局。总结起来就是:
我喜欢将表现层逻辑提取到单独的组件中,这样利于重用,也利于简化根组件。
我将每个组件放到各自的文件中,即使用户界面变得十分复杂,也应该遵循这一条原则。原因在于,每个文件仅包含一个组件更加方便理解。每个文件都很短,因此可以看到整体情况,而好的组件命名(以及文件命名)可以帮助我们理解每个组件的作用。
将组件分割成一次性的域逻辑组件和可重用的组件,可以让浏览代码和删除代码更加容易。
作为一名咨询师,你需要花大量时间去阅读别人的代码。确保阅读代码的过程顺利非常重要。大多数时间我都在修改或扩展已有代码,而简单、健壮、易于理解的代码结构可以帮你更快地完成工作。
接受他人的意见
最后我想用本文开头的观点来结束本文。团队中不同的人有不同的选择和喜好,接受这一点并不容易。但一定要接受吗?我想说,是的。并不是说你要接受关于什么是美观,而是要接受在项目中使用某种代码风格和结构,即使有些人并不完全同意。
让整个团队都在“整洁”或“易阅”问题上达成一致是不现实的。最重要的是,每个人都会表达自己的意见,每个人都会强调为何使用这种方式,并积极讨论团队应该使用这种方式。团队中的大多数人(即使不是全部)都会发表观点,但我认为,最好能够接受统一的代码风格和结构,而不是为了满足每个人的喜好而混合多种风格。
不管最后的选择是什么,接受统一的解决方案,才能在项目中创建整洁、干净、易于理解且标准化的代码风格。
原文:https://react.christmas/2019/22
本文为 CSDN 翻译,转载请注明来源出处。
热 文 推 荐
☞