自从我第一次开始学习JavaScript以来,我一直听说过React,但我承认我只看了一眼就吓到了我。 我看到看起来像是一堆混有JavaScript和思想的HTML,这不是我们一直试图避免的吗? React有什么大不了的?
相反,我专注于学习vanilla JavaScript并在专业环境中使用jQuery。 经过几次沮丧,尝试开始使用React之后,我终于开始尝试了,我开始明白为什么我可能想要使用React而不是使用vanilla JS或jQuery。
我试着将我学到的所有内容浓缩成一个很好的介绍与你分享,所以在这里。
先决条件
在开始使用React之前,您应该事先知道一些事情。 例如,如果您以前从未使用过JavaScript或DOM,那么在尝试解决React问题之前,我会更熟悉它们。
以下是我认为是React的先决条件。
- 基本熟悉HTML和CSS。
- JavaScript和编程的基础知识。
- 对DOM的基本了解。
- 熟悉ES6语法和功能。
- Node.js和npm全局安装。
目标
了解基本的React概念和相关术语,例如
- Babel,Webpack,JSX,component,props,status和生命周期。
- 构建一个非常简单的React应用程序,演示上述概念。
这是最终结果的来源和在线演示。
View Source on GitHub
View Demo
什么是React?
- React是一个JavaScript库 - 最受欢迎的库之一,在GitHub上有超过100,000颗星。
- React不是一个框架(与Angular不同,它更加自以为是)。
- React是一个由Facebook创建的开源项目。
- React用于在前端构建用户界面(UI)。
- React是MVC应用程序的视图层(模型视图控制器)
React最重要的一个方面是,您可以创建组件(类似于自定义,可重用的HTML元素),以快速有效地构建用户界面。 React还使用status和props简化了数据的存储和处理方式。
我们将在整篇文章中详细介绍所有这些内容,让我们开始吧。
设置和安装
有几种方法可以设置React,我会告诉你两个,这样你就可以很好地了解它是如何工作的。
静态HTML文件
第一种方法不是设置React的流行方式,也不是我们将如何完成本教程的其余部分,但是如果您曾经使用过像jQuery这样的库,它将会很熟悉且易于理解,而且它是 如果你不熟悉Webpack,Babel和Node.js,那么最简单的方法就是入门。
让我们从制作一个基本的index.html文件开始。 我们将在头部加载三个CDN - React,React DOM和Babel。 我们还将使用名为root的id创建一个div,最后我们将创建一个脚本标记,您的自定义代码将存在。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Hello React!</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
// React code will go here
</script>
</body>
</html>
在撰写本文时,我正在加载最新的稳定版本的库。
- React - React顶层API
- React DOM - 添加特定于DOM的方法
- Babel - 一种JavaScript编译器,允许我们在旧浏览器中使用ES6 +
我们的应用程序的入口点将是根div元素,它按惯例命名。 您还会注意到文本/ babel脚本类型,这对于使用Babel是必需的。
现在,让我们编写我们的第一个React代码块。 我们将使用ES6类创建一个名为App的React组件。
class App extends React.Component {
//...
}
现在我们将添加render()方法,这是类组件中唯一需要的方法,用于呈现DOM节点。
class App extends React.Component {
render() {
return (
//...
);
}
}
在返回内部,我们将看起来像一个简单的HTML元素。 请注意,我们不会在此处返回字符串,因此请勿在元素周围使用引号。 这叫做JSX,我们很快就会对它有所了解。
class App extends React.Component {
render() {
return <h1>Hello world!</h1>
}
}
最后,我们将使用React DOM render()方法将我们创建的App类渲染到HTML中的根div中。
ReactDOM.render(<App />, document.getElementById('root'))
这是index.html的完整代码。
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Hello React!</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
render() {
return <h1>Hello world!</h1>
}
}
ReactDOM.render(<App />, document.getElementById('root'))
</script>
</body>
</html>
现在,如果您在浏览器中查看index.html,您将看到我们创建的h1标记呈现给DOM。
Cool! 既然你已经完成了这个,你可以看到React并不是那么疯狂的开始。 它只是一些我们可以加载到HTML中的JavaScript帮助程序库。
我们这样做是出于演示目的,但从现在开始我们将使用另一种方法:创建React App。
创建React应用程序
我刚刚用于将JavaScript库加载到静态HTML页面并动态渲染React和Babel的方法效率不高,而且难以维护。
幸运的是,Facebook创建了Create React App,这是一个预先配置了构建React应用程序所需的一切的环境。 它将创建一个实时开发服务器,使用Webpack自动编译React,JSX和ES6,自动加载CSS文件,并使用ESLint来测试和警告代码中的错误。
要设置create-react-app,请在终端中运行以下代码,该目录位于您希望项目所在的目录之上。 确保Node.js中包含5.2或更高版本。
npx create-react-app react-tutorial
完成安装后,移动到新创建的目录并启动项目。
cd react-tutorial
npm start
运行此命令后,将使用新的React应用程序在localhost:3000上弹出一个新窗口。
如果查看项目结构,您将看到/ public和/ src目录,以及常规node_modules,.gitignore,README.md和package.json。
在/ public中,我们的重要文件是index.html,它与我们之前制作的静态index.html文件非常相似 - 只是一个根div。 这次,没有加载任何库或脚本./ src目录将包含我们所有的React代码。
要查看环境如何自动编译和更新您的React代码,请在/src/App.js中找到如下所示的行:
To get started, edit `src/App.js` and save to reload.
并将其替换为任何其他文本。 保存文件后,您会注意到localhost:3000使用新数据进行编译和刷新。
继续并删除/ src目录中的所有文件,我们将创建自己的样板文件,没有任何膨胀。 我们将保留index.css和index.js。
对于index.css,我只是将Primitive CSS的内容复制并粘贴到文件中。 如果需要,可以使用Bootstrap或任何您想要的CSS框架,或者根本不使用。 我发现它更容易使用。
现在在index.js中,我们导入了React,ReactDOM和CSS文件。
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
让我们再次创建我们的App组件。 在此之前,我们只有一个<h1>
,但现在我正在添加一个带有类的div元素。 您会注意到我们使用className而不是class。 这是我们第一次暗示这里编写的代码是JavaScript,而不是HTML。
class App extends Component {
render() {
return (
<div className="App">
<h1>Hello, React!</h1>
</div>
)
}
}
最后,我们将像以前一样将应用程序呈现给根目录。
ReactDOM.render(<App />, document.getElementById('root'))
这是我们的完整index.js。 这次,我们将Component作为React的属性加载,因此我们不再需要扩展React.Component。
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import './index.css'
class App extends Component {
render() {
return (
<div className="App">
<h1>Hello, React!</h1>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
React 开发者工具
有一个名为React Developer Tools的扩展程序,可以让您在使用React时更轻松。 下载适用于Chrome的React DevTools,或者您喜欢使用的任何浏览器。
安装后,当您打开DevTools时,您将看到React的选项卡。 单击它,您将能够在编写组件时对其进行检查。 您仍然可以转到Elements选项卡以查看实际的DOM输出。 现在看起来似乎不是很多,但随着应用程序变得越来越复杂,使用它将变得越来越必要。
现在我们拥有了实际开始使用React所需的所有工具和设置。
JSX: JavaScript + XML
正如您所见,我们在React代码中使用的是HTML,但它并不是HTML。 这是JSX,代表JavaScript XML。
使用JSX,我们可以编写看似HTML的内容,还可以创建和使用我们自己的类似XML的标记。 这是JSX看起来分配给变量的内容。
const heading = <h1 className="site-heading">Hello, React</h1>
编写React不是强制使用JSX。 在引擎盖下,它正在运行createElement,它接受标记,包含属性的对象和组件的子项,并呈现相同的信息。 以下代码将具有与上述JSX相同的输出。
const heading = React.createElement('h1', { className: 'site-heading' }, 'Hello, React!')
JSX实际上更接近JavaScript,而不是HTML,因此在编写时需要注意几个关键的区别。
- 使用className而不是类来添加CSS类,因为class是JavaScript中的保留关键字。
- JSX中的属性和方法是camelCase - onclick将成为onClick。
- 自动关闭标签必须以斜线结尾 - 例如
<img />
JavaScript表达式也可以使用花括号嵌入JSX中,包括变量,函数和属性。
const name = 'Tania'
const heading = <h1>Hello, {name}</h1>
JSX比在vanilla JavaScript中创建和附加许多元素更容易编写和理解,这也是人们喜欢React的原因之一。
Components
到目前为止,我们已经创建了一个组件–App组件。 React中的几乎所有内容都由组件组成,组件可以是类组件或简单组件。
大多数React应用程序都有许多小组件,所有内容都加载到主App组件中。 组件也经常得到自己的文件,所以让我们改变我们的项目来做到这一点。
从index.js中删除App类,所以它看起来像这样。
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './index.css'
ReactDOM.render(<App />, document.getElementById('root'))
我们将创建一个名为App.js的新文件,并将组件放在那里。
import React, { Component } from 'react'
class App extends Component {
render() {
return (
<div className="App">
<h1>Hello, React!</h1>
</div>
)
}
}
export default App
我们将组件导出为App并将其加载到index.js中。 将组件分离到文件中并不是强制性的,但如果不这样做,应用程序将开始变得笨拙和失控。
Class Components
让我们创建另一个组件。 我们要创建一个表。 创建Table.js,并用以下数据填充它。
import React, { Component } from 'react'
class Table extends Component {
render() {
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Job</th>
</tr>
</thead>
<tbody>
<tr>
<td>Charlie</td>
<td>Janitor</td>
</tr>
<tr>
<td>Mac</td>
<td>Bouncer</td>
</tr>
<tr>
<td>Dee</td>
<td>Aspiring actress</td>
</tr>
<tr>
<td>Dennis</td>
<td>Bartender</td>
</tr>
</tbody>
</table>
)
}
}
export default Table
我们创建的这个组件是一个自定义类组件。 我们将自定义组件大写,以区别于常规HTML元素。 回到App.js,我们可以在表中加载,首先导入它:
import Table from './Table'
然后将它加载到App的render()中,之前我们有“Hello,React!”。 我还改变了外容器的类。
return (
<div className="container">
<Table />
</div>
)
如果您回到实时环境,您将看到加载的表格。
现在我们已经看到了自定义类组件是什么。 我们可以反复使用这个组件。 但是,由于数据是硬编码的,因此目前不太有用。
Simple Components
React中的另一种组件是简单组件,它是一个函数。 此组件不使用class关键字。 让我们使用我们的表并为它制作两个简单的组件 - 表头和表体。
我们将使用ES6箭头函数来创建这些简单的组件。 首先是表头。
const TableHeader = () => {
return (
<thead>
<tr>
<th>Name</th>
<th>Job</th>
</tr>
</thead>
)
}
然后是Body。
const TableBody = () => {
return (
<tbody>
<tr>
<td>Charlie</td>
<td>Janitor</td>
</tr>
<tr>
<td>Mac</td>
<td>Bouncer</td>
</tr>
<tr>
<td>Dee</td>
<td>Aspiring actress</td>
</tr>
<tr>
<td>Dennis</td>
<td>Bartender</td>
</tr>
</tbody>
)
}
现在我们的Table类看起来像这样。
class Table extends Component {
render() {
return (
<table>
<TableHeader />
<TableBody />
</table>
)
}
}
一切都应该像以前一样出现。 如您所见,组件可以嵌套在其他组件中,并且可以混合使用简单组件和类组件。
类组件必须包含render(),并且返回只能返回一个父元素。
作为总结,让我们将一个简单的组件与一个类组件进行比较。
Simple Component
const SimpleComponent = () => {
return <div>Example</div>
}
Class Component
class ClassComponent extends Component {
render() {
return <div>Example</div>
}
}
请注意,如果返回包含在一行中,则不需要括号。
Props
现在,我们有一个很酷的Table组件,但数据是硬编码的。 关于React的一个重大问题是它如何处理数据,它使用属性(称为props)和state来实现。 首先,我们将专注于使用道具处理数据。
首先,让我们从TableBody组件中删除所有数据。
const TableBody = () => {
return <tbody />
}
然后让我们将所有数据移动到一个对象数组,就好像我们引入了一个基于JSON的API一样。 我们必须在render()中创建这个数组。
class App extends Component {
render() {
const characters = [
{
name: 'Charlie',
job: 'Janitor',
},
{
name: 'Mac',
job: 'Bouncer',
},
{
name: 'Dee',
job: 'Aspring actress',
},
{
name: 'Dennis',
job: 'Bartender',
},
]
return (
<div className="container">
<Table />
</div>
)
}
}
现在,我们将使用属性将数据传递给子组件(Table),以及如何使用数据属性传递数据。 我们可以随意调用属性,只要它不是保留关键字,所以我将使用characterData。 我传递的数据是字符变量,我会在它周围加上花括号,因为它是一个JavaScript表达式。
return (
<div className="container">
<Table characterData={characters} />
</div>
)
现在数据正在传递给Table,我们必须努力从另一端访问它。
class Table extends Component {
render() {
const { characterData } = this.props
return (
<table>
<TableHeader />
<TableBody characterData={characterData} />
</table>
)
}
}
如果打开React DevTools并检查Table组件,您将在属性中看到数据数组。 这里存储的数据称为虚拟DOM,这是一种快速有效的方法,可以将数据与实际DOM同步。
但是,这些数据还没有在实际的DOM中。 在Table中,我们可以通过this.props访问所有属性。 我们只传递一个属性,通过characterData,所以我们将使用this.props.characterData来检索那些数据。
我将使用ES6属性简写来创建包含this.props.characterData的变量。
const { characterData } = this.props
由于我们的Table组件实际上由两个较小的简单组件组成,因此我将再次通过props将其传递给TableBody。
class Table extends Component {
render() {
const { characterData } = this.props
return (
<table>
<TableHeader />
<TableBody characterData={characterData} />
</table>
)
}
}
现在,TableBody不带任何参数并返回单个标签。
const TableBody = () => {
return <tbody />
}
我们将作为参数传递道具,并通过数组映射以返回数组中每个对象的表行。 此映射将包含在rows变量中,我们将其作为表达式返回。
const TableBody = props => {
const rows = props.characterData.map((row, index) => {
return (
<tr key={index}>
<td>{row.name}</td>
<td>{row.job}</td>
</tr>
)
})
return <tbody>{rows}</tbody>
}
如果您查看应用程序的前端,则现在正在加载所有数据。
你会注意到我已经为每个表行添加了一个键索引。 在React中制作列表时应始终使用键,因为它们有助于识别每个列表项。 我们还将在我们想要操作列表项的那一刻看到这是如何必要的。
属性是将现有数据传递给React组件的有效方法,但是组件不能更改属性 - 它们是只读的。 在下一节中,我们将学习如何使用state来进一步控制React中的处理数据。
State
现在,我们将字符数据存储在变量中的数组中,并将其作为道具传递。 这很好开始,但想象一下我们是否希望能够从数组中删除一个项目。 使用props,我们有单向数据流,但有了状态,我们可以更新组件中的私有数据。
您可以将状态视为应保存和修改的任何数据,而无需将其添加到数据库中 - 例如,在确认购买之前在购物车中添加和删除项目。
首先,我们将创建一个状态对象。
class App extends Component {
state = {}
}
该对象将包含您要在状态中存储的所有内容的属性。 对我们来说,这是角色。
class App extends Component {
state = {
characters: [],
}
}
将我们之前创建的整个对象数组移动到state.characters中。
class App extends Component {
state = {
characters: [
{
name: 'Charlie',
// the rest of the data
},
],
}
}
我们的数据正式包含在该状态。 由于我们希望能够从表中删除一个字符,因此我们将在父App类上创建一个removeCharacter方法。
要检索状态,我们将使用与以前相同的ES6方法获取this.state.characters。 要更新状态,我们将使用this.setState(),一种用于操作状态的内置方法。 我们将根据我们传递的索引过滤数组,并返回新数组。
您必须使用this.setState()来修改数组。 简单地将一个新值应用于this.state.property将不起作用。
removeCharacter = index => {
const { characters } = this.state
this.setState({
characters: characters.filter((character, i) => {
return i !== index
}),
})
}
filter不会变异,而是创建一个新数组,并且是在JavaScript中修改数组的首选方法。 这个特殊的方法是测试一个索引与数组中的所有索引,并返回除了传递的所有索引之外的所有索引。
现在我们必须将该函数传递给组件,并在每个可以调用该函数的字符旁边呈现一个按钮。 我们将removeCharacter函数作为prop传递给Table。
现在您应该了解状态如何初始化以及如何修改状态。
提交表格数据
现在我们将数据存储在状态中,我们可以从状态中删除任何项目。 但是,如果我们希望能够向州添加新数据呢? 在真实世界的应用程序中,您更可能从空状态开始并添加到其中,例如使用待办事项列表或购物车。
在其他任何事情之前,让我们从state.characters中删除所有硬编码数据,因为我们现在将通过表单更新。
class App extends Component {
state = {
characters: [],
}
}
现在让我们继续在名为Form.js的新文件中创建一个Form组件。 我们将创建一个类组件,并且我们将使用构造函数(),这是我们迄今尚未完成的。 我们需要构造函数()来使用它,并接收父级的道具。
我们将把Form的初始状态设置为具有一些空属性的对象,并将该初始状态分配给this.state。
Form.js
import React, { Component } from 'react'
class Form extends Component {
constructor(props) {
super(props)
this.initialState = {
name: '',
job: '',
}
this.state = this.initialState
}
}
我们对此表单的目标是每次在表单中更改字段时更新表单的状态,并且当我们提交时,所有数据将传递到App状态,然后将更新表。
首先,我们将创建每次对输入进行更改时运行的函数。 事件将被传递,我们将Form的状态设置为具有输入的名称(键)和值。
handleChange = event => {
const { name, value } = event.target
this.setState({
[name]: value,
})
}
在我们继续提交表单之前,让我们开始工作。 在渲染中,让我们从状态中获取两个属性,并将它们指定为与正确的表单键对应的值。 我们将把handleChange()方法作为输入的onChange运行,最后我们将导出Form组件。
render() {
const { name, job } = this.state;
return (
<form>
<label>Name</label>
<input
type="text"
name="name"
value={name}
onChange={this.handleChange} />
<label>Job</label>
<input
type="text"
name="job"
value={job}
onChange={this.handleChange} />
</form>
);
}
export default Form;
在App.js中,我们可以渲染表格下方的表格。
return (
<div className="container">
<Table characterData={characters} removeCharacter={this.removeCharacter} />
<Form />
</div>
)
现在,如果我们转到应用程序的前端,我们将看到一个尚未提交的表单。 更新一些字段,您将看到Form的本地状态正在更新。
最后一步是允许我们实际提交该数据并更新父状态。 我们将在App上创建一个名为handleSubmit()的函数,它将通过使用现有的this.state.characters并使用ES6扩展运算符添加新的字符参数来更新状态。
handleSubmit = character => {
this.setState({ characters: [...this.state.characters, character] })
}
让我们确保将它作为Form上的参数传递给我们。
<Form handleSubmit={this.handleSubmit} />
现在在Form中,我们将创建一个名为submitForm()的方法,该方法将调用该函数,并将Form状态作为我们之前定义的字符参数传递。 它还会将状态重置为初始状态,以便在提交后清除表单。
submitForm = () => {
this.props.handleSubmit(this.state)
this.setState(this.initialState)
}
最后,我们将添加一个提交按钮来提交表单。 我们使用onClick而不是onSubmit,因为我们没有使用标准提交功能。 点击将调用我们刚刚创建的submitForm。
<input type="button" value="Submit" onClick={this.submitForm} />
就是这样! 该应用程序已完成。 我们可以在表格中创建,添加和删除用户。 由于Table和TableBody已经从状态中拉出,它将正确显示。
引入API数据
React的一个非常常见的用法是从API中提取数据。 如果您不熟悉API是什么或如何连接到一个API,我建议您阅读如何使用JavaScript连接到API,它将引导您了解API以及如何将它们与vanilla JavaScript一起使用。
作为一个小测试,我们可以创建一个Api.js文件,并在那里创建一个新的应用程序。 我们可以测试的公共API是Wikipedia API,我在这里有一个URL端点用于随机*搜索。 您可以转到该链接查看API - 并确保在浏览器上安装了JSONView。
我们将使用JavaScript的内置Fetch从该URL端点收集数据并显示它。 您可以通过更改index.js中的URL来切换我们创建的应用程序和此测试文件 - 从’./Api’导入应用程序。
我不打算逐行解释这个代码,因为我们已经学会了通过状态数组创建组件,渲染和映射。 此代码的新方面是componentDidMount(),一个React生命周期方法。 生命周期是React中调用方法的顺序。 安装是指插入DOM的项目。
当我们引入API数据时,我们希望使用componentDidMount,因为我们希望在引入数据之前确保组件已呈现给DOM。 在下面的代码片段中,您将看到我们如何从Wikipedia API引入数据,并将其显示在页面上
import React, { Component } from 'react'
class App extends Component {
state = {
data: [],
}
// Code is invoked after the component is mounted/inserted into the DOM tree.
componentDidMount() {
const url =
'https://en.wikipedia.org/w/api.php?action=opensearch&search=Seona+Dancing&format=json&origin=*'
fetch(url)
.then(result => result.json())
.then(result => {
this.setState({
data: result,
})
})
}
render() {
const { data } = this.state
const result = data.map((entry, index) => {
return <li key={index}>{entry}</li>
})
return <ul>{result}</ul>
}
}
export default App
在本地服务器中保存并运行此文件后,您将看到DOM中显示的Wikipedia API数据。