antd5+Next.js样式按需抽离
前言
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,
});
}
}