【问题标题】:NextJS Track Mounted Components in SSRSSR 中的 NextJS 轨道安装组件
【发布时间】:2018-11-21 02:56:55
【问题描述】:

以下内容适用于通过 NextJS 的 SSR。

我正在使用 React 的上下文来跟踪某些已安装组件的 ID。要点是

class Root extends React.Component {
  getChildContext () {
    return {
      registerComponent: this.registerComponent
    }
  }

  registerComponent = (id) => {
    this.setState(({ mountedComponents }) => {
      return { mountedComponents: [...mountedComponents, id ] }
    })
  }

  ...
}

class ChildComponent {
  static contextTypes = { registerComponent: PropTypes.func }

  constructor(props) {
    super(props)

    props.registerComponent(props.id)
  }
}

不幸的是,这只适用于客户端。 this.state.mountedComponents 在服务器上始终为 []。是否有其他方法可以跟踪这些组件服务器端?基本上,我需要将 ID 提供给在文档的 head 中运行的脚本 - 等待客户端应用程序手动安装、运行和附加到头部有点太慢了。

更新

这里有一个简单的示例 repo:https://github.com/tills13/nextjs-ssr-context

this.contextChild 的构造函数中是undefined,如果我将它移动到componentDidMount(目前在repo 中以这种方式设置),它可以工作,但我希望这可以解决服务器-边。我没有死心塌地context,如果有其他方法可以做到这一点,我会全力以赴。

【问题讨论】:

  • 你用的是哪个版本的nextjs?这些上下文的 API 是实验性的。 react 16 有新的 API。
  • 我正在使用 Next 6.0。 “新”上下文 API (React.createContext) 在 SSR 中根本不起作用。讨论于github.com/zeit/next.js/issues/4182 / github.com/zeit/next.js/issues/4194
  • 尝试将函数调用从构造函数移到componentDidMount
  • @gandharvgarg 没有达到目的,因为componentDidMount 只运行客户端。我需要在服务器端完成此操作。
  • @TarunLalwani 更新

标签: reactjs server-side-rendering nextjs react-context


【解决方案1】:

我对此进行了深入研究,我的感觉是,由于多种原因,这是不可能的。

setState 更新是异步的,不会在您的情况下执行。

provider 的渲染甚至会在状态更新之前发生,因此注册代码将在稍后执行并且渲染函数没有最新状态

我输入了不同的console.log,下面是我得到的

Provider constructed with { mountedComponents: [],
  name: 'name-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
getDerivedStateFromProps
Renderring Provider Component { mountedComponents: [],
  name: 'name-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
data loaded
constructor Child { id: '1' } { registerComponent: [Function: value] }
Register Component called 1 { mountedComponents: [],
  name: 'name-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
Registering component 1
constructor Child { id: '2' } { registerComponent: [Function: value] }
Register Component called 2 { mountedComponents: [ '1' ],
  name: 'tarun-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
Registering component 2
constructor Child { id: '3' } { registerComponent: [Function: value] }
Register Component called 3 { mountedComponents: [ '1', '2' ],
  name: 'name-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
Registering component 3
constructor Child { id: '4' } { registerComponent: [Function: value] }
Register Component called 4 { mountedComponents: [ '1', '2', '3' ],
  name: 'name-Sun Jun 24 2018 19:19:08 GMT+0530 (IST)' }
Registering component 4

该信息实际上仅在出现componentDidMount 时可用。但是由于在服务器端渲染没有发生的情况下,您不会获得数据。

我正在进一步挖掘,看看是否有任何黑客可用,但目前我不确定

【讨论】:

    【解决方案2】:

    在构造函数中使用 context API 是可能的,您只需在 构造函数super 中将其作为参数传递方法如下:

    export class Child extends React.Component {
      static contextTypes = { registerComponent: PropTypes.func };
    
      constructor(props, context) {
        super(props, context);
        context.registerComponent(props.id);
      }
    
      render() {
        return <div id={this.props.id} />;
      }
    }
    

    更新:
    因此,问题与不支持任何交互性的 服务器 上的第一个渲染有关, 此方法将阻止在服务器上考虑任何setState,在您的示例中为&lt;Provider /&gt; 不会给予任何帮助,因此我们需要一种解决方法,在任何情况下都是一种破解。

    我得到的解决方案是为每个跟踪的组件渲染一个新的window.ids(使用从 nextjs 提供的 Head):

    export class Child extends React.Component {
      registerComponent = (id) => `
        if (window.ids) {
          window.ids = [...window.ids, ${id}];
        } else window.ids = [${id}];
      `;
    
      render() {
        return (
          <Fragment>
            <Head>
              <script
                className="track"
                dangerouslySetInnerHTML={{
                  __html: `${this.registerComponent(this.props.id)}`,
                }}
              />
            </Head>
            <div id={this.props.id} />
          </Fragment>
        );
      }
    }
    

    所以window.ids 变量将在&lt;App /&gt; renders 之前可用。
    这是一个repos link 来测试它。

    另一种解决方案可能是在服务器上使用 全局变量,然后在每个被跟踪的组件中我们可以改变 该全局变量使用 componentWillMount 生命周期钩子,因为它将仅在服务器上执行,然后 在 html 模板 &lt;head /&gt; 上注入这些 id,但如果我们正在执行 renderToString 方法,这是可能的。

    第二种解决方案:使用 pages/_document 而不是 pages/_app,这样我们就可以访问服务器 before it renders to string!:
    这是回购分支:origin/workaround-using-document

    -子组件:

    export class Child extends React.Component {
      render() {
        return (
          <Fragment>
            <Head>
              <meta className="track" id={this.props.id} />
            </Head>
            <div id={this.props.id} />
          </Fragment>
        );
      }
    }
    

    -文档组件(替换App):

    export default class MyDocument extends Document {
      render() {
        const { head } = this.props;
        const idsFromHead = head.reduce(
          (acc, { props }) =>
            (props.className.includes('track') && [...acc, props.id]) || acc,
          []
        );
    
        return (
          <html>
            <Head>
              <script
                dangerouslySetInnerHTML={{
                  __html: `window.ids=[${idsFromHead}]`,
                }}
              />
            </Head>
            <body className="custom_class">
              <h3>{idsFromHead}</h3>
              <Main />
              <NextScript />
            </body>
          </html>
        );
      }
    }
    

    这种方法 100% 在服务器上工作,因为我们可以在服务器渲染发生之前捕获跟踪的 id。

    但是&lt;NextScript /&gt; 正在生成警告(不知道为什么,可能是来自 nextjs 的错误):

    【讨论】:

    • 你说的是对的,但是你有没有测试过这是否真的解决了问题?
    • @TarunLalwani 是的,即使是 &lt;Provider /&gt; 也会在任何被跟踪的组件之前先被渲染,我有一个解决方法,所以我会用详细信息更新响应。
    • 嗯。有趣 - 就目前而言,第二种方法有效,但在页面转换之间有不稳定的行为。
    • 是的,水化过渡很奇怪,如果我删除&lt;NextScript /&gt;,我将得到一个带有&lt;head&gt; 标签的干净html SSR(用于注入脚本、样式和元数据)并且没有警告,但是在 dom 上没有 react 组件,甚至 pages/_app.js 文档在底层使用 &lt;NextScript /&gt; 来注入捆绑的 js 为死组件带来活力,next 就不行了!
    • @TylerSebastian,试图深入挖掘该警告,但我无法再次复制它,它已消失,我认为它与一些兑现的东西有关!,我刚刚收到 HMR 警告firefox,但是 chrome 没问题,你能验证一下吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-20
    • 2010-10-12
    • 1970-01-01
    • 2013-03-21
    • 2019-08-02
    • 2021-12-29
    • 2021-02-14
    相关资源
    最近更新 更多