likes
comments
collection
share

antd5+Next.js样式按需抽离

作者站长头像
站长
· 阅读数 257

前言

ant design组件库升级到5后,他的css样式解决方案也发生了变化,从less变为了css-in-js,但是这种方案对SSR非常不友好:为了不出现闪屏的情况,会在服务器中将style标签插入html,导致SSR首屏的HTML文件会非常的大。 提交了issues参与讨论后过了几个版本,官方在文档中给出了方法:按需抽离 将首屏需要的CSS内容抽离成CSS文件,这样的话既可以防止首页闪屏又减少了首屏体积,也可以使用CDN加速静态文件。

解决

官方提供了Demo但是我在使用中发现了一些问题,就是运行时生成的CSS文件是不支持直接访问的,就做了一些修改。

新增 genAntdCss.tsx

主要是通过CSS文本的hash判断是否保存了css文件,如果没有保存就保存到_next文件夹中。并且在运行中生成的文件是无法访问的,所以我返回的路径指向是路由文件夹,我想要使用路由页面返回CSS内容做一个中转。

import { createHash } from "crypto";
import fs from "fs";
import path from "path";
import type Entity from "@ant-design/cssinjs/lib/Cache";
import { extractStyle } from "@ant-design/cssinjs";

export type DoExtraStyleOptions = {
  cache: Entity;
};
export function doExtraStyle({ cache }: DoExtraStyleOptions) {
  const baseDir = path.resolve(__dirname, "../../static/css");

  if (!fs.existsSync(baseDir)) {
    fs.mkdirSync(baseDir, { recursive: true });
  }

  const css = extractStyle(cache, true);
  if (!css) return "";

  const md5 = createHash("md5");
  const hash = md5.update(css).digest("hex");

  const fileName = `${hash.substring(0, 18)}.css`;
  const fullpath = path.join(baseDir, fileName);

  if (fs.existsSync(fullpath)) return `/antd/${fileName}`;

  fs.writeFileSync(fullpath, css);

  return `/antd/${fileName}`;
}

修改 _document.tsx

在getInitialProps中进行lin标签的注入在不影响initialProps的基础上对style属性进行修改,同时进行缓存,防止css提取和hash计算的过程重复,每次服务器启动后只需要计算一次即可。注意genAntdCss的路径不要写错。

import Document, { Html, Head, Main, NextScript, DocumentContext } from "next/document";
import { doExtraStyle } from "@/styles/genAntdCss";
import { StyleProvider, createCache } from "@ant-design/cssinjs";

let antdFileNameMap: { [key: string]: string } = {};

const MyDocument = () => (
  <Html lang="zh-CN">
    <Head />
    <body>
      <Main />
      <NextScript />
    </body>
  </Html>
);

MyDocument.getInitialProps = async (ctx: DocumentContext) => {
  const cache = createCache();
  let fileName = antdFileNameMap[ctx.pathname] || "";
  const originalRenderPage = ctx.renderPage;
  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: App => props =>
        (
          <StyleProvider cache={cache}>
            <App {...props} />
          </StyleProvider>
        ),
    });

  const initialProps = await Document.getInitialProps(ctx);

  if (!fileName) {
    fileName = doExtraStyle({
      cache,
    });
    antdFileNameMap[ctx.pathname] = fileName;
  }

  return {
    ...initialProps,
    styles: (
      <>
        {initialProps.styles}
       <link rel="stylesheet" href={`${process.env.CDN}${fileName}`} />
      </>
    ),
  };
};

export default MyDocument;

设置中转

我设置的路径名称是/pages/antd/[name].css ,这个路径要与genAntdCss.tsx返回的文件路径对应,同时distDir变量需要设置,因为生产和开发环境,静态文件夹分别为_next和.next。

import fs from "fs";

let distDir: string = JSON.parse(process.env.__NEXT_PRIVATE_RENDER_WORKER_CONFIG as string).distDir;

// 处理生产环境生成的antdcss文件无法访问的问题
import { GetServerSideProps } from "next";
const Antd = () => null;
export default Antd;
export const getServerSideProps: GetServerSideProps = async ({ res, params }) => {
  let name = params!.name as string | undefined;
  if (typeof name == "string" && name?.endsWith(".css")) {
    try {
      let content = fs.readFileSync(`${distDir}/static/css/${name}`).toString();
      res.setHeader("Content-Type", "text/css");
      res.statusCode = 200;
      res.write(content);
      res.end();
    } catch (error) {
      res.statusCode = 404;
      res.end();
    }
  } else {
    res.statusCode = 404;
    res.end();
  }
  return {
    props: {},
  };
};