likes
comments
collection
share

antd5+Next.js样式按需抽离

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

前言

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

解决

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

Pages Router

编写与:2023/08/17

新增 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: {},
  };
};

App Router

更新于:2024/5/28 app router的结构与page不同 ,我将antd单独抽离为组件,不在卸载_document.tsx中

antd.tsx

"use client";
import { useState, type FC, type ReactNode } from "react";
import zhCN from "antd/locale/zh_CN";
import { ConfigProvider } from "antd";
import { useServerInsertedHTML } from "next/navigation";
import { createCache, StyleProvider } from "@ant-design/cssinjs";
import { usePathname } from "next/navigation";
import { doExtraStyle } from "@/styles/genAntdCss";
import { pathToRegexp } from "path-to-regexp";

interface propsType {
  children: ReactNode;
}

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

let routeFileList: { href: string; regexp?: RegExp }[] = [];
if (typeof window == "undefined") {
  (async () => {
    const glob = await import("glob");
    const path = await import("path");
    routeFileList = glob
      .sync(["src/app/**/**/page.tsx"])
      .map(item => ({
        href: item.split(path.sep).join("/").replace("src/app", "").replace("/page.tsx", ""),
      }))
      .map(item => ({ ...item, href: item.href == "" ? "/" : item.href }))
      .map(item => ({ ...item, regexp: pathToRegexp(item.href) }));
  })();
}

const Antd: FC<propsType> = ({ children }) => {
  const [cache] = useState(() => createCache());
  let pathname = usePathname();

  // 在服务器插入HTML
  useServerInsertedHTML(async () => {
    let path = routeFileList.find(item => item.regexp!.test(pathname))?.href + "";
    let fileName = antdFileNameMap[path];

    // 抽离为单个CSS文件
    if (!fileName) {
      fileName = await doExtraStyle({
        cache,
      });
      antdFileNameMap[path] = fileName;
    }

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

  return (
    <StyleProvider cache={cache}>
      <ConfigProvider locale={zhCN}>{children}</ConfigProvider>
    </StyleProvider>
  );
};
export default Antd;

genAntdCss.tsx

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 async function doExtraStyle({ cache }: DoExtraStyleOptions) {
  if (typeof window == "undefined") {
    const fs = await import(`fs`);
    const path = await import(`path`);

    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, 16)}.css`;
    const fullpath = path.join(baseDir, fileName);

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

    fs.writeFileSync(fullpath, css);

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

转发

在\src\app\static\antd[name]\route.tsx下编写函数

import fs from "fs";

type Params = {
  name: string;
};

export async function GET(res: Request, context: { params: Params }) {
  let name = context.params!.name as string | undefined;
  if (typeof name == "string" && name?.endsWith(".css")) {
    try {
      let content = fs.readFileSync(`.next/static/css/${name}`).toString();
      return new Response(content, {
        status: 200,
        headers: {
          "content-type": "text/css; charset=utf-8",
        },
      });
    } catch (error) {
      return new Response(undefined, {
        status: 404,
      });
    }
  } else {
    return new Response(undefined, {
      status: 404,
    });
  }
}
评论
请登录