React: 支持异步服务器渲染(渲染前等待数据)

创建于 2014-06-24  ·  139评论  ·  资料来源: facebook/react

如果 componentWillMount 可以返回一个 Promise 并且 React 会延迟渲染,直到该 Promise 被解决,这将大大简化构建同构事物的过程。 我已经看到在 react-router 和 rrouter 中尝试做类似的事情,但是将这个责任交给每个组件而不是路由器模块对我来说更有意义。

Component API Server Rendering Backlog Feature Request

最有用的评论

几个月前,我在 JSConf Iceland 做了一个演讲,描述了 React 中即将到来的异步渲染特性(见第二部分): https ://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react

现在@acdlite讨论了如何应用相同的概念来在 React 组件中启用异步等待服务器上的数据,并在标记准备好时逐步刷新标记: https :

希望你会喜欢看这些演讲! 我认为在一年左右的时间里,我们可能能够解决这个问题并为此制定一个正式的策略。

所有139条评论

这不存在的主要原因(我相信)是在客户端,您基本上总是希望显示某种加载指示器而不是延迟渲染。 (这也会使代码变得更加复杂,但我们可能可以解决这个问题。)

如果没有它,我发现有两种情况很难解决:

  • 在服务器端,如果您想在渲染之前获取数据,则不能将数据检索委托给组件,因为您不知道将渲染哪个组件的信息
  • 在客户端收到预渲染的 html 后第一次挂载应用程序时,即使您从服务器上检索到的数据中有某种缓存,您也可能希望使用异步方法来检索这些数据,这会阻止反应从重用html。

react-async用纤程和缓存解决了这些问题。 这可以解决问题,但在我看来,这些只是解决只能在核心中解决的问题的 _hackish_ 解决方案。

让我对这个问题一无所知componentWillMount是异步的,你不会立即返回任何东西,React 应该渲染什么,直到有,什么都没有? 如果是这样,如果还没有数据,为什么不在渲染中不返回任何内容呢? (是的,我得到了服务器端)另外,如果道具在componentWillMount触发之前发生变化会发生什么?

就个人而言,在componentWillMount期间调度异步请求似乎是错误的,除非该组件确实是一个孤立的黑盒并且还实现了加载指示器。 据我了解,React 组件不应该被误认为是更传统的 OOP 实例。 在最好的情况下,React 组件是用于可视化 props 中的数据的工具,如果它是交互式的,那么它也可能是 state。 这是一个视图,而不是视图和模型。

在我看来,这听起来像是问题,React 组件不应该是调度异步请求的组件,你获取所有数据,当数据准备好时,你才调用React.renderComponent 。 相同的解决方案客户端和服务器端。 如果任何异步请求失败,您还可以使用您选择的结果中止。

如果我误解了某些内容,请随意解雇我,但似乎您将 React 组件视为视图和模型,而(似乎)它们只是视图。

让我对这个问题一无所知

我必须承认我没有考虑所有的情况^^。
此功能仅在我们第一次挂载顶级组件时有用,并且在服务器上,确实如此,否则在大多数演员中,您会希望显示加载器指示器。

就个人而言,在 componentWillMount 期间调度异步请求似乎是错误的,除非该组件确实是一个孤立的黑盒并且还实现了加载指示器。 据我了解,React 组件不应该被误认为是更传统的 OOP 实例。 在最好的情况下,React 组件是用于可视化 props 中的数据的工具,如果它是交互式的,那么它也可能是 state。 这是一个视图,而不是视图和模型。

在某种程度上,您会希望“顶级”组件能够检索数据,就像在Flux 示例中所做的那样。
在这个示例中,事情非常简单,因为检索 todo 列表是一个同步操作,如果不是,并且在服务器上预渲染的情况下,我们将在没有数据的情况下进行第一次渲染(并松开预渲染来自服务器的标记)。

在一个简单的应用程序的情况下,一组数据由一个视图层次结构显示,仍然没有太大问题,您可以预加载数据并仍然保持商店的同步属性。
现在,如果应用程序由您在应用程序中重用的多个模块组成,我希望能够将这些模块视为能够在不同商店“订阅”的单独应用程序(负责获取数据)。

也许我弄错了东西,但是网络上的一些讨论/示例让我觉得某处缺少一些东西:

  • 在某些示例中,在我看来@petehunt试图实现类似的目标。
  • react-nested-router 在willTransitionTo推广了一些类似的机制,一些讨论让我觉得没有人提出合适的解决方案。
  • RRouter还提供了某种机制来在渲染/安装组件时预取数据。
  • 最后是我之前说的react-async

@fdecampredon需要明确的是,react-nested-router 中willTransitionTo的目的是_not_用于加载数据,特别是因为从该方法返回一个 promise 确实会阻止渲染新的 UI,这是你不想做的除非你绝对必须。

@fdecampredon每个人都还在努力解决问题,所以如果没有人给出明确的答案,我也不会感到惊讶。 但我猜 Facebook 的开发人员自己一定遇到过几次。

这事有进一步更新吗? 我刚刚开始探索 React,我立即遇到了这个问题。 许多人推荐 React 作为构建同构应用程序的首选解决方案,但只要不解决这个问题,我认为它根本无法完成工作。

在我看来,这听起来像是个问题,React 组件不应该是调度异步请求的组件,你获取所有数据,当数据准备好时,你才调用 React.renderComponent。 相同的解决方案客户端和服务器端。 如果任何异步请求失败,您还可以使用您选择的结果中止。

如果我误解了某些内容,请随意解雇我,但似乎您将 React 组件视为视图和模型,而(似乎)它们只是视图。

如果这是真的,那么 React 只不过是一个稍微不同的模板解决方案/视图层。 这将是一种耻辱,因为有这样的潜力。 当@fdecampredon提到那些由多个模块组成的复杂应用程序时,我真的很理解。 React 非常适合这个。

我不认为这种方法意味着将组件视为视图和模型。 如果您查看 Flux 架构,他们会将 React 组件视为 _controller-views_ ,它们不仅显示数据(来自商店),而且还根据用户交互调用操作。 然后这些操作会更新商店(= 模型)。 对我来说,这听起来就像一个明显的 MVC 架构。

问题在于填充商店的初始操作。 在客户端,可以按照建议从componentDidMount()方法调用该操作相当简单。 另一方面,在服务器端,我们确实需要一些特殊的地方和某种机制来延迟渲染,直到动作完成并且商店被填充。

目前,在我看来,唯一的方法是 React-async/Fibers + Flux。 Flux 的好处是我们不需要一些人工缓存来将组件状态从服务器传输到客户端(就像在原始 react-async 示例中所做的那样),我们可以简单地初始化存储,然后将它们发送到带有 html 标记的客户端(请参阅此示例)。

但这个解决方案确实是_hackish_。

我认为它不一定需要是异步的 componentWillMount; 我什至不确定它是否需要成为生命周期事件。 真正的问题是,在服务器端,在所有内容都被渲染为字符串之前,无法分析组件树。

我解决这个问题的理想解决方案是允许“渲染”只构建组件树,然后我们将能够遍历树以找到需要额外数据的组件,允许我们异步加载更多数据并“重新渲染” " 那个组件子树,然后一旦我们准备好刷新标记,就允许我们将该树转换为字符串。

这复制了我们能够在浏览器中做的事情:拥有一个我们可以根据需要重新渲染的虚拟 DOM。 不同之处在于在浏览器中 DOM 更新可以是隐式的。 在服务器上,我们需要明确说明何时渲染字符串,以便我们可以根据异步数据对虚拟 DOM 执行更新。

我解决这个问题的理想解决方案是允许“渲染”只构建组件树,然后我们将能够遍历树以找到需要额外数据的组件,允许我们异步加载更多数据并“重新渲染” " 那个组件子树,然后一旦我们准备好刷新标记,就允许我们将该树转换为字符串。 — @mridgway

是的,那会很好。 目前,在服务器端渲染组件两次可以作为一种解决方法。

我想引用react-nexus作为我希望在 React 中得到支持的示例。 它本质上是对mountComponent工作方式的重写,只是它构建了组件树,而没有实际将其安装到 DOM 或写出字符串。 这允许您在构建树时遍历组件树并触发异步方法。 这个实现的问题在于,它丢弃了第一棵树,然后无论如何都会调用React.renderToString ,而采用该预渲染树并渲染/安装它会很好。

我愿意在核心内解决这个问题,但需要一些指示。 我认为其中一项要求是使ReactContext不被全局引用,这样异步就不会引起问题。 是否还有其他全局变量也可能遇到此问题?

@mridgway如果我没记错的话,全局ReactContext可能是一个兼容的东西,并将在 0.14 中消失。 但我_可能_是错的。

@gaearon是的,这就是我从 withContext 弃用中得到的感觉。

:+1: 在这台 ATM 上使用react-router@mridgway的方法听起来很合理。

关于这个问题的一个稍微不同的看法是如何处理由打包程序(例如 webpack)生成的代码块的异步加载。

如本票所述 - https://github.com/rackt/react-router/issues/1402 - 异步渲染支持的问题是初始渲染似乎为空,因为相关块尚未在第一个加载渲染过程,导致在 DOM 的根处出现<noscript>并且校验和失败。 之后一切都很快就位,但是在本地工作时确实会导致 UI 闪烁,并且在现场工作时会更糟,尤其是如果要下载的块大小合理(例如 > 30KB)

将大型应用程序分成多个块的能力对我们来说很重要,并且已经解决了数据获取问题(我们使用了 react-router 和嵌套路由,可以在服务器上渲染之前遍历以获取数据依赖项)这是最后一块阻碍我们完全转向前端的 React 解决方案的难题。

@anatomic这不是 React 的责任,你的工作是适当地分块并推迟渲染,直到加载了所有必要的块。 换句话说,如果您的某个组件依赖于某个外部库,那么在尝试使用它之前满足您的问题显然是您的问题,即使尝试过 React 也无法做到,所以这同样适用于所有方面。

随意实施可能更适合您的替代策略,例如<WaitFor for={MyAsyncLoadedCompSignal} until={...} then={() => <MyAsyncLoadedComp ... />} /> 。 但是这些本质上是固执己见的,而不是 React 应该甚至需要提供的东西,所以最好留给社区。

最好将异步内容保留在 React 组件范围之外,这是一个示例:

import React from 'react';
import Layout from './components/Layout';
import NotFoundPage from './components/NotFoundPage';
import ErrorPage from './components/ErrorPage';

const routes = {

  '/': () => new Promise(resolve => {
    require(['./components/HomePage'], HomePage => { // Webpack's script loader
      resolve(<Layout><HomePage /></Layout>);
    });
  }),

  '/about': () => new Promise(resolve => {
    require(['./components/AboutPage'], AboutPage => { // Webpack's script loader
      resolve(<Layout><AboutPage /></Layout>);
    });
  })

};

const container = document.getElementById('app');

async function render() {
  try {
    const path = window.location.hash.substr(1) || '/';
    const route = routes[path];
    const component = route ? await route() : <NotFoundPage />;
    React.render(component, container);
  } catch (err) {
    React.render(<ErrorPage error={err} />, container);
  }
}

window.addEventListener('hashchange', () => render());
render();

请参阅Webpack 代码拆分React.js 从头开始​​路由react-routing (即将推出)

@syranide我会继续努力,但我认为它不像你上面所说的那样二进制。 我们正在使用 react-router,因此可能会引入一些问题,因为路由器是一个组件,而不是位于 React 环境之外。

如果我们采用<WaitFor ... />方法,那么我们肯定会在第一次渲染时获得不同的 DOM,这仍然会导致内容闪烁/消失吗?

如果我们做了方法,我们肯定会在第一次渲染时得到一个不同的 DOM,这仍然会导致内容的闪烁/消失吗?

如果您不希望闪烁(有些人会),只需在渲染之前等待您依赖的所有块都加载完毕,webpack 提供了这个开箱即用的require.resolve

PS。 是的,react-router 和你使用的任何其他东西肯定会使解决方案复杂化,但这仍然不是 React 需要解决的问题。

如果您不希望闪烁(有些人会),只需在渲染之前等待您依赖的所有块都加载完毕,webpack 提供了这个开箱即用的 require.resolve。

我会调查一下,我对require.resolve理解是它是对模块 id 的同步查找并且不涉及到服务器的旅行? 我们使用require.ensure来管理我们的块加载。

再次查看我们的代码,我认为我们可以通过让 react-router 认为它在服务器上呈现,然后遵循标准的客户端方法来绕过这个问题:

const history = new BrowserHistory();

if (typeof history.setup === "function") {
    history.setup();
}

Router.run(routes, history.location, (err, initialState, transition) => {
    React.render(<Provider redux={redux}>{() => <Router key="ta-app" history={history} children={routes} />}</Provider>, document.getElementById('ta-app'));
});

我会调查一下,我对 require.resolve 的理解是它是对模块 id 的同步查找并且不涉及到服务器的访问? 我们使用 require.ensure 来管理我们的块加载。

对不起,是的,我的意思是require.ensure 。 只有在满足所有依赖项时才会执行回调,因此只需将 render/setState 放入其中即可。

好的,这就是我们的做法,谢谢您的回复。 这感觉像是需要在 react-router 中解决的问题,所以我将在那里进行讨论 - 抱歉,如果这是进行此对话的错误地点!

你是对的, require.ensure是要走的路,我想我们的最终问题是它应该链接到当前正在访问的路由,因此直接绑定到路由器。 react-router 是基于组件的,它将它与渲染树联系起来。 如果没有我上面的技巧,我们就只能在异步加载所有内容之前查看树(或复制路由逻辑以允许在顶层加载相关块)。

你是对的, require.ensure 是要走的路,我想我们的最终问题是它应该链接到当前正在访问的路由,因此直接绑定到路由器。 react-router 是基于组件的,它将它与渲染树联系起来。 如果没有我上面的技巧,我们就只能在异步加载所有内容之前查看树(或复制路由逻辑以允许在顶层加载相关块)。

我对 react-router 不是很熟悉,但我仍然认为它只是setRoute(...) => require.ensure([], function() { require(...); setRoute(...); }) ,这真的不实用,所以你引入了另一个级别的间接。 路线图和每个的异步require.ensure加载程序。 编写一个助手function asyncSetRoute(...) { loadRoute(route, function() { setRoute(...); }} ,现在您调用asyncSetRoute ,它将推迟更新路由器,直到一切准备就绪。

伪代码和通用的,但这似乎是我的整体方法。 也许 react-router 应该提供这个,也许不是......也许理想情况下它是作为外部助手提供的,我不确定。

因此,经过数小时的研究,我现在确认服务器端渲染是 _不可能的_,除非您从上到下(?)提供所有内容。

可能的短期解决方案:
A. 预填充一个 store 并使服务器端加载同步

B. 在异步获取您的一个数据对象后,从顶部将所有内容作为一个数据输入。

回复:'A'。 除非您已经知道渲染结构的外观,否则这将不起作用。 我需要渲染它以了解我的各种集合/模型/api 依赖项。 另外,如何在客户端上异步获取,但在服务器上同步,而没有两个不同的 API?

回复:'B'。 基本上和上面一样的问题。 人们是否必须对他们的每条路线进行很少的依赖 JSON?

在我们等待 React 支持的解决方案时,还有其他短期解决方案吗? 任何用户土地叉或插件? https://www.npmjs.com/package/react-async

@尼克斯特凡

我不明白这个问题。 :-(

  1. 在 store 不是单例的地方使用 Flux 或类似 Flux 的容器(Alt、Redux、Flummox 等)。
  2. 在返回 Promise 的路由处理程序上定义静态方法,例如fetchData
  3. 在服务器上,将路由匹配到组件,从它们中获取fetchData并等待它们完成后再渲染。 这将预填充您的 Flux 或 Redux 存储实例。 请注意,存储实例不是单例——它绑定到特定请求,因此请求保持隔离。
  4. 当 Promise 准备好时,与您刚刚预填充的 store 实例同步渲染。

这是描述这种方法的 Redux 的一个很好的教程: https ://medium.com/@bananaoomarang/handcrafting -an-isomorphic-redux-application-with-love-40ada4468af4

@gaearon如果我让您感到困惑,我深表歉意。 感谢您的回复。 从您的列表中,听起来我假设服务器数据依赖的解决方案是只在您的根组件(静态方法/您链接到的文章)中定义数据需求是正确的。 如果您的数据依赖项是在根目录中定义的,那么预填充存储等会容易得多。

这是很好的助焊剂做法,但它不是潜在的限制吗? 如果我在视图树的底部添加一个需要一些非常不同的数据的小组件,那么我需要在根目录中编辑数据依赖项。

我要的是一种深度嵌套组件定义异步数据需求的方法。

如果我必须将他们的需求添加到根组件,我不是将根耦合到那个子组件的需求吗?

@NickStefanreact-routing例如,异步数据获取如下所示:

import Router from 'react-routing/lib/Router';
import http from './core/http';

const router = new Router(on => {
  on('/products', async () => <ProductList />);
  on('/products/:id', async (state) => {
    const data = await http.get(`/api/products/${state.params.id`);
    return data && <Product {...data} />;
  });
});

await router.dispatch('/products/123', component => React.render(component, document.body));

这些解决方案有效,但只是因为所有数据预取都绑定到路由器。 在大多数情况下这不是问题(这是有道理的,URL 定义了页面上应该有什么,因此需要什么数据),但总的来说这是一个限制。 您将永远无法创建一个独立的可重用组件(即 Twitter 流、评论、日历)来自行处理所有事情,而您只需将其插入到组件层次结构中。 我遇到的使这成为可能的唯一方法是react-async但这几乎是一种黑客行为。

我想 React 组件根本不打算以这种方式使用,它们仍然是比 _controller_-views 更多的视图。 可能必须在 React 的基础上出现一个全新的库。

我的梦想是有一天我们能够使用 React 编写完整的 CMS,比如 Wordpress、Drupal 或 Modx。 但是,我们将使用 React 组件组成网站,而不是 HTML/PHP 片段。

@尼克斯特凡

从您的列表中,听起来我假设服务器数据依赖的解决方案是只在您的根组件(静态方法/您链接到的文章)中定义数据需求是正确的。 如果您的数据依赖项是在根目录中定义的,那么预填充存储等会容易得多。

如果我必须将他们的需求添加到根组件,我不是将根耦合到那个子组件的需求吗?

不完全是在根——在路由处理程序级别。 您的应用程序中可能有许多路由处理程序。 此外,像React Router这样的库支持嵌套路由处理程序。 这意味着您的嵌套路由处理程序可以定义一个依赖项,并且路由器匹配将包含一个匹配的分层路由处理程序数组,因此您可以Promise.all它们。 这有意义吗?

它不像“每个组件都可以声明一个依赖项”那样细化,但我发现在大多数情况下,将数据获取限制为路由处理程序(顶部和嵌套处理程序)就是我想要的。 我在下面有纯组件,它们通过props接受数据,所以他们甚至不_知道_它正在被获取。 他们不能要求它是有道理的。

除非您有某种请求批处理解决方案,否则“每个组件都可以声明一个依赖项”的方法是不切实际的。 想象一下,如果<User>声明它需要一个 API 调用,现在你渲染一个包含 100 个<User>的列表——你想等待 100 个请求吗? 这种数据获取需求粒度仅在您有请求批处理解决方案时才有效。 如果这适合您,那么Relay就是这样。 但是你需要一个特殊的后端。

@NickStefan我们目前不支持按组件异步获取。 我们想添加它。 这个问题跟踪我们的进展,尽管没有人积极致力于它,并且需要进行重大重组,所以需要一段时间。

@gaearon

除非您有某种请求批处理解决方案,否则“每个组件都可以声明一个依赖项”的方法是不切实际的。

在考虑了更多之后,我同意你的看法。 我所寻求的实际上并不实用。

我原本以为连你的100示例对团队纪律是可以的(即只需创建一个<UsersContainer>对所有 100 个请求执行 1 个请求)。 但这不是“人员可扩展的”,只是有一个约定。 最好将所有依赖项强制执行到根或路由器。 甚至 Relay 也会迫使您在根目录中声明依赖关系,并使用其三个不同的容器(RelayContainer、RelayRootContainer、RelayRouter 等)。 它证明了这一点,唯一的方法是本质上“提升”你的依赖声明到树上。

你好呀!
有这方面的更新吗?

+1

我坚持认为,如果你真的考虑到这一点,你就不想这样做。 我原本以为我希望 React 进行异步渲染。 但是这个线程中的其他人已经说服了我。

即使在 Relay 中,您基本上也必须使用根容器声明、中继容器等将您的依赖项“提升”到树上。同步渲染是要走的路。

异步渲染可能是一场噩梦。 我是根据在公司代码库上工作的经验说的,该代码库被黑客入侵以在请求动画帧中进行单独更新。

同意。 我曾经认为我们需要这个,但实际上让视图指定要加载的数据是倒退的。 视图是应用程序状态的函数,它本身是请求的函数(如果有会话,也可能是用户的状态)。 这就是 React 的全部意义所在; 允许组件指定应加载哪些数据与 IMO 的“单向数据流”理念背道而驰。

让视图指定要加载的数据实际上是倒退的。

我不完全确定这是真的。 有时,组件是唯一知道要加载哪些数据的东西。 例如,假设您有一个可扩展的树形视图,允许用户浏览大量节点图 - 无法提前知道需要加载哪些数据; 只有组件才能弄清楚。

无论如何,如果我们追求在 web worker (#3092) 中运行 React 代码的想法,这个讨论可能会变得更加相关,其中需要异步来跨桥通信。

在大型树视图的示例中,如果我真的希望能够使用在服务器端已经打开的路径来呈现它,那么我会将该路径添加到 URL 结构中。 如果有几个像这样的复杂组件,那么我会用 GET 参数表示它们的状态。 通过这种方式,这些组件获得了 SSR 的所有好处:它们是可抓取的,可以使用历史进行导航,用户可以共享指向其中节点的链接等。现在,我们还可以让服务器确定需要什么数据被提取以呈现响应。

更新我把图误认为是树,但我仍然觉得用户对该图的视图状态应该由 URL 结构表示。 另外,没有意识到您实际上是在使用 React。 如果您正在开发一些框架以将数据层与视图同构地集成,那就太棒了。 但我认为我们可以同意这超出了视图的范围,并且 React 不应该扮演全栈控制器的角色。

如果已经讨论过,没有阅读整个线程非常抱歉。

真正有用的一件事是,如果有一个react-dom/server方法可以“启动/实例化”组件并触发生命周期方法,但让开发人员决定何时准备好将组件呈现为 html 字符串. 例如,开发人员可以使用 setTimeout 或更可靠的方法来等待异步方法完成。

这是我目前与 redux 一起使用的“hack”来实现这一点:

  // start/instantiate component
  // fires componentWillMount methods which fetch async data
  ReactDOM.renderToString(rootEle)

  // all my async methods increment a `wait` counter 
  // and decrement it when they resolve
  const unsubscribe = store.subscribe(() => {
    const state = store.getState()
    // as a result, when there are no more pending promises, the wait counter is 0
    if (state.wait === 0) {
      unsubscribe()
      // all the data is now in our redux store
      // so we can render the element synchronously
      const html = ReactDOM.renderToString(rootEle)
      res.send(html)
    }
  })

这种方法的问题是第二个ReactDOM.renderToString会再次触发所有componentWillMount方法,这会导致不必要的数据获取。

大家好! 这是目前正在做的事情吗? 我认为这个问题越来越重要。 我最近创建了一个库,可以将 react redux 的connect扩展为dataConnect ,您还可以在其中指定容器数据要求。 然后将这些数据需求捆绑在运行时,并在单个请求中使用 GraphQL 进行查询。 我认为这是数据规范的未来,因为它促进了可组合性和隔离性,并确保了更高性能的获取。 (在 Relay 上看到的所有概念)

上面的问题是服务器端渲染。 由于我无法静态分析最终会得到哪些数据需求,目前,我需要渲染一次以获取要从数据库查询的包,隐藏 redux 存储,然后重新渲染以获取最终字符串. 这个问题类似于@olalonde的问题。

如果能够触发动作和更新反应元素树并按需获取 DOM 字符串结果,那就太棒了。 我是这样描绘的:

const virtualTree = ReactDOM.renderVirtual(rootEle);

// get the bundled query from redux store for example
const bundle = store.getState().bundle;

// Fetch the data according to the bundle
const data = fetchDataSomehow(bundle);

// hydrate store
store.dispatch({type: 'hydrate', data});
// components that should update should be marked on the virtual tree as 'dirty'

virtualTree.update(); // this would only update the components that needed update

const domString = virtualTree.renderToString(); // final result

另一种选择是简单地让它随意更新,就像它在客户端一样,让所有的钩子都存在并且工作,比如 didMount。 但它不会改变 DOM,它只会改变虚拟 dom 表示,并按需渲染为字符串。

你们有什么感想? 有什么需要考虑的,或者我认为它完全错误?

大家好。 我订阅这个问题已经一年左右了。 那时,我认为我需要能够指定应该从组件中加载的数据,因为组件是我的主要模块化单元。 这个问题对我来说真的很重要,我认为它肯定会在下一个版本的 React 中得到解决。

从那时起,我对 REST/HATEOS 背后的理想产生了更深的敬意,因为当其指导原则是应用程序系统(浏览器、爬虫、应用程序服务器、代理、CDN 等)中出现的大规模简单性跟着。 因为它与这个问题有关,_URL 应该是应用程序状态的一个真实表示_。 它,并且只有它,应该确定为请求提供服务所需的信息。 视图层 React 不应该决定这一点; 它应该基于数据构建。 视图是数据的函数,数据是 URL 的函数

过去我一直犹豫着把它扔出去,因为我一直听说这是一个好主意,但现实世界太复杂了,无法实现。 偶尔,我听到一些让我退后一步的例子,我想知道我是不是太迂腐了,还是太理想化了。 但是当我仔细研究这些示例时,我不可避免地找到了一种合理的方式来表示 URL 中的应用程序状态。

  • 您的州是否有独立变化的参数? 将它们表示为单独的查询参数。
  • 您的状态(或部分状态)是否太大而无法放入 URL? 为其命名并将其存储在不可变的数据存储中。 按名称/ ID 引用它。
  • 您的状态变化如此之快以至于您无法永远存储所有状态吗? 恭喜,您拥有合法的大数据,开始存储并开始考虑与之相关的巧妙方法。 或者,您可以回避,只是使用 UPDATE 查询来改变您的数据,并接受您不能永远缓存所有内容(*)。
  • 您是否有针对不同用户的不同视图,但在相同的 URL 上提供服务,例如个性化主页? 根据用户标识导航到包含用户 ID 的 URL。
  • 您是否正在构建一个较旧的应用程序,您无法选择破坏旧的 URL 结构? 这很痛苦,我在同一条船上。 将旧的 URL 结构重定向到架构良好的 URL 结构,将会话数据(或其他)转换为 URL 路径段和参数。
  • 您对应用程序的体系结构几乎没有控制权或根本没有控制权? 这不是 React 的设计初衷,我们不希望 React 被破坏以使其适合诸如相互兼容的 Wordpress / Magnolia / Umbraco 架构之类的东西。

你从所有这些工作中得到了什么,否则你不会有什么?

  • 一个用户通过共享 URL 将另一个用户带到与他们在应用程序中相同的位置的能力。
  • 使用浏览器提供的所有工具导航应用程序的能力。 标准客户端是它的元素。
  • 以客户易于遵循的方式提供复杂分页的能力:响应中的next链接。 FB 图形 API 就是一个很好的例子。
  • Google Analytics(分析)中用户工作流程的详细图表。
  • 可以选择自己从请求日志中构建所述图形。
  • 摆脱混乱的星途:您可以按照预期使用服务器应用程序框架,而不是将所有请求匹配为app.get("*", theOneTrueClientonaserverEntryPoint) 。 您可以从请求中返回正确的 HTTP 状态代码,而不是200 OK\n\n{status: "error"} 。 星途很诱人,但也导致了疯狂。

那么,既然我们的明星工具 React 不再控制操作,我们如何获取数据呢? 我们如何知道要渲染哪些组件?

  1. 路由到数据访问函数并获取相关请求参数的数据。 重复直到你解析了整个 URL 并拥有一个完整的上下文对象。
  2. 将上下文转换为可能的最简单的响应对象,就好像您要响应 API 请求一样。 您是否在为 API 请求提供服务? 然后你就完成并干燥。
  3. 将那个最简单但可能很大的对象传递给顶级组件。 从这里开始,它一直是 React 组件组合。
  4. 渲染到字符串,响应。 让您的 CDN 知道它可以永久缓存它(* 除非您在上面牺牲了这个选项),因为 CDN 通过 URL 识别,并且您的所有状态都有一个 URL。 当然,CDN 没有无限存储,但它们确实具有优先级。

我并不是要在这里拖钓,但我确实强烈认为这个问题中请求的核心主题被误导了,React 不应该实现任何东西来适应它,至少不是因为我在这里看到的原因过去的一年。 好的应用程序架构的某些方面并不明显,而 Web 尤其难以做到正确。 我们应该学习和尊重先人的智慧,他们创造了互联网和万维网,而不是为了舒适而牺牲优雅。

这就是 React 的伟大之处:它是视图。 最好的景色。 只有景色。 λ

将不得不不同意你@d4goxn。 我并不是想让 React 不仅仅是一个视图层,而且路由很重要,但绝对不足以指定所有数据需求。 通过在组件/容器级别指定数据需求,您不会使 React 不仅仅是一个视图层。 这只会为您的应用程序提供更好的隔离和工作流程。 这不仅仅是对工作的简化,而是对大型应用程序的需求。 想象一下我的应用程序有 30 条路由,我想在每条路由上添加一个用户配置文件组件。 在为每个路径指定所有数据要求的路径替代方案之后,您必须通过 30 条路径来添加该数据依赖关系。 如果您在该组件的容器中指定数据需求,则只需将组件添加到您想要的位置。 即插即用。 不仅如此,还可以优化对所有组件数据依赖关系的查询。 Relay 就是一个很好的例子,关于它的讨论比我更好地解释了这一点。

我非常尊重旧智慧,但这不应该成为进化和创造新标准的限制,至少我是这么看的。

我的建议基本上是不改变 React,而是有一个“仅虚拟”的方式来改变 dom/components 树,基本上是一个可以在服务器端“运行”的 React,我认为这很容易做到(只需阻止操作改变 DOM)。 您仍然可以拥有缓存并拥有 CDN。 随着当今网站和应用程序的动态增长,静态缓存将趋于减少,但这是另一个话题😄

如果视图指定了对数据的依赖关系,那么您将需要某种方法来优化依赖关系图并将其转换为最少数量的查询; 您无法在构建视图之前准备数据,尽管您可以将其分为两个阶段,这就是我理解的在这个线程中的计划。 例如,采用一组中等复杂的组件,每个组件都有自己的查询; 也许它们不是同一组件的所有实例,并且没有可以折叠成单个查询的模式。 我想这就是 GraphQL 正在解决的问题。 您仍然需要为每个数据存储实现或集成 GQL 服务器。 整理出优化的 GQL 查询的哪些部分应该由哪个数据存储区处理听起来很复杂,但我的提议也有一些复杂性。

在示例中,有大量路由都需要相同的数据,我实际上不认为这是从 URL 中排除该数据源的标识符的理由。 我会在堆栈根附近安装一个小型数据获取中间件模块,该模块会将用户对象附加到上下文,然后将上下文传递给更多中间件,在到达最终路由处理程序的途中。 根 React 组件可能不关心上下文的那个特定部分,但它会将它传递给可能关心它的下一级子节点,或者他们自己的后代关心它。 如果这确实引入了不合理的复杂或深度布线,那么可能需要像 Flux 商店这样的东西,但这本身就是一个大话题。

分离与隔离:我在那里感受到你的痛苦。 在我的提议中,我们有两个非常不同的系统,将数据从针对存储和检索优化的形式转换为针对抽象简单性优化的形式。 然后我的观点是将其转换为适合它的形式,因为它会向下传递到组件层次结构中。 保持这两个系统松散耦合,同时添加需要添加到两个系统的功能并不是我所说的难,但这也不是阻力最小的路径。 数据存储编程和动态 UI 编程之间的心理转换是真实的。 我们曾经有单独的后端和前端开发人员,HTTP 充当这两种范式之间的接口。 现在我正在尝试将它内化到一个应用程序中,而您正在尝试委托它。

我不相信应用程序中不断增加的复杂性和活动会阻止客户端状态由一个非常小的对象表示,该对象引用服务器上更大的数据集。 考虑一个大型多人第一人称射击游戏:有很多事情发生得很快,并且您希望将必要的最少信息量从客户端传输到游戏主机/服务器。 你能弄到多小? 所有输入状态的映射; 一个时间戳 + 不确定范围,一个约 110 位的键盘字段,以及几十个字节的鼠标/操纵杆和 VR 耳机方向。 客户端将进行预测、乐观渲染和物理化,服务器将从客户端的小状态和状态历史中获取大量信息,并返回相当大量的数据来纠正和更新所有客户端(所有附近代理的所有相同参数,资产推送),但是,客户端请求流可能仍然适合 2KB 请求。 这可能是应用程序架构的福音。

我不会要求你教我 Relay 和 GraphQL。 在他们的 API 达到稳定版本之前,我只是对它们进行了肤浅的探索(它们现在是否被锁定?)如果你仍然确信使用组件来选择 GraphQL 模式,它指定了数据依赖关系,它决定了需要获取哪些实际数据,是一个好计划,那么也许是时候我再看看他们了。 我对那个架构有一些棘手的问题,但我要跑题了。

:啤酒:

PS 我并不是说 HTTP 会是一个很好的 MMOFPS 通信协议

@d4goxn我完全理解您对此的犹豫和怀疑。 在开始使用 GraphQL 以及后来引入 Relay 的概念之前,我从未想过这个问题。 我真的建议你看看。 即使您有多个数据存储,在新的或现有的服务器中实现 GraphQL 也没有您想象的那么困难。 也不想跑题,但这是一个重要的讨论。

我相信这种抽象级别是要走的路。 我一直在 Relax CMS 上使用它,工作流程和隔离是一种幸福。 不仅如此,所请求的数据也不是更多,也不是更少用户界面需要的,这是通过收集每个组件需要的数据并将其合并自动完成的。 如果您有兴趣检查,这是由 Relate http://relax.github.io/relate/how-it-works.html完成的。 有了这个,我还可以在我的应用程序的任何地方包含任何组件,并确保它将作为我的应用程序的一个独立块工作。

唯一需要弄清楚的是服务器端渲染。 在浏览了 react 的源代码之后,我相信可能已经有我所描述的解决方案,如果没有,如果 react 团队中的某个人同意这一点,我可以制定解决方案。

@d4goxn我也敢和你争论 :) 你的原始帖子包含一个声明,即视图是数据的函数,数据是 URL 的函数。 我不会称之为完全揭示。 它几乎适用于任何网站或任何非完全随机的 Web 应用程序(例如,打开对话窗口的状态是否应该也是 URL 的一部分存在一个问题)。 因为我们在学校都学过数学,所以我们知道函数是可以组合的。 所以实际的说法可能是您在屏幕上看到的内容是 URL 的函数。 同样,没有什么真正揭示,这只是显而易见的形式化。

对我来说,主要问题是如何构造这样的函数

您建议的方法与老式的服务器端 MVC 应用程序非常相似(例如 Spring MVC 就是一个很好的例子)。 当前 URL 激活_执行业务逻辑_的相应控制器方法。 它获取所有需要的数据并将它们传递给视图。 我的问题如下:当您查看生成的网页时,它是某种组件层次结构(不一定是 React 组件)。 您需要做的是从 URL 创建或生成层次结构。 但是你必须做两次! 首先,您需要解码控制器中的层次结构以了解您需要获取哪些数据,其次您需要解码视图的层次结构以......好吧......渲染实际视图。 我不认为这是一个非常 _DRY_ 的方法。

我对 Spring MVC 不太熟悉,但我经常使用另一个 MVC 框架(称为 Nette 的 PHP 框架),它以与我想使用 React 的方式非常相似的方式解决了这个问题。 该框架还支持组件。 这个想法是,对于每个组件(例如表单),您在代码中定义一个工厂,该工厂负责实例化组件,尤其是_加载所有必要的数据_。 这样一个组件就可以被包含在视图中的任何地方。 因此,当您编写 HTML 代码并创建_view hierarchy_时,您可以简单地插入一个组件,底层工厂负责必要的初始化。 该组件的行为就像一个小型独立控制器,它有自己的生命周期,它处理它的依赖关系,甚至可以从视图中对其进行参数化,从而增加其可重用性。

我也一直在客户端使用这种方法和 React,它似乎非常可行。 React 组件从一开始就被称为_controller-views_,这就是我使用它们的方式。 我的 _controller_ 组件负责数据获取,然后我的 _view_ 组件只关心视觉效果。 整个页面由这两种类型的组件组成。 它们很好地解耦并且易于重用。

我的应用程序不是同构的(或者他们今天称之为通用的),因为我不需要它,但我正在使用的堆栈应该能够做到这一点(我相信除了仍然处于实验阶段的 Relay 之外,这个堆栈走了在这件事上最长的路径)。 仅供参考,这就是它的样子:

  • 核心工具是react-router 。 它允许在每个 URL 上挂钩异步数据请求,并且它在客户端和服务器端都有效。 从形式上讲,这种方法存在我前面提到的问题。 那些 _controller_ 组件是不可能的。 但这里重要的是react-router 。 它允许在同一个地方定义_data-hierarchy_和_view/component hierarchy_,并且实际的路由定义也是分层的。 感觉非常 _React-like_。
  • 整个状态(数据)由redux处理。 除了所有其他优点外,它将整个状态保存在单个对象中,这意味着在服务器上创建对象并将其发送到客户端非常简单自然。 还要感谢connect()方法,没有必要使用道具从顶层向下传递所有数据(考虑到我的应用程序的当前大小,这绝对是疯狂的)。
  • 另一个难题是重新选择,它允许我们保持状态尽可能小。 并且也大大增加了解耦系数。

它仍然不完美,但与我第一次遇到此线程时可用的内容相比,这是一个重大进步。 示例实现可以在这里找到。

我也有几乎所有的同构工作,除了在数据获取处于组件级别时需要一种方法来完成服务器端渲染。 在不增加哲学论点的情况下,很长一段时间以来,我一直成功地使用 vanilla react(加上本地路由),组件在 componentDidMount 中获取自己的数据。 我真的不喜欢维护一个冗余结构来定义我的组件和数据依赖项的想法——这已经在我的组件树中定义了,在这里我只是需要多个渲染通道来合并数据,因为它是异步获取的,直到完整的组件树已解决。

现在,我只是想支持这对于同构反应日益增长的重要性,imo。 在此期间,我将尝试制定解决方案。 谢谢。

@decodeman如果您有兴趣,对于 Relate,我已经做了一个最小的反应遍历器来从组件获取数据依赖关系 https://github.com/relax/relate/blob/master/lib/server/get-data -dependencies.js。 这样我可以获得所有数据依赖项-> 获取数据-> 对字符串进行反应渲染。

@decodeman ,FWIW,我们一直在大型生产应用程序中成功地使用双重渲染方法(就像你提到的那样)。 它的灵感来自这个 redux-saga 示例,但一般方法应该适用于任何类型的异步加载。 您只需将加载移动到componentWillMount 。 为了帮助保持它的可管理性,我们还有处理常见加载场景的高阶组件(例如,检查 prop x并在 nil 时加载。

我还认为能够在组件中加载数据并拥有某种惰性渲染方法非常重要。 也许为了让双方都开心,创建一个名为 asyncRenderToString 的新方法并添加一个名为asyncOnLoad的新“生命周期”事件或类似的东西。 仅当您调用 asyncRender 方法时才会调用它。 就服务器端和客户端之间的代码重复而言,开发人员只需将公共加载代码移动到单独的方法中并从asyncOnLoadcomponentMount调用该方法即可解决此问题。 检测 asyncOnLoad 是否完成应该可能使用返回的 Promise(如果返回 undefined/方法未定义,它应该直接调用适用的生命周期事件然后渲染)否则它会等到 Promise 解决。

我认为这样的解决方案可以解决我们目前为正确的服务器端渲染而必须使用的所有 hacky 方法。 包括例如允许使用 react router v4 轻松地在服务器端渲染 react 应用程序(不再使用集中式路由配置,这是目前使同构应用程序工作的唯一方法)

这已经解决了它所谓的流检查反应流。

2016 年 11 月 3 日星期四,Florian Krauthan [email protected]
写道:

我也认为能够在其中加载数据非常重要
组件并具有某种惰性渲染方法。 也许两者兼而有之
双方很高兴创建一个名为 asyncRenderToString 的新方法并添加一个新的
“生命周期”事件称为 asyncOnLoad 或类似的东西。 这会
仅当您调用 asyncRender 方法时才会调用。 在代码方面
服务器和客户端之间的重复这可以由开发人员修复
只是将常见的加载代码移动到一个单独的方法中并调用它
来自 asyncOnLoad 和 componentMount 的方法。 检测 asyncOnLoad 是否为
done 可能应该使用返回的 Promise (如果 undefined 是
返回/方法未定义它应该直接调用适用的
生命周期事件然后渲染)否则它会等到承诺
解决。

我认为这样的解决方案将解决我们必须使用的所有 hacky 方式
目前用于正确的服务器端渲染。 包括例如允许
使用 React Router v4 轻松地在服务器端渲染 React 应用程序
(不再使用现在的集中式路由配置
使同构应用程序工作的唯一方法)


您收到此消息是因为您订阅了此线程。
直接回复此邮件,在 GitHub 上查看
https://github.com/facebook/react/issues/1739#issuecomment -258198533,
或使线程静音
https://github.com/notifications/unsubscribe-auth/ATnWLUIEJw4m1Y3A4oGDOBzP6_ajDcqIks5q6g4_gaJpZM4CHAWq
.

@yamat124如果我搜索反应流,我只会找到这个项目: https :

至少根据文档,没有办法有一个生命周期事件来预加载数据,这会延迟子子项的下一次渲染调用,直到 Promise 被解决。 还是您在谈论其他事情?

同样,这都是非常hacky的方式。 它要求路由独立于组件树。 并且路由包含的“主要”组件必须知道需要加载的所有数据(99% 的时间有效,但并非总是如此)。 如果 react 有延迟的组件渲染,两者都将过时。

我同意,这可能是我现在最讨厌反应的事情。 讨厌人们试图通过说它可以手动完成来保护框架不具有此功能,当然可以,就像与软件开发相关的所有事情一样,但这并不意味着它应该这样做。 事实上,如果这个精确的事情本身应该表明它是应该从框架本身内解决的事情,那么有大量的模块在处理这个事实。

Next.js有一个async getInitialProps来做服务器端渲染,不幸的是仍然需要从父母一路调用孩子的getInitialProps回到根。 Apollo GraphQL 客户端还允许遍历所有树以收集服务器端的数据需求,然后将所有内容发回。 Next + Apollo 的示例: https :

如果有异步 React 组件生命周期方法将这些东西组合在一起,那就太好了。

这周刚学了SSR,希望我写的有道理😄

/cc @nkzawa @stubailo

BTW componentWill/DidMount已经是异步的,这有帮助吗?

https://twitter.com/Vjeux/status/772202716479139840

@sedubois React 在渲染之前是否等待承诺解决? 不然真的帮不上忙!

@olalonde似乎是这样: https :

const getMessage = async () => new Promise((resolve) => {
  setTimeout(() => {
    resolve('Got async message!');
  }, 3000);
});
class App extends Component {
  state = {
    message: 'Loading...',
  };
  async componentWillMount() {
    this.setState({ message: await getMessage() });
  }
  render() {
    return (... {this.state.message} ...);
  }
}
loadinggot

@sedubois请注意,在您的示例中, componentWillMount

  async componentWillMount() {
    this.setState({ message: await getMessage() });
  }

返回undefined一个承诺,但屏幕截图显示 React 不会等待(它不能)承诺,因为它呈现Loading...文本。

编辑:更正。

@sedubois @polytypic它还会导致函数内抛出的所有错误被静默忽略,因为 Promise 根本没有被处理。

@polytypic @dantman我不太确定“根本没有处理承诺”是什么意思。 await运算符正在等待它。 您需要尝试捕获错误,我更新了示例(使用类似于 Apollo 的加载/错误签名)。

    try {
      this.setState({ message: await getMessage() });
    } catch(error) {
      this.setState({ error });
    }

并渲染:

{error ? error : loading ? 'Loading...' : message}
screen shot 2016-11-07 at 13 25 46

@sedubois当你async一个函数隐式返回一个承诺时,函数中的任何throw隐式地变成对该承诺的拒绝,而不是抛出堆栈。这意味着componentWillMount现在正在向调用者返回一个承诺。

React 不会对返回值做任何事情,所以你的函数现在返回一个 Promise,期望有人正在对它做某事,但它只是被扔掉了。

因为它没有被处理,这意味着任何对该承诺的拒绝都不会被 React 中的任何代码捕获,并且任何未处理的错误都不会显示在您的控制台中。

即使尝试..catch 也不能完全保护您免受此伤害。如果setState从你的 catch 中抛出一个错误,或者你在 catch 中输入了一个错字,那个错误会默默地消失,你不会意识到有什么东西坏了。

@sedubois实际上,异步方法似乎总是在 JavaScript 中返回一个承诺(好吧,未来的 JavaScript)。 我对 C# 中的 async-await 问题感到困惑。 _为噪音道歉!_

尽管如此,屏幕截图清楚地表明,React 不会等待 promise 被解决。 它在调用componentWillMount后立即调用render并呈现Loading...文本。 然后它在调用render之后再次调用setState 。 如果 React 确实等待 promise 解决,它根本不会呈现Loading...文本。 相反,它会等到 promise 解决,然后才会调用render ,这就是这个问题的全部内容:能够在服务器端渲染期间执行异步 IO。

@dantman @polytypic谢谢👍 是的,肯定需要在 React 内部进行更改以等待响应并正确处理错误。 也许在它的代码中在这里和那里添加一些await可以做到这一点😄

就我个人而言,我使用 Web Workers 来存储应用程序状态并在更改时发出 props(因为我完全分离了数据存储和从 React 组件中获取,并将 React 视为渲染器,以保持单向流原则),所以它只是等到具有要呈现的预期要求(例如特定属性为真)的消息。

这样,即使第一条消息是中间“加载”状态,它们也不会用于静态服务器端呈现,而不需要异步组件方法。

@sedubois它必须是不同的方法堆栈; 或重大变化。 React 当前的渲染方法是同步的,所以我们需要一个不同的服务器渲染方法来返回一个 Promise。

@dantman我不想在这里发表任何意见,因为这些水域对我来说太深了(我最初的目的只是指出 Vjeux 的异步组件WillMount hack ......),但是嗯,不能反应看看类型生命周期方法返回的对象,如果它是一个承诺,异步处理它,否则处理它同步(就像目前一样,即以向后兼容的方式)?

我喜欢@fkrauthan提出的建议。 一个生命周期方法load可以返回一个承诺或undefined和一个额外的异步renderToStringAsync函数。 但是应该始终在客户端和服务器上调用load 。 如果我们使用renderrenderToString ,返回的承诺将被忽略以匹配我们今天的行为。 如果我们使用renderToStringAsync并且load返回一个 Promise,则必须在渲染之前解析该 Promise。

为什么不像Next.js 那样,添加一个在构造函数之前调用的 React async getInitialProps生命周期函数? 如果没有这个方法,直接调用构造函数。

const props = await (Component.getInitialProps ? Component.getInitialProps(ctx) : {});
...
const app = createElement(App, {
  Component,
  props,
  ...
});

看起来 Next.js 设法完成了这项工作,除了它的getInitialProps没有挂钩到 React 组件生命周期中,因此它只能在顶级组件上调用。 那么这些想法就在那里并且可以结合在一起吗?

@sedubois我不认为 getInitialProps 是正确的地方。 首先,使用 ES6 的人没有/使用该方法。 其次,您不想处理 initalProps (这只是默认值),您想在 initalProps + 传入 props 的合并之上工作。 因此,我建议在componentWillMount之前调度一个新的生命周期事件。

我认为@Lenne231 的想法可能会导致一些问题,因为人们可能会在其他生命周期事件中中继该承诺已经得到解决。 现在,同一个生命周期事件有两种不同的行为,这有点棘手。

因此,在服务器和客户端上应该有一个同步渲染方法,它根本不调用那个新的生命周期。 还有一个异步版本,它调用该生命周期并始终等到承诺得到解决后再继续。 在我看来,这将为您提供最大的灵活性,因为您现在可以决定是在客户端渲染上也需要该生命周期,还是仅用于服务器渲染。

使用 ES6 的人没有/使用该方法

@fkrauthan也许您指的是getInitialState ? 这里我说的是一个新的, getInitialProps (Next.js 中使用的名称)。

至于您的第二点,是的,在componentWillMount之前使用新的生命周期方法可能比在constructor之前更好。 我猜在 Next 中情况并非如此,因为他们没有像前面提到的那样调整 React 生命周期的奢侈。 因此,为什么将这些想法带入 React 会很棒。

@sedubois即使是那个。 例如,我使用 es7 功能并在我的班级主体中定义static props = {}; 。 在我看来,这使得代码更易于阅读,我相信一旦 es7 正式发布,就会有越来越多的人转而使用它。

注意:类属性不是“ES7”特性,因为 ES7 已经完成并且唯一的新语言特性是求幂运算符。 您指的是第 2 阶段的类属性提案。 它还不是语言的一部分,可能会改变或被丢弃。

我不认为需要异步函数来响应获取数据,相反,我对可以按需 renderToString 的虚拟渲染器感到满意。 因此,您可以设置操作,并且虚拟树会在呈现为字符串之前相应地更新。 我认为这对于在反应中实现良好的 SSR 是理想的。 留给开发人员如何以及在何处获取数据。

我之前提到过,但是一个好的 api 应该类似于以下内容(对我之前建议的内容进行一些更新):

const virtualTree = ReactDOM.renderVirtual(rootEle);

// get the bundled query from redux store for example
const bundle = store.getState().bundle;

// Fetch the data according to the bundle
const data = await fetchDataSomehow(bundle);

// hydrate store (this will set updates on the virtual tree)
store.dispatch({type: 'hydrate', data});

// final result
const domString = virtualTree.renderToString();

这将避免一些 GraphQL 客户端(例如 Apollo)遇到的问题,即强制它们生成双重 renderToString。 第一个获取数据依赖关系,第二个渲染填充数据。 我认为这是非常浪费处理的,因为 renderToString 非常昂贵。

@bruno12mota我明白你的意思,但再次尝试模拟的库数量(并且使用你的解决方案仍然会有数千个库)对我来说是一个迹象,表明这可能应该是核心功能,而不是你必须做的事情建立在上面。 您的解决方案仍然需要更多的渲染调用(虚拟但仍然渲染调用)来获得输出。

@fkrauthan是的,但是虚拟渲染比必须对字符串进行两次渲染具有更高的性能,这是我认为的主要问题(SSR 中的性能),它是 React 的薄弱部分。 已经有一些实验来改进 react 的renderToString但是 react 团队在这个问题上并没有真正的进展(这里不是批评,他们做得很棒)。

我同意@bruno12mota 的观点,现在也考虑过这个问题,这是最简单的方法。 它让开发人员可以自由决定他们的渲染何时应该“刷新”。

它使一切变得更加复杂。

1.)从代码的角度来看(我查看了代码并使渲染异步应该更容易,然后创建一个只使用 VDom 的渲染器,然后可以在某一点转储)
2.) 现在您也必须重新考虑卸载事件。 它使自定义代码变得比需要的更复杂。 例如,加载数据可能会导致渲染一个也需要加载数据的新组件。 现在突然之间,lib 需要弄清楚哪个组件在哪个位置已经加载了数据,以及哪个组件包含新的数据加载和东西。

我和@bruno12mota 在一起。 VDom 可以说是一个更“低级”的解决方案,但对于 React 开发人员来说可能更容易实现,不会存在向后兼容性问题,并且可以允许社区尝试不同的更高级别的解决方案。

例如,一个更高级别的解决方案可能是一个“更高级别的组件”,它包装了异步componentWillMount方法,等待所有未决的承诺并在一切都解决后触发VDom.renderToString() 。 我在我的应用程序中对 SSR 使用了这种策略,但不幸的是我现在做了很多过度获取,因为没有 VDom。

我认为最好的方法是不改变我们目前拥有的任何功能。 这意味着不让 getInitialState、componentWillMount 或 renderToString 表现不同。

相反,我们应该添加新功能来解决这里讨论的问题。 就像有些函数只能在客户端调用一样,也可能有些函数只能在服务器上调用。

我认为我们可以向 react-dom/server 添加一个函数“renderToStringAsync”。 与普通的 renderToString 不同的是,它返回一个 Promise。

在实现方面,唯一的区别是异步版本会在创建组件实例时首先调用 & await "initialize()",然后再调用 render()。 如果组件没有“初始化”方法,它可以立即调用 render()。

因此,如果我们有以下示例 App 结构:

    ComponentB
    ComponentC
        ComponentD
        ComponentE

该过程将是:

1) instanciate componentA
2) await componentA.initialize();
3) componentA.render()
4) do in parallel(
    instanciate componentB, await componentB.initialize(), componentB.render()
    instanciate componentC, await componentC.initialize(), componentC.render(), do in parallel(
        instanciate componentD, await componentD.initialize(), componentD.render()
        instanciate componentE, await componentE.initialize(), componentE.render()
    )
)
5) render to string

所以,基本上,我们只需要一个新函数“renderToStringAsync”,它使用一个新的可选函数“initialize”。 这不会那么难。

@VanCoding和我想了一段时间的想法一样。 我想知道是否也可以在客户端的 componentDidMount 中自动调用初始化?

所以在服务器上它将是:

initialize()
render()

在客户端它将是:

render() // without data (unless available synchronously)
componentDidMount()
initialize()
render() // with data

我有一个使用 react-router v4 和 react-apollo 和 GraphQL 的反应服务器渲染的工作实现。

它的灵感来自server-side-rendering-code-splitting-and-hot-reloading-with-react-router

基本上服务器渲染使用同步组件,客户端渲染使用异步组件,以便 webpack 异步加载和执行模块 javascript。

顺便说一句,服务器渲染是使用 Nashorn 脚本引擎 (JVM) 实现的。

客户端渲染代码
https://github.com/shendepu/react-ssr-starter-kit/blob/apollo/src/main.js#L63 -L82

服务器渲染代码
https://github.com/shendepu/react-ssr-starter-kit/blob/apollo/src/main.js#L128 -L155

在路由中区分同步或异步组件有一个重要的地方。
Route 中的同步分量
https://github.com/shendepu/react-ssr-starter-kit/blob/apollo/src/routes/Counter/Route.js#L10

RouteAsync 中的异步组件
https://github.com/shendepu/react-ssr-starter-kit/blob/apollo/src/routes/Counter/RouteAsync.js#L7 -L23

注意:文件名必须是 Route.js 或 RouteAsync.js。 代码应始终导入 RouteAsync.js。 如果 Webpack 是为服务器渲染而构建的,它将用 Route.js 替换它。
https://github.com/shendepu/react-ssr-starter-kit/blob/apollo/build/webpack.config.js#L72 -L80

所以会有两个版本的构建distdist_ssrdist用于客户端,而dist_ssr用于只有两个块的服务器:供应商和应用程序. 商店状态被导出并嵌入到 index.html 的<script>中作为__INITIAL_STATE__ 。 有两个版本的构建的原因是ReactDomServer.renderToString()还不支持异步组件。

两个版本构建方法的唯一问题是在开发模式下存在警告:
React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:

但是由于客户端和服务器版本之间的唯一区别是 Route.js 和 RouteAsync.js,所以可以忽略警告,在生产模式下不会显示警告。

对于在服务器端渲染组件之前的异步数据获取:

  • Rest API:在组件使用中添加了静态 loadData
const { matchedRoutes, params } = matchRoutesToLocation(rootRoute.routes, 
                                                        location, [], {}, rootRoute.pattern)
matchedRoutes.filter(route => route.component.loadData).map(route =>
              route.component.loadData(store, params))

探测并执行loadData 。 matchRoutes 是一个匹配的路由数组,其父级来自索引0loadData可以按顺序或并行执行。

  • GraphQL 查询:只需使用getDataFromTree react-apollo/server即可执行所有 GraphQL 查询。

顺便说一句:使用 Nashorn 脚本引擎有一个性能优化机会:由于供应商包含库代码和 polyfill,它们执行一次并重用,因此对于以后的请求,只执行应用程序块。 由于库被移动到供应商块中,应用程序仅在 src 中包含很小的 javascript 代码,因此与每次评估供应商块相比,它的性能提升。 (所有 javascript 编译一次并缓存,为每个请求执行。)

我谎称应用程序块仅包含 src 代码。 它还包含babel-runtimecore-js/library引用的重复babel-runtimebabel-runtime不能被移动到供应商块中,因为它没有 index.js 或 main 条目,因此 webpack 无法将其识别为模块。 但代码量很小。

让我们进一步详细说明@VanCoding的示例: https :

也许初始化不是一个好名字——这个名字应该清楚地表明它只在服务器上使用,或者在渲染延迟到这个函数返回的环境中使用——也就是不在客户端。 想法: getStaticProps , getServerProps , getInitialProps , getAsyncProps ...

此外,最好听取核心团队的意见,开发这种 renderToStringAsync 方法是否存在技术或其他障碍。

@nmaro好吧,我认为该函数不应该返回任何东西,所以我更喜欢不以“get”开头的名称,但除此之外,我仍然认为这将是最好的方法。 我不介意它最终如何称呼。

此外,我已经用大约 40 行代码对此进行

因此,如果 React 团队仍然不希望这种情况发生,那么在 3rd 方库中做到这一点并不难。

或者,我们可以为 renderToString 添加一个选项,如
renderToString(Component, {async: true})

@nmaro这将使函数的返回类型依赖于提供的选项,我认为这不是好的做法。 我认为一个函数应该总是返回相同类型的结果。

@VanCoding你有一个如何使用你的代码作为第三方库的例子吗?

@VanCoding酷!但老实说,我认为很难将其集成到 react 的内部渲染算法中。查看https://github.com/facebook/react/blob/master/src/renderers/dom/stack/server/ReactServerRendering.js并迷失在反应的内部...... IMO我们真的需要来自某人的意见核。

@sedubois,您可以像使用普通的 renderToString 函数一样使用该函数。 但它返回一个解析为字符串的 Promise,而不是直接返回一个字符串。

所以使用它看起来像:

var react = require("react");
var renderAsync = require("react-render-async");
var MyComponent = require("./MyComponent");

renderAsync(react.createElement(MyComponent,{some:"props"}).then(function(html){
    console.log(html);
});

理论上,它应该能够像普通的 renderToString 一样渲染任何现有的 react 组件。 唯一的区别是也支持异步组件。

@nmaro你是对的。 那会很有帮助。

这方面的现状如何?

@firasdib我认为,我们仍在等待一些反应开发者发表评论。

+1

Google 索引问题需要 SSR,下面描述的所有包袱都由我们来处理,因此这个问题对于 React 作为一个通用库的成功非常重要。

我认为如果 JavaScript 提供了一种等待承诺的正确方式,那么整个异步渲染是不必要的。 然后我们可以传递一个指示天气的参数,我们希望数据加载是同步的(对于 SSR)还是异步的(对于真正的 Web 浏览器)。 数据加载逻辑可以在任何 React 组件的构造函数中启动一个 Promise,并且可以在 componentWillMount 中完成等待。

然而,据我了解,React 开发人员对 componentWillMount 的有用性表示怀疑,并建议让构造函数完成所有工作。 无论哪种方式都会奏效。

有些人认为,使组件成为纯视图可以解决这个问题。 它们是部分正确的。 不幸的是,这在最一般的情况下不起作用,在渲染树完成之前我们不知道要加载哪些数据。

问题的核心是 render 函数可以有逻辑来选择要添加到 DOMTree 的组件。 这就是 React 如此强大且非常不适合服务器端渲染的原因。

IMO 真正的解决方案是能够通过反应注册所有已发布的数据加载承诺。 一旦所有数据加载承诺完成,react 就会用渲染结果回叫我们。 这有很多含义:

  • DOMtree 将被渲染多次。 它就像一个实时系统,从不稳定的数据加载状态“稳定”到稳定的“加载”状态。
  • 在有缺陷的系统中存在系统无法达到稳定“加载”状态的风险。 必须引入超时来处理它。

结果是在服务器端对库的使用更加“计算密集”,过程并不快。

因此,我仍在考虑一种解决方法,它会损害通用案例以支持完成工作:)

同时,我们必须在异步数据加载环境中解决 SSR。

  1. 数据加载调用必须从请求 URL 派生(react-router 就是一个例子)。 将所有数据加载承诺收集到一个承诺列表中。
  2. 使用 Promise.all(),它本身就是一个承诺。 当这个承诺完成时,将加载的数据传递给服务器渲染函数。

该模型不依赖于特定的设计架构。 似乎flux/redux 可能会稍微受益,但我不确定,因为我已经在我的应用程序中放弃了redux 模型。

这个模型的问题是上面的#1。 一个简单的应用程序知道所有的数据加载调用。 在具有独立“模块”的复杂应用程序中,此模型需要对原始 React 组件树甚至渲染逻辑进行某种复制。

我看不出如何在不重复已在 render() 函数中编写的逻辑的情况下使 SSR 用于复杂项目。 Flux 可能是一个解决方案,但我不确定。

项目#1 的实现是模糊的。 在 react-router 示例中,我们必须作为路由器返回属于该路由的顶级组件。 我认为我们可以实例化这些组件:

app.use(function(req, res, next) {
    match({ routes, location: req.url }, (error, redirectLocation, renderProps: any) => {
    if (error) {
        res.status(500).send(error.message)
    } else if (redirectLocation) {
        res.redirect(302, redirectLocation.pathname + redirectLocation.search)
    } else if (renderProps) {
        // You can also check renderProps.components or renderProps.routes for
        // your "not found" component or route respectively, and send a 404 as
        // below, if you're using a catch-all route.
        console.log("renderProps", renderProps)
        for (let eachComp of renderProps.components) {
            // create an instance of component
            // ask component to load its data
            // store data loading promise in a collection
            console.log("eachComp: ", eachComp)
        }
        res.status(200).send(renderToString(<RouterContext {...renderProps} />))
        } else {
        res.status(404).send('Not found')
        }
    })
});

因此,顶级组件不再是“纯”组件。 由于能够触发数据加载,这些组件现在扮演顶级控制器的角色。 通量模型并没有让它变得更好。 它只是将数据加载功能关联的路由移动到不同的模块中。

不幸的是,继续以这种方式将数据加载到顶级控制器的子级会导致为反应渲染目的而创建的对象图的重复。 为了简单起见(如果可能的话),我们必须将所有数据加载到顶级控制器级别。

这就是我所看到的。

如果我意识到这些 SSR 的含义,我会三思而后行 React 对谷歌可索引应用程序的有用性。 或许 Flux 是一种解决方案,但 Flux 使应用程序比简单的 React 应用程序更复杂。 我去过那儿。 我必须在多个文件中追踪我的逻辑,而不是简单的数据加载功能。 在我有一个项目运行之前,在理论上它看起来真的非常好。 新人很难开始。 从代码库中拉出 redux 后就没有这样的问题了。 我认为这是所有 UI 设计复杂性的答案 :)

从本周的React Conf 开始,在 React Fiber (React 16) 发布后,他们计划进行以下工作(据说是 React 17):

screen shot 2017-03-16 at 15 55 51

意味着异步 React 生命周期方法可能会出现?

好吧,纤维材料不是主要用于更快/更平滑的重新渲染吗? 在服务器上,我们只渲染一次,所以我不确定这些更改是否会对服务器端渲染产生任何影响。

@VanCoding更新了我上面有点不清楚的消息。 我的意思是他们光纤

对于像我这样登陆这里的人,我最终使用可能有帮助的 react 创建了一个自定义实现。

https://github.com/siddharthkp/reaqt

关键是仅在服务器上运行的 asyncComponentWillMount。 它仅适用于入口点组件,而不是所有组件,以避免此处提到的问题。

@siddharthkp我真的没有看到您的组件试图解决什么? 如果它只支持顶级 Application React 组件的 asyncComponentWillMount,我为什么要使用你的库呢? 我可以在外面解决这个承诺,然后调用渲染? 还是我错过了什么?

@fkrauthan

我可以在外面解决这个承诺,然后调用渲染?

你是对的,这正是它的作用。

吸引力在于您可以通过一个命令获得异步 ssr(+ 代码拆分 + hmr)。 我看到很多人没有实现 ssr,因为它不是超级容易,这就是动机。

它仅支持顶级应用程序 React 组件的 asyncComponentWillMount

  1. 不必只有一个应用程序组件。 您可以获取每个页面/屏幕/入口点的数据。 (在这里推广多个入口点)

  2. 该模式是有意的,如果树中的每个组件都想要它自己的数据,我们最终可能会鼓励一个糟糕的性能模式。 在顶级组件中获取数据并将其作为 props 传递给子组件以对其进行检查。

@siddharthkp你的 repo 如何改进 Next.js(在 React Conf BTW 上也有介绍)?

https://github.com/zeit/next.js

无论如何,关于第三方库的讨论都是 IMHO 题外话,我们感兴趣的是 React 本身的支持。

关于第三方库的讨论是 IMHO 题外话,我们感兴趣的是 React 本身的支持。

我完全同意。 可以在reqt issues中讨论具体

我有一个用例是使用 react-native 和 react-native-vector-icons。 我希望组件的道具由从承诺中收到的数据确定

const AsyncUser = props => fetchUser()
  .then(data => (
     <User {...data} />
   ))

如果支持,我认为这将使许多复杂的代码更容易理解,并可能开辟新的异步模式。

我们有同样的问题,我想做 ssr,不仅搜索预测道具,还搜索 componentWillMount 中的操作获取的内容,我如何等待道具准备好,然后将道具渲染为字符串。

如果有人觉得这很有用,我已经编写了一个名为react-frontload的库,它允许您将一个返回 promise 的函数绑定到一个组件,以便在组件呈现时在服务器和客户端呈现时运行。

该函数在服务器渲染上“同步”运行(不是真的;-),但最终组件渲染被阻塞,直到它解决),并且在客户端渲染上正常异步运行,如果页面只是在客户端上不会再次运行服务器渲染。 您还可以指定更多选项,以便在每个组件的基础上对其运行时间进行更细粒度的控制。

它对于数据加载非常有用 - 几乎是我编写它来解决的用例。 试试看! 😄

https://github.com/davnicwil/react-frontload

<AsyncComponent 
  delayRendering={LoadingComponent}
> 
   {/*return a promise that returns a component here*/}
</AsyncComponent>

考虑一下您不必使用上述方法的所有条件渲染,因为您知道自己拥有数据

加入这个。 我认为它仍然是 React 缺乏的一个基本特性。 也同意即使第三方库可能涵盖这个主题,这应该直接来自内部。

非常好的异步组件接口,@timurtu。

...大家好,我只是想在这里插话,因为我有很多与这个线程直接相关的东西,而且我已经跟踪这个线程很长时间了:

  • 一方面,考虑到 SSR,我认为路由级数据获取是最有意义的——原因如下:
    Redux-First Router 数据获取:解决异步中间件的 80% 用例
  • 使用 Redux 进行服务器渲染现在非常简单,这里有一个使用Redux-First Router的分步指南,说明如何像您以前从未见过的那样进行操作: Server-Render like a Pro /w Redux-First Router in 10 steps
  • 最后,要正确进行代码拆分,您还必须同时进行(即 SSR + 拆分) 。 事实证明,同时做这两件事一直是一个主要的痛点,很少有人成功实现,而且直到现在才有一个通用的包来帮助社区。 React Universal Component + babel-plugin-universal-import + webpack-flush-chunks (和extract-css-chunks-webpack-plugin )是真正解决这个问题的一系列包。 并且是第一次。 基本上,它允许您在服务器上同步渲染组件,但更重要的是,将渲染的确切块传输到客户端,以便在客户端上进行初始同步渲染。 你甚至可以通过 webpack 插件获得 css 块样式表。 这与@timurtu共享的内容和许多此类组件不同,后者需要加载main.js 、解析、评估、渲染,最后几秒钟后请求动态导入。 这些是解决无样式内容闪存 (FOUC) 的解决方案,但不是在您的页面中嵌入服务器上呈现的确切块。 Universal——我的软件包系列的名称——允许您以更快的时间直到交互式(TTI) 以一种像传统 web 应用程序一样运行的同步方式来执行此操作。 你可以在这篇文章中了解更多信息: React Universal Component 2.0 & babel-plugin-universal-import

我知道我没有解决路由级别组件级别的优缺点,但第一个链接主要关注这一点。 基本上它归结为组件级别对 SSR 不利,如果您有嵌套组件,其数据必须按顺序(而不是并行)获取。 而且,如果您正确地每条路线只有一次提取,那么您最好通过将数据依赖项附加到路线实体来正式化合同,而不是在componentDidMount 。 上面的许多其他人也得出了同样的结论。 Redux-First Router按照使 Redux 工作起来非常愉快的那种“合同”的方式很好地形式化了这一点。

Redux-First Router让许多旧范式再次焕然一新,但令人惊讶的是,它恰好适合作为 Redux 生态系统中缺失的一部分。 如果你曾经想要一个原生于 Redux 工作流程的路由器,试试 Redux-First Router。 它相对较新,而且我是开源的新手,但我已经为此工作了一年,并且在发布的 2 个月内获得了很大的关注。 人们很喜欢它。 我真的希望你检查一下并试一试:)。

这也是它的发布文章,它解释了它如何比 React Router 成为 Redux 更好的解决方案,以及它如何以同样出色的 Redux-Little Router 不幸错过的方式实现愿景:
预发布:Redux-First Router — 超越 Redux-Little-Router 的一步

RLR 和 RFR 之间的主要区别在于,RFR 为每个路由调度不同的操作类型(即 URL 更改),而不是总是只是LOCATION_CHANGED 。 这使您可以将 URL 更改切换为 reducer 中的唯一类型,就像任何其他操作一样。 它可能看起来很小,但它有很大的不同。 因此,它不需要<Route />组件,这在 Redux 领域是不必要的。 Redux-First Router 还支持大量功能,从路由thunks数据获取到 React Native、React Navigation、代码拆分、预取等等。

很想听听人们的想法。

@faceyspacey谢谢,我认为它解决了我遇到的问题,例如,当您想基于异步数据设置道具时, react-native-vector-icons可以返回一个图标作为承诺。 这绝对是一种不同的看待事物的方式,但它减少了componentWillMount或 redux X_GET_STARTX_GET_ENDX_GET_ERROR将异步组件绑定到状态的动作样板,当您真的只想提出一次请求来呈现组件时。 如果您正在做诸如轮询数据之类的事情,那么有状态并使用 redux 可能更有意义

@faceyspacey我明白你在说什么关于路由级别,但是让更小的组件只依赖于获取他们需要的数据同时同步渲染它们周围的所有其他东西不是更好吗?

更好的? 在哪里? 在 React Native 上? 在 Facebook 等大型组织的 React Native 应用程序中?

也许在 React Native 上。 在我看来,当有 SSR 时,就没有争论了。 如果每个请求没有多次提取,您想要做的事情根本不可能,除非您再次将其限制为一个可以进行提取的组件。 此时,您最好与路线实体正式签订合同。

至于 React Native 和 SPA,当然可以。 但是测试内置数据部门的组件是一件痛苦的事情。 Apollo 在这里有一些东西,但最后我检查了他们有一个很长的待办事项列表,以便真正正确地进行数据配对组件测试,即无缝。 我更喜欢这样一种设置,即您拥有一个易于设置和模拟的商店。 您可以在所有测试中重用您如何做到这一点。 然后从那里测试组件是带有快照的简单同步渲染。 一旦你在组件中添加了数据 deps,测试 React 就变得不那么直观了。 但是人们会这样做并使其自动化。 标准较少,现在更接近于自定义的临时代码。 拥有一个可以以非常明显的方式异步填充的商店更有效率,而不是为每个需要填充的异步组件制定潜在的不同策略。

如果没有 SSR 并且您的测试策略无缝且不碍事,我不反对组件级别。 如果您是大型公司,那是有道理的,因为每个开发人员都不需要触及顶级路线并可能为其他人破坏它。 这就是 FB 使用 Relay 和 GraphQL 开创这条路线的原因。 事实上,世界上只有 100 家公司真正在这条船上。 因此,对于大多数人/应用程序/公司而言,我认为托管比现实更炒作。 一旦你掌握了路由级别的东西并拥有一个像Redux-First Router这样做得很好的包,那么对于允许成员

https://www.codesandbox.io

基本上,一旦你看到它是如何完成的,我很想听听你的意见。 请记住,这是一个 SPA。 因此,您必须查看演示 repo样板文件以获取 SSR 示例。 但这是一个好的开始。

我认为最简单的方法是支持异步渲染方法(渲染返回一个承诺)。 它将允许根组件等待子组件渲染,当然渲染结果将是一个承诺。 我在这里实现了类似的东西https://github.com/3axap4eHko/preact-async-example

@faceyspacey谢谢你上面的详细回答。 我真的很喜欢 Redux-First Router 的想法,它绝对解决了我所有的问题,并且让事情比我们以前的更加透明和干净。

太谢谢了!

@raRaRa很高兴你喜欢它! ...最大的挑战之一是到目前为止还没有一个很好的路由级数据获取抽象。 React React v3 有回调,v4 有一个很好的模式(特别是react-router-config包),但它在 v4 中并不明显并且不是一流的。 这是事后的想法。 然而更重要的是,它并不存在于 Redux 中。 因此,redux 用户现在才真正第一次拥有完整的“路由级别”抽象。

我认为现在有了 RFR,我们将重新思考“路由级别”与“组件级别”的重要性。 Relay 和 Apollo 之类的东西围绕将数据依赖项与组件配对引起了很多炒作,但是如果您不使用这 2 个,您可能会在生命周期处理程序中执行某些操作而最终陷入痛苦的世界.

也就是说,最终 RFR 将与 Apollo 进行路线级集成。 除了前面提到的罕见例外,在组件级别做事几乎没有什么好处。 至少 80% 的时间(可能 100% 在您的应用程序中),您可以根据路线确定所需的所有数据。 如果不可能,很可能是因为您在“视图层”中派生数据,这些数据很可能被重新考虑为由 URL 解释。 因此,即使使用像 Apollo 这样的工具,您也会开始参与有问题的反模式,这些反模式可能最终会让您感到痛苦,因为让“状态”除了视图层之外别无他处。 即执行这些数据获取所需的状态。 您经常需要细化用于获取数据的确切参数。 因此,您在获取数据之前获取 state 和 props 并转换 2 以获取参数。 这就是现在存在于视图层中并受制于其重新渲染机制的东西(即,当生命周期处理程序被调用时)。 每个应用程序在某个时间点或另一个点最终都会在应该或响应接收/更改的不相关道具时未获取或获取数据。 而且在所有应用程序中,您都必须编写额外的代码来防止不相关的道具发生变化。 即不影响用于获取数据的参数的道具。

至于 React Native + Apollo,问题较少,因为您没有 SSR,因此可能会在服务器上发生顺序数据获取。 但是,当屏幕如此之小时,您真的需要组件级别的数据获取吗? 您通常知道每个“场景”在数据方面的需求。 它不像具有一百万个图表、多个表格等的桌面 Web 面板仪表板。这个用例可能是组件级别开始有意义的最大主要地方。 但即使在那里,您也可以在一个地方指定所有这些小部件的所有要求,并通过 Apollo 获取它们。 我还没有充分考虑,但基本思想是将您的 GraphQL 数据需求附加到路由,然后在您的组件中连接到数据,就像任何其他 Redux 数据一样。 所以更具体地说,这个想法是消除 react-redux 的connect和 Apollo 的等价物。 我只想要一个,我们希望它更平坦。 使用 Apollo 需要解构更深层次的嵌套数据,以访问您真正想要的组件。 愿景是一种看起来就像常规 Redux 的方式,加上路由级别的 graphql 规范。

现在谈论它有点为时过早,因为我几乎没有研究过它,但如果路由级别是您的应用程序的正确方法,那么 Apollo + GraphQL 不应该是一个例外。

最后,如果你所有的数据依赖和异步工作都与组件分离,那么测试就会变得容易得多。 当您可以测试组件而不必担心数据获取时,关注点分离是一个主要的生产力助推器。 当然,在您的测试中,您必须通过提前执行这些异步获取来填充您的商店,但是将它们与您的组件分开,您可以在一些setup帮助函数下轻松地形式化这些工作,所有测试都用于预先在测试组件如何呈现之前填充商店:)

@faceyspacey我完全同意。 但我必须承认,我没有尝试过 Apollo 或 GraphQL,我一定会检查它们,看看它们是否适合我的用例。

我非常习惯于使用 MVC 模式编写 Web 应用程序,我基本上将所有数据和依赖项拉到控制器上。 然后我将数据注入视图或业务逻辑。 就像您说的那样,这使得测试变得更加容易,因为视图没有与数据获取耦合。

我见过 MVC 应用程序,其中 View 部分正在获取数据并执行业务逻辑,这很糟糕。 我都不知道将获取哪些数据,因为它隐藏在某些视图/组件中,并且测试变得更难/不可能。

对我来说,Redux-First Router 很像 MVC 中的 Controller 部分,加上路由。 这很有意义:)

谢谢你。

归根结底,我们的新堆栈是这样的:

男:还原
五:反应
C: Redux-First Router(即将重命名为“Rudy”

当 React 出现时(尤其是 Redux),我们似乎都想抛弃古老的 MVC 智慧,好像它意味着我们可以完全摆脱开发以前应用程序的痛苦。 但我认为最终只是没有构建工具来支持它。 归根结底,我们仍然从 MVC 中受益匪浅,即使在响应式客户端优先的应用程序中也是如此。

在新的客户端优先堆栈与传统的服务器端 MVC 中,它的工作方式可能略有不同,但基本上它是相同的 3 关注点分离。

至于 3 之间的交互/行为差异是什么,基本上过去控制器会在初始阶段获取模型,然后用它们渲染视图; 现在是控制器 (RFR) 立即渲染视图(通过“UI 模型”即 Redux 状态选择),然后在控制器找到域模型(也存储在 Redux 状态)后第二次重新渲染视图

大家好,看这个链接。 这是数据服务器端渲染示例https://github.com/bananaoomarang/isomorphic-redux

@harut55555 idk 可能只是我,但这似乎需要很多代码来执行获取请求和发布请求

任何变化? 发布了 16 个反应,大多数解决方案都不起作用

我认为当前的方法是使用 redux 和 redux observable 或 redux thunk 等工具

我的使用案例

<App>
    <Page>
        <AsyncModule hre="different.com/Button.react.js" /> downloaded from external url on server or client
    </Page>
</App>

问题是在渲染App之前我不知道我会有什么样的组件

为什么不下载数据/内容而不是只是好奇的组件?

什么时候? 我有动态页面,该组件可能是也可能不是

我认为是路由问题,react 应该只渲染页面,而不是管理和请求渲染数据

使用来自 Apollo 的@graphql使数据加载变得非常简单,并且基于 React-Router v4 组件的 API 可以实现简单的可组合路由。 两者都与 Redux 中的应用程序状态正交。

为了进行等待数据获取的单通道流式渲染, render()应该可以返回一个承诺(在客户端上你就像现在一样,只有在服务器上你才返回一个承诺)。

然后@graphql可以在数据加载后简单地为 render() 返回一个承诺; 其余的只是简单的 React。

几个月前,我在 JSConf Iceland 做了一个演讲,描述了 React 中即将到来的异步渲染特性(见第二部分): https ://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react

现在@acdlite讨论了如何应用相同的概念来在 React 组件中启用异步等待服务器上的数据,并在标记准备好时逐步刷新标记: https :

希望你会喜欢看这些演讲! 我认为在一年左右的时间里,我们可能能够解决这个问题并为此制定一个正式的策略。

我记得响。 我们结婚了,一起规划未来。 请现在结束这一切,来见我。 时间工作总是爱

可以使用ReactDOMServer.renderToStaticMarkup遍历树,在它到达异步依赖项的位置放置一个书签(通常从 API 加载数据),并在解决这些依赖项时继续遍历树,直到解决整个树,然后执行最后的ReactDOMServer.renderToString以实际呈现为 HTML,这将是同步的,因为现在所有依赖项都已解决。

我在react-baconjs采用了这种方法: render-to-html.js 。 它的灵感来自@gaearon在另一期推荐这种方法的评论。

@steve-taylor 是的,这行得通。 这也是我在react-frontload 中使用的解决方法,这是一个更通用的解决方案,适用于任何 React 应用程序。

正如您所说,本质上它只是通过运行同步渲染逻辑两次并等待您在第一次渲染中遇到的所有数据加载承诺在运行第二次渲染之前解决所有数据加载承诺来伪造异步渲染。

尽管显然有点浪费,但它工作得很好(第一次渲染的输出不仅仅是承诺,它也是刚刚被丢弃的实际 HTML)。 当真正的异步服务器渲染进入 React 时,这将是惊人的。

@davnicwil 干得好! 我考虑过推广特定的异步 SSR 解决方案。 react-frontload 是否处理无限的异步深度? 问是因为你说“两次”。

@steve-taylor 干杯! 是的,如果深度是指树中组件的深度,它是无限的。 它允许您在组件本身(使用高阶组件)上声明数据加载函数,然后当在第一次渲染时遇到它时,它会运行并收集承诺。

当有更多的子组件也异步加载数据时,这些子组件将不起作用,然后根据结果动态呈现(如果有意义的话)。 这只是意味着应用程序必须结构化,以便没有这种嵌套,我在实践中发现这并不是一个真正的巨大限制。 事实上,在许多方面,它在用户体验方面更好,因为嵌套异步逻辑当然意味着串行请求和更长的等待时间,而扁平化意味着并行请求。

也就是说,嵌套问题(实际上可能是整个异步服务器渲染问题)可能会随着 Suspense 的内容在不久的将来进入 React 得到解决。 🤞

仅供参考,我们已经开始着手这方面的工作。

https://reactjs.org/blog/2018/11/27/react-16-roadmap.html#suspense -for-server-rendering

真棒@gaearon!

@davnicwil react-baconjs支持无限深度。

@gaearon我想知道这是否可以支持创建订阅中的可观察对象,以便我可以将可观察的触发JSX转换为反应组件?

我很想拥有这个功能有两个原因。 首先,在我的应用程序中,状态存储在网络工作者中。 因此,虽然检索组件所需的状态位是一个异步操作,但它需要大约 5 毫秒,而且 React 在等待数据时渲染任何内容是没有意义的。 其次,目前还没有通用的方法将 observable 转换为组件:如果您的 observable 同步发出第一个值,那么您所做的就是订阅以获取该值,然后取消订阅,然后再次订阅以侦听更改(这就是 Replay create-observable 文档中的主题示例)。 如果它异步发出第一个值,那么您最初会呈现 null 并侦听更改。

@steve-taylor react-frontload现在还支持无限深度的组件嵌套,在1.0.7版本中。

希望这个库对登陆这个线程的一些人有用 - 如果您正在寻找一个适用于客户端/服务器渲染的异步数据加载解决方案,并且集成工作非常少,您应该检查一下。

我们之前遇到过这个问题,因为我们的应用是用 React Hooks 构建的,然后我们创建了一个包react-use-api来解决它,这是一个自定义的钩子,可以获取 API 数据并支持 SSR。 我希望这个包裹可以帮助有需要的人。

但是,我们仍然期待等待官方的解决方案,就像react-frontload所说,没有内置的方法可以等待当前渲染开始后发生异步数据加载。

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