JSON服务器
从本节开始研究“后端”,即服务器端功能
我们使用JSON服务器作为我们的服务器
Json-server 将所有数据存储在服务器上的db.json 文件中。 在现实世界中,数据会存储在某种数据库中。 然而,json-server 是一个方便的工具,可以在开发阶段使用服务器端功能,而不需要编写任何程序。
你可以全局安装JSON服务器
npm install -g json-server
全局安装不是必须的。因为我们可以在应用的根目录使用 npx 命令运行json-server
默认情况下,json-server在端口3000上启动; 但是由于 create-react-app 项目设置了3000端口,因此我们必须为 json-server 定义一个备用端口,比如端口3001。
npx json-server --port 3001 --watch db.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
}
]
}
让我们在浏览器中输入地址 http://localhost:3001/notes。 我们可以看到JSON-server 以 JSON 格式提供了我们之前写到文件的便笺:
JSONView插件可以格式化json数据
接下来,我们要实现从JSON-server获取数据和存储数据到JSON-server
The browser as a runtime environment
浏览器作为运行时环境
我们的第一个任务是从地址 http://localhost:3001/notes 获取已经存在的便笺到 React 应用。
旧方法用XMLHttpRequest获取数据,也称为使用 XHR 对象发出的 HTTP 请求:
const xhttp = new XMLHttpRequest()
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
const data = JSON.parse(this.responseText)
// handle the response that is saved in variable data
}
}
xhttp.open('GET', '/data.json', true)
xhttp.send()
使用 XHR已经不再推荐了,而且浏览器已经广泛支持基于所谓的promises的fetch方法,而不是 XHR 使用的事件驱动模型。
npm
我们将使用axios库来代替浏览器和服务器之间的通信。 它的功能类似于fetch,但是使用起来更友好。
现在,几乎所有的 JavaScript 项目都是使用node包管理器定义的,也就是npm
项目使用 npm 的一个明确的说明是位于项目根目录的package.json 文件
{
"name": "notes",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
-- snip --
}
dependencies 定义了项目的依赖
要使用 axios。 理论上,我们可以在package.json 文件中直接定义它,但最好是从命令行安装它
npm install axios
注意:npm-commands 应该始终在项目根目录中运行,在这个目录中可以找到package.json 文件。
安装完成后,axios被添加到了dependencies中
除了将 axios 添加到依赖项之外,npm install 命令还下载了库代码。 与其他依赖项一样,代码可以在根目录中的node_modules 目录中找到。
通过执行如下命令将json-server 安装为开发依赖项(—save-dev代表仅在开发过程中使用)
npm install json-server --save-dev
在script中添加一行
"server": "json-server -p3001 --watch db.json"
此时,我们可以通过下面的命令启动 json-server
npm run server
# 或者
yarn server
我们使用了两次 npm 安装命令,但是有一点不同:
npm install axios
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中添加
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)
当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方法实现的。
const promise = axios.get('http://localhost:3001/notes')
promise.then(response => {
console.log(response)
})
通常不单独声明一个promise变量,而是把.then方法直接写在axios调用方法后面
axios
.get('http://localhost:3001/notes')
.then(response => {
const notes = response.data
console.log(notes)
})
服务器返回的数据是纯文本,基本上只有一个长字符串。 Axios 库仍然能够将数据解析为一个 JavaScript 数组。
让我们从json-server获取数据并渲染到App,下面的代码会有问题,比如只有获得了response才能渲染App组件。
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')
);
})
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修改如下:
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')
-- snip --
}
会发现三个console.log一次输出如下:
render 0 notes
effect
promise fulfilled
render 3 notes
react先渲染了一次App组件,
然后执行useEffect,
获取到数据后setNotes(response.data)修改了状态,再次渲染App组件
让我们重写useEffect
const hook = () => {
console.log('effect')
axios
.get('http://localhost:3001/notes')
.then(response => {
console.log('promise fulfilled')
setNotes(response.data)
})
}
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)
import React, { useEffect, useState } from 'react'
import axios from 'axios'
const Languages = ({ lan }) => (
<>
<h1>Languages</h1>
<ul>{lan.map(item => <li key={item.nativeName}>{item.nativeName}</li>)}</ul>
</>
)
const ShowCountries = ({ countries, allData }) => {
if (countries.length > 10) {
return <div>Too many matches, specify another filter</div>
} else if (countries.length === 1) {
for (let item of allData) {
if (item.name === countries[0]) {
return (
<div>
<h1>{item.name}</h1>
<div>capital: {item.capital}</div>
<div>population: {item.population}</div>
<Languages lan={item.languages} />
<div><img src={item.flag} alt='flag' width="500" /></div>
</div>
)
}
}
return <div>hahaha</div>
} else {
return countries.map(item => <div key={item}>{item}</div>)
}
}
const App = () => {
const [allData, setAllData] = useState([])
const [allCountries, setAllCountries] = useState([])
const [countries, setCountries] = useState([])
useEffect(() => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
console.log(response)
setAllData(response.data)
setAllCountries(response.data.map(item => item.name))
})
}, [])
const handleInputChange = (event) => {
setCountries(allCountries.filter(item => item.indexOf(event.target.value) >= 0))
}
return (
<>
<div>find countries: <input type='text' onChange={handleInputChange}></input></div>
<ShowCountries countries={countries} allData={allData} />
</>
)
}
export default App;
使用天气api
有几十个天气数据提供商。 我用了https://weatherstack.com/
几乎所有气象服务都需要 api-key。 不要将 api-key 保存到源代码管理Git中! 也不能将 api-key 硬编码到源代码中。 取而代之的是使用环境变量来保存密钥。
假设 api-key 是t0p53cr3t4p1k3yv4lu3,可以在启动应用时提供api-key:
REACT_APP_API_KEY='t0p53cr3t4p1k3yv4lu3' npm start // For Linux/macOS Bash
($env:REACT_APP_API_KEY='t0p53cr3t4p1k3yv4lu3') -and (npm start) // For Windows PowerShell
set REACT_APP_API_KEY='t0p53cr3t4p1k3yv4lu3' && npm start // For Windows cmd.exe
您可以从 process.env 对象访问密钥的值:
const api_key = process.env.REACT_APP_API_KEY
// variable api_key has now the value set in startup
如果你使用npx create-react-app …创建了应用,并且想要为环境变量使用其他名称,则环境变量必须以REACTAPP开头。你还可以通过在项目中创建一个名为.env的文件并添加以下内容来使用’.env’ 文件,而不是每次都在命令行中定义。
# .env
REACT_APP_API_KEY=t0p53cr3t4p1k3yv4lu3
在axios中使用天气api
axios
.get(`http://api.weatherstack.com/current?access_key=${process.env.REACT_APP_API_KEY}&query=${item.capital}`)
.then(response => {
setWeather(response.data)
})