d

在服务端将数据Alert出来

在应用中创建便笺时,我们自然希望将它们存储在某个后端服务器中。 在文档中,json-server包提到了所谓的 REST 或 RESTful API:

Get a full fake REST API with zero coding in less than 30 seconds (seriously) 在不到30秒(严肃地)的情况下得到一个完整的模拟 REST API,0编码

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

我们将在本课程的下一章节中进一步了解 REST,但是熟悉 json-server 和 REST api 经常使用的一些约定是很重要的。 特别是,我们将会看到在 REST 中常规使用路由 ,即 url 和 HTTP 请求类型。

REST

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

通过 HTTP GET 请求从服务器获取资源。 例如,对 URLnotes/3 的 HTTP GET 请求将返回 id 为3的便笺。 对notes URL 的 HTTP GET 请求将返回所有便笺的列表。

根据 json 服务器遵守的 REST 约定,通过向notes URL 发出 HTTP POST 请求来创建、存储新的便笺。 新便笺资源的数据在请求的body 中发送。

Json-server 要求以 JSON 格式发送所有数据。 实际上,这意味着数据必须是格式正确的字符串,并且请求必须包含值为application/jsonContent-Type 请求头。

Sending Data to the Server

【发送数据到服务器】

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

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 => {      console.log(response)    })}

我们为便笺创建了一个新对象,但忽略了id 属性,因为最好让服务器为我们的资源生成 id!

使用 axios post 方法将对象发送到服务器。 已注册的事件处理函数从服务器发送回控制台的响应记录。

当我们尝试创建一个新的便笺时,控制台会弹出如下输出:

fullstack content

新创建的便笺资源存储在response对象的data 属性值中。

有时在 Chrome 开发工具的Network 选项卡中检查 HTTP 请求是很有用的,这个选项卡在第 0 章 开始时被大量使用:

fullstack content

我们可以使用检查器来检查 POST 请求中发送的头文件是否符合我们的预期,以及它们的值是否正确。

由于我们在 POST 请求中发送的数据是一个 JavaScript 对象,axios 自动懂得为Content-Type 头设置适当的application/json 值。

新的便笺还没有渲染到屏幕上。 这是因为我们在创建新便笺时没有更新App 组件的状态。 让我们来解决这个问题:

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('')    })
}

后端服务器返回的新便笺将按照使用 setNotes 函数然后重置便笺创建表单的惯例方式添加到应用状态的便笺列表中。 需要记住的一个 重要细节important detailconcat 方法不会改变组件的原始状态,而是创建列表的新副本。

一旦服务器返回的数据开始影响我们 web 应用的行为,我们就会立即面临一系列全新的挑战,例如,通信的异步性。 这就需要新的调试策略,控制台日志和其他调试手段变得越来越重要,我们还必须对 JavaScript 运行时和 React 组件的原理有充分的理解。 光靠猜是不够的。

通过浏览器检查后端服务器的状态是有益的:

fullstack content

这样就可以验证我们打算发送的所有数据是否实际上已经被服务器接收。

在本课程的下一章节中,我们将学习如何在后端实现我们自己的逻辑。 然后,我们将进一步研究一些工具,如postman ,这些工具可以帮助我们调试服务器应用。 但是,通过浏览器检查 json-server 的状态就足以满足我们当前的需求。

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

我们应用当前状态的代码可以在github上的part2-5 分支中找到。

Changing the importance of notes

【改变便笺的重要性】

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

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

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

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

我们向组件添加一个按钮,并将其事件处理作为toggleImportance函数的 props 传递到组件中。

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

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

  // ...

  const toggleImportanceOf = (id) => {    console.log('importance of ' + id + ' needs to be toggled')  }
  // ...

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

注意每个便笺是如何接收它自己唯一的 事件处理函数的,因为每个便笺的 id 是唯一的。

例如,如果我 note.id 是3, toggleImportance(note.id) 返回的事件处理函数将是:

() => { console.log('importance of 3 needs to be toggled') }

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

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

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

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

我们现在可以使用“ dollar-bracket”语法向字符串中添加内容来计算 JavaScript 表达式,例如变量的值。 注意,模板字符串中使用的反引号与常规 JavaScript 字符串中使用的引号不同。

存储在 json-server 后端中的各个便笺可以通过对便笺的唯一 URL 发出 HTTP 请求,以两种不同的方式进行修改。 我们可以用 HTTP PUT 请求替换 整个便笺,或者只用 HTTP PATCH 请求更改便笺的一些属性。

事件处理函数的最终形式如下:

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

函数体中几乎每一行代码都包含重要的细节。 第一行根据每个便笺资源的 id 定义其唯一的 url。

数组的 find方法用于查找要修改的便笺,然后将其分配给note变量。

在此之后,我们创建一个新对象,除了重要性属性,它完全是旧便笺的副本。

使用对象展开object spread语法创建新对象的代码

可能看起来有点奇怪:

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

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

有几点需要指出。 为什么我们要复制我们想要修改的 note 对象,而下面的代码似乎也可以工作:

const note = notes.find(n => n.id === id)
note.important = !note.important

axios.put(url, note).then(response => {
  // ...

不建议这样做,因为变量 note 是对处于组件状态 notes 数组中某个项的引用,而且我们记得在 React 中绝不能直接修改状态。

值得注意的是,新对象 changedNote 只是一个所谓的浅拷贝 ,这意味着新对象的值与旧对象的值相同。 如果旧对象的值本身就是对象,那么新对象中复制的值将引用旧对象中的相同对象

然后这个新便笺与一个 PUT 请求一起发送到后端,它将在后端替换旧对象。

回调函数将组件的 notes 状态设置为一个新数组,该数组包含前一个notes 数组中的所有条目,但旧的条目被服务器返回的更新版本所替换:

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

这是通过 map方法实现的:

notes.map(note => note.id !== id ? note : response.data)

Map 方法通过将旧数组中的每个项映射到新数组中的一个项来创建一个新数组。 在我们的示例中,新数组被有条件地创建,即如果note.id !== id为true,我们只需将项从旧数组复制到新数组中。 如果条件为 false,则将服务器返回的 note 对象添加到数组中。

这个map 技巧起初可能看起来有点奇怪,但是它值得你花一些时间去理解它。 在整个课程中,我们将多次使用这种方法。

Extracting communication with the backend into a separate module

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

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

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

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

const getAll = () => {
  return axios.get(baseUrl)
}

const create = newObject => {
  return axios.post(baseUrl, newObject)
}

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

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

该模块返回一个具有三个函数(getAll, create, and update)的对象,作为其处理便笺的属性。 函数直接返回 axios 方法返回的允诺Promise。

App 组件使用 import访问模块:

import noteService from './services/notes'
const App = () => {

该模块的功能可以直接与导入的变量 noteService 一起使用,具体如下:

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

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

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

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

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

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

  // ...
}

export default App

我们可以将我们的实现更进一步。 当App 组件使用这些函数时,它接收到一个包含 HTTP 请求的整个响应的对象:

noteService
  .getAll()
  .then(response => {
    setNotes(response.data)
  })

App 组件只使用response对象的 response.data 属性。

如果我们只获得响应数据,而不是整个 HTTP 响应,那么使用这个模块会更好。 使用这个模块看起来是这样的:

noteService
  .getAll()
  .then(initialNotes => {
    setNotes(initialNotes)
  })

我们可以通过如下变更模块中的代码来实现这一点(当前的代码包含一些复制粘贴,但我们暂时可以容忍这一点) :

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 
}

我们不再直接回应axios 的承诺。 相反,我们将 promise 分配给 request 变量,并调用它的then 方法:

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

我们函数的最后一行只是相同代码的更简洁的表达式,如下所示:

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

修改后的getAll 函数仍然返回一个 promise,因为 promise 的 then 方法也返回一个 promise

在定义了then 方法的参数直接返回response.data 之后,我们已经让 getAll 函数按照我们希望的方式工作。 当 HTTP 请求成功时,承诺将返回从后端响应中发送回来的数据。

我们必须更新App 组件来处理对模块所做的更改。 我们必须修复作为参数给予noteService对象方法的回调函数,以便它们使用直接返回的响应数据:

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('')
      })
  }

  // ...
}

这一切都相当复杂,试图解释它可能只会让它更难理解。 互联网上充满了讨论这个话题的材料,比如这个this

You do not know JS 一书中,对这个议题进行了很好的解释well,但是解释有很多页

承诺Promise是现代 JavaScript 开发的核心,强烈建议投入合理的时间来理解它们。

Cleaner syntax for defining object literals

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

定义便笺相关服务的模块目前导出一个具有属性getAllcreateupdate 的对象,这些属性分配给处理便笺的函数。

模块的定义是:

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 
}

该模块导出下面这个看起来相当奇怪的对象:

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

在对象定义中,冒号左侧的标签是对象的,而它右侧的标签是在模块内部定义的variables

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

{ 
  getAll, 
  create, 
  update 
}

因此,模块定义被简化为如下形式:

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, create, update }

在使用这种较短的符号定义对象时,我们利用了通过 ES6引入到 JavaScript 中的一个新特性new feature ,使得使用变量定义对象的方法更加简洁。

为了演示这个特性,让我们考虑这样一种情况: 我们给变量赋值如下:

const name = 'Leevi'
const age = 0

在旧版本的 JavaScript 中,我们必须这样定义一个对象:

const person = {
  name: name,
  age: age
}

然而,由于对象中的属性字段和变量名称都是相同的,只需在 ES6 JavaScript 中写入如下内容就足够了:

const person = { name, age }

两个表达式的结果是相同的。 它们都创建了一个值为Leeviname 属性和值为0age 属性的对象。

Promises and errors

【承诺和错误】

如果我们的应用允许用户删除便笺,那么我们可能会出现这样的情况: 用户试图更改已经从系统中删除的便笺的重要性。

让我们通过使 note 服务的getAll 函数返回一个“硬编码”的便笺来模拟这种情况,这个便笺实际上并不存在于后端服务器中:

const getAll = () => {
  const request = axios.get(baseUrl)
  const nonExisting = {
    id: 10000,
    content: 'This note is not saved to server',
    date: '2019-05-30T17:30:31.098Z',
    important: true,
  }
  return request.then(response => response.data.concat(nonExisting))
}

当我们试图更改硬编码说明的重要性时,我们在控制台中看到如下错误消息。 错误说明后端服务器用状态码404not found 响应了我们的 HTTP PUT 请求。

fullstack content

应用应该能够很好地处理这些类型的错误情况。 除非用户碰巧打开了自己的控制台,否则他们无法判断错误确实发生了。 在应用中可以看到错误的唯一方式是单击按钮看看对便笺的重要性没有影响。

我们 之前 提到,一个承诺可以处于三种不同的状态之一。 当 HTTP 请求失败时,相关的承诺是rejected。 我们当前的代码没有以任何方式处理这种拒绝。

拒绝承诺是通过给then 方法提供第二个回调函数来处理的,这个handled 在承诺被拒绝的情况下被调用。

为被拒绝的承诺添加处理程序的更常见的方法是使用catch方法。

实际上,拒绝承诺的错误处理程序的定义如下:

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

如果请求失败,则调用catch 方法注册的事件处理程序。

catch 方法通常是通过将其置于Promise链的更深处来使用的。

当我们的应用发出一个 HTTP 请求时,我们实际上是在创建一个 promise chain:

axios
  .put(`${baseUrl}/${id}`, newObject)
  .then(response => response.data)
  .then(changedNote => {
    // ...
  })

catch 方法可用于在承诺链的末尾定义一个处理程序函数,一旦承诺链中的任何承诺抛出错误,承诺就变成rejected,就会调用该函数。

axios
  .put(`${baseUrl}/${id}`, newObject)
  .then(response => response.data)
  .then(changedNote => {
    // ...
  })
  .catch(error => {
    console.log('fail')
  })

让我们使用这个特性并在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对话框显示给用户,并且已删除的便笺会从状态中过滤掉。

从应用的状态中删除已经删除的便笺是通过数组的 filter方法完成的,该方法返回一个新的数组,其中只包含列表中的项目,作为参数传递的函数返回 true :

notes.filter(n => n.id !== id)

在更严肃的 React 应用中使用alert可能不是一个好主意。 我们很快就会学到一种更先进的向用户显示消息和通知的方式。 然而,在某些情况下,像alert这样简单的、经过实战检验的方法可以作为一个起点。 如果有时间和精力的话,可以在以后添加一个更高级的方法。

我们应用当前状态的代码可以在github上的part2-6 分支中找到。