首页 > 解决方案 > 如何向 ctx.renderPage 添加多个样式表?

问题描述

我想在我的项目中使用 Material UI 和 styled-components。让两者中的一个工作不是问题,但是当我尝试同时添加它们时,它似乎不起作用。

在我的_document.ts中,我像这样使用 Mui:

    ctx.renderPage = () =>
        originalRenderPage({
            enhanceApp: (App) => (props) => muiSheets.collect(<App {...props} />)
        });

我尝试将样式组件添加到此设置中,如他们的示例中所示,

    ctx.renderPage = () =>
        originalRenderPage({
            enhanceApp: (App) => (props) => {
                return {
                    ...muiSheets.collect(<App {...props} />),
                    ...styledComponentsSheet.collectStyles(<App {...props} />),
                };
            },
        });

但我不确定如何合并这两者。是否只能使用一种样式?

这是我的全部_document.tsx

import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheets } from '@material-ui/core/styles';
import { ServerStyleSheet } from 'styled-components';
import theme from '../src/theme';

export default class MyDocument extends Document {
    render() {
        return (
            <Html lang='en'>
                <Head>
                    <meta name='theme-color' content={theme.palette.primary.main} />
                    <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap' />
                </Head>
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        );
    }
}

MyDocument.getInitialProps = async (ctx) => {
    const muiSheets = new ServerStyleSheets();
    const styledComponentsSheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    ctx.renderPage = () =>
        originalRenderPage({
            enhanceApp: (App) => (props) => {
                return {
                    ...styledComponentsSheet.collectStyles(<App {...props} />),
                };
            },
        });

    ctx.renderPage = () =>
        originalRenderPage({
            enhanceApp: (App) => (props) => {
                return {
                    ...muiSheets.collect(<App {...props} />),
                };
            },
        });

    const initialProps = await Document.getInitialProps(ctx);

    return {
        ...initialProps,
        // Styles fragment is rendered after the app and page rendering finish.
        styles: [...React.Children.toArray(initialProps.styles), muiSheets.getStyleElement(), styledComponentsSheet.getStyleElement()],
    };
};

不过,我不确定这是否有效,因为我遇到了错误,例如:

Warning: Prop `className` did not match. Server: "sc-AxirZ hPlbpz" Client: "sc-AxjAm hhoYMA

而且我尝试过更改我的 babelrc,例如在一些 github 问题上提出的建议,包括这个问题,但无济于事。

如何正确合并两个样式表,在这种情况下是 Material UI 和 styled-components?

标签: reactjsnext.js

解决方案


以下设置将适用于 NextJS(请阅读下面的注释以获取更多信息)...

工作示例

编辑 Nextjs - Styled-components & Material Ui


babel.config.js

module.exports = api => {
  // cache babel configurations via NODE_ENV environments
  // development, production, testing, staging...etc
  api.cache(() => process.env.NODE_ENV);

  return {
    presets: ["next/babel"],
    plugins: [
      // leverage the 'babel-plugin-styled-component' plugin for...
      // 1.) consistently hashed component classNames between environments (a must for server-side rendering)
      // 2.) better debugging through automatic annotation of your styled components based on their context in the file system, etc.
      // 3.) minification for styles and tagged template literals styled-components usages
      [
        "styled-components",
        {
          ssr: true,
          displayName: true,
          preprocess: false
        }
      ],
    ]
  };
};

页面/_documents.js

// import the Document (required), import HTML, Head, Main, NextScript (optional)
import Document, { Html, Head, Main, NextScript } from "next/document";
// import the SC 'ServerStyleSheet' (required)
import { ServerStyleSheet } from "styled-components"; 
// import the MUI 'ServerStyleSheets' (required)
import { ServerStyleSheets as MaterialUiServerStyleSheets } from "@material-ui/core/styles";
// import the package.json version (optional)
import { version } from "../../package.json"; 

class CustomDocument extends Document {
  static async getInitialProps(ctx) {
    // create SC sheet
    const sheet = new ServerStyleSheet();
    // create MUI sheets
    const materialUISheets = new MaterialUiServerStyleSheets();
    // keep a reference to original Next renderPage
    const originalRenderPage = ctx.renderPage;

    try {
      // set the renderPage as a function that...
      ctx.renderPage = () =>
        // utilizes the original renderPage function that...
        originalRenderPage({
          // overrides the enhanceApp property with 2 returned wrapped fn's 
          enhanceApp: App => props =>
            // that collects and returns SC + MUI styles from App
            sheet.collectStyles(materialUISheets.collect(<App {...props} />))
        });

      // invoke internal Next getInitialProps (which executes the above)
      const initialProps = await Document.getInitialProps(ctx);
      return {
        // from getInitialProps, spread out any initial props
        ...initialProps,
        // and apply any initial style tags... 
        // and apply the MUI style tags...
        // and apply the SC style tags... 
        // to the document's head:
        // <html>
        //   <head>
        //     ...styles are placed here
        //   </head>
        //   <body>...</body>
        // </html>
        styles: (
          <>
            {initialProps.styles}
            {materialUISheets.getStyleElement()}
            {sheet.getStyleElement()}
          </>
        )
      };
    } finally {
      // seal the SC sheet -- MUI sheets don't need to be sealed or do so internally
      sheet.seal();
    }
  }

  // below is completely optional...
  // create a custom 'render' method for SEO tags 
  render() {
    return (
      <Html lang="en">
        <Head>
          <meta property="og:locale" content="en_US" />
          <meta property="og:site_name" content="NAME OF YOUR WEBSITE HERE" />
          <meta name="theme-color" content="#000000" />
          <meta name="msapplication-TileColor" content="#000000" />
          <meta name="msapplication-navbutton-color" content="#000000" />
          <meta name="apple-mobile-web-app-status-bar-style" content="black" />
          <link rel="icon" href="/favicon.ico" />
          <link rel="manifest" href="/manifest.json" />
          <meta name="build version" content={version} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default CustomDocument;

页面/_app.js

// since we're using 'useEffect' import named exports as React (required)
import * as React from "react";
// import Head for client-side viewport SEO (optional and can be removed)
import Head from "next/head";

// if not using hooks, swap out 'useEffect' for 'componentDidMount'
// and destructure { Component, pageProps } from 'this.props'

// if you're not using 'getInitialProps' on the client-side then 
// you don't need to extend Next's 'App' from 'next/app'
// instead just utilize a function that returns Component with pageProps (like below) 

const App = ({ Component, pageProps }) => {
  React.useEffect(() => {
    // MUI has 2 sheets: 1 for server-side and 1 for client-side
    // we don't want this duplication, so during initial client-side load, 
    // attempt to locate duplicated server-side MUI stylesheets...
    const jssStyles = document.querySelector("#jss-server-side");
    if (jssStyles && jssStyles.parentNode)
      // ...and if they exist remove them from the head 
      jssStyles.parentNode.removeChild(jssStyles);

  }, []);

  return (
    <React.Fragment>
      <Head>
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1, shrink-to-fit=no"
        />
      </Head>
      <Component {...pageProps} />
    </React.Fragment>
  );
};

export default App;

推荐阅读