• 全局文件在 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-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': {},
            // '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 />
                    <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文件

    <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({
        // 开启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 (
                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>
    

    查看页面,完全行得通~

    这是目前能想到的最简单的使用方法了,如果有更好的方法,欢迎在评论里补充~

    分类:
    前端