如何自定义延迟加载页面之间共享的React组件的样式?

发布时间:2020-07-06 13:27

我正在构建一个React应用程序,我开始使用CRA。我使用React Router配置了应用程序的路由。页面组件是延迟加载的。

共有2页:首页关于

...
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));

...
      <Suspense fallback={<div>Loading...</div>}>
        <Switch>
          <Route path="/about" component={About} />
          <Route path="/" component={Home} />
        </Switch>
      </Suspense>
...

每个页面都使用下面的Button组件。

import React from 'react';
import styles from './Button.module.scss';

const Button = ({ children, className = '' }) => (
    <button className={`${styles.btn} ${className}`}>{children}</button>
);

export default Button;

Button.module.scss文件仅将按钮的背景色设置为红色

.btn {
    background: red;
}

Button组件接受一个className道具,然后将其添加到呈现的按钮中。这是因为我想给组件的使用者以自由。例如,在某些页面中可能需要空白,或者背景应该是黄色而不是 red

为简单起见,我只想根据当前页面为Button使用不同的背景色,以便:

  • 主页=>蓝色按钮
  • 关于页面=>黄色按钮

每个页面的定义如下:

import React from 'react';
import Button from './Button';
import styles from './[PageName].module.scss';

const [PageName] = () => (
    <div>
        <h1>[PageName]</h1>
        <Button className={styles.pageBtn}>[ExpectedColor]</Button>
    </div>
);

export default [PageName];

其中[PageName]是页面的名称,[ExpectedColor]是基于上述项目符号列表(蓝色或黄色)的相应预期颜色。

导入的SCSS模块导出一个类.pageBtn,该类将background属性设置为所需的颜色。

注意:我可以在Button组件上使用道具,该道具定义要显示的变体(蓝色/黄色),并根据该道具添加在SCSS文件中定义的类。我不想这样做,因为更改可能不属于某个变体(例如margin-top)。

问题

如果我使用yarn start运行该应用程序,则该应用程序可以正常运行。但是,如果我构建了应用程序(yarn build,然后开始为应用程序提供服务(例如,使用serve -s build),则行为会有所不同,并且该应用程序将无法正常工作。

加载主页页后,该按钮将正确显示为蓝色背景。检查加载的CSS块,其中包含:

.Button_btn__2cUFR {
    background: red
}

.Home_pageBtn__nnyWK {
    background: blue
}

很好。然后,我单击导航链接打开关于页面。即使在这种情况下,按钮也正确显示为黄色背景。检查加载的CSS块,其中包含:

.Button_btn__2cUFR {
    background: red
}

.About_pageBtn__3jjV7 {
    background: yellow
}

当我回到 Home 页面时,该按钮现在显示为红色背景而不是黄色。这是因为 About 页面已加载上面的CSS,该CSS再次定义了Button_btn__2cUFR类。由于该类现在位于Home_pageBtn__nnyWK类定义的之后,因此该按钮显示为红色。

注意:由于Button组件的大小太小,因此不会在 common 块上导出。将其放在一个通用块中可以解决问题。但是,我的问题是关于小型共享组件的。

解决方案

我考虑过两种解决方案,但是我不太喜欢:

提高选择器的特异性

[PageName].module.scss中指定的类可以定义为:

.pageBtn.pageBtn {
   background: [color];
}

这将提高选择器的特异性,并将覆盖默认的Button_btn__2cUFR类。但是,如果每个页面块都非常小(小于30kb),则每个页面块都将包含共享的组件。另外,组件的使用者必须知道该技巧。

弹出并配置Webpack

弹出应用程序(或使用类似react-app-rewired的东西)将允许使用webpack为公共块指定最小大小。但是,这不是我想要的所有组件。


总而言之,问题是:使用延迟加载的路由时,覆盖共享组件样式的正确工作方式是什么?

回答1

您可以将以下逻辑与配置文件一起用于任何页面。另外,您可以从远程服务器(req / res API)发送配置数据,并使用redux进行处理。

查看演示:CodeSandBox

创建components目录并创建如下文件:

src
 |---components
      |---Button
      |     |---Button.jsx
      |     |---Button.module.css

按钮组件:

// Button.jsx

import React from "react";
import styles from "./Button.module.css";

const Button = props => {
  const { children, className, ...otherProps } = props;
  return (
    <button className={styles[`${className}`]} {...otherProps}>
      {children}
    </button>
  );
};

export default Button;

...

// Button.module.css

.Home_btn {
  background: red;
}
.About_btn {
  background: blue;
}

创建utils目录并创建AppUtils.js文件:

此文件处理页面的配置文件并返回新对象

class AppUtils {
  static setRoutes(config) {
    let routes = [...config.routes];

    if (config.settings) {
      routes = routes.map(route => {
        return {
          ...route,
          settings: { ...config.settings, ...route.settings }
        };
      });
    }

    return [...routes];
  }

  static generateRoutesFromConfigs(configs) {
    let allRoutes = [];
    configs.forEach(config => {
      allRoutes = [...allRoutes, ...this.setRoutes(config)];
    });
    return allRoutes;
  }
}

export default AppUtils;

创建app-configs目录并创建routesConfig.jsx文件:

此文件列出并组织了路线。

import React from "react";

import AppUtils from "../utils/AppUtils";
import { pagesConfig } from "../pages/pagesConfig";

const routeConfigs = [...pagesConfig];

const routes = [
  ...AppUtils.generateRoutesFromConfigs(routeConfigs),
  {
    component: () => <h1>404 page not found</h1>
  }
];

export default routes;

index.jsApp.js文件修改为:

// index.js

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <Router>
      <App />
    </Router>
  </React.StrictMode>,
  rootElement
);

...

react-router-config:React的静态路由配置帮助器 路由器。

// App.js

import React, { Suspense } from "react";
import { Switch, Link } from "react-router-dom";
import { renderRoutes } from "react-router-config";

import routes from "./app-configs/routesConfig";

import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
      </ul>
      <Suspense fallback={<h1>loading....</h1>}>
        <Switch>{renderRoutes(routes)}</Switch>
      </Suspense>
    </div>
  );
}

创建pages目录并创建文件和子目录,如下所示:

src
 |---pages
      |---about
      |     |---AboutPage.jsx
      |     |---AboutPageConfig.jsx
      |
      |---home
           |---HomePage.jsx
           |---HomePageConfig.jsx
      |
      |---pagesConfig.js

关于页面文件:

// AboutPage.jsx

import React from "react";
import Button from "../../components/Button/Button";

const AboutPage = props => {
  const btnClass = props.route.settings.layout.config.buttonClass;
  return (
    <>
      <h1>about page</h1>
      <Button className={btnClass}>about button</Button>
    </>
  );
};

export default AboutPage;

...

// AboutPageConfig.jsx

import React from "react";

export const AboutPageConfig = {
  settings: {
    layout: {
      config: {
        buttonClass: "About_btn"
      }
    }
  },
  routes: [
    {
      path: "/about",
      exact: true,
      component: React.lazy(() => import("./AboutPage"))
    }
  ]
};

主页文件:

// HomePage.jsx

import React from "react";
import Button from "../../components/Button/Button";

const HomePage = props => {
  const btnClass = props.route.settings.layout.config.buttonClass;
  return (
    <>
      <h1>home page</h1>
      <Button className={btnClass}>home button</Button>
    </>
  );
};

export default HomePage;

...

// HomePageConfig.jsx

import React from "react";

export const HomePageConfig = {
  settings: {
    layout: {
      config: {
        buttonClass: "Home_btn"
      }
    }
  },
  routes: [
    {
      path: "/",
      exact: true,
      component: React.lazy(() => import("./HomePage"))
    }
  ]
};

...

// pagesConfig.js

import { HomePageConfig } from "./home/HomePageConfig";
import { AboutPageConfig } from "./about/AboutPageConfig";

export const pagesConfig = [HomePageConfig, AboutPageConfig];

编辑部分: 使用 HOC 可能是这样的:CodeSandBox

创建hoc目录和withPage.jsx文件:

src
 |---hoc
      |---withPage.jsx

...

// withPage.jsx

import React, { useEffect, useState } from "react";

export function withPage(Component, path) {
  function loadComponentFromPath(path, setStyles) {
     import(path).then(component => setStyles(component.default));
   }
  return function(props) {
    const [styles, setStyles] = useState();
    
    useEffect(() => {
      loadComponentFromPath(`../pages/${path}`, setStyles);
    }, []);
    return <Component {...props} styles={styles} />;
  };
}

然后显示如下页面:

src
 |---pages
      |---about
      |     |---About.jsx
      |     |---About.module.css
      |
      |---home
           |---Home.jsx
           |---Home.module.css

About.jsx文件:

// About.jsx

import React from "react";
import { withPage } from "../../hoc/withPage";

const About = props => {
  const {styles} = props;
  return (
    <button className={styles && styles.AboutBtn}>About</button>
  );
};

export default withPage(About, "about/About.module.css");

About.module.css文件:

// About.module.css

.AboutBtn {
  background: yellow;
}

Home.jsx文件:

// Home.jsx

import React from "react";
import { withPage } from "../../hoc/withPage";

const Home = props => {
  const { styles } = props;
  return <button className={styles && styles.HomeBtn}>Home</button>;
};

export default withPage(Home, "home/Home.module.css");

Home.module.css文件:

// Home.module.css

.HomeBtn {
  background: red;
}
回答2

我建议不要添加默认样式和使用者样式,而应使用使用者样式代替您的样式,如果未提供,则将您用作回调。使用者仍然可以使用[0][1]关键字来编写默认值。

Button.js

composes

SomePage.module.scss

import React from 'react';
import styles from './Button.module.scss';

const Button = ({ children, className}) => (
    <button className={className ?? styles.btn}>{children}</button>
);

export default Button;

如果愿意,可以使用sass @extends或@mixin

编辑:尚未测试过,但是仅仅是使用.pageBtn { // First some defaults composes: btn from './Button.module.scss'; // And override some of the defautls here background: yellow; } webpack是否可以确保将默认值仅捆绑一次?因此,您不再需要使用composes

来更改Button.js代码。
回答3

解决方案1 ​​

我知道这很明显,但是还是可以的:

在覆盖CSS规则上设置!important,从而绕过特定性:

[PageName].module.scss

.btn {
  color: yellow !important;
}

但是,我认识的大多数严格的开发人员都会不惜一切代价避免使用此关键字。

为什么? 因为当您开始大量使用!important时,css是调试的噩梦。如果您开始以更高的特异性编写!important规则,那么您就知道自己走得太远了

它仅适用于像您这样的极端情况,您不妨使用它。


解决方案2

修复了CRA配置以强制样式标签顺序。

毕竟是开源的:)

您可以在此处输入有关此错误的输入(upvote可能会使其更容易看到):

https://github.com/facebook/create-react-app/issues/7190


解决方案3(更新)

您可以在新的customButton.scss文件中创建SCSS混合,以生成具有更高特异性的CSS规则:

// customButton.scss
@mixin customBtn() {
  :global {
    .customBtn.override {
      @content;
    }
  }
}

我们将使用两个静态类名(使用:global选择器),因为那样的话,它们的名称不会根据它们从何处导入而改变。

现在在页面的SCSS中使用该混合:

// [pageName].module.scss
@import 'customButton.scss';

@include customBtn {
  color: yellow;
}

css输出应为:

.customBtn.override {
  // put everything you want to customize here
  color: yellow;
}

Button.jsx中:除了styles.btn以外,还将两个类名都应用到您的按钮上:

// Button.jsx
const Button = ({ children, className = '' }) => (
    <button className={`${styles.btn} customBtn override ${className}`}>
      {children}
    </button>
);

(请注意,这些不是通过styles对象进行引用,而是直接通过类名进行引用)

主要缺点是这些不是动态类名,因此您必须像以前一样小心避免自己发生冲突。 但我认为应该可以解决问题