React 简介
React 是一个由 Facebook 创建和维护的流行 JavaScript 库。 React 专注于提供一套全面的功能和工具来构建 Web 应用程序中的视图层。 React 提供了专注于组件概念的视图抽象。 组件可以是按钮、表单输入、简单容器(例如 HTML div)或用户界面中的任何其他元素。 这个想法是,您应该能够通过定义和组合具有特定职责的高度可重用组件来构建应用程序的用户界面。
React 与其他 Web 视图库的不同之处在于,它在设计上并未绑定到 DOM。 事实上,它提供了一种称为虚拟 DOM (nodejsdp.link/virtual-dom) 的高级抽象,它非常适合 Web,但也可以用于其他上下文,例如,用于构建移动应用程序、3D 建模 环境,甚至定义硬件组件之间的交互。 简单来说,虚拟 DOM 可以被视为重新渲染以树状结构组织的数据的有效方式。
"Learn it once, use it everywhere."
这是 Facebook 推出 React 时所用的座右铭。 它故意嘲笑著名的 Java 座右铭 “一次编写,随处运行”,其明确意图是强调 Java 哲学的根本转变。 Java 最初的设计目标是允许开发人员编写一次应用程序,然后在不进行任何更改的情况下在尽可能多的平台上运行它们。 相反,React 哲学承认每个平台本质上都是不同的,因此鼓励开发人员编写针对相关目标平台优化的不同应用程序。 React 作为一个库,将重点转向提供方便的设计和架构原则和工具,一旦掌握这些原则和工具,就可以轻松地用于编写特定于平台的代码。
如果您有兴趣了解 React 在与 Web 开发领域不严格相关的上下文中的应用程序,您可以查看以下项目:React Native for mobile apps (nodejsdp.link/react-native)、React PIXI 用于使用 OpenGL 进行 2D 渲染 (nodejsdp.link/react-pixi)、react Three-Fiber 来创建 3D 场景 (nodejsdp.link/react-three-Fiber) 和 React Hardware (nodejsdp.link/react-hardware)。 |
React 在通用 JavaScript 开发环境中如此有趣的主要原因是它允许我们使用几乎相同的代码在客户端和服务器上渲染 React 组件。 换句话说,通过 React,我们能够直接从 Node.js 渲染显示页面所需的 HTML 代码。 然后,当页面在浏览器上加载时,React 将执行一个称为 Hydration 的过程(nodejsdp.link/Hydration),该过程将添加所有仅前端的副作用,如单击处理程序、动画、附加异步数据获取、动态路由、 等等。 Hydration 将静态标记转换为完全交互的体验。
这种方法允许我们构建单页面应用程序(SPA),其中第一次渲染主要发生在服务器上,但是,一旦页面加载到浏览器上并且用户开始单击,则仅渲染页面的部分内容。 需要更改的内容会动态刷新,而不需要重新加载整个页面。
这种设计有两个主要优点:
-
更好的搜索引擎优化(SEO):由于页面标记是由服务器预呈现的,因此各种搜索引擎只需查看服务器返回的HTML 即可了解页面内容。 他们不需要模拟浏览器环境并等待页面完全加载来查看给定页面的内容。
-
更好的性能:由于我们预渲染标记,因此即使浏览器仍在下载、解析和执行页面中包含的 JavaScript 代码,该标记也已在浏览器上可用且可见。 这种方法可以带来更好的用户体验,因为内容加载速度更快,并且渲染期间浏览器“闪烁”更少。
值得一提的是,React 虚拟 DOM 能够优化渲染变化的方式。 这意味着 DOM 在每次更改后都不会完整呈现,而是 React 使用智能内存比较算法,该算法能够预先计算应用于 DOM 的最小更改数量,以便更新视图。 这产生了一种非常有效的快速浏览器渲染机制。 |
现在我们知道 React 是什么了,在下一节中,我们将编写我们的第一个 React 组件!
Hello React
话不多说,让我们开始使用 React 并跳到一个具体的示例。 这将是一个 “Hello World” 类型的示例,但在我们转向更实际的示例之前,它将帮助我们说明 React 背后的主要思想。
让我们首先在新文件夹中创建一个新的 webpack 项目:
npm init -y
npm install --save-dev webpack webpack-cli
node_modules/.bin/webpack init
然后,按照指导说明进行操作。 现在,让我们安装 React:
npm install --save react react-dom
现在,我们创建一个文件 src/index.js,其中包含以下内容:
import react from 'react'
import ReactDOM from 'react-dom'
const h = react.createElement // (1)
class Hello extends react.Component { // (2)
render () { // (3)
return h('h1', null, [ // (4)
'Hello ',
this.props.name || 'World' // (5)
])
}
}
ReactDOM.render( // (6)
h(Hello, { name: 'React' }),
document.getElementsByTagName('body')[0]
)
让我们回顾一下这段代码发生了什么:
-
我们要做的第一件事是为 react.createElement 函数创建一个方便的别名。 在本示例中,我们将多次使用此函数来创建 React 元素。 这些可以是普通 DOM 节点(常规 HTML 标签)或 React 组件的实例。
-
现在,我们定义 Hello 组件,它必须扩展 react.Component 类。
-
每个 React 组件都必须实现一个 render() 方法。 该方法定义了组件在 DOM 上渲染时如何在屏幕上显示,并且必须返回一个 React 元素。
-
我们使用 react.createElement 函数创建一个 h1 DOM 元素。 此方法需要三个或更多参数。 第一个参数是标签名称(作为字符串)或 React 组件类。 第二个参数是一个对象,用于将属性(或 props)传递给组件(如果我们不需要指定任何属性,则为 null)。 最后,第三个参数是子元素的数组(或者也可以传递多个参数)。 元素也可以是文本(文本节点),如我们当前的示例所示。
-
在这里,我们使用 this.props 来访问在运行时传递给该组件的属性。 在这种特定情况下,我们正在寻找 name 属性。 如果通过,我们用它来构造一个文本节点; 否则,我们默认使用字符串 “World”。
-
在最后的代码块中,我们使用 ReactDOM.render() 来初始化我们的应用程序。 该函数负责将 React 应用程序附加到现有页面。 应用程序只不过是 React 组件的一个实例。 在这里,我们实例化 Hello 组件并传递字符串 “React” 作为 name 属性。 最后,作为最后一个参数,我们必须指定页面中的哪个 DOM 节点将是我们应用程序的父元素。 在本例中,我们使用页面的 body 元素,但您可以定位页面中任何现有的 DOM 元素。
现在,您可以通过运行以下命令来查看应用程序的预览:
npm start
您现在应该在浏览器窗口中看到 “Hello React”。 恭喜,您已经构建了第一个 React 应用程序!
React.createElement 的替代品
重复使用 react.createElement() 可能会损害我们的 React 组件的可读性。 事实上,嵌套许多 react.createElement() 调用,即使使用我们的 h() 别名,也会让我们很难理解我们希望组件渲染的 HTML 结构。
因此,直接使用 react.createElement() 并不常见。 为了解决这个问题,React 团队提供并鼓励使用名为 JSX (nodejsdp.link/jsx) 的替代语法。
JSX 是 JavaScript 的超集,允许您将类似 HTML 的代码嵌入到 JavaScript 代码中。 JSX 使得 React 元素的创建类似于编写 HTML 代码。 使用 JSX,React 组件通常更具可读性且更易于编写。 通过查看具体示例可以更容易地理解我们在这里的意思,因此让我们使用 JSX 重写我们的 “Hello React” 应用程序:
import react from 'react'
import ReactDOM from 'react-dom'
class Hello extends react.Component {
render () {
return <h1>Hello {this.props.name || 'World'}</h1>
}
}
ReactDOM.render(
<Hello name="React"/>,
document.getElementsByTagName('body')[0]
)
更具可读性,不是吗?
不幸的是,由于 JSX 不是标准的 JavaScript 功能,因此采用 JSX 需要我们将 JSX 代码“编译”为标准的等效 JavaScript 代码。 在通用 JavaScript 应用程序的上下文中,我们必须在客户端代码和服务器端代码上执行此操作,因此,为了简单起见,我们不会在本章的其余部分中使用 JSX。
有一些相对较新的 JSX 替代方案依赖于标准 JavaScript 标记模板文字(您可以在 nodejsdp.link/template-literals 阅读有关 JavaScript 标记模板文字的更多信息)。 使用模板文字似乎是代码之间的一个很好的折衷方案,代码仍然很容易阅读和编写,并且不必执行中间编译过程。 提供此功能的两个最有前途的库是 htm (nodejsdp.link/htm) 和 esx (nodejsdp.link/esx)。
在本章的其余部分中,我们将使用 htm,所以让我们再次重写我们的 “Hello React” 示例,这次使用 htm:
import react from 'react'
import ReactDOM from 'react-dom'
import htm from 'htm'
const html = htm.bind(react.createElement) // (1)
class Hello extends react.Component {
render () { // (2)
return html`<h1>
Hello ${this.props.name || 'World'}
</h1>`
}
}
ReactDOM.render(
html`<${Hello} name="React"/>`, // (3)
document.getElementsByTagName('body')[0]
)
这段代码看起来非常可读,但让我们快速澄清一下我们在这里如何使用 htm:
-
我们要做的第一件事是创建模板标记函数 html。 该函数允许我们使用模板文字来生成 React 元素。 在运行时,这个模板标签函数将在需要时为我们调用react.createElement()。
-
在这里,我们使用带标签的模板文字和 html 标签函数来创建 h1 标签。 请注意,由于这是标准标记模板文字,因此我们可以使用常规占位符语法 (${expression}) 将动态表达式插入到字符串中。 请记住,模板文字和标记模板文字使用反引号 (`) 而不是单引号 (') 来分隔模板字符串。
-
同样,我们可以使用占位符语法来创建 React 组件的实例(<${ComponentClass}>)。 请注意,如果组件实例包含子元素,我们可以使用特殊的 </> 标记来指示组件的结束(例如,<${Component}><child/></>)。 最后,我们可以将 props 作为普通 HTML 属性传递给组件。
至此,我们应该能够了解一个简单的 “Hello World” React 组件的基本结构了。 在下一节中,我们将向您展示如何管理 React 组件中的状态,这对于大多数实际应用程序来说都是一个重要概念。
有状态组件
在前面的示例中,我们了解了如何构建无状态 React 组件。 无状态是指组件仅接收来自外部的输入(在我们的示例中,它接收的是 name 属性),并且不需要计算或管理任何内部信息即可将自身渲染到 DOM。
虽然拥有无状态组件很好,但有时,您必须管理某种状态。 React 允许我们做到这一点,所以让我们通过一个例子来学习如何做。
让我们构建一个 React 应用程序,该应用程序显示 GitHub 上最近更新的项目列表。
我们可以封装从 GitHub 异步获取数据并将其显示在专用组件上的所有逻辑:RecentGithubProjects 组件。 该组件可通过 query 属性进行配置,它允许我们过滤 GitHub 上的项目。 query prop 将接收一个关键字,例如 “javascript” 或 “react”,并且该值将用于构造对 GitHub 的 API 调用。
最后我们看一下 RecentGithubProjects 组件的代码:
// src/RecentGithubProjects.js
import react from 'react'
import htm from 'htm'
const html = htm.bind(react.createElement)
function createRequestUri(query) {
return `https://api.github.com/search/repositories?q=${
encodeURIComponent(query)
}&sort=updated`
}
export class RecentGithubProjects extends react.Component {
constructor(props) { // (1)
super(props) // (2)
this.state = { // (3)
loading: true,
projects: []
}
}
async loadData() { // (4)
this.setState({loading: true, projects: []})
const response = await fetch(
createRequestUri(this.props.query),
{mode: 'cors'}
)
const responseBody = await response.json()
this.setState({
projects: responseBody.items,
loading: false
})
}
componentDidMount() { // (5)
this.loadData()
}
componentDidUpdate(prevProps) { // (6)
if (this.props.query !== prevProps.query) {
this.loadData()
}
}
render() { // (7)
if (this.state.loading) {
return 'Loading ...'
}
// (8)
return html`
<ul>
${this.state.projects.map(project => html`
<li key=${project.id}>
<a href=${project.html_url}>${project.full_name}</a>:
${' '}${project.description}
</li>
`)}
</ul>`
}
}
该组件中有一些新的 React 概念,所以我们在这里讨论主要细节:
-
在这个新组件中,我们将重写默认构造函数。 构造函数接受作为参数传递给组件的 props。
-
我们要做的第一件事是调用原始构造函数并传播 props,以便 React 可以正确初始化组件。
-
现在,我们可以定义初始组件状态。 我们的最终状态将是 GitHub 项目的列表,但这些项目不会立即可用,因为我们需要动态加载它们。 因此,我们将初始状态定义为布尔标志,表明我们正在将数据和项目列表加载为空数组。
-
loadData() 函数负责发出 API 请求、获取必要的数据并使用 this.setState() 更新内部状态。 请注意, this.setState() 被调用两次:在我们发出 HTTP 请求之前(以激活加载状态)和请求完成时(以取消设置加载标志并填充项目列表)。 当状态改变时,React 会自动重新渲染组件。
-
在这里,我们引入另一个新概念:componentDidMount 生命周期函数。 一旦组件成功实例化并附加(或安装)到 DOM,React 就会自动调用此函数。 这是首次加载数据的完美位置。
-
函数 componentDidUpdate 是另一个 React 生命周期函数,每次更新组件时都会自动调用它(例如,如果新的 props 已传递给组件)。 在这里,我们检查查询属性自上次更新以来是否已更改。 如果是这种情况,那么我们需要重新加载项目列表。
-
最后,让我们看看 render() 函数中发生了什么。 主要要注意的是,这里我们必须处理组件的两种不同状态:加载状态和可显示项目列表的状态。 由于每次状态或 props 改变时 React 都会调用 render() 函数,所以这里只需要一个 if 语句就足够了。 这种技术通常称为条件渲染。
-
在最后一步中,我们使用 Array.map() 渲染元素列表,为使用 GitHub API 获取的每个项目创建一个列表元素。 请注意,每个列表元素都会收到 key 属性的值。 key 属性是一个特殊的属性,在渲染元素数组时推荐使用它。 每个元素都应该提供一个唯一的键。 这个 prop 帮助虚拟 DOM 优化每个渲染过程(如果你想详细了解 React 在这种情况下做了什么,你可以看看 nodejsdp.link/react-reconciliation)。
您可能已经注意到,我们在获取数据时没有处理潜在的错误。 我们可以通过多种方法在 React 中做到这一点。 最优雅的解决方案可能是实现 ErrorBoundary 组件 (nodejsdp.link/error-boundary),但我们会将其作为练习留给您。 |
现在让我们编写主要的应用程序组件。 在这里,我们想要显示一个导航菜单,用户可以在其中选择不同的查询(“JavaScript”、“Node.js”和“React”)来过滤不同类型的 GitHub 项目:
// src/App.js
import react from 'react'
import htm from 'htm'
import {RecentGithubProjects} from './RecentGithubProjects.js'
const html = htm.bind(react.createElement)
export class App extends react.Component {
constructor(props) {
super(props)
this.state = {
query: 'javascript',
label: 'JavaScript'
}
this.setQuery = this.setQuery.bind(this)
}
setQuery(e) {
e.preventDefault()
const label = e.currentTarget.text
this.setState({label, query: label.toLowerCase()})
}
render() {
return html`
<div>
<nav>
<a href="#" onClick=${this.setQuery}>JavaScript</a>
${' '}
<a href="#" onClick=${this.setQuery}>Node.js</a>
${' '}
<a href="#" onClick=${this.setQuery}>React</a>
</nav>
<h1>Recently updated ${this.state.label} projects</h1>
<${RecentGithubProjects} query=${this.state.query}/>
</div>`
}
}
该组件使用其内部状态来跟踪当前选定的查询。 最初,“javascript”查询被设置并传递到RecentGithubProjects 组件。 然后,每次单击导航菜单中的关键字时,我们都会使用新选择的关键字更新状态。 发生这种情况时,将自动调用 render() 方法,并将查询属性的新值传递给RecentGithubProjects。 反过来,RecentGithubProjects 将被标记为已更新,并且它将在内部重新加载并最终更新新查询的项目列表。
需要强调的一个有趣的细节是,在构造函数中,我们显式地将 setQuery() 函数绑定到当前组件实例。 我们这样做的原因是因为这个函数直接用作点击事件的事件处理程序。 在这种情况下,如果没有绑定,对 this 的引用将是未定义的,并且无法从处理程序调用 this.setState()。
此时,我们只需将 App 组件附加到 DOM 即可运行我们的应用程序。 我们开工吧:
// src/index.js
import react from 'react'
import ReactDOM from 'react-dom'
import htm from 'htm'
import { App } from './App.js'
const html = htm.bind(react.createElement)
ReactDOM.render(
html`<${App}/>`,
document.getElementsByTagName('body')[0]
)
最后,让我们使用 npm start 运行应用程序并在浏览器上测试它。
请注意,由于我们在应用程序中使用了 async/await,因此 webpack 生成的默认配置可能无法立即工作。 如果您有任何问题,请将您的配置文件与本书提供的代码示例中的配置文件 (nodejsdp.link/wpconf) 进行比较。 |
尝试刷新页面并单击导航菜单上的各种关键字。 几秒钟后,您应该看到项目列表正在刷新。
至此,您应该非常清楚 React 是如何工作的,如何将组件组合在一起,以及如何利用 state 和 props。 希望这个简单的练习也能帮助您找到您可能想要贡献的新的、有趣的开源 JavaScript 项目!
我们已经涵盖了足够的基础来构建我们的第一个 Universal React 应用程序。 但如果您想精通 React,我们建议您阅读官方 React 文档 (nodejsdp.link/react-docs),以获得对该库更详尽的概述。 |
我们终于准备好利用我们学到的 webpack 和 React 来创建一个简单但完整的通用 JavaScript 应用程序。