前端学习react框架
Polyfill
- Polyfill的准确意思为:用于实现浏览器并不支持的原生API的代码。
shim
- shim是一个库,它将一个新的API引入到一个旧的环境中,而且仅靠旧环境中已有的手段实现。
React历史
- react16.8.0之前是class组件的时代
- react16.8.0正式引入hooks的概念(class组件+函数组件+hooks)
- react17 在17版本中未添加任何新功能,是一个过渡版本(class组件+函数组件+hooks)
- react18最新版本(函数组件+hooks),这个版本已经放弃了对ie11的支持,如果你支持旧的浏览器和设备,如 Internet Explorer,它们没有提供原生的现代浏览器特性,或有些不符合标准的实现,你可以考虑在应用程序中引入一个全局的 polyfill/Polyfiller。
React的生命周期
旧的生命周期react14之前的版本
初始化阶段 : 由ReactDOM.render()触发—初次渲染,会调用以下钩子函数
- constructor()
- componentWillMount()废弃
- render()
- componentDidMount()
更新阶段 :由组件内部this.setSate()或父组件重新render触发,会调用以下钩子函数
- componentWillRecerveProps()废弃
- shouldComponentUpdate()
- componentWillUpdate()废弃
- render()
- componentDidUpdate()
卸载组件 : 由ReactDOM.unmountComponentAtNode()触发
- componentWillUnmount()
详解钩子函数
constructor()
构造函数,通常用于初始化组件的状态和绑定方法。
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
在函数体中,需要先写super(props);
render()
是用来返回组件的UI结构,它是一个纯函数,其中不应该包含任何副作用或改变状态的操作
import React,{Component} from 'react'
export default class Hello extends Component{
render(){
return <h2>Hello,React!</h2>
}
}
componentDidMount()
这个函数是在组件挂载到DOM后执行的,可以在这里获取数据、进行一些异步请求或DOM操作。
componentDidMount() {
// 发起API请求或其他初始化操作
fetchData().then(data => {
this.setState({ data
});
});
}
shouldComponentUpdate(nextProps, nextState)
这个函数是用来判断组件是否需要重新渲染,返回一个布尔值,可以优化性能。
shouldComponentUpdate(nextProps, nextState) {
// 这里假设props有一个min属性,表示计数器的最小值
if (nextProps.min !== undefined && nextState.count < nextProps.min) {
return false;
}
return true;
}
细心观察旧的生命周期图的小伙伴注意到了此钩子函数旁边有个setState(),在组件中,每当调用this.setState()方法时,就会触发shouldComponentUpdate(),如果函数返回true就更新状态,否则不更新。
当调用forceUpdate()强制更新时,会直接来到componentWillUpdate()钩子函数。
何时使用: 只想更新一下组件,但是不想对状态作出任何的更改,就可以调用,通过this.forceUpdate()直接调用
componentDidUpdate(prevProps, prevState)
组件更新后触发,用于处理更新后的操作。
componentDidUpdate(prevProps, prevState) {
console.log('组件已更新完毕');
if (prevProps.id !== this.props.id) {
// 处理props变化的逻辑
console.log('props.id变了');
}
}
componentWillUnmount()
这个函数是在组件卸载前执行的,可以在这里进行一些清理工作,比如取消订阅、清除定时器、取消异步请求或移除事件监听器等。
详解新生命周期react14及之后的版本
初始化阶段
由ReactDOM.render()触发—初次渲染
- constructor()
- getDerivedStateFromProps
- render()
- componentDidMount()
更新阶段
由组件内部this.setSate()或父组件重新render触发,会调用以下钩子函数
- getDerivedStateFromProps
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate
- componentDidUpdate()
卸载组件 :
由ReactDOM.unmountComponentAtNode()触发
- componentWillUnmount()
详解新增的钩子函数
部分钩子函数用法与旧生命周期中的用法相似,不再重复说明。
componentDidUpdate()
componentDidUpdate(prevProps,prevState,snapshot):它在组件更新(即render() 方法执行后)后被调用。它接收三个参数:prevProps、prevState、snapshot。与旧的钩子函数相比,多了一个参数snapshot。
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.props.data !== prevProps.data) {
console.log('this.props中的数据变了');
}
// 使用getSnapshotBeforeUpdate()的返回值
if (snapshot !== null) {
console.log('Snapshot from getSnapshotBeforeUpdate:', snapshot);
}
}
说明:如果getSnapshotBeforeUpdate()有返回值,它会成为componentDidUpdate()的第三个参数,你可以在这里使用它。
getDerivedStateFromProps(nextProps,prevState)
getDerivedStateFromProps(nextProps,prevState):是一个静态方法,用于在组件接收新的props时计算并返回新的state。这个方法在React 16.3版本之后引入,用来替代不推荐使用的componentWillReceiveProps()方法。
class MyComponent extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
// 根据 nextProps 和 prevState 计算并返回新的 state
if (nextProps.value !== prevState.value) {
return { value: nextProps.value };
}
return null; // 如果不需要更新 state,返回 null
}
constructor(props) {
super(props);
this.state = {
value: props.value,
};
}
render() {
return <div>{this.state.value}</div>;
}
}
以下是getDerivedStateFromProps() 的主要特点和使用方式:
- 静态方法:getDerivedStateFromProps() 是一个静态方法,因此不能访问实例的this,它只接收两个参数:nextProps 和 prevState。
- 计算新的state:通常,你可以在这个方法内部根据nextProps 和 prevState 来计算并返回新的state。这个新的state将在组件更新时应用。
- 不触发副作用:与componentDidUpdate() 不同,getDerivedStateFromProps() 不应执行副作用,如发起网络请求。它只用于计算state。
- 不触发副作用:与componentDidUpdate() 不同,getDerivedStateFromProps() 不应执行副作用,如发起网络请求。它只用于计算state。
- 适用于控制组件内部状态:getDerivedStateFromProps() 主要用于控制组件内部的状态,以确保它与外部传入的props保持同步。
getSnapshotBeforeUpdate()
getSnapshotBeforeUpdate(nextProps,prevState):它在组件更新(即将应用新props或state并重新渲染)之前触发。它允许你捕获组件更新前的一些信息,并在组件更新后使用这些信息。
class MyComponent extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
// 根据 nextProps 和 prevState 计算并返回新的 state
if (nextProps.value !== prevState.value) {
return { value: nextProps.value };
}
return null; // 如果不需要更新 state,返回 null
}
constructor(props) {
super(props);
this.myRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 捕获组件更新前的滚动位置
if (prevProps.items.length < this.props.items.length) {
const scrollHeight = this.myRef.current.scrollHeight;
const scrollTop = this.myRef.current.scrollTop;
return { scrollHeight, scrollTop };
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 使用snapshot来恢复滚动位置
if (snapshot !== null) {
this.myRef.current.scrollTop = snapshot.scrollTop + (this.myRef.current.scrollHeight - snapshot.scrollHeight);
}
}
render() {
// 使用ref来获取DOM元素的引用
return <div ref={this.myRef}>{/* 组件内容 */}</div>;
}
}
示例中,getSnapshotBeforeUpdate() 用于捕获滚动位置,然后在componentDidUpdate() 中使用snapshot来恢复滚动位置,以确保用户在滚动列表时不会在更新后失去滚动位置。
以下是关于getSnapshotBeforeUpdate() 的主要特点和用法:
- **触发时机:**getSnapshotBeforeUpdate() 在render() 方法被调用后、组件DOM更新前触发,通常用于在更新前捕获一些DOM信息。
- **接收两个参数:**这个生命周期方法接收两个参数:prevProps、prevState。你可以使用这些参数来比较前后的props和state。
- **返回值:**getSnapshotBeforeUpdate() 方法应该返回一个值(通常是一个对象),它将成为componentDidUpdate() 方法的第三个参数。这个返回值通常用于保存一些DOM相关的信息,比如滚动位置。
- **通常和componentDidUpdate()一起使用:**getSnapshotBeforeUpdate() 结合componentDidUpdate(prevProps, prevState, snapshot) 使用,snapshot参数是getSnapshotBeforeUpdate() 的返回值。你可以在componentDidUpdate() 中使用snapshot来执行DOM操作或其他一些操作。
hooks
函数式组件+Hooks
如果将一个纯函数作为组件,纯函数通过自身的重复执行做到渲染与重复更新,需要在函数多次执行期间保存其中的状态,那我们肯定是需要这个函数之外的空间来存储状态,并且当这个状态被改变时,能监听到并触发函数组件的重新渲染。
Hooks就是这种方式,“钩子”,Hooks将函数钩到一个可能变化的数据源上,当这个数据变化时候,另一边钩的函数会重新执行,生成新的结果。
useState Hooks解决状态的问题
在函数组件中注入useState,就可以通过setStatea方法修改state的值;当state变化时候会触发当前函数重新执行。
import { useState } from 'react'
export default function(){
// 当函数组件中逻辑更新data时,当前函数组件会重新执行,生成最新的dom区更新视图
const [data, setData] = useState(1)
return <div>{data}</div>
}
useEffect Hooks解决生命周期问题
生命周期指的是传统设计上组件是有生命的,组件会经过一个从初始化–更新–销毁的完整的流程,但是函数没有生命的,函数是纯粹执行过程根据state的变化每次执行返回不同的渲染结果。
函数组件不存在生命周期的概念。
useEffect用来处理与渲染无关的副作用代码的。解决函数内部逻辑重复执行时候,渲染结果不被重复执行。
const [state1, setState1] = useState(1)
useEffect(()=>{
//只有state1发生变化时才执行这段副作用代码
}, [state1])
//如果有些逻辑既与state无关又与渲染结果无关,那说明这段副作用只需要执行一次。可以不传入依赖状态,则只会在组件首次渲染时触发。
// useEffect可以返回一个方法,该方法会在组件被销毁时执行,这样也顺便实现了类似传统生命周期中
// “componentDidMount”与“componetWillUnmount”的功能。
useEffect(()=>{
document.addEventListener('click',fn);
return ()=>{
doucument.removeEventListener('click',fn);
}
},[])//不传参数,说明副作用逻辑只需要执行一次
useRef函数组件的逃脱机制
实现函数重复执行渲染过程中的数据共享,能从纯函数重复执行中逃脱,贯彻整个组件渲染的变量
useRef在存储DOM节点,和清理某次渲染过程产生的闭包逻辑有非常重要的意义
import (useState,useRef) from 'react';
export default function timer(){
const[time,setTime] = useState(1);
const timer = useRef(null);
const comRef = useRef(null);
const click = function(){
// 需要在每次执行前清理掉之前的定时器
// 如果不使用useRef,函数组件重复渲染后无法找到上一次函数执行产生的定时器对象
window.clearInterval(timer.current);
// 延时器对象赋值给useRef
timer.current = window.setTimeout(()=>{
setTime(time+1);
},5000);
}
const getDom = function(){
// 我们可以毫无负担的获取到domRef,不受函数组件重复执行的影响
console.log(domRef)
}
return(
<div ref={domRef}>
<p>{time}</p>
<button onClick={click}>add</button>
<button onClick={getDom}>getDom</button>
</div>
)
}
useContext、useCallback、useContext优化组件
useContext
跨组件通信useContext
会破坏组件中简洁的数据流结构,建议不要使用,现实中使用哦redux就可以统一管理数据,跨组件通信
useCallback与useMemo
两个hooks都是用于组件中的缓存
useCallback的作用是返回一个固定引用地址的函数,相当于缓存一个声明的函数,通常用它进行性能优化。
//const cachedFn = useCallback(fn, dependencies)
import { useState, useCallback } from 'react'
export default function MyFun(props) {
console.log('MyFun组件运行了')
const [count, setCount] = useState(1)
const testFn = useCallback(() => {
console.log('测试useCallback')
}, [])
function handleClick() {
setCount(2)
}
return (
<div className='MyFun'>
<div>state: {count}</div>
<button onClick={handleClick}>更新</button>
</div>
)
}
useMemo的作用是在函数组件重新渲染的时候能够缓存计算的结果,通常用它进行性能优化。
// const cachedValue = useMemo(calculateValue, dependencies)
import { useState, useMemo } from 'react'
export default function MyFun(props) {
console.log('MyFun组件运行了')
const [count, setCount] = useState(1)
// 缓存计算的结果
const countPx = useMemo(() => count + 'px', [count])
function handleClick() {
setCount(2)
}
return (
<div className='MyFun'>
<div>state: {count}</div>
<button onClick={handleClick}>更新</button>
</div>
)
}
//当useMemo返回一个计算结果时,它的效果与Vue中computed基本一致,它们的区别在于computed自动收集依赖,useMemo需要我们手动添加依赖。
const countPx = useMemo(() => count + 'px', [count])
const countPx = computed(() => count + 'px')
// 当useMemo返回一个函数时,它的效果与useCallback基本一致。
const fn = useMemo(() => {
return () => {
console.log('useMemo')
}
}, [])
const fn = useCallback(() => {
console.log('useCallback')
}, [])
redux , react-redux ,redux-saga,Dva状态管理工具
- redux:Redux对所有state实行了单向数据流的控制,state只出不进,页面里组件连向仓库的的蓝色方块Dispatch发出state修改请求,只有调用reducers里的函数才能修改state,reducers现在里面还都是同步函数。
- redux-saga:saga实际上就是为了解决redux中存在的异步reducer问题的。redux的dispatch向store发送action去触发reducer的这个过程是可以被拦截下来的,那自然就可以在这个过程里添加各种中间件,来实现各种自定义功能,而saga实际上就是实现了处理异步操作的功能。通过Generator函数实现。
- Redux-Router:Dva里的subscription就是参考的Redux-Router。订阅(监控)数据源(数据源可以是当前的时间,服务器的 websocket连接, keyboard输入,geolocation变化,history路由变化等等),然后根据需要 dispatch相应的 action.
- Dva:Dva是啥,你可以把它理解为Redux + Redux-Saga + Redux-Router。神说,我需要一个state的管理系统,于是Redux出现了;神说,我需要一个能出来异步操作的中间件,于是Redux-Saga出现了;神说,我还想处理一下路由改变引起的数据流,于是Redux-Router出现了;神说我全都要,于是Dva出现了
export default {
namespace: 'example',
state: {},
subscriptions: {
keyboardWatcher({ dispatch }) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
setup({ dispatch, history }) { // 这里的方法名可以随便命名,当监听有变化的时候就会依次执行这的变化,这里的dispatch和history和之前说的是一样的
window.onresize = () => { //这里表示的当浏览器的页面的大小变化时就会触发里面的dispatch方法,这里的save就是reducers中的方法名
dispatch (type:"save")
}
},
onClick ({dispatch}) {
document.addEventListener('click',() => { //这里表示当鼠标点击时就会触发里面的dispatch命令,这里的save就是reducers中的方法名
dispatch (type:"save")
})
}
},
setupHistory({dispatch,history}){
history.listen((location) => {
console.log(location) //这里可以获取当前变化的history路径以及参数,hash所有值,这样就可以在路由地址变化后做处理
....
})
}
effects: {
*fetch({ payload }, { call, put,select }) { // eslint-disable-line
yield put({ type: 'save' });
},
},
reducers: {
save(state, action) {
return { ...state, ...action.payload };
},
},
};
一定要理解Flux单向数据流:
Redux
这个家伙是Flux概念最好的实现之一。我们按照刚才Flux的思想,来看看Flux是怎么实现其中的概念的:
Store
Store在redux里也被定义为Store,他是唯一的数据中心,如何创建Store:
import { createStore } from 'redux';
// 具体参数我们后面再看
const store = createStore(...params);
Action
redux将其封装成一个对象:
const action = {
type: 'ADD',
payload: 1,
};
Dispatcher调度
Store实例下有一个dispatch方法,用来派发我们的每一个Action,这也是View触发Action的唯一方式:
store.dispatch(action)
Reducer 还原剂
通过store.dispatch我们的Store已经接收到了Action,那么之后需要根据Action.type和Action.payload修改Store里的数据,这部分redux提供Reducer来实现这部分操作。他和JS原生的reduce方法一样,接受一个当前state的,返回一个最新的state:
// 接受当前的state和传入的action作为参数
// 默认的state为0,定义默认state的更常用的方法后面我们会介绍
function counterReducer(state = 0, action) {
switch (action.type) {
case 'ADD':
return state + action.payload;
case 'MINUS':
return state - action.payload;
default:
return state;
}
}
store.dispatch派发了action之后,我们肯定希望直接就调用reducer,那么怎么将store和action关联?这就是我们createStore的第一个参数:
const store = createStore(counterReducer);
subscribe
当我们Store中的数据发生了改变之后,如何通知到View发生变化呢?Redux提供了Store的subscribe方法,用来搭建Store和View之间桥梁:
store.subscribe(listener)
这样,当我们的store中的数据发生改变之后,就会立即通知listener发生改变,在react项目中,这个listener一般就是我们的render方法
createStore方法
为什么要把我们一开始初始Store的方法放到现在来说呢?因为这个方法里面的参数是在你了解了Redux是如何实现Flux之后理解起来才比较容易的。createStore接受三个参数:第一个参数就是我们的reducer,第二个参数是我们Store的初始状态,如果设置了他,那么我们reducer中第一个参数默认的初始状态会失效,第三个参数用来处理中间件的
中间件
中间件是Action发起之后,Store接收到之前的一系列扩展方法,使用过中间件之后我们的流程变为:
举个例子🌰:比如我们的处理Action时很可能要进行一些发起请求之类的异步操作,那我们就要用到redux-trunk或者redux-promise这些中间件,具体用法请参考他们的官网。(不过我还是推荐react-saga,详情看后文)
React-Redux
React官方为Redux提供的扩展库,让我们很嗨皮的在项目中使用Redux。
主要功能是connect方法和Provider组件。
Provider组件
在React中Context的基础上实现,如果不了解,请先了解React中的Context部分。我们的React项目一般数据都是自上而下传递的,那么我们如果在根组件上绑定了store,孙子组件们都能拿到store的数据。Provider存在的价值就是我们的孙子组件能更方便的获取到根组件的store。
connect
接受四个参数:
- mapStateToProps:是一个function,字面意思很清楚,就是将Redux中的数据映射到Component的props上,达到Store和View绑定的目的(这不就是前面的subscribe做的吗)。本身参数有两个,一个是Store中的数据,第二个参数是组件本身的props。 mapStateToProps返回一个对象,表明state和要绑定Component的props的映射关系
const mapStateToProps = state => ({
foo: state.bar
})
const Foo = ({foo}) => {
// 这样我们的props里的foo其实是state里的bar
return <div>{foo.bar}</div>
}
export default connect(mapStateToProps)(Foo);
- mapDispatchToProps,和第一个参数雷同,只不过是把dispatch和props关联,使我们的组件可以通过调用Props上的方法来触发dispatch
const mapDispatchToProps = dispatch => ({
add: () => {dispatch({type: 'ADD'})}
})
const Foo = ({add}) => {
// 这样我们的props里的foo其实是state里的bar
return <div onClick={add}>Click Me</div>
}
export default connect(mapStateToProps, mapDispatchToProps)(Foo);
后两个参数用的很少,可以查阅官方文档,不做赘述。
Redux-saga
这是目前处理Redux异步Action的中间件中,我认为最为优雅的方案。首先我们在概念上把这些网络请求,读取等异步操作统称为副作用(Side Effects),并不意味着不好,这完全取决于特定的编程范式。saga是一个Generator函数,如果不清楚,推荐先去看一下这篇文章
使用redux-saga后我们的数据流变成了:
takeEvery
// rootSaga.js
import { takeEvery } from 'redux-saga'
import { call, put } from 'redux-saga/effects'
const timeout = time => new Promise(resolve => setTimeout(resolve, time));
function* addAsync() {
yield call(timeout, 2000);
yield put({ type: 'ADD' });
}
export default function* rootSaga() {
yield* takeEvery("ADD_ASYNC", addAsync)
}
yield call 和 yield put 我们一会儿再看,先解释一下takeEvery,这个函数监听一个action的type,也就是说一旦发现触发了ADD_ASYNC的action,那么就会立即执行addAsync方法
takeLatest
在实际需求中,我们可能只需要拿到最后一次执行的结果,这就用到takeLatest。简单的说,如果我们使用takeEvery,每次ADD_ASYNC触发时都会调用addAsync,而如果是takeLatest,如果ADD_ASYNC连续被触发,那么如果上一次触发未结束,会先结束上一次触发,执行最新的这次触发。示例和takeEvery相似,不做赘述。
Effect 方法
最常用的是 put 和 call
put 可以简单的理解为Redux的dispatch,阻塞后面的逻辑
// 触发一个type为ADD的action,Reducer会重新计算state并返回
yield put({
type: 'ADD',
payload: 1
})
call 调用其他函数,可以是一个Generate函数,也可以是返回一个Promise的普通函数。阻塞后面的逻辑
Dva
看到这里,是不是觉得很繁琐,我写一个项目,怎么要引这么多库?别愁,Dva来了
Dva是Redux和Redux-saga之上的一个框架,就像Egg对于Koa一样,都是框架之上的另一层封装,我们来看一下他是如何解决上面的痛点的
Model
dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions
// addModel.js
{
namespace: 'addModel',
state: 0,
reducers: {
add(state) { return state + 1 },
},
effects: {
*addAfter1Second(action, { delay, put }) {
yield delay(1000);
yield put({ type: 'add' });
},
},
}
然后我们再把Model和Component通过connect连接起来就ok了,connect用法参考前面的react-redux的connect方法。至此我们之前所说的Redux和Redux-Saga都集成在了model中,真的是棒棒。
//这两个搭配使用效果更佳:
Umi
Dva
umi路由
封装网络请求Ajax、axios、fetch 用法、区别,跨域问题
HTTP
- 请求报文
- 行:各种请求类型(GET、POST、DELETE … )+ URL 路径(可能还会带有一些参数) + HTTP 版本
- 头:(Key: Value) 对请求体内容做一个简单的描述
- Host: baidu.com
- Cookie: name=baidu
- Content-type: application/x-www-form-urlencoded
- User-Agent: chrome 83
… - 空行(必须有)
- 体:
- GET 请求请求体为空,POST 请求请求体可以不为空。
例如: username=admin&password=admin
- 响应报文
- 行:HTTP 版本 + 响应状态码 + 响应状态字符串
- 头:对响应体内容做一个相关的描述
- Content-Type: text/html;charset=utf-8
- Content-length: 2048
- Content-encoding: gzip
… - 空行:(必须有)
- 体:主要的返回的结果(html 标签结构)
- 浏览器接到结果后,将响应体结果提取出来,对内容做一个解析,在页面做一个渲染和显示
原生 JS 发送 XMLHttpRequest(XHR)请求
- XHR 是早出现的向后端发送请求的技术
- 原生 js 发送的请求,并不是 Ajax 请求,而是 XHR 请求。在 JQuery 封装过后,才有了 Ajax 请求。
服务端
// 创建一个服务器
// 1. 引入express
const express = require('express');
// 2. 创建一个应用对象
const app = express();
// 3. 创建路由规则
// request 是对请求报文的封装
// response 是对响应报文的封装
app.get('/server', (request, response)=>{
// 设置响应头 设置允许跨域
response.setHeader('Access-Content-Allow-Origen', '*');
// 设置响应体
response.send('Hello Ajax GET');
});
app.post('/server', (request, response)=>{
// 设置响应头 设置允许跨域
response.setHeader('Access-Content-Allow-Origen', '*');
// 设置响应体
response.send('Hello Ajax POST');
});
// 4. 监听端口启动服务
app.listen(8000, ()=>{
console.log("服务已经启动,8000 端口监听中 ... ");
});
客服端请求
// 客户端发送get请求
// 获取button 元素
const btn = document.getElementByTagName('button')[0];
// 获取
const result = document.getElementById("result");
// 绑定事件
btn.onclick = function(){
// 1. 创建对象
const xhr = new XMLHttpRequest();
// 2. 初始化 设置请求方法和url
xhr.open('GET', 'http://127.0.0.1:8000/server?a=100&b=200');
// 3. 发送
xhr.send();
// 4.事件绑定 处理服务器返回的结果
// on when 当 ... 的时候
// readystate 是xhr对象中的属性,表示状态 0 1 2 3 4
// 0 初始化的属性状态
// 1 open方法已经调用完毕
// 2 send方法调用完毕
// 3 服务端返回了部分结果
// 4 服务端返回了所有的结果
// change 改变的时候触发
// 下面的函数会被触发四次,状态每改变一次,就会触发一次
xhr.onreadystatechange = function(){
// 判断(服务端返回了所有的结果)
if(xhr.readyState === 4){
// 判断响应状态码
if(xhr.state >= 200 && xhr.state < 300){
// 处理结果
// console.log(xhr.status); // 状态码
// console.log(xhr.statusText); // 状态字符串
// console.log(xhr.getAllResponseHeaders()); // 所有响应头
// console.log(xhr.response); // 响应体
// 设置result 的文本
result.innerHTML = xhr.response;
}
}
}
}
// 客户端发送post请求
const result = document.getElementById("result");
result.addEventListener("mouseover", function(){
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://127.0.0.1:8000/server');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send('a=100&b=200');
//xhr.send('a:100&b:200');
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
if(xhr.state >= 200 && xhr.state < 300){
result.innerHTML = xhr.response;
}
}
}
})
2. 使用 JQuery 发送 Ajax 请求:
Ajax:Asynchronous Javascript And XML 异步 JavaScript 和 XML
优点:
在网页不刷新的情况下,更新网页的部分内容。
缺点:
针对后端 MVC 模型编程,与前端框架的 MVVM 风格不符
使用 Ajax 必须引入 JQuery,但 JQuery 太大,个性化打包又不能享受 CDN 加速服务
不符合关注分离原则(关注分离:部分发生了变化,不会影响其他部分。够清晰地识别出哪些部分需要改变。)
SEO(Search Engine Optimization 搜索引擎优化)不友好【通过 Ajax 异步请求获取数据,通过 js 动态创建到页面,因此源代码(响应体)中并不存在商品信息,而爬虫只能爬取源代码中的数据】
// 1. 发送get请求
$('button').eq(0).click(function(){
$.get('http://127.0.0.1:8000/jquery-server', {a: 100, b: 200}, function(){
console.log(data);
})
})
// 2. 发送post请求
$('button').eq(1).click(function(){
$.post('http://127.0.0.1:8000/jquery-server', {a: 100, b: 200}, function(){
console.log(data);
})
}, 'json')
// 第四个是一个可选参数:'json',表示数据类型,会自动将服务器返回的json类型的字符串转化为js对象
// 3. 发送通用请求
$('button').eq(2).click(function(){
url: 'http://127.0.0.1:8000/jquery-server',
data: {a: 100, b: 200},
type: 'GET',
dataType: 'json',
success: function(data){
console.log(data);
}
timeout: 2000,
error: function(){
console.log('出错了 ... ')
}
// 头信息
header: {
c: 300, // 这是两个自定义的头
d: 400
}
}, 'json')
// 发送通用请求时,需要在在服务器上更改这两处配置:
// 1. 接受请求方式为all
// 2. 设置允许跨域
app.all('/server', (request, response)=>{
// 设置响应头 设置允许跨域
response.setHeader('Access-Content-Allow-Origen', '*');
// 设置响应体
response.send('Hello Ajax');
});
axios 发送 Ajax 请求
优点:
1、基于 Promise 的 http 请求库,返回一个 Promise 对象,本质上也是对 XHR 的封装。
2、客户端支持防止 CSRF
3、提供了一些并发请求的接口(重要,方便了很多)
4、axios 提供了并发的封装,没有 fetch 的各种问题,而且体积也较小
PS:防止CSRF(跨站请求伪造):就是让我们的每个请求都带一个从 cookie 中拿到的 key,根据浏览器同源策略,假冒的网站是拿不到你 cookie 中的 key 的,这样,后台就可以轻松辨别出这个请求是否是用户在假冒网站上的误导输入,从而采取正确的策略。
// 1. 借助axios发送get请求
const btns = document.querySelectorAll('button');
axios.defaults.baseURL = 'http://127.0.0.1:8000';
btns[0].onclick = function(){
axios.get('/axios-server', {
// url参数
params: {
id: 100,
vip: 10
}
// 请求头信息
headers: {
name: 'zhangsan',
nickName: 'fawaikuangtu'
}
}).then(value => {
console.log(value);
})
}
// 2. 借助axios发送post请求
btns[1].onclick = function(){
axios.post('/axios-server', {
// 请求体信息
username: 'zhangsan',
password: 'luoxiang'
}, {
// url参数
params: {
id: 100,
vip: 10
}
// 请求头信息
headers: {
name: 'zhangsan',
nickName: 'fawaikuangtu'
}
}).then(res=> {
console.log(res);
})
}
// 3. 借助axios发送post请求
btns[2].onclick = function(){
axios({
method: 'POST',
url: '/axios-server',
// 请求体信息
data:{
username: 'zhangsan',
password: 'luoxiang'
},
// url参数
params: {
id: 100,
vip: 10
}
// 请求头信息
headers: {
name: 'zhangsan',
nickName: 'fawaikuangtu'
}
}).then(res=> {
console.log(res);
console.log(res.status);
console.log(res.statusText);
console.log(res.headers);
})
}
使用 fetch 发送异步请求
fetch 不是对 XHR 的封装,也没有使用 XHR 对象,在本质上,应该说是 XHR 的同类,因为它们都是底层的发送请求的方法。
优点:
1、更符合关注分离,没有将输入、输出、状态跟踪很杂在一起
2、基于 Promise,返回一个 Promise 对象。
缺点:
1、过于底层,对400、500的状态码不会 reject,还需要再次封装
2、默认不带 cookie,需要添加配置项 fetch(url, {credentials: ‘include’})
3、不支持 abort,无法直接阻断请求,不支持超时控制,使用 setTimeout、Promise.reject 无法阻止请求,请求过程依旧在后台运行
4、无法像 XHR 一样检测请求进度
5、兼容性不好,IE 不支持。
PS:XHR send 之后,本质请求已经发送, 进入网络传输了,流量浪费已经发生,abort 只是在我们未来某个时机调用,不想要这个请求了,即使远端服务器返回的数据已经被浏览器拿到,在我们调用了 abort 方法后,也可以不执行回调。
// 借助fetch发送post请求
btns[2].onclick = function(){
fetch('http://127.0.0.1:8000/fetch-server?vip=10', {
method: 'POST',
// 请求体信息
body: 'username=zhangsan&password=luoxiang',
// 请求头信息
headers: {
name: 'zhangsan',
nickName: 'fawaikuangtu'
}
}).then(res => {
return res.text();
// return res.json(); // 自动帮我们把数据解析成js对象
}).then(res => {
console.log(res);
})
}
同源策略
请求的 url 与 Ajax 请求的目标资源的 url 协议、域名、端口完全一致时
解决跨域的方案
- JSONP(JSON with Padding)解决跨域
- 只支持 get 请求。
- 借助页面的一些天生可以跨域的 script 标签实现(img、link、iframe、script)
- JQuery 发送 JSONP 请求
这种方式需要我们在 url 后面拼接一个 callback=?,这是格式要求 - CORS(跨域资源共享)
- 这是一个官方的跨域解决方案
- 原理是不需要在客户端做任何操作,告诉服务器,哪些源站通过浏览器有权访问哪些资源
// 3. 创建路由规则
// request 是对请求报文的封装
// response 是对响应报文的封装
app.get('/server', (request, response)=>{
// ------------------- 这里就是CORS策略 -------------------
// 设置响应头 设置允许跨域
response.setHeader('Access-Content-Allow-Origen', '*');
// -------------------------------------------------------
// 设置响应体
response.send('Hello Ajax GET');
});
// 我们一般会再附带两个设置:
response.setHeader('Access-Content-Allow-Origen', '*'); // 哪个页面都可以
response.setHeader('Access-Content-Allow-Headers', '*'); // 头信息可以自定义
response.setHeader('Access-Content-Allow-Method', '*'); // 请求方法可以选择任意方式
网络基础
react+typescript+umi+dva+antd项目
1、先安装node,再创建一个项目文件夹,通过脚手架创建umi项目
npx @umijs/create-umi-app
2、安装依赖
npm i
3、运行项目
npm run start
4、配置路由
在配置文件.umirc.ts中通过 routes 进行配置,格式为路由信息的数组。
//.umirc.ts文件
routes: [
{ path: '/', component: '@/pages/Index' },
{ path: '/User', component: '@/pages/User' },
],
5、路由配置工程化
为了让代码更工程化,我们可以将路由配置单独拆分成一个文件router.tsx
//router.tsx文件
const router: any = [
{ path: '/', component: '@/pages/Index' },
{ path: '/User', component: '@/pages/User' },
]
export default router;
// .umirc.ts
import { defineConfig } from 'umi';
import router from '@/router/router'; // 引入路由配置文件
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
routes: router,
});
6、约定式路由
除配置式路由外,Umi 也支持约定式路由。约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。
备注:约定式路由要先注释.umirc.ts里的routes配置
7、路由跳转
import { history } from 'umi';
// 跳转到指定路由
history.push('/list');
// 带参数跳转到指定路由
history.push('/list?a=b');
history.push({
pathname: '/list',
query: {
a: 'b',
},
});
// 跳转到上一个路由
history.goBack();
示例
//Index.tsx
import React, { Component } from 'react'
import { history } from 'umi';
import styles from '@/assets/Style/Index.less'; // css module
export default class Index extends Component {
// 路由跳转不带参数
toLogin() {
history.push('/login');
}
// 路由跳转带参数
toLoginWithParameter() {
history.push({
pathname: '/login',
query: {
a: 'b',
},
})
}
render() {
return (
<div>
<div className={styles.title}>首页</div>
<div onClick={this.toLogin}>跳转不带参数</div>
<div onClick={this.toLoginWithParameter}>跳转带参数</div>
</div>
)
}
}
import React, { Component } from 'react'
export default class User extends Component {
constructor(props: any) {
super(props);
console.log(props.location.query) // 打印路由参数
}
render() {
return (
<div>
<h1 className={styles.title}>User</h1>
</div>
)
}
}
//User.tsx
import React, { Component } from 'react'
export default class User extends Component {
constructor(props: any) {
super(props);
console.log(props.location.query) // 打印路由参数
}
render() {
return (
<div>
<h1 className={styles.title}>User</h1>
</div>
)
}
}
8、安装插件集@umijs/preset-react(忽略)
@umijs/preset-react是包含antd,dva等一系列插件的插件集,由于使用指令npx @umijs/create-umi-app创建的项目会自动添加此插件,所以之后不需要再安装@umijs/preset-react插件集包含的插件,否则启动项目的时候会报错
9、实例
创建src/models/userInfo.tsx
import { Effect, ImmerReducer, Reducer, Subscription } from 'umi';
export interface UserInfoModelState {
name: string;
age: number;
}
export interface UserInfoModelType {
namespace: 'userInfo';
state: UserInfoModelState;
effects: {
query: Effect;
};
reducers: {
save: Reducer<UserInfoModelState>;
changeName: Reducer<UserInfoModelState>;
// 启用 immer 之后
// save: ImmerReducer<UserInfoModelState>;
};
subscriptions: { setup: Subscription };
}
const UserInfoModel: UserInfoModelType = {
namespace: 'userInfo',
state: {
name: '张三',
age: 20,
},
effects: {
*query({ payload }, { call, put }) {
},
},
reducers: {
save(state, action) {
return {
...state,
...action.payload,
};
},
changeName(state, action) {
return {
...state,
...action.payload,
};
},
// 启用 immer 之后
// save(state, action) {
// state.name = action.payload;
// },
},
subscriptions: {
setup({ dispatch, history }) {
return history.listen(({ pathname }) => {
if (pathname === '/') {
dispatch({
type: 'query',
})
}
});
}
}
};
export default UserInfoModel;
pages/User.tsx
class组件中使用
import React, { Component } from 'react'
import { connect, UserInfoModelState, Loading } from 'umi';
const connect1: any = connect;
@connect1(({ userInfo, loading }: { userInfo: UserInfoModelState; loading: Loading }) => ({
userInfo,
// dva-loading可以自动处理loading状态
loading: loading.models.index,
}))
export default class User extends Component<any, any> {
constructor(props: any) {
super(props);
console.log(props);
this.state = {
username: props.userInfo.name
}
}
// 调用userInfo模块的reducers里的changeName方法
private changeName = () => {
const { dispatch } = this.props;
dispatch({
type: 'userInfo/changeName',
payload:{
name: '李四'
}
})
}
render() {
return (
<div>
<div onClick={this.changeName}>更改用户名</div>
<div className="title">用户名{this.props.userInfo.name}</div>
</div>
)
}
}
函数式组件中使用
import React, { FC } from 'react';
import { UserInfoModelState, ConnectRC, Loading, connect } from 'umi';
interface PageProps {
userInfo: UserInfoModelState;
loading: boolean;
}
const IndexPage: FC<PageProps> = (props) => {
const handleClick = () => {
const { dispatch } : any = props;
dispatch({
type: 'userInfo/changeName',
payload:{
name: '李四'
}
})
}
return (
<div>
<div onClick={handleClick}>更改用户名</div>
<div className="title">用户名{props.userInfo.name}</div>
</div>
)
};
export default connect(({ userInfo, loading }: { userInfo: UserInfoModelState; loading: Loading }) => ({
userInfo,
loading: loading.models.index,
}))(IndexPage);