如何在NextJs中为Material UI的媒体查询实现SSR?

时间:2020-04-29 20:15:54

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

我无法遵循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>
    )   
  }
}

1 个答案:

答案 0 :(得分:5)

首先需要注意的是-我目前没有任何使用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的外观的近似值(未执行,因此可能存在较小的语法问题,并且由于我从未使用过Next.js而在getInitialProps中有一些猜测):

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>
    );
  }
}