next训练营 | 10. 路由守卫 | 实现全局loading

2,280 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

王志远,微医前端技术部

前言

路由切换的控制时机很关键,我们用一个【切换路由页面未加载完成时显示 loading】的效果来学习下如何在 next 中使用路由守卫。效果如下

开始实现

实现思路

  • 全局组件实例上存放 loading 开关属性,并实现在 loading 为 true 时显示加载 gif,为 false 时才展示内容
  • 组件挂载时注册路由守卫事件:路由开始变化时开启 loading,路由结束变化时关闭 loading
  • 组件卸载时关闭路由守卫事件

具体实现

以下修改均在_app.tsx中实现

引入路由
import router from "next/router";
全局组件实例上存放 loading 开关属性
routeChangeStart: any; // 路由开始变化时事件
routeChangeComplete: any; // 路由结束变化时事件
state = {
  loading: false,
};
组件挂载时注册
 componentDidMount() {
    this.routeChangeStart = (url) => {
      this.setState({ loading: true });
    };
    this.routeChangeComplete = (url) => {
      this.setState({ loading: false });
    };
    router.events.on("routeChangeStart", this.routeChangeStart);
    router.events.on("routeChangeComplete", this.routeChangeComplete);
  }
组件卸载时
componentWillUnmount() {
  router.events.off("routeChangeStart", this.routeChangeStart);
  router.events.off("routeChangeComplete", this.routeChangeComplete);
}
最终的_app.tsx
import App, { Container } from "next/app";
import Link from "next/link";
import { Layout, Menu, Icon, Avatar, Spin } from "antd";
import router from "next/router";
import "antd/dist/antd.css";
import { withRouter } from "next/router";
const { Header, Footer } = Layout;
import * as TYEPS from "../store/action-types";
import axios from "../utils/axios";
import createStore from "../store";
import { Provider } from "react-redux";
const __REDUX_STORE__ = "__REDUX_STORE__";
function getStore(initialState) {
  if (typeof window == "undefined") {
    //如果 在服务器端运行的。那么直接创建新仓库返回
    return createStore(initialState);
  } else {
    //如果此代码是在客户端执行的,第一次会创建,以后每次都复用上一次创建的
    if (!window[__REDUX_STORE__]) {
      window[__REDUX_STORE__] = createStore(initialState);
    }
    return window[__REDUX_STORE__];
  }
}
class LayoutApp extends App<any> {
  store: any;
  routeChangeStart: any;
  routeChangeComplete: any;
  state = {
    loading: false,
  };
  constructor(props) {
    super(props);
    console.log("constructor LayoutApp");

    //此构造函数只会在客户端执行一次 这里的 initialState 是 getInitialProps 返回的
    this.store = getStore(props.initialState);
  }
  // 在页面级别渲染时只会被执行一次,即服务端渲染或每次切换客户端渲染都会被执行,但服务端渲染时客户端不会执行
  static async getInitialProps({ Component, ctx }) {
    let store = getStore({});
    let pageProps = {};
    console.log("2. getInitialProps");
    let options: any = {
      url: "/api/currentUser",
    };
    //如果此方法是在服务器执行的,那么会有 ctx.req 属性,它代表本次 node 请求对象
    if (ctx.req && ctx.req.headers.cookie) {
      options.headers = options.headers || {};
      options.headers.cookie = ctx.req.headers.cookie;
    }
    let response = await axios(options);
    if (response.data.code === 0) {
      // 当前登录的用户
      let currentUser = response.data.data;
      store.dispatch({ type: TYEPS.SET_USER_INFO, payload: currentUser });
    }
    if (Component.getInitialProps) {
      // 执行当前页面的 getInitialProps
      let data = await Component.getInitialProps(ctx);
      pageProps = { ...data };
    }
    return { pageProps, initialState: store.getState() };
  }
  componentDidMount() {
    this.routeChangeStart = (url) => {
      this.setState({ loading: true });
    };
    this.routeChangeComplete = (url) => {
      this.setState({ loading: false });
    };
    router.events.on("routeChangeStart", this.routeChangeStart);
    router.events.on("routeChangeComplete", this.routeChangeComplete);
  }
  componentWillUnmount() {
    router.events.off("routeChangeStart", this.routeChangeStart);
    router.events.off("routeChangeComplete", this.routeChangeComplete);
  }
  render() {
    console.log("3.LayoutApp.render");
    let { Component, pageProps } = this.props as any;
    let { currentUser } = this.store.getState();
    let pathname = this.props.router.pathname;
    pathname = "/" + pathname.split("/")[1];
    return (
      <Provider store={this.store}>
        <style jsx>
          {`
            a {
              display: inline-block !important;
            }
          `}
        </style>
        <Layout>
          <Header className="header">
            <Menu
              theme="dark"
              mode="horizontal"
              style={{ lineHeight: "64px", display: "inline-block" }}
              selectedKeys={[pathname]}
              defaultSelectedKeys={[pathname]}
            >
              <Menu.Item key="/">
                <Icon type="home" />
                <Link href="/">
                  <a>首页</a>
                </Link>
              </Menu.Item>
              <Menu.Item key="/user">
                <Icon type="/user" />{" "}
                <Link href="/user">
                  <a>用户管理</a>
                </Link>
              </Menu.Item>
              <Menu.Item key="/profile">
                <Icon type="profile" />
                <Link href="/profile">
                  <a>个人中心</a>
                </Link>
              </Menu.Item>
              <Menu.Item key="/login">
                <Icon type="login" />
                <Link href="/login">
                  <a>登录</a>
                </Link>
              </Menu.Item>
            </Menu>
            {currentUser && (
              <div
                style={{
                  display: "inline-block",
                  float: "right",
                  color: "red",
                }}
              >
                <Avatar style={{ color: "#F00", backgroundColor: "#CCC" }}>
                  {currentUser.username}
                </Avatar>
              </div>
            )}
          </Header>
          {this.state.loading ? (
            <Spin style={{ fontSize: 50, margin: "50px auto" }} />
          ) : (
            <Component {...pageProps} />
          )}
          <Footer style={{ textAlign: "center" }}>@copyright wzyan</Footer>
        </Layout>
      </Provider>
    );
  }
}
export default withRouter(LayoutApp);