CSS Modules Detailed Explanation and Practice in React

CSS is the slowest evolving piece in the front-end field. Due to the rapid popularity of ES2015/2016 and the rapid development of tools such as Babel/Webpack, CSS has been far behind and has gradually become a pain point for large-scale project engineering. It has also become a problem that must be solved before the front-end moves towards complete modularization.

There are many CSS modular solutions, but there are two main categories. One is to completely abandon CSS and use JS or JSON to write styles. Radium , jsxstyle , react-style  belong to this category. The advantage is that it can provide CSS with the same powerful modularity as JS; the disadvantage is that it cannot use the mature CSS pre-processor (or post-processor) Sass/Less/PostCSS, :hover and  :active it is complicated to deal with pseudo-classes. The other is to still use CSS, but use JS to manage style dependencies, which represents  CSS Modules . CSS Modules can maximize the combination of existing CSS ecology and JS modularization capabilities, and the API is concise to almost zero learning cost. Separate JS and CSS are still compiled when released. It does not depend on React, as long as you use Webpack, you can use it in Vue/Angular/jQuery. It is the best CSS modular solution in my opinion. Recently, it has been widely used in projects, and the details and ideas in practice are shared below.

What problems did CSS modularization encounter?

CSS modularity is important to solve two problems: import and export of CSS styles. Import on demand flexibly to reuse code; when exporting, you must be able to hide the internal scope to avoid global pollution. Sass/Less/PostCSS and others tried to solve the problem of weak CSS programming ability. As a result, they did a good job, but this did not solve the most important problem of modularity. Facebook engineer  Vjeux  first raised a series of CSS-related problems encountered in React development. Together with my personal opinion, the summary is as follows:

  1. Global pollution
    CSS uses a global selector mechanism to set styles, which has the advantage of making it easy to rewrite styles. The disadvantage is that all styles are globally effective, and styles may be overwritten by mistakes, resulting in very ugly  !important, even inline  !important and complex selector weight count tables , which increase the probability of making mistakes and the cost of use. The Shadow DOM in the Web Components standard can completely solve this problem, but its approach is a bit extreme. The style is completely localized, which makes it impossible to rewrite the style externally and loses flexibility.
  2. Naming confusion
    Due to the problem of global pollution, in order to avoid style conflicts when multi-person collaborative development, selectors are becoming more and more complex, and it is easy to form different naming styles, which is difficult to unify. With more styles, the naming will become more confusing.
  3. Incomplete dependency management
    Components should be independent of each other. When a component is introduced, only the CSS styles it needs should be introduced. But the current practice is to introduce its CSS in addition to JS, and it is difficult for Saas/Less to compile a separate CSS for each component, and the introduction of CSS for all modules causes waste. The modularity of JS is very mature. It is a good solution if JS can manage CSS dependencies. Webpack  css-loader provides this capability.
  4. Unable to share variables.
    Complex components need to use JS and CSS to process styles together, which will cause some variables to be redundant in JS and CSS. Sass/PostCSS/CSS, etc. do not provide the ability to share variables across JS and CSS.
  5. Incomplete code compression.
    Due to the uncertainty of the mobile network, CSS compression has now reached an abnormal level. Many compression tools will convert '16px' to '1pc' in order to save one byte. But I can't do anything with very long class names, and the force is not used on the blade.

The above problem cannot be solved if only CSS itself is used. If CSS is managed through JS, it is very easy to solve. Therefore, the solution given by Vjuex is completely  CSS in JS , but this is equivalent to abandoning CSS completely. In JS When writing CSS in Object syntax, it is estimated that all the friends I just saw were shocked. Until CSS Modules appeared.

CSS Modules modular scheme

CSS Modules Logo

CSS Modules uses  ICSS  to solve the two problems of style import and export. Correspond to  :import and  :export two new pseudo-classes respectively.

:import("path/to/dep.css") {
  localAlias: keyFromDep;
  /* ... */
}
:export {
  exportedKey: exportedValue;
  /* ... */
}

But programming using these two keywords directly is too troublesome, and they are rarely used directly in actual projects. What we need is the ability to manage CSS with JS. After combining with Webpack  css-loader , you can define styles in CSS and import them in JS.

Enable CSS Modules

// webpack.config.js
css?modules&localIdentName=[name]__[local]-[hash:base64:5]

Adding is  modules to enable it, which localIdentName is to set the naming rule of the generated style.

/* components/Button.css */ 
.normal {/* all styles related to normal*/} 
.disabled {/* all styles related to disabled*/}
/* components/Button.js */
import styles from './Button.css';

console.log(styles);

buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`

The generated HTML is

<button class="button--normal-abc53">Submit</button>

Note  button--normal-abc53 that CSS Modules follow the  localIdentName automatically generated class name. Among them  abc53 is the sequence code generated according to the given algorithm. After such obfuscation, the class name is basically unique, which greatly reduces the probability of style coverage in the project. At the same time, modify the rules in the production environment to generate a shorter class name, which can improve the compression rate of CSS.

The result printed by the console in the above example is:

Object {
  normal: 'button--normal-abc53',
  disabled: 'button--disabled-def884',
}

CSS Modules process all the class names in CSS, and use objects to save the correspondence between the original class and the obfuscated class.

Through these simple processing, CSS Modules have achieved the following points:

  • All styles are local, which solves naming conflicts and global pollution problems
  • Flexible configuration of class name generation rules can be used to compress class names
  • Just refer to the JS of the component to get all the JS and CSS of the component
  • Still CSS, almost zero learning cost

Style default partial

After using CSS Modules, it is equivalent to adding one to each class name  :localto achieve the localization of the style. If you want to switch to the global mode, use the corresponding one  :global.

.normal { 
  color: green; 
} 

/* The above is equivalent to the following*/ 
:local(.normal) { 
  color: green; 
} 

/* Define the global style*/ 
:global(.btn) { 
  color: red; 
} 

/* Define multiple global styles*/ 
:global { 
  .link { 
    color: green; 
  } 
  .box { 
    color: yellow; 
  } 
}

Compose to combine styles

For style reuse, CSS Modules only provides the only way to deal with: composes combination

/* components/Button.css */ 
.base {/* all common styles*/} 

.normal { 
  composes: base; 
  /* normal other styles*/ 
} 

.disabled { 
  composes: base; 
  /* disabled other styles*/ 
}
import styles from './Button.css';

buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`

The generated HTML becomes

<button class="button--base-daf62 button--normal-abc53">Submit</button>

Because .normal of the composes in the  middle  .base, normal will become two classes after compilation.

Composes can also combine styles in external files.

/* settings.css */ 
.primary-color { 
  color: #f40; 
} 

/* components/Button.css */ 
.base {/* All common styles*/} 

.primary { 
  composes: base; 
  composes: primary- color from'./settings.css'; 
  /* primary other styles*/ 
}

For most projects,  composes Sass/Less/PostCSS is no longer needed. But if you want to use it, because it  composes is not a standard CSS syntax, an error will be reported during compilation. You can only use the preprocessor's own syntax for style reuse.

class naming skills

The naming convention of CSS Modules is extended from BEM. BEM divides style names into 3 levels, namely:

  • Block: Corresponding module name, such as Dialog
  • Element: Corresponding to the node name Confirm Button in the module
  • Modifier: Corresponding to the state of the node, such as disabled, highlight

In summary, BEM finally got the class name  dialog__confirm-button--highlight. The double symbol  __ and  -- is used to distinguish it from the separator between words in the block. Although it may seem strange, BEM has been adopted by many large projects and teams. We also recognize this naming method in practice.

The CSS file name in CSS Modules exactly corresponds to the Block name, so you only need to consider Element and Modifier. The way BEM corresponds to CSS Modules is:

/* .dialog.css */
.ConfirmButton--disabled {
}

You can also not follow the complete naming convention and use camelCase to put Block and Modifier together:

/* .dialog.css */
.disabledConfirmButton {
}

How to realize CSS and JS variable sharing

Note: There is no concept of variables in CSS Modules. CSS variables here refer to variables in Sass.

The :export keywords mentioned above  can output variables in CSS to JS. The following demonstrates how to read Sass variables in JS:

/* config.scss */
$primary-color: #f40;

:export {
  primaryColor: $primary-color;
}
/* app.js */
import style from 'config.scss';

// 会输出 #F40
console.log(style.primaryColor);

CSS Modules tips

CSS Modules are subtracting existing CSS. In order to pursue simplicity and control , the author suggests to follow the following principles:

  • Do not use selectors, only use class names to define styles
  • Do not cascade multiple classes, use only one class to define all styles
  • All styles are  composes combined to achieve reuse
  • Not nested

The above two principles are equivalent to weakening the most flexible part of the style, which is difficult for new users to accept. The first is not difficult to practice, but the second, if the module status is too much, the number of classes will increase exponentially.

Be aware that the above is called a recommendation because CSS Modules does not force you to do this. It sounds contradictory. Because most CSS projects have deep historical legacy, too many restrictions mean increased migration costs and the cost of cooperation with external parties. There must be some compromises in the initial use. Fortunately, CSS Modules does this very well:

What if I use multiple classes for an element?

No problem, the style still works.

How can I use a class with the same name in a style file?

No problem, although these classes with the same name may be compiled with random codes, they still have the same name.

What if I use pseudo-classes, tag selectors, etc. in the style file?
No problem, all these selectors will not be converted and will appear in the compiled css intact. In other words, CSS Modules will only transform the styles related to the class name and id selector name.

But note that the above three "ifs" should not happen as much as possible.

CSS Modules combined with React practice

In  className use css directly at the  class name.

/* dialog.css */
.root {}
.confirm {}
.disabledConfirm {}
import classNames from 'classnames';
import styles from './dialog.css';

export default class Dialog extends React.Component {
  render() {
    const cx = classNames({
      [styles.confirm]: !this.state.disabled,
      [styles.disabledConfirm]: this.state.disabled
    });

    return <div className={styles.root}>
      <a className={cx}>Confirm</a>
      ...
    </div>
  }
}

Note that generally the class name corresponding to the outermost node of the component is  root. The classnames  library is used here  to manipulate class names.
If you don't want to input frequently  styles.**, you can try  react-css-modules , which uses high-order functions to avoid repeated input  styles.**.

CSS Modules combined with historical project practices

A good technical solution is not only powerful and cool, but also capable of smooth migration of existing projects. CSS Modules are very flexible at this point.

How to override local styles outside

When a confused class name is generated, the naming conflict can be resolved, but because the final class name cannot be predicted, it cannot be overwritten by a general selector. Our current project practice is to add data-role attributes to the key nodes of the component  , and then override the style through the attribute selector.

Such as

// dialog.js
  return <div className={styles.root} data-role='dialog-root'>
      <a className={styles.disabledConfirm} data-role='dialog-confirm-btn'>Confirm</a>
      ...
  </div>
// dialog.css
[data-role="dialog-root"] {
  // override style
}

Because CSS Modules only transform class selectors, there is no need to add attribute selectors here  :global.

How to coexist with global styles

The front-end project will inevitably introduce normalize.css or other global css files. Using Webpack allows global styles and local styles of CSS Modules to coexist harmoniously. The following is the webpack configuration code used in our project:

module: {
  loaders: [{
    test: /\.jsx?$/,
    loader: 'babel'
  }, {
    test: /\.scss$/,
    exclude: path.resolve(__dirname, 'src/styles'),
    loader: 'style!css?modules&localIdentName=[name]__[local]!sass?sourceMap=true'
  }, {
    test: /\.scss$/,
    include: path.resolve(__dirname, 'src/styles'),
    loader: 'style!css!sass?sourceMap=true'
  }]
}
/* src/app.js */ 
import'./styles / app.scss'; 
import Component from'./view 

/Component ' /* src/views/Component.js */ 
// The following are component-related styles 
import ' ./Component.scss';

The directory structure is as follows:

src
├── app.js
├── styles
│   ├── app.scss
│   └── normalize.scss
└── views
    ├── Component.js
    └── Component.scss

In this way, all global styles can  src/styles/app.scss be imported into it. All other src/views styles included in the catalog  are partial.

to sum up

CSS Modules are a good solution to the modularization problems currently faced by CSS. It can be used in conjunction with Sass/Less/PostCSS, etc., and can make full use of existing technology accumulation. At the same time, it can be flexibly matched with the global style, which is convenient for the gradual migration to CSS Modules in the project. The implementation of CSS Modules is also lightweight, and can be migrated at low cost after standard solutions are available in the future. If you happen to encounter similar problems in your product, it is worth a try.

Guess you like

Origin blog.csdn.net/promiseCao/article/details/86702223