如何在NextJS项目的头部标签中内联CSS?

6

我的NextJS项目具有以下Webpack配置:

import path from 'path';
import glob from 'glob';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import webpack from 'webpack';
import dotenv from 'dotenv';
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin';

import withSass from '@zeit/next-sass';

dotenv.config();

module.exports = withSass({
  distDir: '.build',
  webpack: (config, { dev, isServer }) => {
    if (isServer) {
      return config;
    }
    config.plugins.push(
      new webpack.optimize.LimitChunkCountPlugin({
        maxChunks: 1,
      }),
    );
    config.optimization.minimizer.push(
      new OptimizeCSSAssetsPlugin({}),
    );
    return config;
  },
});

这使得我能够在任何页面导入任意数量的scss文件,并将它们全部捆绑在一起,作为单个文件进行压缩,并如此提供:
<link rel="stylesheet" href="/_next/static/css/styles.84a02761.chunk.css">

然而,我更喜欢将样式定义内联到我的 <head> 标签中,而不是使用 <link>。是否有可能在不堆积大量第三方模块的情况下实现这一点?
如果不行,那么能否将结果的 <link>rel 更改为 preload,并添加 as="style" crossorigin
3个回答

11

我通过微调 pages/_document.jsx 文件成功地将CSS内联化。我扩展了由NextJS本地提供的<Head>组件,并将其添加到我的自定义文档标记中。以下是我修改的部分内容:

import { readFileSync } from 'fs';
import { join } from 'path';

class InlineStylesHead extends Head {
  getCssLinks() {
    return this.__getInlineStyles();
  }

  __getInlineStyles() {
    const { assetPrefix, files } = this.context._documentProps;
    if (!files || files.length === 0) return null;

    return files.filter(file => /\.css$/.test(file)).map(file => (
      <style
        key={file}
        data-href={`${assetPrefix}/_next/${file}`}
        dangerouslySetInnerHTML={{
          __html: readFileSync(join(process.cwd(), '.build', file), 'utf-8'),
        }}
      />
    ));
  }
}

class MyDocument extends Document {
  render() {
    return (
      <Html lang="en" dir="ltr">
        <InlineStylesHead>
          <meta name="theme-color" content="#ffcc66" />
        </InlineStylesHead>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

我将这个解决方案归功于 https://github.com/zeit/next-plugins/issues/238#issuecomment-432211871


提醒一下,由于Next.js 9.5.0+中不再支持_documentProps,因此这将不再起作用。 - James Robert Singleton
你需要执行 const {assetPrefix, files } = this.context - James Robert Singleton
1
术语纠正:<style>...</style> 元素不是“内联样式”,而是嵌入式样式表。内联样式是通过 style 属性直接应用于元素的 CSS 属性。 - callum
2
我在升级到NextJS 9.5+后,在this.context中获取文件未定义。 - Achal Jain
这里也是 - 在 NextJs 10 中,文件和 assetPrefix 都未定义。 - Vlad
1
我不得不将 .build 更改为 .next 才能使其在生产环境中正常工作。 - Aral Roca

9

Next.js现在可以自动内联关键CSS

这个功能是实验性的,需要通过一个标志才能启用,但我们很乐意听取您的反馈:

  1. 在next.config.js中添加 experimental: { optimizeCss: true }
  2. 安装critters@0.0.7作为依赖项

就这样了!

参考:https://twitter.com/hdjirdeh/status/1369709676271726599


4

对于 NextJS 9.5.0 及以上版本,只需使用以下代码:

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

import {readFileSync} from "fs"
import {join} from "path"

class InlineStylesHead extends Head {
  getCssLinks(files) {
    const {
      assetPrefix,
      devOnlyCacheBusterQueryString,
      dynamicImports,
    } = this.context
    const cssFiles = files.allFiles.filter((f) => f.endsWith('.css'))
    const sharedFiles = new Set(files.sharedFiles)

    // Unmanaged files are CSS files that will be handled directly by the
    // webpack runtime (`mini-css-extract-plugin`).
    let dynamicCssFiles = dedupe(
      dynamicImports.filter((f) => f.file.endsWith('.css'))
    ).map((f) => f.file)
    if (dynamicCssFiles.length) {
      const existing = new Set(cssFiles)
      dynamicCssFiles = dynamicCssFiles.filter(
        (f) => !(existing.has(f) || sharedFiles.has(f))
      )
      cssFiles.push(...dynamicCssFiles)
    }

    let cssLinkElements = []
    cssFiles.forEach((file) => {

      if (!process.env.__NEXT_OPTIMIZE_CSS) {
        cssLinkElements.push(
          <style
            key={file}
            data-href={`${assetPrefix}/_next/${encodeURI(
              file
            )}${devOnlyCacheBusterQueryString}`}
            dangerouslySetInnerHTML={{
              __html: readFileSync(join(process.cwd(), '.next', file), 'utf-8'),
            }}
          />
        )
      }

      cssLinkElements.push(
        <style
          key={file}
          data-href={`${assetPrefix}/_next/${encodeURI(
            file
          )}${devOnlyCacheBusterQueryString}`}
          dangerouslySetInnerHTML={{
            __html: readFileSync(join(process.cwd(), '.next', file), 'utf-8'),
          }}
        />
      )
    })

    if (
      process.env.NODE_ENV !== 'development' &&
      process.env.__NEXT_OPTIMIZE_FONTS
    ) {
      cssLinkElements = this.makeStylesheetInert(
        cssLinkElements
      )
    }

    return cssLinkElements.length === 0 ? null : cssLinkElements
  }

}

function dedupe(bundles) {
  const files = new Set()
  const kept = []

  for (const bundle of bundles) {
    if (files.has(bundle.file)) continue
    files.add(bundle.file)
    kept.push(bundle)
  }
  return kept
}

export default class MyDocument extends Document {
  
  render() {
    return (
      <Html lang="ru" dir="ltr">
        <InlineStylesHead/>
        <body>
        <Main />
        <NextScript />
        </body>
      </Html>
    );
  }

}

出于好奇,你为什么要添加这个 dedupe 函数?我在 Next 的原始实现中没有看到过它。 - Rafael Lebre
这段代码帮了我很大的忙,但是它有一些错误,所以我创建了一个gist来进行一些微调。我正在删除一些 .file,因为参数已经是字符串而不是对象,并且我还删除了重复的 cssLinkElements.pushNext用于预加载目的,但对于内联来说没有意义。 - Rafael Lebre

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接