02-React 17 Design Patterns Best Practices
What you can learn
在这一节,我们主要讨论如下话题:
- 什么是JSX,我们为什么推荐使用它
- 什么是Babel,我们如何使用它来写现代的JavaScript代码
- JSX的主要特点,他和 HTML 的区别
- 用一种优雅,可维护的方式进行 JSX 的最佳实践
- linting(尤其是ESLint)能够在团队编程中保持JavaScript的一致性
- 函数式编程的基础,学习为什么遵循函数式范式能够更好的编写 React 组件
Babel 7
React 提供了两种方法来定义我们的element
1.使用JavaScript 函数
class HelloMessage extends React.Component {
render() {
// React 17 弃用 React.createElement,改为 _jsx(...)
return React.createElement(
"div",
null,
"Hello",
this.props.name
);
}
}
ReactDOM.render(
React.createElement(HelloMessage, { name:"Taylor" }),
document.getElementById('hello-example')
);
在React 17中,
React.createElement('div')
已弃用,现在在React内部使用 react/JSX-runtime 来呈现 JSX,这意味着我们将使用 _jsx(’ div’, {}) 之类的东西。也就是说,不再需要为了编写JSX代码而导入React对象。
2.使用JSX语法糖(XML-like syntax)
class HelloMessage extends React.Component {
render() {
return (
<div>
Hello {this.props.name}
</div>
);
}
}
ReactDOM.render(
<HelloMessage name="Taylor" />,
document.getElementById('hello-example')
);
这两个例子均可以在React官方文档的https://reactjs.org/#examples中找到。
为了使用 JSX 和 ES6,我们需要安装 Babel 7。Babel 是一个在 React 社区中广泛运用的 JavaScript 编译器(JavaScript Compiler)
我们需要明白的是,因为我们希望使用 JSX 和 ES6,使我们的代码简洁优雅且具有可维护性,但这种语法浏览器无法识别,因此我们使用 Babel 来将我们的代码转化为 ES5 或更低的可被浏览器支持的版本。
在以前的Babel 6.x 版本,我们通过安装babel-cli
,其中包含babel-node
和 babel-core
,在最新版本中,我们将其分离开了,因此,我们需要通过如下命令安装 Babel 7:
npm install -g @babel/core @babel/node
在多数情况下,开发者往往不希望安装全局包,而是将其内置在项目中,但在教学过程中,这样是比较方便的
安装完毕后,我们就可以通过限免的命令将我们编写的JSX ES6 语法转换成浏览器可支持的JavaScript版本:
babel source.js -o output.js
Babel的强大之处在于,他是高度可配置的,虽然我们可以手动配置,但更方便的选择是使用一些预设好的配置:
npm install -g @babel/preset-env @babel/preset-react
Creating our first element
现在,我们的编程环境已经可以支持 JSX,让我们从一个创建一个div
element 开始学习,在JavaScript中,我们可以通过_jsx
函数来创建一个div
element:
_jsx(
// props
'div' ,
// props
{}
)
用 JSX 语法糖实现则是如下所示:
<div />
这像是一个HTML语法,但存在于 .js 文件中。需要注意的是,JSX只是一种语法糖,他在交给浏览器执行前是会被转化成 JavaScript代码的,即当我们运行Babel时,<div />
JSX代码会被转换成_jsx('div', {})
JavaScript代码,这是我们在编写模板时应该始终牢记的一点。
DOM elements and React components
通过 JSX, 我们可以创建 HTML 元素和 React 组件,二者的唯一区别在于首字母是否大写。
- 渲染HTML按钮,我们使用
<button />
,Babel为我们编译的js代码为:_jsx('button', {})
- 渲染button组件,我们使用
<Button />
,Babel为我们编译的js代码为:_jsx(Button, {})
如果你能明白这两个参数的区别,说明你对前一章知识掌握的还不错,正如上一章所讲的 element,拥有一个重要的参数type,我们将字符串传入type时,表示DOM节点,我们将函数传给type时,表示React字符串。
Props
当我们的DOM element 或者 React Component 需要 参数(Props)时,使用 JSX 是非常方便的:
<img
src="https://www.js.education/images/logo.png"
alt="JS Education"
/>
其等价的 JavaScript 代码可读性就差多了:
_jsx(
// type
"img",
// props
{
src:"https://www.js.education/images/logo.png",
alt:"JS Education"
}
)
Children
JSX允许您定义子元素来描述元素树,并组合复杂元素UI,一个基本的例子是一个包含文本的链接,如下所示:
<a href="https://js.education">Click me! </a>
转译为JavaScript代码如下所示:
_jsx(
// type
"a",
// props
{
href:"https://www.js.education"
},
// children
"Click me! "
)
我们的链接可能被包含在一个div元素中以满足一些布局要求,而JSX代码片段实现如下:
<div>
<a href="https://www.js.education">Click me! </a>
</div>
等价的 JavaScript 写法如下:
_jsx(
// type
"div",
// prop
null,
// children
_jsx(
// type
"a",
//prop
{ href:"https://www.js.education" },
// children
"Click me!"
)
)
现在应该清晰的看出,JSX的类似xml的语法是如何使一切更加可读和可维护的,但重要的是要知道,与我们的JSX并行的JavaScript可以控制元素的创建。
我们还可以在 Children 中使用JavaScript表达式,比如函数或变量,为此,我们必须将表达式括在花括号内:
<div>
Hello, {variable}.
{/* 大括号内的 () => console.log("Function") 是JS表达式,本句注释也是JS表达式 */}
I' m a {() => console.log(' Function' )}.
</div>
Differences with HTML
到目前为止,我们已经看到了JSX和HTML之间的相似之处。现在让我们看看它们之间的细微差别以及它们存在的原因。
Attributes
我们必须始终牢记,JSX不是标准语言,它会被转换成JavaScript。因此,有些属性不能使用。
例如,我们必须使用className而不是class,必须使用htmlFor而不是For,如下所示:
<label className="awesome-label" htmlFor="name" />
这样做的原因是class和for在JavaScript中是保留字。
Style
样式属性不像HTML那样接受CSS字符串,而是接受一个JavaScript对象,其中样式为小驼峰命名:
{/* div style="background-color: red" */}
<div style={{ backgroundColor: 'red' }} />
注意,这里最外层的大括号表示其包含的内容为JavaScript表达式,内层的大括号表示一个JavaScript 对象,因此看起来像是两层大括号
因此,你也可以将你的样式定义为变量,再传入其中:
const styles = {
backgroundColor: ' red'
}
<div style={styles} />
Root
由于每一个JSX元素最终都会被转换成与其对应的JavaScript函数,由于不能在JavaScript代码中返回两个函数,所以在同级上有多个元素时(每个元素对应自己的JavaScript函数,因此一个.js文件中的最外层就会出现多个return语句),我们必须将它们包装在父级中。
让我们看一个简单的例子:
<div />
<div />
这会给我们带来以下错误:Adjacent JSX elements must be wrapped in an enclosing tag.
。因此我们需要在其外层包裹一个父标签:
<div>
<div />
<div />
</div>
从 React 16.2.0 之后,我们可以直接返回一个数组了:
return [
<li key="1">First item</li>,
<li key="2">Second item</li>,
<li key="3">Third item</li>
]
此外,React现在有一个叫做Fragment的新功能,它也可以作为元素的特殊包装:
import{ Fragment }from 'react'
return(
<Fragment>
<h1>Anh1heading</h1>
Sometexthere.
<h2>Anh2heading</h2>
Moretexthere.
Evenmoretexthere.
</Fragment>
)
或者你可以使用空标签(<></>):
return (
<>
<ComponentA />
<ComponentB />
<ComponentC />
</>
)
Spaces
需要再次强调的是,JSX并不是HTML,即使它是类XML(XML-like)语法,在空格方面,JSX有一点反直觉:
<div>
<span>My</span>
name is
<span>Carlos</span>
</div>
如果你将其视为HTML,则运行结果应该为My name is Carios
(这也是我们所期望的),但实际上,上述代码如果为JSX时,运行结果则为:Myname isCarios
。如果你希望带上空格,你需要显式的打出" "
才行:
<div>
<span>My</span>
{' '}
name is
{' '}
<span>Carlos</span>
</div>
Boolean attributes
当你不给属性(props)赋值时,JSX会自动推断为 true:
<button disabled />
{/* _jsx("button", { disabled: true }) */}
<button disabled={false} />
{/* _jsx("button", { disabled: false }) */}
因此,为了避免困惑,最好每次都显式声明。
Spread attributes
这里有一个非常好用的特性,就是使用展开属性运算符...
,它来自ECMAScrip 提案中的 展开/剩余属性(rest/spread)。当你希望将一个对象中的所有属性传递给 element 时非常好用。
const attrs = {
id: 'myId' ,
className: 'myClass'
}
return <div {...attrs} />
转译后的JavaScript代码为:
var attrs = {
id: 'myId' ,
className: 'myClass'
}
return _jsx('div' , attrs)
Template literals
模板字面量(Template literals)是一种允许内嵌表达式的字符串字面量,你可以在里面使用多行字符串和字符串的插值特性。
模板字面量使用反引号` `
包裹,通过${表达式}
使用预设值:
const name = `Carlos`
const multilineHtml = `<p>
This is a multiline string
</p>`
console.log(`Hi, my name is ${name}` )
Common patterns
现在我们已经了解了JSX的工作方式,接下来我们将了解如何按照一些有用的约定和技术以正确的方式使用它。
Multiline
- 当存在嵌套关系时,总是分行,而不仅写在一行:
{/* 推荐 */}
<div>
<Header />
<div>
<Main content={...} />
</div>
</div>
{/* 不推荐 */}
<div><Header /><div><Main content={...} /></div></div>
当然,如果标签内部时变量或文本,则不建议分行。
- 当element 存在多行时,需要用小括号
( )
包裹:
return <div />
return (
<div />
)
如果多行时不添加括号,则可能出现一些bug,例如下面的例子:
return
<div />
该 JSX 被编译成 JavaScript 时,其代码为:
return
_jsx("div", null)
由于 JavaScript 会自动在行尾添加缺失的分号,因此该 JavaScript 被视为两个语句。
Multi-properties
当 element 具有多属性时,一种常见的解决方案是将每个属性写在新行上,并使用同一级缩进,然后将结束括号与开始标记对齐:
<button
foo="bar"
veryLongPropertyName="baz"
onSomething={this.handleSomething}
/>
Conditionals
在 JavaScript 中,我们可以使用 if else 等表达式来进行条件判断,但在 JSX 中,我们根据不同的情况,总结出了如下的最佳实现,仅供参考:
{/* 使用短路方式判断是否需要渲染 */}
{isLoggedIn && <LoginButton />}
{/* 使用三目运算符判断渲染组件 */}
<div>
{isLoggedIn ? <LogoutButton /> : <LoginButton />}
</div>
{/* 复杂条件时,编写判断函数 */}
const canShowSecretData = () => {
const { dataIsReady, isAdmin, userHasPermissions } = props
return dataIsReady && (isAdmin | | userHasPermissions)
}
return (
<div>
{this.canShowSecretData() && <SecretData />}
</div>
)
还有一些其他进行条件判断的方式,需要引入外部依赖,这里仅作详细介绍,不做理解和解释:
npm install --save render-if # 安装外部依赖
const { dataIsReady, isAdmin, userHasPermissions } = props // 获取必要参数
// 使用renderIf创造一个组件,名为canShowSecretData
const canShowSecretData = renderIf(
dataIsReady && (isAdmin | | userHasPermissions)
) ;
return (
<div>
{/* 满足条件时会渲染 */}
{canShowSecretData(<SecretData />) }
</div>
) ;
还有一种可以实现类似功能的外部依赖,它使用了高阶组件,即参数为组件,返回一个强化的组件,在后面的章节会详细讲述高阶组件的概念:
npm install --save react-only-if # 安装外部依赖
import onlyIf from ' react-only-if'
const SecretDataOnlyIf = onlyIf(
({ dataIsReady, isAdmin, userHasPermissions }) => dataIsReady &&
(isAdmin | | userHasPermissions)
) (SecretData)
const MyComponent = () => (
<div>
<SecretDataOnlyIf
dataIsReady={...}
isAdmin={...}
userHasPermissions={...}
/>
</div>
)
export default MyComponent
这种方式,好像我们在使用一个普通的组件。
Loops
在JSX模板里,当一个函数返回一个数组时,数组里的每一个项都会被编译成一个element
// users = [{name=flag},{name=iris}]
<ul>
{users.map(user => <li>{user.name}</li>) }
</ul>
上例中的users.map()返回的数组会被逐一编译成element,顺序排放在ul标签里:
<ul>
<li>flag</li>
<li>ifis</li>
</ul>
Control statements
如果你不喜欢使用JavaScript的控制语句,JSX 也提供了自己的控制语句,但是需要外部依赖,因此这里仅做记录,不做解释:
npm install --save jsx-control-statements
一旦它被安装,我们必须把它添加到我们的Babel插件列表配置文件.babelrc中:
"plugins": ["jsx-control-statements"]
<If condition={this.canShowSecretData}>
<SecretData />
</If>
// 等价于:{canShowSecretData ? <SecretData /> : null}
<Choose>
<When condition={...}>
<span>if</span>
</When>
<When condition={...}>
<span>else if</span>
</When>
<Otherwise>
<span>else</span>
</Otherwise>
</Choose>
<ul>
<For each="user" of={this.props.users}>
<li>{user.name}</li>
</For
Sub-rendering
拆解组件为多个部分,以提高其可维护性
const renderUserMenu = () => {
// JSX for user menu
}
const renderAdminMenu = () => {
// JSX for admin menu
}
return (
<div>
<h1>Welcome back! </h1>
{userExists && renderUserMenu() }
{userIsAdmin && renderAdminMenu() }
</div>
)
Styling code
在本节中,你将学习如何实现EditorConfig和ESLint,通过验证你的代码风格来提高你的代码质量。在你的团队中有一个标准的代码风格,避免使用不同的代码风格是很重要的。
EditorConfig
EditorConfig 可以帮助开发者在不同IDE之间维持代码风格的一致性
你需要在项目根路径创建一个后缀为.editorconfig的文件,作者的配置如下:
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.html]
indent_size = 4
[*.css]
indent_size = 4
[*.md]
trim_trailing_whitespace = false
Prettier
Prettier是一种有腔调?(opinionated)的代码格式化器,它得到许多语言的支持,可以与大多数编辑器集成。这个插件非常有用,因为你可以在保存代码时格式化代码,而不需要在代码审查时讨论代码风格,这将节省你大量的时间和精力。
同样,在项目根目录配置.prettierrc文件,作者的配置如下:
{
"arrowParens": "avoid",
"bracketSpacing": true,
"jsxSingleQuote": false,
"printWidth": 100,
"quoteProps": "as-needed",
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
}
ESLint
主要用来避免语法错误
作者在这里讲了一些ESLint的规则,我仅作记录,不做解释:
安装:
npm install -g eslint eslint-config-airbnb eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react
配置文件:.eslintrc:
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier"] ,
"extends": [
"airbnb",
"eslint: recommended",
"plugin: @typescript-eslint/eslint-recommended",
"plugin: @typescript-eslint/recommended",
"plugin: prettier/recommended"
] ,
"settings": {
"import/extensions": [". js", ". jsx", ". ts", ". tsx"] ,
"import/parsers": {
"@typescript-eslint/parser": [". ts", ". tsx"]
},
"import/resolver": {
"node": {
"extensions": [". js", ". jsx", ". ts", ". tsx"]
}
}
},
"rules": {
"semi": [2, "never"] ,
"@typescript-eslint/class-name-casing": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"no-restricted-syntax": "off",
"no-use-before-define": "off",
"import/extensions": "off",
"import/prefer-default-export": "off",
"max-len": [
"error",
{
"code": 100,
"tabWidth": 2
}
] ,
"no-param-reassign": "off",
"no-underscore-dangle": "off",
"react/jsx-filename-extension": [
1,
{
"extensions": [". tsx"]
}
] ,
"import/no-unresolved": "off",
"consistent-return": "off",
"jsx-a11y/anchor-is-valid": "off",
"sx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-static-element-interactions": "off",
"react/jsx-props-no-spreading": "off",
"jsx-a11y/label-has-associated-control": "off",
"react/jsx-one-expression-per-line": "off",
"no-prototype-builtins": "off",
"no-nested-ternary": "off",
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
}
Functional programming
当我们编写JSX时,除了遵循最佳实践和使用一个linter来加强一致性和更早地发现错误之外,我们还可以做一件事来清理我们的代码:遵循FP(Functional programming)风格。下面简单的介绍一下FP风格的基本概念,随着React的深入学习,你会发现React代码同样遵循这FP的设计风格。
First-class Function
头等函数(First-class Function),它们可以被视为任何其他变量,即可以将一个函数作为参数传递给其他函数,或者它可以由另一个函数返回(详见高阶函数)并作为一个变量的值赋值。
这允许我们引入高阶函数(Higher-Order Functions,HoFs)的概念。HoFs是接受一个函数作为参数,也可以选择一些其他参数,并返回一个函数的函数。返回的函数通常用一些特殊的行为来增强。例如:
const add = (x, y) => x + y
const log = fn => (. . . args) => {
return fn(. . . args)
}
const logAdd = log(add)
在 React 中,同样有高阶组件的概念
Purity
FP的一个重要方面是编写纯函数(Purity)。纯函数,含义是没有任何副作用,即不会改变非函数作用域内的任何东西。
const add = (x, y) => x + y // 纯函数
const add = y => x += y // 非纯函数
第一个函数不会改变作用域外的x,y的值,而第二个函数会改变其作用域外x的值
在 React 中,我们的组件不希望改变其作用域外的任何东西
Immutability
我们已经看到了如何编写不改变状态的纯函数,但是如果我们需要改变变量的值呢?在FP中,函数不是改变变量的值,而是创建一个新的变量并返回它。这种处理数据的方式称为不可变性(Immutability)。
// arr = [1, 2]
const add3 = arr => arr. push(3) // 不具有不可变性
// 连续执行两次,arr = [1, 2, 3, 3]
const add3 = arr => arr. concat(3) // 具有不可变性
// 连续执行两次,arr = [1, 2, 3]
这样的好处是,当给予相同的输入时,重复执行函数能够得到相同的结果
在 React 中,如状态更新则是返回一个全新的状态,而并非在原有的状态上添加删除
Currying
函数柯里化:柯里化(curry)是将接受多个参数的函数转换为一个函数的过程,接受一个参数,然后返回一个函数。让我们看一个例子来澄清这个概念:
// 普通函数
const add = (x, y) => x + y
// 柯里化
const add = x => y => x + y
const add1 = add(1)
add1(2) ; // 3
add1(3) ; // 4
柯里化使得第一个参数的值被存储起来。
我还没学到React对应的实现
Composition
最后,FP中一个可以应用于React的重要概念是组合(Composition)。函数(或React 组件)可以组合生成具有更高级特性和属性的新功能。
const add = (x, y) => x + y
const square = x => x * x
const addAndSquare = (x, y) => square(add(x, y))
按照这个范例,我们最终得到了可以组合在一起的小的、简单的、可测试的纯函数。
Summary
- 学习了大量关于JSX如何工作以及如何在组件中正确使用它的知识。我们从语法的基础开始,创建了一个坚实的知识库,使我们能够掌握JSX及其特性。
- 学习了如何配置Prettier,以及ESLint及其插件如何帮助我们更快地发现问题,并在我们的代码库中执行一致的风格指南。
- 学习了FP的基础知识,这是理解编写React应用程序时要用到的重要概念。
我们准备在下一章中更深入地挖掘React并学习如何编写真正可重用的组件。