在 react-router 中调用 getComponent 时如何显示加载 UI?

IT技术 javascript reactjs react-router react-redux
2021-05-24 07:06:31

我真的是 React 的新手,当使用 getComponent 加载路由时,我无法弄清楚如何呈现“正在加载...”屏幕。getComponent 调用工作正常并显示组件,但 UI 上没有指示请求和响应之间正在发生任何事情。这就是我想弄清楚的。

import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';


var Routes = {
  path: "/",
  component: Main,
  indexRoute: {
    component: Home
  },
  childRoutes: [
    {
      path: "test",
      component: Test
    },
    {
      path: "about",
      getComponent: function(path, cb) {
        require.ensure([], (require) => {
          cb(null, require("./pages/about/About.jsx"));
        });
      }
    }
  ]
};

export default Routes;

在尝试使用 onEnter 或 getComponent 函数强制“加载”组件显示失败后,我想也许我应该尝试使用 Redux 将加载状态设置为 true/false 并让我的主视图组件显示加载屏幕:

import React from 'react';
import {connect} from 'react-redux';

import NavBar from '../components/Navigation/NavBar.jsx';
import Footer from '../components/Footer.jsx';
import Loading from './Loading.jsx';
import navItems from '../config/navItems.jsx';
import setLoading from '../actions/Loading.jsx';

var Main = React.createClass({
  renderPage: function() {
    if (this.props.loading) {
      return (
        <Loading/>
      );
    } else {
      return this.props.children;
    }
  },
  render: function() {
    return (
      <div>
        <header id="main-header">
          <NavBar navigation={navItems}/>
        </header>
        <section id="main-section">
          {this.renderPage()}
        </section>
        <Footer id="main-footer" />
      </div>
    );
  }
});

function mapStateToProps(state) {
  return {
    loading: state.loading
  }
}

export default connect(mapStateToProps)(Main);

如果我使用操作手动设置加载状态,这似乎有效,这正是我想要做的。但是(我觉得这将是一个真正的菜鸟问题)我不知道如何从路由器内部访问商店/调度员。

我不确定我是否使用了错误的搜索词或其他什么,但我完全没有想法,每个 react-router/redux 教程似乎都跳过了我觉得必须是一个常见问题的内容。

任何人都可以指出我正确的方向(并且还让我知道我在做什么是最佳实践?)?

编辑:我会尝试进一步澄清这一点。在第一个代码块中,您可以看到,如果我单击一个<Link to="/about">元素,则会触发 getComponent 函数,这将延迟加载 About.jsx 组件。我遇到的问题是我不知道如何显示某种加载指示器/微调器,它会在单击链接后立即出现,然后在组件加载后将其替换。

更多编辑:我尝试创建一个用于加载异步路由的包装器组件,它似乎可以工作,但是感觉真的很糟糕,我确信这不是正确的方法。路由代码现在看起来像这样:

import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
import AsyncRoute from './pages/AsyncRoute.jsx';


var Routes = {
  path: "/",
  component: Main,
  indexRoute: {
    component: Home
  },
  childRoutes: [
    {
      path: "test",
      component: Test
    },
    {
      path: "about",
      component: AsyncRoute("about")
    }
  ]
};

export default Routes;

AsyncRoute.jsx 页面如下所示:

import React from 'react';

function getRoute(route, component) {
  switch(route) {
    // add each route in here
    case "about":
      require.ensure([], (require) => {
        component.Page = require("./about/About.jsx");
        component.setState({loading: false});
      });
    break;
  }
}

var AsyncRoute = function(route) {
  return React.createClass({
    getInitialState: function() {
      return {
        loading: true
      }
    },
    componentWillMount: function() {
      getRoute(route, this);
    },
    render: function() {
      if (this.state.loading) {
        return (
          <div>Loading...</div>
        );
      } else {
        return (
          <this.Page/>
        );
      }
    }
  });
};

export default AsyncRoute;

如果有人有更好的主意,请告诉我。

4个回答

我想我已经弄清楚了。这可能是也可能不是正确的处理方式,但它似乎有效。我也不知道为什么我没有早点想到这一点。

首先,将我的 createStore 代码移动到它自己的文件 (store.jsx) 中,这样我就可以将它导入到主入口点以及我的 Routes.jsx 文件中:

import {createStore} from 'redux';
import rootReducer from '../reducers/Root.jsx';

var store = createStore(rootReducer);

export default store;

Root.jsx 看起来像这样(这是一个丑陋的混乱,但我只是想得到一些在基本层面上有效的东西,然后我会清理它):

import {combineReducers} from 'redux';
import user from './User.jsx';
import test from './Test.jsx';

var loading = function(state = false, action) {
  switch (action.type) {
    case "load":
      return true;
    case "stop":
      return false;
    default:
      return state;
  }
};


export default combineReducers({
  user,
  test,
  loading
});

我制作了一个基本组件,根据 Redux 商店的“加载”值显示加载/加载:

import React from 'react';
import {connect} from 'react-redux';

var Loading = React.createClass({
  render: function() {
    if (this.props.loading) {
      return (
        <h1>Loading</h1>
      );
    } else {
      return (
        <h1>Loaded</h1>
      );
    }
  }
});

export default connect(state => state)(Loading);

现在我的 Routes.jsx 文件看起来像这样(注意我已经导入了 Redux 商店):

import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
import store from './config/store.jsx';

var Routes = {
  path: "/",
  component: Main,
  indexRoute: {
    component: Home
  },
  childRoutes: [
    {
      path: "test",
      component: Test
    },
    {
      path: "about",
      getComponent: function(path, cb) {
        store.dispatch({type: "load"})
        require.ensure([], (require) => {
          store.dispatch({type: "stop"});
          cb(null, require("./pages/about/About.jsx"));
        });
      }
    }
  ]
};

export default Routes;

这似乎有效。一旦<Link/>点击a转到 /about 路由,就会调度一个动作,将主存储中的“加载”状态设置为 true。这会导致<Loading/>组件自行更新(我认为它最终会在窗口的角落或类似的地方渲染一个微调器)。require.ensure([])运行这个奇怪的函数是为了让 webpack 做它花哨的代码拆分,一旦组件被加载,就会调度另一个动作来将加载状态设置为 false,并渲染组件。

我对 React 还是很陌生,虽然这似乎有效,但我不确定这是否是正确的方法。如果有人有更好的方法,请加入!

遵循与@David MI 相同的方法,实现了一个加载减速器和一个包装分派的函数。

除去店铺的创建和管理,基本如下:

加载减速器:

// ------------------------------------
// Constants
// ------------------------------------
export const LOADING = 'LOADING'

// ------------------------------------
// Actions
// ------------------------------------
const loadQueue = []
export const loading = loading => {
    if (loading) {
        loadQueue.push(true)
    } else {
        loadQueue.pop()
    }

    return {
        type: LOADING,
        payload: loadQueue.length > 0
    }
}

export const actions = {
    loading
}

// ------------------------------------
// Action Handlers
// ------------------------------------

const ACTION_HANDLERS = {
    [LOADING]: (state, action) => (action.payload)
}

// ------------------------------------
// Reducer
// ------------------------------------
const initialState = false
export default function reducer (state = initialState, action) {
    const handler = ACTION_HANDLERS[action.type]
    return handler ? handler(state, action) : state
}

请注意loadingQueue,对于嵌套路由,在有剩余module要获取时如何保持加载消息处于活动状态。

withLoader函数:

import { loading } from 'loadingReducer'

const withLoader = (fn, store) => {
    return (nextState, cb) => {
        store.dispatch(loading(true))

        fn(nextState, (err, cmp) => {
            store.dispatch(loading(false))
            cb(err, cmp)
        })
    }
}

export default withLoader

现在在定义新路由时,我们可以使用 withLoader 隐式分发加载操作:

一些路线:

import withLoader from 'withLoader'
import store from 'store'

const route = {
    path: 'mypath',
    getComponent: withLoader((nextState, cb) => {
        require.ensure([], require => {
            cb(null, require('something').default)
        }, 'NamedBundle')
    }, store)
}
export default route

好的,让我们看看我是否可以在这里阐明这一点:

我不知道如何从路由器内部访问商店/调度员

没有必要这样做AFAIK。您可以指定所有路由,列出应该响应每个路由的组件(就像您在上面所做的那样),然后将每个组件连接到 redux 存储。对于连接,您的mapStateToProps函数可以以更简单的方式编写,如下所示:

export default connect(state => state)(Main);

关于loading状态:我认为有一个缓慢加载的组件并在加载时显示等待指示器是朝着错误方向迈出的一步。我宁愿有一个快速加载的组件,它从后端异步加载所有数据,当数据尚不可用时,该组件会呈现一个等待指示器。一旦数据可用,就可以显示它。这基本上就是您在第二次编辑中绘制的草图。

如果您可以将其从实际数据中删除,那就更好了,即不存在数据 -> 显示加载屏幕/存在数据 -> 显示真实屏幕这样,您可以避免在加载标志不同步的情况下出现问题。(从技术上讲:避免冗余。)

因此,与其让包装器通用,我宁愿为加载屏幕创建一个独立的组件,并在每个单独的组件需要它时显示它。(这些需求是不同的,所以似乎很难以通用的方式处理这个问题。)像这样:

var Page = function(route) {
  return React.createClass({
    getInitialState: function() {
      // kick off async loading here
    },
    render: function() {
      if (!this.props.myRequiredData) {
        return (
          <Loading />
        );
      } else {
        return (
          // display this.props.myRequiredData
        );
      }
    }
  });
};

动态负载异步路由器正在使用require.ensure,它使用jsonp从网络下载脚本。由于网络缓慢,有时,UI 阻塞,屏幕仍然显示预览响应组件。

@Nicole ,真正慢的不是组件内部的数据加载,而是组件本身,因为 jsonp