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

Posted

技术标签:

【中文标题】如何在 NextJs 中为 Material UI 的媒体查询实现 s-s-r?【英文标题】:How to implement s-s-r for Material UI's media queries in NextJs? 【发布时间】: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 s-s-rMatchMedia = 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:  s-s-rMatchMedia ,
        ,
      
    >
      <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】:

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

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

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

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:  s-s-rMatchMedia: mobileSsrMatchMedia 
  
);
const desktopMuiTheme = createMuiTheme(
  props: 
    // Change the default options of useMediaQuery
    MuiUseMediaQuery:  s-s-rMatchMedia: 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:  s-s-rMatchMedia: mobileSsrMatchMedia 
  
);
const desktopMuiTheme = createMuiTheme(
  props: 
    // Change the default options of useMediaQuery
    MuiUseMediaQuery:  s-s-rMatchMedia: 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 将服务器渲染你的所有页面,这意味着自动静态优化将不起作用。

以上是关于如何在 NextJs 中为 Material UI 的媒体查询实现 s-s-r?的主要内容,如果未能解决你的问题,请参考以下文章

NextJs/Material-ui 不尊重 makeStyles CSS

带有 nextjs 的 material-ui 选项卡?

在 NextJS、nginx 和 Material-ui(s-s-r) 中使用 CSP

Material UI + Nextjs:抽屉

如何在 Material-UI 中为对话框设置高度?

如何在 React Material UI 中为属性添加 css