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:
- 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. - 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. - 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. Webpackcss-loader
provides this capability. - 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. - 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 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 :local
to 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.