项目解析jsx文件_万字编程宝典耐心看,前端开发项目的JavaScript库中React详解

090a3e9ae428b7b6d3dc377a44c32eeb.png

学习是个吃苦的过程,而学编程那就是个更吃苦的事情,一个技术点需要上万的字来说明,一个实现页面需要上万的代码数据来凑,既然走上了这条路,愿大家无怨无悔,今天为大家带来了万字编程宝典中关于前端开发项目中javascript库中recact详解,你看或者不看,它就在这里。

一、React是什么?

React是一个声明式,高效且灵活的用于构建用户界面的JavaScript库。使用React可以将一些简短、独立的代码片段组合成复杂的UI界面,这些代码片段被称为”组件”。

二、React特点

  • 1.声明式设计 -React采用声明范式,可以轻松描述应用。
  • 2.高效 -React通过对DOM的模拟,最大限度地减少与DOM的交互。
  • 3.灵活 -React可以与已知的库或框架很好地配合。
  • 4.JSX -JSX是JavaScript语法的扩展。React开发不一定使用JSX,但我们建议使用它。
  • 5.组件 -通过React构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。
  • 6.单向响应式的数据流 - React实现了单向相应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。

三、引入React.js库

React.js框架本身包含两个部分: - 1.react.js:提供了React.js核心功能代码,如:虚拟dom、组件 - 2.React-dom.js:提供了与浏览器交互的DOM功能,如:dom渲染

<!DOCTYPE html>
    <html lang="m en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>

        <div id="app"></div>
        
        <script src="./js/react.production.min.js"></script>
        <script src="./js/react-dom.production.min.js"></script>
        <script type="text/jsx" src="js/app.js"></script>
    </body>
    </html>
    // 需注意:最后一个script标签的type属性为text / jsx。这是因为React独有的JSX语法,跟JavaScript不兼容。凡是使用JSX的地方,都要加上type=’text/jsx’。

js/app.js

ReactDOM.render(
    '<h1>Hello,world!</h1>',
    document.getElementById('app')
);

上面代码将一个h1标签插入app节点,运行结果如下:

  • Hello,world!

4e7de63a73450a90165ca30cd9b15eb5.png

React.render()

ReactDOM.render(element,container[,callback])

element: 要渲染的内容 container: 要渲染的内容存放容器 callback: 渲染后的回调函数

四、JSX

1.JSX是什么?

  • JSX是一个基于JavaScript + XML 的一个- 扩展语法
  • 它可以作为 值 使用
  • 它并不是 字符串
  • 它也不是 HTML
  • 它可以配合 JavaScript 表达式 一起使用

2.JSX优点:

  • JSX执行更快,因为它在编译为JavaScript代码后进行了优化
  • 它是类型安全的,在编译过程中就能发现错误
  • 使用JSX编写模块更加简单快速

3.JSX语法:

  • 1> 如果输出多行结构,可以使用一对小括号来包含整个结构
const App = (
    <div>
        <h1>开课吧</h1>
        <p>web前端高级工程师</p>
    </div>
)
  • 2> 注意:JSX只能有一个顶级父元素
const App = (
    <div>
        <h1>开课吧</h1>
        <p>web前端高级工程师</p>
    </div>
    <div>第二个</div>
)
//以上代码会出错
  • 3> 在JSX中可以嵌套表达式:
et name = '开课吧';
let title = 'web前端高级工程师';
const App = (
    <div>
        <h1>{name}</h1>
        <p>{title}</p>
    </div>
);
//注意分清楚 表达式 与 语句 的区别
// if、for、while这些都是语句,JSX不支持语句

<h1>{if(true){...}}</h1>//错误
  • 4> JSX中的注释
<div>
    {/*注释*/}
    {/*
            多行注释
    */}
</div>
  • 5> JSX更偏向于JavaScript,所以对于一些特殊的属性,使用的是JavaScript中的属性名风格
// 错误
const App = (
    <div class="box1"></div>
);
// 正确
const App = (
    <div className="box1"></div>
);
  • 6> 为了更加方便的操作元素的style,针对style这个属性有特殊的处理
const App = (
    <div style={{width: '100px', height: '100px', color:'red'}}></div>
);

// 这里的两个 <u>{{}}</u> ,外部的大括号表示的是前面说的表达式语法中的大括号,里面的大括号是表示对象的大括号

let skin = {width: '100px', height: '100px', color:'red'};
const App = (
    <div style={skin}></div>
);
  • 7> JSX中的表达式也可以使用在属性上,但是使用的时候需要注意:不要使用引号包含
let id = 'kaikeba';

// 错误
const App = (
    <div id="{id}"></div>
);

// 正确
const App = (
    <div id={id}></div>
);

4.JSX输出类型:

字符串、数字:原样输出 数组:转成字符串,数组.join(‘’) 使用 空字符串 而不是默认的 逗号 连接 其他对象:不能直接输出 布尔值、空、未定义:会被忽略

5.JSX列表渲染:

  • 如果需要渲染一组数据,我们可以通过遍历(数组遍历、对象遍历...)等操作,返回一组jsx

数据:

let zMouse = {
  name: '张三',
  gender: '男',
  skills: ['JavaScript', 'Node.js'],
  interests: ['音乐', '足球', '编程']
};

数组:

function getSkills(){
    return (
        <ul>
            {zMouse.skills.map(skill => <li></li>)}
        </ul>
    )
}
const App = (
    <div>{getSkills()}</div>
);
// 或者
const App = (
    <div>
        <ul>
            {zMouse.skills.map(skill => <li>{skill}</li>)}
        </ul>
    </div>
)

对象:

function getKeys(){
    let arr = [];
    for(let k in zMouse){
        arr.push(<li>{k}</li>)
    }
    return arr;
}

const App = (
    <div>
        <ul>{getKeys()}</ul>
    </div>
)

//或者
const App = (
    <div>
        <ul>
            {Object.keys(zMouse).map(key => <li>{key}</li>)}
        </ul>
    </div>
)

Key - 默认情况下,React从性能上考虑,会尽可能的复用结构,针对 同组可变列表 结构,为了避免出现某些方面的问题,通常会给每一个列表添加一个 唯一的key

<ul>
  {[{id:1,name:'zs',id:2,name:'ls'}].map(user => <li key={user.id}>{user.name}</li>)}
</ul>

 // 注意:key 的值不推荐使用数组的下标

条件渲染

function moreInterests() {
    if (zMouse.interests.length > 2) {
        return <a href="#">更多</a>
    }
}
const App = (
        <div>
            爱好:{zMouse.interests.map(interest=>{
            return <span style={{marginRight:"10px"}}>{interest}</span>
        })}
        {moreInterests()}
    </div>
);

三目运算符

const App = (
        <div>
            爱好:{zMouse.interests.map(interest=>{
            return <span style={{marginRight:"10px"}}>{interest}</span>
        })}
        {zMouse.interests.length > 2 ? <a href="#">更多</a> : null}
    </div>
);

与或运算符

const App = (
    <div>
        爱好:{zMouse.interests.map(interest=>{
                    <span style={{marginRight:"10px"}}>{interest}</span>
              })}
          {zMouse.interests.length > 2 && <a href="#">更多</a>}
          {zMouse.interests.length < 4 || <a href="#">更多</a>}
    </div>
);

五、React脚手架

  • create-react-app是用于搭建react项目的脚手架。它的优势在于省略了很多涉及配置的地方,能够更加容易上手。

1.安装与使用:

通过 npm、yarn、npx都可以

1>安装 npm

npm i -g create-react-app

yarn

 yarn global add create-react-app

2>使用 安装完成以后,即可使用create-react-app命令

  • create-react-app 项目名称 或者通过npx的方式
  • npx cerate-react-app 项目名称

2.项目目录结构说明:

运行命令以后,就会在运行命令所在的目录下面创建一个以项目名称为名的目录:

my-app/
  README.md        //主要是项目的说明文件,在里面写一些关于本项目的说明
  node_modules/    //里面放的是项目依赖的第三方的包,这些包不是我们自己写的,是脚手架工具要实现自己的功能,需要依赖外部的一些第三方的包文件来实现
  package.json     //脚手架自动生成的一个文件,是一个node的文件,里面有一些项目的脚手架的版本,以及一些npm的命令
  public/        
    index.html     //项目首页的html模板
    favicon.ico    //图标
  src/             //src里面放的是项目所有的源代码
    App.css
    App.js
    App.test.js    //自动化测试的文件
    index.css
    index.js       //项目的入口文件
    logo.svg

3.命令脚本:

create-react-app同时也提供了其它一些命令来帮助我们进行开发 1> npm start 启动一个内置的本地WebServer,根目录映射到 ’./public’目录,默认端口:3000 2> npm test 运行 Jest 测试 3> npm run build 打包应用(准备上线)

六、组件之状态与通信

在React.js中,定义一个组件的最简单的方式就是 函数

1> 函数式组件:

function Kaikeba() {
        return (
            <div>
                <h2>开课吧!</h2>
            </div>
        );
    }
    ReactDOM.render(
        <Kaikeba />,
        document.getElementById('app')
    );

// 函数的名称就是组件的名称,必须是首字母大写
// 函数的返回值就是组件要渲染的内容

2> 类组件:

 class Miaov extends React.Component {
        render() {
            return (
                <div>
                    <h2>妙味!</h2>
                </div>
            );
        }
    }

// 组件类必须继承 React.Component
// 组件类必须有 render方法
// render 方法的返回值就是组件要渲染的内容
// 类的名称(组件的名称),也必须是首字母大写

3> 组件复用 - 数据传入

使用组件的时候通过 标签属性 -property 的方式传入数据,在组件内部通过构造函数参数(如果是函数组件,则通过函数参数)来接收传入的数据

<组件名称 属性名称="值" />
    // 使用表达式
<组件名称 属性名称={表达式} />

   ReactDOM.render(
        <FriendList datas={datas} />,
        document.getElementById('app')
   );

4> 接收参数 -props

  • 函数式组件:通过函数的第一个参数来接收;
  • 类式组件:通过类的 props 属性接收;

无论是函数式组件还是类式组件,都会把传入的参数封装成一个对象

<组件名称 属性名称1="值1" 属性名称二={表达式二} />

    // 函数式组件:
    // 函数创建的组件是无状态组件,没有state,没有生命周期方法,它是一种只负责展示的纯组件
    function 组件名称(参数) {
        // 参数的结构为:
        参数 = {
            属性名称1: "值1",
            属性名称二: 表达式二的值
        }
    
        return <div>组件结构</div>
    }
 // 类式组件
    class 组件名称 extends React.Component {
        constructor(参数) {
            super(参数);

            this.props = {
                属性名称1: "值1",
                属性名称2: 表达式二的值
            }
        }
    
        render() {
            return <div>组件结构</div>
        }
    }

// 在类式组件中,需要注意:
// 1.当子类重写 constructor,则必须调用父类 super
// 2.把接收到的参数传入父类构造函数,父类构造函数中会创建一个对象属性:props 类存储传入的参数数据

5> 通过参数动态渲染组件结构

class FriendList extends React.Component {
        constructor(props) {
            super(props);
        }
    
        render() {
            let {datas} = this.props;
            return (
            <div className="friend-list">
                {Object.keys(datas).map((key, index) => (
                    <div className="friend-group" key={key}>
                        <dt>{datas[key].title}</dt>
                        {datas[key].list.map((list, index) => (
                            <dd key={index}>{list}</dd>
                        )}
                    </div>
                ))}
            </div>
            );
        }
    }

6> 子组件提取

组件与类一样,是一个不断提取的过程,当我们发现某个部分可复用或者结构复杂的时候,我们可以对它再次进行提取

class FriendGroup extends React.Component {
            constructor(props) {
                super(props);
            }
    
            render() {
                let {data} = this.props;
                return (
                    <div className="friend-group">
                        <dt>{data.title}</dt>
                        {data.list.map((list, index) => {
                            <dd key={index}>{list}</dd>
                        })}
                    </div>
                );
            }
    }

    class FriendList extends React.Component {
            constructor(props) {
                super(props);
            }
    
            render() {
                let {datas} = this.props;
                return (
                    <div className="friend-list">
                        {Object.keys(datas).map((key, index) => (
                                <FriendGroup data={datas[key]} key={key} />
                        ))}
                    </div>
                );
            }
    }

7> 组件状态

  • 组件状态是什么? 状态被用来存储组件在某段时间内状态改变的信息。用户事件或系统事件会导致一些经典的状态改变。(比如:对用户输入的回应、服务器的请求、生命周期函数)
  • 组件 state 工作是这样的:先给组件设置一个默认状态,再获取当前状态,最后更新这个状态
  • 修改 state 的正确姿势: 不能直接修改state 在React中,直接修改state并不会触发render函数,所以下面的写法是错误的。
this.state.title = 'React';   // 错误
    
// 组件的State只能通过setState()方式进行修改。例如:

this.setState({title: 'React'});     // 正确
  • setState方法由父类React.Component提供,当该方法被调用时,组件的state会被更新,同时会被重新调用组件的render方法对组件进行渲染。
  • 组件构造函数是唯一可以对 state 直接赋值(初始化)的位置

8> 绑定事件

class FriendGroup extends React.Component {
        constructor(props) {
            super(props);
                this.state = {
                    expanded: false
                };
                this.expand = this.click.expand();
        }
    
        expand() {
            this.setState({
                expanded: !this.state.expanded
            });
        }
    
        render() {
            let {data} = this.props;
            let {expanded} = this.state;
            return (
                <div className={["friend-group",expanded && "expanded"].join(' ')}>
                    <dt onClick={this.expand}>{data.title}</dt>
                    {data.list.map((list, index) => {
                        <dd key={index}>{list}</dd>
                    })}
                </div>
            );
        }
    }

React.js的事件绑定需要注意:

  • 事件名称是驼峰命名的
  • 事件绑定函数的this指向
  • 通过bind改变this指向,为了能够在方法中调用对象的属性和其他方法, 我们需要把this指向组件
  • 通过箭头函数处理
<dt onClick={e=>{this.expand}}>{data.title}</dt>

如上代码: 事件绑定函数的第一个参数是事件对象 因为this指向了组件对象,那么获取当前事件触发元素,可以通过e.target来获取

9> 获取原生DOM对象 有的时候我们也是需要操作原生DOM对象的,除了可以通过事件源来获取,还可以通过ref的方式来获取

class MyComponent extends React.Component {
        render() {
            return (
                <div>
                    <input ref={el=>this.el=el} />
                    <button onClick={this.todo.bind(this)}>Button</button>
                </div>
            );
        }
        todo() {
            console.log(this.el.value);
        }
    }

10> 更新异步 出于性能考虑,setState方法的修改并不是立即生效的

 // this.state.val = 0
    this.setState({
        val: this.state.val + 1
    });
    // this.state.val 的值并不是 1
    console.log(this.state.val);

11> 更新合并 React会把多个setState合并成一个调用

// this.state.val = 0
    this.setState({
        val: this.state.val + 1
    });
    this.setState({ // 因为异步的问题,this.state.val 的值在这里还是0
        val: this.state.val + 1
    });

12> props与state的区别 - state的主要作用是用于 组件保存、控制、修改自己的可变状态,在组件内部进行初始化,也可以在组件内部进行修改,但是组件外部不能修改组件的state - props的主要作用是让使用该组件的父组件可以传入参数来配置该组件,它是外部传进来的配置参数,组件内部无法控制也无法修改 - state和props都可以决定组件的外观和显示状态。通常,props作为不变数据或者初始化数据传递给组件,可变状态使用state - 能使用props就不要使用state

13> 无状态组件 - 没有状态的组件,我们成为 无状态组件。 - 因为状态会带来复杂性,所以,通常我们推荐使用无状态组件。 - 函数式组件没有state,所以通常我们编写使用函数式组件来编写无状态组件

14> 数据流 - 在React.js中,数据是从 上自下流动(传递) 的,也就是一个父组件可以把它的state/props通过props传递给它的 子组件 ,但是 子组件不能修改props。 - React.js是单向数据流,如果子组件需要修改父组件状态(数据),是通过 回调函数 方式来完成的。

15> React组件通信方式汇总:

需要组件之前进行通信的几种情况: - 父组件向子组件通信 - 子组件向父组件通信 - 跨级组件通信 - 没有嵌套关系组件之间的通信

1.父组件向子组件通信

父组件通过向子组件传递props,子组件得到props后进行相应的处理

5dd9476237b4d8f18ec4afba063c59af.png

2.子组件向父组件通信

649cce2ddf4675b313d98a9dc24eb7fe.png

3.跨级组件通信

使用context context是一个全局变量,像是一个大容器,在任何地方都可以访问到,我们可以把要通信的信息放在context上,然后在其他组件中可以随意取到

使用context也很简单,需满足: - 1、上级组件要声明自己支持context,提供context中属性的PropTypes,并提供一个函数来返回相应的context对象 - 2、子组件要声明自己主要使用context,并提供其需要使用的context属性的PropTypes - 3、父组件需提供一个getChildContext函数,以返回一个厨师的context对象 如果组件中使用构造函数(constructor),还需要在构造函数中传入第二个参数context,并在super调用父类构造函数时传入context,否则会造成组建中无法使用context。

下面例子中的组件关系:ListItem是List的子组件,List是app的子组件

7ac446475eb1a61fce686311406b901b.png

4.没有嵌套关系的组件通信(兄弟组件)

使用自定义事件机制

实现这样一个功能: 点击List2中的一个按钮,改变List1中的信息显示(List1和List2没有任何嵌套关系,App是他们的父组件)

  • 首先需要项目中安装events包:
npm install events --save
  • 在src下新建一个util目录里面见一个events.js
import { EvenetEmitter } from 'events';
  export default new EventEmitter();
  • list1.js
import React, { Component } from 'react';
    import emitter from '../util/events';

    class List extends Component {
        constructor(props) {
            super(props);
            this.state = {
                message: 'List1',
            };
        }
        componentDidMount() {
            // 组件装载完成以后声明一个自定义事件
            this.eventEmitter = emitter.addListener('changeMessage', (message) => {
                this.setState({
                    message,
                });
            });
        }
        componentWillUnmount() {
            emitter.removeListener(this.eventEmitter);
        }
        render() {
            return (
                <div>
                    {this.state.message}
                </div>
            );
        }
    }
export default List;
  • List2.js
import React, { Component } from 'react';
    import emitter from '../util/events';

    class List2 extends Component {
        handleClick = (message) => {
            emitter.emit('changeMessage', message);
        };
        render() {
            return (
                <div>
                    <button onClick={this.handleClick.bind(this, 'List2')}>点击我改变List1组件中显示信息</button>
                </div>
            );
        }
    }
  • APP.js
import React, { Component } from 'react';
    import List1 from './components/List1';
    import List2 from './components/List2';

    export default class App extends Component {
        render() {
            return (
                <div>
                    <List1 />
                    <List2 />
                </div>
            );
        }
    }
  • 自定义事件是典型的发布订阅模式,通过向事件对象上添加监听器和触发事件来实现组件之间的通信

七、React生命周期

1> 第一个是组件初始化阶段:

  • super(props)用来调用基类的构造方法(constructor()),也将父组件的props注入给子组件,供子组件读取(组件总props只读不可变,state可变)
  • 而constructor()用来做一些组件的初始化工作,如定义this.state的初始内容。
import React, { Component } from 'react';

    class Test extends Component {
        constructor(props) {
            super(props);
        }
    }

2> 第二个是组件的挂载阶段:

  • componentWillMount: 在组件挂载到DOM前调用,且只会被调用一次,在这边调用this.setState不会引起组件重新渲染,也可以把写在这边的内容提前放到constructor()中,所以项目中很少用。
  • render: 根据组件的props和state(无论两者的重传递和重赋值,论值是否有变化,都可以引起组件重新render),return一个React元素 (描述组件,即UI),不负责组件实际渲染工作,之后由React自身根据此元素去渲染出页面DOM。render是纯函数(函数的返回结果只依赖它的参数;函数执行过程里面没有副作用),不能在里面执行this.setState,会有改变组件状态的副作用。
  • componentDidMount: 组件挂载到DOM后调用,且只会被调用一次

3> 第三个是组件的更新阶段:

在讲述此阶段前需要先明确下react组件更新机制。setState引起的state更新或父组件更新render引起的props更新,更新后的state和props相对之前无论是否有变化,都将引起组件的重新渲染。

造成组件更新有两类(三种)情况: - 1.父组件重新render 父组件重新render引起子组件重新render的情况有两种: a. 直接使用,每当父组件重新render导致的重传props,子组件将直接跟着重新渲染,无论props是否有变化。可通过shouldComponentUpdate方法优化。

class Child extends Component {
    shouldComponentUpdate(nextProps){ // 应该使用这个方法,否则无论props是否有变化都将会导致组件跟着重新渲染
            if(nextProps.someThings === this.props.someThings){
            return false
            }
        }
        render() {
            return <div>{this.props.someThings}</div>
        }
    }

b.在componentWillReceiveProps方法中,将props转换成自己的state

class Child extends Component {
        constructor(props) {
            super(props);
            this.state = {
                someThings: props.someThings
            };
        }
        componentWillReceiveProps(nextProps) { // 父组件重传props时就会调用这个方法
            this.setState({someThings: nextProps.someThings});
        }
        render() {
            return <div>{this.state.someThings}</div>
        }
    }

在该函数(componentWillReceiveProps)中调用 this.setState() 将不会引起第二次渲染。 是因为componentWillReceiveProps中判断props是否变化了,若变化了,this.setState将引起state变化,从而引起render,此时就没必要再做第二次因重传props引起的render了,不然重复做一样的渲染了。

  • 2.组件本身调用setState,无论state有没有变化。可通过shouldComponentUpdate方法优化。
class Child extends Component {
        constructor(props) {
            super(props);
            this.state = {
                someThings:1
            }
        }
        shouldComponentUpdate(nextStates){ // 应该使用这个方法,否则无论state是否有变化都将会导致组件重新渲染
            if(nextStates.someThings === this.state.someThings){
                return false
            }
        }

        handleClick = () => { // 虽然调用了setState ,但state并无变化
            const preSomeThings = this.state.someThings
            this.setState({
                someThings: preSomeThings
            })
        }

        render() {
            return <div onClick = {this.handleClick}>{this.state.someThings}</div>
        }
    }

此阶段分为componentWillReceivePropsshouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate

  • componentWillReceiveProps(nextProps) 此方法只调用于props引起的组件更新过程中,参数nextProps是父组件传给当前组件的新props。但父组件render方法的调用不能保证重传给当前组件的props是有变化的,所以在此方法中根据nextProps和this.props来查明重传的props是否改变,以及如果改变了要执行啥,比如根据新的props调用this.setState出发当前组件的重新render
  • shouldComponentUpdate(nextProps, nextState) 此方法通过比较nextProps,nextState及当前组件的this.props,this.state,返回true时当前组件将继续执行更新过程,返回false则当前组件更新停止,以此可用来减少组件的不必要渲染,优化组件性能。 ps:这边也可以看出,就算componentWillReceiveProps()中执行了this.setState,更新了state,但在render前(如shouldComponentUpdate,componentWillUpdate),this.state依然指向更新前的state,不然nextState及当前组件的this.state的对比就一直是true了。
  • componentWillUpdate(nextProps, nextState) 此方法在调用render方法前执行,在这边可执行一些组件更新发生前的工作,一般较少用。
  • render render方法在上文讲过,这边只是重新调用。
  • componentDidUpdate(prevProps, prevState) 此方法在组件更新后被调用,可以操作组件更新的DOM,prevProps和prevState这两个参数指的是组件更新前的props和state

4>卸载阶段:

此阶段只有一个生命周期方法:

  • componentWillUnmount

此方法在组件被卸载前调用,可以在这里执行一些清理工作,比如清楚组件中使用的定时器,清楚componentDidMount中手动创建的DOM元素等,以避免引起内存泄漏。

八、React路由

  • 使用React构建的单页面应用,要想实现页面间的跳转,首先想到的就是使用路由。在React中,常用的有两个包可以实现这个需求,那就是react-router和react-router-dom。

react-router-dom

安装

  • 首先进入项目目录,使用npm安装react-router-dom: npm install react-router-dom --save-dev //这里可以使用cnpm代替npm命令

基本操作

  • 然后我们新建两个页面,分别命名为“home”和“detail”。在页面中编写如下代码:
// home.js
    import React from 'react';
    export default class Home extends React.Component {
        render() {
            return (
                <div>
                    <a>去detail</a>
                </div>
            )
        }
    }

    // detail.js
    import React from 'react';
    export default class Home extends React.Component {
        render() {
            return (
                <div>
                    <a>回到home</a>
                </div>
            )
        }
    }

    // 然后再新建一个路由组件,命名为“Router.js”,并编写如下代码:
    import React from 'react';
    import {HashRouter, Route, Switch} from 'react-router-dom';
    import Home from '../home';
    import Detail from '../detail';

    const BasicRoute = () => (
        <HashRouter>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route exact path="/detail" component={Detail}/>
            </Switch>
        </HashRouter>
    );

    export default BasicRoute;
  • 如上代码定义了一个纯路由组件,将两个页面组件Home和Detail使用Route组件包裹,外面套用Switch作路由匹配,当路由组件检测到地址栏与Route的path匹配时,就会自动加载响应的页面。

然后在入口文件中——我这里指定的是index.js——编写如下代码:

import React from 'react';
    import ReactDOM from 'react-dom';
    import Router from './router/router';

    ReactDOM.render(
        <Router/>,
        document.getElementById('root')
    );

这里相当于向页面返回了一个路由组件。我们先运行项目看一下效果,在地址栏输入“http://localhost:3000/#/”:

864609630ace30cd62133436454c9177.png

输入“http://localhost:3000/#/detail”:

af4a15cb82361371593bf932031a8acf.png

通过a标签跳转 可以看到其实路由已经开始工作了,接下来我们再来做页面间的跳转。在home.js和detail.js中,我们修改如下代码:

// home.js
    import React from 'react';
    export default class Home extends React.Component {
        render() {
            return (
                <div>
                    <a href='#/detail'>去detail</a>
                </div>
            )
        }
    }

    // detail.js
    import React from 'react';
    export default class Home extends React.Component {
        render() {
            return (
                <div>
                    <a href='#/'>回到home</a>
                </div>
            )
        }
    }

重新打包运行,在浏览器地址栏输入“http://localhost:3000/”, 试试看页面能否正常跳转。如果不能,请按步骤一步一步检查代码是否有误。以上是使用a标签的href进行页面间跳转,此外react-router-dom还提供了通过函数的方式跳转页面。

通过函数跳转 - 首先我们需要修改router.js中的两处代码:

import {HashRouter, Route, Switch, hashHistory} from 'react-router-dom';
<HashRouter history={hashHistory}>
  • 然后在home.js中:import React from 'react';
export default class Home extends React.Component {
        constructor(props) {
            super(props);
        }
        render() {
            return (
                <div>
                    <a href='#/detail'>去detail</a>
                    <button onClick={() => this.props.history.push('detail')}>通过函数跳转</button>
                </div>
            )
        }
    }

在a标签下面添加一个按钮并加上onClick事件,通过this.props.history.push这个函数跳转到detail页面。 在路由组件中加入的代码就是将history这个对象注册到组件的props中去,然后就可以在子组件中通过props调用history的push方法跳转页面。

很多场景下,我们还需要在页面跳转的同时传递参数,在react-router-dom中,同样提供了两种方式进行传参。

url传参 - 在router.js中,修改如下代码:

<Route exact path="/detail/:id" component={Detail}/>
  • 然后修改detail.js,使用this.props.match.params获取url传过来的参数:
componentDidMount() {
        console.log(this.props.match.params);
    }

在地址栏输入“http://localhost:3000/#/detail/3”,打开控制台:

3b46ece92fd48d186a0e85bf4813cba5.png

可以看到传过去的id=3已经被获取到了。react-router-dom就是通过“/:”去匹配url传递的参数。

隐式传参 此外还可以通过push函数隐式传参。 修改home.js代码如下:

import React from 'react';
    export default class Home extends React.Component {
        constructor(props) {
            super(props);
        }
        render() {
            return (
                <div>
                    <a href='#/detail/3'>去detail</a>
                        <button onClick={() => this.props.history.push({
                            pathname: '/detail',
                            state: {
                                id: 3
                            }
                    })}>通过函数跳转</button>
                </div>
            )
        }
    }

在detail.js中,就可以使用this.props.history.location.state获取home传过来的参数:

componentDidMount() {
        //console.log(this.props.match.params);
        console.log(this.props.history.location.state);
    }

跳转后打开控制台可以看到参数被打印:

c3e03e17ef36bd31866f2cfe877ecbd7.png

其他函数

replace 有些场景下,重复使用push或a标签跳转会产生死循环,为了避免这种情况出现,react-router-dom提供了replace。在可能会出现死循环的地方使用replace来跳转:

this.props.history.replace('/detail');

goBack 场景中需要返回上级页面的时候使用:

this.props.history.goBack();

嵌套路由 - 首先定义父级组件MainLayout

import React from 'react';
    import './MainLayout.scss';
    const { Header, Sider, Content } = Layout;
    export default class MainLayout extends React.Component {
        render() {
            return (
                <div className='main-layout'>
                    父组件
                </div>
            );
        }
    }
  • 然后定义子组件Home:
import React, {useState} from 'react';
    import {Modal, Select} from "antd";
    import {connect} from 'react-redux';
    import {addCount} from '../../servers/home';
    function Home(props) {
        const [visible, setVisible] = useState(false);
        const {countNum: {count}, dispatch} = props;
        return (
            <div>
                子组件
            </div>
        )
    }
    export default Home;

然后将它们添加进路由router.js,并且关联父子关系:

import React from 'react';
    import {HashRouter, Route, Switch} from "react-router-dom";
    import Home from '../pages/Home/Home';
    import MainLayout from '../layout/MainLayout';

    const BasicRouter = () => (
        <HashRouter>
            <Switch>
                <Route path="/index" component={
                    <MainLayout>
                    <Route exact path="/" component={Home}/>
                    <Route exact path="/index" component={Home}/>
                    <Route path="/index/home" component={Home}/>
                    </MainLayout>
                }/>
            </Switch>
        </HashRouter>
    );
    export default BasicRouter;

在MainLayout中,修改如下代码:

import React from 'react';
    import './MainLayout.scss';
    const { Header, Sider, Content } = Layout;
    export default class MainLayout extends React.Component {
        render() {
            return (
                <div className='main-layout'>
                    {this.props.children}
                </div>
            );
        }
    }

如此,一个嵌套路由就完成了。

react-router和react-router-dom的区别

  • 写法1:
import {Swtich, Route, Router, HashHistory, Link} from 'react-router-dom';
  • 写法2:
import {Switch, Route, Router} from 'react-router';
    import {HashHistory, Link} from 'react-router-dom';

先简单说下各自的功能: - react-router: 实现了路由的核心功能 react-router-dom: 基于react-router,加入了在浏览器运行环境下的一些功能,例如:Link组件,会渲染一个a标签,Link组件源码a标签行; BrowserRouter和HashRouter组件,前者使用pushState和popState事件构建路由,后者使用window.location.hash和hashchange事件构建路由。 - react-router-native: 基于react-router,类似react-router-dom,加入了react-native运行环境下的一些功能。

结论: - react-router-dom依赖react-router,所以我们使用npm安装依赖的时候,只需要安装相应环境下的库即可,不用再显式安装react-router。基于浏览器环境的开发,只需要安装react-router-dom;基于react-native环境的开发,只需要安装react-router-native。npm会自动解析react-router-dom包中package.json的依赖并安装。


  • react-router-dom中package.json依赖:
"dependencies": {
        "history": "^4.7.2",
        "invariant": "^2.2.2",
        "loose-envify": "^1.3.1",
        "prop-types": "^15.5.4",
        "react-router": "^4.2.0",
        "warning": "^3.0.0"
    }

安装了react-router-dom,npm会解析并安装上述依赖包。可以看到,其中包括react-router。

所以,回到最开始的写法。基于浏览器环境的开发,写法1就可以了。

Route组件 - Route组件用于组件的显示,Route组件上有path和component属性,对应的path会显示对应的component,路径和组件产生对应的映射关系

<div>
        <Route path='/' component={Home}></Route>
        <Route path='/page1' component={Page1}></Route>
        <Route path='/page2' component={Page2}></Route>
</div>
    //以上代码,当浏览器访问/page1或者/page2路径,都会渲染Home组件,因为先和‘/’匹配了
  • component属性:component能够接受一个组件名,也可以接受一个函数
<Route path="/" exact render={()=><h1>首页</h1>}/>
  • 路由匹配的方式:要路径开头匹配成功即会显示对应的组件,就是当访问/page1路径的时候会匹配 / 的路径 ,解决办法是 exact属性
  • exact:作用是严格匹配,路径是什么就匹配什么,避免路径匹配不正确的问题
<div>
        <Route path='/' exact component={Home}></Route>
        <Route path='/page1' component={Page1}></Route>
</div>
    //此时访问'/page1',就不会再匹配‘/’了

Link组件和NavLink组件 - Link 组件是react-router中提供的路由切换的组件,基于它可以实现点击的时候实现路由的切换

Link组件的属性

  • to:'/xxx?xxx=123' 跳转到指定的路由地址
  • to: {pathname:'/xxx',search:'xxx:123',state:''}//可以基于state的方式传参
  • replace 默认值false 是替换HISTORY stack中当前的地址(true) 还是追加一个新的地址 (默认是追加)
  • 基于link组件渲染,渲染后的结果是一个A标签,to对应的信息最后会变成href的内容
<Link to={{pathname:'/',search:'lx=logo'}}  className="navbar">强国首页</Link>

NavLink组件

NavLink组件和LINK类似,不同在于,NavLink组件在当前页面地址和组件对应地址相吻合的时候,会默认给组件加一个active样式,让其有选中态(可以修改选中状态) - activeClassName:把默认你加的active样式类改为自己设定的(默认是active) - activeStyle:给匹配的NavLink设置行内样式 - exact 控制匹配的时候是否严格匹配(和route的一样) - isActive:()=>{} 匹配成功后执行对应函数(相当于路由的钩子函数)

<NavLinkto={{pathname:'/',search:'lx=logo'}}  className="navbar">强国首页</NavLink>
    //匹配成功时,此元素会有active类名
    //有时会有bug=>类名不切换,可以用hashChange事件监听或者使用withRouter

路由对象中的 history、location、 match、params

1.history对象 (用于编程式导航)

  • let {history}=this.props;
  • history.go(n)//到任意有的记录中
  • history.push() //追加一条记录
  • history.replace()//不会追加记录
  • history.goback()//回退
  • history.goforward()//前进

2.location

  • let {location:{pathname,search,state}}=this.props;
  • pathname当前的hash路由地址
  • search查询字符串
  • state 基于 redirect、link、nav-link中的to={},传递的是一个对象,对象中编写的state,就可以通过 loactuion.state获取

3.match 匹配动态路由的值主要使用 params动态路由参数

let {match:{params}}=this.props;
console.log(params.id)
//获取的是动态路由匹配的一些结果,可用于动态传参

二级路由

<Switch>
        {/*进入客户管理页面,我们默认为其展示的就是List区域内容,或者使用重定向也可以*/}
        <Route path='/custom' exact component={List}></Route>
        <Route path='/custom/list' component={List}></Route>
        <Route path='/custom/create' component={Create}></Route>
        <Route path='/custom/Detail/:id' component={Detail}></Route>
        {/*进入客户管理页面,我们默认为其展示的就是List区域内容*/}
        {/*<Redirect from='/custom' to='/custom/list'></Redirect>*/}
    </Switch>

Switch组件,用于嵌套Route组件 - 和switch case一样,只能 选择一个,避免一个路径匹配多个组件 像这种情况,可能都会被渲染到页面上,此时Switch组件就有用了

<Route path="/about" component={About}/>
    <Route path="/:user" component={User}/>
    <Route component={NoMatch}/>{/*无论前边路由是否匹配成功,都会走这个*/}

路由数据传输方式 不推荐: - 本地存储 localstroage - redux存储 (页面刷新数据就没了) 点击列表中某一项的时候,把信息存储到本地或者redux中,跳转到详情页面,把信息从本地或者redux中获取即可

推荐: - 1.问号传参

<Link to={{
        pathname:'/home/user',
        search:`?id=${item.id}`, //问号传参
    }}>问号传参</Link>
    // 受控组件中获取参数通this.props.location.search
  • 2.state传值(一旦页面刷新state传的值就没了)
<Link to={{
        pathname:'/home/user',
        state:{'id':item.id},
    }}>state传值</Link>
    // 受控组件中获取参数通过  this.props.location.state
  • 3.URL地址参数 path=‘/custom/detail/:id’
//动态路径设置
    <Link to={{
        pathname:`/home/user/${item.id}`
    }}>URL地址参数</Link>
    //Route路径配置
    <Route path='/custom/Detail/:id' component={User}></Route>
    //User组件内获取参数
    this.props.match.params.id

路由重定向 Redirect

<Redirect from='/user' to='/user/list'></Redirect>//from 从哪个路由  to定向到哪个路由

处理默认子路由

  • Redirect
<Route path='/user/list' component={List}></Route><Redirect from='/user' to='/user/list'></Redirect>//路径 /user下的默认显示的子路由是 /user/list  ,/user和/user/list 最后渲染的都是List组件
  • 两个路径都指向一个组件
<Route path='/user' exact component={List}></Route><Route path='/user/list' component={List}></Route>

九、Redux

Redux 是一个独立的 JavaScript 状态管理库。 曾经有人说过这样一句话。

"如果你不知道是否需要 Redux,那就是不需要它。"

Redux 的创造者 Dan Abramov 又补充了一句。

"只有遇到 React 实在解决不了的问题,你才需要 Redux 。"

核心概念 理解 Redux 几个核心概念与它们之间的关系: - store - state - action - reducer

Store Store就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个Store。

Redux提供createStore这个函数,用来生成Store:

import { createStore } from 'redux';
    const store = createStore(fn);

上面代码中,createStore函数接受另一个函数作为参数,返回新生成的 Store 对象。 state Store对象包含所有数据。如果想得到某个时点的数据,就要对Store生成快照。这种时点的数据集合,就叫做State。

当前时刻的State,可以通过store.getState()拿到:

import { createStore } from 'redux';
    const store = createStore(fn);
    const state = store.getState();

Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。

state是只读的 - 这里需要注意的是,为了保证数据状态的可维护和测试,不推荐直接修改state中的原数据 - 通过纯函数修改state 什么是纯函数? 1.相同的输入永远返回相同的输出 2.不修改函数的输入值 3.不依赖外部环境状态 4.无任何副作用 - 使用纯函数的好处? 1.便于测试 2.有利重构

Action State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。

Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置:

const action = {
        type: 'ADD_TODO',
        payload: 'Learn Redux'
    };
    // 上面代码中,Action 的名称是ADD_TODO,它携带的信息是字符串Learn Redux。
    // 可以这样理解,Action 描述当前发生的事情。改变 State 的唯一办法,就是使用 Action。它会运送数据到 Store。

Action Creator View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。

const ADD_TODO = '添加 TODO';
        function addTodo(text) {
            return {
                type: ADD_TODO,
                text
            }
        }
    const action = addTodo('Learn Redux');
    // 上面代码中,addTodo函数就是一个 Action Creator。

store.dispatch() store.dispatch()是 View 发出 Action 的唯一方法。

import { createStore } from 'redux';
    const store = createStore(fn);
    store.dispatch({
        type: 'ADD_TODO',
        payload: 'Learn Redux'
    });
    // 上面代码中,store.dispatch接受一个 Action 对象作为参数,将它发送出去。

结合 Action Creator,这段代码可以改写如下。

store.dispatch(addTodo('Learn Redux'));

Reducer Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。

Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State:

const reducer = function (state, action) {
        // ...
        return new_state;
    };

整个应用的初始状态,可以作为 State 的默认值。下面是一个实际的例子:

const defaultState = 0;
    const reducer = (state = defaultState, action) => {
        switch (action.type) {
            case 'ADD':
                return state + action.payload;
            default: 
                return state;
        }
    };
    const state = reducer(1, {
        type: 'ADD',
        payload: 2
    });
    // 上面代码中,reducer函数收到名为ADD的 Action 以后,就返回一个新的 State,作为加法的计算结果。
实际应用中,Reducer 函数不用像上面这样手动调用,store.dispatch方法会触发 Reducer 的自动执行。为此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入createStore方法。
    import { createStore } from 'redux';
    const store = createStore(reducer);
    // 上面代码中,createStore接受 Reducer 作为参数,生成一个新的 Store。以后每当store.dispatch发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。

store.subscribe() Store 允许使用store.subscribe方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。

import { createStore } from 'redux';
    const store = createStore(reducer);
    store.subscribe(listener);

显然,只要把 View 的更新函数(对于 React 项目,就是组件的render方法或setState方法)放入listen,就会实现 View 的自动渲染。

store.subscribe方法返回一个函数,调用这个函数就可以解除监听:

let unsubscribe = store.subscribe(() =>
        console.log(store.getState())
    );
    unsubscribe();

Reducer 的拆分

  • Reducer 函数负责生成 State。由于整个应用只有一个 State 对象,包含所有数据,对于大型应用来说,这个 State 必然十分庞大,导致 Reducer 函数也十分庞大

请看下面的例子:

const chatReducer = (state = defaultState, action = {}) => {
    const { type, payload } = action;
        switch (type) {
            case ADD_CHAT:
                return Object.assign({}, state, {
                    chatLog: state.chatLog.concat(payload)
                });
            case CHANGE_STATUS:
                return Object.assign({}, state, {
                    statusMessage: payload
                });
            case CHANGE_USERNAME:
                return Object.assign({}, state, {
                    userName: payload
                });
            default: return state;
        }
    };

上面代码中,三种 Action 分别改变 State 的三个属性: - ADD_CHAT:chatLog属性 - CHANGE_STATUS:statusMessage属性 - CHANGE_USERNAME:userName属性 这三个属性之间没有联系,这提示我们可以把 Reducer 函数拆分。不同的函数负责处理不同属性,最终把它们合并成一个大的 Reducer 即可

const chatReducer = (state = defaultState, action = {}) => {
        return {
            chatLog: chatLog(state.chatLog, action),
            statusMessage: statusMessage(state.statusMessage, action),
            userName: userName(state.userName, action)
        }
    };
    // 上面代码中,Reducer 函数被拆成了三个小函数,每一个负责生成对应的属性。
    // 这样一拆,Reducer 就易读易写多了。而且,这种拆分与 React 应用的结构相吻合:一个 React 根组件由很多子组件构成。这就是说,子组件与子 Reducer 完全可以对应。

Redux 提供了一个combineReducers方法,用于 Reducer 的拆分。你只要定义各个子 Reducer 函数,然后用这个方法,将它们合成一个大的 Reducer:

import { combineReducers } from 'redux';
    const chatReducer = combineReducers({
        chatLog,
        statusMessage,
        userName
    })
    export default todoApp;
    // 上面的代码通过combineReducers方法将三个子 Reducer 合并成一个大的函数。

这种写法有一个前提,就是 State 的属性名必须与子 Reducer 同名。如果不同名,就要采用下面的写法:

const reducer = combineReducers({
        a: doSomethingWithA,
        b: processB,
        c: c
    })

    // 等同于
    function reducer(state = {}, action) {
        return {
            a: doSomethingWithA(state.a, action),
            b: processB(state.b, action),
            c: c(state.c, action)
        }
    }
// 总之,combineReducers()做的就是产生一个整体的 Reducer 函数。该函数根据 State 的 key 去执行相应的子 Reducer,并将返回结果合并成一个大的 State 对象。

下面是combineReducer的简单实现。

const combineReducers = reducers => {
  return (state = {}, action) => {
    return Object.keys(reducers).reduce(
      (nextState, key) => {
        nextState[key] = reducers[key](state[key], action);
        return nextState;
      },
      {} 
    )
  }}

你可以把所有子 Reducer 放在一个文件里面,然后统一引入:

import { combineReducers } from 'redux'import * as reducers from './reducers'const reducer = combineReducers(reducers)

工作流程

0257cd29daa7e4b4b1bf463512b9e64c.png
  • 首先,用户发出 Action。
store.dispatch(action);
  • 然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。
let nextState = todoApp(previousState, action);
  • State 一旦有变化,Store 就会调用监听函数。
// 设置监听函数
    store.subscribe(listener);
  • listener可以通过store.getState()得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。
function listerner() {
    let newState = store.getState();
        component.setState(newState);   
    }

Provider 组件

  • 想在 React 中使用 Redux ,还需要通过 react-redux 提供的 Provider 容器组件把 store 注入到应用中
// index.js
    import {Provider} from 'react-redux';
    import store from './store';
    ReactDOM.render(
        <Provider store={store}>
            <Router>
                <App />
            </Router>
        </Provider>, 
        document.getElementById('root')
    );

connect 方法

  • 有了 connect 方法,我们不需要通过 props 一层层的进行传递, 类似路由中的 withRouter ,我们只需要在用到 store 的组件中,通过 react-redux 提供的 connect 方法,把 store 注入到组件的 props 中就可以使用了
import {connect} from 'react-redux';
        class Main extends React.Component {
            render() {
                console.log(this.props);
            }
        }
    export default connect()(Main);
    // 默认情况下,connect 会自动注入 dispatch 方法

注入 state 到 props

export default connect( state => {
        return {
            items: state.items
        }
    } )(Main);

connect 方法的第一个参数是一个函数 - 该函数的第一个参数就是 store 中的 state : store.getState() - 该函数的返回值将被解构赋值给 props : this.props.items

redux-chunk

  • 这是一个把同步 dispatch 变成异步 dispatch 的中间件

安装

npm i -S redux-thunk
    import {createStore, combineReducers, applyMiddleware} from 'redux';
    import thunk from 'redux-thunk';
    import { composeWithDevTools } from 'redux-devtools-extension';

    import user from './reducer/user';
    import items from './reducer/items';
    import cart from './reducer/cart';

    let reducers = combineReducers({
        user,
        items,
        cart
    });

    const store = createStore(
        reducers,
        composeWithDevTools(applyMiddleware(
            thunk
        ))
    );
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值