React高级


一、React脚手架

1、创建并启动

第一步,全局安装:npm install -g create-react-app

第二步,切换到想创项目的目录,使用命令:create-react-app 项目名称 create-react-app 项目名称 --template typescript

第三步,进入项目文件夹:cd hello-react

第四步,启动项目:npm start

相关命令

如果使用yarn,只需把npm替换为yarn即可

启动

npm start

打包为静态文件

npm run build

以测试方式运行(基本不用)

npm test

默认关于webpack的配置文件时隐藏的,可通过以下命令可视化,不过之后无法再次隐藏

npm run eject

执行第二步报错

PS G:\zz_learn_front\1_react_demo> create-react-app react_demo
create-react-app : 无法加载文件 G:\codesoftware\install\node\node_global\create-react-app.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https
:/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies。
所在位置 行:1 字符: 1
+ create-react-app react_demo
+ ~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [],PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

解决方法:

window左下角搜索 Windos PowerShell,并以管理员身份运行,依次执行以下命令

set-ExecutionPolicy RemoteSigned

输入A,回车,之后查看当前状态

get-ExecutionPolicy

项目部署

全局安装serve

npm i serve -g

运行

serve -s build

2、脚手架文件结构

public ---- 静态资源文件夹
    favicon.icon ------ 网站页签图标
    index.html -------- 主页面 (重要)
    logo192.png ------- logo图
    logo512.png ------- logo图
    manifest.json ----- 应用加壳的配置文件
    robots.txt -------- 爬虫协议文件
src ---- 源码文件夹
    App.css -------- App组件的样式
    App.js --------- App组件 (重要)
    App.test.js ---- 用于给App做测试
    index.css ------ 样式
    index.js ------- 入口文件 (重要)
    logo.svg ------- logo图
    reportWebVitals.js
        --- 页面性能分析文件(需要web-vitals库的支持)
    setupTests.js
        ---- 组件单元测试的文件(需要jest-dom库的支持)

3、index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <!-- %PUBLIC_URL% : 代表public文件夹路径 -->
    <!-- 网站图标 -->
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <!-- 开启理想视口,用于做移动端网页的适配 -->
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <!-- 用于配置浏览器页签+地址栏的颜色(仅支持安卓手机浏览器) -->
    <meta name="theme-color" content="#000000" />
    <!-- 网站描述信息,用于百度搜索 -->
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <!-- 网站被添加到手机主屏幕的图标,只用于苹果手机 -->
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!-- 应用加壳时的配置文件 -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <!-- 当浏览器不支持js时展示 noscript 的内容 -->
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

4、index.js

入口文件

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// 用于性能测试
import reportWebVitals from './reportWebVitals';

//渲染虚拟DOM到页面
ReactDOM.render(
    /*  <React.StrictMode> 用于检查 App及其子组件的代码是否合理 */
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// 用于性能测试
reportWebVitals();

5、组件引入

  1. 为了区分组件和普通js文件,可以把定义组件的js文件后缀改成jsx
  2. 一个组件一个文件夹
  3. 引入js文件或者jsx文件时,可以不写后缀名
  4. 组件文件夹中的文件可以都命名为index,例如index.jsx/index.css,引入的时候可以直接引到目录名就行了
// 引入 react 
import React,{Component} from 'react'
import './index.css'

export default class Welcome extends Component{
    render(){
        return <h2 className="title">Welcome</h2>
    }
}

6、css模块化

当多个组件引入各自的css样式后,挂载到app组件中时,可能多个组件加载的css具有重名样式,此时后引入的会覆盖之前引入的。

可将css文件名加上module,模块化

index.module.css

.title{
    background-color: orange;
}

index.jsx

import React,{Component} from 'react'
import hello from './index.module.css'

export default class Hello extends Component{
    render(){
        return <h2 className={hello.title}>Hello,React!</h2>
    }
}

7、快速创建组件

webstorm可以使用以下快捷命令创建组件基本结构

rcc :简单的类式组件

rcfc:具有所有生命周期的类式组件

rsf:简单的函数式组件

8、组件化编程流程

1. 拆分组件: 拆分界面,抽取组件
2. 实现静态组件: 使用组件实现静态页面效果
3. 实现动态组件
    3.1 动态显示初始化数据
        3.1.1 数据类型
        3.1.2 数据名称
        3.1.2 保存在哪个组件?
    3.2 交互(从绑定事件监听开始)

二、案例

1、各模块思路

整体

分为五个组件:

  • App组件:外层容器组件
  • Header组件:输入框
  • Item组件:单个可选单元
  • List组件:包含多个Item组件
  • Footer组件:底部

输入名称回车添加列表项

在Header组件监听输入键位,当监听到Enter键时,将输入框中的内容传到父组件(子组件通过调用父组件定义的方法实现数据传递)中,父组件设置新的状态值,实现新增列表项

获取选中的列表项,并设置done属性

在Item组件中监听复选框的选中状态是否变化,如果变化,则通过App父组件定义的方法实现数据传递,将状态变化的列表项的id和变化后的选中状态传递到父组件App中,并在App中设置done属性值

2、App.js

import React, {Component} from 'react';
import './App.css';

import Header from './components/Header'
import Footer from './components/Footer'
import List from './components/List'

class App extends Component {

  // 初始化状态
  state = {
    todos: [
      {id: 1,name: "吃饭",done: true},
      {id: 2,name: "睡觉",done: true},
      {id: 3,name: "敲代码",done: false},
      {id: 4,name: "看书",done: false}
    ]
  }

  //用于子组件传递数据给父组件,原理是父组件通过 props 向子组件传递一个函数,子组件通过调用这个函数将数据作为返回值传递到父组件
  addTodo = (todoObj)=>{
    console.log("接收到子组件传递的数据",todoObj)
    // 获取原来的todos数组
    const {todos} = this.state
    //追加一个todo,新todo数组最前面是子组件传递的todo对象,后面是原本的todo对象
    const newTodos = [todoObj,...todos];
    //更新状态
    this.setState({todos:newTodos})
  }

  //勾选item
  selectItem = (id,isDone)=>{
    const {todos} = this.state;
    // 为选中的todo的done设置值
    const newTodos = todos.map((todoObj)=>{
      if(todoObj.id === id){
        return {...todoObj,done:isDone}
      }else{
        return todoObj
      }
    })
    this.setState({todos: newTodos});
  }

  //删除列表项
  delTodo = (id)=>{
    console.log(id)
    const {todos} = this.state;
    // 删除指定id的todo
    const newTodos = todos.filter((todoObj)=>{
      return todoObj.id !== id;
    })
    this.setState({todos: newTodos});
  }

  //checkAllTodo用于全选
  checkAllTodo = (done)=>{
    //获取原来的todos
    const {todos} = this.state
    //加工数据
    const newTodos = todos.map((todoObj)=>{
      return {...todoObj,done}
    })
    //更新状态
    this.setState({todos:newTodos})
  }

  //清楚已完成任务
  handleClearAllDone = ()=>{
    const {todos} = this.state;
    const newTodos = todos.filter((todo)=>{
      return !todo.done;
    })
    this.setState({todos: newTodos})
  }

  render() {
    const {todos} = this.state;
    return (
        <div className="todo-container">
          <div className="todo-wrap">
            <Header addTodo={this.addTodo}/>
            <List todos={todos} selectItem={this.selectItem} delTodo={this.delTodo}/>
            <Footer todos={todos} checkAllTodo={this.checkAllTodo} handleClearAllDone={this.handleClearAllDone}/>
          </div>
        </div>
    );
  }
}

export default App;

3、Header.jsx

import React, {Component} from 'react';

// 通过 npm i nanoid 安装,可通过调用 nanoid() 生成 uuid
import {nanoid} from "nanoid";
// 通过 npm install --save prop-types 安装,用于限制属性类型
import PropTypes from "prop-types";

class Index extends Component {

    // 对接收的 props 进行:类型和必要性的限制
    static propTypes = {
        //函数类型,必须有
        addTodo: PropTypes.func.isRequired
    }

    change = (event)=>{
        const {key,target} = event;
        // 判断是否按下回车
        if (key !== "Enter"){
            return;
        }
        console.log(target.value);
        // 封装todo对象
        const todoObj = {id: nanoid(),name: target.value,done: false}
        //将todo对象返回到父组件
        this.props.addTodo(todoObj);
        //清空输入
        target.value = '';
    }

    render() {
        return (
            <div className="todo-header">
                <input onKeyUp={this.change} type="text" placeholder="请输入你的任务名称,按回车键确认"/>
            </div>
        );
    }
}

export default Index;

4、Item.jsx

import React, {Component} from 'react';

class Index extends Component {

    state = {
        showDel: false, // 标志鼠标是否移入
    }

    // 判断鼠标是否移入,如果移入则当前行展示删除按钮,并且背景变灰
    handleMouse = (b)=>{
        return ()=>{
            this.setState({showDel:b})
        }
    }

    //复选框回调
    changeDone = (event)=>{
        console.log(this.props.todo.id,event.target.checked)
        this.props.selectItem(this.props.todo.id,event.target.checked)
    }

    //删除列表项
    delTodo = ()=>{
        return ()=>{
            if (window.confirm("确定删除吗?")){
                this.props.delTodo(this.props.todo.id)
            }
        }
    }

    render() {
        const {todo} = this.props;
        const {showDel} = this.state;
        return (
            <li onMouseEnter={this.handleMouse(true)} 
                onMouseLeave={this.handleMouse(false)}
                style={{backgroundColor: showDel ? '#ddd': 'white'}}>
                <label>
                    <input type="checkbox" checked={todo.done} 
                        onChange={this.changeDone}/>
                    <span>{todo.name}</span>
                </label>
                <button onClick={this.delTodo()} 
                    className="btn btn-danger" 
                    style={{display: showDel?'':'none'}}>删除</button>
            </li>
        );
    }
}

export default Index;

5、List.jsx

import React, {Component} from 'react';

import Item from '../Item'

class Index extends Component {
    render() {
        const {todos,selectItem,delTodo} = this.props;
        return (
            <ul className="todo-main">
                {
                    // map函数,用于遍历数组
                    todos.map((todo)=>{
                        return <Item todo={todo} key={todo.id} selectItem={selectItem} delTodo={delTodo}/>
                    })
                }
            </ul>
        );
    }
}

export default Index;

6、Footer.jsx

import React, {Component} from 'react';

class Index extends Component {

    //全选checkbox的回调
    handleCheckAll = (event)=>{
        this.props.checkAllTodo(event.target.checked)
    }

    //清除已完成任务的回调
    handleClearAllDone = ()=>{
        return ()=>{
            this.props.handleClearAllDone()
        }
    }

    render() {
        const {todos,handleClearAllDone} = this.props
        //已完成的个数
        const doneCount = todos.reduce((pre,todo)=> pre + (todo.done ? 1 : 0),0)
        //总数
        const total = todos.length
        return (
            <div className="todo-footer">
                <label>
                    <input type="checkbox"  
                        onChange={this.handleCheckAll} 
                        checked={doneCount === total && total !== 0 ? true : false}/>
                </label>
                <span>
                  <span>已完成{doneCount}</span> / 全部{total}
                </span>
                <button className="btn btn-danger" 
                    onClick={handleClearAllDone}>清除已完成任务</button>
            </div>
        );
    }
}

export default Index;

7、总结

1、拆分组件、实现静态组件,注意:className、style的写法

2、动态初始化列表,如何确定将数据放在哪个组件的state中?

​ - 某个组件使用:放在其 自身 的state中

​ - 某些组件使用:放在他们共同的 父组件 state中(官方称此操作为:状态提升)

3、关于父子之间通信:

【父组件】给【子组件】传递数据:通过props传递

【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数

4、注意defaultChecked (只在第一次指定的时候有作用,之后就没作用了)和 checked的区别,类似的还有:defaultValue 和 value

5、状态在哪里,操作状态的方法就在哪里

三、配置代理

用于解决跨域问题,原理是在前端的端口上在起一个服务器,接收前端请求,并转发到后端,从而解决跨域

方法一

在package.json中追加如下配置

"proxy":"http://localhost:5000"

说明:

  1. 优点:配置简单,前端请求资源时可以不加任何前缀。
  2. 缺点:不能配置多个代理。
  3. 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)

方法二

  1. 第一步:创建代理配置文件(文件名固定)

    在src下创建配置文件:src/setupProxy.js
  2. 编写setupProxy.js配置具体代理规则:

    const proxy = require('http-proxy-middleware')
    
    module.exports = function(app) {
      app.use(
        proxy('/api1', {  //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
          target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址)
          changeOrigin: true, //控制服务器接收到的请求头中host字段的值
          /*
              changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
              changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
              changeOrigin默认值为false,但我们一般将changeOrigin值设为true
          */
          pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
        }),
        proxy('/api2', { 
          target: 'http://localhost:5001',
          changeOrigin: true,
          pathRewrite: {'^/api2': ''}
        })
      )
    }

说明:

  1. 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
  2. 缺点:配置繁琐,前端请求资源时必须加前缀。

四、网络请求

1、axios方式

采用axios,见vue的文章

2、fetch方式(了解)

  • fetch: 原生函数,不再使用XmlHttpRequest对象提交ajax请求

  • 老版本浏览器可能不支持

get

fetch(url).then(function(response) {
    return response.json()
}).then(function(data) {
    console.log(data)
}).catch(function(e) {
    console.log(e)
});

post

fetch(url, {
    method: "POST",
    body: JSON.stringify(data),
}).then(function(data) {
    console.log(data)
}).catch(function(e) {
    console.log(e)
})

结合 async 和 await

search = async()=>{
        //获取用户的输入(连续解构赋值+重命名)
        const {keyWordElement:{value:keyWord}} = this
        //发送请求前通知List更新状态
        PubSub.publish('atguigu',{isFirst:false,isLoading:true})

        //发送网络请求---使用fetch发送(未优化)
        /* fetch(`/api1/search/users2?q=${keyWord}`).then(
            response => {
                console.log('联系服务器成功了');
                return response.json()
            },
            error => {
                console.log('联系服务器失败了',error);
                return new Promise(()=>{})
            }
        ).then(
            response => {console.log('获取数据成功了',response);},
            error => {console.log('获取数据失败了',error);}
        ) */

        //发送网络请求---使用fetch发送(优化)
        try {
            const response= await fetch(`/api1/search/users2?q=${keyWord}`)
            const data = await response.json()
            console.log(data);
            PubSub.publish('atguigu',{isLoading:false,users:data.items})
        } catch (error) {
            console.log('请求出错',error);
            PubSub.publish('atguigu',{isLoading:false,err:error.message})
        }
    }

五、组件之间通信

1、父传子

(1)类式组件

  • 你不能改变 props。当你需要交互性时,你可以设置 state。

1、利用props即可

父组件:

<List todos={todos}/>

子组件:

const {todos} = this.props;

2、接收标签体内容

标签体内容是特殊的标签属性,子组件通过this.props.children可以获取标签体内容,同时也可以通过设置该属性动态设置标签体。

(2)函数式组件

1、父组件将 props 传递给子组件

export default function Profile() {
  return (
    <Avatar
      person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
      size={100}
    />
  );
}

如果 person= 后面的双花括号让你感到困惑,请记住,在 JSX 花括号中,它们只是一个对象。

2、子组件中读取 props

你可以通过在 function Avatar 之后直接列出它们的名字 person, size 来读取这些 props。这些 props 在 ({ 和 }) 之间,并由逗号分隔。这样,你可以在 Avatar 的代码中使用它们,就像使用变量一样。

function Avatar(props) {
  let person = props.person;
  let size = props.size;
  // ...
}

结构后,也可写成下面的代码

function Avatar({ person, size }) {
  // 在这里 person 和 size 是可访问的
}

(3)指定默认值

如果你想在没有指定值的情况下给 prop 一个默认值,你可以通过在参数后面写 = 和默认值来进行解构:

function Avatar({ person, size = 100 }) {
  // ...
}

现在, 如果 <Avatar person={...} /> 渲染时没有 size prop, size 将被赋值为 100

默认值仅在缺少 size prop 或 size={undefined} 时生效。 但是如果你传递了 size={null}size={0},默认值将 被使用。

(4)展开语法传递 props

有时候,传递 props 会变得非常重复:

function Profile({ person, size, isSepia, thickBorder }) {

  return (
    <div className="card">
      <Avatar
        person={person}
        size={size}
        isSepia={isSepia}
        thickBorder={thickBorder}
      />
    </div>
  );
}

重复代码没有错(它可以更清晰)。但有时你可能会重视简洁。一些组件将它们所有的 props 转发给子组件,正如 Profile 转给 Avatar 那样。因为这些组件不直接使用他们本身的任何 props,所以使用更简洁的“展开”语法是有意义的:

function Profile(props) {

  return (
    <div className="card">
      <Avatar {...props} />
    </div>
  );
}

这会将 Profile 的所有 props 转发到 Avatar,而不列出每个名字。

(5)随时间变化

export default function Clock({ color, time }) {
  return (
    <h1 style={{ color: color }}>
      {time}
    </h1>
  );
}

这个例子说明,一个组件可能会随着时间的推移收到不同的 props。 Props 并不总是静态的!在这里,time prop 每秒都在变化。当你选择另一种颜色时,color prop 也改变了。Props 反映了组件在任何时间点的数据,并不仅仅是在开始时。

2、子传父

父组件

//用于子组件传递数据给父组件,原理是父组件通过 props 向子组件传递一个函数,
//子组件通过调用这个函数将数据作为返回值传递到父组件
addTodo = (todoObj)=>{
    console.log("接收到子组件传递的数据",todoObj)
    // 获取原来的todos数组
    const {todos} = this.state
    //追加一个todo,新todo数组最前面是子组件传递的todo对象,后面是原本的todo对象
    const newTodos = [todoObj,...todos];
    //更新状态
    this.setState({todos:newTodos})
}

render() {
    const {todos} = this.state;
    return (
        <Header addTodo={this.addTodo}/>
    );
}

子组件:(通过将数据作为参数调用父组件的方法实现传递)

this.props.addTodo(todoObj);

3、发布订阅

1、工具库: PubSubJS

2、下载: npm install pubsub-js --save

3、可用户任意组件之间的通信,父子、兄弟、祖孙等。

4、使用:

import PubSub from 'pubsub-js' //引入

var token = PubSub.subscribe('topic', function(topic,data){ }); //订阅

PubSub.unsubscribe(token);    //取消订阅

PubSub.publish('topic', data) //发布消息

(1)发布

import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import axios from 'axios'

export default class Search extends Component {

    search = ()=>{
        //获取用户的输入(连续解构赋值+重命名)
        const {keyWordElement:{value:keyWord}} = this
        //发送请求前通知List更新状态
        PubSub.publish('atguigu',{isFirst:false,isLoading:true})
        //发送网络请求
        axios.get(`/api1/search/users?q=${keyWord}`).then(
            response => {
                //请求成功后通知List更新状态
                PubSub.publish('topic',{isLoading:false,users:response.data.items})
            },
            error => {
                //请求失败后通知App更新状态
                PubSub.publish('atguigu',{isLoading:false,err:error.message})
            }
        )
    }

    render() {
        return (
            <section className="jumbotron">
                <h3 className="jumbotron-heading">搜索github用户</h3>
                <div>
                    <input ref={c => this.keyWordElement = c} type="text" placeholder="输入关键词点击搜索"/>&nbsp;
                    <button onClick={this.search}>搜索</button>
                </div>
            </section>
        )
    }
}

(2)订阅

import React, { Component } from 'react'
import PubSub from 'pubsub-js'
import './index.css'

export default class List extends Component {

    state = { //初始化状态
        users:[], //users初始值为数组
        isFirst:true, //是否为第一次打开页面
        isLoading:false,//标识是否处于加载中
        err:'',//存储请求相关的错误信息
    }

    componentDidMount(){
        // 订阅主题为'topic'的消息
        this.token = PubSub.subscribe('topic',(topic,stateObj)=>{
            this.setState(stateObj)
        })
    }

    componentWillUnmount(){
        //取消订阅
        PubSub.unsubscribe(this.token)
    }

    render() {
        const {users,isFirst,isLoading,err} = this.state
        return (
            <div className="row">
                {
                    isFirst ? <h2>欢迎使用,输入关键字,随后点击搜索</h2> :
                    isLoading ? <h2>Loading......</h2> :
                    err ? <h2 style={{color:'red'}}>{err}</h2> :
                    users.map((userObj)=>{
                        return (
                            <div key={userObj.id} className="card">
                                <a rel="noreferrer" href={userObj.html_url} target="_blank">
                                    <img alt="head_portrait" src={userObj.avatar_url} style={{width:'100px'}}/>
                                </a>
                                <p className="card-text">{userObj.login}</p>
                            </div>
                        )
                    })
                }
            </div>
        )
    }
}

4、组件通信总结

组件间的关系:

  • 父子组件
  • 兄弟组件(非嵌套组件)
  • 祖孙组件(跨级组件)

几种通信方式:

    1.props:
        (1).children props
        (2).render props
    2.消息订阅-发布:
        pubs-sub、event等等
    3.集中式管理:
        redux、dva等等
    4.conText:
        生产者-消费者模式

比较好的搭配方式:

    父子组件:props
    兄弟组件:消息订阅-发布、集中式管理
    祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多)

六、路由

新版https://juejin.cn/post/7234058088930934842

1、react-router-dom

下载react-router-dom: npm install react-router-dom

基本使用

1、明确好界面中的导航区、展示区

2、导航区的a标签改为Link标签(可在Link中嵌套图片等,实现点击图片跳转)
<Link to="/xxxxx">Demo</Link>

3、展示区写Route标签进行路径的匹配
<Route path='/xxxx' component={Demo}/>

4、<App>的最外侧包裹了一个<BrowserRouter><HashRouter>

相关 API

1. 内置组件
<BrowserRouter>
<HashRouter>
<Route>
<Redirect>
<Link>
<NavLink>
<Switch>

2. 其它
history对象
match对象
withRouter函数

2、基本使用

index.js

//引入react核心库
import React from 'react'
//引入ReactDOM
import ReactDOM from 'react-dom'
//
import {BrowserRouter} from 'react-router-dom'
//引入App
import App from './App'

ReactDOM.render(
    <BrowserRouter>
        <App/>
    </BrowserRouter>,
    document.getElementById('root')
)

app.jsx

import React, { Component } from 'react'
import {Link,Route} from 'react-router-dom'
import Home from './components/Home'
import About from './components/About'

export default class App extends Component {
    render() {
        return (
            <div>
                <div className="row">
                    <div className="col-xs-offset-2 col-xs-8">
                        <div className="page-header"><h2>React Router Demo</h2></div>
                    </div>
                </div>
                <div className="row">
                    <div className="col-xs-2 col-xs-offset-2">
                        <div className="list-group">

                            {/* 原生html中,靠<a>跳转不同的页面 */}
                            {/* <a className="list-group-item" href="./about.html">About</a>
                            <a className="list-group-item active" href="./home.html">Home</a> */}

                            {/* 在React中靠路由链接实现切换组件--编写路由链接 */}
                            <Link className="list-group-item" to="/about">About</Link>
                            <Link className="list-group-item" to="/home">Home</Link>
                        </div>
                    </div>
                    <div className="col-xs-6">
                        <div className="panel">
                            <div className="panel-body">
                                {/* 注册路由,根据情况渲染 About和Home组件 */}
                                <Route path="/about" component={About}/>
                                <Route path="/home" component={Home}/>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        )
    }
}

3、路由组件和一般组件

1.写法不同:

一般组件:<Demo/>

路由组件:<Route path="/demo" component={Demo}/>

2.存放位置不同:

一般组件:components

路由组件:pages

3.接收到的props不同:

一般组件:写组件标签时传递了什么,就能收到什么

路由组件:接收到三个固定的属性

history:
    go: ƒ go(n)
    goBack: ƒ goBack()
    goForward: ƒ goForward()
    push: ƒ push(path, state)
    replace: ƒ replace(path, state)

location:
    pathname: "/about"
    search: ""
    state: undefined

match:
    params: {}
    path: "/about"
    url: "/about"

4、NavLink使用与封装

NavLink可以实现路由链接的高亮(用于替换Link标签),通过activeClassName属性指定样式名,默认是"active"

<NavLink activeClassName="demo" className="list-group-item" to="/home">Home</NavLink>

封装 NavLink

import React, { Component } from 'react'
import {NavLink} from 'react-router-dom'

export default class MyNavLink extends Component {
  render() {
    // console.log(this.props);
    return (
      <NavLik activeClassName="demo" className="list-group-item" {...this.props} />
    )
  }
}

标签体内容是特殊的标签属性通过this.props.children可以获取标签体内容

使用封装的组件

<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>

5、Switch标签

  1. 通常情况下,path和component是一一对应的关系。
  2. Switch可以提高路由匹配效率(单一匹配)。这样只要匹配到了第一个就不会再往下匹配了
<Switch>
  <Route path="/about" component={About}/>
  <Route path="/home" component={Home}/>
  <Route path="/home" component={Test}/>
</Switch>

6、多级路径刷新时样式丢失

  1. public/index.html 中 引入样式时不写 .// (常用)【绝对路径】
  2. public/index.html 中 引入样式时不写 ./%PUBLIC_URL% (常用)
  3. 路由标签使用HashRouter,替代BrowserRouter (用得少)

7、严格匹配与模糊匹配

  1. 默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)
  2. 开启严格匹配:<Route exact={true} path="/about" component={About}/>
  3. 严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由

8、Redirect【重定向】

  1. 一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由
  2. 具体编码:
<Switch>
    <Route path="/about" component={About}/>
    <Route path="/home" component={Home}/>
    <Redirect to="/about"/>
</Switch>

9、嵌套路由

1、注册子路由时要写上父路由的path值

<NavLink className="list-group-item" to="/home/news">News</NavLink>
<Route path='/home/news' component={News} />

2、路由的匹配是按照注册路由的顺序进行的

3、不可以开启严格模式

Home 组件

Home组件内部还有嵌套一层路由

import React, { Component } from 'react'
import { Route, NavLink,Redirect,Switch } from 'react-router-dom'
import News from './News'
import Message from './Message'

export default class Home extends Component {
  render() {
    return (
      <div>
        <h3>我是Home的内容</h3>
        <div>
          <ul className="nav nav-tabs">
            <li>
              <NavLink className="list-group-item" to="/home/news">News</NavLink>
            </li>
            <li>
              <NavLink className="list-group-item" to="/home/message">Message</NavLink>
            </li>
          </ul>
          <Switch>
            <Route path='/home/news' component={News} />
            <Route path='/home/message' component={Message} />
            <Redirect to='/home/news' />
          </Switch>
        </div>
      </div>
    )
  }
}

10、向路由组件传参

(1)params参数方式

1、路由链接(携带参数):<Link to='/demo/test/tom/18'}>详情</Link>

2、注册路由(声明接收):<Route path="/demo/test/:name/:age" component={Test}/>

3、接收参数(params是一个对象):this.props.match.params

父组件传递参数

import React, { Component } from 'react'
import { Link, Route } from 'react-router-dom';
import Detail from './Detail';

export default class Message extends Component {
  state = {
    messageArr: [
      { id: '01', title: '消息1' },
      { id: '02', title: '消息2' },
      { id: '03', title: '消息3' },
    ]
  }
  render() {
    const { messageArr } = this.state;
    return (
      <div>
        <ul>
          {
            messageArr.map((msgObj) => {
              return (
                <li key={msgObj.id}>
                  {/* 向路由组件传递params参数 */}
                  <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
                </li>
              )
            })
          }
        </ul>
        <hr />
        {/* 声明接收params参数 */}
        <Route path="/home/message/detail/:id/:title" component={Detail}/>
      </div>
    )
  }
}

子组件接收参数

// 接收params参数
const {id, title} = this.props.match.params

(2)search参数方式

1、路由链接(携带参数):

<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>
    {msgObj.title}
</Link>

2、注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>

3、接收参数:this.props.location.search

​ 接收到的search为字符串 ?name=tom&age=18',需自己处理

4、备注:获取到的search是urlencoded编码字符串,需要借助querystring解析

querystring 使用

import qs from 'querystring'    //react自带,无需下载

// 对象转urlencoded
let obj = {name:'tom', age:18}
console.log(qs.stringify(obj)) // name=tom&age=18

// urlencoded转对象
let str = 'carName=Benz&price=199'
console.log(qs.parse(str)) // {carName: 'Benz', price: 199}

子组件接收参数,并解析

// 接收search参数
const {search} = this.props.location
const {id,title} = qs.parse(search.slice(1))

(3)state参数方式

1、路由链接(携带参数)(to属性为对象):

<Link to={{pathname:'/home/message/detail',state:{id:obj.id,title:obj.title}}}>
    {obj.title}
</Link>

2、注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>

3、接收参数:this.props.location.state

4、备注:不会在地址栏中展示参数数据,刷新也可以保留住参数【history对象记录着】,但是清除浏览器缓存会导致参数丢失,需要注意防止出现 undefind 导致报错

子组件接收参数

const {id,title} = this.props.location.state || {}

const findResult = DetailData.find((detailObj)=>{
    return detailObj.id === id
}) || {}

(4)总结

params 用的比较多

state用于携带比较敏感的信息

传递参数

export default class Message extends Component {
  render() {
    const {messageArr} = this.state
    return (
      <div>
        <ul>
          {
            messageArr.map((msgObj)=>{
              return (
                <li key={msgObj.id}>

                  {/* 向路由组件传递params参数 */}
                  {/* <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link> */}

                  {/* 向路由组件传递search参数 */}
                  {/* <Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link> */}

                  {/* 向路由组件传递state参数 */}
                  <Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link>

                </li>
              )
            })
          }
        </ul>
        <hr/>
        {/* 声明接收params参数 */}
        {/* <Route path="/home/message/detail/:id/:title" component={Detail}/> */}

        {/* search参数无需声明接收,正常注册路由即可 */}
        {/* <Route path="/home/message/detail" component={Detail}/> */}

        {/* state参数无需声明接收,正常注册路由即可 */}
        <Route path="/home/message/detail" component={Detail}/>
      </div>
    )
  }
}

接收参数

import React, { Component } from 'react'
// import qs from 'querystring'

export default class Detail extends Component {
  render() {
    console.log(this.props);

    // 接收params参数
    // const {id,title} = this.props.match.params 

    // 接收search参数
    // const {search} = this.props.location
    // const {id,title} = qs.parse(search.slice(1))

    // 接收state参数
    const {id,title} = this.props.location.state || {}

    const findResult = DetailData.find((detailObj)=>{
      return detailObj.id === id
    }) || {}
    return (
      <ul>
        <li>ID:{id}</li>
        <li>TITLE:{title}</li>
        <li>CONTENT:{findResult.content}</li>
      </ul>
    )
  }
}

11、路由跳转模式

(1)push模式

默认的模式,无需配置

采用压栈的方式,记录每个页面的跳转,可返回上一个页面,即出栈,上一个页面可以是同级路由的页面

(2)replace模式

同样是采用压栈的方式,区别是replace模式当跳转到同级页面时,是采用替换栈顶的方式,所以只能返回上一级路由的页面,不可返回到同级路由页面

只需在NavLink,新增属性replace,即可开启该模式

<NavLink replace to="/home/news" className="list-group-item"/>

12、编程式路由跳转

借助this.prosp.history对象上的API对操作路由跳转、前进、后退

  • this.prosp.history.push() :实现push跳转
  • this.prosp.history.replace() :实现replace跳转
  • this.prosp.history.goBack() :回退
  • this.prosp.history.goForward() :前进
  • this.prosp.history.go() :跳转到指定页面,参数为数字,正数为前进几个页面,负数为返回

push 跳转

//push跳转+携带params参数
this.props.history.push(`/home/message/detail/${id}/${title}`)

//push跳转+携带search参数
this.props.history.push(`/home/message/detail?id=${id}&title=${title}`)

//push跳转+携带state参数
this.props.history.push(`/home/message/detail`,{id,title})

replace 跳转

//replace跳转+携带params参数
this.props.history.replace(`/home/message/detail/${id}/${title}`)

//replace跳转+携带search参数
this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`)

//replace跳转+携带state参数
this.props.history.replace(`/home/message/detail`,{id,title})

前进和回退

this.props.history.goBack()

this.props.history.goForward()

this.props.history.go(-2)

13、一般组件转路由组件

在一些情况下一般组件中可能需要使用路由组件中的API,此时需要把一般组件转化为路由组件,可通过withRouter 实现

  • withRouter 可以加工一般组件,让一般组件具备路由组件所特有的API
  • withRouter 的返回值是一个新组件

使用步骤

1、引入 import {withRouter} from 'react-router-dom'

2、修改暴露组件代码 export default withRouter(Header)

3、此时一般组件可使用路由组件的API

import React, { Component } from 'react'
import {withRouter} from 'react-router-dom'

class Header extends Component {

    back = ()=>{
        this.props.history.goBack()
    }

    forward = ()=>{
        this.props.history.goForward()
    }

    go = ()=>{
        this.props.history.go(-2)
    }

    render() {
        console.log('Header组件收到的props是',this.props);
        return (
            <div className="page-header">
                <h2>React Router Demo</h2>
                <button onClick={this.back}>回退</button>&nbsp;
                <button onClick={this.forward}>前进</button>&nbsp;
                <button onClick={this.go}>go</button>
            </div>
        )
    }
}

export default withRouter(Header)

14、BrowserRouter与HashRouter的区别

1.底层原理不一样:
    BrowserRouter使用的是H5的history API(与 this.props.history不同,this.props.history        是react对H5的history API的封装),不兼容IE9及以下版本。
    HashRouter使用的是URL的哈希值。
2.path表现形式不一样
    BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
    HashRouter的路径包含#,例如:localhost:3000/#/demo/test
3.刷新后对路由state参数的影响
    (1).BrowserRouter没有任何影响,因为state保存在history对象中。
    (2).HashRouter刷新后会导致路由state参数的丢失!!!
4.备注:HashRouter可以用于解决一些路径错误相关的问题。

七、Ant Design

蚂蚁金服提供的React第三方ui组件库,中文官网:

https://ant-design.gitee.io/components/overview-cn/

引入方式

npm add antd

1、按需引入

需要对 create-react-app 的默认配置进行自定义,这里我们使用 react-app-rewired (一个对 create-react-app 进行自定义配置的社区解决方案)。

引入 react-app-rewired 并修改 package.json 里的启动配置。由于新的 react-app-rewired@2.x 版本的关系,你还需要安装 customize-cra

npm add react-app-rewired customize-cra

修改 package.json(将以下三行替换 )

/* package.json */
"scripts": {
    /* 删除 */
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",

    /* 新增 */
    "start": "react-app-rewired start",    
    "build": "react-app-rewired build",    
    "test": "react-app-rewired test"
}

然后在项目根目录创建一个 config-overrides.js 用于修改默认配置。

module.exports = function override(config, env) {
  // do stuff with the webpack config...
  return config;
};

使用 babel-plugin-import

注意:antd 默认支持基于 ES module 的 tree shaking,js 代码部分不使用这个插件也会有按需加载的效果。

babel-plugin-import 是一个用于按需加载组件代码和样式的 babel 插件(原理),现在我们尝试安装它并修改 config-overrides.js 文件。

yarn add babel-plugin-import

config-overrides.js

const { override, fixBabelImports } = require('customize-cra');

module.exports = override(
    fixBabelImports('import', {
        libraryName: 'antd',
        libraryDirectory: 'es',
        style: 'css',
    }),
);

然后移除前面在 src/App.css 里全量添加的 @import '~antd/dist/antd.css'; 样式代码,并且按下面的格式引入模块。

  // src/App.js
  import React, { Component } from 'react';
- import Button from 'antd/es/button';
+ import { Button } from 'antd';
  import './App.css';

  class App extends Component {
    render() {
      return (
        <div className="App">
          <Button type="primary">Button</Button>
        </div>
      );
    }
  }

  export default App;

最后重启 yarn start 访问页面,antd 组件的 js 和 css 代码都会按需加载,你在控制台也不会看到这样的警告信息。关于按需加载的原理和其他方式可以阅读这里

2、国际化

index.js,中引入以下2个import语句,并用<ConfigProvider locale={zhCN}>包裹 app组件

import zhCN from 'antd/lib/locale/zh_CN';
import { ConfigProvider } from 'antd';

ReactDOM.render(
    <ConfigProvider locale={zhCN}>
        <App />
    </ConfigProvider>,
  document.getElementById('root')
);

八、redux

1、简介

文档

中文文档: http://www.redux.org.cn/

Github: https://github.com/reactjs/redux

下载

npm add redux

是什么

  1. redux是一个专门用于做状态管理的JS库(不是react插件库)。
  2. 可以用在react, angular, vue等项目中, 但基本与react配合使用。
  3. 作用: 集中式管理react应用中多个组件共享的状态。

之前学过vuex,这两个差不多,都是状态管理用的

使用场景

  1. 某个组件的状态,需要让其他组件可以随时拿到(共享)
  2. 一个组件需要改变另一个组件的状态(通信)
  3. 总体原则:能不用就不用, 如果不用比较吃力才考虑使用

工作流程

2、redux的三个核心概念

(1)action

动作的对象

包含 2 个属性

  • type:标识属性, 值为字符串, 唯一, 必要属性

  • data:数据属性, 值类型任意, 可选属性

例子:{ type: ‘ADD_STUDENT’,data:{name: ‘tom’,age:18} }

(2)reducer

用于初始化状态、加工状态。

加工时,根据旧的state和action, 产生新的state的纯函数。

(3)store

将state、action、reducer联系在一起的对象

如何得到此对象?

import {createStore} from 'redux'
import reducer from './reducers'
const store = createStore(reducer)

此对象的功能?

getState(): 得到state
dispatch(action): 分发action, 触发reducer调用, 产生新的state
subscribe(listener): 注册监听, 当产生了新的state时, 自动调用

3、API

(1)createStore()

作用:创建包含指定reducer的store对象

(2)store对象

① 作用

redux库最核心的管理对象

它内部维护着:

  • state

  • reducer

② 核心方法

getState()    //用于获取state
dispatch(action)    //执行store中的方法,修改状态
subscribe(listener)        //订阅redux中的状态改变

③ 具体编码

store.getState()
store.dispatch({type:'INCREMENT', number})
store.subscribe(render)

(3)applyMiddleware()

作用:应用上基于redux的中间件(插件库)

(4)combineReducers()

作用:合并多个reducer函数

4、求和案例

实现步骤

(1).去除Count组件自身的状态
(2).src下建立:
            -redux
               -store.js
               -count_reducer.js
               -count_action.js 专门用于创建action对象
               -constant.js 存放常量
(3).store.js:
         1).引入redux中的createStore函数,创建一个store
         2).createStore调用时要传入一个为其服务的reducer
         3).记得暴露store对象

(4).count_reducer.js:
         1).reducer的本质是一个函数,接收:preState,action,返回加工后的状态
         2).reducer有两个作用:初始化状态,加工状态
         3).reducer被第一次调用时,是store自动触发的,
                     传递的preState是undefined,
                     传递的action是:{type:'@@REDUX/INIT_a.2.b.4}

(5).在根目录index.js中监测store中状态的改变,一旦发生改变重新渲染App组件
      备注:redux只负责管理状态,至于状态的改变驱动着页面的展示,要靠我们自己写。

(6)在自己的组件中调用 store.dispatch({type:'increment',data:value*1}) 更新store中的状态  

(1)store.js

/* 
    该文件专门用于暴露一个store对象,整个应用只有一个store对象
*/

//引入createStore,专门用于创建redux中最为核心的store对象
import {createStore} from 'redux'
//引入为Count组件服务的reducer
import countReducer from './count_reducer'
//暴露store
export default createStore(countReducer)

(2)count_redux.js

/* 
    1.该文件是用于创建一个为Count组件服务的reducer,reducer的本质就是一个函数
    2.reducer函数会接到两个参数,分别为:之前的状态(preState),动作对象(action)
*/
import {INCREMENT,DECREMENT} from './constant'

const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
    // console.log(preState);
    //从action对象中获取:type、data
    const {type,data} = action
    //根据type决定如何加工数据
    switch (type) {
        case INCREMENT: //如果是加
            return preState + data
        case DECREMENT: //若果是减
            return preState - data
        default:
            return preState
    }
}

(3)count_action.js

/* 
    该文件专门为Count组件生成action对象
*/
import {INCREMENT,DECREMENT} from './constant'

export const createIncrementAction = data => ({type:INCREMENT,data})
export const createDecrementAction = data => ({type:DECREMENT,data})

(4)constant.js

/* 
    该模块是用于定义action对象中type类型的常量值,目的只有一个:便于管理的同时防止程序员单词写错
*/
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'

(5)index.js

根路径下的 index.js

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './redux/store'

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

store.subscribe(()=>{
    ReactDOM.render(<App/>,document.getElementById('root'))
})

(6)自定义组件中使用

import React, { Component } from 'react'
//引入store,用于获取redux中保存状态
import store from '../../redux/store'
//引入actionCreator,专门用于创建action对象
import {createIncrementAction,createDecrementAction} from '../../redux/count_action'

export default class Count extends Component {

    state = {carName:'奔驰c63'}

    //加法
    increment = ()=>{
        const {value} = this.selectNumber
        store.dispatch(createIncrementAction(value*1))
    }
    //减法
    decrement = ()=>{
        const {value} = this.selectNumber
        store.dispatch(createDecrementAction(value*1))
    }
    //奇数再加
    incrementIfOdd = ()=>{
        const {value} = this.selectNumber
        const count = store.getState()
        if(count % 2 !== 0){
            store.dispatch(createIncrementAction(value*1))
        }
    }
    //异步加
    incrementAsync = ()=>{
        const {value} = this.selectNumber
        setTimeout(()=>{
            store.dispatch(createIncrementAction(value*1))
        },500)
    }

    render() {
        return (
            <div>
                <h1>当前求和为:{store.getState()}</h1>
                <select ref={c => this.selectNumber = c}>
                    <option value="1">1</option>
                    <option value="2">2</option>
                    <option value="3">3</option>
                </select>&nbsp;
                <button onClick={this.increment}>+</button>&nbsp;
                <button onClick={this.decrement}>-</button>&nbsp;
                <button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>&nbsp;
                <button onClick={this.incrementAsync}>异步加</button>&nbsp;
            </div>
        )
    }
}

5、异步Action

(1).明确:延迟的动作不想交给组件自身,想交给action
(2).何时需要异步action:想要对状态进行操作,但是具体的数据靠异步任务返回。
(3).具体编码:
         1).yarn add redux-thunk,并配置在store中
         2).创建action的函数不再返回一般对象,而是一个函数,该函数中写异步任务。
         3).异步任务有结果后,分发一个同步的action去真正操作数据。
(4).备注:异步action不是必须要写的,完全可以自己等待异步任务的结果了再去分发同步action。

(1)store.js

/* 
    该文件专门用于暴露一个store对象,整个应用只有一个store对象
*/

//引入createStore,专门用于创建redux中最为核心的store对象
import {createStore,applyMiddleware} from 'redux'
//引入为Count组件服务的reducer
import countReducer from './count_reducer'
//引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'
//暴露store
export default createStore(countReducer,applyMiddleware(thunk))

(2)count_action.js

/* 
    该文件专门为Count组件生成action对象
*/
import {INCREMENT,DECREMENT} from './constant'

//同步action,就是指action的值为Object类型的一般对象
export const createIncrementAction = data => ({type:INCREMENT,data})
export const createDecrementAction = data => ({type:DECREMENT,data})

//异步action,就是指action的值为函数,异步action中一般都会调用同步action,异步action不是必须要用的。
export const createIncrementAsyncAction = (data,time) => {
    return (dispatch)=>{
        setTimeout(()=>{
            dispatch(createIncrementAction(data))
        },time)
    }
}

(3)异步调用

在自己的组件中调用即可将异步任务交给redux完成

store.dispatch(createIncrementAsyncAction(value*1,500))

九、react-redux

1、简介

是什么

  1. 一个React插件库
  2. 专门用来简化React应用中使用redux

下载安装

npm install react-redux

UI组件

  • 只负责 UI 的呈现,不带有任何业务逻辑

  • 通过props接收数据(一般数据和函数)

  • 不使用任何 Redux 的 API

  • 一般保存在components文件夹下

容器组件

  • 负责管理数据和业务逻辑,不负责UI的呈现
  • 使用 Redux 的 API
  • 一般保存在containers文件夹下

2、相关API

  • Provider:让所有组件都可以得到state数据

  • connect:用于包装 UI 组件生成容器组件

  • mapStateToprops:将外部的数据(即state对象)转换为UI组件的标签属性

  • mapDispatchToProps:将分发action的函数转换为UI组件的标签属性

3、基本使用

(1).明确两个概念:
        1).UI组件:不能使用任何redux的api,只负责页面的呈现、交互等。
        2).容器组件:负责和redux通信,将结果交给UI组件。
(2).如何创建一个容器组件————靠react-redux 的 connect函数
        connect(mapStateToProps,mapDispatchToProps)(UI组件)
            -mapStateToProps:映射状态,返回值是一个对象
            -mapDispatchToProps:映射操作状态的方法,返回值是一个对象
(3).备注1:容器组件中的store是靠props传进去的,而不是在容器组件中直接引入
(4).备注2:mapDispatchToProps,也可以是一个对象

(1)下载安装

npm install react-redux

(2)components/Count/index.jsx

UI组件

import React, { Component } from 'react'

export default class Count extends Component {

    state = {carName:'奔驰c63'}

    //加法
    increment = ()=>{
        const {value} = this.selectNumber
        this.props.jia(value*1)
    }
    //减法
    decrement = ()=>{
        const {value} = this.selectNumber
        this.props.jian(value*1)
    }
    //奇数再加
    incrementIfOdd = ()=>{
        const {value} = this.selectNumber
        if(this.props.count % 2 !== 0){
            this.props.jia(value*1)
        }
    }
    //异步加
    incrementAsync = ()=>{
        const {value} = this.selectNumber
        this.props.jiaAsync(value*1,500)
    }

    render() {
        //console.log('UI组件接收到的props是',this.props);
        return (
            <div>
                <h1>当前求和为:{this.props.count}</h1>
                <select ref={c => this.selectNumber = c}>
                    <option value="1">1</option>
                    <option value="2">2</option>
                    <option value="3">3</option>
                </select>&nbsp;
                <button onClick={this.increment}>+</button>&nbsp;
                <button onClick={this.decrement}>-</button>&nbsp;
                <button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>&nbsp;
                <button onClick={this.incrementAsync}>异步加</button>&nbsp;
            </div>
        )
    }
}

(3)containers/Count/index.jsx

容器组件

//引入Count的UI组件
import CountUI from '../../components/Count'
//引入action
import {
    createIncrementAction,
    createDecrementAction,
    createIncrementAsyncAction
} from '../../redux/count_action'

//引入connect用于连接UI组件与redux
import {connect} from 'react-redux'

/* 
    1.mapStateToProps函数返回的是一个对象;
    2.返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value
    3.mapStateToProps用于传递状态
*/
function mapStateToProps(state){
    return {count:state}
}

/* 
    1.mapDispatchToProps函数返回的是一个对象;
    2.返回的对象中的key就作为传递给UI组件props的key,value就作为传递给UI组件props的value
    3.mapDispatchToProps用于传递操作状态的方法
*/
function mapDispatchToProps(dispatch){
    return {
        jia:number => dispatch(createIncrementAction(number)),
        jian:number => dispatch(createDecrementAction(number)),
        jiaAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time)),
    }
}

//使用connect()()创建并暴露一个Count的容器组件
export default connect(mapStateToProps,mapDispatchToProps)(CountUI)

(4)App.jsx

给容器组件传递store

import React, { Component } from 'react'
import Count from './containers/Count'
import store from './redux/store'

export default class App extends Component {
  render() {
    return (
      <div>
        {/* 给容器组件传递store */}
        <Count store={store} />
      </div>
    )
  }
}

4、优化

(1)简写mapDispatchToProps

//使用connect()()创建并暴露一个Count的容器组件
export default connect(
  state => ({count:state}),

  //mapDispatchToProps的一般写法
  /* dispatch => ({
    jia:number => dispatch(createIncrementAction(number)),
    jian:number => dispatch(createDecrementAction(number)),
    jiaAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time)),
  }) */

  //mapDispatchToProps的简写
  {
    jia:createIncrementAction,
    jian:createDecrementAction,
    jiaAsync:createIncrementAsyncAction,
  }
)(CountUI)

(2)Provider

容器组件可以检测redux中的状态改变,并渲染页面,所以不需要在index.js中检测了
不要在App.jsx中给子组件传递store了

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './redux/store'
import {Provider} from 'react-redux'

ReactDOM.render(
    <Provider store={store}>
        <App/>
    </Provider>,
    document.getElementById('root')
)

(3)整合UI和容器组件

每个组件两个文件夹太麻烦了,直接整合在一起就好了~

containers/Count/index.jsx

import React, { Component } from 'react'
//引入action
import {
  createIncrementAction,
  createDecrementAction,
  createIncrementAsyncAction
} from '../../redux/count_action'
//引入connect用于连接UI组件与redux
import {connect} from 'react-redux'

//定义UI组件
class Count extends Component {

  state = {carName:'奔驰c63'}

  //加法
  increment = ()=>{
    const {value} = this.selectNumber
    this.props.jia(value*1)
  }
  //减法
  decrement = ()=>{
    const {value} = this.selectNumber
    this.props.jian(value*1)
  }
  //奇数再加
  incrementIfOdd = ()=>{
    const {value} = this.selectNumber
    if(this.props.count % 2 !== 0){
      this.props.jia(value*1)
    }
  }
  //异步加
  incrementAsync = ()=>{
    const {value} = this.selectNumber
    this.props.jiaAsync(value*1,500)
  }

  render() {
    //console.log('UI组件接收到的props是',this.props);
    return (
      <div>
        <h1>当前求和为:{this.props.count}</h1>
        <select ref={c => this.selectNumber = c}>
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
        </select>&nbsp;
        <button onClick={this.increment}>+</button>&nbsp;
        <button onClick={this.decrement}>-</button>&nbsp;
        <button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>&nbsp;
        <button onClick={this.incrementAsync}>异步加</button>&nbsp;
      </div>
    )
  }
}

//使用connect()()创建并暴露一个Count的容器组件
export default connect(
  state => ({count:state}),

  //mapDispatchToProps的一般写法
  /* dispatch => ({
    jia:number => dispatch(createIncrementAction(number)),
    jian:number => dispatch(createDecrementAction(number)),
    jiaAsync:(number,time) => dispatch(createIncrementAsyncAction(number,time)),
  }) */

  //mapDispatchToProps的简写
  {
    jia:createIncrementAction,
    jian:createDecrementAction,
    jiaAsync:createIncrementAsyncAction,
  }
)(Count)

5、开发者工具

1、谷歌浏览器安装Redux Dev Tools插件

2、npm install redux-devtools-extension

3、在store中进行配置

import {composeWithDevTools} from 'redux-devtools-extension'
const store = createStore(allReducer,composeWithDevTools(applyMiddleware(thunk)))

6、最终版

image-20211005153039250

(1)App.jsx

import React, { Component } from 'react'
import Count from './containers/Count' //引入的Count的容器组件
import Person from './containers/Person' //引入的Person的容器组件

export default class App extends Component {
    render() {
        return (
            <div>
                <Count/>
                <hr/>
                <Person/>
            </div>
        )
    }
}

(2)src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './redux/store'
import {Provider} from 'react-redux'

ReactDOM.render(
    /* 此处需要用Provider包裹App,目的是让App所有的后代容器组件都能接收到store */
    <Provider store={store}>
        <App/>
    </Provider>,
    document.getElementById('root')
)

(3)redux

store.js

/* 
    该文件专门用于暴露一个store对象,整个应用只有一个store对象
*/

//引入createStore,专门用于创建redux中最为核心的store对象
import {createStore,applyMiddleware} from 'redux'
//引入汇总之后的reducer
import reducer from './reducers'
//引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'
//引入redux-devtools-extension
import {composeWithDevTools} from 'redux-devtools-extension'

//暴露store 
export default createStore(reducer,composeWithDevTools(applyMiddleware(thunk)))

constant.js

/* 
    该模块是用于定义action对象中type类型的常量值,目的只有一个:便于管理的同时防止程序员单词写错
*/
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
export const ADD_PERSON = 'add_person'

reducers/index.js

/* 
    该文件用于汇总所有的reducer为一个总的reducer
*/
//引入combineReducers,用于汇总多个reducer
import {combineReducers} from 'redux'
//引入为Count组件服务的reducer
import count from './count'
//引入为Person组件服务的reducer
import persons from './person'

//汇总所有的reducer变为一个总的reducer
export default  combineReducers({
    count,
    persons
})

reducers/count.js

/* 
    1.该文件是用于创建一个为Count组件服务的reducer,reducer的本质就是一个函数
    2.reducer函数会接到两个参数,分别为:之前的状态(preState),动作对象(action)
*/
import {INCREMENT,DECREMENT} from '../constant'

const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
    // console.log('countReducer@#@#@#');
    //从action对象中获取:type、data
    const {type,data} = action
    //根据type决定如何加工数据
    switch (type) {
        case INCREMENT: //如果是加
            return preState + data
        case DECREMENT: //若果是减
            return preState - data
        default:
            return preState
    }
}

reducers/person.js

import {ADD_PERSON} from '../constant'

//初始化人的列表
const initState = [{id:'001',name:'tom',age:18}]

export default function personReducer(preState=initState,action){
    // console.log('personReducer@#@#@#');
    const {type,data} = action
    switch (type) {
        case ADD_PERSON: //若是添加一个人
            //preState.unshift(data) //此处不可以这样写,这样会导致preState被改写了,personReducer就不是纯函数了。
            return [data,...preState]
        default:
            return preState
    }
}

actions/count.js

/* 
    该文件专门为Count组件生成action对象
*/
import {INCREMENT,DECREMENT} from '../constant'

//同步action,就是指action的值为Object类型的一般对象
export const increment = data => ({type:INCREMENT,data})
export const decrement = data => ({type:DECREMENT,data})

//异步action,就是指action的值为函数,异步action中一般都会调用同步action,异步action不是必须要用的。
export const incrementAsync = (data,time) => {
    return (dispatch)=>{
        setTimeout(()=>{
            dispatch(increment(data))
        },time)
    }
}

actions/person.js

import {ADD_PERSON} from '../constant'

//创建增加一个人的action动作对象
export const addPerson = personObj => ({type:ADD_PERSON,data:personObj})

(4)containers

Count/index.jsx

import React, { Component } from 'react'
//引入action
import {
    increment,
    decrement,
    incrementAsync
} from '../../redux/actions/count'
//引入connect用于连接UI组件与redux
import {connect} from 'react-redux'

//定义UI组件
class Count extends Component {

    state = {carName:'奔驰c63'}

    //加法
    increment = ()=>{
        const {value} = this.selectNumber
        this.props.increment(value*1)
    }
    //减法
    decrement = ()=>{
        const {value} = this.selectNumber
        this.props.decrement(value*1)
    }
    //奇数再加
    incrementIfOdd = ()=>{
        const {value} = this.selectNumber
        if(this.props.count % 2 !== 0){
            this.props.increment(value*1)
        }
    }
    //异步加
    incrementAsync = ()=>{
        const {value} = this.selectNumber
        this.props.incrementAsync(value*1,500)
    }

    render() {
        //console.log('UI组件接收到的props是',this.props);
        return (
            <div>
                <h2>我是Count组件,下方组件总人数为:{this.props.renshu}</h2>
                <h4>当前求和为:{this.props.count}</h4>
                <select ref={c => this.selectNumber = c}>
                    <option value="1">1</option>
                    <option value="2">2</option>
                    <option value="3">3</option>
                </select>&nbsp;
                <button onClick={this.increment}>+</button>&nbsp;
                <button onClick={this.decrement}>-</button>&nbsp;
                <button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>&nbsp;
                <button onClick={this.incrementAsync}>异步加</button>&nbsp;
            </div>
        )
    }
}

//使用connect()()创建并暴露一个Count的容器组件
export default connect(
    state => ({
        count:state.count,
        personCount:state.persons.length
    }),
    {increment,decrement,incrementAsync}
)(Count)

Person/index.jsx

import React, { Component } from 'react'
import {nanoid} from 'nanoid'
import {connect} from 'react-redux'
import {addPerson} from '../../redux/actions/person'

class Person extends Component {

    addPerson = ()=>{
        const name = this.nameNode.value
        const age = this.ageNode.value*1
        const personObj = {id:nanoid(),name,age}
        this.props.addPerson(personObj)
        this.nameNode.value = ''
        this.ageNode.value = ''
    }

    render() {
        return (
            <div>
                <h2>我是Person组件,上方组件求和为{this.props.count}</h2>
                <input ref={c=>this.nameNode = c} type="text" placeholder="输入名字"/>
                <input ref={c=>this.ageNode = c} type="text" placeholder="输入年龄"/>
                <button onClick={this.addPerson}>添加</button>
                <ul>
                    {
                        this.props.persons.map((p)=>{
                            return <li key={p.id}>{p.name}--{p.age}</li>
                        })
                    }
                </ul>
            </div>
        )
    }
}

export default connect(
    state => ({
        persons:state.persons,
        count:state.count
    }),//映射状态
    {addPerson}//映射操作状态的方法
)(Person)

十、扩展

1. setState

注意:setState 是异步更新,在调用之后立即获取state中的数据,获取不到更新后的数据,要获取的话需要在参数后加一个后调函数,如下

setState更新状态的2种写法

    (1). setState(stateChange, [callback])------对象式的setState
            1.stateChange为状态改变对象(该对象可以体现出状态的更改)
            2.callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用

    (2). setState(updater, [callback])------函数式的setState
            1.updater为返回stateChange对象的函数。
            2.updater可以接收到state和props。
            4.callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。
总结:
        1.对象式的setState是函数式的setState的简写方式(语法糖)
        2.使用原则:
                (1).如果新状态不依赖于原状态 ===> 使用对象方式
                (2).如果新状态依赖于原状态 ===> 使用函数方式
                (3).如果需要在setState()执行后获取最新的状态数据, 
                    要在第二个callback函数中读取
add = ()=>{
    //对象式的setState
    //1.获取原来的count值
    const {count} = this.state
    //2.更新状态
    this.setState({count:count+1},()=>{
        console.log(this.state.count);
    })
    //console.log('12行的输出',this.state.count); //0 

    //函数式的setState
    this.setState( state => ({count:state.count+1}))
}

2. lazyLoad

路由组件的lazyLoad,实现按需加载,提高性能

//1.引入 lazy、Suspense
import React, { Component,lazy,Suspense} from 'react'
import {NavLink,Route} from 'react-router-dom'

import Loading from './Loading'

// import Home from './Home'
// import About from './About'

//2.替换上面注释的两行
//通过React的lazy函数配合import()函数动态加载路由组件 ===> 路由组件代码会被分开打包
const Home = lazy(()=> import('./Home') )
const About = lazy(()=> import('./About'))

// ....

//3.通过<Suspense>指定在加载得到路由打包文件前显示一个自定义loading界面
<Suspense fallback={<Loading/>}>
    {/* 注册路由 */}
    <Route path="/about" component={About}/>
    <Route path="/home" component={Home}/>
</Suspense>

3. Hooks

(1)是什么?

(1). Hook是React 16.8.0版本增加的新特性/新语法
(2). 可以让你在函数组件中使用 state 以及其他的 React 特性

(2)三个常用的Hook

(1). State Hook: React.useState()
(2). Effect Hook: React.useEffect()
(3). Ref Hook: React.useRef()

(3)State Hook

(1). State Hook让函数组件也可以有state状态, 并进行状态数据的读写操作
(2). 语法: const [xxx, setXxx] = React.useState(initValue)  
(3). useState()说明:
        参数: 第一次初始化指定的值在内部作缓存,也就是默认值
        返回值: 包含2个元素的数组, 第1个为内部当前状态值, 第2个为更新状态值的函数
(4). setXxx()2种写法:
        setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值
        setXxx(value => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值
const [count,setCount] = React.useState(0)

//加的回调
function add(){
    //setCount(count+1) //第一种写法
    setCount(count => count+1 )
}

(4)Effect Hook

(1). Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
(2). React中的副作用操作:
        发ajax请求数据获取
        设置订阅 / 启动定时器
        手动更改真实DOM
(3). 语法和说明: 
        useEffect(() => { 
          // 在此可以执行任何带副作用操作
          return () => { // 在组件卸载前执行
            // 在此做一些收尾工作, 比如清除定时器/取消订阅等
          }
        }, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行

(4).useEffect(() => {},[]) 参数说明:
        1、回调函数,调用时机由第二个参数决定
        2、监测对象
            1) 数组为空时,不监测数据,仅在挂载时调回调函数
            2) 数组不为空时,监测指定数据,会在数据改变、挂载时,调用回调函数
            3) 第二个参数省略时,监测所有数据,会在数据改变、挂载时,调用回调函数

(5). 可以把 useEffect Hook 看做如下三个函数的组合
        componentDidMount()
        componentDidUpdate()
        componentWillUnmount() 

    React.useEffect(()=>{
        let timer = setInterval(()=>{
            setCount(count => count+1 )
        },1000)
        return ()=>{
            clearInterval(timer)
        }
    },[])

(5)Ref Hook

(1). Ref Hook可以在函数组件中存储/查找组件内的标签或任意其它数据
(2). 语法: const refContainer = useRef()
(3). 作用:保存标签对象,功能与React.createRef()一样
    //提示输入的回调
    function show(){
        alert(myRef.current.value)
    }

<input type="text" ref={myRef}/>
<button onClick={show}>点我提示数据</button>

(6)代码

function Demo(){
    //console.log('Demo');

    const [count,setCount] = React.useState(0)
    const myRef = React.useRef()

    React.useEffect(()=>{
        let timer = setInterval(()=>{
            setCount(count => count+1 )
        },1000)
        return ()=>{
            clearInterval(timer)
        }
    },[])

    //加的回调
    function add(){
        //setCount(count+1) //第一种写法
        setCount(count => count+1 )
    }

    //提示输入的回调
    function show(){
        alert(myRef.current.value)
    }

    //卸载组件的回调
    function unmount(){
        ReactDOM.unmountComponentAtNode(document.getElementById('root'))
    }

    return (
        <div>
            <input type="text" ref={myRef}/>
            <h2>当前求和为:{count}</h2>
            <button onClick={add}>点我+1</button>
            <button onClick={unmount}>卸载组件</button>
            <button onClick={show}>点我提示数据</button>
        </div>
    )
}

export default Demo

4. Fragment

当不得不用一个没有实际作用的标签去包裹代码时,可以使用Fragment或空标签,该标签不会在前端中渲染,为的是骗过jsx的语法检测。

Fragment或空标签,的区别是Fragment可以有一个属性key,可用于遍历,而空标签不能有属性,Fragment只能使用key属性,不能有其他属性

<Fragment><Fragment>
<></>

作用:可以不用必须有一个真实的DOM根标签了

5. Context

理解

一种组件间通信方式, 常用于【祖组件】与【后代组件】间通信

使用

1) 创建Context容器对象:(写在组件外)
    const XxxContext = React.createContext()  

2) 渲染子组时,外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:
    <xxxContext.Provider value={数据}>
        子组件
    </xxxContext.Provider>

3) 后代组件读取数据:

    //第一种方式:仅适用于类组件 
      static contextType = xxxContext  // 声明接收context
      this.context // 读取context中的value数据

    //第二种方式: 函数组件与类组件都可以
      <xxxContext.Consumer>
          {value => `${value.username},年龄是${value.age}`}
      </xxxContext.Consumer>

注意

在应用开发中一般不用context, 一般都用它的封装react插件

6. PureComponent

Component的2个问题

  1. 只要执行setState(),即使不改变状态数据, 组件也会重新render() ==> 效率低

  2. 只当前组件重新render(), 就会自动重新render子组件,纵使子组件没有用到父组件的任何数据 ==> 效率低

效率高的做法

只有当组件的state或props数据发生改变时才重新render()

原因

Component中的shouldComponentUpdate()总是返回true

解决

办法1: 
    重写shouldComponentUpdate()方法
    比较新旧state或props数据, 如果有变化才返回true, 如果没有返回false
办法2:  
    使用PureComponent,类继承使用PureComponent而不是component
    PureComponent重写了shouldComponentUpdate(), 只有state或props数据有变化才返回true
    注意: 
        只是进行state和props数据的浅比较, 如果只是数据对象内部数据变了, 返回false  
        不要直接修改state数据, 而是要产生新数据
项目中一般使用PureComponent来优化
export default class Parent extends PureComponent {}

7. render props

如何向组件内部动态传入带内容的结构(标签)?

Vue中: 
    使用slot(插槽)技术, 也就是通过组件标签体传入结构  <A><B/></A>
React中:
    使用children props: 通过组件标签体传入结构
    使用render props: 通过组件标签属性传入结构,而且可以携带数据,一般用render函数属性

就是说在子组件中预留一个位置,父组件可以通过render props,动态设置子组件中该位置渲染的组件,而不需要改变子组件

children props

<A>
  <B>xxxx</B>
</A>
{this.props.children}
问题: 如果B组件需要A组件内的数据, ==> 做不到 

render props

<A render={(data) => <C data={data}></C>}></A>
A组件(预留组件位置): {this.props.render(内部state数据)}
C组件: 读取A组件传入的数据显示 {this.props.data} 

只改变父组件Parent 的代码即可动态渲染 子组件A 中预留区域的组件,可更换为其他组件等。

import React, { Component } from 'react'
import './index.css'
import C from '../1_setState'

export default class Parent extends Component {
    render() {
        return (
            <div className="parent">
                <h3>我是Parent组件</h3>
                <A render={(name)=><C name={name}/>}/>
            </div>
        )
    }
}

class A extends Component {
    state = {name:'tom'}
    render() {
        console.log(this.props);
        const {name} = this.state
        return (
            <div className="a">
                <h3>我是A组件</h3>
                {/*预留组件位置*/}
                {this.props.render(name)}
            </div>
        )
    }
}

class B extends Component {
    render() {
        console.log('B--render');
        return (
            <div className="b">
                <h3>我是B组件,{this.props.name}</h3>
            </div>
        )
    }
}

8. 错误边界

理解:

错误边界(Error boundary):用来捕获后代组件错误,渲染出备用页面,避免因为一个组件出错而导致所有页面无法展示。

特点:

只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误

使用方式:

getDerivedStateFromError配合componentDidCatch

注意:错误边界只在生产环境中有效,开发环境只会展示一会儿备用页面,然后还是会报错。

// 生命周期函数,一旦后台组件报错,就会触发
static getDerivedStateFromError(error) {
    console.log(error);
    // 在render之前触发
    // 返回新的state
    return {
        hasError: true,
    };
}

// 子组件报错时调用,也是一个生命周期函数
componentDidCatch(error, info) {
    // 统计页面的错误。发送请求发送到后台去
    console.log(error, info);
}

十一、操作缓存和cookie

1、操作localStorage

除非被清除,否则永久保存
大小一般为5MB
仅在客户端(即浏览器)中保存,不参与和服务器的通信
存在 XSS 注入的风险,只要打开控制台,就可以随意修改它们的值

(1)下载引入

npm install localStorage --save

# 如果使用typescript ,还得引入下面依赖
npm i --save-dev @types/localStorage

引入

// 引入
import localStorage from "localStorage";

(2)存

localStorage.setItem("phone", "123")

//对象
let obj = {"name":"xiaoming", "age":"16"}
localStorage.setItem("phone", JSON.stringify(obj));

(3)取

localStorage.getItem("phone")

//对象
let user = JSON.parse(localStorage.getItem("phone"))

(4)删

//指定删
localStorage.removeItem('phone');

//全删
localStorage.clear(); 

(5)设置过期时间

//设置缓存,设置localStorageSet 过期时间
const localStorageSet = (name, data) => {
    const obj = {
        data,
        expire: new Date().getTime() + 1000 * 60 * 30
    };
    localStorage.setItem(name, JSON.stringify(obj));
};

//读取缓存,且比较时间戳是否过期
const localStorageGet = name => {
    const storage = localStorage.getItem(name);
    const time = new Date().getTime();
    let result = null;
    if (storage) {
        const obj = JSON.parse(storage);
        if (time < obj.expire) {
            result = obj.data;
        } else {
            localStorage.removeItem(name);
        }
    }
    return result;
};

2、操作Cookie

可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效
大小4K左右
每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题
存在 XSS 注入的风险,只要打开控制台,就可以随意修改它们的值

1、下载依赖

cnpm install  react-cookies --save-dev

2、引入

import cookie from 'react-cookies'

3、存

cookie.save('userId', "123")

4、取

cookie.load('userId')

5、删

cookie.remove('userId')

6、设置失效

let inFifteenMinutes = new Date(new Date().getTime() + 24 * 3600 * 1000);//一天
cookie.save('userId', "123",{ expires: inFifteenMinutes })

  目录