类组件
this指向的问题
1.问题代码:
class App extends React.Component {
// 组件状态
state = {
msg: 'hello react'
}
// 事件处理函数
handleClick() {
console.log(this) // 这里的this是 undefined
}
render() {
console.log(this) // 这里的this是当前的组件实例 App
return (
<div>
<button onClick={this.handleClick}>点我</button>
</div>
)
}
}
结果是这样:
- render方法中的this指向的是当前react组件。
- 事件处理程序中的this指向的是
undefined
2.原因:
this的指向是谁调用指向谁,render中是react调用指向react组件,事件处理程序是window调用,而class类和模块的内部,默认就是严格模式,严格模式中window为undefined。所以类中的函数this指向了undefined
3.解决事件函数this指向:
方式1:
在事件处理程序外套一层箭头函数
缺点:需要在处理函数外额外包裹一个箭头函数, 结构不美观
优点:
前面讲过在 {this.handleClick ( )}这里面调用处理函数的时候不要加小括号,不然里面的程序会立即执行掉,现在在外面包裹一层箭头函数之后,不仅可以加上小括号,还能实现传参,后面会用到
class App extends React.Component {
state = {
msg: 'hello react'
}
handleClick () {
console.log(this.state.msg)
}
render () {
return (
<div>
<button onClick={ () => { this.handleClick ( ) }}>点我</button>
</div>
)
}
}
方式2:使用bind
通过bind()方法改变函数this指向并不执行该函数的特性解决
class App extends React.Component {
state = {
msg: 'hello react'
}
handleClick () {
console.log(this.state.msg)
}
render () {
return (
<div>
<button onClick={ this.handleClick.bind (this) }>点我</button>
</div>
)
}
}
方式3:
在class中声明事件处理函数的时候直接使用箭头函数
class App extends React.Component {
state = {
msg: 'hello react'
}
handleClick = () => {
console.log(this.state.msg)
}
render() {
return (
<div>
<button onClick={this.handleClick}>点我</button>
</div>
)
}
}
优点:
代码简洁,直观,使用最多的一种方式
props限制propTypes
限制props传入的值类型
方式一、写在类的外面
// 需要引入PropTypes
import PropTypes from 'prop-types'
// 组件名称.propTypes,PropTypes.类型
Person.propTypes = {
name: PropTypes.string.isRequired, //限制name必传,且为字符串
age: PropTypes.number, // 限制age为数值
speak: PropTypes.func // 限制speak为函数
}
方式二、写在类的里面,当成静态属性
class Person extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired
}
}
指定默认标签属性值
Person.defaultProps = {
name: '小张',
}
Ref
创建 Refs
Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef(); }
render() {
return <div ref={this.myRef} />; }
}
访问 Refs
当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。
const node = this.myRef.current;
ref 的值根据节点的类型而有所不同:
- 当
ref属性用于 HTML 元素时,构造函数中使用React.createRef()创建的ref接收底层 DOM 元素作为其current属性。 - 当
ref属性用于自定义 class 组件时,ref对象接收组件的挂载实例作为其current属性。 - 你不能在函数组件上使用
ref属性,因为他们没有实例。
在函数组件内部使用 ref 属性
function CustomTextInput(props) {
// 这里必须声明 textInput,这样 ref 才可以引用它 const textInput = useRef(null);
function handleClick() {
textInput.current.focus(); }
return (
<div>
<input
type="text"
ref={textInput} /> <input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
用高阶函数柯里化去实现
什么是高阶函数?
高阶函数:如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
- 1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。
- 2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。
- 常见的高阶函数有:Promise、setTimeout、arr.map()等等
什么是函数柯里化呢?
函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。例如下面这个例子一样:
function sum(a){
return(b)=>{
return (c)=>{
return a+b+c
}
}
}
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example1 extends Component {
state: Readonly<{ username: string; password: string }> = {
username: "", //用户名
password: "", //密码
};
handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { username, password } = this.state;
alert(`你输入的用户名是:${username},你输入的密码是:${password}`);
};
//保存表单数据到状态中
saveFormData = (dataType: string)=>{
return (event:ChangeEvent<HTMLInputElement>)=>{
this.setState({[dataType]:event.target.value})
}
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveFormData('username')} type="text" name="username"/>
密码:<input onChange={this.saveFormData('password')} type="password" name="password"/>
<button>登录</button>
</form>
);
}
}
不使用函数柯里化实现
import React, { ChangeEvent, Component, FormEvent } from "react";
export default class example1 extends Component {
state: Readonly<{ username: string; password: string }> = {
username: "", //用户名
password: "", //密码
};
handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { username, password } = this.state;
alert(`你输入的用户名是:${username},你输入的密码是:${password}`);
};
//保存表单数据到状态中
saveFormData = (dataType: string, event: ChangeEvent<HTMLInputElement>) => {
this.setState({ [dataType]: event.target.value });
};
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:
<input onChange={(event) => this.saveFormData("username", event)} type="text" name="username" />
密码:
<input onChange={(event) => this.saveFormData("password", event)} type="password" name="password" />
<button>登录</button>
</form>
);
}
}
这种就是常规的写法,写个回调然后里面有个函数,通过传入类型和值,去修改相应的state,这个也还是个高阶函数,但是没有了柯里化了。推荐使用第一种效率更高。
生命周期方法
图解
常见方法版
( 图片来源:projects.wojtekmaj.pl/react-lifec… )
完整方法版
( 图片来源:projects.wojtekmaj.pl/react-lifec… )
方法
- contructor
constructor(props);
通常,在 React 中,构造函数仅用于以下两种情况:
- 通过给
this.state赋值对象来初始化内部state。( 唯一可以直接修改state的地方) - 为事件处理函数绑定实例
在 constructor() 函数中不要调用 setState() 方法。
constructor(props) {
super(props);
// 不要在这里调用 this.setState()
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
- getDerivedStateFromProps
static getDerivedStateFromProps(props, state)
这个方法是 “如何用 props 初始化 state ” 的最佳实践。
⚠️ 注意,该方法在每次 render 前都会被调用。
此方法适用于罕见的用例(表单控件获取默认值)。当 state 是从 props 获取时,就必须要维护两者的一致性,这将会增加复杂度和 bug。
- shouldComponentUpate
shouldComponentUpdate(nextProps, nextState);
当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。首次渲染或使用 forceUpdate() 时不会调用该方法。
此方法 仅作为性能优化 的方式而存在。它的工作一般可以由 PrueComponent 自动实现。
后续版本,
React可能会将shouldComponentUpdate仅视为提示,并且,当返回false时,仍可能导致组件重新渲染。
- render
render();
用于描述 DOM 结构,组件中唯一必须实现的方法。
- getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState);
能在组件发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。
- componentDidMount
componentDidMount();
在组件挂载后(插入 DOM 树中)立即调用。在这里可以安全操作 DOM 节点、发送ajax 请求(DOM 节点的初始化)或一些副作用的事情(订阅)
如果你在这里调用了 setState,它将触发额外渲染,虽然此渲染会发生在浏览器更新屏幕之前,但会导致性能问题。
- componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot);
在更新后会被立即调用。首次渲染不会执行此方法。
⚠ 注意, 如果直接调用 setState(),它必须被包裹在一个条件语句里,否则会导致死循环。
- componentWillUnmount
componentWillUnmount();
在组件卸载及销毁之前直接调用,做一些资源释放操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。
⚠️ 注意:不应调用 setState() ,因为该组件将永远不会重新渲染。
Fetch API
Fetch API提供了一个 JavaScript 接口,用于访问和操纵HTTP的请求和响应等。提供了一个全局 fetch() 方法来跨网络异步获取资源。
与AJAX的区别
Fetch 规范与 jQuery.ajax() 主要有三种方式的不同:
- 当接收到一个代表错误的 HTTP 状态码, 即使响应的 HTTP 状态码是 404 或 500。从
fetch()返回的 Promise **不会被标记为 reject,**相反,会标记为 resolve (但是会把 resolve 的返回值的ok属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。 fetch()可以接收跨域 cookies;也可以使用fetch()建立起跨域会话。fetch不会发送 cookies。除非你使用了credentials 的初始化选项
Fetch()示例
给fetch() 提供一个参数指明资源路径,会返回一个包含响应结果的promise。当然它只是一个 HTTP 响应,为了获取JSON的内容,我们需要使用 json() 方法:
默认发送GET请求
fetch('http://example.com/movies.json')
.then(response => response.json())
.then(data => console.log(data));
带参数的GET请求:
var id=1
fetch(`https://www.easy-mock.com/mock/5f507e38a758c95f67d6eb42/fetch/getmsg?id=${id}`)
.then(response => response.json())
.then(data => console.log(data));
发送POST请求
var data={
id:'1',
}
fetch('https://www.easy-mock.com/mock/5f507e38a758c95f67d6eb42/fetch/postmsg',{
method:'POST',
body:data
})
.then(response => response.json())
.then(data => console.log(data));
Fetch()
用于发起获取资源的请求。它返回一个 promise,这个 promise 会在请求响应后被 resolve,并传回 Response 对象。当遇到网络错误时,fetch() 返回的 promise 会被 reject,并传回 TypeError。成功的 fetch() 检查不仅要包括 promise 被 resolve,还要包括 Response.ok 属性为 true。HTTP 404 状态并不被认为是网络错误。
语法
fetch(input,{init});
参数
input——定义要获取的资源,可以是:
init—— 可选 | 一个配置项对象,包括所有对请求的设置。可选的参数有:
var myInit={
//请求的 body 信息 //如:Blob、BufferSource、FormData、URLSearchParams 或者 USVString 对象
body: JSON.stringify(data), //这里必须匹配 'Content-Type' //注意 GET 或 HEAD 方法的请求不能包含 body 信息。
//请求的 cache 模式。//如:default, no-cache, reload, force-cache, only-if-cached
cache: 'no-cache',
//请求的 credentials。//包括:omit、same-origin,include
credentials: 'same-origin',
//请求的头信息
headers: {
'user-agent': 'Mozilla/4.0 MDN Example',
'content-type': 'application/json'
},
//请求使用的方法 //如:GET, POST, PUT, DELETE等
method: 'POST',
//请求的模式 //如 cors、 no-cors 或者 same-origin。
mode: 'cors',
//重定向模式 //如 follow|自动重定向, error|如果产生重定向将自动终止并且抛出一个错误, manual|手动处理重定向
redirect: 'follow',
//USVString //如 no-referrer、client或一个 URL。默认是 client
referrer: 'no-referrer',
}
发送带凭据的请求
//发送包含凭据的请求
fetch('https://example.com', {
credentials: 'include'
})
//如果你只想在请求URL与调用脚本位于同一起源处时发送凭据
fetch('https://example.com', {
credentials: 'same-origin'
})
//确保浏览器不在请求中包含凭据
fetch('https://example.com', {
credentials: 'omit'
})
上传json数据
var data = {username: 'example'};
fetch('https://example.com/profile', {
method: 'POST',
body: JSON.stringify(data),
headers: new Headers({
'Content-Type': 'application/json'
})
}).then(res => res.json())
.catch(error => console.error('Error:', error))
.then(response => console.log('Success:', response));
上传文件
var formData = new FormData();
var fileField = document.querySelector("input[type='file']");// 获取<input type="file" /> 元素
formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);
fetch('https://example.com/profile/avatar', {
method: 'PUT',
body: formData
})
.then(response => response.json())
.catch(error => console.error('Error:', error))
.then(response => console.log('Success:', response));
上传多个文件
var formData = new FormData();/
var photos = document.querySelector("input[type='file'][multiple]");
formData.append('title', 'My Vegas Vacation');
// formData 只接受文件、Blob 或字符串,不能直接传递数组,所以必须循环嵌入
for (let i = 0; i < photos.files.length; i++) {
formData.append('photo', photos.files[i]);
}
fetch('https://example.com/posts', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(response => console.log('Success:', JSON.stringify(response)))
.catch(error => console.error('Error:', error));
检测请求是否成功
如果遇到网络故障,
fetch()promise 将会 reject,带上一个TypeError对象。虽然这个情况经常是遇到了权限问题或类似问题——比如 404 不是一个网络故障。想要精确的判断fetch()是否成功,需要包含 promise resolved 的情况,此时再判断Response.ok是不是为 true。类似以下代码:
fetch('flowers.jpg').then(function(response) {
if(response.ok) {
return response.blob();
}
throw new Error('Network response was not ok.');
}).then(function(myBlob) {
var objectURL = URL.createObjectURL(myBlob);
myImage.src = objectURL;
}).catch(function(error) {
console.log('There has been a problem with your fetch operation: ', error.message);
});
自定义请求对象
var myHeaders = new Headers();
var myInit = { method: 'GET',
headers: myHeaders,
mode: 'cors',
cache: 'default' };
var myRequest = new Request('flowers.jpg', myInit);
fetch(myRequest).then(function(response) {
return response.blob();
}).then(function(myBlob) {
var objectURL = URL.createObjectURL(myBlob);
myImage.src = objectURL;
});
react 路由
SPA理解
SPA是单页面应用,整个应用只有一个完整的页面
路由分类
后端路由
1.用于处理客户端提交的请求 2.注册路由:router.get(path, function(req, res)) 3.工作过程:当node接收到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回数据
前端路由
- 浏览器端路由,value时component,用于展示页面内容
- 注册路由:
- 工作过程,当浏览器的path变为/test时,路由组件就会变为Test组件 在了解路由模式前,我们先看下 什么是单页面应用,vue-router 的实现原理是怎样的,这样更容易理解路由。
SPA与前端路由
- SPA(单页面应用,全程为:Single-page Web applications)指的是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序,简单通俗点就是在一个项目中只有一个html页面,它在第一次加载页面时,将唯一完成的html页面和所有其余页面组件一起下载下来,所有的组件的展示与切换都在这唯一的页面中完成,这样切换页面时,不会重新加载整个页面,而是通过路由来实现不同组件之间的切换。
- 单页面应用(SPA)的核心之一是:更新视图而不重新请求页面。 优点:
- 具有桌面应用的即时性、网站的可移植性和可访问性
- 用户体验好、快,内容的改变不需要重新加载整个页面
- 良好的前后端分离,分工更明确
缺点:
- 不利于搜索引擎的抓取
- 首次渲染速度相对较慢
vue Router实现原理
vue-router 在实现单页面路由时,提供了两种方式:Hash 模式和 History 模式;vue2是 根据 mode 参数来决定采用哪种方式,默认是 Hash 模式,手动设置为 History 模式。更新视图但不重新请求页面”是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有以下两种方式:
Hash
简述
- vue-router 默认为 hash 模式,使用 URL 的 hash 来模拟一个完整的 URL,当 URL 改变时,页面不会重新加载;# 就是 hash符号,中文名为哈希符或者锚点,在 hash 符号后的值称为 hash 值。
- 路由的 hash 模式是利用了 window 可以监听 onhashchange 事件来实现的,也就是说 hash 值是用来指导浏览器动作的,对服务器没有影响,HTTP 请求中也不会包括 hash 值,同时每一次改变 hash 值,都会在浏览器的访问历史中增加一个记录,使用“后退”按钮,就可以回到上一个位置。所以,hash 模式 是根据 hash 值来发生改变,根据不同的值,渲染指定DOM位置的不同数据。
参考:Vue 前端路由工作原理,hash与history之间的区别
特点
- url中带一个 # 号
- 可以改变URL,但不会触发页面重新加载(hash的改变会记录在 window.hisotry 中)因此并不算是一次 HTTP 请求,所以这种模式不利于 SEO 优化
- 只能修改 # 后面的部分,因此只能跳转与当前 URL 同文档的 URL
- 只能通过字符串改变 URL
- 通过 window.onhashchange 监听 hash 的改变,借此实现无刷新跳转的功能。
- 每改变一次 hash ( window.location.hash),都会在浏览器的访问历史中增加一个记录。
- 路径中从 # 开始,后面的所有路径都叫做路由的
哈希值并且哈希值它不会作为路径的一部分随着 http 请求,发给服务器
参考:在SPA项目的路由中,注意hash与history的区别
History
简述
- history 是路由的另一种模式,在相应的 router 配置时将 mode 设置为 history 即可。
- history 模式是通过调用 window.history 对象上的一系列方法来实现页面的无刷新跳转。
- 利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。
- 这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会向后端发送请求。
特点
- 新的URL可以是与当前URL同源的任意 URL,也可以与当前URL一样,但是这样会把重复的一次操作记录到栈中。
- 通过参数stateObject可以添加任意类型的数据到记录中。
- 可额外设置title属性供后续使用。
- 通过pushState、replaceState实现无刷新跳转的功能。
- 路径直接拼接在端口号后面,后面的路径也会随着http请求发给服务器,因此前端的URL必须和向发送请求后端URL保持一致,否则会报404错误。
- 由于History API的缘故,低版本浏览器有兼容行问题。
参考:在SPA项目的路由中,注意hash与history的区别、前端框架路由实现的Hash和History两种模式的区别
生产环境存在问题
因为 history 模式的时候路径会随着 http 请求发送给服务器,项目打包部署时,需要后端配置 nginx,当应用通过 vue-router 跳转到某个页面后,因为此时是前端路由控制页面跳转,虽然url改变,但是页面只是内容改变,并没有重新请求,所以这套流程没有任何问题。但是,如果在当前的页面刷新一下,此时会重新发起请求,如果 nginx 没有匹配到当前url,就会出现404的页面。
那为什么hash模式不会出现这个问题呢?
上文已讲,hash 虽然可以改变URL,但不会被包括在 HTTP 请求中。它被用来指导浏览器动作,并不影响服务器端,因此,改变 hash 并没有改变URL,所以页面路径还是之前的路径,nginx 不会拦截。 因此,切记在使用 history 模式时,需要服务端允许地址可访问,否则就会出现404的尴尬场景。
那为什么开发环境时就不会出现404呢?
因为在 vue-cli 中 webpack 帮我们做了处理
解决问题
生产环境 刷新 404 的解决办法可以在 nginx 做代理转发,在 nginx 中配置按顺序检查参数中的资源是否存在,如果都没有找到,让 nginx 内部重定向到项目首页。