React 从入门到入土(一)-- 基础知识以及 jsx 语法
先附上React官网 ,有很多问题都要通过查询官方文档来解决,要学会查文档~
一、React 简介
1. 关于 React
整几个面试题来认识一下~~
什么是 React ?
React 是一个用于构建用户界面的 JavaScript 库。
- 是一个将数据渲染为 HTML 视图的开源 JS 库
- 它遵循基于组件的方法,有助于构建可重用的 UI 组件
- 它用于开发复杂的交互式的 web 和移动 UI
React 有什么特点?
- 使用虚拟 DOM 而不是真正的 DOM
- 它可以用服务器渲染
- 它遵循单向数据流或数据绑定
- 高效
- 声明式编码,组件化编码
React 的一些主要优点?
- 它提高了应用的性能
- 可以方便在客户端和服务器端使用
- 由于使用 JSX,代码的可读性更好
- 使用React,编写 UI 测试用例变得非常容易
2. Hello React
首先需要引入几个 react 包,我直接用的是老师下载好的
- React 核心库、操作 DOM 的 react 扩展库、将 jsx 转为 js 的 babel 库
const VDOM = <h1>Hello,React</h1>
ReactDOM.render(VDOM,document.querySelector(".test"))
3. 虚拟 DOM 和真实 DOM 的两种创建方法
3.1 JS 创建虚拟 DOM
//1.创建虚拟DOM,创建嵌套格式的dom
const VDOM=React.createElement('h1',{id:'title'},React.createElement('span',{},'hello,React'))
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM,document.querySelector('.test'))
3.2 Jsx 创建虚拟DOM
//1.创建虚拟DOM
const VDOM = ( /* 此处一定不要写引号,因为不是字符串 */
<h1 id="title">
<span>Hello,React</span>
</h1>
)
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM,document.querySelector('.test'))
js 的写法并不是常用的,常用jsx来写,毕竟JSX更符合书写的习惯
二、jsx 语法
-
定义虚拟DOM,不能使用
“” -
标签中混入JS表达式的时候使用
{}
id = {myId.toUpperCase()}
-
样式的类名指定不能使用class,使用
className -
内敛样式要使用
{{}}包裹
style={{color:'skyblue',fontSize:'24px'}}
-
不能有多个根标签,只能有一个根标签
-
标签必须闭合,自闭合也行
-
如果小写字母开头,就将标签转化为 html 同名元素,如果 html 中无该标签对应的元素,就报错;如果是大写字母开头,react 就去渲染对应的组件,如果没有就报错
记几个
1. 注释
写在花括号里
ReactDOM.render(
<div>
<h1>小丞</h1>
{/*注释...*/}
</div>,
document.getElementById('example')
);
2. 数组
JSX 允许在模板中插入数组,数组自动展开全部成员
var arr = [
<h1>小丞</h1>,
<h2>同学</h2>,
];
ReactDOM.render(
<div>{arr}</div>,
document.getElementById('example')
);
tip: JSX 小练习
根据动态数据生成 li
const data = ['A','B','C']
const VDOM = (
<div>
<ul>
{
data.map((item,index)=>{
return <li key={index}>{item}</li>
})
}
</ul>
</div>
)
ReactDOM.render(VDOM,document.querySelector('.test'))
React 从入门到入土(二)-- 面向组件编程
一、组件的使用
当应用是以多组件的方式实现,这个应用就是一个组件化的应用
注意:
组件名必须是首字母大写
虚拟DOM元素只能有一个根元素
虚拟DOM元素必须有结束标签
< />
渲染类组件标签的基本流程
-
React 内部会创建组件实例对象
-
调用
render()得到虚拟 DOM ,并解析为真实 DOM -
插入到指定的页面元素内部
1. 函数式组件
//1.先创建函数,函数可以有参数,也可以没有,但是必须要有返回值 返回一个虚拟DOM
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
//2.进行渲染
ReactDOM.Render(<Welcom name = "ljc" />,document.getElementById("div"));
上面的代码经历了以下几步
- 我们调用
ReactDOM.render()函数,并传入<Welcome name="ljc" />作为参数。 - React 调用
Welcome组件,并将{name: 'ljc'}作为 props 传入。 Welcome组件将Hello, ljc元素作为返回值。- React DOM 将 DOM 高效地更新为
Hello,ljc。
2. 类式组件
class MyComponent extends React.Component {
state = {isHot:false}
render() {
const {isHot} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot?'炎热':'凉爽'}</h1>
}
changeWeather = ()=>{
const isHot = this.state.isHot
this.setState({isHot:!isHot})
}
}
ReactDOM.render(<MyComponent/>,document.querySelector('.test'))
这玩意底层不简单,this的指向真的需要好好学习
在优化过程中遇到的问题
- 组件中的 render 方法中的 this 为组件实例对象
- 组件自定义方法中由于开启了严格模式,this 指向
undefined如何解决- 通过 bind 改变 this 指向
- 推荐采用箭头函数,箭头函数的
this指向
- state 数据不能直接修改或者更新
3. 其他知识
包含表单元素的组件分为非受控租价与受控组件
- 受控组件:表单组件的输入组件随着输入并将内容存储到状态中(随时更新)
- 非受控组件:表单组件的输入组件的内容在有需求的时候才存储到状态中(即用即取)
二、组件实例三大属性
1. state
React 把组件看成是一个状态机(State Machines)。通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。
React 里,只需更新组件的 state,然后根据新的 state 重新渲染用户界面(不要操作 DOM)。
简单的说就是组件的状态,也就是该组件所存储的数据
类式组件中的使用
使用的时候通过this.state调用state里的值
在类式组件中定义state
- 在构造器中初始化
state - 在类中添加属性
state来初始化
修改 state
在类式组件的函数中,直接修改state值
this.state.weather = '凉爽'
页面的渲染靠的是
render函数
这时候会发现页面内容不会改变,原因是 React 中不建议 state不允许直接修改,而是通过类的原型对象上的方法 setState()
setState()
this.setState(partialState, [callback]);
partialState: 需要更新的状态的部分对象callback: 更新完状态后的回调函数
有两种写法:写法1
this.setState({
weather: "凉爽"
})
写法2:
// 传入一个函数,返回x需要修改成的对象,参数为当前的 state
this.setState(state => ({count: state.count+1});
setState是一种合并操作,不是替换操作
- 在执行
setState操作后,React 会自动调用一次render() render()的执行次数是 1+n (1 为初始化时的自动调用,n 为状态更新的次数)
2. props
与state不同,state是组件自身的状态,而props则是外部传入的数据
类式组件中使用
在使用的时候可以通过 this.props来获取值 类式组件的 props:
- 通过在组件标签上传递值,在组件中就可以获取到所传递的值
- 在构造器里的
props参数里可以获取到props - 可以分别设置
propTypes和defaultProps两个属性来分别操作props的规范和默认值,两者都是直接添加在类式组件的原型对象上的(所以需要添加static) - 同时可以通过
...运算符来简化
函数式组件中的使用
函数在使用props的时候,是作为参数进行使用的(props)
函数组件的 props定义:
- 在组件标签中传递
props的值 - 组件函数的参数为
props - 对
props的限制和默认值同样设置在原型对象上
3. refs
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
在我们正常的操作节点时,需要采用DOM API 来查找元素,但是这样违背了 React 的理念,因此有了
refs
有三种操作refs的方法,分别为:
- 字符串形式
- 回调形式
createRef形式
字符串形式refs
虽然这个方法废弃了,但是还能用,还很好用hhh~
回调形式的refs
组件实例的ref属性传递一个回调函数c => this.input1 = c (箭头函数简写),这样会在实例的属性中存储对DOM节点的引用,使用时可通过this.input1来使用
使用方法
<input ref={c => this.input1 = c } type="text" placeholder="点击按钮提示数据"/>
我的理解
c会接收到当前节点作为参数,ref的值为函数的返回值,也就是this.input1 = c,因此是给实例下的input1赋值
createRef 形式(推荐写法)
React 给我们提供了一个相应的API,它会自动的将该 DOM 元素放入实例对象中
我们先给DOM元素添加ref属性
<input ref={this.MyRef} type="text" placeholder="点击弹出" />
<input ref={this.MyRef1} type="text" placeholder="点击弹出" />
通过API,创建React的容器,会将DOM元素赋值给实例对象的名称为容器的属性的current,好烦..
MyRef = React.createRef();
MyRef1 = React.createRef();
注意:专人专用,好烦,一个节点创建一个容器
//调用
btnOnClick = () =>{
//创建之后,将自身节点,传入current中
console.log(this.MyRef.current.value);
}
注意:我们不要过度的使用 ref,如果发生时间的元素刚好是需要操作的元素,就可以使用事件对象去替代。过度使用有什么问题我也不清楚,可能有 bug 吧
4. 事件处理
-
React 使用的是自定义事件,而不是原生的 DOM 事件
-
React 的事件是通过事件委托方式处理的(为了更加的高效)
-
可以通过事件的
event.target获取发生的 DOM 元素对象,可以尽量减少refs的使用
三、高阶函数
关于这部分的知识,之前的笔记有记过了,我真是太棒了
链接高阶函数,关于AOP,偏函数,柯里化都有不错的记录,感觉还是不错的
React 入门(三) -- 生命周期 LifeCycle
在 React 中为我们提供了一些生命周期钩子函数,让我们能在 React 执行的重要阶段,在钩子函数中做一些事情。那么在 React 的生命周期中,有哪些钩子函数呢,我们来总结一下
React 生命周期
React 生命周期主要包括三个阶段:初始化阶段,更新阶段,销毁阶段
初始化阶段
1. constructor 执行
constructor 在组件初始化的时候只会执行一次
通常它用于做这两件事
- 初始化函数内部
state - 绑定函数
constructor(props) {
console.log('进入构造器');
super(props)
this.state = { count: 0 }
}
现在我们通常不会使用 constructor 属性,而是改用类加箭头函数的方法,来替代 constructor
例如,我们可以这样初始化 state
state = {
count: 0
};
2. static getDerivedStateFromProps 执行 (新钩子)
这个是 React 新版本中新增的2个钩子之一,据说很少用。
getDerivedStateFromProps 在初始化和更新中都会被调用,并且在 render 方法之前调用,它返回一个对象用来更新 state
getDerivedStateFromProps 是类上直接绑定的静态(static)方法,它接收两个参数 props 和 state
props 是即将要替代 state 的值,而 state 是当前未替代前的值
注意:
state的值在任何时候都取决于传入的props,不会再改变
如下
static getDerivedStateFromProps(props) {
return props
}
ReactDOM.render(<Count count="109"/>,document.querySelector('.test'))
count 的值不会改变,一直是 109
2. componentWillMount 执行(即将废弃)
如果存在
getDerivedStateFromProps和getSnapshotBeforeUpdate就不会执行生命周期componentWillMount。
该方法只在挂载的时候调用一次,表示组件将要被挂载,并且在 render 方法之前调用。
这个方法在 React 18版本中将要被废弃,官方解释是在 React 异步机制下,如果滥用这个钩子可能会有 Bug
3. render 执行
render() 方法是组件中必须实现的方法,用于渲染 DOM ,但是它不会真正的操作 DOM,它的作用是把需要的东西返回出去。
实现渲染 DOM 操作的是 ReactDOM.render()
注意:避免在
render中使用setState,否则会死循环
4. componentDidMount 执行
componentDidMount 的执行意味着初始化挂载操作已经基本完成,它主要用于组件挂载完成后做某些操作
这个挂载完成指的是:组件插入 DOM tree
初始化阶段总结
执行顺序 constructor -> getDerivedStateFromProps 或者 componentWillMount -> render -> componentDidMount
更新阶段
这里记录新生命周期的流程
1. getDerivedStateFromProps 执行
执行生命周期getDerivedStateFromProps, 返回的值用于合并 state,生成新的state。
2. shouldComponentUpdat 执行
shouldComponentUpdate() 在组件更新之前调用,可以通过返回值来控制组件是否更新,允许更新返回 true ,反之不更新
3. render 执行
在控制是否更新的函数中,如果返回 true 才会执行 render ,得到最新的 React element
4. getSnapshotBeforeUpdate 执行
在最近一次的渲染输出之前被提交之前调用,也就是即将挂载时调用
相当于淘宝购物的快照,会保留下单前的商品内容,在 React 中就相当于是 即将更新前的状态
它可以使组件在 DOM 真正更新之前捕获一些信息(例如滚动位置),此生命周期返回的任何值都会作为参数传递给
componentDidUpdate()。如不需要传递任何值,那么请返回 null
5. componentDidUpdate 执行
组件在更新完毕后会立即被调用,首次渲染不会调用
到此更新阶段就结束了,在 React 旧版本中有两个与更新有关的钩子函数 componentWillReceiveProps 和 componentWillUpdate 都即将废弃
componentWillReceiveProps 我不太懂
componentWillUpdate 在 render 之前执行,表示组件将要更新
销毁阶段
componentWillUnmount 执行
在组件即将被卸载或销毁时进行调用。
总结
初始化
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
更新
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
销毁
- componentWillUnmount()
React入门学习(四)-- diffing 算法
前言
diff 算法是 React 提升渲染性能的一种优化算法,在 React 中有着很重要的地位,也不止于 React ,在 Vue 中也有 diff 算法,似乎没有差别。在最近的 React 学习中,学到了 diff 算法,感觉视频中的内容有点浅,对 diff 算法不够深入,因此想要深入的了解以下 diff 算法。于是在掘金,知乎,CSDN 等平台上,看了大量的博客,都非常地不错,可惜看不明白,wwww。所以这篇文章只是自己对于 diff 算法的一点理解,有什么问题或者错误的地方,大家一定要指出!
什么是虚拟 DOM ?
在谈 diff 算法之前,我们需要先了解虚拟 DOM 。它是一种编程概念,在这个概念里,以一种虚拟的表现形式被保存在内存中。在 React 中,render 执行的结果得到的并不是真正的 DOM 节点,而是 JavaScript 对象
虚拟 DOM 只保留了真实 DOM 节点的一些基本属性,和节点之间的层次关系,它相当于建立在 JavaScript 和 DOM 之间的一层“缓存”
<div class="hello">
<span>hello world!</span>
</div>
上面的这段代码会转化可以转化为虚拟 DOM 结构
{
tag: "div",
props: {
class: "hello"
},
children: [{
tag: "span",
props: {},
children: ["hello world!"]
}]
}
其中对于一个节点必备的三个属性 tag,props,children
- tag 指定元素的标签类型,如“
li,div” - props 指定元素身上的属性,如
class,style,自定义属性 - children 指定元素是否有子节点,参数以数组形式传入
而我们在 render 中编写的 JSX 代码就是一种虚拟 DOM 结构。
什么是 diff 算法?
其实刚开始学习 React 的时候,很多人可能都听说过 React 很高效,性能很好这类的话语,这其实就是得益于 diff 算法和 Virturl DOM 的完美结合。
单纯的我刚开始会认为
React 也只不过是引入了别人的 diff 算法而已,能有多厉害,又不是原创 ?
但当我查阅了众多资料后,发现被提及最多的是一个 “传统 diff 算法”
其实 React 针对 diff 算法做出的优化,才是我们应当学习的
React 将原先时间复杂度为 O() 的传统算法,优化到了 O(n)
大致执行过程图
那 React 是如何实现的呢?
三个策略
为了将复杂度降到 O(n),React 基于这三个策略进行了算法优化
- Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
- 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
针对这三个策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化
tree diff 分层求异
首先会将新旧两个 DOM 树,进行比较,这个比较指的是分层比较。又由于 DOM 节点跨层级的移动操作很少,忽略不计。React 通过 updataDepth 对 虚拟 DOM 树进行层级控制,只会对同层节点进行比较,也就是图中只会对相同颜色方框内的 DOM 节点进行比较。例如:
当对比发现节点消失时,则该节点及其子节点都会被完全删除,不会进行更深层次的比较,这样只需要对树进行一次遍历,便能完成整颗 DOM 树的比较
这里还有一个值得关注的地方:DOM 节点跨层级移动
为什么会提出这样的问题呢,在上面的删除原则中,我们发现当节点不存在了就会删除,那我只是给它换位了,它也会删除整个节点及其子节点吗?
如图,我们需要实现这样的移动,你可能会以为它会直接这样移动
但是实际情况,并不是这样的。由于 React 只会简单的进行同层级节点位置变化,对于不同层级的节点,只有创建和删除操作,当发现 B 节点消失时,就会销毁 B,当发现 C 节点上多了 B 节点,就会创建 B 以及它的子节点。
因此这样会非常的复杂,所以 React 官方并不建议我们进行 DOM 节点跨级操作
component diff
在组件层面上,也进行了优化
- 如果是同一类型的组件,则按照原策略继续比较 虚拟 DOM tree
- 如果不是,则将这个组件记为
dirty component,从而替换整个组件下的所有子节点
同时对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点就可以节省大量的 diff 运算的时间,因此 React 允许用户通过 shouldComponentUpdate() 判断该组件是否需要进行 diff 算法分析
总的来说,如果两个组件结构相似,但被认定为了不同类型的组件,则不会比较二者的结构,而是直接删除
element diff
element diff 是专门针对同一层级的所有节点的策略。当节点在同一层级时,diff 提供了 3个节点操作方法:插入,移动,删除
当我们要完成如图所示操作转化时,会有很大的困难,因为在新老节点比较的过程中,发现每个节点都要删除再重新创建,但是这只是重新排序了而已,对性能极大的不友好。因此 React 中提出了优化策略:
允许添加唯一值 key 来区分节点
引入 key 的优化策略,让性能上有了翻天覆地的变化
那 key 有什么作用呢?
当同一层级的节点添加了 key 属性后,当位置发生变化时。react diff 进行新旧节点比较,如果发现有相同的 key 就会进行移动操作,而不会删除再创建
那 key 具体是如何起作用的呢?
首先在 React 中只允许节点右移
因此对于上图中的转化,只会进行 A,C 的移动
则只需要对移动的节点进行更新渲染,不移动的则不需要更新渲染
为什么不能用 index 作为 key 值呢?
index 作为 key ,如果我们删除了一个节点,那么数组的后一项可能会前移,这个时候移动的节点和删除的节点就是相同的 key ,在react中,如果 key 相同,就会视为相同的组件,但这两个组件是不同的,这样就会出现很麻烦的事情,例如:序号和文本不对应等问题
所以一定要保证 key 的唯一性
建议
React 已经帮我们做了很多了,剩下的需要我们多加注意,才能有更好的性能
基于三个策略我们需要注意
tree diff 建议:开发组件时,需要注意保持 DOM 结构稳定
component diff 建议:使用 shouldComponentUpdate() 来减少不要的更新
element diff 建议:减少最后一个节点移动到头部的操作,这样前面的节点都需要移动
参考资料
关于手写实现 diff 算法,还有点难度,这事等学完 React 后再说吧
非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈
React 入门学习(五)-- 认识脚手架
简介
这篇文章主要围绕 React 中的脚手架,来解决一下几个问题
灵魂三问:是什么?为什么?怎么办?
- 什么是脚手架?
- 为什么要用脚手架?
- 怎么用脚手架?
🍕 1. 什么是 React 脚手架?
在我们的现实生活中,脚手架最常用的使用场景是在工地,它是为了保证施工顺利的、方便的进行而搭建的,在工地上搭建的脚手架可以帮助工人们高校的去完成工作,同时在大楼建设完成后,拆除脚手架并不会有任何的影响。
在我们的 React 项目中,脚手架的作用与之有异曲同工之妙
React 脚手架其实是一个工具帮我们快速的生成项目的工程化结构,每个项目的结构其实大致都是相同的,所以 React 给我提前的搭建好了,这也是脚手架强大之处之一,也是用 React 创建 SPA 应用的最佳方式
🍔 2. 为什么要用脚手架?
在前面的介绍中,我们也有了一定的认知,脚手架可以帮助我们快速的搭建一个项目结构
在我之前学习 webpack 的过程中,每次都需要配置 webpack.config.js 文件,用于配置我们项目的相关 loader 、plugin,这些操作比较复杂,但是它的重复性很高,而且在项目打包时又很有必要,那 React 脚手架就帮助我们做了这些,它不需要我们人为的去编写 webpack 配置文件,它将这些配置文件全部都已经提前的配置好了。
据我猜测是直接输入一行命令就能打包完成。
目前还没有学习到哪,本文主要讲脚手架的项目目录结构以及安装
🍟 3. 怎么用 React 脚手架?
这也是这篇文章的重点,如何去安装 React 脚手架,并且理解它其中的相关文件作用
首先介绍如何安装脚手架
1. 安装 React 脚手架
首先确保安装了 npm 和Node,版本不要太古老,具体是多少不大清楚,建议还是用 npm update 更新一下
然后打开 cmd 命令行工具,全局安装 create-react-app
npm i create-react-app -g
然后可以新建一个文件夹用于存放项目
在当前的文件夹下执行
create-react-app hello-react
快速搭建项目
再在生成好的 hello-react 文件夹中执行
npm start
启动项目
接下来我们看看这些文件都有什么作用
2. 脚手架项目结构
hello-react
├─ .gitignore // 自动创建本地仓库
├─ package.json // 相关配置文件
├─ public // 公共资源
│ ├─ favicon.ico // 浏览器顶部的icon图标
│ ├─ index.html // 应用的 index.html入口
│ ├─ logo192.png // 在 manifest 中使用的logo图
│ ├─ logo512.png // 同上
│ ├─ manifest.json // 应用加壳的配置文件
│ └─ robots.txt // 爬虫给协议文件
├─ src // 源码文件夹
│ ├─ App.css // App组件的样式
│ ├─ App.js // App组件
│ ├─ App.test.js // 用于给APP做测试
│ ├─ index.css // 样式
│ ├─ index.js // 入口文件
│ ├─ logo.svg // logo图
│ ├─ reportWebVitals.js // 页面性能分析文件
│ └─ setupTests.js // 组件单元测试文件
└─ yarn.lock
再介绍一下public目录下的 index.html 文件中的代码意思
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
以上是删除代码注释后的全部代码
第5行
指定浏览器图标的路径,这里直接采用 %PUBLIC_URL% 原因是 webpack 配置好了,它代表的意思就是 public 文件夹
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
第6行
用于做移动端网页适配
<meta name="viewport" content="width=device-width, initial-scale=1" />
第七行
用于配置安卓手机浏览器顶部颜色,兼容性不大好
<meta name="theme-color" content="#000000" />
8到11行
用于描述网站信息
<meta
name="description"
content="Web site created using create-react-app"
/>
第12行
苹果手机触摸版应用图标
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
第13行
应用加壳时的配置文件
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
以上就是关于 React 脚手架的全部内容了,非常感谢你的阅读💕
React 入门学习(六)-- TodoList 案例
引言
TodoList 案例在前端学习中挺重要的,从原生 JavaScript 的增删查改,到现在 React 的组件通信,都是一个不错的案例,这篇文章主要记录,还原一下通过 React 实现 TodoList 的全过程
一、拆分组件
首先第一步需要做的是将这个页面拆分成几个组件
首先顶部的输入框,可以完成添加项目的功能,可以拆分成一个 Header 组件
中间部分可以实现一个渲染列表的功能,可以拆分成一个 List 组件
在这部分里面,每一个待办事项都可以拆分成一个 Item 组件
最后底部显示当前完成状态的部分,可以拆分成一个 Footer 组件
在拆分完组件后,我们下一步要做的就是去实现这些组件的静态效果
二、实现静态组件
首先,我们可以先写好这个页面的静态页面,然后再分离组件,所以这就要求我们
以后写静态页面的时候,一定要有明确的规范
- 打好注释
- 每个部分的 CSS 要写在一个地方,不要随意写
- 命名一定要规范
- CSS 选择器不要关联太多层级
- 在写 HTML 时就要划分好布局
这样有利于我们分离组件
首先,我们在 src 目录下,新建一个 Components 文件夹,用于存放我们的组件,然后在文件夹下,新建 Header 、Item、List 、Footer 组件文件夹,再创建其下的 index.jsx,index.css 文件,用于创建对应组件及其样式文件
todolist
├─ package.json
├─ public
│ ├─ favicon.ico
│ └─ index.html
├─ src
│ ├─ App.css
│ ├─ App.jsx
│ ├─ Components
│ │ ├─ Footer
│ │ │ ├─ index.css
│ │ │ └─ index.jsx
│ │ ├─ Header
│ │ │ ├─ index.css
│ │ │ └─ index.jsx
│ │ ├─ item
│ │ │ ├─ index.css
│ │ │ └─ index.jsx
│ │ └─ List
│ │ ├─ index.css
│ │ └─ index.jsx
│ └─ index.js
└─ yarn.lock
最终目录结构如上
然后我们将每个组件,对应的 HTML 结构 CV 到对应组件的 index.jsx 文件中 return 出来,再将 CSS 样式添加到 index.css 文件中
记得,在 index.jsx 中一定要引入 index.css 文件
实现了静态组件后,我们需要添加事件等,来实现动态组件
三、实现动态组件
🍎 1. 动态展示列表
我们目前实现的列表项是固定的,我们需要它通过状态来维护,而不是通过组件标签来维护
首先我们知道,父子之间传递参数,可以通过 state 和 props 实现
我们通过在父组件也就是 App.jsx 中设置状态
再将它传递给对应的渲染组件 List
const { todos } = this.state
<List todos={todos}/>
这样在 List 组件中就能通过 props 来获取到 todos
我们通过解构取出 todos
const { todos, updateTodo } = this.props
再通过 map 遍历渲染 Item 数量
{
todos.map(todo => {
return <Item key={todo.id} {...todo}/>
})
}
同时由于我们的数据渲染最终是在 Item 组件中完成的,所以我们需要将数据传递给 Item 组件
这里有两个注意点
- 关于
key的作用在 diff 算法的文章中已经有讲过了,需要满足唯一性 - 这里采用了简写形式
{...todo},这使得代码更加简洁,它代表的意思是
id = {todo.id} name = {todo.name} done = {todo.done}
在 Item 组件中取出 props 即可使用
const { id, name, done } = this.props
这样我们更改 APP.jsx 文件中的 state 就能驱动着 Item 组件的更新,如图
同时这里需要注意的是
对于复选框的选中状态,这里采用的是 defaultChecked = {done},相比于 checked 属性,这个设定的是默认值,能够更改
🍍 2. 添加事项功能
首先我们需要在 Header 组件中,绑定键盘事件,判断按下的是否为回车,如果为回车,则将当前输入框中的内容传递给 APP 组件
因为,在目前的学习知识中,Header 组件和渲染组件 List 属于兄弟组件,没有办法进行直接的数据传递,因此可以将数据传递给 APP 再由 APP 转发给 List。
// Header/index.jsx
handleKeyUp = (event) => {
// 结构赋值获取 keyCode,target
const { keyCode, target } = event
// 判断是不是回车
if (keyCode !== 13) return
if(target.value.trim() === '') {
alert('输入不能为空')
}
// 准备一个todo对象
const todoObj = { id: nanoid(), name: target.value, done: false }
// 传递给app
this.props.addTodo(todoObj)
// 清空
target.value = ''
}
我们在 App.jsx 中添加了事件 addTodo ,这样可以将 Header 组件传递的参数,维护到 App 的状态中
// App.jsx
addTodo = (todoObj) => {
const { todos } = this.state
// 追加一个 todo
const newTodos = [todoObj, ...todos]
this.setState({ todos: newTodos })
}
在这小部分中,需要我们注意的是,我们新建的 todo 对象,一定要保证它的 id 的唯一性
这里采用的 nanoid 库,这个库的每一次调用都会返回一个唯一的值
npm i nanoid
安装这个库,然后引入
通过 nanoid() 即可生成唯一值
🍋 3. 实现鼠标悬浮效果
接下来我们需要实现每个 Item 中的小功能
首先是鼠标移入时的变色效果
我的逻辑是,通过一个状态来维护是否鼠标移入,比如用一个 mouse 变量,值给 false 当鼠标移入时,重新设定状态为 true 当鼠标移出时设为 false ,然后我们只需要在 style 中用mouse 去设定样式即可
下面我们来代码实现
在 Item 组件中,先设定状态
state = { mouse: false } // 标识鼠标移入,移出
给元素绑定上鼠标移入,移出事件
<li onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)} ><li/>
当鼠标移入时,会触发 onMouseEnter 事件,调用 handleMouse 事件传入参数 true 表示鼠标进入,更新组件状态
handleMouse = flag => {
return () => {
this.setState({ mouse: flag })
}
}
再在 li 身上添加由 mouse 控制的背景颜色
style={{ backgroundColor: this.state.mouse ? '#ddd' : 'white' }}
同时通过 mouse 来控制删除按钮的显示和隐藏,做法和上面一样
观察 mouse 的变化
🍉 4. 复选框状态维护
我们需要将当前复选框的状态,维护到 state 当中
我们的思路是
在复选框中添加一个 onChange 事件来进行数据的传递,当事件触发时我们执行 handleCheck 函数,这个函数可以向 App 组件中传递参数,这样再在 App 中改变状态即可
首先绑定事件
// Item/index.jsx
<input type="checkbox" defaultChecked={done} onChange={this.handleCheck(id)} />
事件回调
handleCheck = (id) => {
return (event) => {
this.props.updateTodo(id, event.target.checked)
}
}
由于我们需要传递 id 来记录状态更新的对象,因此我们需要采用高阶函数的写法,不然函数会直接执行而报错,复选框的状态我们可以通过 event.target.checked 来获取
这样我们将我们需要改变状态的 Item 的 id 和改变后的状态,传递给了 App
内定义的updateTodo 事件,这样我们可以在 App 组件中操作改变状态
我们传递了两个参数 id 和 done
通过遍历找出该 id 对应的 todo 对象,更改它的 done 即可
// App.jsx
updateTodo = (id, done) => {
const { todos } = this.state
// 处理
const newTodos = todos.map(todoObj => {
if (todoObj.id === id) {
return { ...todoObj, done }
} else {
return todoObj
}
})
this.setState({ todos: newTodos })
}
这里更改的方式是 { ...todoObj, done },首先会展开 todoObj 的每一项,再对 done 属性做覆盖
🍏 5. 限制参数类型
在我们前面写的东西中,我们并没有对参数的类型以及必要性进行限制
在前面我们也学过这个,我们需要借助 propTypes 这个库
首先我们需要引入这个库,然后对 props 进行限制
// Header
static propTypes = {
addTodo: PropTypes.func.isRequired
}
在Header 组件中需要接收一个 addTodo 函数,所以我们进行一下限制
同时在 List 组件中也需要进行对 todos 以及 updateTodo 的限制
如果传入的参数不符合限制,则会报 warning
🍒 6. 删除按钮
现在我们需要实现删除按钮的效果
这个和前面的挺像的,首先我们分析一下,我们需要在 Item 组件上的按钮绑定点击事件,然后传入被点击事项的 id 值,通过 props 将它传递给父元素 List ,再通过在 List 中绑定一个 App 组件中的删除回调,将 id 传递给 App 来改变 state
首先我们先编写 点击事件
// Item/index.jsx
handleDelete = (id) => {
this.props.deleteTodo(id)
}
绑定在点击事件的回调上
子组件想影响父组件的状态,需要父组件传递一个函数,因此我们在 App 中添加一个 deleteTodo 函数
// app.jsx
deleteTodo = (id) => {
const { todos } = this.state
const newTodos = todos.filter(todoObj => {
return todoObj.id !== id
})
this.setState({ todos: newTodos })
}
然后将这个函数传递给 List 组件,再传递给 Item
增加一个判断
if(window.confirm('确认删除')) {
this.props.deleteTodo(id)
}
🍓 7. 获取完成数量
我们在 App 中向 Footer 组件传递 todos 数据,再去统计数据
统计 done 为 true 的个数
const doneCount = todos.reduce((pre, todo) => {
return pre + (todo.done ? 1 : 0)
}, 0)
再渲染数据即可
🍊 8. 全选按钮
首先我们需要在按钮上绑定事件,由于子组件需要改变父组件的状态,所以我们的操作和之前的一样,先绑定事件,再在 App 中传一个函数个 Footer ,再在 Footer 中调用这个函数并传入参数即可
这里需要特别注意的是
defaulChecked 只有第一次会起作用,所以我们需要将前面写的改成 checked 添加 onChange 事件即可
首先我们先在 App 中给 Footer 传入一个函数 checkAllTodo
// App.jsx
checkAllTodo = (done) => {
const { todos } = this.state
const newTodos = todos.map((todoObj => {
return { ...todoObj, done: done }
}))
this.setState({ todos: newTodos })
}
// render
<Footer todos={todos} checkAllTodo={this.checkAllTodo}/>
然后我们需要在 Footer 中调用一下
handleCheckAll = (event) => {
this.props.checkAllTodo(event.target.checked)
}
这里我们传入了一个参数:当前按钮的状态,用于全选和取消全选
同时我们需要排除总数为0 时的干扰
<input type="checkbox" checked={doneCount === total && total !== 0? true : false} onChange={this.handleCheckAll} />
🥭 9. 删除已完成
给删除按钮添加一个点击事件,回调中调用 App 中添加的删除已完成的函数,全都一个套路
强烈建议这个自己打
首先在 Footer 组件中调用传来的函数,在 App 中定义函数,过滤掉 done 为 true 的,再更新状态即可
// App.jsx
clearAllDone = () => {
const { todos } = this.state
const newTodos = todos.filter((todoObj) => {
return todoObj.done !== true
})
this.setState({ todos: newTodos })
}
总结
- 注意:className、style 写法
- 父组件给子组件传递数据,采用
props - 子组件给父组件传递数据,通过
props,同时提前给子组件传递一个函数 - 注意
defaultChecked和checked的区别 - 一定要自己敲一下,好好理解数据传递
非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈
React 入门学习(七)-- 脚手架配置代理
引言
React 本身只关注于页面,并不包含发送 Ajax 请求的代码,所以一般都是集成第三方的包,或者自己封装的
自己封装的话,比较麻烦,而且也可能考虑不全
常用的有两个库,一个是JQuery,一个是 axios
- JQuery 这个比较重,因为 Ajax 服务也只是它这个库里的一小块功能,它主要做的还是 DOM 操作,而这不利于 React ,不推荐使用
- axios 这个就比较轻,而且采用 Promise 风格,代码的逻辑会相对清晰,推荐使用
因此我们这里采用 axios 来发送客户端请求
以前,我们在发送请求的时候,经常会遇到一个很重要的问题:跨域!
在我以前的学习中,基本上都需要操作后端服务器代码才能解决跨域的问题,配置请求头,利用 script,这些都需要后端服务器的配合,因此我们前端需要自己解决这个问题的话,就需要这个技术了:代理。
在说代理之前,先谈谈为什么会出现跨域?
这个应该是源于浏览器的同源策略。所谓同源(即指在同一个域)就是两个页面具有相同的协议,主机和端口号, 当一个请求 URL 的协议、域名、端口三者之间任意一个与当前页面 URL 不同即为跨域 。
也就是说 xxx:3000和 xxx:4000 会有跨域问题,xxx:3000 与 abc:3000 有跨域问题
那接下来我们采用配置代理的方式去解决这个问题
关于跨域的问题解决方案,在之后的文章会有总结 ~
1. 全局代理
第一种方法,我把它叫做全局代理,因为它直接将代理配置在了配置文件 package.json 中
"proxy":"http://localhost:5000"
// "proxy":"请求的地址"
这样配置代理时,首先会在抓原请求地址上访问,如果访问不到文件,就会转发到这里配置的地址上去请求
我们需要做的就是在我们的请求代码中,将请求的地址改到转发的地址,即可
但是这样会有一些问题,它会先向我们请求的地址,也就是这里的 3000 端口下请求数据,如果在 3000 端口中存在我们需要访问的文件,会直接返回,不会再去转发
因此这就会出现问题,同时因为这种方式采用的是全局配置的关系,导致只能转发到一个地址,不能配置多个代理
2. 单独配置
这也是我自己起的名字,这种配置方式,可以给多个请求配置代理,非常不错
它的工作原理和全局配置是一样的,但是写法不同
首先我们需要在 src 目录下,创建代理配置文件 setupProxy.js
注意:这个文件只能叫这个名字,脚手架在启动的时候,会自动执行这些文件
第二步
配置具体的代理规则,我们大致讲讲这些是什么意思
-
首先我们需要引入这个
http-proxy-middleware中间件,然后需要导出一个对象,这里建议使用函数,使用对象的话兼容性不大好 -
然后我们需要在
app.use中配置,我们的代理规则,首先proxy接收的第一个参数是需要转发的请求,我的理解是一个标志的作用,当有这个标志的时候,预示着我们需要采用代理,例如/api1,我们就需要在我们axios的请求路径中,加上/api1,这样所有添加了/api1前缀的请求都会转发到这 -
第二个参数接受的是一个对象,用于配置代理。
target属性用于配置转发目标地址,也就是我们数据的地址changeOrigin属性用于控制服务器收到的请求头中host字段,可以理解为一个伪装效果,为true时,收到的host就为请求数据的地址pathRewrite属性用于去除请求前缀,因为我们通过代理请求时,需要在请求地址前添加一个标志,但是实际的地址是不存在这个标志的,所以我们一定要去除这个前缀,这里采用的有点类似于正则替换的方式
配置一个代理的完整代码如下
const proxy = require('http-proxy-middleware')
module.exports = function(app) {
app.use(
proxy('/api1', {
target: 'http://localhost:5000', //配置转发目标地址
changeOrigin: true, //控制服务器接收到的请求头中host字段的值
pathRewrite: {'^/api1': ''} //去除请求前缀址(必须配置)
}),
)
}
关于脚手架配置代理的内容就到这里啦!
非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈
React 入门学习(八)-- GitHub 搜索案例
引言
本文主要介绍 React 学习中 Github 搜索案例,这个案例主要涉及到了 Axios 发送请求,数据渲染以及一些中间交替效果的实现
个人感觉在做完 TodoList 案例之后,这个案例会很轻松,只是多加了一个 Loading 效果的实现思路,以及一些小细节的完善,感觉练练手还是很不错的
一、实现静态组件
和之前的 TodoList 案例一样,我们需要先实现静态组件,在实现静态组件之前,我们还需要拆分组件,这个页面的组件,我们可以将它拆成以下两个组件,第一个组件是 Search,第二个是 List
接下来我们需要将提前写好的静态页面,对应拆分到组件当中
注意:
- class 需要改成 className
- style 的值需要使用双花括号的形式
最重要的一点就是,img 标签,一定要添加 alt 属性表示图片加载失败时的提示。
同时,a 标签要添加 rel="noreferrer"属性,不然会有大量的警告出现
二、axios 发送请求
在实现静态组件之后,我们需要通过向 github 发送请求,来获取相应的用户信息
但是由于短时间内多次请求,可能会导致请求不返回结果等情况发生,因此我们采用了一个事先搭建好的本地服务器
我们启动服务器,向这个地址发送请求即可
这个请求类型是 GET 请求,我们需要传递一个搜索的关键字,去请求数据
我们首先要获取到用户点击搜索按钮后输入框中的值
在需要触发事件的 input 标签中,添加 ref 属性
<input ref={c => this.keyWordElement = c} type="text" placeholder="输入关键词点击搜索" />
我们可以通过 this.keyWordElement 属性来获取到这个当前节点,也就是这个 input 框
我们再通过 value 值,即可获取到当前 input 框中的值
// search 回调
const { keyWordElement: { value: keyWord } } = this
这里采用的是连续的解构赋值,最后将 value 改为 keyWord ,这样好辨别
获取到了 keyWord 值,接下来我们就需要发送请求了
axios.get(`http://localhost:3000/api1/search/users?q=${keyWord}`).then(
response => {
this.props.updateAppState({ isLoading: false, users: response.data.items })
},
error => {
this.props.updateAppState({ isLoading: false, err: error.message })
}
)
我们将 keyWord 接在请求地址的后面,来传递参数,以获得相关数据
这里会存在跨域的问题,因我我们是站在 3000 端口向 5000 端口发送请求的
因此我们需要配置代理来解决跨域的问题,我们需要在请求地址前,加上启用代理的标志 /api1
// setupProxy.js
const proxy = require('http-proxy-middleware')
module.exports = function (app) {
app.use(
proxy('/api1', {
target: 'http://localhost:5000',
changeOrigin: true,
pathRewrite: {
'^/api1': ''
}
})
)
}
这样我们就能成功的获取到了数据
三、渲染数据
在获取到了数据之后,我们需要对数据进行分析,并将这些数据渲染到页面上
比较重要的一点是,我们获取到的用户个数是动态的,因此我们需要通过遍历的方式去实现
同时我们的数据当前存在于 Search 组件当中,我们需要在 List 组件中使用,所以我们需要个 Search 组件传递一个函数,来实现子向父传递数据,再通过 App 组件,向List 组件传递数据即可得到 data
users.map((userObj) => {
return (
<div key={userObj.id} className="card">
<a rel="noreferrer" href={userObj.html_url} target="_blank">
<img alt="avatar" src={userObj.avatar_url} style={{ width: '100px' }} />
</a>
<p className="card-text">{userObj.login}</p>
</div>
)
})
这里我们通过 map 遍历整个返回的数据,来循环的添加 card 的个数
同时将一些用户信息添加到其中
四、增加交互
做到这里其实已经完成了一大半了,但是似乎少了点交互
- 加载时的 loading 效果
- 第一次进入页面时 List 组件中的欢迎使用字样
- 在报错时应该提示错误信息
这一些都预示着我们不能单纯的将用户数据直接渲染,我们需要添加一些判断,什么时候该渲染数据,什么时候渲染 loading,什么时候渲染 err
首先我们需要增加一些状态,来指示我们该渲染什么,比如
- 采用
isFrist来判断页面是否第一次启动,初始值给true,点击搜索后改为false - 采用
isLoading来判断是否应该显示 Loading 动画,初始值给false,在点击搜索后改为true,在拿到数据后改为false - 采用
err来判断是否渲染错误信息,当报错时填入报错信息,初始值给空
state = { users: [], isFirst: true, isLoading: false, err: '' }
这样我们就需要改变我先前采用的数据传递方式,采用更新状态的方式,接收一个状态对象来更新数据,这样就不用去指定什么时候更新什么,就可以减少很多不必要的函数声明
同时在 App 组件给 List 组件传递数据时,我们可以采用解构赋值的方式,这样可以减少代码量
// App.jsx
// 接收一个状态对象
updateAppState = (stateObj) => {
this.setState(stateObj)
}
<Search updateAppState={this.updateAppState} />
<List {...this.state} />
这样我们只需要在 List 组件中,判断这些状态的值,来显示即可
// List/index.jsx
// 对象解构
const { users, isFirst, isLoading, err } = this.props
// 判断
{
isFirst ? <h2>欢迎使用,输入关键字,点击搜索</h2> :
isLoading ? <h2>Loading...</h2> :
err ? <h2 style={{ color: 'red' }}>{err}</h2> :
users.map((userObj) => {
return (
// 渲染数据块
//为了减少代码量,就不贴了
)
})
}
我们需要先判断是否第一次,再判断是不是正在加载,再判断有没有报错,最后再渲染数据
我们的状态更新是在 Search 组件中实现的,在点击搜索之后数据返回之前,我们需要将 isFirst 改为 false ,isLoading 改为 true
接收到数据后我们再将 isLoading 改为 false 即可
以上就是 Github 搜索案例的实现过程
最终效果图
前端路还有很长,今天我就大二啦!加油吧!!!
非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈
React 入门学习(九)-- 消息订阅发布
引言
在昨天写的 Github 案例中,我们采用的是 axios 发送请求来获取数据,同时我们需要将数据从 Search 中传入给 App,再由 App 组件再将数据传递给 List 组件,这个过程会显得多此一举。同时我们要将 state 状态存放在 App 组件当中,但是这些 state 状态都是在 List 组件中使用的,在 Search 组件中做的,只是更新这些数据,那这样也会显得很没有必要,我们完全可以将 state 状态存放在 List 组件中,但是这样我们又会遇到技术难题,兄弟组件间的数据通信。那这里我们就学习一下如何利用消息订阅发布来解决兄弟组件间的通信
消息发布订阅
要解决上面的问题,我们可以借助发布订阅的机制,我们可以将 App 文件中的所有状态和方法全部去除,因为本来就不是在 App 组件中直接使用这些方法的,App 组件只是一个中间媒介而已
我们先简单的说一下消息订阅和发布的机制
就拿我们平常订杂志来说,我们和出版社说我们要订一年的足球周刊,那每次有新的足球周刊,它都会寄来给你。
换到代码层面上,我们订阅了一个消息假设为 A,当另一个人发布了 A 消息时,因为我们订阅了消息 A ,那么我们就可以拿到 A 消息,并获取数据
那我们要怎么实现呢?
首先引入 pubsub-js
我们需要先安装这个库
yarn add pubsub-js
引入这个库
import PubSub from 'pubsub-js'
订阅消息
我们通过 subscribe 来订阅消息,它接收两个参数,第一个参数是消息的名称,第二个是消息成功的回调,回调中也接受两个参数,一个是消息名称,一个是返回的数据
PubSub.subscribe('search',(msg,data)=>{
console.log(msg,data);
})
发布消息
我们采用 publish 来发布消息,用法如下
PubSub.publish('search',{name:'tom',age:18})
有了这些基础,我们可以完善我们昨天写的 GitHub 案例
将数据的更新通过 publish 来传递,例如在发送请求之前,我们需要出现 loading 字样
// 之前的写法
this.props.updateAppState({ isFirst: false, isLoading: true })
// 改为发布订阅方式
PubSub.publish('search',{ isFirst: false, isLoading: true })
这样我们就能成功的在请求之前发送消息,我们只需要在 List 组件中订阅一下这个消息即可,并将返回的数据用于更新状态即可
PubSub.subscribe('search',(msg,stateObj)=>{
this.setState(stateObj)
})
同时上面的代码会返回一个 token ,这个就类似于定时器的编号的存在,我们可以通过这个 token 值,来取消对应的订阅
通过 unsubscribe 来取消指定的订阅
PubSub.unsubscribe(this.token)
扩展 -- Fetch
首先 fetch 也是一种发送请求的方式,它是在 xhr 之外的一种,我们平常用的 Jquery 和 axios 都是封装了 xhr 的第三方库,而 fetch 是官方自带的库,同时它也采用的是 Promise 的方式,大大简化了写法
如何使用呢?
fetch('http://xxx')
.then(response => response.json())
.then(json => console.log(json))
.catch(err => console.log('Request Failed', err));
它的使用方法和 axios 非常的类似,都是返回 Promise 对象,但是不同的是, fetch 关注分离,它在第一次请求时,不会直接返回数据,会先返回联系服务器的状态,在第二步中才能够获取到数据
我们需要在第一次 then 中返回 response.json() 因为这个是包含数据的 promise 对象,再调用一次 then 方法即可实现
但是这么多次的调用 then 并不是我们所期望的,相信看过之前生成器的文章的伙伴,已经有了想法。
我们可以利用 async 和 await 配合使用,来简化代码
可以将 await 理解成一个自动执行的 then 方法,这样清晰多了
async function getJSON() {
let url = 'https://xxx';
try {
let response = await fetch(url);
return await reasponse.json();
} catch (error) {
console.log('Request Failed', error);
}
}
最后关于错误对象的获取可以采用 try...catch 来实现
关于 fetch 的更多内容
强烈推荐阮一峰老师的博文:fetch
非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈