react

92 阅读15分钟

类组件

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,这个也还是个高阶函数,但是没有了柯里化了。推荐使用第一种效率更高。

生命周期方法

图解

常见方法版

react-lifecycle-1.png ( 图片来源:projects.wojtekmaj.pl/react-lifec…

完整方法版

react-lifecycle.png

( 图片来源: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);

propsstate 发生变化时,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——定义要获取的资源,可以是:

  • 一个 USVString 字符串,包含要获取资源的 URL。
  • 一个 Request 对象。

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接收到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回数据

前端路由

  1. 浏览器端路由,value时component,用于展示页面内容
  2. 注册路由:
  3. 工作过程,当浏览器的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  模式。更新视图但不重新请求页面”是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有以下两种方式:

image.png

Hash

简述

  • vue-router   默认为 hash 模式,使用 URL 的  hash  来模拟一个完整的 URL,当 URL 改变时,页面不会重新加载;#  就是  hash符号,中文名为哈希符或者锚点,在  hash  符号后的值称为  hash  值。
  • 路由的  hash  模式是利用了  window 可以监听 onhashchange 事件来实现的,也就是说  hash  值是用来指导浏览器动作的,对服务器没有影响,HTTP 请求中也不会包括  hash  值,同时每一次改变  hash  值,都会在浏览器的访问历史中增加一个记录,使用“后退”按钮,就可以回到上一个位置。所以,hash 模式 是根据  hash 值来发生改变,根据不同的值,渲染指定DOM位置的不同数据。

参考:Vue 前端路由工作原理,hash与history之间的区别

image.png

 特点

  • 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,但浏览器不会向后端发送请求。

 参考:深入了解前端路由 hash 与 history 差异

特点

  • 新的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  内部重定向到项目首页。