跳到内容

a

从渲染集合到模块学习

在开始新的部分之前,让我们回顾一下去年被证明是较难的一些主题。

console.log

一个有经验的JavaScript程序员和一个菜鸟之间有什么区别?有经验的老鸟使用console.log的次数要多10-100倍

矛盾的是,一个菜鸟程序员应该比一个有经验的程序员更需要console.log(或任何调试方法),这也是事实。

当某些东西不工作时,不要只是猜测什么是错的。相反,要记录或使用一些其他的调试方法。

NB 正如第一章节所说的,当你使用console.log命令进行调试时,不要用 "Java式 "的加号来连接。不要写:

console.log('props value is' + props)

而要用逗号分开要打印的东西:

console.log('props value is', props)

如果你把一个对象和一个字符串连接起来并记录到控制台(就像我们的第一个例子),其结果将无法使用:

props value is [Object object]

相反,当你把对象作为不同的参数用逗号隔开传递给console.log,就像我们上面的第二个例子,对象的内容被打印到开发者控制台,成为可检查的字符串。

如果有必要,请阅读更多关于调试React-应用的内容。

Protip: Visual Studio Code snippets

通过Visual Studio Code,可以很容易地创建"'片段',即快速生成常用的重复使用的代码部分,很像Netbeans中的 "sout "工作方式。

创建片段的说明可以查看这里

有用的、现成的片段也可以在市场中作为VS Code插件找到。

最重要的片段是用于console.log()命令的片段,例如,clog。这可以这样创建。

{
  "console.log": {
    "prefix": "clog",
    "body": [
      "console.log('$1')",
    ],
    "description": "Log output to console"
  }
}

使用console.log()来调试你的代码非常普遍,因此Visual Studio Code内置了这个片段。要使用它,请输入log并点击tab来自动完成。更多功能的console.log()片段扩展可以在市场中找到。

JavaScript Arrays

从这里开始,我们将使用JavaScript数组的函数式编程方法,例如findfiltermap--我们将一直这么使用。它们的操作原理与Java 8中的流相同,在过去几年中,在大学计算机系的"Ohjelmoinnin perusteet"和"Ohjelmoinnin jatkokurssi" 课程以及编程MOOC中都有使用。

如果你对用数组进行函数式编程感到陌生,那么至少可以看看YouTube视频系列JavaScript中的函数式编程的前三部分。

Event Handlers Revisited

基于去年的课程,事件处理有些难度。

如果你觉得自己在这个主题上的知识需要精进一下的话,那么值得读一读上一章节复习事件处理程序最后的修订章节。

将事件处理程序传递给App组件的子组件,引起了一些问题。关于这个话题的小复习可以在这里找到。

Rendering Collections

我们现在将在React中为一个类似于第0章节中的示例程序做'前端',或浏览器端应用逻辑。

让我们从下面开始(文件App.js)。

const App = (props) => {
  const { notes } = props

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        <li>{notes[0].content}</li>
        <li>{notes[1].content}</li>
        <li>{notes[2].content}</li>
      </ul>
    </div>
  )
}

export default App

文件index.js如下所示:

import React from 'react'
import ReactDOM from 'react-dom/client'

import App from './App'

const notes = [
  {
    id: 1,
    content: 'HTML is easy',
    date: '2019-05-30T17:30:31.098Z',
    important: true
  },
  {
    id: 2,
    content: 'Browser can execute only JavaScript',
    date: '2019-05-30T18:39:34.091Z',
    important: false
  },
  {
    id: 3,
    content: 'GET and POST are the most important methods of HTTP protocol',
    date: '2019-05-30T19:20:14.298Z',
    important: true
  }
]

ReactDOM.createRoot(document.getElementById('root')).render(
  <App notes={notes} />
)

每个笔记都包含它的文本内容和一个时间戳,以及一个用于标记该笔记是否被归类为重要的 布尔值,还有一个唯一的id

上面的例子之所以有效,是因为数组中正好有三个笔记。

通过引用一个硬编码的索引号来访问数组中的对象,从而渲染一个单一的笔记。

<li>{notes[1].content}</li>

这当然不实用。我们可以通过使用map函数从数组对象中生成React元素来改进这一点。

notes.map(note => <li>{note.content}</li>)

其结果是一个li元素的数组。

[
  <li>HTML is easy</li>,
  <li>Browser can execute only JavaScript</li>,
  <li>GET and POST are the most important methods of HTTP protocol</li>,
]

然后可以放在ul标签内。

const App = (props) => {
  const { notes } = props

  return (
    <div>
      <h1>Notes</h1>
      <ul>        {notes.map(note => <li>{note.content}</li>)}      </ul>    </div>
  )
}

因为生成li标签的代码是JavaScript,它必须像其他JavaScript代码一样被包裹在JSX模板的大括号内。

我们还通过把箭头函数的声明分成多行来使代码更容易阅读。

const App = (props) => {
  const { notes } = props

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note =>
          <li>            {note.content}          </li>        )}
      </ul>
    </div>
  )
}

Key-attribute

尽管应用似乎在工作,但控制台有一个讨厌的警告。

fullstack content

正如错误信息中链接的React页面所提示的;列表项,即由map方法生成的元素,必须有一个唯一的键值:一个叫做key的属性。

我们来添加键值。

const App = (props) => {
  const { notes } = props

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note =>
          <li key={note.id}>            {note.content}
          </li>
        )}
      </ul>
    </div>
  )
}

错误信息就消失了。

React使用数组中对象的键属性来决定如何在组件重新渲染时更新该组件生成的视图。关于这一点,更多的可以查看React文档

Map

了解数组方法,map是如何工作的,对课程剩下的章节至关重要。

应用包含一个名为notes的数组。

const notes = [
  {
    id: 1,
    content: 'HTML is easy',
    date: '2019-05-30T17:30:31.098Z',
    important: true
  },
  {
    id: 2,
    content: 'Browser can execute only JavaScript',
    date: '2019-05-30T18:39:34.091Z',
    important: false
  },
  {
    id: 3,
    content: 'GET and POST are the most important methods of HTTP protocol',
    date: '2019-05-30T19:20:14.298Z',
    important: true
  }
]

让我们停顿一下,研究一下map是如何工作的。

如果下面的代码被添加到,比方说,文件的末端。

const result = notes.map(note => note.id)
console.log(result)

[1, 2, 3] 会被打印出来

map总是创建一个新的数组,其中的元素是通过mapping从原始数组的元素中创建的:原始值作为map方法函数的参数。

而这个函数就是

note => note.id

这是一个简洁形式写的箭头函数。完整的形式应该是:

(note) => {
  return note.id
}

该函数得到一个笔记对象作为参数,并返回id字段的值。

把命令改成:

const result = notes.map(note => note.content)

结果是一个包含笔记内容的数组。

这已经非常接近我们使用的React代码了。

notes.map(note =>
  <li key={note.id}>{note.content}</li>
)

它生成一个li标签,包含每个笔记对象的笔记内容。

因为函数参数是传递给 map 方法 -

note => <li key={note.id}>{note.content}</li>

 - 是用来创建视图元素的,变量的值必须被渲染在大括号内。试着看看如果去掉大括号会发生什么。

大括号的使用在一开始会造成一些痛苦,但你很快就会习惯。React的视觉反馈是即时的。

Anti-pattern: Array Indexes as Keys

我们可以通过使用数组索引作为键来使控制台中的错误信息消失。通过向map方法的回调函数传递第二个参数,可以拿到索引。

notes.map((note, i) => ...)

当这样调用时,i被分配为笔记所在的数组中的索引值。

因此,定义行的生成也是不出错的一种方法:

<ul>
  {notes.map((note, i) =>
    <li key={i}>
      {note.content}
    </li>
  )}
</ul>

然而,这是不推荐的,即使它看起来工作得很好,也会产生想不到的问题。

这篇文章中可以阅读更多关于这个问题的内容。

Refactoring Modules

让我们把代码整理一下。我们只对prop的字段notes感兴趣,所以让我们直接使用解构来检索它。

const App = ({ notes }) => {  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note =>
          <li key={note.id}>
            {note.content}
          </li>
        )}
      </ul>
    </div>
  )
}

如果你忘记了解构是什么意思以及它是如何工作的,请复习一下一下解构部分

我们将把显示一个笔记分离成独立的组件Note

const Note = ({ note }) => {  return (    <li>{note.content}</li>  )}
const App = ({ notes }) => {
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note =>          <Note key={note.id} note={note} />        )}      </ul>
    </div>
  )
}

注意,现在必须为Note组件定义key属性,而不是像以前那样为li标签定义。

整个React应用可以写在一个文件中。虽然这当然不是很实用。通常的做法是将每个组件作为ES6-module在自己的文件中声明。

我们一直都在使用模块。文件index.js的前几行。

import React from 'react'
import ReactDOM from 'react-dom/client'

import App from './App'

import了三个模块,使它们可以在该文件中使用。模块React被放入变量React,模块react-dom被放入变量ReactDOM,而定义应用主要组件的模块被放入变量App

让我们把我们的Note组件移到自己的模块中。

在较小的应用中,组件通常被放在一个叫做components的目录中,而这个目录又被放在src目录中。惯例是用组件的名字来命名文件。

现在,我们将为我们的应用创建一个名为components的目录,并在其中放置一个名为Note.js的文件。

Note.js文件的内容如下。

import React from 'react'

const Note = ({ note }) => {
  return (
    <li>{note.content}</li>
  )
}

export default Note

模块的最后一行 exports 声明的模块,即变量Note

现在使用该组件的文件--App.js--可以导入 该模块。

import Note from './components/Note'
const App = ({ notes }) => {
  // ...
}

模块导出的组件现在可以在变量Note中使用,就像之前那样。

注意,当导入我们自己的组件时,必须给出它们的位置,与导入的文件位置有关

'./components/Note'

开头的句号--.--指的是当前目录,所以模块的位置是当前目录下components子目录中一个叫Note.js的文件。文件名的扩展名.js可以省略。

除了使组件声明分离到自己的文件中,模块还有很多其他的用途。我们将在本课程的后面再学习它们。

该应用的当前代码可以在GitHub上找到。

请注意,仓库的main分支包含了该应用的后期版本的代码。目前的代码在分支part2-1中。

fullstack content

如果你克隆了这个项目,在用npm run dev启动应用之前,运行npm install命令。

When the Application Breaks

在你的编程生涯的早期(甚至编码30年后),经常发生的情况是,应用就是完全崩溃了。对于动态类型的语言,如JavaScript,这种情况就更多了,因为编译器不会检查数据类型。例如,函数变量或返回值。

例如,一个 "React崩了 "可能是这样的。

fullstack content

在这些情况下,你最好的办法是使用console.log方法。

引起崩溃的那段代码是这样的。

const Course = ({ course }) => (
  <div>
    <Header course={course} />
  </div>
)

const App = () => {
  const course = {
    // ...
  }

  return (
    <div>
      <Course course={course} />
    </div>
  )
}

我们将通过在代码中加入console.log命令来探寻故障的原因。因为首先要渲染的是App组件,所以应当把第一个console.log放在那里。

const App = () => {
  const course = {
    // ...
  }

  console.log('App works...')
  return (
    // ..
  )
}

要看到控制台中的打印内容,我们必须在长长的红色错误墙上滚动鼠标。

fullstack content

当发现日志是有效的,就该深入打日志了。如果组件被声明为一个单一的语句,或一个没有返回的函数,就会使打印到控制台的难度增加。

const Course = ({ course }) => (
  <div>
    <Header course={course} />
  </div>
)

该组件可以改为较长的形式,以便我们增加打印功能。

const Course = ({ course }) => {
  console.log(course)  return (
    <div>
      <Header course={course} />
    </div>
  )
}

问题的根源往往是prop被期望为不同的类型,或者用不同的名字调用,其结果就是解构失败。当去掉重构,我们看到props实际包含的内容时,问题往往会开始自行解决。

const Course = (props) => {  console.log(props)  const { course } = props
  return (
    <div>
      <Header course={course} />
    </div>
  )
}

如果问题仍然没有得到解决,除了继续通过在你的代码周围撒上更多的console.log语句来寻找错误之外,真的没有什么可做的。

在下一个问题的模型答案完全崩掉之前(由于prop的类型不对),我不得不用console.log来调试它之后,我把这一章加入了教材。