9
头图

Image source: https://unsplash.com

Author of this article: iwyvi

background

With the development of the business, some code logic may be used in multiple projects at the same time. In order to avoid copying and pasting the code every time it is used and updated, it is necessary to construct a component library. There are many aspects to consider when building a component library. This article mainly discusses how to choose a CSS style scheme suitable for a component library in the React ecosystem.

Currently developing a project running in a browser, the styles that can be selected are mainly divided into three types according to the writing method: the first is regular CSS (regular CSS), that is, native CSS and various preprocessing languages; the second is in JS The CSS in JS scheme for profiling styles, such as styled-components ; the third is to write tool classes in HTML, and the CSS framework generates the corresponding style scheme, such as Tailwind CSS .

However, when we build a component library, the angle of consideration may be different from that of ordinary projects. Not only the development experience, but also the user's feelings must be considered. Therefore, this article does not analyze the CSS style scheme from the writing level, but discusses the following two issues from the perspective of component library development:

  1. How CSS relates to JS, i.e. how components use styles, and how CSS participates in packaging.
  2. How the component library handles style naming conflicts under different associations.

case analysis

In the React ecosystem, there is no unified style management solution, so there are multiple solutions for how to deal with CSS, and different solutions have their own advantages and disadvantages.

When constructing a component library, on the one hand, we hope that it will be easier to write during development, and on the other hand, we hope that it will be more convenient to use. CSS is an indispensable part of the component library. Choosing an appropriate style scheme will affect the subsequent development and use experience.

How CSS and JS are related

First, let's start with how CSS and JS are related. Different CSS and JS association methods have different style introduction methods, and also have their own characteristics in terms of on-demand loading, performance and SSR support.

This article divides the components library's use of CSS into the following three types:

  1. Style and logic are separated . The CSS and JS of the component are separated at the code level. The JS does not introduce style files, and separate logic and style files are generated when the component library is packaged. For users of the component library, to add a component, the component code and CSS file need to be imported separately. Component libraries that use this solution include Ant Design , Zent , etc.
  2. Style and logic combine . Package the JS and CSS of the component together, and finally output only the JS file. When using it, you only need to import components and you can use it directly. There are currently two main implementation forms of this scheme:

    1. Write CSS in JS. For example, use styled-components , Emotion and other CSS in JS solutions. Representative component libraries include MUI , Mantine , etc.
    2. Regular CSS is still used when writing code, the style file in the component import , and the CSS is entered into JS through the packaging tool. For example, use webpack with style-loader . Components (libraries) based on this scheme include react-mobile-picker , Angular Material and so on.
  3. Styles and logical associations . The JS and CSS of the component are separated at the code level, and independent logic and style files are generated after packaging, but the style file is directly referenced in the component, and the corresponding import statement is retained in the packaging result. There are Semi Design , React Spectrum , Ant Design Mobile 5.0 that use this scheme.

Each of these options has its own advantages and disadvantages, and the following article will analyze them in detail:

style and logic separation

This CSS organization scheme is most common in the construction of component libraries, and there are a large number of component libraries in various frameworks that use this form. CSS is written in a separate style file. The JS of the component does not directly import CSS, but when using the component library, you need to import components and styles separately.

The component libraries that use this solution have a notable feature. In their installation tutorials, users will be asked to introduce one or more CSS files by themselves. Generally speaking, this CSS file will contain the styles of all the components of the entire component library.

The advantages of this scheme are:

  • It has wide applicability and can support various development environments of component library users.
  • Regardless of the technology stack of the component library, the same set of styles can be used on component libraries based on multiple frameworks.
  • There is no need to consider the support for SSR (server-side rendering), the CSS file is provided externally, so the SSR process is completely controlled by the user of the component library.
  • It can directly provide source files such as less , sass and so on, which is convenient for external coverage of variables, and realizes functions such as theme customization or skinning.

But this solution also has some problems:

  • Requires users to manually import style files. If the complete CSS file is directly imported, but not all components in the component library are used in actual use, some useless styles will be packaged into the project.
  • It is more complicated for the component library to support the function of importing CSS on demand. It not only requires the developer of the component library to process the packaging process and products, but also requires the user to import style files according to certain rules. First, the component library developer needs to define a set of directory organization specifications for style files, so that they can support packaging style files in units of components during the packaging process, and then users can manually import style files for corresponding components as needed. For component libraries with specific directory organization specifications, there are already plugins that can assist in generating import statements in the compilation phase, such as babel-plugin-import , unplugin-vue-components , etc.
  • If there is a reference relationship between the components inside the component library, in order to implement on-demand import, the styles of the packaged components may be redundant.

style and logic

In this scheme, CSS is stored in JS in the form of strings or objects, and usually the packaged code will include a runtime for mounting styles.

This scheme has the following advantages:

  • There is no need for the user to introduce the style file separately, only the import component can be used
  • Naturally supports on-demand loading, each component only needs to handle its own style

But again this solution is not perfect:

  • You need to bring a runtime, which may increase the size of the code and have a performance impact.
  • Compared to a separate CSS file, the styles of this scheme are all in JS, which may not take full advantage of browser caching.
  • Support for SSR requires capabilities provided by specific implementations, which will be described in detail later.

This scheme mainly has the following two implementation forms:

CSS in JS

CSS in JS is a different style scheme from regular CSS files. It is mostly used in the React ecosystem and solves some of the pain points that exist when using regular CSS, such as naming conflicts and style redundancy.

Today, CSS in JS frameworks are flourishing, and there are two categories: runtime and zero-runtime. styled-component and emotion This category belongs to the runtime, style writing, changing and mounting are all done in JS, and the framework will provide the corresponding runtime to handle these tasks. linaria and vanilla-extract This category belongs to zero runtime. They are written in a similar way to the runtime framework, but need to configure the compilation process, and will output standard CSS after compilation.

Because this section discusses the combination of style and logic, the CSS in JS mentioned next refers to the runtime framework.

The introduction of runtime means more JS code, so there must be a performance difference between CSS in JS and regular CSS. Real-world CSS vs. CSS-in-JS performance comparison This article analyzes the performance difference between styled-component and linaria from the perspective of user experience (as can be seen from the above linaria a CSS in JS framework with zero runtime, i.e. it can represent the performance of regular CSS), concludes that regular CSS outperforms CSS in JS in every way.

In addition to performance issues, since the style injection process is performed by the runtime provided by the CSS in JS framework, the SSR process also requires additional processing by the framework. Fortunately, almost all mainstream CSS in JS frameworks provide SSR support.

At present, most CSS in JS frameworks require users to add additional style collection and insertion processes in the server-side rendering process in order to successfully use SSR. A few frameworks such as emotion provide an SSR support solution that does not require additional configuration. When rendering on the server side, the style of the component will be inline style tag is placed in front of the component DOM . But this solution also has some problems: when the same component is used multiple times in the page, the rendered HTML will contain multiple duplicate styles; in addition, because the extra style tags are inserted , will affect :nth-child() this type of selector.

Why is this extra process unavoidable in SSR ? A rendering on the server side can be considered as calling the ReactDOMServer renderToString method of --- 2dce3a5e93719f8c57ae118356c33318---, but this method does not provide the ability for internal components to perceive the rendering state. For a component, if it does not record whether it has been rendered, it can only adopt a zero-configuration scheme like emotion , and the component will bring its own style every time it is rendered; The global variable is used to record whether it has been rendered. If it has not been rendered, insert the style. If it has been rendered, it will not insert the style. It is not difficult to find that the component cannot distinguish multiple renderings on the server side, because the global variable used to record the state is always on the server side. It is the same. In order for the component to mark whether the state of being rendered is refreshed in each round of rendering, a variable needs to be created to store this state in each rendering, and the above behaviors are the additional style collection process mentioned above. .

However, it is precisely because of the addition of the style collection process that most CSS in JS solutions support the extraction of critical styles (Critical CSS), which can reduce the size of the above-the-fold request during SSR, which is also one of its advantages.

Bundle CSS into JS

This solution is generally to package regular CSS and JS directly together through a packaging tool and corresponding plug-ins, such as webpack + style-loader or rollup + rollup-plugin-styles .

Usually, the runtimes introduced by these plugins have DOM operations, and will report an error or do nothing in the SSR stage, and wait for the CSR stage to really inject styles, so it cannot support SSR .

To support SSR is not completely without choice, webpack ecological isomorphic-style-loader provides SSR support, basically equivalent style-loader . However, its implementation and effect are similar to the CSS in JS solution. During development, you need to give the component package a higher-order component to load styles. Also in the SSR stage, you need to collect and inject styles through the methods it provides. .

In the React ecosystem, few component libraries use this style scheme, and only a few single-component projects use this packaging scheme. On the one hand, the reason is that it is difficult to provide SSR support for this solution. On the other hand, since the regular CSS has been written, it is better to directly export the file and let the user handle it by himself.

However, in some non-React ecosystems, there are still many component libraries constructed using this solution, because their mainstream development tools provide a complete set of solutions including packaging and development, while the React ecosystem is full of flowers and there is no unified development. tools, so there is no uniform styling solution either.

style and logic

The development process of this scheme is similar to the style and logic separation scheme. The main difference is that the import statement of the imported style file is directly retained in the output result. If the user's project can process the CSS file correctly, then the Do only import components can be used. And this solution can also support on-demand loading without introducing a large and comprehensive CSS file.

But this scheme also has some drawbacks:

  1. For the developer of the component library, if the preprocessing language is used, the process of packaging and compilation will be more complicated, and the import statement of the final product of the component needs to be correctly associated with the compiled CSS file.
  2. There are certain requirements for the user's development configuration, which needs to be able to correctly process the CSS files imported by the code in the component library (for example, configure the corresponding loader under webpack ). If you need to support SSR , you also need to modify the configuration of the packaging tool, so that the component library files are also involved in the construction, to avoid rendering errors caused by the direct execution of CSS files on the node side.

Summarize

style and logic separation style and logic style and logic
Development and packaging process medium Simple complex
output file JS files and CSS files js file JS files and CSS files
Instructions Introduce JS and CSS respectively Only import JS Only import JS
load on demand Additional support required support support
performance impact none With extra runtime, it may have an impact none
SSR support Additional support is required (some plans do not support) Support (may require user to adjust configuration)
support writing Regular CSS / Zero Runtime CSS in JS Regular CSS / CSS in JS Regular CSS / Zero Runtime CSS in JS
key style extraction handle it yourself support handle it yourself

Component library style naming conflict handling

Resolving style naming conflicts is also a problem that needs to be considered when building a component library. Developers always want to use simpler names without naming conflicts.

Currently in the React ecosystem, the common schemes for creating style namespaces are as follows:

convention naming convention

The naming convention is to follow an artificial naming convention in the entire component library, such as the BEM specification or uniformly adding prefixes to all selector names. The advantage of this scheme is that there is no need to adjust the packaging scheme, but the disadvantage is that the rules are all based on human conventions, and it depends on the developer's self-consciousness during development, which may be troublesome when multiple people maintain; You also need to write some boilerplate code to generate canonical names, which is more labor-intensive.

CSS Modules

CSS Modules is a solution to the namespace problem. It can generate selector names based on specified rules, without requiring developers to abide by strict specifications, and avoid polluting global styles.

The following is a simple example, the original code is like this:

 .test {
   color: red;
}
 import styles from 'index.less';
// ...
<div className={styles.test} />

After conversion, it becomes like this:

 ._xxxxxx {
   color: red;
}
 var modules_xxx = {"test":"_xxxxxx"};
// ...
<div className={modules_xxx['test']} />

By adding a hash value to the selector, etc., the selector will not conflict with other places.

But when we develop component libraries with CSS Modules, we also need to consider these issues:

  • Using CSS Modules requires some processing of the compilation process of the component library. If you want to use a packaging scheme that separates style and logic, you need to remove the reference statement to the style file in the packaged code, and only keep the data converted by the selector name; if you use the scheme of style and logic association, you need to keep the selector in the At the same time as the name conversion, the compiled style file is correctly imported.
  • Because there is no guarantee that the user's development environment also supports CSS Modules, the style source files cannot be provided directly to the outside world.
  • Because the generated selector name is unstable and may change frequently, for the user of the component library, the component style cannot be overridden in the outer layer.

CSS in JS

Yes, the CSS in JS solution has appeared again, because this solution intentionally solved the problem of class naming at the beginning of its birth. Since selector names are dynamically generated, there is no need to follow naming conventions or worry about naming conflicts during development.

Take Emotion as an example:

 import styled from '@emotion/styled';
const Test = styled.div`
    color: red;
`;
// ...
<Test />

When running on the browser, the real DOM is rendered as:

 <style data-emotion="css">.css-1vdv3ej{color:red;}</style>
<!-- ... -->
<div class="css-1vdv3ej"></div>

As mentioned above, CSS in JS solutions are divided into two types: runtime and zero-runtime. The zero-runtime solution will eventually output CSS files with random selector names after compilation. This effect is similar to CSS Modules. For example vanilla-extract calls itself "CSS Modules-in-TypeScript". So zero-runtime CSS in JS schemes can be used in all schemes that support regular CSS.

Selection thinking

Association scheme selection

To choose a style scheme, first determine how CSS and JS relate, and then consider how to handle style naming.

If you plan to build a component library that supports multiple frameworks, the most preferred packaging solution is the separation of style and logic. Under this scheme, a basic style is produced, which can be shared in a multi-frame component library, and does not need to be processed separately in each framework, and has the widest applicability.

However, if you only need to support the React technology stack, the above solutions can be further considered according to the usage situation:

To construct a component library at the current time node, the point of this article is: under the requirement of compatibility, it can accept the writing method of CSS in JS, and can tolerate some of its own shortcomings (such as performance problems or some writing restrictions), it is preferred Consider a CSS in JS scheme with runtime. Developers of component libraries do not need to spend time on naming, importing and packaging styles, and can easily load styles on demand when using them, and support SSR and critical rendering path style extraction.

If the writing style of CSS in JS is unacceptable, and you still prefer the writing style of conventional CSS, or you feel that there are too many CSS in JS solutions and it is difficult to make a choice, you can also consider the following conventional CSS solutions:

If the application scenario of the component library clearly does not need to support SSR, and you do not need to consider obtaining the CSS file separately for performance optimization such as caching, you can choose to package the CSS directly into JS. This solution does not need to adjust the packaging process, and users only need import components to use, and the logic and style can support on-demand loading.

If the above conditions are not met, the scheme with style and logic association can be preferred. If the component library is used in an internal project, the project needs to support the style files directly imported in the packaged component library.

The separation of style and logic is the most general and safe solution. It should be noted that this solution is more troublesome to implement on-demand loading. Usually, the component library will consider providing a set of processing methods for generating on-demand loading style statements, such as guiding users to use babel-plugin-import and give the corresponding configuration.

Naming scheme choice

Choosing to write regular CSS, you also need to consider the naming rules for styles.

If it is a basic UI component library, it is more recommended to use the convention naming scheme:

  1. UI component libraries are usually maintained by relatively fixed maintenance personnel, which facilitates the implementation of a unified naming convention;
  2. When basic components are used in business, customization scenarios may be involved. This solution can directly provide source code to the outside world, which is convenient for covering variables or styles in specific services, giving users greater freedom.

If it is a business component library, it is more recommended to use the CSS Modules / zero-runtime CSS in JS solution:

  1. The business component library itself is strongly related to the business and may be maintained by developers of different businesses. It is difficult to implement a unified naming convention, and it cannot be guaranteed that everyone can strictly abide by it. And using CSS Modules can help developers to ensure that the namespace of styles will not be polluted.
  2. Business components are less involved in custom scenarios and do not need to meet the needs of overriding variables or styles.

Summarize

There is currently no dominant style scheme in the React ecosystem, and various existing schemes have their own advantages and disadvantages. Therefore, choosing a suitable style scheme requires comprehensive consideration of many aspects.

This article compares and analyzes the style scheme selection of the component library from the two aspects of the association between CSS and JS and the handling of naming conflicts. There is no perfect scheme, but there are definitely more suitable ones for different business scenarios. I hope this article can provide some help for the selection of components when building a component library.

refer to

This article is published from the NetEase Cloud Music technical team, and any form of reprinting of the article is prohibited without authorization. We recruit various technical positions all year round. If you are ready to change jobs and happen to like cloud music, then join us at grp.music-fe(at)corp.netease.com!

云音乐技术团队
3.6k 声望3.5k 粉丝

网易云音乐技术团队