告别秃头!设计让开发者省心的React组件(内含详细代码)

223 阅读8分钟

全文共7556字,预计学习时长20分钟

来源:Pexels


用“你秃了,你也强了”这句话来形容程序员再合适不过了。

但是,为了变强就必须付出“秃头”这种巨大代价吗?谁不渴望成为一个人见人爱又技术逆天的美男(女)子?


这可真是所有程序员开发者的悲哀……

来源:Pexels


为此,小芯本期特别整理了设计防止开发者头秃的React组件的一些操作,希望能为工作在一线,无私奉献自己宝贵头发的程序员小哥哥小姐姐们带来一丝温暖。


好吧,我们开始吧~


设计可重复使用的React组件绝对是一项挑战,之所以这么说,是因为大家平常是以团队形式工作的,而且组件的使用者不光有设计人员,还有其他开发人员。


再者,如果不重视出现错误的组件,但凡以后出现问题就会吃更大的亏。(小芯在这里用设计一词,是觉得设计就像不断深入思考,再进行实际操作,编写代码的过程一样。)


理想状态是先花时间设计再动手操作。


下面将讨论在设计可维护且适合于开发者的React组件时遇到的一些常见操作和需要遵循的原则。


Context-Free组件


设计可重复使用的组件,需要确保组件尽可能地做到与场景无关。


就像用Button而不是ReportPageConfirmationButton,用Card而非UserCard,用Avatar代替ProfileAvatar,等等。这样一来,就可以扩展项目中组件的使用,同时还会避免在不同场景中使用组件出现混淆的问题。


比如,从功能和UI角度来看,在SettingPage内使用ReportPageConfirmationButton可能是正确的,但语义方面就会有误。


另一个优点是降低与特定用例紧密相连的操作的抽象性。可以看看下方的Button组件:


functionButton ({ children }) {
return <buttondata-testid="login-button">{children}<button/>
}


该Button组件适用于任何场景,即使data-testid只在登录时起作用,UI也是正确的。


但出于某些原因,该组件与登录按钮的执行是绑定的。所以,将data-testid property提取出来作为prop。


functionButton ({ children, dataTestId }) {
return <button data-testid={dataTestId}>{children}<button/>
}// Usage
<Button dataTestId="login-button">Login</Button>


简单且符合语义的组件prop


使用JSX的默认值和JavaScript的强制转换


考虑下面的Button组件:


importReact from 'react';
import Loader from '../Loader';function Button ({ loading }) {
return <button>{ loading ?<Loader /> : 'Click me!'}<button/>
}

提供的prop没有值时,会默认为true。这两个JSX表达是一样的。



<Buttonloading={true} /><Button loading />


不仅如此,也可以利用JavaScript强制转换的属性。这两个JSX表达也是一样的。


<Buttonloading={true} /><Button loading />


这些看起来不起眼,但如果React项目中有类型检查功能,那它们就会起作用。


typeTButtonProps = {
loading?: Boolean
}// So if you do this the type checking won't complain
<Button />


同样,大家想一下为何小芯在这里会用loading而不是isLoading?每次都提供值的话,isLoadingprop就是有用的。



<ButtonisLoading={true} /><Button isLoading={false} />


但事实并非如此。我们用了JSX的默认值和JavaScript的强制转换,所以结尾将会是:



<ButtonisLoading/>


这看起来可能难以理解,因为并没有讲清楚按钮是否会显示加载器。


//This is much better
<Button loading />


考虑组件用例

来源:Pexels


小芯发现一个有用的思路就是依据组件用例来设计组件prop。


比如跟团队中的设计者讨论,决定需要显示加载状态的Button键。


一般情况下,有两个选择。执行loadingprop或notLoadingprop项。


//WITH LOADING PROPS// Button in normal state
<Button />// Button in loading state
<Button loading/>// WITH NOTLOADING PROPS// Button in normal state
<Button notLoading={false} />// Button in loading state
<Button />


很明显,大多数Button用例不用于显示加载状态。这种情况最好用loadingprop取代notLoading。


否则最后代码库中就会遇到很多这样的执行:



<ButtonnotLoading={false} />


为了练习思考,大家可以想一下如果Button组件会产生相反执行呢?


在跟设计者沟通后,弄清楚需要另一个按钮,而多数情况下,这个按钮会显示加载状态。我们可以把该组件称为LoadingButton。


多数情况下,有了相同的API作为Button组件,需要执行:



<LoadingButtonloading />


因为已经明确了大多数用例会显示加载状态,因此可以改变执行,以便显示具有Loading状态的LoadingButton:

//Displaying the loader
<LoadingButton />// Not displaying the loader
<LoadingButton notLoading />


Style Prop命名与CSS属性命名相同


有时会给组件增加功能完善其CSS属性,倾向于使用与CSS属性名称相同的prop名称。


理由就是尽可能得简单易懂,毕竟作为前端设计者会经常用到CSS。


就好比想改进Button组件,使其能通过十六进制的方式来接收prop,修饰其自身颜色和文本颜色。



<Buttoncolor=”#000” textColor=”#fff” />

可以更新组件prop名称,用这个来替代:



<ButtonbackgroundColor=”#000” color=”#fff” />
EventHandler Prop


给按钮增添点击功能。

functiononClickHandler = () => console.log('Clicked!')<ButtononClickButton={onClickHandler}/>
// vs
<Button onClick={onClickHandler}/>

在这种情况下,最好用onClickprop而不是onClickButton,给eventHandler prop命名时尽可能与事件名保持一致,因为有时可能需要将事件与特定场景绑定在一起或者会遇到多种相似事件。


可以想一下有取消和确认按钮的modal组件


执行onCancel和onConfirm,或onClickCancelButton和 onClickConfirmButton。


此时,小芯会倾向于onCancel和onConfirm,因为onClick部分依附于Button组件,长度较短,而且从使用者的角度来看,不会丢失取消或确认功能。


//Modal Component
function Modal ({ children, onCancel, onConfirm }) {
return (
<div>
{children}
<ButtononClick={onCancel}>Cancel</Button>
<ButtononClick={onConfirm}>Confirm</Button>
</div>
)
}// Usage
<Modal
onCancel={() =>console.log('Cancelled!')}
onConfirm={() =>console.log('Confirmed!')}
>
Are you sure you want to delete thisitem?
</Modal>

尽可能多地抽象化

来源:Pexels


例如,执行MultiSelect组件,该组件本质上是由一列可搜索的复选框选项构成的。只会显示出与搜索相关的选项。


这里有两个选项,选项A是在组件内部执行搜索功能,选项B是将搜索转移功能转移到父组件上。


为了简洁明了,可以假设已经执行了Option组件。


//Option A
function MultiSelect ({ options, onSelectOption, searchInputValue, onSearch }){
return (
<div>
<input value={searchInputValue}onChange={onSearch} placeholder="Search here" />
<ul>
{
options.map((option) =><Option value={option.value} label={option.label} onClick={onSelectOption}/>)}
</ul>
</div>
)
}// Option A Usage
function ParentComponent () {
const options = ['Cat', 'Dog', 'Mouse','Elephant'] const [ searchInputValue, setSearchInputValue ] = useState('');
const onSearchHandler = ({ target })=> {
setSearchInputValue(target.value)
} const [ selectedOptions,setSelectedOptions ] = useState([])
// Implement the filter
const filteredOptions =options.filter((option) => option.includes(searchInputValue)) return (
<MultiSelect
options={filteredOptions}
searchInputValue={searchInputValue}
onSearch={setSearchInputValue}
onSelectOptions={setSelectedOptions}
>
)
}

同时考虑选项B的执行:


//Option B
function MultiSelect ({ options, onSelectOption }) {
const [ search, setSearch ] =useState('');
onChangeSearchHandler = ({ target })=> {
setSearch(target.value);
}const filteredOptions =options.filter((option) => option.includes(search))return (
<div>
<input value={searchInputValue}onChange={onChangeSearchHandler} placeholder="Search here" />
<ul>
{
filteredOptions.map((option) =><Option value={option.value} label={option.label} onClick={onSelectOption}/>)}
</ul>
</div>
)
}// Option B Usage
function ParentComponent () {
const options = ['Cat', 'Dog', 'Mouse','Elephant']const [ selectedOptions, setSelectedOptions ] = useState([])return (
<MultiSelect
options={options}
searchInputValue={searchInputValue}
onSearch={setSearchInputValue}
onSelectOptions={setSelectedOptions}
>
)
}

ParentComponent处的执行会减少。


现在假定会在多个环节使用该组件,同时也会进行搜索。ParentComponent不需要接收任何有关搜索操作的信息。


抽象但可自定义的组件


将执行抽象化以及可自定义听起来是相对的,但确实可以同时存在。


通过modifier函数修饰组件的内部功能


对用户隐藏执行步骤与可自定义听起来是相对的。


回到MultiSelect组件,添加另一个功能,再通过自定义算法进行搜索。


functionMultiSelect ({ options, onSelectOption, searchModifier }) {
const [ search, setSearch ] =useState('');
onChangeSearchHandler = ({ target })=> {
setSearch(target.value);
}const filteredOptions =options.filter((option) => searchModifier instanceOf Function ?searchModifier(option, search) : option.includes(search))return (
<div>
<input value={searchInputValue}onChange={onChangeSearchHandler} placeholder="Search here" />
<ul>
{
filteredOptions.map((option) =><Option value={option.value} label={option.label} onClick={onSelectOption}/>)}
</ul>
</div>
)
}

自定义renderer函数的render prop模式


为了让用户完全掌控以何种方式显示组件,可以通过render prop模式。考虑一下这个操作:


functionMultiSelect ({ options, onSelectOption, searchModifier, children }) {
const [ search, setSearch ] =useState('');
onChangeSearchHandler = ({ target })=> {
setSearch(target.value);
} const filteredOptions =options.filter((option) => searchModifier instanceOf Function ?searchModifier(option, search) : option.includes(search)) if (children instanceOf Function) {
return children(filteredOptions,search, onChangeSearchHandler)
}return (
<div>
<input value={searchInputValue}onChange={onChangeSearchHandler} placeholder="Search here" />
<ul>
{
filteredOptions.map((option) =><Option value={option.value} label={option.label} onClick={onSelectOption}/>)}
</ul>
</div>
)
}// Usage
function ParentComponent() {
return (
<MultiSelect>
{
(options, searchValue,onChangeSearchHandler) => {
return
<div>
<input value={searchValue}onChange={onChangeSearchHandler}/>
<span>Here are the optionlist!</span>
{
options.map(option =><span>{option}</span)
}
</div> }
</MultiSelect> )
}

假如目标是更加精确,就可以对MultiSelect组件的指定部分执行特定的render。举个例子,可以通过searchInputRenderer,这样一来用户就能控制搜索输入的内容。


functionMultiSelect ({ options, onSelectOption, searchModifier, children,searchInputRenderer }) {
...return (
<div>
{
searchInputRenderer instanceOfFunction ?
searchInputRenderer(searchInputValue,onChangeSearchHandler):
<input value={searchInputValue}onChange={setSearch} placeholder="Search here" />
}
<ul>
{
filteredOptions.map((option) =><Option value={option.value} label={option.label} onClick={onSelectOption}/>)}
</ul>
</div>
)}

看了这么多,不知道大家学到没有~

留言 点赞 关注

我们一起分享AI学习与发展的干货
欢迎关注全平台AI垂类自媒体 “读芯术”


(添加小编微信:dxsxbb,加入读者圈,一起讨论最新鲜的人工智能科技哦~)