React学习(二)——react中的路由

React 学习

React 路由

现代的前端应用大多都是 SPA(单页应用程序),也就是只有一个 HTML 页面的应用程序。因为它的用户体
验更好、对服务器的压力更小,所以更受欢迎。为了有效的使用单个页面来管理原来多页面的功能,前端路由
应运而生。

  • 前端路由的功能:让用户从一个视图(页面)导航到另一个视图(页面)
  • 前端路由是一套映射规则,在 React 中,是 URL 路径 与 组件 的对应关系
  • 使用 React 路由简单来说,就是配置 路径和组件(配对)

React 路由使用

  1. 安装依赖
npm i react-router-dom
## 或者
yarn add react-router-dom
  1. 导入路由组件
// 使用 H5 的 history API 实现(localhost:3000/first)
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
// 使用 URL 的哈希值实现(localhost:3000/#/first)
import { HashRouter as Router, Route, Link } from "react-router-dom";
  • React 路由有两种模式:
    1. 使用哈希值实现(hash): HashRouter
    2. 使用 H5 中的 history API: BrowserRouter(推荐)
  1. 使用 Router 包裹应用

整个应用使用一次即可!也就是用 Router 包裹整个应用


    
.....
  1. 使用 Link 组件定义导航菜单

这个组件最终会被渲染成一个 a 标签

to 属性表示:浏览器地址栏中的 pathname(location.pathname)

主页
  1. 使用 Route 组件配置路由规则和要展示的组件(路由出口)
  • Route 组件:配置路由规则以及要展示的组件
    • path 属性表示:路由规则
    • component 属性表示:该路由规则匹配时要展示的组件

Route 组件放在页面中哪个位置,那么,组件的内容就会展示在哪个地方

// 定义一个Home组件
const Home = () => 

这是主页

// 匹配到的路由显示对应的组件内容
import { BrowserRouter as Router, Route, Link } from "react-router-dom";

// 定义两个组件
const Home = () => <h1>这是主页</h1>;
const Login = () => <h1>这是登录页</h1>;

class Hello extends React.PureComponent {
    render() {
        return (
            <Router>
                <div>
                    <Link to="/home">主页</Link>
                    <br />
                    <Link to="/login">登录页</Link>
                    <Route path="/home" component={Home} />
                    <Route path="/login" component={Login} />
                </div>
            </Router>
        );
    }
}

路由执行过程

  1. 点击 Link 组件(a 标签),修改了浏览器地址栏中的 url 。
  2. React 路由监听到地址栏 url 的变化。
  3. React 路由内部遍历所有 Route 组件,使用路由规则( path )与 pathname 进行匹配。
  4. 当路由规则(path)能够匹配地址栏中的 pathname 时,就展示该 Route 组件的内容。

编程式导航

通过 JS 代码来实现页面跳转

  • history 是 React 路由提供的,用于获取浏览器历史记录的相关信息

  • push(path):跳转到某个页面,参数 path 表示要跳转的路径

  • go(n): 前进或后退到某个页面,参数 n 表示前进或后退页面数量(比如:-1 表示后退到上一页)

  • 组件中通过 props 来获取到路由信息

    • 对于函数组件来说就是通过 参数 props 来获取
    • 对于类组件来说,就是通过 this.props 来获取
// 改造上面的Home组件
const Home = props => {
    console.log(props);
    return (
        <div>
            <h1>这是主页</h1>
            <button
                onClick={() => {
                    props.history.go(-1);
                }}
            >
                返回上一页
            </button>
            <button
                onClick={() => {
                    props.history.push("/login");
                }}
            >
                去登录页
            </button>
        </div>
    );
};

路由信息

props 中的路由信息主要有三部分

  • history 用于获取浏览器历史记录的相关信息(有三种模式)

    1. browser history 使用的是 H5 提供的 history API
    2. hash history 使用的是哈希模式就是带 # 的
    3. memory history 使用的是内存历史记录,再测试和非DOM中非常有用(比如react-native)

    参数信息

    • length - (number) 历史堆栈中的条目数,即执行一次路由跳转,就会数量加1
    • action - (string) 当前操作(PUSH,REPLACE或POP)
    • location - (object) 这个就是下面的那个location,两者是一样的
    • push(path, [state]) - (function) 将新条目推送到历史堆栈,可以用于实现路由跳转,可以实现返回
    • replace(path, [state]) - (function) 替换历史堆栈上的当前条目,也可以实现路由跳转,但是无法再返回
    • go(n) - (function) 通过指定参数 n 来实现,跳转到那个堆栈信息,常用 this.props.history.go(-1) 来实现返回
    • goBack() - (function) 这个就相当于 go(-1)
    • goForward() - (function) 这个就相当于 `go(1)
    • block(prompt) - (function) 常用于提示用户是否要离开当前页面
  • location url 地址信息

    • hash url 中的 hash 值
    • pathname 路径名称
    • search 查询参数 就是 ? 后面的参数部分
  • match 路由的path 和 url 相匹配的信息

    • params - (object) 里面包含的就是解析url 得到的 动态路由参数,以键值对的形式存储
    • isExact - (boolean) 是否精确匹配
    • path - (string) 就是我们路由组件中 path 属性的值,用于构建嵌套路由
    • url - (string) 与url 匹配的部分

默认路由

默认路由地址为:/ , 默认路由,在进入页面时,就会被匹配

<Route path="/" component={Home} />

路由匹配模式

  1. 模糊匹配

只要 pathname 以 path 开头就会匹配成功

比如:当我们跳转到 “/login” 路由下时,会发现 默认路由 也匹配到了,这就是 模糊匹配 ,所以会导致这样的结果

“/” : 可以匹配到 所有 pathname
“/home” : 可以匹配到所有以 “/home” 开头的路由 ,比如 “/home” “/home/abc”,但是无法匹配 “/home1” ,需要把 “/home” 当做一个整体

  1. 精确匹配

只有当 path 和 pathname 完全匹配时才会展示该路由

  • 给 Route 添加 exact 属性,就会让当前路由规则变为精确匹配
  • 此时,只有当 pathname 和 path 完全匹配时,才会展示该组件。
// 精确匹配:
// pathname 是 '/first',path 是 '/',此时,就不会匹配了
// 只有当 pathname 是 '/' 并且 path 也是 '/' ,此时才会匹配
<Route exact path="/" component={Home} />

推荐:给默认路由添加 exact 属性。

嵌套路由(子路由)

在 React 中,我们是用的 Route 就是一个组件,通过这个组件来控制匹配的路由展示哪一个组件的内容,既然是展示的内容是一个组件,那么就可以在那个组件中再来一个路由,这样就实现了嵌套路由

const List = () => <p>这是主页中的一个子路由</p>;

const Home = props => {
    console.log(props);
    return (
        <div>
            <h1>这是主页</h1>
            <Route path="/home/list" component={List} />
        </div>
    );
};

注意:子路由的 path 必须以父路由的 path 开头,我们既要展示 home 页的内容,又要展示 list 页的内容,那么就要这样使用 /home/list ,如果你只想展示 list 的内容就没有必要用什么嵌套路由了

路由重定向

react-router-dom 提供了一个组件 Redirect 用于实现重定向

一般我们都是把 ‘/’ 默认路由重定向到首页,这样可以使得在地址栏中的输入更简洁

// 通过 render 属性来返回一个 组件,这个组件可以实现重定向 to 表示定向到那个路由下面
<Route exact path='/' render={() => <Redirect  to='/index'/>}

路由高阶组件

注意:只有通过路由 Route 组件直接渲染的内容,才能够通过 props 获取到路由信息。也就是说:间接渲染的组件(子组件),它不是直接通过 Route 组件渲染出来的,因此,这个组件中,无法直接获取到路由相关信息。

解决方法:

  • 使用路由的高阶组件 withRouter 来包装 NavHeader 组件,这样,在该组件中就可以获取到路由信息了。
  • 使用了高阶组件的组件就可以通过 props 来获取相应的值
import React from "react";
import { NavBar } from "antd-mobile";

function NavHeader({ history, children }) {
    return (
        <NavBar
            className={styles.navBar}
            mode="light"
            icon={<i className="iconfont icon-back" />}
            onLeftClick={() => history.go(-1)}
        >
            {children}
        </NavBar>
    );
}

// 通过高阶组件,让我们的 NavHeader 组件有了相应的路由信息
export default withRouter(NavHeader);

路由参数

让一个路由规则,可以同时匹配多个符合该规则(格式)的 URL 路径。

示例:

/detail/:id
<Route path="/detail/:id" component={xxx} />
/detail/:id/:name
<Route path="/detail/:id/:name" component={xxx} />

第一个情况可以匹配一个参数 id ,可匹配 /detail/1 或者 /detail/2

第二个情况可以匹配两个参数 id 和 name ,匹配 /detail/1/zs 或者 /detail/2/ls

获取路由参数

通过路由渲染的组件,可以直接通过 props 来获取相应的路由信息 >>>>> proprs.match.params

对应上面的示例(使用 class 组件就加 this,用函数组件就不加 this )

// /detail/:id   ---->  /detail/1
const { id } = this.props.match.params 

// /detail/:id/:name  ----> /detail/1/zs
const { id , name} = this.props.match.params

props.match.params 中对应的参数属性名就是 路由参数中的 名字 例如 id 或者 name

鉴权路由

所谓的鉴权路由就是指需要有某些权限才能访问的页面,比如需要登录之后才可以访问,或者有某些角色权限才可以访问,与Vue中的导航守卫功能类似

react中,都是基于组件的,并没有想Vue中那样帮我们直接在路由中集成了,因此我们需要自己封装一个组件来实现鉴权路由。react-router-dom 尽管在文档里没有说怎么做,但是给了我们一个完整的例子 Redirect(Auth)

分析例子发现,其实所谓的鉴权路由,其实实际渲染的就是 Route 只是在外面进行了一层包裹处理

function PrivateRoute({ component: Component, ...rest }) {
  return (
    <Route
      {...rest}
      render={props =>
        fakeAuth.isAuthenticated ? (
          <Component {...props} />
        ) : (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: props.location }
            }}
          />
        )
      }
    />
  );
}

上面这段代码中的render,具体成文字描述就是 : 访问 Component 页面组件的时候,如果已经登录了,则渲染 Component 这个页面组件(传递过来的参数是小写字母开头,但是react中的组件是以大写字母开头因此进行重命名),否在就通过重定向跳转到 /login 页面,进行登录,并且传递一个 state 过去,用于当登录成功后,跳转到对应的页面,而不是简单的go(-1)

我们封装组件的时候 剩余参数(…rest)这个是干嘛用的呢 ? 其实 Route 组件还有一些别的参数,比如 exact 这些其他参数我们如果不传递过来使用,那么我们封装的组件就会不起作用,无法实现例如精确比配这样的功能,封装路由组件的原则就是不改变原来Route的使用方式,与之前的使用保持一致性

这样封装后,我们就可以用这个组件来替换之前的 Route 路由了

// 原先
<Route path="/protected" component={Protected} />
// 现在
<PrivateRoute path="/protected" component={Protected} />

具体效果可以通过上面的连接进行访问查看

NavLink 组件

  • NavLink 相当于 Link 组件,但是,它自带高亮类名(默认是 active)
  • 可以通过 activeClassName 来修改默认的高亮类名
<NavLink activeClassName="todos-active" to="/all">
  ALL
</NavLink>

Switch 组件

  • Switch 组件:用来包裹多个 Route 组件,被包裹的多个 Route 组件中,只要有一个路由规则匹配了,就不在匹配后续的路由规则了
<Switch>
  <Route exact path="/"/>
  <Route exact path="/:filter?"/>
</Switch>

// 注意:如果两个路由规则中有重叠的部分,那么,应该使用 Switch 组件来包裹。
// 并且 匹配范围小的在前,匹配范围大的在后
<Switch>
  <Route path="/detail/:id"/>
  <Route path="/:type/:page"/>
</Switch>

CSS IN JS

CSS IN JS:是 React 中用来解决组件之间样式相互影响、覆盖问题的一类解决方案的统称

常用的两个方案

  1. styled-components
  2. CSS Modules(推荐)

CSS Modules 方案解决样式冲突的原理:将我们写的类名,替换为全局唯一的样式名称

  • React 脚手架中使用 CSSModules 对 CSS 命名采用了 BEM 的命名规范
  • 比如:[filename]_[classname]__[hash]
  • 实际上,我们只需要提供 classname ,其他的两部分,脚手架会自动生成

CSS Modules 在脚手架中的使用

  1. 创建名称为:[name].module.css 的样式文件
  2. 导入样式文件:import styles from './index.module.css'
  3. 使用样式:
// navBar 就是我们自己写的 CSS 类名
// 'NavHeader_navBar__Sby3N' 这是 脚手架 自动生成的全局唯一的类名
// styles 是一个对象
styles = {
    navBar: "NavHeader_navBar__Sby3N";
};

使用原则

  • 推荐使用驼峰命名,来指定 CSS 类名
  • 避免嵌套
    • 因为 CSS Modules 中的类名都是全局唯一的,实际上,如果所有样式全部使用 CSS Modules 来实现,就不需要嵌套
    • 因为 嵌套 的目的,就是为了提升权重,或者,只让某个样式在某种条件下生效。。。
  • 对于全局的样式,需要使用 :global() 包裹(比如:字体图标的样式、组件库的样式等)
/* 通过 :global() 来告诉 react 脚手架这是一个全局类名,不要对它进行重命名 */
.navBar :global(.icon-back) {
    color: #333;
}

.navBar :global(.am-navbar-title) {
    color: #333;
}

/* SASS 配合 CSSModules 使用 */
.navBar {
    margin-top: -45px;
    background-color: #f6f5f6;

    /* 全局样式: */
    :global {
        .icon-back,
        .am-navbar-title {
            color: #333;
        }
    }
}

React 中虚拟 DOM 和 Diff 算法补充

  • 因为 React 中使用 虚拟 DOM 和 Diff 算法 来实现了部分更新,通过对比更新前后的两个虚拟 DOM 对象来找到差异的地方,最终,只将差异的地方重新渲染到页面中。
  • 在这个过程中,实际上 React 在 Diff 对比更新前后两个虚拟 DOM 对象时,会优先对比这两个虚拟 DOM 对象的 key,看是否相同,如果 key 相同,就复用该组件;如果 key 不同,则直接不再复用该组件,而是新建组件

举个开发中遇到的实际例子

场景:使用了 antd-mobile 组件库中的一个 PickView 组件,并且有三个按钮共用这一个组件,只是渲染的数据是不同的,点击按钮 A,渲染省市数据,点击按钮 B,渲染季节,点击按钮 C,渲染工资范围

问题:当我们在三个中都选择了某些数据条件的情况下,点击任意一个,然后随意切换,这时候会发现我们选择的数据并没有默认的显示(前提是已经设置了 PickView 的 value 属性,并且值是我们选择的值)

分析:这里我们在这个 PickView 组件的外面包裹了一个组件,因为有三类的数据,所以就进行了状态提升。当我们点击按钮进行切换的时,组件是处于更行阶段的,而我们的默认数据是通过父子组件传值从父组件传递过来的,在 state 中进行了初始化赋值,但是这初始化的操作只在挂载阶段执行一次不会再次执行。所以就不会多次出发赋值

// 个别组件不用在意,这是我自己项目用使用到的
export default class FilterPicker extends Component {
    // 把父组件传递过来的值赋值给value,这样当我们选择数据后,再次点开就可看到上次的数据
    state = {
        value: this.props.defaultValue
    };

    onChange = val => {
        this.setState({
            value: val
        });
    };

    render() {
        const { onCancle, onSave, data, cols, type } = this.props;
        return (
            <>
                {/* 选择器组件: */}
                <PickerView
                    data={data}
                    value={this.state.value}
                    onChange={this.onChange}
                    cols={cols}
                />

                {/* 底部按钮 */}
                <FilterFooter
                    onCancle={onCancle}
                    onSave={() => onSave(type, this.state.value)}
                />
            </>
        );
    }
}

解决办法:

  1. 因为处于更新阶段,所以我们只要在 组件的 componentDidUpdate 方法中对 value 值进行更新就行(注意:需要添加条件判断,不然会导致循环更新)
  2. 第二中方法就是使用 虚拟 DOM 和 Diff 算法,在父组件中给 渲染的子组件添加 key 属性
<FilterPicker
    key={openType}
    defaultValue={defaultValue}
    type={openType}
    cols={cols}
    data={data}
    onCancle={this.onCancle}
    onSave={this.onSave}
/>

脚手架中项目打包优化

create-react-app 中隐藏了 webpack 的配置,隐藏在 react-scripts 包中。

修改脚手架的 webpack 配置有两种方式:

  • 运行命令 npm run eject 释放 webpack 配置(注意:不可逆操作)
  • 通过第三方包重写 webpack 配置(比如:react-app-rewired 等)

组件库的按需加载(如antd-mobile)

这部分在官网有详细操作,可以按照官网的步骤来执行

基于路由的代码分割

目的:将代码按照路由进行分割,只在访问该路由时才加载该组件内容,提高首屏加载速度。

React.lazy() 方法 + import() 方法 、Suspense 组件 代码分割

React.lazy() 作用:处理动态导入的组件,让其像普通组件一样使用。

import(‘组件路径’) 作用: 告诉 webpack ,这是一个代码分割点,进行代码分割。

Suspense 组件:用来在动态组件加载完成之前,显示一些 loading 内容,需要包裹动态组件内容。

封装的通用组件

吸顶组件

这是一个比较常见的效果,当我们滚动页面的时候,当滚动一定距离,某个组件就会停留在顶部,比如搜索框、导航栏等

常见的实现方式有两种:

  1. 通过监听滚动了多少距离然后来修改组件样式,当滚动到一定距离后添加样式,让组件脱标,注意,此时需要个页面容器增加一个顶部的padding,避免因为脱标导致内容有塌陷
  2. 这种就是下面代码实现的例子,用一个占位容器div 来标志是否到达定不了(top=0 ? ),当到达顶部后即(top=0)让目标组件增加样式后脱标了,占位容器高度变为目标容器的高度,这个值可以通过使用组件的时候传递过来
import React, { Component, createRef } from "react";
import styles from "./index.module.scss";
import PropTypes from "prop-types";

class Sticky extends Component {
    placeholderRef = createRef();
    contentRef = createRef();

    handleScroll = () => {
        const height = this.props.height;
        // console.log(this.contentRef.current.getBoundingClientRect().height);
        const placeholder = this.placeholderRef.current;
        const content = this.contentRef.current;

        const { top } = placeholder.getBoundingClientRect();

        if (top <= 0) {
            // 需要让筛选框吸顶,并且让占位内容有高度(高度为筛选框的高度)
            placeholder.style.height = height;
            content.classList.add(styles.fixed);
        } else {
            // 占位内容高度为0,筛选框取消吸顶
            placeholder.style.height = 0;
            content.classList.remove(styles.fixed);
        }
    };

    // 添加窗口滚动事件
    componentDidMount() {
        window.addEventListener("scroll", this.handleScroll);
    }

    // 组件销毁的时候注销事件
    componentWillUnmount() {
        window.removeEventListener("scroll", this.handleScroll);
    }

    render() {
        const { children } = this.props;
        return (
            <div>
                {/* 占位符,用于当筛选框吸顶脱标后撑高度 */}
                <div ref={this.placeholderRef} />
                {/* 筛选框的内容 */}
                <div ref={this.contentRef}>{children}</div>
            </div>
        );
    }
}

Sticky.propTypes = {
    height: PropTypes.number.isRequired
};

export default Sticky;

吸顶的时候添加的样式

.fixed {
    position: fixed;
    top: 0;
    z-index: 1;
    width: 100%;
}

使用吸顶组件


	

你可能感兴趣的:(前端开发)