JSON服务器

从本节开始研究“后端”,即服务器端功能
我们使用JSON服务器作为我们的服务器

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

你可以全局安装JSON服务器

  1. npm install -g json-server

全局安装不是必须的。因为我们可以在应用的根目录使用 npx 命令运行json-server
默认情况下,json-server在端口3000上启动; 但是由于 create-react-app 项目设置了3000端口,因此我们必须为 json-server 定义一个备用端口,比如端口3001。

  1. npx json-server --port 3001 --watch db.json

在项目根目录新建一个db.json文件

  1. {
  2. "notes": [
  3. {
  4. "id": 1,
  5. "content": "HTML is easy",
  6. "date": "2019-05-30T17:30:31.098Z",
  7. "important": true
  8. },
  9. {
  10. "id": 2,
  11. "content": "Browser can execute only JavaScript",
  12. "date": "2019-05-30T18:39:34.091Z",
  13. "important": false
  14. },
  15. {
  16. "id": 3,
  17. "content": "GET and POST are the most important methods of HTTP protocol",
  18. "date": "2019-05-30T19:20:14.298Z",
  19. "important": true
  20. }
  21. ]
  22. }

让我们在浏览器中输入地址 http://localhost:3001/notes。 我们可以看到JSON-server 以 JSON 格式提供了我们之前写到文件的便笺:
image.png
JSONView插件可以格式化json数据

接下来,我们要实现从JSON-server获取数据和存储数据到JSON-server

The browser as a runtime environment

浏览器作为运行时环境
我们的第一个任务是从地址 http://localhost:3001/notes 获取已经存在的便笺到 React 应用。
旧方法用XMLHttpRequest获取数据,也称为使用 XHR 对象发出的 HTTP 请求:

  1. const xhttp = new XMLHttpRequest()
  2. xhttp.onreadystatechange = function() {
  3. if (this.readyState == 4 && this.status == 200) {
  4. const data = JSON.parse(this.responseText)
  5. // handle the response that is saved in variable data
  6. }
  7. }
  8. xhttp.open('GET', '/data.json', true)
  9. xhttp.send()

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

npm

我们将使用axios库来代替浏览器和服务器之间的通信。 它的功能类似于fetch,但是使用起来更友好。
现在,几乎所有的 JavaScript 项目都是使用node包管理器定义的,也就是npm
项目使用 npm 的一个明确的说明是位于项目根目录的package.json 文件

  1. {
  2. "name": "notes",
  3. "version": "0.1.0",
  4. "private": true,
  5. "dependencies": {
  6. "@testing-library/jest-dom": "^5.11.4",
  7. "@testing-library/react": "^11.1.0",
  8. "@testing-library/user-event": "^12.1.10",
  9. "react": "^17.0.2",
  10. "react-dom": "^17.0.2",
  11. "react-scripts": "4.0.3",
  12. "web-vitals": "^1.0.1"
  13. },
  14. "scripts": {
  15. "start": "react-scripts start",
  16. "build": "react-scripts build",
  17. "test": "react-scripts test",
  18. "eject": "react-scripts eject"
  19. },
  20. -- snip --
  21. }

dependencies 定义了项目的依赖
要使用 axios。 理论上,我们可以在package.json 文件中直接定义它,但最好是从命令行安装它

  1. npm install axios

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

安装完成后,axios被添加到了dependencies
除了将 axios 添加到依赖项之外,npm install 命令还下载了库代码。 与其他依赖项一样,代码可以在根目录中的node_modules 目录中找到。
image.png

通过执行如下命令将json-server 安装为开发依赖项(—save-dev代表仅在开发过程中使用)

  1. npm install json-server --save-dev

在script中添加一行

  1. "server": "json-server -p3001 --watch db.json"

image.png
此时,我们可以通过下面的命令启动 json-server

  1. npm run server
  2. # 或者
  3. yarn server

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

  1. npm install axios
  2. npm install json-server --save-dev

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

Yarn和npm命令对比

npm install yarn
npm install react —save yarn add react
npm uninstall react —save yarn remove react
npm install react —save-dev yarn add react —dev
npm update —save yarn upgrade
npm install -g react yarn global add react

Axios and Promises

打开2个终端,分别运行json-server和react项目
在项目的index.js中添加

  1. import axios from 'axios'
  2. const promise = axios.get('http://localhost:3001/notes')
  3. console.log(promise)
  4. const promise2 = axios.get('http://localhost:3001/foobar')
  5. console.log(promise2)

当index.js变化时,React不会自动感知;
在项目的根目录创建一个 .env 文件,并加上**FAST_REFRESH=false**, 重启项目。

可以看到上面2个promise一个fullfilled, 一个rejected

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兑现: 这意味着操作已经完成,最终的值是可用的,这通常是一个成功的操作。 这种状态有时也被称为resolve。
  • The promise is rejected拒绝:它意味着一个错误阻止了最终值,这通常表示一个失败操作。


如果我们想要访问Promise表示的操作的结果,那么必须为promise注册一个事件处理。 这是通过 .then方法实现的。

  1. const promise = axios.get('http://localhost:3001/notes')
  2. promise.then(response => {
  3. console.log(response)
  4. })

image.png
通常不单独声明一个promise变量,而是把.then方法直接写在axios调用方法后面

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

服务器返回的数据是纯文本,基本上只有一个长字符串。 Axios 库仍然能够将数据解析为一个 JavaScript 数组。

让我们从json-server获取数据并渲染到App,下面的代码会有问题,比如只有获得了response才能渲染App组件。

  1. import ReactDOM from 'react-dom';
  2. import App from './App';
  3. import axios from 'axios'
  4. axios
  5. .get('http://localhost:3001/notes')
  6. .then(response => {
  7. const notes = response.data
  8. ReactDOM.render(
  9. <App notes={notes} />,
  10. document.getElementById('root')
  11. );
  12. })

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.

fetch数据,设置订阅,手动更改DOM都可以使用Effect-hooks
将notes项目中的App.js修改如下:

  1. import React, { useState, useEffect } from 'react'
  2. import axios from 'axios'
  3. import Note from './components/Note'
  4. const App = () => {
  5. const [notes, setNotes] = useState([])
  6. const [newNote, setNewNote] = useState('')
  7. const [showAll, setShowAll] = useState(true)
  8. useEffect(() => {
  9. console.log('effect')
  10. axios
  11. .get('http://localhost:3001/notes')
  12. .then(response => {
  13. console.log('promise fulfilled')
  14. setNotes(response.data)
  15. })
  16. }, [])
  17. console.log('render', notes.length, 'notes')
  18. -- snip --
  19. }

会发现三个console.log一次输出如下:

  1. render 0 notes
  2. effect
  3. promise fulfilled
  4. render 3 notes

react先渲染了一次App组件,
然后执行useEffect,
获取到数据后setNotes(response.data)修改了状态,再次渲染App组件

让我们重写useEffect

  1. const hook = () => {
  2. console.log('effect')
  3. axios
  4. .get('http://localhost:3001/notes')
  5. .then(response => {
  6. console.log('promise fulfilled')
  7. setNotes(response.data)
  8. })
  9. }
  10. useEffect(hook, [])

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

我的思考:useEffect解决了上面要获取到response才能渲染App组件的问题

exercise 2.12 - 2.14

REST API
API https://restcountries.eu 以机器可读的格式,提供了不同国家的大量数据。即所谓的 REST API。
创建一个应用,可以查看不同国家的数据。 应用能从all中获取数据。

控制台要始终开着, 不然有错误都不知道
遍历数组用for(let i of arr)

  1. import React, { useEffect, useState } from 'react'
  2. import axios from 'axios'
  3. const Languages = ({ lan }) => (
  4. <>
  5. <h1>Languages</h1>
  6. <ul>{lan.map(item => <li key={item.nativeName}>{item.nativeName}</li>)}</ul>
  7. </>
  8. )
  9. const ShowCountries = ({ countries, allData }) => {
  10. if (countries.length > 10) {
  11. return <div>Too many matches, specify another filter</div>
  12. } else if (countries.length === 1) {
  13. for (let item of allData) {
  14. if (item.name === countries[0]) {
  15. return (
  16. <div>
  17. <h1>{item.name}</h1>
  18. <div>capital: {item.capital}</div>
  19. <div>population: {item.population}</div>
  20. <Languages lan={item.languages} />
  21. <div><img src={item.flag} alt='flag' width="500" /></div>
  22. </div>
  23. )
  24. }
  25. }
  26. return <div>hahaha</div>
  27. } else {
  28. return countries.map(item => <div key={item}>{item}</div>)
  29. }
  30. }
  31. const App = () => {
  32. const [allData, setAllData] = useState([])
  33. const [allCountries, setAllCountries] = useState([])
  34. const [countries, setCountries] = useState([])
  35. useEffect(() => {
  36. axios
  37. .get('https://restcountries.eu/rest/v2/all')
  38. .then(response => {
  39. console.log(response)
  40. setAllData(response.data)
  41. setAllCountries(response.data.map(item => item.name))
  42. })
  43. }, [])
  44. const handleInputChange = (event) => {
  45. setCountries(allCountries.filter(item => item.indexOf(event.target.value) >= 0))
  46. }
  47. return (
  48. <>
  49. <div>find countries: <input type='text' onChange={handleInputChange}></input></div>
  50. <ShowCountries countries={countries} allData={allData} />
  51. </>
  52. )
  53. }
  54. export default App;

使用天气api

有几十个天气数据提供商。 我用了https://weatherstack.com/
几乎所有气象服务都需要 api-key。 不要将 api-key 保存到源代码管理Git中! 也不能将 api-key 硬编码到源代码中。 取而代之的是使用环境变量来保存密钥。
假设 api-key 是t0p53cr3t4p1k3yv4lu3,可以在启动应用时提供api-key:

  1. REACT_APP_API_KEY='t0p53cr3t4p1k3yv4lu3' npm start // For Linux/macOS Bash
  2. ($env:REACT_APP_API_KEY='t0p53cr3t4p1k3yv4lu3') -and (npm start) // For Windows PowerShell
  3. set REACT_APP_API_KEY='t0p53cr3t4p1k3yv4lu3' && npm start // For Windows cmd.exe

您可以从 process.env 对象访问密钥的值:

  1. const api_key = process.env.REACT_APP_API_KEY
  2. // variable api_key has now the value set in startup

如果你使用npx create-react-app …创建了应用,并且想要为环境变量使用其他名称,则环境变量必须以REACTAPP开头。你还可以通过在项目中创建一个名为.env的文件并添加以下内容来使用’.env’ 文件,而不是每次都在命令行中定义。

  1. # .env
  2. REACT_APP_API_KEY=t0p53cr3t4p1k3yv4lu3

在axios中使用天气api

  1. axios
  2. .get(`http://api.weatherstack.com/current?access_key=${process.env.REACT_APP_API_KEY}&query=${item.capital}`)
  3. .then(response => {
  4. setWeather(response.data)
  5. })

参考

什么是REST?