Emotion是一个专门为使用 JavaScript 编写 CSS 样式而设计的库
使用Emotion
有两种主要的方法,一种是框架无关的;另一种是与 React 一起使用。
配置css
属性
在Emotion
中,为元素添加了css
属性,默认情况下,不知道如何解析css
属性,因此需要使用配置支持css
属性。
Typescript 配置 emotion
"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
文件并配置
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;
`;
theming
上下文的主题配置,所以的子组件都可以获取到主题配置。
import { ThemeProvider } from "@emotion/react";
const theme = {
colors: {
primary: "hotpink",
},
};
申明 Theme 类型
默认情况下theme
是一个空对象。所以在TypeScript
环境下找不到对应的属性,会报错。
创建emotion.d.ts
文件
import "@emotion/react";
declare module "@emotion/react" {
export interface Theme {
color: {
primary: string;
positive: string;
negative: string;
};
}
}
修改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
);
结论
开发体验还可以,不需要插件也可能进行代码提示,配置稍微麻烦一点。
感觉上
Emotion
包含了styled-components
。styled-components
目前也包含了Emotion
所有的功能。React 本身也支持 inline style。
Emotion
也支持 inline style,它扩展了 jsx 的语法增加了一个css prop
,可以支持复杂的样式。但这也是缺点,更改了 React。