跳到主要内容

Emotion

· 阅读需 15 分钟

Emotion是一个专门为使用 JavaScript 编写 CSS 样式而设计的库

使用Emotion有两种主要的方法,一种是框架无关的;另一种是与 React 一起使用。

官网

GitHub

配置css属性

Emotion中,为元素添加了css属性,默认情况下,不知道如何解析css属性,因此需要使用配置支持css属性。

Typescript 配置 emotion

tsconfig.json
"jsxImportSource": "@emotion/react"

不然 TypeScript 无法解析css属性,会报以下错误

TS2322: Type '{ children: string; css: SerializedStyles; }' is not assignable to type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.   Property 'css' does not exist on type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.

create-react-app(CRA)

npx create-react-app emotion-hello --template typescript
cd emotion-hello
npm i @emotion/react

主要依赖版本

{
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-scripts": "5.0.1",
"typescript": "^4.6.4",
"@emotion/react": "^11.9.0"
}

Babel

CRA中,封装得比较死,使用@craco/craco对其进行扩展.

安装@craco/craco
npm i @craco/craco -D
创建craco.config.js文件并配置
craco.config.js
module.exports = {
babel: {
presets: [
[
"@babel/preset-react",
{ runtime: "automatic", importSource: "@emotion/react" },
],
],
plugins: ["@emotion/babel-plugin"],
},
};

JSX Pragma

在 import 时候添加注释
/** @jsx jsx */
import { css, jsx } from "@emotion/react";

会有以下错误

 pragma and pragmaFrag cannot be set when runtime is automatic.
解决方案一
+ /** @jsxRuntime classic */
/** @jsx jsx */
解决方案二
+ /** @jsxImportSource @emotion/react */
- /** @jsx jsx */

结论

推荐使用babel的方式配置Emotion

css prop

申明类型可以看到函数重载,有两种方式调用css方法。

export function css(
template: TemplateStringsArray,
...args: Array<CSSInterpolation>
): SerializedStyles;

export function css(...args: Array<CSSInterpolation>): SerializedStyles;

模板字符串

使用css方法可以直接传入模板字符串。

这里的style跟 inline-style 或 style file 中写的样式是一模一样的。

这种方式更符合我们日常编写样式的习惯。

import { css } from "@emotion/react";

<div
css={css`
padding: 32px;
background-color: hotpink;
font-size: 24px;
border-radius: 4px;
&:hover {
color: ${color};
}
`}
/>;

Object

import { css } from "@emotion/react";

const color = "white";

<div
css={css({
padding: 20,
backgroundColor: "hotpink",
fontSize: 24,
borderRadius: 4,
"&:hover": {
color,
},
})}
/>;

可以直接给 css 传递对象。不需要调用 css 方法。

import { css } from "@emotion/react";

const color = "white";

<div css={{
padding: 20,
backgroundColor: "hotpink",
fontSize: 24,
borderRadius: 4,
"&:hover": {
color,
},
}}>

方法返回值

export interface SerializedStyles {
name: string;
styles: string;
map?: string;
next?: SerializedStyles;
}
  • name 就是 class 名称(默认会添加 css 前缀)
  • styles 真正的样式
  • map soucemap

style 优先级

import { CSSObject } from "@emotion/react";

interface PProps {
css?: CSSObject;
}

function P(props: PProps) {
return (
<p
css={{
margin: 0,
fontSize: 12,
lineHeight: 1.5,
fontFamily: "sans-serif",
color: "black",
}}
{...props}
/>
);
}
export default function ArticleText() {
return (
<P
css={{
fontSize: 14,
fontFamily: "Georgia, serif",
color: "darkgray",
}}
/>
);
}

ArticleText 结果

相当于合并样式,根据css规范 “Order of Appearance” ,后定义的属性值(绿色)会覆盖先定义的属性值(红色)

.css-kcvhz8-P-ArticleText {
margin: 0;
- font-size: 12px;
line-height: 1.5;
- font-family: sans-serif;
- color: black;
+ font-size: 14px;
+ font-family: Georgia,serif;
+ color: darkgray;
}

结论

props 对象中的 css 属性优先级高于组件内部的 css 属性,在调用组件时可以覆盖组件默认样式

Styled Components

样式化组件。

styled是一种创建 React components 的方式,并把样式添加到该组件上。受到 styled-components and glamorous启发。

安装依赖

npm i @emotion/styled

如何创建样式化组件

模板字符串

创建一个 button 组件

import styled from "@emotion/styled";

const Button = styled.button`
color: red;
width: 200px;
height: 50px;
`;

<Button>Hello styled components</Button>;

Object

创建一个 div 容器组件

import styled from "@emotion/styled";

const Container = styled.div({
color: "red",
width: 1200,
backgroundColor: "blue",
margin: "0 auto",
});

<Container>
<Button>Hello styled components</Button>
</Container>;

覆盖样式话组件内部的默认样式

模板字符串

import styled from "@emotion/styled";

const Button = styled.button`
width: 200px;
height: 50px;
color: ${(props) => props.color || "red"};
`;

<Button>默认颜色</Button>
<Button color="green">更改颜色</Button>

Object

整个参数都传递方法。

import styled from "@emotion/styled";

export const Container = styled.div((props: { backgroundColor?: string }) => ({
color: "red",
width: 1000,
backgroundColor: props.backgroundColor || "blue",
margin: "0 auto",
}));

第一个参数传递对象,第二个对象传递方法。

export const Container = styled.div(
{
color: "red",
width: 1000,
margin: "0 auto",
},
(props: { backgroundColor?: string }) => ({
backgroundColor: props.backgroundColor,
})
);

为任意组件添加样式

styled可以接收任何组件,并且增加一个className属性(实际上就是 css 方法生成的 className)。

const Basic = ({ className }: { className?: string }) => (
<div className={className}>Some Text</div>
);

模板字符串

import styled from "@emotion/styled";

export const Fancy = styled(Basic)`
color: hotpink;
`;

<Fancy />;

Object

import styled from "@emotion/styled";

export const Fancy = styled(Basic)({
color: "hotpink",
});

<Fancy />;

父组件设置子组件样式

模板字符串

import styled from "@emotion/styled";

export const Child = styled.div`
color: red;
`;

export const Parent = styled.div`
${Child} {
color: green;
}
`;

<Parent>
<Child>Green because I am inside a Parent</Child>
</Parent>
<Child>Red because I am not inside a Parent</Child>

只有在 Parent 中的 Child 颜色才会变成绿色。

Object

import styled from "@emotion/styled";

const Child = styled.div({
color: "red",
});

// TS2464 a computed property name must be of type 'string', 'number', 'symbol', or 'any'.
// https://github.com/emotion-js/emotion/issues/1275#event-6533934489
const Parent = styled.div({
[Child as any]: {
color: "green",
},
});

<Parent>
<Child>Green because I am inside a Parent</Child>
</Parent>;
<Child>Red because I am not inside a Parent</Child>;

嵌套组件

使用嵌套选择器&(表示组件本身)

import styled from "@emotion/styled";

export const Nesting = styled.span`
color: lightgreen;
& > a {
color: hotpink;
}
`;

<Nesting>
This is <a>nested</a>
</Nesting>;

有些时候还可以使用&嵌套在另外一个元素里面。这个在有些场景下需要。

const paragraph = css`
color: turquoise;

header & {
color: green;
}
`;
render(
<div>
<header>
<p css={paragraph}>This is green since it's inside a header</p>
</header>
<p css={paragraph}>This is turquoise since it's not inside a header.</p>
</div>
);

as

别名

要使用样式组件中的样式但是更改渲染的元素标签,就可以使用as属性

import styled from "@emotion/styled";

const Button = styled.button`
color: hotpink;
`;

<Button as="a">Emotion as props</Button>;

样式组合

import { css } from "@emotion/react";

const danger = css`
color: red;
`;

const base = css`
background-color: darkgreen;
color: turquoise;
`;

export const Composition = () => (
<div>
<div css={base}>This will be turquoise</div>
<div css={[danger, base]}>
This will be also be turquoise since the base styles overwrite the danger
styles.
</div>
<div css={[base, danger]}>This will be red</div>
</div>
);

<Composition />;

直接把css变量值传递到另外一个css中。

const base = css`
color: hotpink;
`

render(
<div
css={css`
${base};
background-color: #eee;
`}
>
This is hotpink.
</div>
)

优先级

在样式组合中,取决于样式的先后调用顺序,不取决于样式申明顺序。

Media Queries

在 emotion 中使用media queries与 css 中一模一样。

模板字符串

<p
css={css`
font-size: 30px;
@media (min-width: 420px) {
font-size: 50px;
}
`}
>
Some text!
</p>

Object

<p
css={css({
fontSize: 30,
"@media (min-width: 420px)": {
fontSize: 50,
},
})}
>
Some text!
</p>

可重用的媒体查询

根据设定的 breakpoints,很容易创建响应式布局

import { jsx, css } from "@emotion/react";

const breakpoints = [576, 768, 992, 1200];

const mediaQueries = breakpoints.map((bp) => `@media (min-width: ${bp}px)`);

render(
<div>
<div
css={{
color: "green",
[mediaQueries[0]]: {
color: "gray",
},
[mediaQueries[1]]: {
color: "hotpink",
},
}}
>
Some text!
</div>
<p
css={css`
color: green;
${mediaQueries[0]} {
color: gray;
}
${mediaQueries[1]} {
color: hotpink;
}
`}
>
Some other text!
</p>
</div>
);

借助 facepaint 可以简化媒体查询

npm i facepaint
npm i @types/facepaint
const breakpoints = [576, 768, 992, 1200];
const mq = facepaint(breakpoints.map((bp) => `@media (min-width: ${bp}px)`));
<div
css={mq({
color: ["green", "gray", "hotpink"],
})}
>
Some text.
</div>;

Global

全局样式

可以写多个 Global 样式。

styles可以是Object模板字符串

import { css, Global } from "@emotion/react";

<Global
styles={css`
.some-class {
color: hotpink;
}
`}
/>
<Global
styles={{
body: { margin: 0 },
a: { textDecoration: "none", color: "red" },
".some-class": {
fontSize: 50,
textAlign: "center",
},
}}
/>
<a>This is red</a>
<div className="some-class">This classname is .some-class</div>

Keyframes

可以使用Keyframes定义动画

import styled from "@emotion/styled";
import { keyframes } from "@emotion/react";

const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;

export const Rotate = styled.div`
display: inline-block;
animation: ${rotate} 2s linear infinite;
padding: 2rem 1rem;
font-size: 1.2rem;
`;
实时编辑器
结果
Loading...

theming

上下文的主题配置,所以的子组件都可以获取到主题配置。

import { ThemeProvider } from "@emotion/react";

const theme = {
colors: {
primary: "hotpink",
},
};

申明 Theme 类型

默认情况下theme 是一个空对象。所以在TypeScript环境下找不到对应的属性,会报错。

创建emotion.d.ts文件

emotion.d.ts
import "@emotion/react";

declare module "@emotion/react" {
export interface Theme {
color: {
primary: string;
positive: string;
negative: string;
};
}
}

修改tsconfig.json

tsconfig.json
  "include": [
"src",
+ "emotion.d.ts"
]

css prop

从 css props 属性中获取上下文的 theme。

import { ThemeProvider } from "@emotion/react";

const CSSProp = () => {
return (
<div css={(theme) => ({ color: theme.colors.primary })}>
some other text
</div>
);
};

const theme = {
colors: {
primary: "hotpink",
},
};

<ThemeProvider theme={theme}>
<CSSProp />
</ThemeProvider>;

styled

import styled from "@emotion/styled";

const Styled = styled.div`
color: ${(props) => props.theme.colors.primary};
`;

<ThemeProvider theme={theme}>
<Styled>some other text</Styled>
</ThemeProvider>;

useTheme

import { ThemeProvider, useTheme } from "@emotion/react";

export const Hook = () => {
const theme = useTheme();
return <div css={{ color: theme.colors.primary }}>some other text</div>;
};

const theme = {
colors: {
primary: "hotpink",
},
};

<ThemeProvider theme={theme}>
<Hook />
</ThemeProvider>;

多个 theme 合并

import { ThemeProvider, withTheme } from "@emotion/react";

const theme = {
backgroundColor: "green",
color: "red",
};

const adjustedTheme = (ancestorTheme) => ({ ...ancestorTheme, color: "blue" });

<ThemeProvider theme={theme}>
<ThemeProvider theme={adjustedTheme}>
<Text>Boom shaka laka!</Text>
</ThemeProvider>
</ThemeProvider>;

Label

给 className 添加一个后缀名称,提高可读性。

import { css, jsx } from "@emotion/react";

let style = css`
color: hotpink;
label: some-name;
`;

let anotherStyle = css({
color: "lightgreen",
label: "another-name",
});

let ShowClassName = ({ className }) => (
<div className={className}>{className}</div>
);

<div>
<ShowClassName css={style} />
<ShowClassName css={anotherStyle} />
</div>;

测试看起来只是会多一个 label 名称

// 默认情况
css-1gfxf27-style
css-5pgj1t-anotherStyle
// 添加label
css-wxo79j-some-name-style
css-1i0m745-another-name-anotherStyle

ClassNames

创建一个className传递给子组件。在React中是一个render props模式

import { ClassNames } from "@emotion/react";

let SomeComponent = (props: any) => (
<div className={props.wrapperClassName}>
in the wrapper!
<div className={props.className}>{props.children}</div>
</div>
);

<ClassNames>
{({ css }) => (
<SomeComponent
wrapperClassName={css({ color: "green" })}
className={css`
color: hotpink;
`}
>
from children!!
</SomeComponent>
)}
</ClassNames>;

只有通过ClassNames获取到的css才会生成一个 className 名称。

Attaching Props

css附加到一个常规的React组件上,props 中的css将高于组件内部的css

同 style 优先级例子一样

CacheProvider

安装依赖

npm i @emotion/cache stylis
npm i @types/stylis -D
import { CacheProvider, css } from "@emotion/react";
import createCache from "@emotion/cache";
import { prefixer } from "stylis";

const customPlugin = () => {};

const myCache = createCache({
key: "my-prefix-key",
stylisPlugins: [
customPlugin,
// has to be included manually when customizing `stylisPlugins` if you want to have vendor prefixes added automatically
prefixer,
],
});

<CacheProvider value={myCache}>
<div
css={css`
display: flex;
width: 80px;
`}
>
<div
css={css`
flex: 1;
transform: scale(1.1);
color: hotpink;
`}
>
Some text
</div>
</div>
</CacheProvider>;

就会使用my-prefix-key作为 css class前缀,替换掉默认的前缀css

Object Styles

css prop

<div
css={{
color: "darkorchid",
backgroundColor: "lightgray",
}}
>
This is darkorchid.
</div>
const hotpink = css({
color: "hotpink",
});

styled

import styled from "@emotion/styled";

const Button = styled.button(
{
color: "darkorchid",
},
(props) => ({
fontSize: props.fontSize,
})
);

子选择器

<div
css={{
color: "darkorchid",
"& .name": {
color: "orange",
},
}}
>
This is darkorchid.
<div className="name">This is orange</div>
</div>

Media Queries

<div
css={{
color: "darkorchid",
"@media(min-width: 420px)": {
color: "orange",
},
}}
>
This is orange on a big screen and darkorchid on a small screen.
</div>

Numbers

默认情况下,px会自动添加到数字后面。除非他是无单位的 css 属性。

<div
css={{
padding: 8,
zIndex: 200,
}}
>
This has 8px of padding and a z-index of 200.
</div>

数组

最后还是会平铺成一个对象。

<div
css={[
{ color: "darkorchid" },
{ backgroundColor: "hotpink" },
{ padding: 8 },
]}
/>

Fallback

降级机制

定义数组,如果浏览器不支持linear-gradient特性,就会使用red

<div
css={{
background: ["red", "linear-gradient(#e66465, #9198e5)"],
height: 100,
}}
/>

组合

css可以相互传递。

const hotpink = css({
color: "hotpink",
});

const hotpinkHoverOrFocus = css({
"&:hover,&:focus": hotpink,
});

const hotpinkWithBlackBackground = css(
{
backgroundColor: "black",
color: "green",
},
hotpink
);

结论

  1. 开发体验还可以,不需要插件也可能进行代码提示,配置稍微麻烦一点。

  2. 感觉上Emotion包含了styled-componentsstyled-components目前也包含了Emotion所有的功能。

  3. React 本身也支持 inline style。Emotion也支持 inline style,它扩展了 jsx 的语法增加了一个css prop,可以支持复杂的样式。但这也是缺点,更改了 React。

源码