写点什么

React 的 10 种迷你开发模式

  • 2019-09-20
  • 本文字数:8800 字

    阅读完需:约 29 分钟

React 的 10 种迷你开发模式

过去几年里,我参与了几个较大的 React 项目,也做了非常多的小项目,在这过程中,我总结了一些 React 常用的开发模式。这些模式是我在 React 入门阶段非常希望看到的,如果你是 React 新手,那你今天算是赚到了,如果你已经是 React 老鸟,不妨看看有哪些模式。本文较长,如果觉得一些介绍比较枯燥(比如 3、6、8、10),可以选择跳过。

1.数据传输

我建议每个 React 初学者都去了解 React 组件向下传递数据(对象、字符串等)的模式,以及传递让子组件传回数据的方法的模式。可以联想救援队将食物与对讲机送抵井底被困矿工。举个例子:



图中左右分别是父组件和子组件,你可以想象连接父子组件的这两个属性允许了数据的双向流通。其实这一条不算真正意义上的开发模式,下面才是。

2.修复 HTML 原生 input

React 和 web 组件的一大好处是,当页面出现 bug,你可以很快定位到问题所在。如果你在考虑页面中使用不同的输入标签,你会发现这些标签的命名大多是无意义,因此,如果我在处理一个包含多个输入的页面时,我首先会处理这个问题



  • 输入应该通过 onChange 方法返回值,而不是通过 Javascript 事件绑定

  • 保证 onChange 返回值的类型与输入的值类型统一,如果 typeofprops.value 是一个 number 类型,应该将 e.target.value 转换成 number 类型再返回

  • 一套 radio 标签和一个 select 标签在功能上都是相同的,唯一的区别只是 UI 的不同,推荐项目中保留一个 组件,可以通过属性 ui=“radio” 或者 ui=“dropDown” 进行控制


以上是我处理原生输入标签时用到的方法,你可以选择其他方式,关键是将这些原生标签转换成为你所用,再也不需要忍受那些糟糕的原生输入标签了。

3.给 input 绑定唯一 ID 的标签

关于 input 输入,如果你注重用户体验,你应该给每个标签绑定一个,通过 id/for 属性进行关联。但如果给每一个 input 都想一个独一无二且生动形象的 ID,那样太浪费时间,而且使用随机生成 ID 的方式也不可行,客户端与服务器端生成的 ID 不同,导致校验不通过,这里推荐你创建一个提供增量 ID 的模块,并在 input 组件中使用,如下所示:


class Input extends React.Component {
constructor(props) {
super(props);
this.id = getNextId();
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.props.onChange(e.target.value);
}
render() {
return (
<label htmlFor={this.id}>
{this.props.label}
<input
id={this.id}
value={this.props.value}
onChange={this.onChange}
/>
</label>
);
}
}
复制代码


虽然这里解决 ID 的问题,但是这个方案有漏洞,getNextId()方法每被调用一次,数字会增加,如果是在服务端渲染,这个数字会持续增加到,因此应该在每次渲染之前进行一次重置(每一次网络请求)。因此,一个完整的获取 ID 模块应该是这样:


let count = 1;
export const resetId = () => {
count = 1;
}
export const getNextId = () => {
return element-id-${count++};
}
复制代码

4.通过 props 控制 CSS

当你想在不同的实例中应用不同的 CSS 样式,你可以通过传入不同的 props 值来控制需要应用的样式。表面上看,这样的操作似乎很简单,但实际应用中往往会出现很多错误。我总结共有三种不同的方式来控制组件的样式:


使用主题


借鉴主题的思路,将一系列的 CSS 声明组合在一起,统一成一个主题,在组件中生命组件的主题,例如 primary 按钮以及 secondary 按钮:Hello 一个组件中尽量使用一个主题。


使用标记


也许你的页面中会有一些圆角 button,但这样的风格不符合你已经定义的主题,遇到这种情况,你可能要去找 UI 商量一个统一的方案,或是在元素中添加一个布尔属性,像这样等:<Buttontheme="secondary"rounded>Hello 等同于这种写法:<Button theme=“secondary” rounded{true}>Hello


设置值


当然,你肯定会遇到直接在组建中写 CSS 样式的情况,像这样:<Iconwidth=“25” height=“25” type=“search” />


举个例子


设想你现在需要实现一个链接,但现在有三种截然不同的主题,一些链接有下划线,一些没有,就像这样:



下面给出我的处理方式:


// demo.jsxconst Link = (props) => {
let className = link link--${props.theme}-theme;
if (!props.underline) className += ' link--no-underline';
return <a href={props.href} className={className}>{props.children}</a>;
};
Link.propTypes = {
theme: PropTypes.oneOf([
'default', // primary color, no underline
'blend', // inherit surrounding styles'primary-button', // primary color, solid block
]),
underline: PropTypes.bool,
href: PropTypes.string.isRequired,
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.array,
PropTypes.string,
]).isRequired,
};
Link.defaultProps = {
theme: 'default',
underline: false,
};
复制代码


CSS 代码如下:


// demo.css
.link--default-theme,
.link--blend-theme:hover {
color: #D84315;
}


.link--blend-theme {
color: inherit;
}
.link--default-theme:hover,
.link--blend-theme:hover {
text-decoration: underline;
}
.link--primary-button-theme {
display: inline-block;
padding: 12px 25px;
font-size: 18px;
background: #D84315;
color: white;
}
.link--no-underline {
text-decoration: none;
}
复制代码


你可能注意到我在类名(例如 link-no-underline )中使用了 --,源自于我过去一直以少写 CSS 代码为目标,但后来意识到这是错的。如果样式可以更好地应用在布局中,我更喜欢使用一些双重和多选择器规则集。虽然我之前提过,但我还想再强调一下,扩展一个网站最困难的部分是 CSS 的部分,Javascript 部分都很容易,CSS 开始就写的很混乱,后面维护会非常困难,一入布局深似海。实际项目中,一些 web 开发者往往被 CSS 特异性给难倒了,如果你在浏览网页,不妨查看一下页面中的元素(比如导航栏中的提示图标)是如何用 CSS 实现的。如果你不想打开控制台去找,也可以思考这个元素(比如圆圈中包含数字)的实现样式涉及了哪些 CSS 规


则。


译者注:所说的元素包含在原文站点中


共有二十三条规则,还不包括从其他十一条规则下集成的规则,其中 line-height


被复写了九次……即便 line-height 是一只猫,也未能幸免于难。



译者注:猫有九命的梗


使用 React 之后,我们更好地处理页面样式,更周到地决定设计哪些类应用在我们的组件中,将全局设置迁移到 Button.scss 文件中,移除所有对于特异性以及文件顺序的依赖。边注:我梦想有一天,我们再也不需要浏览器对于样式使用的建议。::user-agent-styles: none-whatsoever; 让这样的梦想成为现实。

5.开关组件

开关组件是呈现众多组件之一的组件。可以是用来展示页面的。组件或者是 tab 集合中的 tab,也可以是模态组件中的不同模式。


我过去习惯使用 switch 语句处理,实际传递到我想要渲染的组件,再从组件本身导出对组件的引用(作为命名导出,作为组件的属性)。


现在看来这些都是可怕的方式,我已经解决的一个潜在危险方法是我用一个对象将 prop 值映射到组件中。


import HomePage from './HomePage.jsx';
import AboutPage from './AboutPage.jsx';
import UserPage from './UserPage.jsx';
import FourOhFourPage from './FourOhFourPage.jsx';
const PAGES = {
home: HomePage,
about: AboutPage,
user: UserPage,
};
const Page = (props) => {
const Handler = PAGES[props.page] || FourOhFourPage;
return <Handler {...props} />
};
Page.propTypes = {
page: PropTypes.oneOf(Object.keys(PAGES)).isRequired,
};
复制代码


PAGE 对象中的值可以在 prop 类型中用来捕获开发时错误。当然,我们可以像这样使用 ,如果你将 home、about 缓和 user ,分别提换成/、/about 以及 /user,那么你就有了半个路由啦。(下一步想法:移除 react-router)

6.进入组件内部件

如果你想提高用户体验,不妨试试在页面输入较频繁的输入框添加 autofocus,非常简单,但却可以大大提高用户的使用体验。设想页面中有一个登陆表单,而作为“用户体验高级设计师”的你想在表单的“用户名”输入框中添加一个闪烁的光标,但发现登陆表单显示在模态框中,而 autofocus 属性只能应用在页面加载。


现在你该怎么办 ?


你可能会用 Javascript 实现,给 input 标签一个 id,再用


document.getElementById(‘user-name-input’).focus()让输入框聚焦。这种方法虽然有效,但不够优雅,你的程序中对字符串匹配的依赖应该越少越好。比较幸运的是,有一种非常简单的方法可以实现这个效果:


class SignInModal extends Component {
componentDidMount() {
this.InputComponent.focus();
}
render() {
return (
<div>
<label>User name: </label>
<Input
ref={comp => { this.InputComponent = comp; }}
/>
</div>
)
}
}
复制代码


需要注意的是,当你对组件使用 ref 时,是对组件的引用(而不是底层元素),所以你可以访问其方法。

7.almost component

如设想你正在写一个搜索用户的组件,当你输入的时候,你会看到一列潜在匹配的用户名和头像,就像这样。



当你在设计这个组件时,你可能会犹豫,列表中的每一项都属 SearchSuggestion 组件吗 ?只有几行 HTML 和 CSS 代码,也许不是 ?但我曾经告诉自己:“如果感到疑惑,那就再建一个新组件”。我如果这样做,就一个单独的组件都没了。相反,只有一个给每个入口返回对应 DOM 的 renderSearchSuggestion 方法,我就生成了如下的结果:


const SearchSuggestions = (props) => {
// renderSearchSuggestion() behaves as a pseduo SearchSuggestion component
// keep it self contained and it should be easy to extract later if needed
const renderSearchSuggestion = listItem => (
<li key={listItem.id}>{listItem.name} {listItem.id}</li>
);
return (
<ul>
{props.listItems.map(renderSearchSuggestion)}
</ul>
);
}
复制代码


如果需求变得更复杂或者你想在其他地方使用这个组件,你可以把这段代码复制到新的组件中。


不要过早地组件化,组件不像茶匙,你可以有很多组件。


我的意思不是要你把你觉得应该独立成组件的部分合并到父组件中,而是想让你把那些你认为不应该独立成组件的部分做一些改进,让它看起来和所在的组件更贴合(如果可以的话)。

8.用于格式化文字的组件

当我刚接触 React 时,我觉得组件是一个非常大的东西,一种给 DOM 结构分组的方法,但实际上,组件就像是用于格式化的一种方法。这里有一个 组件,输入一个数字会返回一个漂亮的字符串(加上小数点或者 $ 符)。


constPrice = (props) => {
constprice = props.children.toLocaleString('en', {
style: props.showSymbol ? 'currency' :undefined,
currency: props.showSymbol ? 'USD' :undefined,
maximumFractionDigits: props.showDecimals ? 2: 0,
});
return<span className={props.className}>{price}</span>
};

Price.propTypes= {
className: React.PropTypes.string,
children: React.PropTypes.number,
showDecimals: React.PropTypes.bool,
showSymbol: React.PropTypes.bool,
};

Price.defaultProps= {
children: 0,
showDecimals: true,
showSymbol: true,
};

constPage = () => {
const lambPrice = 1234.567;
const jetPrice = 999999.99;
const bootPrice = 34.567;
return (
<div>
<p>One lamb is <PriceclassName="expensive">{lambPrice}</Price></p>
<p>One jet is <PriceshowDecimals={false}>{jetPrice}</Price></p>
<p>Those gumboots will set ya back<Price showDecimals={false} showSymbol={false}>{bootPrice}</Price>bucks.</p>
</div>
);
};
复制代码


注意:代码中没有对获取的数字进行校验……

9.Store 服务于组件

这行代码我已经写过无数遍了(虽然夸张了点):if (props.user.signInStatus === SIGN_IN_STATUSES.SIGNED_IN)…最近我意识到,我这样做是不是错了,我想知道的是“用户登录了没”,而不是“用户登录的状态是否等于已登录 ?”对于我的组件而言,他们应该有足够的发展,而不该因为了忧虑这些小事叨扰它们,他们不该管得到的 price 参数是否是 Number 类型,也不应该为了一个参数 true 或者 false 烦心。如你所见,如果在 store 中定义的数据符合你的组件要求,你的组件就会简洁很多。如我之前所说,bug 隐藏在复杂逻辑之后,你的组件越简洁,出现 bug 的几率就越低。但开发中肯定会遇到一些复杂的场景,关于如何解决这些问题,我这里有几点经验:


  1. 制定组件的一般结构以及其所需要的数据

  2. 设计满足这些需求的 stroe

  3. 尽量使你传入的数据匹配 stroe 的要求关于最后一点,我建议创建一个单独的模块来完成所有输入数据的格式处理,属性重命名、字符串转数字、对象转数组、Date 字符串转 Date 对象等等。


10.不要通过相对路径引入组件


import Button from ‘…/…/…/…/Button/Button.jsx’;


import Icon from ‘…/…/…/…/Icon/Icon.jsx’;


import Footer from ‘…/…/Footer/Footer.jsx’;


用下面的方式替代上面的引用方式,是不是觉得清爽很多 ? import {Button, Icon, Footer} from ‘Components’;理论上可以这么做


  • 创建一个 index.js 文件来引用你所有的组件

  • 使用 Webpack 的 resolve.alias 来重定向所有组件到 index 文件我目前还没尝试过这种方法,我打算在先有的项目中拿一个出来转换成这样的组织方式(蛤蛤,骗你的,我一直都是这么做的)。但正如我之前写的代码一样,我后来意识到这种方式是错的,原因如下:


1.Webpack 2 中的 resolve.alias 失效了


2.因为组件不在 node_modules 里,所以这算是一个 eslint 错误


3.如果你有一个好的 IDE,那么它会知道项目里的所有组件,如果你忘了加一些属性值,它会温馨地提示你添加,你可以通过 cmd/Ctrl + 点击就可以打开这些组件所在的文件。如果用我之前的方式引用组件,那么 IDE 将找不到我的组件的位置,我就失去了这些温馨智能的功能。



标注:matthew hsiung 在关于 eslint 和 WebStorm 的 issue 回复下面提供了一


个解决方案


本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。


原文链接:


https://mp.weixin.qq.com/s/bEAL19teUdROOaZ4_un0UA


2019-09-20 16:11870

评论

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

用友系列之 YonBuilder 低代码平台概论和基本使用

YonBuilder低代码开发平台

低代码 可视化

Python 中的数字类型与转换技巧

小万哥

Python 程序员 软件 后端 开发

使用Optional优雅避免空指针异常

Java随想录

Java 异常

Java训练营毕业总结

jjn0703

福利贴|这是一个程序员不看一定会后悔的问题

Zilliz

非结构化数据 Milvus Zilliz 向量数据库

close()关闭文件方法

梦笔生花

上新啦!腾讯云云原生数据湖产品DLC 2.2.5版本发布,来看特性详解

腾讯云大数据

数据湖

Github上线即遭狂转!上百人通过这份算法手抄本成功上岸字节

程序员万金游

#java java 架构 #算法 #数据结构 #java编程

IoTDB 在国际数据库性能测试排行榜中位居第一?测试环境复现与流程详解第一弹!

Apache IoTDB

腾讯Java后端社招三面,差点就挂了!

程序员小毕

Java 程序员 面试 程序人生 架构师

活动预告 | 中国数据库联盟(ACDU)中国行第三站定档成都,邀您探讨数据库前沿技术

墨天轮

MySQL 数据库 oracle postgresql zabbix

Generative AI 新世界 | 扩散模型原理的代码实践之采样篇

亚马逊云科技 (Amazon Web Services)

机器学习 #人工智能 生成式人工智能 Amazon SageMaker 大语言模型

火山引擎边缘云:数智化项目管理助力下的业务增长引擎

火山引擎边缘云

数字化 飞书 数智化 #项目管理

华为智慧屏,吹尽狂沙始到金

脑极体

AI智慧屏

轻量应用服务器,助力个人开发者最低成本创业

YG科技

华为云耀云服务器L实例:带你探索轻量应用服务器的魅力

YG科技

ICCV 2023|小红书 4 篇入选论文亮点解读,「开集视频目标分割」获得 Oral

小红书技术REDtech

算法 ICCV

跨网传输文件时,如何通过日志记录来审计追溯?

镭速

跨网文件传输

从技术创新到应用实践,百度智能云发起大模型平台应用开发挑战赛!

不叫猫先生

百度智能云 千帆大模型平台

企业内部通讯,WorkPlus助您打造高效沟通平台

WorkPlus

Redis类型(Type)与编码(Encoding)

Java随想录

redis

Redis内存碎片:深度解析与优化策略

Java随想录

Java redis

WorkPlus Meet 视频会议,自主可控,支持私有化部署

WorkPlus

rabbitMQ到底是个啥东西?

程序员万金游

Java 开发 #java Rabbit MQ

华为阅读“鲁迅专栏”已上线,读国内名家作品就上华为阅读

最新动态

大道总是孤独的——查理芒格如是说

少油少糖八分饱

投资 长期主义 能力圈 查理芒格 股东大会

彻底告别传统FTP,新的替代FTP产品比你想象的好的多

镭速

传输协议 FTP传输替代方案

HarmonyOS使用多线程并发能力开发

HarmonyOS开发者

HarmonyOS

WorkPlus私有化部署IM即时通讯平台,构建高效安全的局域网办公环境

WorkPlus

为什么要使用zookeeper

Jerry Tse

zookeeper 分布式锁 分布式系统 共识算法 数据强一致性

TinyEngine 低代码引擎到底是什么?

英勇无比的消炎药

开源 前端 低代码

React 的 10 种迷你开发模式_文化 & 方法_李俊冬_InfoQ精选文章