preface
Recently, I developed a set of customized C-side component library in my business. In this process, I encountered some problems in the selection and design of component library technology. After referring to multiple component libraries inside and outside the company, I determined the final plan. This paper hopes to enlightens readers on the technology selection and design of component library by introducing the scheme comparison in the process of technology selection and the considerations in the design of component library.
The idea of a complete component library scheme
Technical selection of component library
Style scheme selection
Sass/Less
|
Atomic CSS
|
CSS-in-JS
|
|
Full support for style overrides
|
✅
|
✅
|
❗(className support required)
|
Rem layout support
|
✅
|
✅
|
❗(most library support)
|
readability
|
strong
|
A little weak
|
strong
|
It costs
|
low
|
In the
|
high
|
Whether SSR is supported
|
Natural support
|
Natural support
|
Need additional support
|
Whether streaming rendering is supported
|
Natural support
|
Natural support
|
Need additional support
|
Support postcss
|
✅
|
✅
|
❗(own plugin ecosystem)
|
In fact, all three styles can coexist, but the actual development is dominated by one or the other.
Sass/Less
This is the most familiar approach, and it has the advantages of being flexible, cheap to develop (most engineers are familiar with them), fully supporting the style of external overridden components, and the disadvantages of being difficult to debug (you need to go to Runtime to know the hit rules), and difficult to implement static analysis.
Atomic CSS
Using Atomic CSS allows for a smaller package size when the UI is sufficiently standardized, and for individual components, there is no need to declare styles except for a few that cannot be abstracted and custom animations. The downside, of course, is that the code is slightly less readable. At the same time, developers need to be familiar with the atomic style of the project, which increases the development cost.
CSS-in-JS
Css-in-js refers to a third party library, including styled- Component, Emotion, AND JSS, that generates CSS styles through JS at runtime. The advantage of CSS-in-JS is that it can effectively solve the problem of “component styles change with data”. The disadvantage, however, is that in order to support external overwriting of internal elements, the internal elements need to be given a className, and postCSS is not supported. Instead, the specific CSS-in-JS library has its own plugin ecology. A few libraries, such as Emotion, do not have rem support. In addition, when doing SSR and streaming rendering, both need to increase the extraction style logic in node layer, which increases the development cost and additional overhead.
In summary: Atomic CSS is a good choice if you have a mature UI specification. Secondly, using traditional SASS /less styles is also maintenance friendly (most front-end developers are familiar with it). When choosing CSS-in-JS, consider the team’s development habits and the cost of getting started.
Icon scheme selection
React Component
|
iconfont
|
SVGUseElement
|
Write the SVG element directly
|
Base64 introduced
|
|
According to the need to load
|
✅
|
❌
|
✅
|
✅
|
✅
|
reuse
|
✅
|
✅
|
❌
|
❌
|
❌
|
Icon to flip
|
✅
|
✅
|
✅
|
✅
|
✅
|
Style cover
|
✅
|
❗ (Support)
|
✅
|
✅
|
❗ (Support)
|
Rendering quality
|
✅
|
❗ (WebKit distortion)
|
✅
|
✅
|
✅
|
Modify SVG internal elements
|
✅
|
❌
|
✅
|
✅
|
❌
|
When choosing the Icon scheme, besides the rendering quality, we should also focus on its flexibility in order to have a better fit.
iconfont
Iconfont has the advantage of being the most compatible and supports IE6 and later versions. However, because iconfont scheme uses icon as text, rendering distortion is caused by anti-aliasing of text in webKit kernel browser. In addition, because all ICONS are packaged into a font file, on-demand loading is not supported, resulting in a large package size. This can easily lead to a page refresh after loading the icon font:
Base64 introduced
Base64 is also a common method, but because SVG is introduced as a background image, it can only control its size, cannot overwrite its color, and cannot modify elements inside SVG, which is not flexible enough. For h5 in the end, which usually adopts MPA structure, it is not conducive to the reuse of ICON between different spas. Also, the base64 string is about 1.3 times the length of an SVG file (optimized).
React Component, SVGUseElement, and write SVG elements directly
Each of these approaches essentially renders SVG as an HTML element, but uses it differently.
The compatibility of SVG’s basic capabilities is basically fine except for the lack of animation and scaling in IE11, while SVG effect(mainly using transform, filter, etc.) is well supported on android4.4 and above. SVG has a bottleneck in animation performance, and fortunately we can use CSS animation instead.
The disadvantage of writing SVG elements directly is that it is completely impossible to reuse the same icon.
Svguseelements are implemented using
elements,
elements, and SVG Fragment identifiers, but in general, SVG elements are declared at the top and introduced with
As a result of the current research, the best way is to use SVGR to convert SVG to React Component, which supports on-demand, full style overwriting capability. At the same time, it supports custom AST templates, which can add custom classnames to SVG elements during conversion. It is easy to implement automatic adaptation of ICON RTL and Dark Mode(more on this later).
SVGR integrates SVGo to optimize SVG files. It can erase useless attributes and hidden elements in SVG. For details, see SvGo-Github.
Summary: At present, it seems that using SVGR to convert SVG to generate React Component to build icon is the best way, which can be easily loaded and reused on demand and has the strongest adaptability. Icon can be made into a dedicated NPM package for use in component libraries or directly in business repositories.
The core design of the component library
Dark Mode is suitable
In fact, this section discusses compatibility issues and real-world business scenarios when using the Dark Mode capabilities of component libraries on a business basis. However, the component library itself serves the business. In this sense, this section is also a part of the component library, which provides guidance on how to provide better Dark Mode adaptation capabilities.
Multi-topic ability
Dark mode is essentially a multi-theme problem at run time, with support for switching different theme colors at run time. We can use CSS variables to define colors and then use them in Sass/Less/ CSS conventions:
:root{
--bg-default: #fff;
}
:root[theme="dark"]{
--bg-default: #000;
}
.button{
background-color: var(--bg-default);
}
Copy the code
This way, as soon as we set the value of the custom theme property to dark in the < HTML > element, the color will be switched automatically. As long as we define the color variable and agree to use it, we can support multiple themes by writing it once when developing the component.
Unfortunately CSS variables have compatibility issues on android4, IE11 and below. We have the following three solutions:
css-vars-ponyfill
|
postcss-css-variables
|
Generates the bottom pocket property
|
|
Support for multiple color
|
✅
|
✅
|
❌
|
Extra package size
|
7.3KB(minifed + Gzipped)
|
And the number of theme colors
|
Almost negligible
|
Support the SSR
|
✅
|
✅
|
✅
|
We can implement a PostCSS plugin to generate bottom-pocket attributes like this:
Button {background-color: var(--bg-default); }.button{background-color: # FFF; Background-color: var(--bg-default); background-color: var(--bg-default); // For browsers that support CSS variables this line overrides the previous line}Copy the code
The biggest advantage is that the increased package size is almost negligible, while the disadvantage is that colors that do not support CSS variables effectively force a set of theme colors to be displayed. For mobile in-app pages, environments that do not support CSS variables can be equated to environments that do not have dark mode, using a light mode theme color as a backstop.
There is another way to achieve compatibility, such as the following:
.button{
background-color: #fff;
}
.theme-a .button{
background-color: #000;
}
.thema-b .button{
background-color: #ccc;
}
Copy the code
The advantage of this is that there are no compatibility issues at all. The disadvantage is that it increases development costs. Fortunately, We can easily generate such declarations from the writing of CSS variables using postCSS-CSS-variables. Another disadvantage is that the additional CSS package size multiplies as the theme color increases.
Css-vars-ponyfill supports multiple theme colors perfectly, with the disadvantage of generating a fixed extra package size.
Summary: Support for multi-theme colors at runtime mainly uses CSS variables, while the business repository solves compatibility issues and can be selected on a case-by-case basis. If it is an in-end H5 and only needs dark and light mode, consider using the PostCSS Plugin to generate the bottom of the pocket attribute. Otherwise, use CSS-vars-ponyfill or postCSs-CSS-variables.
Judge the Dark Mode
Media query (PREFERence-color-scheme
) |
The JS API listens for media queries
|
|
Whitelist escape
|
Not easy to support
|
Easy to support
|
Media queries
Dark Mode can be easily detected using the PREFERn-color-scheme feature, combined with our CSS variables, like this:
:root{ --bg-default: #fff; } @media (prefers-color-scheme: dark) { :root{ --bg-default: #000; Root [theme="light"]{--bg-default: # FFF; }Copy the code
Whitelist escape refers to the fact that in our business, there may be some pages that do not support Dark Mode, such as active pages, lottery pages, etc. We can force light Mode by adding a theme attribute to the HTML.
The advantage of media query is that it is easy to use. Media query automatically listens for changes in system Settings (whether to turn on dark mode) without adding extra code to THE HTML. The disadvantage lies in the need to escape the case, the writing is cumbersome.
The JS API listens for media queries
Examples of using the JS API are as follows:
<body>
<script>
const mql = window.matchMedia('(prefers-color-scheme: dark)');
function matchMode(e) {
const $root = document.documentElement;
if (e.matches) {
$root.setAttribute('theme', 'dark');
} else {
$root.removeAttribute('theme');
}
}
mql.addListener(matchMode);
</script>
<div id="root"></div>
</body>
Copy the code
The styles section looks like the CSS variable example we started with:
:root{
--bg-default: #fff;
}
:root[theme="dark"]{
--bg-default: #000;
}
Copy the code
The advantage of this scheme is that it is flexible and easy to add other logic to the script to support whitelist escape. The disadvantage is that in order to support SSR, this part of the script needs to be written at the top of the BODY element of the HTML template, which increases the access cost for the user of the component library.
Summary: Although using JS API to listen to media query to determine Dark Mode will slightly increase the cost of accessing component library from the perspective of possible whitelist escape and business changes. But it is worth the flexibility benefits and is recommended.
RTL adaptation
RTL is an essential part of the component library if it supports internationalization. RTL(Right to Left) refers to the fact that some languages, such as Arabic, are read from right to left, which leads to the need to reverse left and right on the UI (in most cases, with some exceptions), some ICONS need to be mirrored, gestures slide from right to left, input fields are entered from right to left, See BiDirectionality – Material for more details.
Layout of the adapter
We can use the native DIR attribute to support most of the RTL capabilities by setting the attribute dir=’ RTL ‘on the HTML. In the browser environment, you can use the NavigatorLanguage API to get the page language, and then set the value of dir based on whether the current language is RTL. In the Node environment, the request header accept-language can be used to obtain the page Language, determine the value of dir and inject it into the returned page. Setting dir=’ RTL ‘automatically reverses the global Flex horizontal layout and automatically right-aligns the text (unless a text-align declaration is displayed). However, attributes such as marin-left, left, and border-left (similar in other directions) do not automatically adapt. There are several ways to solve this problem. We can look at the code intuitively:
// Method 1: style overlay, ant design uses this method. Button {margin-left: 16px; } html[dir='rtl'] .button{ margin-left: 0; Margin-right: 16px; margin-right: 16px; margin-right: 16px; HTML [dir=' LTR ']. Button {margin-left: 16px; margin-left: 16px; } html[dir='rtl'] .button{ margin-right: 16px; } // Atomic CSS HTML [dir=' LTR '].ms-16{margin-left: 16px; } html[dir='rtl'] .ms-16{ margin-right: 16px; }Copy the code
We can see that method 1 and method 2 are not very convenient, while method 3 requires the UI to be very normalized (converging margin and padding to enumerable states), and cannot cover all cases. Fortunately, we can use rTL-sensitive Properties like margin-inline-start to solve this problem (see CSS Logical Properties for more) :
.button{// Other attributes margin-inline-start: 16px; }Copy the code
There are some compatibility issues in the actual use, we can use postCSs-bidirection, which will convert the above declaration to:
.button{// other attributes} HTML [dir=" LTR "]. Button {margin-left: 16px; } html[dir="rtl"] .button { margin-right: 16px; }Copy the code
We can use RTL sensitive properties, which do not conflict with Atomic CSS and can be used in combination when appropriate.
Icon adaptation
Under RTL, part of the icon needs to be mirrored. As we described earlier, the best way to use Icon is to convert SVG to React Component using SVGR. In this way, we can add a class for ICONS that need RTL flipping at conversion time, such as flip-rTL, and then the component library provides the following CSS declarations for business use:
[dir="rtl"] .flip-rtl {
transform: scaleX(-1);
}
Copy the code
Whether the icon is mirrored or not may be a matter biased to the design side. If we host the design draft of icon on figMA platform, we can agree with the designer on the naming of the icon that needs to be flipped under RTL. Then implement a script that automatically downloads SVG source files, processes SVGO, converts them to React Component using SVGR, and automatically determines whether flip- RTL class is needed during the conversion process. In this way, both the component library and the business development process, the R&D does not need to worry about icon’s mirroring, reducing communication and acceptance costs.
Gestures fit
Some components, such as the progress bar component, slide left to right under traditional LTR, but slide right to left under RTL. We could simply add isRTL props to this class of components, but this is obviously not a good idea because we need to calculate and pass in the props value whenever we use it. With this in mind, we can abstract some common capabilities, global injection, for the entire component library.
Global configuration
We can use the React Context to inject global configurations such as direction(LTR/RTL) and prefixCls(class name prefix). For example, the root node of the application has a ConfigProvider package around it:
import ConfigProvider from 'myComponent';
// ...
export default () => (
<ConfigProvider direction="rtl">
<App />
</ConfigProvider>);
Copy the code
Get from component using hooks:
import { useConfigContext } from 'myComponent'; export default () => { const { rtl, prefixCls, platform } = useConfigContext(); / /... }Copy the code
Context can even be used to achieve local configuration that is different from global configuration. It is very flexible and can easily expand the capability of global configuration in the later stage. It also solves the pain point that we repeatedly pass in some global universal properties as props to each component.
The component hierarchy
Before the development of the component library, the hierarchy of the component library should be planned in order to increase the code reuse and flexibility of the component library.
We should plan out some basic components first to avoid further refactoring. Switch, Checkbox, and Radio (which logically differ only in whether the active state is cancelled or still activated) can be implemented on top of a BaseSwitch abstraction. For buttons, which also occur in other components such as popovers, we can abstract a BaseButton or rewrite its style in other components using ConfigProvier’s prefixCls. For form-related components, you can first implement some atomic input, textarea, and then implement Form with Lable, check state, UI and related form. input, etc. For shell layer components, you can encapsulate a Portal component to provide capabilities and so on. Metrial UI also abstracts a Box component, all components are written based on the Box component, to achieve global layout and style control.
style
Stylewise, without Atomic CSS, we can encapsulate UI specifications (combinations of word weight, text size, and line height) as mixins in Sass /less, reducing the likelihood of errors. You can also encapsulate common capabilities such as text overflow display ellipsis, pseudo-element implementation with a 0.5px border, and so on. These encapsulated variables and mixins can be used not only within the component library, but also for business use (especially in custom component libraries). Also, the Z-index of components that differ from the UI component library should be specified to avoid a hierarchy that does not conform to expectations.
other
Some hooks used in the component library, such as the scrolling of frozen pages used by the shell component, can be used in mainstream open source libraries such as React-Use or custom developed. If a component library is expected to support Preact (a much lighter library with the react syntax), switching to preact can be used to avoid using a non-Preact-enabled syntax during development. At the same time, utils(some functional capabilities) used in component libraries should also be considered compatible with node environments to support SSR. Without introducing significant costs, the component library should take sufficient account of possible technology selection by the business side to avoid limiting technology implementation on the business side.
Component libraries are built using TSC or rollup, and animation libraries are used depending on your needs (CSS animations are lighter).
Additional details about the component library
Quality assurance
In terms of process, the quality assurance of component library mainly includes code review, strict UI acceptance and QA testing. From a technical point of view, the authority to send packets can be converged, and automatic packet sending can be realized in CI/CD combined with Seman-release, so as to eliminate the dangerous operation of randomly sending packets on non-master branches in the process of r&d. There are also unit tests, snapshot tests, E2E tests and other common technical means, limited to the length of this article will not be detailed.
specification
Specifications are designed to ensure quality, ease of use by business parties, and increase the extensibility of component libraries. For example, style encapsulation mentioned above, commonly used mixin encapsulation, mandatory use of color variables, etc. There is also a uniform component library API style specification designed to reduce the cost of use by the business side.
To effect
Component libraries usually have a demo site, and the main technology options are Stylegudist, Storybook, etc., which can be selected according to the team’s habits. For mobile component libraries, you can rewrite their components via webpack aliases to support mobile preview for easy UI acceptance. For internationalized components, devTools in the form of VConsole can be provided to visually switch between Dark /light Mode and RTL/LRT, improving efficiency in the development and testing process.
Some think
Component library development is a very uI-dependent business, and we need to communicate it well. At the same time, we should not be limited to the component library itself, but consider the efficiency in the process of development and testing, the difficulty of access in the business, and whether it can cope with business changes, etc., from a more global perspective. In addition, if it is a general component library, the promotion of the component library is a top priority. Only with more business access can the further iteration of the component library be promoted and a virtuous cycle be formed.
About us
Bytedance international e-commerce recruitment of front-end engineer (internship, campus recruitment, social recruitment not limited), broad business prospects, great technical challenges, send an email to “[email protected]”, subject remarks [international e-commerce + internship/campus recruitment/social recruitment]
Welcome to “Byte front end ByteFE”
Resume mailing address: [email protected]