【问题标题】:How to implement SSR for Material UI's media queries in NextJs?如何在 NextJs 中为 Material UI 的媒体查询实现 SSR?
【发布时间】:2020-08-14 01:52:27
【问题描述】:

我无法遵循 documentation 实现 Material UI 的媒体查询,因为它是为普通的 React 应用程序指定的,而我正在使用 NextJs。具体来说,我不知道将文档指定的以下代码放在哪里:

import ReactDOMServer from 'react-dom/server';
import parser from 'ua-parser-js';
import mediaQuery from 'css-mediaquery';
import { ThemeProvider } from '@material-ui/core/styles';

function handleRender(req, res) {
  const deviceType = parser(req.headers['user-agent']).device.type || 'desktop';
  const ssrMatchMedia = query => ({
    matches: mediaQuery.match(query, {
      // The estimated CSS width of the browser.
      width: deviceType === 'mobile' ? '0px' : '1024px',
    }),
  });

  const html = ReactDOMServer.renderToString(
    <ThemeProvider
      theme={{
        props: {
          // Change the default options of useMediaQuery
          MuiUseMediaQuery: { ssrMatchMedia },
        },
      }}
    >
      <App />
    </ThemeProvider>,
  );

  // …
}

我想实现这个的原因是因为我使用媒体查询来有条件地渲染某些组件,如下所示:

const xs = useMediaQuery(theme.breakpoints.down('sm'))
...
return(
  {xs ?
     <p>Small device</p>
  :
     <p>Regular size device</p>
  }
)

我知道我可以使用 Material UI 的 Hidden,但我喜欢这种方法,其中媒体查询是具有状态的变量,因为我还使用它们来有条件地应用 css。

我已经在使用 styled components 和 Material UI 的 SRR 样式。这是我的_app.js

  import NextApp from 'next/app'
  import React from 'react'
  import { ThemeProvider } from 'styled-components'

  const theme = { 
    primary: '#4285F4'
  }

  export default class App extends NextApp {
    componentDidMount() {
      const jssStyles = document.querySelector('#jss-server-side')
      if (jssStyles && jssStyles.parentNode)
        jssStyles.parentNode.removeChild(jssStyles)
    }

    render() {
      const { Component, pageProps } = this.props

      return (
        <ThemeProvider theme={theme}>
          <Component {...pageProps} />
          <style jsx global>
            {`  
              body {
                margin: 0;
              }   
              .tui-toolbar-icons {
                background: url(${require('~/public/tui-editor-icons.png')});
                background-size: 218px 188px;
                display: inline-block;
              }   
            `}  
          </style>
        </ThemeProvider>
      )   
    }
  }

这是我的_document.js

import React from 'react'
import { Html, Head, Main, NextScript } from 'next/document'

import NextDocument from 'next/document'

import { ServerStyleSheet as StyledComponentSheets } from 'styled-components'
import { ServerStyleSheets as MaterialUiServerStyleSheets } from '@material-ui/styles'

export default class Document extends NextDocument {
  static async getInitialProps(ctx) {
    const styledComponentSheet = new StyledComponentSheets()
    const materialUiSheets = new MaterialUiServerStyleSheets()
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: App => props =>
            styledComponentSheet.collectStyles(
              materialUiSheets.collect(<App {...props} />)
            )   
        })  

      const initialProps = await NextDocument.getInitialProps(ctx)

      return {
        ...initialProps,
        styles: [
          <React.Fragment key="styles">
            {initialProps.styles}
            {materialUiSheets.getStyleElement()}
            {styledComponentSheet.getStyleElement()}
          </React.Fragment>
        ]   
      }   
    } finally {
      styledComponentSheet.seal()
    }   
  }

  render() {
    return (
      <Html lang="es">
        <Head>
          <link
            href="https://fonts.googleapis.com/css?family=Comfortaa|Open+Sans&display=swap"
            rel="stylesheet"
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )   
  }
}

【问题讨论】:

    标签: reactjs material-ui next.js server-side-rendering


    【解决方案1】:

    首先需要注意的是——我自己目前没有任何使用 SSR 的经验,但我有 deep knowledge of Material-UI,我认为通过您在问题中包含的代码和 Next.js 文档,我可以帮助您工作通过这个。

    您已经在您的_app.js 中展示了如何将您的theme 设置为您的样式组件ThemeProvider。您还需要为 Material-UI ThemeProvider 设置一个主题,并且您需要根据设备类型在两个可能的主题之间进行选择。

    首先定义你关心的两个主题。这两个主题将使用 ssrMatchMedia 的不同实现——一种用于移动设备,一种用于桌面。

    import mediaQuery from 'css-mediaquery';
    import { createMuiTheme } from "@material-ui/core/styles";
    
    const mobileSsrMatchMedia = query => ({
      matches: mediaQuery.match(query, {
        // The estimated CSS width of the browser.
        width: "0px"
      })
    });
    const desktopSsrMatchMedia = query => ({
      matches: mediaQuery.match(query, {
        // The estimated CSS width of the browser.
        width: "1024px"
      })
    });
    
    const mobileMuiTheme = createMuiTheme({
      props: {
        // Change the default options of useMediaQuery
        MuiUseMediaQuery: { ssrMatchMedia: mobileSsrMatchMedia }
      }
    });
    const desktopMuiTheme = createMuiTheme({
      props: {
        // Change the default options of useMediaQuery
        MuiUseMediaQuery: { ssrMatchMedia: desktopSsrMatchMedia }
      }
    });
    

    为了在两个主题之间进行选择,您需要利用请求中的用户代理。这里是我的知识很浅的地方,所以这里的代码中可能存在一些小问题。我认为您需要使用 getInitialProps(或 Next.js 9.3 或更高版本中的 getServerSideProps)。 getInitialProps 接收 context object ,您可以从中获取 HTTP 请求对象 (req)。然后,您可以使用与 Material-UI 文档示例中相同的方式使用 req 来确定设备类型。

    下面是我认为_app.js 应该看起来的近似值(未执行,因此可能存在轻微的语法问题,并且在getInitialProps 中有一些猜测,因为我从未使用过 Next.js):

    import NextApp from "next/app";
    import React from "react";
    import { ThemeProvider } from "styled-components";
    import { createMuiTheme, MuiThemeProvider } from "@material-ui/core/styles";
    import mediaQuery from "css-mediaquery";
    import parser from "ua-parser-js";
    
    const theme = {
      primary: "#4285F4"
    };
    
    const mobileSsrMatchMedia = query => ({
      matches: mediaQuery.match(query, {
        // The estimated CSS width of the browser.
        width: "0px"
      })
    });
    const desktopSsrMatchMedia = query => ({
      matches: mediaQuery.match(query, {
        // The estimated CSS width of the browser.
        width: "1024px"
      })
    });
    
    const mobileMuiTheme = createMuiTheme({
      props: {
        // Change the default options of useMediaQuery
        MuiUseMediaQuery: { ssrMatchMedia: mobileSsrMatchMedia }
      }
    });
    const desktopMuiTheme = createMuiTheme({
      props: {
        // Change the default options of useMediaQuery
        MuiUseMediaQuery: { ssrMatchMedia: desktopSsrMatchMedia }
      }
    });
    
    export default class App extends NextApp {
      static async getInitialProps(ctx) {
        // I'm guessing on this line based on your _document.js example
        const initialProps = await NextApp.getInitialProps(ctx);
        // OP's edit: The ctx that we really want is inside the function parameter "ctx"
        const deviceType =
          parser(ctx.ctx.req.headers["user-agent"]).device.type || "desktop";
        // I'm guessing on the pageProps key here based on a couple examples
        return { pageProps: { ...initialProps, deviceType } };
      }
      componentDidMount() {
        const jssStyles = document.querySelector("#jss-server-side");
        if (jssStyles && jssStyles.parentNode)
          jssStyles.parentNode.removeChild(jssStyles);
      }
    
      render() {
        const { Component, pageProps } = this.props;
    
        return (
          <MuiThemeProvider
            theme={
              pageProps.deviceType === "mobile" ? mobileMuiTheme : desktopMuiTheme
            }
          >
            <ThemeProvider theme={theme}>
              <Component {...pageProps} />
              <style jsx global>
                {`
                  body {
                    margin: 0;
                  }
                  .tui-toolbar-icons {
                    background: url(${require("~/public/tui-editor-icons.png")});
                    background-size: 218px 188px;
                    display: inline-block;
                  }
                `}
              </style>
            </ThemeProvider>
          </MuiThemeProvider>
        );
      }
    }
    

    【讨论】:

    • 我编辑了这个,但现在我发现它没有立即应用:初始化 deviceType 时起作用的是const deviceType = parser(ctx.ctx.req.headers["user-agent"]).device.type || "desktop";
    • @ArturoCuya 奇怪。基于documentation 我希望它只是ctx.req 而不是ctx.ctx.req 但只要它有效....
    • 很酷的好答案。但不要在您的 _app.js 文件中使用 getInitialprops 执行此操作。如果你这样做了,那么下一个 js 将服务器渲染你的所有页面,这意味着自动静态优化将不起作用。
    猜你喜欢
    • 2017-04-24
    • 2020-12-20
    • 2021-04-09
    • 2020-05-12
    • 2019-07-09
    • 2015-07-15
    • 2022-12-20
    • 2022-11-09
    • 1970-01-01
    相关资源
    最近更新 更多