写点什么

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:11839

评论

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

在离开新手村后,你该如何的走出呢?打造属于你的快与慢的能力。

叶小鍵

架构 idea

型火🔥

架构 原则 架构之道

探索 Snabbdom 模块系统原理

Geek_z9ygea

JavaScript Vue Web Vue 3 Snabbdom

工作多年后我更明白了UT的重要性

好好学习,天天向上

Python PyAutoGUI 库

HoneyMoose

uni-app跨端开发H5、小程序、IOS、Android(四):了解uni-app项目结构

黑马腾云

html5 微信小程序 uni-app android iOS Developer

科技强国的使命召唤中,百度AI埋下三根未来“引线”

脑极体

Vue3源码 | createApp都干了什么?

梁龙先森

源码分析 大前端 Vue3

OpenCV 写图像也有讲究,取经之路第 5 天

梦想橡皮擦

28天写作 3月日更

2021十大区块链领域即将起飞

CECBC

区块链 投资

为何数字人民币要采用“小额匿名、大额可溯”的设计?

CECBC

数字货币

正则表达式的使用与匹配原理解析

Guanngxu

正则表达式

MongoDB中的null类型查询

Kylin

mongodb 3月日更 21天挑战 数据库查询 NoSql查询语法

【LeetCode】矩阵置零Java题解

Albert

算法 LeetCode 28天写作 3月日更

FISCO BCOS 开发环境节点搭建 | 联盟链开发(一)

李大狗

区块链 联盟链 FISCO BCOS 狗哥

大数据中流量分析常见分类

大数据技术指南

大数据 28天写作 3月日更

hive数据倾斜解决办法

五分钟学大数据

大数据 hive 28天写作 3月日更

Docker 教程(三):Docker 命令

看山

Docker

2.3 Go语言从入门到精通:数据类型

xcbeyond

3月日更 Go 语言

Seldon使用(一):简介及入门

托内多

tensorflow kubeflow Kubernetes PyTorch seldon

JVM - 类加载器

insight

3月日更

Java8中的 Stream 那么彪悍,你知道它的原理是什么吗?

Java小咖秀

Java 面试 stream java8 开发

架构师训练营 4 期 第12周

引花眠

架构师训练营 4 期

产品训练营第八章作业

Arnold

银行业只是开始,60个可以被区块链改变的行业

CECBC

数字技术

Go Channel源码分析

非晓为骁

源码分析 channel Go 语言

(继续码字) 因果有顺序吗?是一种必要充分条件吗?

mtfelix

28天写作 bewriting 胡思乱想

新年上班第一天生产环境分布式文件系统崩了!!

冰河

高可用 分布式存储 fastdfs 可扩展 无限扩容

领域驱动设计101 - 通用语言

luojiahu

领域驱动设计 DDD

Spark详细剖析

五分钟学大数据

大数据 spark 28天写作 3月日更

控制台的安装与使用 | 联盟链开发(二)

李大狗

联盟链 FISCO BCOS 狗哥

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