主要内容
- Css(Scss) 全局与模块化;
- 使用别名引入 Scss 文件
- 支持阿拉伯语等从右到左展示;
- px 自动转 rem。
- babel-plugin-react-css-modules 简化模块化 class 写法
- classnames 插件简化 className 字符串拼接
开发环境
Windows 10;
编辑器PhpStorm;
node 版本12.16.2;
Next.js 版本9.4.4
项目初始化
CSS(SCSS) 全局与模块化
Next.js 内置方案
Next.js 官方文档 中给出了全局和模块文件使用方法(9.3+):
- 全局文件在 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({
// 开启css模块化
cssModules: true,
cssLoaderOptions: {
importLoaders: 1,
// scoped class 格式
localIdentName: "[local]__[hash:base64:5]",
},
webpack: (config) => {
// 全局变量和mixin
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-envmodule.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': {}, // 'postcss-flexbugs-fixes'... } } -
添加 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 /> <body> <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, //1rem=22px, 设计稿中html元素字体大小/rootValue=转换后rem值 unitPrecision: 5, //转换后保留的小数点位数 propList: ['*'], //需要转换的属性 mediaQuery: false, // 是否转换 @media 条件中的px(只影响条件,不影响代码块) minPixelValue: 2, // 1px 不转换,大于等于2px的转换 exclude: /node_modules/i }, //'postcss-flexbugs-fixes'... } } -
设置 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文件
<Head> <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
<Head> <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({ // 开启css模块化 cssModules: true, cssLoaderOptions: { importLoaders: 1, // scoped class 格式,localIdentName 改成 // localIdentName: "[local]__[hash:base64:5]", 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: #dd0000;
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 (
<div
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 (
<div
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 (
<div
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 (
<div
className={cn(
'global-container',
{ 'global-red': isRed }
)}
styleName={cn(
'local-container',
{ 'local-red': isRed }
)}
onClick={() => setIsRed(!isRed)}
>
Toggle Color
</div>
)
}
查看页面,完全行得通~
这是目前能想到的最简单的使用方法了,如果有更好的方法,欢迎在评论里补充~