跳到内容

a

React 简介

我们现在将开始入门的可能是本课程最重要的主题,即React库。让我们从制作一个简单的React应用开始,同时了解React的核心概念。

到目前为止,最简单的方法是使用一个叫做Vite的工具来开始。

让我们用create-vite创建一个新的应用:

npm create vite@latest

让我们按照下列方式回答create-vite提出的问题:

fullstack content

我们现在创建了一个名为part1的应用。如果我们在回答问题“Install with npm and start now?”时选“Yes”的话,create-vite还会自动安装需要的依赖并启动应用。但是我们打算手动执行这些步骤,这样我们可以看看他们是怎么做的。

接下来,让我们进入应用的目录并安装需要的库:

cd part1
npm install

该应用的打开方式如下

npm run dev

控制台显示该应用已在本地主机的5173端口运行,地址为http://localhost:5173/

fullstack content

Vite默认在端口5173启动应用。如果这个端口被占用,Vite使用下一个空闲端口号。

打开浏览器和文本编辑器,这样你就能在屏幕上同时看到代码和网页。

fullstack content

应用的代码位于src文件夹中。让我们简化默认代码,使文件main.jsx的内容如下所示:

import ReactDOM from 'react-dom/client'

import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

而文件App.jsx看起来是这样的:

const App = () => {
  return (
    <div>
      <p>Hello world</p>
    </div>
  )
}

export default App

文件App.cssindex.css和目录assets可以删除,因为我们现在的应用并不需要它们。

组件

文件App.jsx现在定义了一个名为AppReact组件。文件main.jsx的最后一行命令

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

将其内容渲染到div-元素中,该元素在文件public/index.html中定义,其id值为'root'。

默认情况下,文件index.html不包含任何我们在浏览器中可见的HTML标记:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>part1</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

你可以试着在该文件中添加一些HTML。但当使用React时,所有需要渲染的内容通常被定义为React组件。

让我们仔细看一下定义组件的代码:

const App = () => (
  <div>
    <p>Hello world</p>
  </div>
)

正如你可能猜到的,这个组件将被渲染成一个包裹着p-标签的div-标签,而p-标签包含文本Hello world

从技术角度来说,该组件被定义为一个JavaScript函数。下面是一个函数(它不接收任何参数):

() => (
  <div>
    <p>Hello world</p>
  </div>
)

然后这个函数被赋值给一个常量App

const App = ...

在JavaScript中定义函数有多种方法。这里我们将使用箭头函数,它是在较新的JavaScript版本ECMAScript 6,又称ES6中引入的。

因为函数只由一个表达式组成,所以我们使用了简写来表示这一段代码:

const App = () => {
  return (
    <div>
      <p>Hello world</p>
    </div>
  )
}

换句话说,该函数返回表达式的值。

定义该组件的函数可以包含任何种类的JavaScript代码。把你的组件修改成:

const App = () => {
  console.log('Hello from component')
  return (
    <div>
      <p>Hello world</p>
    </div>
  )
}

export default App

然后观察控制台中发生了什么

fullstack content

前端开发的第一规矩:

始终打开控制台

让我们一起重复一遍:我保证在课程中,以及在我接下来的人生中,在开发网页时始终打开控制台

也可以在一个组件内渲染动态内容。

将组件修改成:

const App = () => {
  const now = new Date()
  const a = 10
  const b = 20

  return (
    <div>
      <p>Hello world, it is {now.toString()}</p>
      <p>
        {a} plus {b} is {a + b}
      </p>
    </div>
  )
}

大括号内的任何JavaScript代码都会被计算,计算的结果会被嵌入到组件产生的HTML中的定义位置。

注意不要丢掉组件底下的这行代码

export default App

export语句在教材的大多数示例中都被省略了。但如果没有export语句,组件和整个应用都会无法运行。

还记得之前保证过始终打开控制台吗?去掉这一行后,控制台打印出了什么?

JSX

看起来React组件返回的是HTML标记。然而,事实并非如此。React组件的布局大多是用JSX编写的。虽然JSX看起来像HTML,但我们实际上是在写JavaScript。在底层,由React组件返回的JSX会被编译成JavaScript。

编译后,我们的应用如下所示:

const App = () => {
  const now = new Date()
  const a = 10
  const b = 20
  return React.createElement(
    'div',
    null,
    React.createElement(
      'p', null, 'Hello world, it is ', now.toString()
    ),
    React.createElement(
      'p', null, a, ' plus ', b, ' is ', a + b
    )
  )
}

编译是由Babel处理的。用Vite创建的项目会自动编译。我们将在本课程的第7章节中学习更多关于这个主题的内容。

也可以把React写成“纯JavaScript”而不使用JSX。虽然没有正常人会这样做的。

实际上,JSX很像HTML,区别在于通过JSX,你可以在大括号内编写适当的JavaScript来轻松嵌入动态内容。JSX的理念与许多模板语言非常相似,例如和Java Spring一起在服务端使用的Thymeleaf。

JSX是“类XML”语言,这意味着每个标签都需要关闭。例如,换行是一个空元素,在HTML中可以这样写:

<br>

但在编写JSX时,标签需要关闭:

<br />

多个组件

让我们修改App.js文件如下:

const Hello = () => {  return (    <div>      <p>Hello world</p>    </div>  )}
const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello />    </div>
  )
}

我们定义了一个新的组件Hello,并把它用在了App组件里。当然,一个组件可以多次使用:

const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello />
      <Hello />      <Hello />    </div>
  )
}

注意:在这些示例以及将来的示例中,底部的export部分被省略。但它仍然是代码正常运行所必须的

用React编写组件是很容易的,通过组合组件,即使是比较复杂的应用也可以保持相当的可维护性。事实上,React的一大核心理念就是将应用由许多专门的可重复使用的组件组成。

另一个强制的惯例是在应用的组件树的顶端有一个叫做App根组件。然而,我们将在第6章节中讲到,有些情况下,App实际上并不是根组件,而是被包裹在一个适当的实用组件中。

props:向组件传递数据

可以使用所谓的props向组件传递数据。

让我们将Hello组件修改如下:

const Hello = (props) => {  return (
    <div>
      <p>Hello {props.name}</p>    </div>
  )
}

现在定义组件的函数有一个参数props。作为一个参数,该参数接收一个对象,其字段对应于组件使用者定义的所有“props”。

props的定义如下:

const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="George" />      <Hello name="Daisy" />    </div>
  )
}

props的数量可以是任意的,它们的值可以是“硬编码”的字符串或JavaScript表达式的结果。如果props的值是通过JavaScript得到的,props必须包裹在大括号中。

让我们修改代码,让Hello组件使用两个props:

const Hello = (props) => {
  console.log(props)  return (
    <div>
      <p>
        Hello {props.name}, you are {props.age} years old      </p>
    </div>
  )
}

const App = () => {
  const name = 'Peter'  const age = 10
  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Maya" age={26 + 10} />      <Hello name={name} age={age} />    </div>
  )
}

App组件发送的props有变量的值、表达式的计算结果和普通的字符串。

Hello组件还会将对象props的值打印到控制台上。

我真心希望你的控制台是开着的。否则,记住之前保证过的:

我保证在课程中,以及在我接下来的人生中,在开发网页时始终打开控制台

软件开发并非易事。如果不利用所有可用的工具,比如网页控制台和console.log打印的调试信息,那就更难了。专业人士始终都用这两样工具。对初学者来说,没有任何理由不用这些美妙的工具来让生活更轻松。

可能遇见的错误信息

如果你的项目装的React版本是18或更早的,你可能会在此时遇到这样的报错信息:

fullstack content

这实际上并不是个错误,只是工具ESLint产生的警告。你可以在文件eslint.config.js中添加下面这一行来关闭react/prop-types警告

export default [
  { ignores: ['dist'] },
  {
    files: ['**/*.{js,jsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      parserOptions: {
        ecmaVersion: 'latest',
        ecmaFeatures: { jsx: true },
        sourceType: 'module',
      },
    },
    settings: { react: { version: '18.3' } },
    plugins: {
      react,
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...js.configs.recommended.rules,
      ...react.configs.recommended.rules,
      ...react.configs['jsx-runtime'].rules,
      ...reactHooks.configs.recommended.rules,
      'react/jsx-no-target-blank': 'off',
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      'react/prop-types': 0,    },
  },
]

我们将在第3章节详细介绍到ESLint。

一些注意事项

React已经能生成相当清晰的错误信息。尽管如此,你仍应该以非常小的步骤前进,来确保每一个改变都能如愿以偿,至少在初学的时候如此。

控制台应始终打开。如果浏览器报告错误,不建议继续写更多的代码,寄希望有奇迹出现。相反,你应该试着理解错误的原因,然后比如说回到之前的工作状态。

fullstack content

我们之前提到过,在编写React时,在代码中写console.log()命令(打印到控制台)是可行的,也是值得的。

还要记住,React组件名称的首字母必须大写。如果你尝试这样定义一个组件:

const footer = () => {
  return (
    <div>
      greeting app created by <a href="https://github.com/mluukkai">mluukkai</a>
    </div>
  )
}

然后这样使用它

const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Maya" age={26 + 10} />
      <footer />    </div>
  )
}

页面不会显示在Footer组件中定义的内容,相反,React只会创建一个空的footer元素,即HTML内置的元素,而不是同名的自定义React元素。如果你把组件名称的第一个字母改为大写字母,那么React就会创建一个定义在Footer组件中的div-元素,并在页面上渲染。

注意,React组件的内容(通常)需要包含一个根元素。比如如果我们试图定义没有最外层的div元素的App组件:

const App = () => {
  return (
    <h1>Greetings</h1>
    <Hello name="Maya" age={26 + 10} />
    <Footer />
  )
}

结果一条错误信息。

fullstack content

使用根元素并不是唯一可行的选择。一个组件的数组也是一个有效的解决方案:

const App = () => {
  return [
    <h1>Greetings</h1>,
    <Hello name="Maya" age={26 + 10} />,
    <Footer />
  ]
}

然而,在定义应用的根元素时,这种做法并不特别明智,并且这样做使代码看起来有点难看。

由于根元素是强制规定的,我们在DOM树中有“额外的”div元素。我们可以通过使用Fragment来避免“额外的”div元素,即用一个空元素来包装组件要返回的元素:

const App = () => {
  const name = 'Peter'
  const age = 10

  return (
    <>
      <h1>Greetings</h1>
      <Hello name="Maya" age={26 + 10} />
      <Hello name={name} age={age} />
      <Footer />
    </>
  )
}

现在编译成功了,由React生成的DOM也不再包含额外的div元素。

不要渲染对象

考虑一个将我们朋友的姓名和年龄打印到屏幕上的应用:

const App = () => {
  const friends = [
    { name: 'Peter', age: 4 },
    { name: 'Maya', age: 10 },
  ]

  return (
    <div>
      <p>{friends[0]}</p>
      <p>{friends[1]}</p>
    </div>
  )
}

export default App

可是,屏幕上什么都没有。我在代码里找问题找了15分钟,但是我还是不知道哪里出了问题。

我终于想起来我们之前保证过的

我保证在课程中,以及在我接下来的人生中,在开发网页时始终打开控制台

控制台飘着红色的文字:

fullstack content

问题的核心在于对象不是有效的React子组件,也就是说应用尝试渲染对象但失败了。

代码尝试这样渲染一个朋友的信息

<p>{friends[0]}</p>

但出了问题,因为大括号中要渲染的东西是一个对象。

{ name: 'Peter', age: 4 }

在React里,在大括号中渲染的每个东西都必须是原始值,比如数字或字符串。

修好的代码如下:

const App = () => {
  const friends = [
    { name: 'Peter', age: 4 },
    { name: 'Maya', age: 10 },
  ]

  return (
    <div>
      <p>{friends[0].name} {friends[0].age}</p>
      <p>{friends[1].name} {friends[1].age}</p>
    </div>
  )
}

export default App

现在朋友的名字分别在大括号中渲染

{friends[0].name}

年龄亦然

{friends[0].age}

在改正错误后,你应该按🚫来清除控制台的错误,然后刷新页面并确保没有显示新的错误信息。

还有一个小小的注意事项。React是支持渲染数组的,只要数组中的每个元素都能渲染(比如数字或字符串)。所以下面这个程序是能运行的,虽然结果可能不是我们想要的:

const App = () => {
  const friends = [ 'Peter', 'Maya']

  return (
    <div>
      <p>{friends}</p>
    </div>
  )
}

在这一章节中,还不需要用到表格的直接渲染,我们会在下一章节中提起。