目录

全栈公开课2020学习笔记 02 - 与服务器通信

从渲染集合到模块学习

使用 VS Code 可以安装 React 的代码片段 Snippets,安装在这里

善用 console.log 来进行调试,使用 console.log('props value is', props),用 , 分隔而不是用 + 连接 object。

在应用崩溃使用 console.log 来调试时,应该从最先渲染的组件开始插入 console.log 来排查出错问题的位置,然后结合 打印数组、对象等方式找出具体的问题并解决,直到程序正常运行为止。

JavaScript Arrays JS数组

学习视频,待整理

Rendering collections with Map method 用 Map 方法渲染集合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React from 'react'
import ReactDOM from 'react-dom'

const notes = [
  {
    id: 1,
    content: '我喜欢 react',
    date: '2020-10-29T15:17:10.098Z',
    important: true
  },
  {
    id: 2,
    content: '启动电脑开VS,欢迎来到编程世界。',
    date: '2020-10-29T15:18:26.092Z',
    important: false
  },
  {
    id: 3,
    content: 'GET 和 POST 是 HTTP 协议中最重要的方法',
    important: true
  }
]

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

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

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

以上 notes 数组仅有三个对象,可以通过引用一个硬编码的索引号来访问数组中对象。

使用 map 函数从数组对象生成 React 元素。

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

通过这样的方法可以生成 li 元素的数组。

1
2
3
4
5
[
  <li>我喜欢 react</li>,
  <li>启动电脑开VS欢迎来到编程世界</li>,
  <li>GET  POST  HTTP 协议中最重要的方法</li>,
]

现在重构之前的代码,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const App = (props) =>
{
  const { notes } = props

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

map 的工作方式

1
2
const result = notes.map(note => note.id)
console.log(result)     // 打印出 [1,2,3]

使用 map 方法会创建一个新数组,其元素是从原始数组通过 mapping 映射创建的,映射的逻辑是使用作为 map 方法传递进去的函数。

这个函数是

1
2
3
4
5
6
7
8
// 紧凑形式
note => note.id

// 以上函数的完整形式
(note) =>
{
  return note.id
}

该函数获取一个 note 对象作为参数,然后番位其 id 字段的值。

Key attribute Key属性

正如 React 列表 & Key 的说明,列表项,map 方法生成的每个元素,都必须有一个唯一的键值,名为 key 的属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const App = (props) =>
{
  const { notes } = props

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

虽然说可以使用数组的索引作为键,避免控制台报错,但这仍然可能会导致意想不到的问题。,这有问题案例

1
2
3
4
5
6
7
// 不要这样做
<ul>
  {notes.map((note, index) =>
    <li key={index}>
      {note.content}
    </li>
  )}

Refactoring modules 重构模块

将 App 重构,使得 App 更简洁易用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 新增 Note 组件
const Note = ({ note }) =>
{
  return (
    <li>{note.content}</li>
  )
}

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

此处要注意,这样必须在 Note 组件定义 key 属性,而不是在 li 标签定义。

通常来说,在较小型的应用中,组件一般放在 src/components 目录下,一般约定按照组件名称来命名文件。

例如创建一个 Node 组件,我们将 Node.js 文件存在 components 目录下,其文件内容为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import React from 'react'

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

export default Note

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

那么在使用这个组件的文件中,使用 import 方法来使用这个模块。

1
2
3
4
5
6
7
8
import React from 'react'
import ReactDom from 'react-dom'
import Note from './components/Note'

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

导入自己的组件时,它们的位置必须给出导入文件的相对路径。 './components/Note'

开头的 . 是指当前工作目录,因此引入文件的位置是当前目录下的 components 文件夹中的 Node.js 文件,其中 .js 可以省略。

表单

使用 useState 方法将新增的笔记添加到 App 组件状态中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const App = ({ notes }) =>
{
  const [notes, setNotes] = useState(notes)
  const [newNote, setNewNote] = useState('a new note...')

  const addNote = (event) =>
  {
    event.preventDefault() // 阻止事件的默认操作

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

    setNotes(notes.concat(noteObject)) // 使用 concat 方法来新建
    setNewNote('')
  }

  const handleNoteChange = (event) => setNewNote(event.target.value)

  return (
    <div>
      <h1>笔记</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>
  )
}

其中注意点 <input /> 组件中,如果只有 value={newNote} 属性受控,将会导致 input 组件渲染成一个只读元素,这将正确的修改 input 的value,必须搭配 onChange={handleNoteChange} 才能正确控制 input 元素的状态。相反的,如果只有 onChange={handleNoteChange} 而没有 value={newNote},那样 setNewNote('') 将不会重置 input 元素的值。

Filtering Displayed Elements 过滤显示的元素

知识点

三目运算符 - condition ? val1 : val2

condition 为 true 时,返回 val1,否则返回 val2


数组的 filter 方法 - arr.filter()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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) // 因为 note.important 本身是布尔值,所以省略了 '=== true' 的判断

  return (
    <div>
      <h1>Notes</h1>
      <div>
        <button onClick={() => setShowAll(!showAll)}>
          show {showAll ? '显示重要' : '显示全部' }
        </button>
      </div>
      <ul>
        {notesToShow.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
      // ...        
    </div>
  )
}

通过三目运算符判断 showAll 的状态来过滤需要显示的数据以及按钮的值。

从服务器获取数据

一般的项目包含前端「客户端」和后端「服务端」。截至目前,程序仅涉及到前端的开发。

接下来使用 JSON 服务器 作为后端。

使用 npm install -g json-server 执行全局安装 json-server,也可以在应用根目录使用 npx json-server --port 3001 --watch db.json 运行 json-server 服务器。

在项目根目录创建 db.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "notes": [
    {
      "id": 1,
      "content": "我喜欢 react",
      "date": "2020-10-29T15:17:10.098Z",
      "important": true
    },
    {
      "id": 2,
      "content": "启动电脑开VS,欢迎来到编程世界。",
      "date": "2020-10-29T15:18:26.092Z",
      "important": false
    },
    {
      "id": 3,
      "content": "GET 和 POST 是 HTTP 协议中最重要的方法",
      "date": "2020-10-29T15:17:10.098Z",
      "important": true
    }
  ]
}

为什么使用 3001 端口?是因为前端 React 程序占用了 3000 端口,所以将 json-server 设定为 3001 端口。

可以通过 http://localhost:3001/notes 来访问 db.jsonnotes 数据了。

选用 json-server 作为后端服务器的原因是在开发阶段可以很快速的实现功能。实际项目一般会将数据存在数据库中,如 MySQL MongoDB 等。

The browser as a runtime environment

XHR「XMLHttpRequest」是1999年引入的技术,使用 XHR 对象发出 HTTP 请求。

现代浏览器基本上都支持了 promises 的 fetch 方法,而不是 XHR 使用的事件驱动型,所以现在一般也不用 XHR 方法。

但还是有必要了解一下 XHR 方法如何请求数据的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const xhttp = new XMLHttpRequest()

xhttp.onreadystatechange = function()
{
  if (this.readyState == 4 && this.status == 200)
  {
    const data = JSON.parse(this.responseText)
  }
}

xhttp.open('GET', '/data.json', true)
xhttp.send()

以上将事件处理程序注册到 xhttp 对象,当 xhttp 的状态发生改变时执行。XHR 请求是异步执行,并非自上而下的同步执行。

JavaScript 引擎,或者运行环境,遵循异步模型。原则上所有的 IO 操作「除了一些特例」都以非阻塞方式执行。这意味着代码执行在调用 IO 函数之后立即继续,而不需要等待它返回。

异步操作完成的某个时刻,JavaScript 引擎才会调用注册到该操作的事件处理程序。

JavaScript 是单线程的,所以它并不能并行执行代码,若没有使用非阻塞方式,浏览器将会在执行代码时卡住,直到代码执行结束才恢复响应。

为了保证浏览器的 responsive,代码逻辑要让任何一个单一计算不花费太多时间。

现代浏览器可以使用 Web Worker 来运行并行化代码。但注意的是单个浏览器窗口仍然是由一个单线程处理。

npm

目前几乎所有 JavaScript 项目都是使用 node 包管理器定义的,也就是 npm。使用 npm 会在项目根目录创建一个 package.json 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
  "name": "notes",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.4.0",
    "@testing-library/user-event": "^7.2.1",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-scripts": "3.3.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

dependencies 定义了该项目所需要的依赖或外部库。

安装依赖也非常方便,在项目根目录使用终端 npm install axios 命令即可。

当执行完成后,会在 package.json 的 dependencies 中添加 axios。当然我们也可以直接在 package.json 增加这一条,然后在根目录通过执行 npm install安装所有依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
}
  "dependencies": {
    // ...
    "axios": "^0.19.2",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-scripts": "3.3.0"
  },
  // ...
}

如何安装开发依赖?开发依赖是在开发中提供帮助,实际项目并不需要它。

1
npm install json-server --save-dev

以上安装了 JSON 服务器,在 package.json 中增加 server script 来帮助我们快速启动 JSON 服务器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  // ... 
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "server": "json-server -p3001 --watch db.json"
  },
}

这样可以通过 npm run server 来启动 JSON 服务器了。

Axios and promises

axios 库是代替浏览器和服务器之间的通信,功能类似于 fetch。在上一节使用 npm 安装了 axios 库,在项目中像引入 react 库一样 import 即可。

1
2
3
4
5
6
7
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)

由于 JSON 服务器中仅有 notes 数据,所以访问 http://localhost:3000 控制台会有报错,如下:

Axios 的 get 方法会返回一个 promise

Mozilla’s 网站上的文档对 promises 的解释:

A Promise is an object representing the eventual completion or failure of an asynchronous operation.

Promise承诺是一个对象,用来表示异步操作的最终完成或失败。

promise 是一个表示异步操作的对象,有三种不同状态

  • The promise is pending
  • The promise is fulfilled
  • The promise is rejected

pending 表示还在提交中,最终值处于不可用状态。

fulfilled 表示数据已兑现,操作成功,最终值可用。这种状态有时也被称为 resolve

rejected 表示拒绝,操作失败,说明出现了错误阻止最终值。

回到上面的示例,在一个请求的 PromiseStatusresolved 也就是 fulfilled 状态,表示是成功的。而第二个是 rejected,控制台提示错误原因,是因为我们HTTP GET 请求的地址是不存在的「404错误」。

如果我们想要访问 promise 表示的操作结果,必须要向 promise 注册一个事件处理程序。这里是使用 then 方法实现。

1
2
3
4
5
6
const promise = axios.get('http://localhost:3001/notes')

promise.then(response =>
{
  const notes = response.data
})

JavaScript 运行时环境调用由 then 方法注册的回调函数,并提供一个 response 对象作为参数。response 对象包含与 HTTP GET 请求响应相关的所有基本数据,也包括返回的 data、status code 和 headers。

通常情况下,没有必要将 promise 对象存储在一个变量中,而且将 then 链接到 axios 方法中也是很常用的。所以重构代码如下:

1
2
3
4
5
6
axios
  .get('http://localhost:3001/notes')
  .then(response =>
  {
    const notes = response.data
  })

服务器返回的数据是纯文本,基本上只有一个长字符串。 Axios 库仍然能够将数据解析为一个 JavaScript 数组,因为服务器使用 content-type 头指定数据格式为 application/json; charset=utf-8

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

import axios from 'axios'

axios.get('http://localhost:3001/notes').then(response => {
  const notes = response.data
  ReactDOM.render(
    <App notes={notes} />,
    document.getElementById('root')
  )
})

以上方法向本地服务器请求数据并渲染,虽然也能够执行,但它只有在将整个 App 组件渲染完成后才会得到成功的 response。而且 axios.get 命令应该放在组件中的哪个位置也是个问题。

这样的方法不是很好,在实际开发中会遇到很多问题。

结合 Effect hooks 可以很好的处理。

Effect-hooks

16.8.0版本还引入了 effect hooks 新特性。 像文档里说的:

The Effect Hook lets you perform side effects in function components. Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects.

Effect Hook 可以让你在函数组件中执行副作用。数据获取、设置订阅和手动更改 React 组件中的 DOM 都是副作用的例子。

因此,effect hooks正是从服务器获取数据时使用的正确工具。

更改原先的代码,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 兑现')
        setNotes(response.data)
      })
  }, [])

  console.log('render', notes.length, '笔记')

  // ...
}

执行上述代码后,控制台会依次显示以下内容:

1
2
3
4
render 0 笔记
effect
promise fulfilled
render 3 笔记

useEffect 函数一共有2个参数,第一个是函数本身,根据文档描述:

By default, effects run after every completed render, but you can choose to fire it only when certain values have changed.

默认情况下,effects 在每次渲染完成后运行,但是你可以选择只在某些值发生变化时才调用。

第二个是指定effect运行的频率[] 空数组表示只在组件的第一次渲染时运行。

1
useEffect(function, [])

The development runtime environment

目前为止项目的运行环境情况如下图:

在服务端将数据 Alert 出来

REST 表现层状态转换

REST(Representational state transfer),不是一种标准,而是一种设计风格。REST通常基于HTTP、URI、XML以及HTML这些现有的广泛流行的协议和标准。

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

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

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

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

Sending Data to the Server 发送数据到服务器

使用 POST 方法将对象发送至服务器,创建一个新对象,不需要发送 id 属性,因为 id 交给服务器生成更加合理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
addNote = (event) =>
{
  event.preventDefault()
  const newObject = 
  {
    content: newNote,
    date: new Date(),
    important: Math.random() < 0.5,
  }

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

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

通过 Chrome 开发工具的 Network 选项卡检查 HTTP 请求来查看是否符合预期。

Postman 工具用来调试服务器应用非常易用,Chrome 扩展官方已弃用(DEPRECATED),使用 App 功能更加强大。

Changing the important of Notes

向 Note 组件添加用于调整 Notes 的 important 值的按钮:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const Note = ({ note, toggleImportance }) =>
{
  const label = note.important ? '设置为不重要' : '设置为重要'

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

在 App 组件增加事件处理程序 toggleImportance

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const App = () =>
{
  const [notes, setNote] = useState([])
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)

  // ...

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

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

  // ...

  return (
    <div>
      <h1>笔记</h1>
      <div>
        <button onClick={() => setShowAll(!showAll)}>
          show {showAll ? '显示重要' : '显示全部' }
      </div>
      <ul>
        {noteToShow.map((note) =>
          <Note
            key={note.id}
            note={note}
            toggleImportance={() => toggleImportance(note.id)}
          />
        )}
      </ul>
      // ...
    </div>
  )
}

通过 PUT 方法把对应 idnote 修改,修改成重要或是不重要。并通过回调函数将组件内的 Notes 状态更新。

Extracting communication with the backend into a separate module

将与后端通信提取到单独的模块中去,常用的存放目录是 src/services

services 目录下创建 note.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import axios from 'axios'

const baseUrl = 'http://localhost:3001/notes'

const getAll = () => axios.get(baseUrl)
const create = (newObject) => axios.post(baseUrl, newObject)
const update = (id, newObject) => axios.put(`${baseUrl}/${id}`, newObject)

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

修改 App.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// ...
import noteService from './services/notes'

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

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

  const toggleImportance = (id) =>
  {
    const note = notes.find(note => note.id === id)
    const changeNote = { ...note, important: !note.important }

    noteService
      .update(id, changeNote)
      .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 只使用 response 对象的 response.data 属性,那么可以使用如下方法会更好一些。

services/note.js 做如下修改;

1
2
3
4
5
6
7
const getAll() =>
{
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

// 其余方法以此类推

相应的 App 组件里对应的 service 也要做修改:

1
2
3
4
5
6
noteService
  .getAll()
  .then(initialNotes =>
  {
    setNotes(initialNotes)
  })

getAll 函数返回了一个 Promise, 当 HTTP 请求成功时,Promise 将返回从后端响应中发送回来的数据。

承诺Promise是现代 JavaScript 开发的核心,需要投入一定时间理解它们。 You do not know JS - Chapter 3 Promises Promise chaining

cleaner syntax for defining object literals 用于定义对象字面量的更清晰的语法

上章节中,service/note.js 最后 export 了三个对象:

1
2
3
4
5
6
export default
{
  getAll: getAll,
  create: create,
  update: update
}

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

由于键和赋值变量是相同的,同时也因 ES6 引入到 JavaScript 中,可以简化成如下:

1
export default { getAll, create, update }

Promises and errors

使用 catch 方法来处理 Promise 是 reject 的状态的情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
axios
  .get('http://localhost:3001/foobar')
  .then(response =>
  {
    console.log('请求成功')
  })
  .catch(error =>
  {
    console.log('请求失败')
  })

通常 catch 方法置于 Promise 链的更深处来使用,如下:

1
2
3
4
5
6
7
8
axios
  .get(baseUrl)
  .then(response => response.data)
  .then(changeNote =>
  {
    // ...
  })
  .cartch(error => console.log('请求失败')

修改原先 App 组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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(
        `未找到该条 '${note.content}' 笔记,可能已从服务器删除。`
      )
      setNotes(notes.filter(n => n.id !== id))
    })
}

给 React 应用加点样式

  • 导入外部 css 文件
  • 内联样式

导入外部 css 文件的方式,在 index.js 使用 import

1
import './index.css'

之后在 index.css 添加相关样式即可。常规 html 文件在元素中使用 class 属性来对应样式表的类选择器,而 react 中需要使用 className 来对应。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const Note = ({ note, toggleImportance }) => {
  const label = note.important 
    ? '设置为不重要'
    : '设置为重要';

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

内联样式的使用方法如下:

需要注意的是,内联样式中无法使用 伪类 pseudo-classes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const Footer = () => {
  const footerStyle = {
    color: 'green',
    fontStyle: 'italic',
    fontSize: 16
  }
  return (
    <div style={footerStyle}>
      <br />
      <em>footer content</em>
    </div> 
  )
}

内联样式和其他一些将样式添加到 React 组件的方法完全违背了旧的惯例。 传统上,将 CSS 与内容(HTML)和功能(JavaScript)解耦被认为是最佳实践。 根据这个古老的思想流派,我们的目标是将 CSS、 HTML 和 JavaScript 编写到它们各自的文件中。

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

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