现代Web编程-与服务端通信

教程链接:全栈公开课2020

一.从渲染集合到模块学习

console.log

使用 console.log 命令进行调试时,不要用Java的方式,将所有东西用'+'连在一起。即不要这么写:

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

如果你把一个对象和一个字符串(用加号)连接起来,然后把它记录到控制台上(就像上面第一个例子那样) ,结果将是相当没有用的:
props value is [Object object]
而应该用逗号把要打印的东西分开:

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

对象的内容将作为有意义的字符串打印到开发者控制台中。

高级技巧: Visual Studio Code 的代码片段

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

有用的、现成的代码片段也可以在 VS 代码插件中找到,例如这里.

渲染集合

让我们从如下代码开始:

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

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
  }
]

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

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

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

由于生成li 标签的代码是 JavaScript,所以就要像所有其他 JavaScript 代码一样,在 JSX 模板中使用花括号来包装它。

Key-属性

尽管该应用似乎运行良好,但在控制台上有一个烦人的警告:
warning
React 使用数组中对象的key属性来确定组件在重新渲染时,如何更新组件生成的视图。列表项,即 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>
  )
}

错误就消失了。

反模式: 将数组的索引作为键

通过使用数组的索引作为键,我们可以使控制台上的错误消息消失。然而,这是不推荐的,因为可能导致意想不到的问题,即使它似乎能正常工作。

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

更多内容请点击这里

重构模块

我们将单独显示一个便笺到它自己的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-模块。
现在,我们将为应用创建一个名为components 的目录,并在其中放置一个名为Note.js 的文件。

Note.js 文件的内容如下:

import React from 'react'

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

export default Note

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

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

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

import React from 'react'
import ReactDOM from 'react-dom'
import Note from './components/Note'

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

App也是一个组件,所以让我们在它自己的模块中声明它。 因为它是应用的根组件,所以我们将它放在 src 目录中。 文件内容如下:

import React from 'react'
import Note from './components/Note'

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


export default App

index.js 文件剩下的内容是:

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

const notes = [
  // ...
]

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

当应用挂掉了

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

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

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

二.表单

让我们继续扩展我们的应用,允许用户添加新的便笺。

为了让我们的页面在添加新便笺时更新,最好将便笺存储在App 组件的状态中。 让我们导入useState函数,并使用它定义一个状态,这个状态用props传进来的初始便笺数组作为状态初始化。

import React, { useState } from 'react'
import Note from './components/Note'

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)

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

export default App 

该组件使用 useState 函数来初始化状态,该状态用props传进来的note 数组作为初始状态,保存到notes中。
接下来,让我们在组件中添加一个 HTML 表单 ,用于添加新的便笺。

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)

  const addNote = (event) => {
    event.preventDefault()
    console.log('button clicked', event.target)
  }

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>
        <input />
        <button type="submit">save</button>
      </form>   
    </div>
  )
}

事件处理立即调用event.preventDefault()方法,它会阻止提交表单的默认操作。 因为默认操作会导致页面重新加载。
但我们如何访问表单中input 元素中包含的数据呢?

有许多方法可以实现这一点; 我们将介绍的第一种方法是使用所谓的受控组件。

让我们添加一个名为 newNote 的新状态,用于存储用户提交的输入,让我们将它设置为input 元素的value 属性;
另外,为了能够编辑 input 元素,我们必须注册一个事件处理 来同步对 input 所做的更改和组件的状态:

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState(
    'a new note...'
  ) 

  const addNote = (event) => {
    event.preventDefault()
    console.log('button clicked', event.target)
  }

  const handleNoteChange = (event) => {
    console.log(event.target.value)
    setNewNote(event.target.value)
  }

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleNoteChange}
        />
        <button type="submit">save</button>
      </form>   
    </div>
  )
}

现在App 组件的 newNote 状态反映了输入的当前值,这意味着我们可以完成 addNote 函数来创建新的便笺:

const addNote = (event) => {
  event.preventDefault()
  const noteObject = {
    content: newNote,
    date: new Date().toISOString(),
    important: Math.random() < 0.5,
    id: notes.length + 1,
  }

  setNotes(notes.concat(noteObject))
  setNewNote('')
}

使用数组的 concat 方法添加新便笺到便笺列表中,该方法不会改变原始的 notes 状态数组,而是会创建数组的一个新副本,并将新项添加到尾部。 这很重要,因为我们绝不能在React中直接改变状态

过滤显示的元素

让我们为我们的应用添加一些新的功能,允许我们只查看重要的便笺。

让我们在App 组件中添加一个状态,用于同步应该显示哪些便笺;
让我们更改组件,以便它存储要显示在 notesToShow 变量中的所有便笺的列表。 列表中的项取决于组件的状态:

import React, { useState } from 'react'
import Note from './components/Note'

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState('') 
  const [showAll, setShowAll] = useState(true)

  // ...

  const notesToShow = showAll
    ? notes
    : notes.filter(note => note.important === true)

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notesToShow.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
      // ...
    </div>
  )
}

在 JavaScript 中,val1 == val2 并不能在所有情况下都像预期的那样工作,在比较中使用专门的val1 === val2更安全。 你可以在这里here阅读更多关于这个议题的描述 。

接下来,让我们添加一些功能,使用户能够从用户界面切换应用的 showAll状态。

有关修改如下:

import React, { useState } from 'react' 
import Note from './components/Note'

const App = (props) => {
  const [notes, setNotes] = useState(props.notes) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)

  // ...

  return (
    <div>
      <h1>Notes</h1>
      <div>
        <button onClick={() => setShowAll(!showAll)}>
          show {showAll ? 'important' : 'all' }
        </button>
      </div>
      <ul>
        {notesToShow.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
      // ...        
    </div>
  )
}

显示便笺的方式(显示所有 还是 显示重要)由一个按钮控制。

三.从服务器获取数据

让我们使用一个在开发过程中使用的工具,称为JSON 服务器 ,作为我们的服务器。

在项目的根目录中创建一个名为db.json 的文件,其内容如下:

{
  "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
    }
  ]
}

使用命令npm install -g json-server安装 JSON 服务器。 全局安装不是必须的,因为我们可以在应用的根目录使用 npx 命令运行json-server:
npx json-server --port 3001 --watch db.json
默认情况下,json-server在端口3000上启动; 但是由于 create-react-app 项目设置了3000端口,因此我们指定定义为3001。

在浏览器中输入地址 http://localhost:3001/notes。 我们可以看到JSON-server 以 JSON 格式提供了我们之前写到文件的便笺。
如果你的浏览器无法格式化 json 数据的显示,可以安装一个合适的插件,例如JSONView

在现实世界中,数据会存储在某种数据库中。 然而,json-server 是一个方便的工具,可以在开发阶段使用服务器端功能,而不需要编写任何程序。

浏览器作为一个运行时环境

我们的第一个任务是从地址 http://localhost:3001/notes 获取已经存在的便笺到 React 应用。

使用 XHR已经不再推荐了,而且浏览器已经广泛支持基于所谓的promisesfetch方法,而不是 XHR 使用的事件驱动模型。

目前,JavaScript 引擎是单线程的,这意味着它们不能并行执行代码。 因此,在实践中需要使用非阻塞模型来执行 IO 操作。 否则,浏览器将在从服务器获取数据时“冻结(卡住)”。

在当今的浏览器中,可以在所谓的web workers的帮助下运行并行化的代码。 然而,单个浏览器窗口的事件循环仍然是由一个单线程处理。

npm

我们可以使用前面提到的基于承诺promise的fetch函数从服务器中获取数据。 fetch是一个很好的工具。 它是标准化的,所有现代浏览器(不包括 IE,因为它不是)都支持它。

我们将使用axios库来代替浏览器和服务器之间的通信。 它的功能类似于fetch,但是使用起来更友好。
几乎所有的 JavaScript 项目都是使用node包管理器定义的,也就是npm
从命令行安装axios:

npm install axios --save

注意: npm-commands 应该始终在项目根目录中运行,在这个目录中可以找到package.json 文件。

Axios 现在被包含在依赖中了:

{
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.4.0",
    "@testing-library/user-event": "^7.2.1",
    "axios": "^0.19.2",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-scripts": "3.3.0"
  },
  // ...
}

让我们做另一个补充,通过执行如下命令将json-server 安装为开发依赖项(仅在开发过程中使用) :

npm install json-server --save-dev

在package.json 文件的scripts部分添加一个小的修改:

{
  // ... 
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "server": "json-server -p3001 --watch db.json"
  },
}

现在,我们可以在没有参数定义的情况下方便地使用如下命令从项目根目录启动 json-server:

npm run server

我们使用了两次 npm 安装命令,但是有一点不同:

npm install axios --save
npm install json-server --save-dev

参数之间有细微的差别。axios被安装为应用的运行时依赖项(-- save) ,因为程序的执行需要库的存在。 而另一个,json-server是作为开发依赖项(-- save-dev)安装的,因为程序本身并不需要它。 它用于在软件开发过程中提供帮助。

Axios and promises

现在我们可以使用 axios 了。将如下内容添加到文件index.js 中:

import axios from 'axios'

const promise = axios.get('http://localhost:3001/notes')
console.log(promise)

const promise2 = axios.get('http://localhost:3001/foobar')
console.log(promise2)

Axios 的 get 方法会返回一个promise
promise 是一个表示异步操作的对象,它可以有三种不同的状态:

1.The promise is pending提交中: 这意味着最终值(下面两个中的一个)还不可用。

2.The promise is fulfilled兑现: 这意味着操作已经完成,最终的值是可用的,这通常是一个成功的操作。 这种状态有时也被称为resolve。

3.The promise is rejected拒绝:它意味着一个错误阻止了最终值,这通常表示一个失败操作。

示例中的第一个承诺是fulfilled,表示一个成功的请求。 而第二个是rejected,控制台告诉我们原因。 看起来我们试图向一个不存在的地址发出了 HTTP GET 请求。

如果我们想要访问承诺表示的操作的结果,那么必须为承诺注册一个事件处理。 这是通过 then方法实现的。通常没有必要将 promise 对象存储在一个变量中,而将 then方法调用链到 axios 方法调用是很常见的,因此它可以直接跟在 axios 方法调用后面:

axios.get('http://localhost:3001/notes').then(response => {
  const notes = response.data
  console.log(notes)
})

现在终于可以开始使用从服务器获取的数据了。但是,命令 axios.get 应该放在组件中的哪个位置,这一点并不明显。

Effect-hooks

我们已经使用了React version 16.8.0引入的state hooks,它为 React 组件提供了定义为函数的状态,也就是所谓的函数式组件。 16.8.0版本的React还引入了effect hooks新特性,是从服务器获取数据时使用的正确工具。
App组件更改如下:

import React, { useState, useEffect } from 'react'
import axios from 'axios' 
import Note from './components/Note'

const App = () => {
  const [notes, setNotes] = useState([]) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)

  useEffect(() => {
    console.log('effect')
    axios
      .get('http://localhost:3001/notes')
      .then(response => {
        console.log('promise fulfilled')
        setNotes(response.data)
      })
  }, [])
  console.log('render', notes.length, 'notes')

  // ...
}

函数 useEffect 实际上需要两个参数 。第一个是函数本身。 默认情况下,effects 在每次渲染完成后运行,但是你可以选择只在某些值发生变化时才调用。
useEffect的第二个参数用于指定effect运行的频率。 如果第二个参数是一个空数组[],那么这个effect只在组件的第一次渲染时运行。

开发的运行时环境

我们整个应用的配置已经逐渐变得更加复杂。 让我们回顾一下发生了什么,在哪里发生的。 下图描述了应用的组成
application
构成我们的 React 应用的 JavaScript 代码在浏览器中运行。 浏览器从React dev server 获取 JavaScript,这是运行 npm start 命令后运行的应用。 dev-server 将 JavaScript 转换成浏览器可以理解的格式。 除此之外,它还将来自不同文件的 JavaScript 整合到一个文件中。 我们将在本课程的第7章节中更详细地讨论开发服务器。

在开发的这个阶段,应用的所有部分都放在软件开发人员的机器上,也就是本地主机。 当应用被部署到互联网上时,情况发生了变化。 我们将在第三章节讨论这个。

四.在服务端将数据Alert出来

Json-server 与 REST API 的教科书定义提供的描述不完全匹配,但是自称是 RESTful 的大多数服务都不完全匹配。

REST

在 REST 术语中,我们将单个数据对象(如应用中的便笺)称为resources。 每个资源都有一个唯一的地址——它的 URL。 根据 json-server 使用的一般约定,我们将能够在资源 URL, 即notes/3上定位某个便笺,其中3是资源的 id。 另一方面, notes url 指向包含所有便笺的资源集合。

发送数据到服务器

让我们对负责创建新便笺的事件处理进行如下更改:

addNote = event => {
  event.preventDefault()
  const noteObject = {
    content: newNote,
    date: new Date(),
    important: Math.random() > 0.5,
  }

  axios
    .post('http://localhost:3001/notes', noteObject)
    .then(response => {
      setNotes(notes.concat(response.data))
      setNewNote('')
    })
}

在本课程的下一章节中,我们将学习如何在后端实现我们自己的逻辑。

注意: 在当前版本的应用中,浏览器在便笺中添加了创建日期属性。 由于运行浏览器的机器的时钟可能错误地配置,所以让后端服务器为我们生成这个时间戳要明智得多。

改变便笺的重要性

让我们为每个便笺添加一个按钮,用于切换它的重要性。

我们对Note 组件进行如下更改:

const Note = ({ note, toggleImportance }) => {
  const label = note.important
    ? 'make not important' : 'make important'

  return (
    <li>
      {note.content} 
      <button onClick={toggleImportance}>{label}</button>
    </li>
  )
}

App组件定义了 toggleImportanceOf事件处理函数,并将其传递给每个Note组件:

const App = () => {
  const [notes, setNotes] = useState([]) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)

  // ...

  const toggleImportanceOf = id => {
    const url = `http://localhost:3001/notes/${id}`
    const note = notes.find(n => n.id === id)
    const changedNote = { ...note, important: !note.important }

    axios.put(url, changedNote).then(response => {
      setNotes(notes.map(note => note.id !== id ? note : response.data))
    })
}

  // ...

  return (
    <div>
      <h1>Notes</h1>
      <div>
        <button onClick={() => setShowAll(!showAll)}>
          show {showAll ? 'important' : 'all' }
        </button>
      </div>      
      <ul>
        {notesToShow.map((note, i) => 
          <Note
            key={i}
            note={note} 
            toggleImportance={() => toggleImportanceOf(note.id)}
          />
        )}
      </ul>
      // ...
    </div>
  )
}

这里有一个简短的提醒: 事件处理以类 java 的方式通过加号连接字符串定义字符串:

console.log('importance of ' + id + ' needs to be toggled')

在 ES6中,添加 template string 语法可以用一种更好的方式来编写类似的字符串:

console.log(`importance of ${id} needs to be toggled`)

使用对象展开object spread语法创建新对象的代码
,可能看起来有点奇怪:

const changedNote = { ...note, important: !note.important }

实际上,{ ...note }创建一个新对象,其中包含来自 note 对象的所有属性的副本。 当我们在 spreaded 对象后面的花括号中添加属性时,例如{ ...note, important: true },那么新对象的重要性属性的值将为 true。

将与后端的通信提取到单独的模块中

在添加了用于与后端服务器通信的代码之后,App 组件变得有些臃肿。 本着单一职责原则的精神,我们认为将这种通信提取到它自己的模块是明智的。

让我们创建一个src/services目录,并添加一个名为notes.js 的文件:

import axios from 'axios'
const baseUrl = 'http://localhost:3001/notes'

const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

const create = newObject => {
  const request = axios.post(baseUrl, newObject)
  return request.then(response => response.data)
}

const update = (id, newObject) => {
  const request = axios.put(`${baseUrl}/${id}`, newObject)
  return request.then(response => response.data)
}

export default { 
  getAll: getAll, 
  create: create, 
  update: update 
}

如果我们只返回响应数据,而不是整个 HTTP 响应。getAll函数仍然返回一个promise,因为 promise 的then方法也返回一个 promise。
App 组件使用 import访问模块,noteService对象方法的回调函数中可以直接使用返回的响应数据:

import noteService from './services/notes'

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

  useEffect(() => {
    noteService
      .getAll()
        .then(initialNotes => {
        setNotes(initialNotes)
      })
  }, [])

  const toggleImportanceOf = id => {
    const note = notes.find(n => n.id === id)
    const changedNote = { ...note, important: !note.important }

    noteService
      .update(id, changedNote)
        .then(returnedNote => {
        setNotes(notes.map(note => note.id !== id ? note : returnedNote))
      })
  }

  const addNote = (event) => {
    event.preventDefault()
    const noteObject = {
      content: newNote,
      date: new Date().toISOString(),
      important: Math.random() > 0.5
    }

    noteService
      .create(noteObject)
        .then(returnedNote => {
        setNotes(notes.concat(returnedNote))
        setNewNote('')
      })
  }

  // ...
}

export default App

用于定义对象字面量的更清晰的语法

定义便笺相关服务的模块目前导出一个具有属性getAll、create 和update 的对象:

{ 
  getAll: getAll, 
  create: create, 
  update: update 
}

由于键和赋值变量的名称是相同的,我们可以用更简洁的语法来编写对象定义:

{ 
  getAll, 
  create, 
  update 
}

因此,模块定义被简化为如下形式:
export default { getAll, create, update }

Promises and errors

当 HTTP 请求失败时,相关的承诺是rejected。 为被拒绝的承诺添加处理程序的更常见的方法是使用catch方法。

axios
  .get('http://example.com/probably_will_fail')
  .then(response => {
    console.log('success!')
  })
  .catch(error => {
    console.log('fail')
  })

catch 方法可用于在承诺链的末尾定义一个处理程序函数,一旦承诺链中的任何承诺抛出错误,承诺就变成rejected,就会调用该函数。
让我们使用这个特性并在App 组件中注册一个错误处理程序:

const toggleImportanceOf = id => {
  const note = notes.find(n => n.id === id)
  const changedNote = { ...note, important: !note.important }

  noteService
    .update(id, changedNote).then(returnedNote => {
      setNotes(notes.map(note => note.id !== id ? note : returnedNote))
    })
    .catch(error => {
      alert(
        `the note '${note.content}' was already deleted from server`
      )
      setNotes(notes.filter(n => n.id !== id))
    })
}

错误消息会通过弹出可靠的老式alert对话框显示给用户,并且已删除的便笺会从状态中过滤掉。

给React应用加点样式

让我们在src 目录下添加一个新的index.css 文件,然后通过导入index.js 文件将其添加到应用中:
import './index.css'
让我们在index.CSS 文件中添加如下 CSS 规则:

h1 {
  color: green;
  font-style: italic;
}

CSS 规则由选择器 和声明 组成。 选择器定义规则应该应用于哪些元素。 上面的选择器是h1,它将匹配我们应用中的所有h1 头标记。
声明将 color 属性设置为值green。
一个 CSS 规则可以包含任意数量的属性。

如果我们想把我们的风格特别地应用到便笺上,那么最好使用类选择器

在常规 HTML 中,class 被定义为class 属性的值:
<li class="note">some text...</li>
在React中,我们必须使用className属性而不是 class 属性。 考虑到这一点,让我们对Note 组件进行如下更改:

const Note = ({ note, toggleImportance }) => {
  const label = note.important 
    ? 'make not important' 
    : 'make important';

  return (
    <li className='note'>
      {note.content} 
      <button onClick={toggleImportance}>{label}</button>
    </li>
  )
}

类选择器使用. classname 语法定义:

.note {
  color: grey;
  padding-top: 5px;
  font-size: 15px;
}

改进错误信息

我们先前实现了当用户试图通过alert方法切换删除便笺的重要性时,显示错误消息。 让我们将错误消息实现为它自己的 React 组件。

这个组件非常简单:

const Notification = ({ message }) => {
  if (message === null) {
    return null
  }

  return (
    <div className="error">
      {message}
    </div>
  )
}

样式规则:

.error {
  color: red;
  background: lightgrey;
  font-size: 20px;
  border-style: solid;
  border-radius: 5px;
  padding: 10px;
  margin-bottom: 10px;
}

让我们在App 组件中添加一个名为errorMessage 的新状态。 让我们用一些错误信息来初始化它,这样我们就可以立即测试我们的组件:

const App = () => {
  const [notes, setNotes] = useState([]) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)
  const [errorMessage, setErrorMessage] = useState('some error happened...')

  // ...

  return (
    <div>
      <h1>Notes</h1>
      <Notification message={errorMessage} />
      <div>
        <button onClick={() => setShowAll(!showAll)}>
          show {showAll ? 'important' : 'all' }
        </button>
      </div>      
      // ...
    </div>
  )
}

现在,我们准备添加显示错误消息的逻辑。 让我们用下面的方法更改 toggleImportanceOf 函数:

  const toggleImportanceOf = id => {
    const note = notes.find(n => n.id === id)
    const changedNote = { ...note, important: !note.important }

    noteService
      .update(changedNote).then(returnedNote => {
        setNotes(notes.map(note => note.id !== id ? note : returnedNote))
      })
      .catch(error => {
        setErrorMessage(
          `Note '${note.content}' was already removed from server`
        )
        setTimeout(() => {
          setErrorMessage(null)
        }, 5000)
        setNotes(notes.filter(n => n.id !== id))
      })
  }

当出现错误时,我们向 errorMessage 状态添加一个错误描述消息。 与此同时,我们启动一个计时器,它将在5秒后将 errorMessage状态设置为null。

Inline styles 【内嵌样式】

任何 React 组件或元素都可以通过style属性作为 JavaScript 对象提供一组 CSS 属性。
在 CSS 中,它看起来像这样:

{
  color: green;
  font-style: italic;
  font-size: 16px;
}

但是作为一个 React inline style 内置样式对象,它看起来是这样的:

 {
  color: 'green',
  fontStyle: 'italic',
  fontSize: 16
}

接下来,我们可以通过创建一个Footer 组件向应用添加一个“ bottom block” ,并为它定义如下行内样式:

const Footer = () => {
  const footerStyle = {
    color: 'green',
    fontStyle: 'italic',
    fontSize: 16
  }

  return (
    <div style={footerStyle}>
      <br />
      <em>Note app, Department of Computer Science, University of Helsinki 2020</em>
    </div> 
  )
}

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

  return (
    <div>
      <h1>Notes</h1>

      <Notification message={errorMessage} />

      // ...  

      <Footer />
    </div>
  )
}

内联样式有一定的限制,例如,所谓的pseudo-classes不能直接使用。

传统上,将 CSS 与内容(HTML)和功能(JavaScript)解耦被认为是最佳实践。

React的哲学,事实上,是这个极端的对立面。 由于将 CSS、 HTML 和 JavaScript 分离成单独的文件在大型应用中似乎不利于伸缩,所以 React 将应用按照其逻辑功能实体进行划分。

构成应用功能实体的结构单元是 React 组件。 React 组件定义了组织内容的 HTML,确定功能的 JavaScript 函数,以及组件的样式; 所有这些都放在一个地方。 这是为了创建尽可能独立和可重用的单个组件。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×