React


2018-12-4 React

React的介绍

  • React来自于Facebook公司的开源项目
  • React 可以开发单页面应用 spa(单页面应用)
  • react 组件化模块化 开发模式
  • React通过对DOM的模拟(虚拟dom),最大限度地减少与DOM的交互 (数据绑定)
  • react灵活 React可以与已知的库或框架很好地配合。
  • react 基于jsx的语法,JSX是React的核心组成部分,它使用XML标记的方式去直接声明界面, html js混写模式

React中的核心概念

  1. 虚拟DOM(Virtual DOM)
  2. Diff算法(虚拟DOM的加速器,提升React性能的法宝)

虚拟DOM(Vitural DOM)

React将DOM抽象为虚拟DOM,虚拟DOM其实就是用一个对象来描述DOM,通过对比前后两个对象的差异,最终只把变化的部分重新渲染,提高渲染的效率
为什么用虚拟dom,当dom反生更改时需要遍历 而原生dom可遍历属性多大231个 且大部分与渲染无关 更新页面代价太大
如何实现一个 Virtual DOM 算法
理解 Virtual DOM

Diff算法

Reconciliation diff diff算法 - 中文文档 不可思议的 react diff React diff 算法

问题

(1)Q:为什么react里可以在JS里return一段“HTML”代码?

A:这是react特有的JSX语法糖,它并不是一个真正的DOM节点,react会调用ReactDom.render()方法,它接受两个参数:创建的模板,插入该模板的目标位置。这就涉及到了react的“虚拟DOM”的概念。众所周知,DOM操作是十分损耗性能的,尤其对于循环渲染来说,更是如此。因此,react引入了“虚拟DOM”。事实上,所有的DOM节点都能够用一个js对象来表示,他有三个属性:标签名、属性和子元素,这样整个页面的DOM树就可以用这样一个对象来表示,操作js对象的性能损耗远远小于对DOM的操作。react采用Diff算法对改变前后的js对象进行比对,先对js对象进行修改,最后统一渲染DOM,这样就减少了DOM的渲染次数,优化了性能。

(2)Q:感觉代码中并没有用到React,为什么需要引入,是否能够删除?

A:不能删除,因为JSX模板需要通过React来解析,删除后,JSX不能被识别。

(3)Q:官方的demo中组件继承的是Component类,为什么这里引入PureComponent?

A:react的数据在更新时,会引起所有子组件的重新渲染,但可能这个数据和某些甚至所有的子组件都无关,实际上是不需要重新渲染的,这样就造成了性能浪费。react中有shouldComponentUpdate生命周期,可以控制是否更新组件,但是每个组件中加这样一个判断十分繁琐。好在ReactFiber(react16+)为我们新增了PureComponent特性,可以在底层自动为我们判断是否更新组件,这样无疑提升了性能。

(4)Q:这里为div定义类名为什么使用className而不是class?

A:这是react为了区分声明类的关键字class,所有的css类名都应该使用className。

入门

super关键字:

Es6中的super可以用在类的继承中,super关键字,它指代父类的实例(即父类的this对象)。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

class Person {
  constructor (name) {
    this.name = name;
  }
}

class Student extends Person {
  constructor (name, age) {
    super(); // 用在构造函数中,必须在使用this之前调用
    this.age = age;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

super(props):需要传递props作为super()的参数,就是你需要在构造函数内使用this.props

组件 绑定属性

1.所有的模板要被一个根节点包含起来

2.模板元素不要加引号

3.{}绑定数据

4.绑定属性注意:

class 要变成 className
for 要变成 htmlFor
style属性和以前的写法有些不一样

<div style={{'color':'blue'}}>{this.state.title}</div>
<div style={{'color':this.state.color}}>{this.state.title}</div>
1
2

5.循环数据要加key

6.组件的构造函数中一定要注意 super

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象

constructor(props){
    super(props);  /*用于父子组件传值  固定写法*/

    this.state={
        userinfo:'张三'
    }
}
1
2
3
4
5
6
7

7.组件名称首字母大写、组件类名称首字母大写

8.注释 {/* aaaa */}

事件,方法

在以类继承的方式定义的组件中,为了能方便地调用当前组件的其他成员方法或属性(如:this.state),通常需要将事件处理函数运行时的 this 指向当前组件实例。
绑定事件处理函数this的几种方法:
第一种方法:

run(){
      alert(this.state.name)
}
<button onClick={this.run.bind(this)}>按钮</button>
1
2
3
4

第二种方法:

  // 构造函数中改变
	this.run = this.run.bind(this);

 	run(){
        	alert(this.state.name)
 	 }
 	<button onClick={this.run}>按钮</button>
1
2
3
4
5
6
7

第三种方法:

 run=()=> {
      alert(this.state.name)
 }

<button onClick={this.run}>按钮</button>
1
2
3
4
5

事件对象:在触发DOM上的某个事件时,会产生一个事件对象event。这个对象中包含着所有与事件有关的信息

run=(event)=>{
    // console.log(event);
    // alert(event.target);   /*获取执行事件的dom节点*/
    event.target.style.background='red';
    //获取dom的属性
    alert(event.target.getAttribute('aid'))
}
1
2
3
4
5
6
7

获取表单的值

{/* 获取表单的值
1、监听表单的改变事件                        onChange
2、在改变的事件里面获取表单输入的值           事件对象
3、把表单输入的值赋值给username              this.setState({})
4、点击按钮的时候获取 state里面的username     this.state.username
 */}
1
2
3
4
5
6

ref

同vue
1、给元素定义ref属性

<input ref="username" />  
1

2、通过this.refs.username 获取dom节点

键盘事件

onKeyDown onKeyUp

//键盘事件
class List extends React.Component {
    constructor(props) {
        super(props);
        this.state = { 
            username:''
         };
    }
    inputKeyUp=(e)=>{
        if(e.keyCode==13){
            alert(e.target.value);
        }
    }
    render() {
            return (
              <div>
                  <input onKeyUp={this.inputKeyUp}/>
              </div>
            );
     }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

双向数据绑定

class Todolist extends Component {
    constructor(props) {
        super(props);
        this.state = { 
            username:"111"
        };
    }
    inputChange=(e)=>{
        this.setState({
            username:e.target.value
        })
    }
    setUsername=()=>{
        this.setState({
            username:'李四'
        })
    }
    render() {
        return (
            <div>
                {/* model改变影响View    view改变反过来影响model  */}
                <input  value={this.state.username} onChange={this.inputChange}/> 
               <p> {this.state.username}</p>
               <button onClick={this.setUsername}>改变username的值</button>
            </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

约束性和非约束性组件:

非约束性组件:<input type="text" defaultValue="a" /> 这个 defaultValue 其实就是原生DOM中的 value 属性。这样写出的来的组件,其value值就是用户输入的内容,React完全不管理输入的过程。
约束性组件<input value={this.state.username} type="text" onChange={this.handleUsername} />这里,value属性不再是一个写死的值,他是 this.state.username, this.state.username 是由 this.handleChange 负责管理的。这个时候实际上 inputvalue根本不是用户输入的内容。而是onChange 事件触发之后,由于 this.setState 导致了一次重新渲染。不过React会优化这个渲染过程。看上去有点类似双向数据绑定

表单

class ReactForm extends Component {
    constructor(props) {
        super(props);
        this.state = {  
            msg:"react表单",
            name:'',  
            sex:'1',     
            city:'',      
            citys:[ 
                
                '北京','上海','深圳'            
            ],
            hobby:[   
                {  
                    'title':"睡觉",
                    'checked':true
                },
                {  
                    'title':"吃饭",
                    'checked':false
                },
                {  
                    'title':"敲代码",
                    'checked':true
                }
            ],
            info:''
        };
        this.handleInfo=this.handleInfo.bind(this);
    }
    handelSubmit=(e)=>{
            //阻止submit的提交事件
            e.preventDefault();
            console.log(this.state.name,this.state.sex,this.state.city,this.state.hobby,this.state.info);
    }
    handelName=(e)=>{
        this.setState({
            name:e.target.value
        })
    }
    handelSex=(e)=>{
        this.setState({
            sex:e.target.value
        })
    }
    handelCity=(e)=>{
        this.setState({
            city:e.target.value
        })
    }
    handelHobby=(key)=>{
        var hobby=this.state.hobby;
        hobby[key].checked=!hobby[key].checked;
        this.setState({
            hobby:hobby
        })
    }
    handleInfo(e){
        this.setState({
            info:e.target.value
        })
    }
    render() {
        return (
            <div>
                <h2>{this.state.msg}</h2>
                <form onSubmit={this.handelSubmit}>
                  用户名:  <input type="text" value={this.state.name}  onChange={this.handelName}/> <br /><br />
                  性别:    <input type="radio" value="1" checked={this.state.sex==1}  onChange={this.handelSex}/><input type="radio"  value="2" checked={this.state.sex==2}  onChange={this.handelSex}/><br /><br /> 
                 居住城市:
                        <select value={this.state.city} onChange={this.handelCity}>
                            {
                                this.state.citys.map(function(value,key){
                                    return <option key={key}>{value}</option>
                                })
                            }
                        </select>
                <br /><br />
                 爱好:   
                    {
                        // 注意this指向
                        this.state.hobby.map((value,key)=>{
                            return (
                               <span key={key}>
                                    <input type="checkbox"  checked={value.checked}  onChange={this.handelHobby.bind(this,key)}/> {value.title} 
                               </span>
                            )
                        })
                    }
                    <br /><br />
                  备注:<textarea vlaue={this.state.info}  onChange={this.handleInfo} />
                 <input type="submit"  defaultValue="提交"/>
                  <br /><br /> <br /><br />
                </form>
            </div>
        );
    }
}
export default ReactForm;
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

组件,父子组件

React中的组件: 解决html 标签构建应用的不足。

使用组件的好处:把公共的功能单独抽离成一个文件作为一个组件,哪里里使用哪里引入。

父子组件:组件的相互调用中,我们把调用者称为父组件,被调用者称为子组件
父子组件传值:
父组件给子组件传值
1.在调用子组件的时候定义一个属性 <Header msg='首页'></Header>
2.子组件里面 this.props.msg

说明:父组件不仅可以给子组件传值,还可以给子组件传方法,以及把整个父组件传给子组件。

父组件主动获取子组件的数据
1、调用子组件的时候指定ref的值 <Header ref='header'></Header>
2、通过this.refs.header 获取整个子组件实例
defaultProps:父子组件传值中,如果父组件调用子组件的时候不给子组件传值,可以在子组件中使用defaultProps定义的默认值

Header.defaultProps={
    title:'标题'
}
1
2
3

propTypes:验证父组件传值的类型合法性

import PropTypes from 'prop-types'
Header.propTypes = {
    name: PropTypes.string
};
1
2
3
4

都是定义在子组件里面

生命周期

组件加载之前,组件加载完成,以及组件更新数据,组件销毁。触发的一系列的方法 ,这就是组件的生命周期函数

组件加载的时候触发的函数:

constructorcomponentWillMountrendercomponentDidMount

组件数据更新的时候触发的生命周期函数:

shouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate

你在父组件里面改变props传值的时候触发的:

componentWillReceiveProps

组件销毁的时候触发的:

componentWillUnmount

加载的时候: componentWillMountrendercomponentDidMount(dom操作)
更新的时候: componentWillUpdaterendercomponentDidUpdate
销毁的时候: componentWillUnmount

import React, { Component } from 'react';
class Lifecycle extends Component {
    constructor(props) {
        console.log('01构造函数');
        super(props);
        this.state = { 
            msg:'我是一个msg'
         };
    }  
    //组件将要挂载的时候触发的生命周期函数
    componentWillMount(){
        console.log('02组件将要挂载');
    }
    //组件挂载完成的时候触发的生命周期函数
    componentDidMount(){
        //dom操作放在这个里面    请求数据也放在这个里面
        console.log('04组件将要挂载');
    }
    //是否要更新数据  如果返回true才会执行更新数据的操作
    shouldComponentUpdate(nextProps, nextState){
        console.log('01是否要更新数据');
        console.log(nextProps);
        console.log(nextState);
        return true;
    }
    //将要更新数据的时候触发
    componentWillUpdate(){
        console.log('02组件将要更新');
    }
    //组件更新完成
    componentDidUpdate(){
        console.log('04组件数据更新完成');
    }
    // 你在父组件里面改变props传值的时候触发的
    componentWillReceiveProps(){
        console.log('父子组件传值,父组件里面改变了props的值触发的方法')
    }
    setMsg=()=>{
        this.setState({
            msg:'我是改变后的msg的数据'
        })
    }
    //组件销毁的时候触发的生命周期函数   用在组件销毁的时候执行操作
    componentWillUnmount(){
            console.log('组件销毁了');
    }
    render() {
        console.log('03数据渲染render');
        return (
            <div>
                生命周期函数演示--- {this.state.msg}-----{this.props.title}
                <br />
                <br />
                <button onClick={this.setMsg}>更新msg的数据</button>
            </div>
        );
    }
}
export default Lifecycle;
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

antd

antd官网

  1. 安装antd npm install antd --save / yarn add antd / cnpm install antd --save
  2. 在您的react项目的css文件中引入 Antd的css @import '~antd/dist/antd.css';
  3. 看文档使用:
    如使用Button:
    1、在对应的组件中引入Antd import { Button } from 'antd';
    2、<Button type="primary">Primary</Button>
  4. React中使用Antd高级配置,按需引入css样式

我们现在已经把组件成功运行起来了,但是在实际开发过程中还有很多问题,例如上面的例子实际上加载了全部的 antd 组件的样式(对前端性能是个隐患)。

  1. 安装antd npm install antd --save
  2. 安装(react-app-rewired)一个对 create-react-app 进行自定义配置的社区解决方案 yarn add react-app-rewired / cnpm install react-app-rewired --save
  3. 修改 package.json react-scripts 需改为react-app-rewired
  "scripts": {
      "start": "react-app-rewired start",
      "build": "react-app-rewired build",
      "test": "react-app-rewired test --env=jsdom",
      "eject": "react-app-rewired eject"
 }
1
2
3
4
5
6
  1. 在项目根目录创建一个 config-overrides.js 用于修改默认配置
module.exports = function override(config, env) {
   // do stuff with the webpack config...
 return config;
};
1
2
3
4
  1. 安装babel-plugin-import babel-plugin-import是一个用于按需加载组件代码和样式的 babel插件 yarn add babel-plugin-import / cnpm install babel-plugin-import --save
  2. 修改 config-overrides.js
const { injectBabelPlugin } = require('react-app-rewired');
  module.exports = function override(config, env) {
   config = injectBabelPlugin(
         ['import', { libraryName: 'antd', libraryDirectory: 'es', style: 'css' }],
           config,
    );
   return config;
 };
1
2
3
4
5
6
7
8
  1. 然后移除前面在 src/App.css 里全量添加的 @import '~antd/dist/antd.css'; 直接引入组件使用就会有对应的css
import { Button } from 'antd';
  <Button type="primary">Primary</Button>
1
2

router路由

react-router
react路由的配置:

  1. 找到官方文档
  2. 安装 cnpm install react-router-dom --save
  3. 找到项目的根组件引入react-router-dom import { BrowserRouter as Router, Route, Link } from "react-router-dom";
  4. 复制官网文档根组件里面的内容进行修改 (加载的组件要提前引入)
<Router>
    <Link to="/">首页</Link>
    <Link to="/news">新闻</Link>
    <Link to="/product">商品</Link>
   <Route exact path="/" component={Home} />
   <Route path="/news" component={News} />    
   <Route path="/product" component={Product} />   
 </Router>
1
2
3
4
5
6
7
8

exact表示严格匹配

动态路由

  1. 动态路由配置
    <Route path="/content/:aid" component={Content} />
  2. 对应的动态路由加载的组件里面获取传值
    this.props.match.params
componentDidMount(){
    //获取动态路由的传值
    console.log(this.props.match.params.aid);
}

<Route path="/content/:aid" component={Content} />

<Link to={`/content/${value.aid}`}>{value.title}</Link>
1
2
3
4
5
6
7
8

get传值

    <Route path="/content?aid=1" component={Content} />         
    import url from 'url';
    componentDidMount(){
        //获取get传值
        console.log(url.parse(this.props.location.search,true));
        var query=url.parse(this.props.location.search,true).query;
        console.log(query)
    }
1
2
3
4
5
6
7
8

路由嵌套

<div className="left">
  <Link to="/user/">个人中心</Link>
  <br />
  <Link to="/user/info">用户信息</Link>
</div>
<div className="right">
  <Route exact path={`${this.props.match.url}/`} component={Main} />
  <Route  path="/user/info" component={Info} />
</div>
1
2
3
4
5
6
7
8
9

路由模块化

{
routes.map((route,key)=>{
    if(route.exact){
      return <Route key={key} exact path={route.path}                     
      // route.component     value.component   <User  {...props}  routes={route.routes} />
      render={props => (
        // pass the sub-routes down to keep nesting
        <route.component {...props} routes={route.routes} />
      )}
      />
    }else{
      return <Route  key={key}  path={route.path} 
      render={props => (
        // pass the sub-routes down to keep nesting
        <route.component {...props} routes={route.routes} />
      )}
      />
    }
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

BrowserRouter

一个使用了 HTML5 history API 的高阶路由组件,保证你的 UI 界面和 URL 保持同步。此组件拥有以下属性:
basename: string
作用:为所有位置添加一个基准URL
使用场景:假如你需要把页面部署到服务器的二级目录,你可以使用 basename 设置到此目录。

<BrowserRouter basename="/minooo" />
<Link to="/react" /> // 最终渲染为 <a href="/minooo/react">
1
2

getUserConfirmation: func
作用:导航到此页面前执行的函数,默认使用 window.confirm
使用场景:当需要用户进入页面前执行什么操作时可用,不过一般用到的不多。

const getConfirmation = (message, callback) => {
  const allowTransition = window.confirm(message)
  callback(allowTransition)
}
<BrowserRouter getUserConfirmation={getConfirmation('Are you sure?', yourCallBack)} />
1
2
3
4
5

forceRefresh: bool
作用:当浏览器不支持 HTML5 的 history API 时强制刷新页面。
使用场景:同上。

const supportsHistory = 'pushState' in window.history
<BrowserRouter forceRefresh={!supportsHistory} />
1
2

keyLength: number
作用:设置它里面路由的 location.key 的长度。默认是6。(key的作用:点击同一个链接时,每次该路由下的 location.key都会改变,可以通过 key 的变化来刷新页面。)
使用场景:按需设置。

<BrowserRouter keyLength={12} />
1

children: node
作用:渲染唯一子元素。
使用场景:作为一个 Reac t组件,天生自带 children 属性。

Route

它最基本的职责就是当页面的访问地址与 Route 上的 path 匹配时,就渲染出对应的 UI 界面。
<Route> 自带三个 render method 和三个 props
render methods 分别是:

  • <Route component>
  • <Route render>
  • <Route children>

每种 render method 都有不同的应用场景,同一个<Route> 应该只使用一种 render method ,大部分情况下你将使用 component 。

props 分别是:

  • match
  • location
  • history

所有的 render method 无一例外都将被传入这些 props。

component 只有当访问地址和路由匹配时,一个 React component 才会被渲染,此时此组件接受 route props (match, location, history)。
当使用 component 时,router 将使用 React.createElement 根据给定的 component 创建一个新的 React 元素。这意味着如果你使用内联函数(inline function)传值给 component 将会产生不必要的重复装载。对于内联渲染(inline rendering), 建议使用 render prop。

<Route path="/user/:username" component={User} />
const User = ({ match }) => {
  return <h1>Hello {match.params.username}!</h1>
}
1
2
3
4

render: func
此方法适用于内联渲染,而且不会产生上文说的重复装载问题。

// 内联渲染
<Route path="/home" render={() => <h1>Home</h1} />
// 包装 组合
const FadingRoute = ({ component: Component, ...rest }) => (
  <Route {...rest} render={props => (
    <FadeIn>
      <Component {...props} />
    </FaseIn>
  )} />
)
<FadingRoute path="/cool" component={Something} />
1
2
3
4
5
6
7
8
9
10
11

children: func
有时候你可能只想知道访问地址是否被匹配,然后改变下别的东西,而不仅仅是对应的页面。

<ul>
  <ListItemLink to="/somewhere" />
  <ListItemLink to="/somewhere-ele" />
</ul>

const ListItemLink = ({ to, ...rest }) => (
  <Route path={to} children={({ match }) => (
    <li className={match ? 'active' : ''}>
      <Link to={to} {...rest} />
    </li>
  )}
)
1
2
3
4
5
6
7
8
9
10
11
12

path: string
任何可以被 path-to-regexp解析的有效 URL 路径

<Route path="/users/:id" component={User} />
1

如果不给path,那么路由将总是匹配。

exact: bool
如果为 true,path 为 '/one' 的路由将不能匹配 '/one/two',反之,亦然。 strict: bool
对路径末尾斜杠的匹配。如果为 true。path 为 '/one/' 将不能匹配 '/one' 但可以匹配 '/one/two'。

如果要确保路由没有末尾斜杠,那么 strict 和exact 都必须同时为 true

为你的应用提供声明式,无障碍导航。
to: string
作用:跳转到指定路径
使用场景:如果只是单纯的跳转就直接用字符串形式的路径。

<Link to="/courses" />
1

to: object
作用:携带参数跳转到指定路径
作用场景:比如你点击的这个链接将要跳转的页面需要展示此链接对应的内容,又比如这是个支付跳转,需要把商品的价格等信息传递过去。

<Link to={{
  pathname: '/course',
  search: '?sort=name',
  state: { price: 18 }
}} />
1
2
3
4
5

replace: bool
为 true 时,点击链接后将使用新地址替换掉上一次访问的地址,什么意思呢,比如:你依次访问 '/one' '/two' '/three' ’/four' 这四个地址,如果回退,将依次回退至 '/three' '/two' '/one' ,这符合我们的预期,假如我们把链接 '/three' 中的 replace 设为 true 时。依次点击 one two three four 然后再回退会发生什么呢?会依次退至 '/three' '/one'!

这是<Link> 的特殊版,顾名思义这就是为页面导航准备的。因为导航需要有 “激活状态”。
activeClassName: string
导航选中激活时候应用的样式名,默认样式名为 active

<NavLink
  to="/about"
  activeClassName="selected"
>MyBlog</NavLink>
1
2
3
4

activeStyle: object
如果不想使用样式名就直接写style

<NavLink
  to="/about"
  activeStyle={{ color: 'green', fontWeight: 'bold' }}
>MyBlog</NavLink>
1
2
3
4

xact: bool
若为 true,只有当访问地址严格匹配时激活样式才会应用 strict: bool
若为 true,只有当访问地址后缀斜杠严格匹配(有或无)时激活样式才会应用 isActive: func
决定导航是否激活,或者在导航激活时候做点别的事情。不管怎样,它不能决定对应页面是否可以渲染。

Switch

只渲染出第一个与当前访问地址匹配的 <Route><Redirect>
思考如下代码,如果你访问 /about,那么组件 About User Nomatch 都将被渲染出来,因为他们对应的路由与访问的地址 /about 匹配。这显然不是我们想要的,我们只想渲染出第一个匹配的路由就可以了,于是<Switch>应运而生!

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

也许你会问,为什么 RR4 机制里不默认匹配第一个符合要求的呢,答:这种设计允许我们将多个<Route>组合到应用程序中,例如侧边栏(sidebars),面包屑 等等。 另外,<Switch> 对于转场动画也非常适用,因为被渲染的路由和前一个被渲染的路由处于同一个节点位置!

<Fade>
  <Switch>
    {/* 用了Switch 这里每次只匹配一个路由,所有只有一个节点。 */}
    <Route/>
    <Route/>
  </Switch>
</Fade>

<Fade>
  <Route/>
  <Route/>
  {/* 不用 Switch 这里可能就会匹配多个路由了,即便匹配不到,也会返回一个null,使动画计算增加了一些麻烦。 */}
</Fade>
1
2
3
4
5
6
7
8
9
10
11
12
13

children: node
<Switch>下的子节点只能是 <Route><Redirect> 元素。只有与当前访问地址匹配的第一个子节点才会被渲染。<Route> 元素用它们的 path 属性匹配,<Redirect> 元素使用它们的 from 属性匹配。如果没有对应的 path 或 from,那么它们将匹配任何当前访问地址。

Redirect

<Redirect>渲染时将导航到一个新地址,这个新地址覆盖在访问历史信息里面的本该访问的那个地址。
to: string
重定向的 URL 字符串
to: object
重定向的 location 对象
push: bool
若为真,重定向操作将会把新地址加入到访问历史记录里面,并且无法回退到前面的页面。
from: string
需要匹配的将要被重定向路径。

Prompt

当用户离开当前页面前做出一些提示。
message: string
当用户离开当前页面时,设置的提示信息。

<Prompt message="确定要离开?" />

1
2

message: func
当用户离开当前页面时,设置的回掉函数

<Prompt message={location => (
  `Are you sue you want to go to ${location.pathname}?` 
)} />
1
2
3

when: bool
通过设置一定条件要决定是否启用 Prompt

history

histoty 是 RR4 的两大重要依赖之一(另一个当然是 React 了),在不同的 javascript 环境中, history 以多种能够行驶实现了对会话(session)历史的管理。
我们会经常使用以下术语:

  • "browser history" - history 在 DOM 上的实现,用于支持 HTML5 history API 的浏览器
  • "hash history" - history 在 DOM 上的实现,用于旧版浏览器。
  • "memory history" - history 在内存上的实现,用于测试或非 DOM 环境(例如 React Native)。

history 对象通常具有以下属性和方法:

  • length: number 浏览历史堆栈中的条目数
  • action: string 路由跳转到当前页面执行的动作,分为 PUSH, REPLACE, POP
  • location: object 当前访问地址信息组成的对象,具有如下属性:
  • pathname: string URL路径
  • search: string URL中的查询字符串
  • hash: string URL的 hash 片段
  • state: string 例如执行 push(path, state) 操作时,location 的 state 将被提供到堆栈信息里,state 只有在 browser 和 memory history 有效。
  • push(path, [state]) 在历史堆栈信息里加入一个新条目。
  • replace(path, [state]) 在历史堆栈信息里替换掉当前的条目
  • go(n) 将 history 堆栈中的指针向前移动 n。
  • goBack() 等同于 go(-1)
  • goForward 等同于 go(1)
  • block(prompt) 阻止跳转

history 对象是可变的,因为建议从 <Route> 的 prop 里来获取 location,而不是从 history.location 直接获取。这样可以保证 React 在生命周期中的钩子函数正常执行,例如以下代码:

class Comp extends React.Component {
  componentWillReceiveProps(nextProps) {
    // locationChanged
    const locationChanged = nextProps.location !== this.props.location

    // 错误方式,locationChanged 永远为 false,因为history 是可变的
    const locationChanged = nextProps.history.location !== this.props.history.location
  }
}
1
2
3
4
5
6
7
8
9

location

location 是指你当前的位置,将要去的位置,或是之前所在的位置

{
  key: 'sdfad1'
  pathname: '/about',
  search: '?name=minooo'
  hash: '#sdfas',
  state: {
    price: 123
  }
}
1
2
3
4
5
6
7
8
9

在以下情境中可以获取 location 对象

  • 在 Route component 中,以 this.props.location 获取
  • 在 Route render 中,以 ({location}) => () 方式获取
  • 在 Route children 中,以 ({location}) => () 方式获取
  • 在 withRouter 中,以 this.props.location 的方式获取

location 对象不会发生改变,因此可以在生命周期的回调函数中使用 location 对象来查看当前页面的访问地址是否发生改变。这种技巧在获取远程数据以及使用动画时非常有用

componentWillReceiveProps(nextProps) {
  if (nextProps.location !== this.props.location) {
    // 已经跳转了!
  }
}
1
2
3
4
5

可以在不同情境中使用 location:

  • <Link to={location} />
  • <NaviveLink to={location} />
  • <Redirect to={location />
  • history.push(location)
  • history.replace(location)

match

match 对象包含了 <Route path> 如何与 URL 匹配的信息,具有以下属性:

  • params: object 路径参数,通过解析 URL 中的动态部分获得键值对
  • isExact: bool 为 true 时,整个 URL 都需要匹配
  • path: string 用来匹配的路径模式,用于创建嵌套的 <Route>
  • url: string URL 匹配的部分,用于嵌套的<Link>

在以下情境中可以获取 match 对象

  • 在 Route component 中,以 this.props.match获取
  • 在 Route render 中,以 ({match}) => () 方式获取
  • 在 Route children 中,以 ({match}) => () 方式获取
  • 在 withRouter 中,以 this.props.match的方式获取
  • matchPath 的返回值

当一个 Route 没有 path 时,它会匹配一切路径。

脚手架

npx create-react-app reactdemo
1

文件结构

/  --  根目录
  --  public/  --  公共资源目录
  ----  api/  --  mock数据目录
  --  src/  --  项目目录
  ----  common/  --  通用组件目录
  ----  pages/  --  页面组件目录
  ------  home/  --  首页目录
  --------  store/  --  组件的redux仓库
  ----------  actionCreators.js  --  创建redux的action
  ----------  constains.js  --  定义action常量
  ----------  index.js  --  组件redux对外的统一接口
  ----------  reducer.js  --  状态处理
  --------  index.js  --  组件
  --------  style.js  --  组件样式
  ------  statics/  --  静态资源目录
  ------  store/  --  项目redux仓库
  --------  index.js  --  创建store
  --------  reducer.js  --  组合各组件store
  ------  App.js  --  项目入口
  ------  index.js  --  挂载App
  ------  style.js  --  全局样式
  ----  package.json  --  打包依赖
  ----  README.md
  ----  yarn-error.log
  ----  yarn.lock
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

react-app-rewired

react-app-rewired 是 react 社区开源的一个修改 CRA 配置的工具。
在 CRA 创建的项目中安装了react-app-rewired( npm install react-app-rewired --save-dev )后,可以通过创建一个config-overrides.js 文件来对 webpack 配置进行扩展。因此react-app-rewired不兼容CRA2了,但可以使用customize-cra来兼容CRA2。作者还推荐使用next.js或Razzle脚手架,它们都支持开箱即用的自定义Webpack,有空时后面在介绍这两种使用。

npm install react-app-rewired --save-dev
npm install customize-cra --save-dev
npm install --save-dev @babel/plugin-proposal-decorators
1
2
3

我们在根目录新建一个config-overrides.js 文件

const {
  override,
  addLessLoader,
  addDecoratorsLegacy,
  disableEsLint,
  useBabelRc,
  addWebpackAlias
} = require('customize-cra')
const path = require('path')

module.exports = override(
  //添加修饰器 根目录下创建.babelrc
  useBabelRc(),
  //禁用默认eslint,使用自定义eslint,根目录下创建.eslintrc.js
  disableEsLint(),
  //在传统模式下添加装饰器。一定要@babel/plugin-proposal-decorators安装
  addDecoratorsLegacy(),
  //添加less-loader配置
  addLessLoader(),
  //配置简化路径
  addWebpackAlias(
    {
      '@style': path.resolve(__dirname, 'src/style'),
      '@api': path.resolve(__dirname, 'src/api'),
      '@resource': path.resolve(__dirname, 'src/resource'),
      '@components':path.resolve(__dirname, 'src/components'),
      '@config': path.resolve(__dirname, 'src/config'),
      '@pages': path.resolve(__dirname, 'src/pages'),
      '@utils': path.resolve(__dirname, 'src/utils')
    }
  )
)
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

package.json

 "scripts": {
    "start": "set PORT=5555 && react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },
1
2
3
4
5
6

安装antd

npm install antd -save-dev
1

按需引用antd模块,使用 babel-plugin-import(推荐)

npm install babel-plugin-import --save-dev
1

修改配置文件 .babelrc

{
  "plugins": [
    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }] // `style: true` 会加载 less 文件
  ]
}
1
2
3
4
5

安装react-router

npm install --save react-router
1

安装react-router-dom

npm install --save react-router-dom
1

安装axios

npm install --save axios
1

npm run build 遇到的问题

打包后打开index.html页面空白报错,原因是访问路径不对,我需要设置在根目录下,在package.json下添加"homepage": "."。
包体积过大,里面包含了.map文件,启动了sourcemap,因此每次打包都会产生.map文件。我们可以在命令行中去掉他,“scripts”添加GENERATE_SOURCEMAP=false

"scripts": {
    "start": "cross-env REACT_APP_ENV=development react-app-rewired start",
    "build": "cross-env GENERATE_SOURCEMAP=false REACT_APP_ENV=production react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject",
    "test1": "cross-env GENERATE_SOURCEMAP=false REACT_APP_ENV=test1 react-app-rewired build",
    "test2": "cross-env REACT_APP_ENV=test2 react-app-rewired build"
  },
1
2
3
4
5
6
7
8

PWA(渐进式web应用)

Progressive Web App简称 PWA,是提升 Web App 的体验的一种新方法,能给用户原生应用的体验。
我们可以看到在src文件夹下有个registerServiceWorker.js,这文件其实就是注册 service worker,它是在后台运行的一个线程,可以用来处理离线缓存、消息推送、后台自动更新等任务。这样你下次访问时,就可以更快的获取资源。而且因为资源离线缓存,因此在离线或者网络较差的情况下也正常访问。注意,registerServiceWorker注册的service worker 只在生产环境中生效(process.env.NODE_ENV === 'production'),还有必须是hppts服务 。如果我们想要使用cra2提供的pwa特性的话,在build前将src/index.js下的serviceWorker.unregister() 改成serviceWorker.register()注册service worker。
在public文件夹中有个manifest.json文件。PWA 添加至桌面的功能实现依赖于 manifest.json,manifest.json用于指定应用的显示名称、图标、应用入口文件地址及需要使用的设备权限等信息。也就是说用户可以直接将站点添加快捷方式到桌面,免去去应用商店下载的麻烦。
在没网和网弱的状态下也能读取到资源还能瞬间秒开,还能实现消息通知,添加应用横屏等等,还能将站点添加成快捷键放到桌面。使用PWA是不是感觉更像是原生app了。

react-loadable 按需加载

loadable组件

import React from 'react';
import Loadable from 'react-loadable';
import '../style/load.styl'
// 按需加载组件
export default function withLoadable (comp) {
  return Loadable({
    loader:comp,
    loading:(props)=>{
      let msg
      if (props.error) {
        return <p>加载错误,请刷新</p>
      } else if (props.timedOut) {
        return <p>加载超时</p>
      } else if (props.pastDelay) {
        return(
          <div className='loading-mask'>
            <div className='container'>
              <img className='loading-img' src={require('../assets/img/loading.gif')} />
            </div>
          </div>
        )
      } else {
        return null;
      }
    },
    timeout:10000
  })
}
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

类似vue的路由

import Loadable from './loadable';

const routes = [
  {
    path: '/',
    component: Loadable(()=>import('../components/app')),
    exact:true,
  },
  {
    path: '/app',
    component: Loadable(()=>import('../components/app')),
    children: [
      {
        path: '/app/docs',
        component: Loadable(()=>import('../components/docs')),
        children: [
          {
            path: '/app/docs/docs1',
            component: Loadable(()=>import('../components/three')),
          }
        ]
      }
    ]
  },
  {
    path: '/test',
    component: Loadable(()=>import('../components/test')),
  }
]

export default routes

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

主要路由配置

import React from 'react';
import {BrowserRouter, Route, Switch} from 'react-router-dom';
import {Provider} from 'react-redux';
import {hot} from 'react-hot-loader';
import Store from '../redux';
import DevTools from '../redux/DevTools';

import routes from './config'

const Router = ({component: Component, children, ...rest}) => (
    <Route
        {...rest}
        render={props => (
            <Component {...props} ><Switch>{children}</Switch></Component>
        )}
    />
)

function creatRouter(r) {
  return r.map((route, key) => {
    if (route.children) {
      return (
        <Router exact={route.exact} key={key} path={route.path} component={route.component}>{creatRouter(route.children)}</Router>
      )
    } else {
      return (<Router exact={route.exact} key={key} path={route.path} component={route.component}/>)
    }
  })
}

const Root = () => (
    <BrowserRouter>
        <Provider store={Store}>
            <div className="router-content">
                {/*{__DEVELOPMENT__ && <DevTools />}*/}
                <DevTools />
                <Switch>
                  {creatRouter(routes)}
                </Switch>
            </div>
        </Provider>
    </BrowserRouter>
);

export default hot(module)(Root);

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
Last Updated: 2019-10-25 11:34:43 AM