Redux: 最好的异步服务器端加载技术?

创建于 2015-06-15  ·  63评论  ·  资料来源: reduxjs/redux

首先,我喜欢这个库和你们正在使用的模式。 👍👍

我正在尝试使用 redux 来构建一个同构的应用程序。 到目前为止,它工作得很好,除了我需要弄清楚如何在返回初始页面加载之前等待我的商店加载(在服务器上)。 理想情况下,加载应该在商店本身中进行,但是当我调用dispatch(userActions.load()) ,商店必须返回新状态(即return { ...state, loading: true }; ),因此它无法返回对等待。 dispatch()返回由于某种原因传递给它的操作。 我真的很喜欢这样的东西...

dispatch(someAsyncAction, successAction, failureAction) => Promise

...在调度其他两个操作之一之前,承诺不会解决。

那是可以通过中间件模式启用的吗?

我是否完全偏离了基地,并且已经有一种简单的方法可以做到这一点?

谢谢。

最有用的评论

// Middleware
export default function promiseMiddleware() {
  return (next) => (action) => {
    const { promise, types, ...rest } = action;
    if (!promise) {
      return next(action);
    }

    const [REQUEST, SUCCESS, FAILURE] = types;
    next({ ...rest, type: REQUEST });
    return promise.then(
      (result) => next({ ...rest, result, type: SUCCESS }),
      (error) => next({ ...rest, error, type: FAILURE })
    );
  };
}

// Usage
function doSomethingAsync(userId) {
  return {
    types: [SOMETHING_REQUEST, SOMETHING_SUCCESS, SOMETHING_FAILURE],
    promise: requestSomething(userId),
    userId
  };
}

所有63条评论

嘿,谢谢!

理想情况下,装载应在商店本身进行

Redux 强制 Stores 是完全同步的。 你所描述的应该发生在动作创建者中。

我_认为_它甚至可以使用默认的 thunk 中间件。 你的动作创建者看起来像:

export function doSomethingAsync() {
  return (dispatch) => {
    dispatch({ type: SOMETHING_STARTED });

    return requestSomething().then(
      (result) =>  dispatch({ type: SOMETHING_COMPLETED, result }),
      (error) =>  dispatch({ type: SOMETHING_FAILED, error })
    );
  };
}

并处理 Store 中的实际(细粒度)操作。

也可以编写自定义中间件来删除样板。

天才! 我想我忽略了一些明显的东西。 我喜欢 _doing_ 和 _storing_ 的分离。

我期待看到这个库的成长,尽管它已经很完善了。 干杯,@gaearon!

您还可以像这样编写自定义中间件

export default function promiseMiddleware() {
  return (next) => (action) => {
    const { promise, ...rest } = action;
    if (!promise) {
      return next(action);
    }

    next({ ...rest, readyState: 'request' );
    return promise.then(
      (result) => next({ ...rest, result, readyState: 'success' }),
      (error) => next({ ...rest, error, readyState: 'failure' })
    );
  };
}

并使用它而不是默认的。

这将让您编写异步操作创建者,例如

function doSomethingAsync(userId) {
  return {
    type: SOMETHING,
    promise: requestSomething(userId),
    userId
  };
}

并让它们变成

{ type: SOMETHING, userId: 2, readyState: 'request' }
{ type: SOMETHING, userId: 2, readyState: 'success' }
{ type: SOMETHING, userId: 2, readyState: 'failure' }

哦,那也很好,当我问最初的问题时,我想到的更多。 我不知道我是否喜欢减少动作常量的数量以换取添加if来检查商店内的readyState的权衡。 我想我可能更喜欢每个动作的附加_SUCCESS_FAILURE版本,以避免将if放入case

谢谢,不过。

是的,这完全取决于你的口味。 您可以使用类似的版本将types: { request: ..., success: ..., failure: ... }转换为操作。 这就是让它成为中间件而不是融入库的重点:每个人对这些东西都有自己的品味。

// Middleware
export default function promiseMiddleware() {
  return (next) => (action) => {
    const { promise, types, ...rest } = action;
    if (!promise) {
      return next(action);
    }

    const [REQUEST, SUCCESS, FAILURE] = types;
    next({ ...rest, type: REQUEST });
    return promise.then(
      (result) => next({ ...rest, result, type: SUCCESS }),
      (error) => next({ ...rest, error, type: FAILURE })
    );
  };
}

// Usage
function doSomethingAsync(userId) {
  return {
    types: [SOMETHING_REQUEST, SOMETHING_SUCCESS, SOMETHING_FAILURE],
    promise: requestSomething(userId),
    userId
  };
}

哦,伙计,我喜欢那个解决方案。 比像您提出的第一个解决方案那样拥有then()和对dispatch()额外调用要好得多。 中间件万岁!

让我知道它是如何(以及是否 ;-)它起作用的!
我们还没有真正测试过自定义中间件。

你遗漏了} (那是 -1 分😀),但它的作用就像一个魅力! 第一次。

:+1:

@erikras我很

这只是伪代码,所以不要把它粘贴到任何地方,但我正在使用 react-router(它的 api 变化与 redux 一样快)是这样的:

app.get('/my-app', (req, res) => {
  Router.run(routes, req.path, (error, initialState) => {
    Promise.all(initialState.components
      .filter(component => component.fetchData) // only components with a static fetchData()
      .map(component => {
        // have each component dispatch load actions that return promises
        return component.fetchData(redux.dispatch);
      })) // Promise.all combines all the promises into one
      .then(() => {
        // now fetchData() has been run on every component in my route, and the
        // promises resolved, so we know the redux state is populated
        res.send(generatePage(redux));
      });
  });
});

这能解决问题吗?

@est

在 Slack 中引用您的问题:

我有一个路由处理程序

 static async routerWillRun({dispatch}) {
   return await dispatch(UserActions.fooBar());
 }

其中UserActions.fooBar()是:

export function fooBar() {
 return dispatch => {
   doAsync().then(() => dispatch({type: FOO_BAR}));
 };
}

然后在服务器渲染中我让步:

 yield myHandler.routerWillRun({dispatch: redux.dispatch});

但它不起作用。

我认为这里的问题是您实际上并没有从fooBar的嵌套方法中返回任何内容。

要么删除大括号:

export function fooBar() {
  return dispatch =>
    doAsync().then(() => dispatch({type: FOO_BAR}));
}

或者添加一个明确的return语句:

export function fooBar() {
  return dispatch => {
    return doAsync().then(() => dispatch({type: FOO_BAR}));
  };
}

无论哪种方式,使用上面建议的自定义承诺中间件可能会更容易。

@erikras关于你最后一条评论,你在initialState.components上调用 fetchData 方法(在 Router.run 的回调中),你从中获取组件引用的对象只返回匹配的路由处理程序。 您对访问可能不是匹配路由处理程序的组件(即子组件)但需要获取数据有什么想法?

这是我正在谈论的一个例子

import React from 'react';
import Router from 'react-router';
import {Route, RouteHandler, DefaultRoute} from 'react-router';

//imagine Bar needs some data
const Bar = React.createClass({
  render(){
    return(
      <div>bar</div>);
  }
});

const Foo = React.createClass({
  render(){
    return (
      <div>
        foo
        <Bar/>
      </div>);
  }
});


const App = React.createClass({
  render(){
    return (
      <div>
        <RouteHandler />
      </div>
    );
  }
});

const routes = (
  <Route path="/" handler={App} name="App">
    <DefaultRoute handler={Foo} name="Foo"/>
  </Route>
);

Router.run(routes,'/',function(Root,state){
  console.log(state);
});

输出:

{ path: '/',
  action: null,
  pathname: '/',
  routes: 
   [ { name: 'App',
       path: '/',
       paramNames: [],
       ignoreScrollBehavior: false,
       isDefault: false,
       isNotFound: false,
       onEnter: undefined,
       onLeave: undefined,
       handler: [Object],
       defaultRoute: [Object],
       childRoutes: [Object] },
     { name: 'Foo',
       path: '/',
       paramNames: [],
       ignoreScrollBehavior: false,
       isDefault: true,
       isNotFound: false,
       onEnter: undefined,
       onLeave: undefined,
       handler: [Object] } ],
  params: {},
  query: {} }

您将无法访问 Routes 中的 Bar

@erikras太棒了! 这正是我想要走的那种路线。 感谢分享。

@iest我希望双关语是故意的,通过迭代匹配的路线来“沿着路线走”。 :-)

@mattybow确实如此。 如果你_真的_需要一个不在你的路由中的组件来加载某些东西,那么唯一的选择是运行React.renderToString()一次(丢弃结果),在componentWillMount()执行所有加载,然后以某种方式随时随地保存承诺。 这就是我在react-router支持服务器端渲染之前使用我自己的本地路由解决方案所做的。 我建议需要非路由组件进行加载可能是设计问题的征兆。 在大多数用例中,路由知道其组件需要哪些数据。

@erikras
您是否有任何公共仓库可以在那里查看有关您的解决方案的完整示例?

@transedward我希望我这样做了,但是到目前为止,我在这里详述的方法仍然非常不成熟。 对不起。

+1 高级同构示例
我喜欢这是要去的地方!

@transedward这是一个示例项目,其中包含我拼凑的所有前沿技术。 https://github.com/erikras/react-redux-universal-hot-example/

@erikras这太棒了! 您能否提交 PR 以将其添加到此自述文件和 React Hot Loader 的文档的“入门工具包”部分?

谢谢! 提交了 PR。

@erikras很棒 - 谢谢!

请注意,基于本次对话中的一些 ide,我制作了一个中间件来处理 Promise: https :

@pburtchaell有该库由@acdlite为好。 https://github.com/acdlite/redux-promise

对此有两个想法:

  1. 转换为动作的 Promise 可以与动作一起传递并放入 Redux Store。

然后,要知道您的页面是否已准备好呈现,只需检查所有 Promise 是否已完成。 也许使用一个中间件,将所有传递的承诺链接在一起,并在没有挂起的承诺时提供承诺。

  1. 让选择器为丢失的数据调用操作怎么样?

假设您要呈现消息 3,那么您的消息容器将呈现<Message id={3}>并且消息选择器将检查state.msgs[3]存在,如果不存在,则调度消息加载承诺。

所以将两者结合起来,组件会自动选择他们需要的数据,你会知道它们什么时候完成。

我非常确定“不要将任何不可序列化的内容放入存储或操作中”。 这是对我来说非常有效的不变量之一(例如,允许时间旅行)并且需要_非常_令人信服的理由来考虑更改它。

然后,要知道您的页面是否已准备好呈现,只需检查所有 Promise 是否已完成。 也许使用一个中间件,将所有传递的承诺链接在一起,并在没有挂起的承诺时提供承诺。

这其实不需要最后在 store 里放 promise,我喜欢这个。 不同之处在于,在调度链的末端,原始操作不包含任何承诺。 这些是由上述中间件“收集”的。

在处理 promise 时,我经常做的一件事是保留对 promise 的引用,这样当其他对同一事物的请求进来时,我只需返回相同的 promise,提供去抖动。 然后我在承诺完成后的一段时间删除引用,提供可配置的缓存。

我真的必须开始在实际应用程序中使用 Redux,因为我想知道如何处理 Redux 中的这些引用。 我有点想将它们放入商店以使 actionCreator 无状态(或至少使状态显式)。 通过操作传递承诺是导出它的好方法,但是之后您需要以某种方式取回它。 嗯。

我真的很期待@wmertens最后一点的
避免多个并发调用(如果有待处理的东西什么都不做)是一个常见的用例,不确定什么是最好的方法来回答这个问题(因为你不能从 actionCreator 访问商店的状态)。

actionCreator 用户(组件)是否应该每次在状态中检查他是否可以调度动作? 你必须每次都这样做..也许你需要引入你自己的@connect注释来包装这个?

因为你不能从 actionCreator 访问商店的状态

为什么? 如果您使用thunk 中间件,您可以做得很好。

function doSomething() {
  return { type: 'SOMETHING' };
}

function maybeDoSomething() {
  return function (dispatch, getState) {
    if (getState().isFetching) {
      return;
    }

    dispatch(doSomething());
  };
}

store.dispatch(maybeDoSomething());

这解决了它!
我有一段时间(无缘无故)认为在 actionCreator 中访问状态是一种不好的做法^^ 因为动作创建者调用者可以访问状态,我不明白为什么 actionCreator 不应该,所以很酷我们可以这样做:)

谢谢@gaearon

@gaearon ,这种使用 Thunk 和不同操作的技术http://gaearon.github.io/redux/docs/api/applyMiddleware.html 是否比您上面的答案更可取:

您还可以像这样编写自定义中间件

export default function promiseMiddleware() {
  return (next) => (action) => {
    const { promise, ...rest } = action;
    if (!promise) {
      return next(action);
    }

    next({ ...rest, readyState: 'request' );
    return promise.then(
      (result) => next({ ...rest, result, readyState: 'success' }),
      (error) => next({ ...rest, error, readyState: 'failure' })
    );
  };
}

并使用它而不是默认的。
这将让您编写异步操作创建者,例如

function doSomethingAsync(userId) {
  return {
    type: SOMETHING,
    promise: requestSomething(userId),
    userId
  };
}

并让它们变成

{ type: SOMETHING, userId: 2, readyState: 'request' }
{ type: SOMETHING, userId: 2, readyState: 'success' }
{ type: SOMETHING, userId: 2, readyState: 'failure' }

还,

我认为最后一部分,你的意思是:

{ type: SOMETHING, userId: 2, readyState: 'request' }
{ type: SOMETHING, userId: 2, result, readyState: 'success' }
{ type: SOMETHING, userId: 2, error, readyState: 'failure' }

我猜这取决于是否要为承诺的successfailure回调创建单独的操作,而不是使用自动生成的回调。

在您的 thunk 示例中:

function makeASandwichWithSecretSauce(forPerson) {

  // Invert control!
  // Return a function that accepts `dispatch` so we can dispatch later.
  // Thunk middleware knows how to turn thunk async actions into actions.

  return function (dispatch) {
    return fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize('The Sandwich Shop', forPerson, error))
    );
  };
}

那么对于无法获取秘密酱汁的通用错误回调不一定有意义,因为获取酱汁可能有不同的情况。

因此,我可以看到 thunk 模型更加灵活。

可能像日志记录或什至切换“正在进行的异步繁忙指示器”之类的东西是更合适的中间件示例?

@贾斯汀808

两者都很好。 选择对您来说不那么冗长的内容以及对您的项目更有效的内容。 我的建议是从使用 thunk 开始,如果您看到某些模式重复,请将其提取到自定义中间件中。 你也可以混合它们。

我创建了一个 ActionStore,用于将触发操作(加载、成功、失败)的状态与其他状态分开。 但我不知道这是否违背 Redux/Flux 的基础。 我已经在stackoverflow 中发布了有关此内容的信息。

@gabrielgiussi我认为https://github.com/acdlite/redux-promise也可以实现你想要的,而不必在状态中存储承诺。 状态应该始终是可序列化的。

@wmertens感谢您的建议。 我会看看 repo 但为什么我的状态不能序列化? 还是你说只是为了我注意?

@gabrielgiussi我没有仔细看,但看起来你是
将承诺或功能放入商店。 无论如何,那个项目
我认为也应该工作得很好。

2015 年 8 月 10 日星期一,19:15 gabrielgiussi [email protected]写道:

@wmertens https://github.com/wmertens感谢您的建议。 我会
看看 repo 但为什么我的状态不能序列化? 或者您
说只是为了我注意?


直接回复此邮件或在 GitHub 上查看
https://github.com/gaearon/redux/issues/99#issuecomment -129531103。

哇。
(在手机上输入,请原谅简洁)

实际上,我在 store 中放置了自定义 Action 对象,它们只是具有简单属性(id、state、payload)的 Immutable.Record 和创建并返回 Promise 的 action 触发器,所以我没有将 Promise 放入 Store。 但我可能在别的地方弄坏了一些东西,je。 谢谢@wmertens。

@gabrielgiussi

以及创建并返回 Promise 的动作触发器

不要将函数或任何其他不可序列化的东西放入状态。

对不起。 我试着说

以及创建并返回 Promise 的函数触发器

我实际放入商店的是一个 Action 对象(名称不是最好的):

export default class Action extends Immutable.Record({state: 'idle', api: null, type: null, payload: null, id: null}){
load(){
  return this.set('state','loading');
}

succeed(){
  return this.set('state','succeeded');
}

fail(){
  return this.set('state','failed');
}

ended(){
  return this.get('state') != 'loading' && this.get('state') != 'idle';
}

endedWithSuccess(){
  return this.get('state') == 'succeeded';
}

endedWithFailure(){
  return this.get('state') == 'failed';
}

trigger() {
  return (dispatch) => {
    dispatch({type: this.get('type') + '_START', action: this});
    let payload = this.get('payload');
    this.get('api').call({dispatch,payload}).then((result) => {
      dispatch({type: this.get('type') + '_SUCCESS',id: this.get('id'), result: result.result});
    }).catch((result) => {
        dispatch({type: this.get('type') + '_FAIL',id: this.get('id'), result: result.result});
      });
  }
}
}

我创建了一个用于解决这个问题的库(参见 #539),它的工作原理是让中间件为挂起的操作返回承诺,并等待所有这些承诺得到解决。

@gaearon你写的这段代码https://github.com/rackt/redux/issues/99#issuecomment -112212639,

这是包含在 redux 库中的东西还是我必须手动创建的东西? 抱歉,如果这是一个新手问题,请进入 React / Flux (Redux)。 刚开始本教程https://github.com/happypoulp/redux-tutorial

@banderson5144

它不包括在内。 它只是让您了解自己可以做什么——但您可以自由地以不同的方式去做。
类似的内容发布为https://github.com/pburtchaell/redux-promise-middleware。

感谢您提供这些有用的信息。 我想在重置商店时挑选你的大脑 -

  • 您为新用户重置商店状态
  • 在向用户提供商店和 html 之前,您等待一些异步操作完成
  • 之前为另一个用户运行的异步操作完成,您的商店被污染

你们是如何解决这个问题的/有什么想法吗? 只是为每个用户开设一个新商店会起作用吗?

你说的是服务器渲染? 根据每个请求创建一个新商店。 我们在文档中有一个服务器渲染指南。

谢谢我会这样做

在尝试了解之后……

这太天真了吗? (似乎没有其他人这样做 - 我认为)

// server.js
app.use(function (req, res) {
    match({…}, function (error, redirectLocation, renderProps) {
        …

        if (renderProps) {
            const store = configureStore();

            const promises = renderProps.components.map(function (component, index) {
                if (typeof component.fetchData !== 'function') {
                    return false;
                }

                return component.fetchData(store.dispatch);
            });

            Promise.all(promises).then(function () {
                res.status(200).send(getMarkup(store, renderProps));
            });
        }
    })
});
// home.js
export class Home extends Component {
    static fetchData() {
        return Promise.all([
            dispatch(asyncAction);
        ]);
    },

    componentDidMount() {
        const { dispatch } = this.props;

        Home.fetchData(dispatch);
    }
}

export default connect()(Home);
// action.js
export function asyncAction() {
    return (dispatch, getState) => {
        dispatch(request);

        return fetch(…)
            .then(response => response.json())
            .then(data => dispatch(requestSuccess(data)))
        ;
    }
}

我也试图找出@mattybow问题的解决方案https://github.com/rackt/redux/issues/99#issuecomment -112980776(管理数据获取的嵌套组件),但没有这样的成功(不是确定如何从componentWillMount收集承诺)。

@chemoish我也在尝试使用 react 和 redux 来了解服务器端渲染。 文档中的示例并没有很好地处理这个用例。 我不想再次指定服务器上的每个 API 请求并将其与我的组件耦合。 该组件只应指定如何获取所需的数据(然后服务器应获取)。

您的解决方案看起来非常适合实现这一目标。 这对你有用吗? 谢谢

编辑:我是对的,“componentDidMount”在服务器上呈现时不会在客户端上再次触发?

@ms88privat我还没有得到关于解决方案的太多反馈,也没有测试过它的限制。

但是,上面提出的解决方案要求每个页面都知道其所有子组件的数据。 我还没有深入研究嵌套组件本身担心数据管理(由于收集嵌套的承诺)。

它似乎在做你所期望的,所以现在对我来说已经足够了。


componentDidMount将再次被触发(参见 https://facebook.github.io/react/docs/component-specs.html#mounting-componentdidmount)。 您可以使用此方法或其他适合您需要的生命周期方法。

如果商店已经满了(或您认为合适的任何业务逻辑),我会通过阻止fetch代码执行来解决这个问题。

检查https://github.com/reactjs/redux/blob/master/examples/async/actions/index.js#L47以了解我在说什么。

@chemoish

如果商店已满,我会通过阻止执行提取代码来解决此问题

好的,我明白了。 谢谢。

但是,上面提出的解决方案要求每个页面都知道其所有子组件的数据。

也许我误读了您的解决方案,但无论服务器端渲染如何,这不是必要的要求吗? (例如,如果我刷新当前路线,即使它是 SPA,它也应该呈现相同的状态)

它会,但无论出于何种原因,您可能希望嵌套组件管理其自己的数据获取。

例如,一个组件在许多页面上重复,但每个页面都没有太多的数据获取需求。

@chemoish我不确定我们是否在同一页面上。 让我试着解释一下我的观点是什么。

例如,我得到了三个嵌套组件:

  • 组件1(静态数据Fetch1)

    • 组件 2(静态 dataFetch2)

    • 组件 3(静态 dataFetch3)

他们每个人都有自己的“componentDidMount”方法,有自己的 dataFetching 声明(通过其静态 dataFetching 方法调度操作)。

如果我没有服务器端渲染并刷新当前 URL,我的组件将挂载并触发之后加载所有必需数据所需的所有操作。

使用服务器端渲染,您的match函数和renderProps将提取所有三个组件,因此我可以访问所有静态 dataFetching 方法,然后允许我获取所需的所有数据最初的服务器端渲染?

您提供的示例中是否有对match function的引用? 谢谢。

@ms88privat renderProps.components是一组路由器组件,没有比这更深入的了。 @chemoish意味着通过他的实现,您无法描述更深层次组件上的数据获取需求。

@DominicTobias thx,您有解决此问题的方法吗? 是否有可能获得所有嵌套组件?

也许这可以帮助? https://github.com/gaearon/react-side-effect
用于从嵌套元素中收集所有元标记: https :

很抱歉再次打扰这个讨论,但我最近遇到了用异步操作预填充状态的相同问题。

我可以看到@erikras将他的样板项目移到了redux-async-connect 。 我想知道,是否有人找到了其他解决方案?

@vtambourine我一直在看https://github.com/markdalgleish/redial ,这很有帮助

是的,我已经看过了。 但是我没弄明白,如何确定数据
在将代码重新初始化为 n 后,获取挂钩将不会第二次运行
客户。
在 Пт, 18 марта 2016 г. 22:54,Sean Matheson通知@github.com
写道:

@vtambourine https://github.com/vtambourine我一直在看
https://github.com/markdalgleish/redial很有帮助


你收到这个是因为你被提到了。
直接回复此邮件或在 GitHub 上查看
https://github.com/reactjs/redux/issues/99#issuecomment -198517067

也很好奇是否有人为这个挑战找到了稳定的解决方案。 我喜欢@erikras的样板文件,但正如@vtambourine 所提到的,它已转移到 redux-async-connect,这似乎不是一个稳定的长期解决方案: #81 redux-async-connect 死了吗? .

@vtambourinehttps://github.com/makeomatic/redux-connect 上有一个叉子,并且维护良好。 它有类似的 API,但有一些更改,如果您有兴趣,请查看

对于那些对@gaearon提到的带有中间件的 redux 解决方案感兴趣的人,我有一个示例项目,它实现了这种技术并允许组件本身请求他们需要的服务器端数据

https://github.com/peter-mouland/react-lego-2016#redux -with-promise-middleware

如何使用这种方法对动作创建者进行单元测试?

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

rui-ktei picture rui-ktei  ·  3评论

ms88privat picture ms88privat  ·  3评论

ramakay picture ramakay  ·  3评论

olalonde picture olalonde  ·  3评论

cloudfroster picture cloudfroster  ·  3评论