AJAX调用

基于现在流行的前后端分离架构,通过前端调用后端API的需求就很有必要。但是React本身只能使用自身state和props中的数据来渲染UI,这就需要你自己使用一个基于AJAX技术来获取数据的函数库了。

当前主流的库有:FetchAxiosSuperagent,这些HTTP请求库区别不大,挑喜欢用。

React中请求AJAX的时机一般是组件第一次被挂载到浏览器DOM,这时候你就可以根据获取的数据来更行状态了。

类组件将AJAX请求放到componentDidMount()生命周期中,然后数据取得之后使用setState方法赋值给变量。下面是一个获取GitHub用户数据API的请求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export default class App extends React.Component {
  constructor(props){
    super(props)
    this.state = {
      data: []
    };
  }
  
  componentDidMount(){
    fetch("https://api.github.com/users?per_gage=3")
    .then((res) => res.json())
    .then(
    	(data) => {this.setState({data: data});},
      (error) => {console.log(error)}
    );
  }
  
  render(){
    const { data } = this.state;
    return (
    	<div>
      	<h1>React AJAX call</h1>
        <ul>{data.map((item) => (<li key={item.id}>{item.login}</li>))}</ul>
      </div>
    )
  }
}

当同样的请求使用函数组件获取时,要将请求放到useEffect()hook中,并且第二个参数设置为[],这样hook只在第一次渲染时运行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
export default function App(){
  const [data, setData] = useState([]);

  useEffect(()=>{
    fetch("https://api.github.com/users?per_gage=3")
    .then((res) => res.json())
    .then(
    	(data) => {setData(data);},
      (error) => {console.log(error)}
    );
  }, []);

  return (
    <div className="App">
      <h1>React AJAX call</h1>
      <ul>
        {data.map((item) => (<li key={item.id}>{item.login}</li>))}
      </ul>
    </div>
  )
}

如果你的网络情况不是太好的话,你可能后明显的感觉到页面渲染延迟,这时我们可以添加一个加载中的提示,告诉用户耐心等待。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export default function App(){
  const [data, setData] = useState([]);
  const [isLoading, setLoading] = useState(true)
  const [error, setError] = useState(false)

  useEffect(()=>{
    setTimeout(()=>{
      fetch("https://api.githu.com/users?per_gage=3")
      .then((res) => res.json())
      .then(
        (data) => {
          setData(data);
          setLoading(false);
        },
        (error) => {
          setError(error);
          setLoading(false);
        }
      );
    }, 2000);
  }, []);

  if (error) {
    return <div>Fetch request error: {error.message}</div>;
  } else if (isLoading) {
    return <h1>Loading data...</h1>
  }else{
    return (
      <div className="App">
        <h1>React AJAX call</h1>
        <ul>
          {data.map((item) => (<li key={item.id}>{item.login}</li>))}
        </ul>
      </div>
    );
  }
}

我们通过isLoading来控制输出,当数据没获取完成,它的值是true,显示正在加载的动画;当数据获取完成它被设置为false,能够正确显示获取到的数据。为了演示效果,这里还在useEffect中添加超时函数,用来模拟网络阻塞的情况。最后代码中还加了对错误信息的处理。

使用Axios

Axios是JavaScript对于HTTP的请求库,它跟原生的fetchAPI相似,但是添加了更多有用的特性(来自官方文档):

  • Make XMLHttpRequests from the browser
  • Make http requests from node.js
  • Supports the Promise API
  • Intercept request and response
  • Transform request and response data
  • Cancel requests
  • Automatic transforms for JSON data
  • Client side support for protecting against XSRF

通常对于JavaScript开发者来说,Axios更加通用,它不仅能够自动转化JSON响应到JavaScript数组或者对象,同时无论是服务器端(node.js)或浏览器端都能使用,而fetch在Node.js中不是原生支持。

在React中使用axios

使用axios需要额外安装包

1
npm install axios

剩下的跟使用fetch一样,把请求放在componentDidMount()或者useEffect hook当中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
componentDidMount(){
  axios.get("https://api.github.com/users?per_gage=3")
  .then(
    (response)=>{
      this.setState({
        data: response.data
      });
    }
  )
  .catch(error => {
    console.log(error)
  })
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
useEffect(()=>{
  setTimeout(()=>{
    axios.get("https://api.github.com/users?per_gage=3")
    .then((respone) => {
      setData(respone.data);
    })
    .catch((error) => {
      console.log(error);
    });
  }, 2000);
}, []);

Axios请求示例

Axios 支持所有的HTTP请求方法,你可以在请求之前对axios进行设置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
axios({
  method: 'get',
  url: 'https://api.github.com/users'
})
.then(function (response){
  console.log(response)
});

// 或者

axios({
  method: 'post',
  url: 'https://api.github.com/users',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
})
.then(function (response){
  console.log(response)
});

或者直接使用对应的方法

1
2
3
4
5
6
7
8
axios.get("https://api.github.com/users")
axios.post("https://api.github.com/users", {
    firstName: 'Fred',
    lastName: 'Flintstone'
})
axios.put("https://api.github.com/users")
axios.patch("https://api.github.com/users")
axios.delete("https://api.github.com/users")

同时执行多个请求

你可以使用Promise.all()配合Axios来并行发出多个请求,等到响应返回之后在执行接下来的逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function getUserAccount(){
  return axios.get('/user/123456')
}

function getUserPermissions(){
  return axios.post('/user/123456/permissions', {
    firstName: 'Fred',
    lastName: 'Flintstone'    
  })
}

Promise.all([getUserAccount(), getUserPermissions()])
	.then(function (results)){
		const acct = results[0];
		const perm = results[1];    
}

多个响应以请求的顺序返回成一个数组

React Router

React router 是为了解决React应用路由问题的第三方库,它封装了浏览器history API使你的应用UI与浏览器URL同步。当你访问/about页面,React Router会确保About相关页面渲染到屏幕。作为在浏览器运行的单页面应用,React Router提供的功能能让你方便的在浏览器端切换,而不用每个URL都向服务器发送请求。

React Router有两个包:给React使用的react-router-dom,给React Native使用的react-router-native,我们web应用只用前者就可以了。

1
npm install react-router-dom

React Router通常会用到三个组件:BrowserRouterRouteLink

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { BrowserRouter as Router, Route} from 'react-router-dom';

class RouterNavigationSample extends React.Component {
  render(){
    return (
      <Router>
        <>
          <NavigationComponent />
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
        </>
      </Router>
    )
  }
}

BrowserRouter导入别名Router,作为所有React组件的父组件,在最外层。它会拦截浏览器请求的URL并匹配到正确的Route组件。例如浏览器URL是localhost:3000/aboutRouter会查找路径为/about对应的组件,这里我们向路径/aboutRoute注册的组件是About

在根路径上我们加入了exact参数,如果没有这个参数,任何path中包含/的路由都会被渲染成Home组件。

Link组件被用作导航,替换传统的<a>标签。传统的锚点标签会在每次点击的时候完全刷新,它不适用于React 应用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class NavigationComponent extends React.Component {
  render(){
    return (
      <>
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/about">About page</Link></li>
        </ul>
        <hr/>
      </>
    )
  }
}

动态路由

接着我们来学习另外两个路由组件:SwitchRedirect

Switch组件会在找到第一个匹配路由之后渲染,并停止向下匹配。比如说有下面的例子

1
2
3
4
5
import { Ruote } from 'react-router';

<Route path="/about" component={About} />
<Route path="/:user" component={User} />
<Route component={NoMatch} />

上面的代码中,/about会匹配到全部的三个路由,导致它们全部被渲染并且彼此紧邻。通过使用Switch组件,路由仅在匹配到About之后就停止。

1
2
3
4
5
6
7
import { Switch, Ruote } from 'react-router';

<Switch>
  <Route path="/about" component={About} />
  <Route path="/:user" component={User} />
  <Route component={NoMatch} />
</Switch>;

这里组件的顺序特别重要,所以确保在声明参数路由、404路由之前先整理好所有静态路由。

Redirect组件用于重定向,from参数填写旧的链接,to参数写将要转向的路由。

1
2
import { Redirect } from 'react-router';
<Redirect from='/old-match' to='/will-match' />;

嵌套路由

直接放案例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import { BrowserRouter as Router, Link, Route} from 'react-router-dom';

const users = [
  {
    id: '1',
    name: 'Nathan',
    role: 'Web Developer',
  },
  {
    id: '2',
    name: 'Johnson',
    role: 'React Developer',
  },
  {
    id: '3',
    name: 'Alex',
    role: 'Python Developer',
  }
]

const Home = () => {
  return <div>This is the home page</div>
};

const About = () => {
  return <div>This is the about page</div>
};

const Users = () => {
  return (
    <>
      <ul>
        {
          users.map(({name, id}) => (<li key={id}><Link to={`/users/${id}`}>{name}</Link></li>))
        }
      </ul>
      <Route path='/users/:id' component={User} />
      <hr/>
    </>
  );
};

const User = (({match}) => {
  const user = users.find((user) => user.id === match.params.id);

  return (
    <div>
      Hello! I'm {user.name} and I'm a {user.role}
    </div>
  );
})

class NavigationComponent extends React.Component {
  render(){
    return (
      <>
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/about">About page</Link></li>
          <li><Link to="/users">Users page</Link></li>
        </ul>
        <hr/>
      </>
    )
  }
}

class RouterNavigationSample extends React.Component {
  render(){
    return (
      <Router>
        <>
          <NavigationComponent />
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
          <Route path="/users" component={Users} />
        </>
      </Router>
    )
  }
}

export default function App(){
  return <RouterNavigationSample />
}

这里我们加了些用户数据,想要实现的功能是:通过路由展示当前用户列表,用户列表中每个元素又能指向用户详情。

首先可以先写好对应的路由和跳转超链接,接着创建Users组件,它显示所有用户列表,同时也要用到LinkRoute来创建跳转到详情页面的子路由。

最后在用户详情User组件中通过/users/:id传过来的参数渲染对应用户的详细信息。

这里有一些需要了解的知识点,每次组件被特定的路由渲染,该组件都会接收到来自React Router的props。有三个路由参数会被向下传递,它们分别是:

matchlocationhistory,而接收/:id的参数正是match.params.id,最后的id便是路由中的名称,比如你换成了/:userId,那相应的参数应该改为

match.params.userId

了解了route props之后,现在我们可以重构一下Users组件中的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const Users = ({match}) => {
  return (
    <>
      <ul>
        {
          users.map(({name, id}) => (<li key={id}><Link to={`${match.url}/${id}`}>{name}</Link></li>))
        }
      </ul>
      <Route path={`${match.url}/:id`} component={User} />
      <hr/>
    </>
  );
};

这样就实现了动态路由

向路由组件传递参数

你可能会觉得向路由组件传递参数跟普通的组件传递方式一样:

1
<Route path="/about" component={About} user="Jelly" />

那你就大错特错了,React Router没办法将写在Route中的props转发到component的props中,我们需要另辟它法。

React Router 提供了 render参数,它接收一个方法,当URL被匹配了之后会被调用。这里的props就能够被组件的props接收到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<Route 
  path="/about"
  render={props => <About {...props} admin="Blus" />}
  />


// the component
const About = props => {
  return <div>This is the about page {props.admin}</div>;
}

首先你将React Router听歌的props作为参数传递给自定义的组件,这时组件能够使用 matchloationhistory这些参数。同时你也可以提供自定义的参数,上面的例子中admin就是这样。

React Router hooks

随着hooks的加入,React Router的开发者也提供了自己的hooks,你可以在自己的React函数组件中任意使用。它将大大提高你编写导航组件的效率。

useParams hook

useParams hook能够直接返回当前路由中的动态参数

例如,目前有一个`/post/:slug的URL,按照我们之前的处理方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
export default function App(){
  return (
  	<Router>
    	<div>
      	<nav>
        	<ul>
          	<li><Link to="/">Home</Link></li>
            <li><Link to="/post/hello-world">First Post</Link></li>
          </ul>
        </nav>
        <Switch>
        	<Route path="/post/:slug" component={Post} />
          <Route path="/"><Home /></Route>
        </Switch>
      </div>
    </Router>
  );
}

这里我们将Post组件传入到/post/:slug路由当中,这样我们就可以在组件中使用match.params来取得参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Old way to fetch parameters
function Post({ match }) {
  let params = match.params;
  return (
  	<div>
    	In React Router v4, you get parameters from the props.
      Current parameter is <strong>{params.slug}</strong>
    </div>
  );
}

这样做可行,但是如果项目中动态路由数目剧增,管理起来会很不方便。你需要了解哪个路由中的组件需要使用到props,哪个不需要。同时这个由Route传递下来的match对象,需要你手动的依次传递到下层的其他DOM组件。

这时候useParams hook就应运而生了,它能够在不使用组件props的情况下获取到当前路由的参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<Switch>
	<Route path="/post/:slug" component={Post} />
  <Route path="/users/:id/:hash">
    <Users />
  </Route>
  <Route>
    <Home />
  </Route>
</Switch>

function Users() {
  let params = useParams();
  return (
    <div>
    	In React Router v5, You can use hooks to get paramters.
      <br />
      Current id parameter is <strong>{params.id}</strong>
      <br />
      Current hash parameter is <strong>{params.hash}</strong>
    </div>
  )
}

如果这时候Users组件的子组件同样需要用到这些参数,你只需要接着使用useParams()就可以了。

useLocations hook

在React Router v4版本中,跟获取参数一样,你需要使用组件props模式来取得location对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<Route path="/post/:number" component={Post} />

function Post(props){
  return (
    <div>
    	In React Router v4, you get the location object from props.
      <br />
      Current pathname: <strong>{props.location.pathname}<strong>
    </div>
  );
}

v5.1以后,可以使用useLocation hook获取

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<Route path="/users/:id/:password" >
  <Users />
</Route>

// new way to fetch location with hooks
function Users(){
  let location = useLocation();
  return (
    <div>
    	In React Router v5, You can use hooks to get location object.
      <br />
      Current pathname: <strong>{location.pathname}<strong>
    </div>
  );
}

useHistory hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<Route path="/post/:slug" component={Post}>

// Old way to fetch history
function Post(props){
    return (
      <div>
      	In React Router v4, you get the history object from props.
        <br />
        <button type="button" onClick={() => props.history.goBack()}>Go back</button>
      </div>
    );
  }

使用useHistory hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<Route path="/users/:id/:hash">
  <Users>
</Route>


function Users(){
    let history = useHistory();
    return (
      <div>
      	In React Router v5, You can use hooks to get history object.
        <br />
        <button type="button" onClick={() => history.goBack()}>Go back</button>
      </div>
    );
  }

使用这些hooks能够极大的简化代码,我们不再需要在组件里面传递componentrender参数,仅需要在<Route>中传递URl path,并且把需要渲染的组件包裹在它之间就行。

useRouteMatch hook

有时候为了获取 match 对象,我们需要使用<Route>组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<Route path="/post">
  <Post />
</Route>


function Post(){
    return (
      <Route 
        path="/post/:slug"
        render={({ match }) => {
          return (
          	<div>Your current path: <strong>{match.path}</strong></div>
          );
        }}
        />
    );
  }

v5版本使用hooks的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<Route path="/users">
  <Users />
</Route>

function Users() {
  let match = useRouteMatch("/users/:id/:hash");
  return (
  	<div>
    	In React Router v5, You can use useRouteMatch to get match object.
      <br />
      Current match path: <strong>{match.path}</strong>
    </div>
  )
}

Context API

通常来说在React中,你的数据需要从顶层组件一层一层传递到下层组件,即使这些数据只有最后一层组件才需要用到。这种自上而下的数据流有一个好处就是能够精准的定位数据传递过程中在哪里出现了问题。

不过这种方法确实有点儿死板,所以React提供了类似定义全局变量的功能,这就是Context API

在一个组件中定义的数据作为provider生产者,而其他使用这个数据的组件作为consumer消费者。通过Context API共享的数据可以理解为React 组件树中的全局变量,它可以用来实现的功能有:共享当前认证用户、选择的主题或者语言。

useState hook使用类似,可以通过React.createContext()来创建,并传递默认值

1
2
3
4
import React from 'react';

// default to 'cn'
const LanguageContext = React.createContext('cn')

这样我们就得到一个可以使用的LanguageContext,它提供了LanguageContext.ProviderLanguageContext.Consumer两个组件。

生成context

context中的数据通常是由state提供,一般当state改变,context相应的数据也会改变。你需要使用Provider组件将自定义组件包起来

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import React from 'react';

const LanguageContext = React.createContext('cn')

function App() {
  const language = 'en'
  
  return (
    <LanguageContext.Provider value={language}>
      <Hello />
    </LanguageContext.Provider>
  )
}

这样<Hello>组件以及它的子组件都能使用LanguageContext提供的值。

方法组件使用context内容的方式

方法组件可以使用提供的 useContext hook

1
2
3
4
5
6
7
8
function Hello(){
  const language = useContext(LanguageContext)
  
  if (language === "en"){
    return <h1>Hello!</h1>
  }
  return <h1>你好</h1>
}

我们根据上面的代码修改,使得context值能够被改变

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { useState, useContext } from 'react';

const LanguageContext = React.createContext()

function App() {
  const [language, setLanguage] = useState("en");
  
  const changeLangeuage = () => {
    if (language === "en"){
      setLanguage("cn")
    } else {
      setLanguage("en")
    }
  }
  
  return (
    <LanguageContext.Provider value={language}>
      <Hello />
      <button onClick={changeLanguage}>Change Language</button>
    </LanguageContext.Provider>
  )
}

function Hello(){
  const language = useContext(LanguageContext)
  
  if (language === "en"){
    return <h1>Hello!</h1>
  }
  return <h1>你好</h1>
}

在类组件中使用context值

在类组件中你可以使用Context.Consumer组件来获取值。这个组件需要一个子函数,它会将当前的值传递进去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Hello extends React.Component {
  render() {
    return (
    	<LanguageContext.Consumer>
        {(language) => {
          if (language === "en"){
            return <h1>Hello!</h1>
          }
          return <h1>你好</h1>
        }}
      </LanguageContext.Consumer>
    );
  }
}

这种组件中套函数的方法,看着有点儿反人类,所以React官方还提供了另外一种方法,使用contextType,将context对象赋值给contextType,可以使用this.context得到context的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Hello extends React.Component {
  static contextType = LanguageContext;
  render() {
    const language = this.context;
    if (language === "en"){
      return <h1>Hello!</h1>
    }
    return <h1>你好</h1>
  }
}

由于全局变量的特性,你可以给多个组件提供相同的值。也就是说一个provider可以有多个consumers

Some Tips

不要使用变量展开的方式传递props

1
2
3
4
function MainComponent() {
  const props = { firstName: "Jack", lastName: "Skeld"}
  return <Hello {...props} />
}

这种方式将变量一股脑的传递到子组件,如果参数很多或者获取的数据来自其他API,全部传递的话,子组件会得到很多无用的数据,不便于定位数据。所以还是推荐用到什么数据就传递什么数据到子组件。

使用propTypes和defaultProps来对参数做限制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import React from "react";
import ReactDOM from "react-dom";

function App() {
  return <Greeting name="Nathan" />;
}
    
function Greeting(props){
  return (
  	<p>
    	Hello! I'm {props.name},
      a {props.name} years old {props.occupation}.
      Pleased to meet you!
    </p>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

上面的例子中props.nameprops.occupation没有被传递,但是运行不会报错,React会忽略这两个参数,直接渲染剩下的数据。

我们可以使用第三方包propTypes来对参数进行限制

1
npm install --save prop-types
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";

function App() {
  return <Greeting name="Nathan" />;
}
    
function Greeting(props){
  return (
  	<p>
    	Hello! I'm {props.name},
      a {props.name} years old {props.occupation}.
      Pleased to meet you!
    </p>
  );
}

Greeting.propTypes = {
  // name must be a string and defined
  name: PropTypes.string.isRequired,
  // age must be a number and defined
  age: PropTypes.number.isRequired,
  // occupation must be a string and defined
  occupation: PropTypes.string.isRequired,
}

Greeting.defaultProps = {
  name: "Nathan",
  age: 27,
  occupation: "Software Developer"
}

ReactDOM.render(<App />, document.getElementById("root"));

上面的代码我们通过propTypes限制了传递参数的类型以及是否必须要求提供,同时也加入了默认值。如果不定义默认值,我们不向组件传递值或者传递值类型错误,就会在console抛出告警。加入了默认值之后,不传值的话它会使用你定义的默认值。