全局文件在 pages/_app.js 文件中引入(其它地方引入会报错);
模块文件要以 .module.(css|scss|sass) 命名,可以在任意模块中引入;
模块文件已经内置了 scope 功能,每个class将分配一个唯一id,不用担心命名冲突;
使用上述方案有两个问题无法解决:
为了方便代码复用,部分底层组件不希望做scope转换,只能作为全局代码在 pages/_app.js 文件中引入,导致组件代码和样式代码在不同的地方引用,不易维护;
全局变量 和 mixin 必须在每一个用到的文件中手动 import,太麻烦。
为了解决上述问题,引入了两个插件
@zeit/next-sass
: 解决全局和模块化问题
sass-resources-loader
:解决全局变量和 mixin 问题;
为了避免冲突,脚本检测到 @zeit/next-sass 配置后会自动禁用内置的模块化功能,所以不需要考虑兼容问题。
命令行中将看到下面的提示:
Warning: Built-in CSS support is being disabled due to custom CSS configuration being detected.
npm install --save-dev @zeit/next-sass node-sass sass-resources-loader
新建文件 next.config.js
const withSass = require('@zeit/next-sass');
module.exports = withSass({
cssModules: true,
cssLoaderOptions: {
importLoaders: 1,
localIdentName: "[local]__[hash:base64:5]",
webpack: (config) => {
config.module.rules.push({
enforce: 'pre',
test: /.scss$/,
loader: 'sass-resources-loader',
options: {
resources: ['./components/styles/variables.scss'],
return config;
全局变量:components/styles/variables.scss
$color: #56ad6a;
全局样式:components/styles/global.scss
:global {
.global-container {
margin: 0 auto;
color: $color;
模块样式:components/Example/style.scss
.local-container {
border: solid 1px $color;
width: 300px;
height: 300px;
text-align: left;
模块组件:components/Example/index.tsx
import styles from './style.scss';
export default function Example() {
return <div className={'global-container ' + styles['local-container']}> Test Scss </div>
页面组件:pages/index.js 重命名为 pages/index.tsx,修改代码(与初始化中一致,不需要改动)
import Example from '../components/Example/index';
export default function Home() {
return <Example/>
引入全局样式,新建 pages/_app.tsx
import '../components/styles/global.scss';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
export default MyApp
重启项目,查看元素,观察class 变化
遇坑:编辑器错误提示
代码编译没问题,命令行与浏览器无报错,但是编辑器中出现报错提示
TS2307: Cannot find module './style.scss' or its corresponding type declarations.
添加文件 declarations.d.ts
declare module '*.scss' {
const content: {[className: string]: string};
export = content;
tsconfig.json 的 include 数组中添加配置,解决~
"include": [
"declarations.d.ts"
使用别名引入scss文件
按照 官方文档 修改tsconfig.json,添加别名,@ 指向根目录,@@ 指向 components 目录
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@@/*": ["components/*"]
修改 _app.tsx 中的 import 路径,重启。
import '@@/styles/global.scss';
支持阿拉伯语等从右到左展示
使用 postcss 插件 postcss-rtl。
根目录下新建文件 postcss.config.js
拷贝Next.js中的默认配置(官方文档),并安装默认配置中涉及的插件 npm i -D postcss-flexbugs-fixes postcss-preset-env
module.exports = {
plugins: {
'postcss-flexbugs-fixes': {},
'postcss-preset-env': {
autoprefixer: {
flexbox: 'no-2009',
stage: 3,
features: {
'custom-properties': false,
执行 npm i -D postcss-rtl
安装插件后,plugins 对象中添加 postcss-rtl 配置
module.exports = {
plugins: {
'postcss-rtl': {},
添加 pages/_document.tsx, 在html元素上添加 dir 属性
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
render() {
return (
<Html dir="rtl">
<Head />
<Main />
<NextScript />
</body>
</Html>
export default MyDocument;
查看元素,观察class 变化
px 转 rem
使用 postcss 插件 postcss-pxtorem。
执行 npm i -D postcss-pxtorem
安装插件;
postcss.config.js 中添加配置项
module.exports = {
plugins: {
'postcss-rtl': {},
'postcss-pxtorem': {
rootValue: 22,
unitPrecision: 5,
propList: ['*'],
mediaQuery: false,
minPixelValue: 2,
exclude: /node_modules/i
设置 html 元素的 font-size
新建 public/static/css/reset.css ,复制下面的代码,根据网站实际适应的宽度和字体大小调整 font-size 数值。
html {font-size: 11px;}
@media (min-width: 320px){ html{font-size: 9.3867px;} }
@media (min-width: 360px){ html{font-size: 10.5600px;} }
@media (min-width: 375px){ html{font-size: 11px;} }
@media (min-width: 384px){ html{font-size: 11.2640px;} }
@media (min-width: 414px){ html{font-size: 12.1440px;} }
@media (min-width: 448px){ html{font-size: 13.1413px;} }
@media (min-width: 480px){ html{font-size: 14.0800px;} }
@media (min-width: 512px){ html{font-size: 15.0187px;} }
@media (min-width: 544px){ html{font-size: 15.9573px;} }
@media (min-width: 576px){ html{font-size: 16.8960px;} }
@media (min-width: 608px){ html{font-size: 17.8347px;} }
@media (min-width: 640px){ html{font-size: 18.7733px;} }
@media (min-width: 750px){ html{font-size: 22px;} }
pages/_document.tsx 文件中引入该css文件
<link rel="stylesheet" type="text/css" href="/static/css/reset.css"/>
</Head>
上面的css代码可以覆盖大部分设备,如果需要更精确的匹配,需要使用js。
新建 public/static/js/rem.js 文件,复制下面代码,根据网站适应的宽度和字体大小调整代码中的 数值:
(function() {
function addEvent(domElem, eventName, func, useCapture) {
useCapture = typeof useCapture !== 'undefined' ? useCapture : false;
domElem.addEventListener(eventName, func, useCapture);
function setHtmlFontSize() {
var minSiteWidth = 320,
maxSiteWidth = 750,
maxFontSize = 22;
var windowWidth = window.innerWidth ? window.innerWidth : document.documentElement.offsetWidth;
windowWidth = Math.min(Math.max(windowWidth, minSiteWidth), maxSiteWidth);
let fs = ~~(windowWidth * maxFontSize / maxSiteWidth);
let tagHtml = document.getElementsByTagName('html')[0];
tagHtml.style.cssText = 'font-size: ' + fs + 'px';
let realfz = ~~(+window.getComputedStyle(tagHtml).fontSize.replace('px', '') * 10000) / 10000;
if (fs !== realfz) {
tagHtml.style.cssText = 'font-size: ' + fs * (fs / realfz) + 'px';
setHtmlFontSize()
addEvent(window, 'resize', setHtmlFontSize)
})();
pages/_document.tsx 文件中引入该js
<script src="/static/js/rem.js"/>
</Head>
查看元素,观察class 变化;缩放窗口,测试文字宽高等有没有实现等比缩放。
babel-plugin-react-css-modules 简化模块化 class 写法
公司所有项目 css 的 class 都规定了使用小写加横杠的方式。
在上面的例子中由于class中带有横杠(“local-container”),组件中引用只能写 className={styles['local-container']}
,为了简化写法,引入babel-plugin-react-css-modules 插件。
npm i -D babel-plugin-react-css-modules postcss-scss
按照插件文档配置,添加文件 .babelrc
"plugins": [
["react-css-modules", {
"generateScopedName": "[local]__[hash:base64:5]",
"exclude": "node_modules",
"filetypes": {
".scss": {
"syntax": "postcss-scss"
修改组件代码 components/Example/index.tsx
import './style.scss';
export default function Example() {
return <div className="global-container" style="local-container"> Test Scss </div>
运行代码后报错
error - ./pages/_app.tsx 7:9
Module parse failed: Unexpected token (7:9)
File was processed with these loaders:
* ./node_modules/@next/react-refresh-utils/loader.js
* ./node_modules/next/dist/build/webpack/loaders/next-babel-loader.js
You may need an additional loader to handle the result of these loaders.
| pageProps
| }) {
> return <Component {...pageProps} />;
参考 Next.js 文档 Customizing Babel Config,修改配置
"presets": [
"next/babel"
"plugins": [
["react-css-modules", {
"generateScopedName": "[local]__[hash:base64:5]",
"exclude": "node_modules",
"filetypes": {
".scss": {
"syntax": "postcss-scss"
报错,无法解析别名
error - ./pages/_app.tsx
Error: D:\workspace\ava\nextjs-css\pages\_app.tsx: Cannot find module '@@/styles/global.scss'
解决方法:
npm i -D babel-plugin-module-resolver
修改 .babelrc,
"plugins": [
["module-resolver", {
"root": ["./"],
"alias": {
"@": ".",
"@@": "./components"
}],
查看元素,模块做了scope处理,styleName属性移除,className合并都实现了,但是!css没了!(后来验证发现Mac是没问题的,只有 windows 有这个问题)
解决办法:
npm i -D generic-names
添加文件 build/generateScopedName.js
const path = require('path');
const genericNames = require('generic-names');
const generate = genericNames('[local]__[hash:base64:5]', {
context: process.cwd()
const generateScopedName = (localName, filePath) => {
var relativePath = path.relative(process.cwd(), filePath);
return generate(localName, relativePath);
module.exports = generateScopedName;
修改 next.config.js
const generateScopedName = require('./build/generateScopedName');
module.exports = withSass({
cssModules: true,
cssLoaderOptions: {
importLoaders: 1,
getLocalIdent: (context, localIdentName, localName) => {
return generateScopedName(localName, context.resourcePath)
问题到此已经解决,此乃大坑,原理与排查过程记录可以看文档 babel-plugin-react-css-modules hash problem。
遇坑:编辑器错误提示,无法正确解析 styleName
components/Example/index.tsx 中出现错误提示
TS2322: Type '{ children: string; className: string; styleName: string; }' is not assignable to type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'. Property 'styleName' does not exist on type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.
安装 @types/react-css-modules 即可解决:
npm install --save-dev @types/react-css-modules
classnames 插件简化 className 字符串拼接
dom 标签中含有动态 class 时,写法比较繁琐。比如下面的例子,点击 global-container 容器切换颜色:
components/styles/variables.scss 添加全局变量
$colorRed:
components/styles/global.scss,添加全局样式
:global {
.global-red {
color: $colorRed;
components/Example/style.scss,添加模块样式
.local-red {
border-color: $colorRed;
修改 components/Example/index.tsx,使用字符串拼接,阅读十分困难,且很容易漏空格:
import { useState } from 'react';
import styles from './style.scss';
export default function Example() {
const [isRed, setIsRed] = useState(false);
return (
className={'global-container ' + styles['local-container'] + (isRed ? ' global-red ' + styles['local-red'] : '')}
onClick={() => setIsRed(!isRed)}
Toggle Color
</div>
还有一种常用写法是将所有 class 塞进数组,再将数组转成字符串
export default function Example() {
const [isRed, setIsRed] = useState(false);
const classes = ['global-container', styles['local-container']];
if (isRed) {
classes.push('global-red');
classes.push(styles['local-red']);
return (
className={classes.join(' ')}
onClick={() => setIsRed(!isRed)}
Toggle Color
</div>
利用插件 classnames 简化写法如下
import { useState } from 'react';
import classnames from 'classnames/bind';
import styles from './style.scss';
const cx = classnames.bind(styles);
export default function Example() {
const [isRed, setIsRed] = useState(false);
return (
className={cx(
'global-container',
['local-container'],
{ 'global-red': isRed },
{ 'local-red': isRed }
onClick={() => setIsRed(!isRed)}
Toggle Color
</div>
看起来简单多了,className 写法简化了,babel-plugin-react-css-modules 插件带来的 styleName 能否结合使用呢?
阅读 classnames(classnames/index.js)源码发现,这个插件只做了一件事,就是把所有参数组合成字符串。理论上 styleName 也是可以用的,测试一下
import { useState } from 'react';
import cn from 'classnames';
import './style.scss';
export default function Example() {
const [isRed, setIsRed] = useState(false);
return (
className={cn(
'global-container',
{ 'global-red': isRed }
styleName={cn(
'local-container',
{ 'local-red': isRed }
onClick={() => setIsRed(!isRed)}
Toggle Color
</div>
查看页面,完全行得通~
这是目前能想到的最简单的使用方法了,如果有更好的方法,欢迎在评论里补充~