目录
REST
Sending Data to the Server
Changing the importance of notes
Extracting communication with the backend into a separate module
Cleaner syntax for defining object literals
Promises and errors
Get a full fake REST API with zero coding in less than 30 seconds (seriously)
在不到30秒(严肃地)的情况下得到一个完整的模拟 REST API,0编码
在REST 术语将单个数据对象(如应用中的便笺)称为resources。 每个资源都有一个唯一的地址——URL。 我们将能够在资源 URL, 即notes/3上定位某个便笺,其中3是资源的 id。 另一方面, notes url 指向包含所有便笺的资源集合。
通过 HTTP GET 请求从服务器获取资源。 例如,对 URLnotes/3 的 HTTP GET 请求将返回 id 为3的便笺。 对notes URL 的 HTTP GET 请求将返回所有便笺的列表。
通过向notes URL 发出 HTTP POST 请求来创建、存储新的便笺。 新便笺资源的数据在请求的body 中发送。
Json-server 要求以 JSON 格式发送所有数据。 数据必须是格式正确的字符串,并且请求必须包含值为application/json 的Content-Type 请求头。
【发送数据到服务器】
让我们对负责创建新便笺的事件处理进行如下更改:
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 方法将对象发送到服务器。
尝试创建一个新的便笺时,控制台会弹出如下输出:
新创建的便笺资源存储在response对象的data 属性值中。
在 Chrome 开发工具Network 选项卡中检查 HTTP 请求:
可以使用检查器来检查 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 detail 是 concat 方法不会改变组件的原始状态,而是创建列表的新副本。
一旦服务器返回的数据开始影响 web 应用的行为,就会立即面临一系列全新的挑战,例如,通信的异步性。 这就需要新的调试策略,控制台日志和其他调试手段,还必须对 JavaScript 运行时和 React 组件的原理有充分的理解。
通过浏览器检查后端服务器的状态:
验证我们发送的所有数据是否已经被服务器接收。
注意: 在当前版本的应用中,浏览器在便笺中添加了创建日期属性。 由于运行浏览器的机器的时钟可能错误地配置,所以最好让后端服务器生成这个时间戳。
【改变便笺的重要性】
为每个便笺添加一个按钮,用于切换它的重要性。
我们对Note 组件进行如下更改:
const Note = ({ note, toggleImportance }) => {
const label = note.important
? 'make not important' : 'make important'
return (
{note.content}
)
}
我们向组件添加一个按钮,并将其事件处理作为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 (
Notes
{notesToShow.map((note, i) =>
toggleImportanceOf(note.id)}
/>
)}
// ...
)
}
每个便笺根据唯一的 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 属性在原始对象中取其先前值的反值。
注意,新对象 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 对象添加到数组中。
【将与后端的通信提取到单独的模块中】
在添加了用于与后端服务器通信的代码之后,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
}
我们将 promise 分配给 request 变量,并调用它的then 方法:
const getAll = () => {
const request = axios.get(baseUrl)
return request.then(response => response.data)
}
修改后的getAll 函数仍然返回一个 promise,因为 promise 的 then 方法也返回一个 promise。
在定义了then 方法的参数直接返回response.data 之后, 当 HTTP 请求成功时,promise 将返回从后端响应中发送回来的数据。
更新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('')
})
}
// ...
}
Promise是现代 JavaScript 开发的核心,强烈建议投入合理的时间来理解。
【用于定义对象字面量的更清晰的语法】
定义便笺相关服务的模块,导出一个具有属性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: 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 }
两个表达式的结果是相同的。 它们都创建了一个值为Leevi 的name 属性和值为0 的age 属性的对象。
【承诺和错误】
如果应用允许用户删除便笺,那么可能会出现这样的情况: 用户试图更改已经从系统中删除的便笺的重要性。
通过使 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 请求。
应用应该能够很好地处理这些类型的错误情况。 除非用户碰巧打开了自己的控制台,否则无法判断错误确实发生了。
一个 promise 有三种不同的状态,当 HTTP 请求失败时是rejected。 当前的代码没有以任何方式处理这种拒绝。
rejected 通过给then 方法提供第二个回调函数来处理的,handled 在承诺被拒绝的情况下被调用。
为rejected 添加处理程序的更常见的方法是使用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 方法可用于在承诺链的末尾定义一个处理程序函数,一旦 promise 链中的任何承诺抛出错误,承诺就变成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)
像alert这样简单的、经过实战检验的方法可以作为一个起点。可以在以后添加一个更高级的方法。