从零开始创建 ReactJS 设计系统
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
设计系统时代已经来临。放眼望去,到处都是公司发布各种新奇的设计系统,名字也花里胡哨,比如Polaris、Lightning或Carbon 。而像Material UI这样的开源系统,由于其高质量和易用性,几乎被所有项目所采用。
但既然您已经来到这里,我就无需赘述设计系统的优势了。您肯定已经了解在所有项目中为 Web 组件使用单一数据源的好处,以及拥有一个专门的生态系统来控制和创建一致的样式指南对开发人员来说是多么重要。
你可能也在问自己和我一样的问题:究竟需要哪些要素才能创建一个设计系统?我把我遇到的大多数系统中绝对必不可少的特征都记了下来,然后就开始了我的工作。
设计系统的要求
- 成分
- 用于查看组件的开发环境
- 文档(包含属性类型和示例)
- 单元测试(理想情况下,还应进行视觉回归测试)
- 自动代码检查和格式化
归根结底,其实很简单。
我们需要共享组件,一个构建组件的地方,以及一个记录组件文档的地方。还需要代码检查和测试工具,以确保代码无错且运行正常。
这就是设计系统可以如此简洁的最佳之处。如果最终用户只是将我们的组件导入到他们的应用程序中(通过 NPM 或其他方式),我们就无需构建或转译代码。最终用户会以他们自己的方式处理这些。只有当您想要分发组件文件(以便用户可以
<script>从 CDN 将其作为标签导入)时,才需要构建代码。
堆栈
为了本教程的目的,我将使用以下堆栈:
- 组件系统: ReactJS
- CSS in JS: react-jss
- 用于开发的代码转译:Babel
- 开发环境: StorybookJS
- 成分测试: jest + [酶]
- 文档: react-styleguideist(https://github.com/airbnb/enzyme)
- 代码检查和格式化: ESLint + Prettier
让我们逐一解决这些需求,一步一步地构建这个设计系统。
成分
我们将使用 ReactJS 构建组件,并且在本例中,会使用 CSS in JS 库来设置样式。当然,您也可以在自己的系统中使用 CSS、SASS、LESS 或任何您喜欢的库。我选择 CSS in JS 是因为它在设计系统中具有诸多优势。
在 JS 中使用 CSS可以带来诸多优势,例如减少无效 CSS、按需优化样式(无需加载整个 CSS 样式表,避免不必要的加载),以及通过在组件级别分离 CSS 实现更高的模块化。样式逻辑不再局限于文件,因为所有类名都是唯一生成的.bem--btn-1389429,所以无需担心命名空间冲突或样式泄漏。
这样一来,我们的组件就可以尽可能轻量级,并且可以轻松地在其他应用程序中进出,而无需使用会与其他系统冲突的全局样式。
如果你不知道什么是 JavaScript 中的 CSS,这里有你需要知道的一切。以及为什么你可能不想使用它。
我将简要介绍一下我是如何根据设计系统选择 CSS in JS 库的。如果您对此不感兴趣,可以直接跳到“开始您的项目”部分。
JS中的CSS之战
市面上有很多 CSS in JS 工具,从Styled Components到Aphrodite,再到Emotion和 JSS 等等,不胜枚举。那么,该如何从中做出选择呢?
我主要考虑以下几个因素:易用性、软件包大小和性能(加载和渲染时间)。
如果你喜欢编写更像传统 CSS 的代码,那么Styled Components或Emotion可能是你的理想选择。如果你想要开箱即用的 React Native 支持,Aphrodite 则更适合你。至于性能方面……嗯……各项数据似乎 都 参差不齐 。
我的 CSS in JS 选择
最终,我选择了 JSS(特别是用于 React 集成的 react-jss)。它性能出色,打包体积小,而且像 Material UI 这样的大型系统已经证明了它的价值。
我最初选择 Emotion 是因为我喜欢编写真正的 CSS 而不是 JSON。但使用 Emotion 进行主题化的效果不如 JSS。理论上,不使用 Emotion 的“样式化”组件时,Emotion 的性能更好,但当你想要在 CSS 中使用主题变量时,它又强制你使用这些组件。性能下降,抽象化程度更高,依赖性也更强——这不是我想要的系统。
JSS 显然是最终赢家,是时候构建我们的系统了。
开始你的项目
- 为你的项目创建一个新文件夹:
mkdir your-design-system - 在项目文件夹内,初始化一个 NPM 包:
npm init - 创建 Git 仓库:
git init - 安装开发依赖项:
npm i --save-dev react react-dom babel-cli babel-core babel-preset-env babel-preset-react @storybook/react @storybook/addon-options
- 安装依赖项:
npm install react-jss - 进入你的配置文件
package.json并添加对等依赖项:
{
"peerDependencies": {
"react": "^16.0.0",
"react-dom": "^16.0.0"
}
.babelrc在项目根目录下创建一个文件,并添加以下预设配置:
{
"presets": ["env", "react"]
}
- 在项目根目录下创建一个名为 `<project_root>` 的新文件夹
.storybook,并在该文件夹中创建一个config.js包含以下配置的文件:
import { configure } from '@storybook/react';
import { setOptions } from "@storybook/addon-options";
// Option defaults:
setOptions({
/**
* Name to display in the top left corner
* @type {String}
*/
name: 'JSS Design System',
/**
* URL for name in top left corner to link to
* @type {String}
*/
url: 'https://github.com/whoisryosuke',
/**
* Show story component as full screen
* @type {Boolean}
*/
goFullScreen: false,
/**
* Display left panel that shows a list of stories
* @type {Boolean}
*/
showLeftPanel: true,
/**
* Display horizontal panel that displays addon configurations
* @type {Boolean}
*/
showDownPanel: false,
/**
* Display floating search box to search through stories
* @type {Boolean}
*/
showSearchBox: false,
/**
* Show horizontal addons panel as a vertical panel on the right
* @type {Boolean}
*/
downPanelInRight: false,
/**
* Sorts stories
* @type {Boolean}
*/
sortStoriesByKind: false,
/**
* Regex for finding the hierarchy separator
* @example:
* null - turn off hierarchy
* /\// - split by `/`
* /\./ - split by `.`
* /\/|\./ - split by `/` or `.`
* @type {Regex}
*/
hierarchySeparator: null,
/**
* Sidebar tree animations
* @type {Boolean}
*/
sidebarAnimations: true,
/**
* ID to select an addon panel
* @type {String}
*/
selectedAddonPanel: undefined // The order of addons in the "Addons Panel" is the same as you import them in 'addons.js'. The first panel will be opened by default as you run Storybook
})
// This will search the /src/components/ folder (and sub-folders) for any files that match <filename>.story.js
// (e.g /src/components/Button/Button.story.js)
const req = require.context('../src/components/', true, /(\.story\.js$)|(\.story\.jsx$)/);
function loadStories() {
req.keys().forEach((filename) => req(filename));
}
configure(loadStories, module)
现在你已经有了一个可以开始开发组件的基础项目!让我们来分析一下刚才发生了什么:
src/components/我们创建了一个新项目(NPM、Git 等),安装了所有依赖项,并为 Babel 和 Storybook 设置了默认配置。Storybook 配置明确指示 Storybook 从文件夹中获取所有带有特定后缀的故事.story.js。
请查看StorybookJS 的慢速入门指南,了解更多设置方面的信息。
创建我们的第一个组件
我们将把组件放在/src/components/文件夹中。每个组件都将存储在自己的文件夹中,最好采用 Pascal 命名法(例如:组件名称示例)。文件夹内将包含所有组件、故事、测试以及一个index.js用于为所有组件提供默认导出的文件。
它应该看起来像这样:
components
└─┬ Button
├── Button.js
├── Button.story.js
├── Button.test.js
├── ButtonAlternate.js
├── ButtonAlternate.story.js
└── ButtonAlternate.test.js
让我们首先创建一个新组件/src/components/Button/Button.js:
import React from "react";
// The HOC we wrap our components in to apply styles
import injectSheet from "react-jss";
// Your CSS file - in a JS object
const styles = theme => ({
// All top level object keys are different class names
myButton: {
// Global style applied from theming
color: theme.text.color,
margin: {
// jss-expand gives more readable syntax
top: 5, // jss-default-unit makes this 5px
right: 0,
bottom: 0,
left: "1rem"
},
// And we get SASS/LESS like qualities with the nested &
"& span": {
// jss-nested applies this to a child span
fontWeight: "bold" // jss-camel-case turns this into 'font-weight'
}
},
myLabel: {
fontStyle: "italic"
}
});
// Define the component using these styles and pass it the 'classes' prop.
// Use this to assign scoped class names.
const Button = ({ classes, children }) => (
<button className={classes.myButton}>
<span className={classes.myLabel}>{children}</span>
</button>
);
// Export component with HOC to apply styles from above
export default injectSheet(styles)(Button)
让我们来分解这个组件,了解 JSS 的工作原理。
首先我们看到的是一个用于 CSS 样式的变量styles。在这个例子中,该styles变量是一个函数,它接受一个theme对象作为参数,并返回一个 CSS 类对象。该theme对象包含我们放在theme.js文件中的全局值,从而允许我们设置动态值,例如theme.text.color。
如果不需要访问任何主题变量,则该
styles变量可以只是一个对象(而不是一个函数)。
变量下方styles是实际的按钮本身,它只是一个功能性的 React 组件。injectSheet高阶组件 (HOC) 处理样式变量,并classes为按钮组件提供一个属性。我们从中获取类名,并使用它们进行应用className={classes.buttonClassName}。
所以基本上:
- CSS 是用 JavaScript 对象编写的。
- 我们将组件封装在一个“高阶组件”(HOC)中(见下文)。
- HOC 将 CSS对象编译成实际的 CSS并将其注入到应用程序中(作为
<style></style><div> 元素<head>)。 - HOC 还为我们的组件提供了一个
classesprop,其中包含我们之前在 CSS 对象中编写的任何类名。 - 然后我们使用对象将类名应用于我们的组件
classes(有点像CSS 模块)。
既然我们已经有了一个组件,让我们来实际弄清楚如何看待它。
搭建开发环境
开发 React 组件的难点在于搭建一个能够支持整个流程的开发环境。通常情况下,你会使用类似Create React App这样的样板项目,或者创建自定义的 Webpack 配置来将 JS 编译成页面。与其让项目因为 Create React App 的各种依赖项而变得臃肿,或者仅仅为了查看组件就费力地配置 Webpack,我们使用 StorybookJS。
StorybookJS是一个用于组件开发的实时环境。在本地启动 StorybookJS 会在浏览器中打开一个组件库,并提供组件的实时预览(保存更改后支持热重载)。通过创建“故事”,我们可以浏览组件,甚至创建不同的状态(例如按钮的激活或禁用状态)。
具有讽刺意味的是,StorybookJS 使用 Create React App 来运行你的应用程序。它只是按需下载而已。
创作故事
我们的 Storybook 配置(/.storybook/config.js)会遍历我们的src/components/文件夹,并查找任何带有后缀的文件.story.js。
我们可以通过创建一个文件来创建我们的第一个故事src/components/Button/Button.story.js:
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { ThemeProvider } from 'react-jss'
import theme from "../../theme/theme";
import Button from "./Button";
storiesOf('Button', module)
.add('with text', () => (
<ThemeProvider theme={theme}>
<Button onClick={action('clicked')}>Hello Button</Button>
</ThemeProvider>
))
.add('with some emoji', () => (
<ThemeProvider theme={theme}>
<Button onClick={action('clicked')}><span role="img" aria-label="so cool">😀 😎 👍 💯</span></Button>
</ThemeProvider>
));
Storybook 使用一个名为 `createStories` 的函数storiesOf来创建组件的故事。我们将.add(description, component)组件的各种变体方法链接到该函数中。
这里唯一特殊的地方在于,我们将组件包裹在一个<ThemeProvider>组件中。这得益于react-jssReact 的 Context Provider 系统,它利用 Context Provider 将theme对象传递给我们的组件<Button>。如果我们不进行包裹,就无法theme在组件中使用该变量。
运行你的故事书
启动 StorybookJS 非常简单。这将在http://localhost:9001/npm run storybook启动一个开发服务器。在这里,您可以找到 Storybook 控制面板以及您在其中创建的所有故事。src/components/
如果在 Storybook 开发服务器运行时对组件进行任何更改并保存,它将重新构建并重新加载所有更改。
就这么简单!
你会惊讶地发现,开发 React 组件并将其部署为 NPM 包是多么容易。
说到底,你只需要编写 React 代码并提交到 Git 仓库即可。无需设置像 Webpack、Parcel 甚至 Babel 这样复杂的构建流程。你的组件会被导入到其他人的项目中,由他们负责转译。实际上,项目越简单越好。
您可以在这里查看我最终的JSS 设计系统样板代码,也可以查看标记为“starter”的分支。或者,您可以查看Emotion JS 设计系统样板代码进行比较。
在本教程的下一部分,我将介绍如何添加测试、代码检查和自动化组件文档!
如果您想将您的设计系统添加到 NPM,请查看他们的指南(链接在此)。
摇滚起来🤘
Ryo
参考
- react-jss
- React 故事书
- CSS in JS 性能 1
- CSS in JS 性能优化 2
- CSS in JS 性能 3
- CSS in JS 性能 4
- CSS in JS 性能 5
- CSS in JS 性能优化 6 - 为什么 Material UI 选择 JSS 而不是 Styled Components
- circuit-ui - 基于情感的设计系统
- 视频:Siddharth Kshetrapal - 我们需要谈谈我们的前端工作流程 - ReactFest
- 视频:Jon Gold - react-sketchapp:设计是数据的函数
- 风格指南