目录
The browser as a runtime environment
The development runtime environment
此前一直围绕“前端” ,即客户端(浏览器)功能。 之后将开始研究“后端” ,即服务器端功能。 先熟悉在浏览器中执行的代码如何与后端通信。
使用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
}
]
}
可以使用命令 npm install -g json-server在自己的电脑上安装 JSON 服务器。
在应用的根目录使用 npx 命令运行json-server:
npx json-server --port 3001 --watch db.json
默认情况下,json-server在端口3000上启动; 但是由于 create-react-app 项目设置了3000端口,因此必须为 json-server 定义一个备用端口,比如端口3001。
在浏览器中输入地址 http://localhost:3001/notes,可以看到JSON-server 以 JSON 格式提供了之前写到文件的便笺:
浏览器安装JSONView 可以格式化 json 数据的显示。
接下来将便笺保存到服务器,即将便笺保存到 json-server。 React代码从服务器获取便笺并将其渲染到屏幕上。 无论何时向应用添加新便笺,React 代码都会将其发送到服务器,以使新便笺保存在“内存”中。
Json-server 将所有数据存储在服务器上的db.json 文件中。 实际上,数据会存储在某种数据库中。 而json-server 是一个方便的工具,可以在开发阶段使用服务器端功能,而不需要编写任何程序。
The browser as a runtime environment
【浏览器作为一个运行时环境】
现在从地址 http://localhost:3001/notes 获取已经存在的便笺到 React 应用。
使用XMLHttpRequest获取数据,也称为使用 XHR 对象发出的 HTTP 请求。 这是1999年引入的一项技术,已经不再推荐了。浏览器已经广泛支持基于所谓的promises的fetch方法,而不是 XHR 使用的事件驱动模型。
使用 XHR 获取数据的方式如下:
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()
在开始时,我们将一个事件处理程序 注册到表示 HTTP 请求的xhttp对象,当 xhttp对象的状态发生变化时,JavaScript 运行时将调用该对象。 如果状态的变化意味着对请求的响应已经到达,那么数据将得到相应的处理。
事件处理中的代码是在请求发送到服务器之前定义的。 尽管如此,事件处理中的代码将在稍后的时间点执行。 因此,代码并不是“从顶部到底部”同步执行,而是异步执行。 JavaScript 调用了事件处理,而这个事件处理是在之前某个时刻注册的。
另一方面,JavaScript 引擎,或者运行时环境,遵循异步模型asynchronous model.。 原则上,这要求所有的IO-操作(除了一些例外)都以非阻塞方式执行。 这意味着代码执行在调用 IO 函数之后立即继续,而不需要等待它返回。
当一个异步操作完成时,在它完成之后的某个时刻,JavaScript 引擎才调用注册到该操作的事件处理。
目前,JavaScript 引擎是单线程的,这意味着它们不能并行执行代码。 因此,在实践中需要使用非阻塞模型来执行 IO 操作。
这种单线程的 JavaScript 引擎的另一个后果是,如果某些代码的执行占用了大量的时间,那么浏览器将在执行期间停滞不前。 如果我们在应用顶部添加如下代码:
setTimeout(() => {
console.log('loop..')
let i = 0
while (i < 50000000000) {
i++
}
console.log('end')
}, 5000)
一切正常运转5秒钟。 但是,当运行定义为 setTimeout 参数的函数时,浏览器将在长循环执行期间停止。 即使是浏览器的标签也不能在循环执行期间关闭,至少在 Chrome 中不能。
为了让浏览器保持responsive响应性,即能够以足够的速度连续地对用户操作作出反应,代码逻辑需要让任何单一的计算都不会花费太长的时间。
关于这个议题的补充材料,参考What the heck is the event loop anyway?
当今的浏览器中可以在所谓的 web workers 的帮助下运行并行化的代码。 然而,单个浏览器窗口的事件循环仍然是由一个单线程处理。
npm
从服务器获取数据。
可以使用前面提到的基于promise的fetch函数从服务器中获取数据。 fetch是是标准化工具,所有现代浏览器(不包括 IE,因为它不是)都支持它。
使用axios库来代替浏览器和服务器之间的通信。 它的功能类似于fetch,但是使用起来更友好。
目前几乎所有的 JavaScript 项目都是使用node包管理器定义的,也就是npm(Node Package Manager)。
使用 create-react-app 创建的项目也遵循 npm 格式,位于根目录的package.json 文件可以说明该项目使用了npm:
{
"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 部分定义了项目具有的依赖dependencies 或外部库。
现在使用 axios。 理论上可以在package.json 文件中直接定义它,但最好是从命令行安装它。
npm install axios
注意: npm-commands 应该始终在项目根目录中运行,在这个目录中可以找到package.json 文件。
Axios 现在被包含在依赖中了:
{
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.2.1",
"axios": "^0.19.2", "react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "3.3.0"
},
// ...
}
除了将 axios 添加到依赖项之外,npm install 命令还下载了库代码。 与其他依赖项一样,代码可以在根目录中的node_modules 目录中找到。
通过执行如下命令将json-server 安装为开发依赖项(仅在开发过程中使用) :
npm install json-server --save-dev
在package.json 文件的scripts部分添加一个小的修改:
{
// ...
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"server": "json-server -p3001 --watch db.json" },
}
现在可以在没有参数定义的情况下方便地使用如下命令从项目根目录启动 json-server:
npm run server
注意: 在启动新服务器之前,以前启动的 json-server必须终止,否则会出现问题:
错误信息中的红色打印提示我们这个问题的原因:
Cannot bind to the port 3001. Please specify another port number either through --port argument or through the json-server.json configuration file 不能绑定到3001端口。 请通过 -- port 参数或通过 json-server.json 配置文件指定另一个端口号。
应用不能将自己绑定到端口。 原因是端口3001已经被先前启动的 json-server 占用了。
我们使用了两次 npm 安装命令,但是有一点不同:
npm install axios
npm install json-server --save-dev
参数之间有细微的差别。axios 被安装为应用的运行时依赖项 (--save),因为程序的执行需要库的存在。 而另一个, json-server 是作为开发依赖项(--save-dev)安装的,因为程序本身并不需要它。 它用于在软件开发过程中提供帮助。
Axios and promises
现在可以使用 axios 了。让json-server跑在3001端口。
同时运行 json-server 和react 应用需要使用两个terminal 窗口。一个用来保持json-server 的运行,另一个来跑react应用。
axios可以像 React那样 import 。
将如下内容添加到文件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)
打开浏览器访问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兑现: 这意味着操作已经完成,最终的值是可用的,这通常是一个成功的操作。 这种状态有时也被称为resolve。
-
The promise is rejected拒绝:它意味着一个错误阻止了最终值,这通常表示一个失败操作。
我们示例中的第一个promise是fulfilled,表示一个成功的axios.get('http://localhost:3001/notes') 请求。
而第二个是rejected,控制台告诉我们试图向一个不存在的地址发出了HTTP GET 请求。
如果想要访问promise表示的操作的结果,那么必须为promise注册一个事件处理。 这是通过 then方法实现的:
const promise = axios.get('http://localhost:3001/notes')
promise.then(response => {
console.log(response)
})
The following is printed to the console: 下面的代码打印到控制台:
JavaScript 运行时环境调用由 then 方法注册的回调函数,并提供一个response 对象作为参数。response 对象包含与 HTTP GET 请求响应相关的所有基本数据,也包括返回的data、status code 和headers。
通常没有必要将 promise 对象存储在一个变量中,而将 then方法调用链到 axios 方法调用是很常见的,因此它可以直接跟在 axios 方法调用后面:
axios.get('http://localhost:3001/notes').then(response => {
const notes = response.data
console.log(notes)
})
回调函数获取了响应中包含的数据,将其存储在一个变量中,并将便笺打印到控制台。
要格式化chained 方法调用,以一种更易读的方法是将每个调用放在独立的行上:
axios
.get('http://localhost:3001/notes')
.then(response => {
const notes = response.data
console.log(notes)
})
服务器返回的数据是纯文本,基本上只有一个长字符串。 Axios 库仍然能够将数据解析为一个 JavaScript 数组,因为服务器使用content-type 头指定数据格式为application/json; charset=utf-8。
现在可以开始使用从服务器获取的数据了。从我们本地服务器请求 Notes 并渲染,但这种方法有许多问题,比如我们只有将整个App 渲染完成后才会得到成功的response :
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')
)
})
Effect-hooks
与 React version 16.8.0一起引入的 state hooks为 React 组件提供了定义为函数的状态,也就是所谓的 函数式组件 。 16.8.0版本还引入了 effect hooks 新特性:
The Effect Hook lets you perform side effects in function components. Effect Hook Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects.
Effect Hook 允许在函数组件中执行其他功能, 比如数据获取、设置订阅和手动更改 React 组件中的 DOM。
从index.js 中删除数据的获取逻辑。由于我们需要从服务端获取notes, 不再需要将数据作为props传递给App 组件。 所以我将 index.js 简化为:
ReactDOM.render(<App />, document.getElementById('root'))
App组件更改如下:
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')
// ...
}
添加了一些有用的打印来清晰执行的进程。
这是打印到控制台的内容
render 0 notes
effect
promise fulfilled
render 3 notes
首先执行定义组件的函数体,并首次渲染组件。 此时打印 render 0 notes ,这意味着还没有从服务器获取数据。
下面的函数,或者说React 的 effect:
() => {
console.log('effect')
axios
.get('http://localhost:3001/notes')
.then(response => {
console.log('promise fulfilled')
setNotes(response.data)
})
}
在渲染完成后会立即执行。 函数的执行结果是effect 被打印到控制台,axios.get 命令从服务器获取到数据,并将如下函数注册为事件处理:
response => {
console.log('promise fulfilled')
setNotes(response.data)
})
当数据从服务器到达时,JavaScript 运行时会调用注册为事件处理的函数,该函数将promise fulfilled 输出到控制台,并使用函数setNotes(response.data) 将从服务器接收的便笺存储到状态中。
对状态更新函数的调用会触发组件的重新渲染。 结果,render 3 notes 被打印到控制台,从服务器获取的便笺被显示到屏幕上。
整体看一下 effect hook :
useEffect(() => {
console.log('effect')
axios
.get('http://localhost:3001/notes').then(response => {
console.log('promise fulfilled')
setNotes(response.data)
})
}, [])
重写一下代码。
const hook = () => {
console.log('effect')
axios
.get('http://localhost:3001/notes')
.then(response => {
console.log('promise fulfilled')
setNotes(response.data)
})
}
useEffect(hook, [])
现在可以更清楚地看到函数 useEffect 实际上需要两个参数 。第一个是函数本身。 根据文档描述:
By default, effects run after every completed render, but you can choose to fire it only when certain values have changed.
默认情况下,effects 在每次渲染完成后运行,但是你可以选择只在某些值发生变化时才调用。
因此,默认情况下,effect是总是 在组件渲染之后才运行。 然而我们只想在第一次渲染的时候执行这个效果。
useEffect的第二个参数用于指定effect运行的频率。 如果第二个参数是一个空数组 [],那么这个effect只在组件的第一次渲染时运行。
除了从服务器获取数据之外,Effect-Hook还有许多用例。
也可以这样编写 effect 函数的代码:
useEffect(() => {
console.log('effect')
const eventHandler = response => {
console.log('promise fulfilled')
setNotes(response.data)
}
const promise = axios.get('http://localhost:3001/notes')
promise.then(eventHandler)
}, [])
对事件处理函数的引用被分配给变量eventHandler。 Axios 的get方法返回的promise存储在变量 promise 中。 回调的注册是通过将 eventHandler变量作为参数 (事件处理函数的引用)传递给promise 的 then 方法的来实现的。
useEffect(() => {
console.log('effect')
axios
.get('http://localhost:3001/notes')
.then(response => {
console.log('promise fulfilled')
setNotes(response.data)
})
}, [])
但现在添加的新便笺还不能存储在服务器上。
The development runtime environment
【开发的运行时环境】
下图描述了应用的组成
构成 React 应用的 JavaScript 代码在浏览器中运行。 浏览器从React dev server 获取 JavaScript,这是运行 npm start 命令后运行的应用。 dev-server 将 JavaScript 转换成浏览器可以理解的格式。 除此之外,它还将来自不同文件的 JavaScript 整合到一个文件中。
在浏览器中运行的 React 应用从计算机3001端口上运行的JSON-server 获取 JSON 格式的数据。 Json-server 从db.json 文件中获取数据。