現在有了 MUI ,還有跟其相搭配的 emotion,樣式的調整上已經很方便了,但是目前 NextJS 已經開始預設使用 React Server Component ,而 emotion 還無法支援這樣的元件形式,會導致最後所有元件還是回歸 client 這邊 ,無法享受到 Server Component 的好處。
所以這邊試著加入 TailwindCSS 來改善這點,在Server Component 上可以使用 tailwind 來改樣式,也能用來局部修改 MUI 的樣式,減少因 CSS-in-JS 產出的 css 檔案所佔的空間。
老實說以整體樣式管理上來說這不是個好辦法,因為 MUI 也有自己一套主題樣式的架構,所以之後還要想辦法縫合兩邊的樣式主題管理架構,免得一國兩制。
首先幫 app 這邊設定好 tailwind,來安裝套件,這邊跟一般的 tailwind 安裝相同。
pnpm add -D tailwindcss@latest postcss@latest autoprefixer@latest
到目標專案初始化 tailwind 設定。
cd apps/ironman-nextjs
npx tailwindcss init -p
改 postcss 設定,因為跟平常不同 config 檔不在根目錄上。
// apps/ironman-nextjs/postcss.config.js
const { join } = require('path');
module.exports = {
plugins: {
tailwindcss: { config: join(__dirname, 'tailwind.config.js') }, // 指定目前 app 的 config
autoprefixer: {},
},
};
再來是指定 tailwind 要掃描的範圍,tailwind 會根據指定的掃描範圍內找到的 class 來產生對應的 css 檔案,並捨去掉沒有用到的 class ,來輕量化樣式檔案。
因為在 monorepo 架構下很多元件是在 app 之外的,要讓 tailwind 也查到那些元件的家門上不然就不會生成樣式,這裡 NX 提供了輔助, createGlobPatternsForDependencies
會偵測目前專案有依賴的 lib ,產生對應的目錄資料給 tailwind 使用。
// apps/ironman-nextjs/tailwind.config.js
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
const { join } = require('path');
module.exports = {
content: [
join(
__dirname,
'{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}'
),
...createGlobPatternsForDependencies(__dirname),
],
theme: {
extend: {},
},
plugins: [],
};
在根節點上引入 tailwind 樣式
/* apps/ironman-nextjs/app/global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
// apps/ironman-nextjs/app/layout.tsx
import './global.css';
另外在用 NX 指令建置的時候要特別指定 postcss 的 config 位置
// apps/ironman-nextjs/project.json
{
"targets": {
"build": {
"executor": "@nx/next:build",
"options": {
// ...
"postcssConfig": "apps/ironman-nextjs/postcss.config.js"
},
// ...
},
},
//...
}
再來為了將 tailwind 運用在 MUI 上要做的設定,tailwind 有自己一套 css reset 的樣式,但我們已經有 MUI 的 CssBaseline 了,所以關掉 tailwind 的。
// apps/ironman-nextjs/tailwind.config.js
// ...
module.exports = {
// ...
corePlugins: {
preflight: false, // 關掉 Tailwind 預設的 css reset,避免跟 MUI 的重複
},
};
再來因為 MUI 也有自己的預設 class ,如果 tailwind 的 class 權重跟 MUI 的相當的話,就可能出現無法覆蓋樣式的情況。
所以要設定 tailwind 的 important 參數來加強 tailwind 所產生樣式的權重,指定範圍是整個 app 所以用根結點的 id 作為增加權重的標的。
// apps/ironman-nextjs/tailwind.config.js
// ...
module.exports = {
// ...
important: '#__next', // 設定為根節點 id
};
// apps/ironman-nextjs/app/layout.tsx
import './global.css';
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider } from '@ironman-nextjs/ui/react-components';
//...
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body id="__next"> {/* 加上 id */}
<CssBaseline />
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
另外要變更的是樣式的載入順序,emotion 產生的動態樣式一般會最後載入,導致可能覆蓋掉 tailwind 的樣式,所以要設定成反過來優先載入。
// apps/ironman-nextjs/app/layout.tsx
// ...
import { StyledEngineProvider } from '@mui/material/styles';
// ...
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body id="__next">
<StyledEngineProvider injectFirst>{/* 優先載入 */}
<CssBaseline />
<ThemeProvider>{children}</ThemeProvider>
</StyledEngineProvider>
</body>
</html>
);
}
設定好之後就能在元件上添加 tailwind class 了,即使遠在 lib 當中的元件也能套上樣式。
import Button from '@mui/material/Button';
<Button variant="contained" className="bg-green-500 hover:bg-green-700">
Contained
</Button>
最後別忘了還有一個 storybook 要設定,基本上的步驟是相同的,只是在指定 tailwind 的 important 範圍的時候要注意指向 storybook 的元件 iframe 中的根節點,而不是整個 storybook app 的根節點
// libs/ui/react-storybook/tailwind.config.js
module.exports = {
// ...
important: '#storybook-root', // 元件預覽區 iframe 的根節點
corePlugins: {
preflight: false,
},
};
還有因為 storybook 會去抓取自身範圍外的 story 來顯示,所以 tailwind 也需要設定觀察同樣的範圍來生成樣式
// libs/ui/react-storybook/tailwind.config.js
const { join } = require('path');
module.exports = {
content: [
join(
__dirname,
'{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}',
),
// 觀察範圍與 storybook 相同
join(
__dirname,
'../../**/ui/**/src/lib/**/*!(*.stories|*.spec).{ts,tsx,html}',
),
],
theme: {
extend: {},
},
important: '#storybook-root',
plugins: [],
corePlugins: {
preflight: false,
},
};