a

从渲染集合到模块学习

在新的章节开始之前,让我们回顾一下去年的课程中认为是难点的一些话题。

console.log

What's the difference between an experienced JavaScript programmer and a rookie? The experienced one uses console.log 10-100 times more.

一个JavaScript 老鸟和菜鸟有什么区别? 老鸟使用 console.log的次数是菜鸟的数十倍甚至数百倍。

矛盾的是,实际上,菜鸟比老鸟更需要 console.log (或任何其他调试方法)。

当某些事情不能正常工作时,不要只是猜测错误,而应记录或使用其他调试方法。

注意:如前一章所说的,当你使用 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 的代码片段】

使用 Visual Studio Code能够很容易创建“代码片段(snippets)” ,即快速生成常用代码块的快捷方式,很像 Netbeans 中的“ sout”。

创建代码片段的说明可以在这里找到 here.。

有用的、现成的代码片段也可以在 VS 代码插件中找到,在插件市场.

最重要的片段是用于 console.log() 命令的片段,例如clog:

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

使用 console.log() 来debug 你的代码十分常见, Visual Studio Code 有内置的snippet。可以使用 log 和tab键来自动补全

JavaScript Arrays

【JavaScript 数组】

从现在开始,我们将一直使用 JavaScript 数组的函数式编程方法,比如 find, filter, 和 map。 它们和 Java 8中的streams 一样遵循一般原则,这些原则在过去几年里被用在大学计算机科学系的 Ohjelmoinnin perusteet 和 Ohjelmoinnin jatkokurssi 课程,以及 MOOC 编程中。

如果使用数组的函数式编程对你来说感觉很陌生,那么至少可以看看 YouTube 视频系列的前三部分 Functional Programming in JavaScript

Event handlers revisited

【事件处理复习】

基于去年的课程,事件处理证明是一个难点内容。

如果你觉得自己关于这个议题的知识需要复习一下,那么应该阅读上一章节结尾的复习章节 事件处理复习

将事件处理传递给App 组件的子组件引发了一些问题。 关于这个议题的一个小复习在这里

Rendering Collections

【渲染集合】

现在,我们将在 React 中为类似于 第0章中的示例应用,编写“前端”或叫浏览器端的应用逻辑。

注意:为了统一翻译上下文,从现在开始,我将按照如下约定翻译。

  • Note 应用实际上是在创建一个和提醒、便笺相关的应用,因此以下的Note均翻译为便笺。

让我们从如下代码开始( App.js):

import React from 'react'

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 ReactDOM from 'react-dom'
import App from './App.js'

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.render(
  <App notes={notes} />,
  document.getElementById('root')
)

每个便笺都包含其文本内容、时间戳以及一个布尔值,用于标记该便笺是否重要,便笺还包含一个惟一的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>,
]

然后可以把这些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

【Key-属性】

尽管该应用似乎运行良好,但在控制台上有一个烦人的警告:

fullstack content

正如错误消息中的链接 React page 所建议的,列表项,即 map 方法生成的每个元素,都必须有一个唯一的键值: 一个名为key 的属性。

让我们添加上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 使用数组中对象的key属性来确定组件在重新渲染时,如何更新组件生成的视图。 更多的说明在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
}

该函数获取一个 note 对象作为参数,然后返回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

【重构模块】

让我们把代码整理一下。 我们只对props的字段 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 标签定义key 属性。

可以在单个文件中编写整个 React 应用。 虽然实践中很少这么用。 通常的做法是将每个组件在其自己的文件中,声明为一个ES6-模块

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

import ReactDOM from 'react-dom'
import App from './App.js'

为了让它们能够在代码中使用,就import 了两个模块: React 模块被放入一个名为 React 的变量中, React-DOM 模块放到了 ReactDOM 变量中。

为了让它们能够在代码中使用,就import 了两个模块: react-dom 模块放到了 ReactDOM 变量中,主应用的模块被放入一个名为 App 的变量中。

让我们将我们的Note 组件移动到它自己的模块中。

在较小型的应用中,组件通常放在一个名为components 的目录中,而这个components目录又放在src 目录中。 约定是:按照组件的名称来命名文件。

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

Note.js 文件的内容如下:

import React from 'react'

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

export default Note

由于这是一个 React-组件,因此我们必须导入 React。

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

现在使用这个组件的文件,即App.js,可以 import 这个模块了:

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

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

注意,当导入我们自己的组件时,它们的位置必须给出导入文件相对路径:

'./components/Note'

开头的 句点 指的是当前工作目录,因此模块的位置是当前components 的子目录中的一个名为Note.js 的文件。 文件扩展名(.js)可以省略。

除了能使组件声明能够分离到它们自己的文件中之外,模块还有许多其他用途。 我们将在本课程稍后讨论这些问题。

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

注意,仓库的主分支包含应用的后续版本的代码。 当前的代码在分支 part2-1中:

fullstack content

如果您克隆了项目,请在启动应用之前运行命令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>
  )
}

通常,问题的根源在于,props的类型不同,或者使用了与实际名称不同的名称调用,导致结果解构失败。 解决问题的开始通常是去掉解构的方式,来看看 props 中到底包含什么。

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

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

在下一个问题完全崩掉之前(由于 props 的类型错误),我不得不用 console.log 来debug,于是我将这一章节加到了教材中