六、JS 中的 CSS(Reason 中)
关于 React 的一个很棒的事情是,它让我们可以在一个文件中配置组件的标记、行为和样式。随着时间的推移,这种搭配会对开发人员体验、版本控制和代码质量产生级联效应(无意双关)。在本章中,我们将简要探讨什么是 JS-in-JS,以及我们如何在 Reason 中处理 JS-in-JS。当然,将一个组件分解到不同的文件和/或使用更传统的 CSS 解决方案是完全有效的,如果这是您喜欢的话。
在本章中,我们将讨论以下主题:
- 什么是 JS 中的 CSS?
- 使用
styled-components
- 使用
bs-css
什么是 JS 中的 CSS?
定义 JS 中的 CSS 目前是 JavaScript 社区中一个两极分化的话题。JS 中的 CSS 诞生于组件时代。现代网络很大程度上是用组件模型构建的。几乎所有的 JavaScript 框架都接受了它。随着它的采用越来越多,越来越多的团队开始在同一个项目的不同组件上工作。想象一下,你在一个分布式团队中开发一个大型应用,每个团队都在并行开发一个组件。如果没有团队标准化 CSS 约定,您将会遇到 CSS 范围问题。如果没有某种类型的标准化 CSS 样式指南,多个团队很容易对类名进行样式化,从而影响其他非预期的组件。随着时间的推移,出现了许多解决方案来大规模解决 CSS 的这个和其他相关问题。
简史
一些流行的 CSS 惯例包括边界元法、SMACSS 和 OOCSS。这些解决方案中的每一个都要求开发人员学习约定,并记住正确地应用它;否则,仍然会出现令人沮丧的范围界定问题。
CSS 模块成为了一个更安全的选择,开发人员可以将 CSS 导入到 JavaScript 模块中,构建步骤会自动将该 CSS 本地定位到该 JavaScript 模块。CSS 本身仍然是写在一个普通的 CSS(或 SASS)文件中。
JS-in-JS 更进一步,允许您直接在 JavaScript 模块中编写您的 CSS,自动地将该 CSS 本地作用于组件。这对许多开发者来说是正确的;其他人从一开始就不喜欢。一些 JS 中的 CSS 解决方案,比如styled-components
,允许开发人员将 CSS 和组件直接耦合在一起。您可以使用<Header />
代替<header className="..." />
,其中Header
组件是使用styled-components
及其 CSS 定义的,如以下代码所示:
import React from 'react';
import styled from 'styled-components';
const Header = styled.header`
font-size: 1.5em;
text-align: center;
color: dodgerblue;
`;
曾经有一段时间styled-components
有性能问题,因为在库可以在 DOM 中动态创建样式表之前,JavaScript 包必须下载、编译和执行。由于服务器端的渲染支持,这些问题现在已经得到了很大程度的解决。那么,我们能在《理性》中做到这一点吗?让我们看看!
使用样式化组件
styled-components
最受欢迎的特性之一是基于组件道具动态创建 CSS 的能力。使用此功能的一个原因是创建组件的替代版本。然后,这些替代版本将被封装在样式化组件本身中。下面是一个<Title />
的例子,文本可以居中或左对齐,也可以加下划线。
const Title = styled.h1`
text-align: ${props => props.center ? "center" : "left"};
text-decoration: ${props => props.underline ? "underline" : "none"};
color: white;
background-color: coral;
`;
render(
<div>
<Title>I'm Left Aligned</Title>
<Title center>I'm Centered!</Title>
<Title center underline>I'm Centered & Underlined!</Title>
</div>
);
理性语境下的挑战在于通过style-components
API 创建一个可以动态处理道具的组件。考虑以下styled.h1
函数和我们的<Title />
组件的绑定。
/* StyledComponents.re */
[@bs.module "styled-components"] [@bs.scope "default"] [@bs.variadic]
external h1: (array(string), array('a)) => ReasonReact.reactClass = "h1";
module Title = {
let title =
h1(
[|
"text-align: ",
"; text-decoration: ",
"; color: white; background-color: coral;",
|],
[|
props => props##center ? "center" : "left",
props => props##underline ? "underline" : "none",
|],
);
[@bs.deriving abstract]
type jsProps = {
center: bool,
underline: bool,
};
let make = (~center=false, ~underline=false, children) =>
ReasonReact.wrapJsForReason(
~reactClass=title,
~props=jsProps(~center, ~underline),
children,
);
};
h1
函数接受字符串数组作为第一个参数,接受表达式数组作为第二个参数。这是因为这是 ES6 标记的模板文字的 ES5 表示。在h1
函数的情况下,表达式数组是传递给反应组件的道具的函数。
我们使用[@bs.variadic]
装饰器来允许任意数量的参数。在原因端,我们使用一个数组,在 JavaScript 端,该数组被扩展为任意数量的参数。
使用[@bs.variadic]
让我们快速切入主题,进一步探索[@bs.variadic]
。让我们假设您想要绑定到Math.max()
,它可以接受一个或多个参数:
/* JavaScript */
Math.max(1, 2);
Math.max(1, 2, 3, 4);
这是[@bs.variadic]
的完美案例。我们在原因端使用一个数组来保存参数,数组将被扩展以匹配 JavaScript 中的上述语法。
/* Reason */
[@bs.scope "Math"][@bs.val][@bs.variadic] external max: array('a) => unit = "";
max([|1, 2|]);
max([|1, 2, 3, 4|]);
好了,我们回到styled-components
的例子。我们可以如下使用<Title />
组件:
/* Home.re */
let component = ReasonReact.statelessComponent("Home");
let make = _children => {
...component,
render: _self =>
<StyledComponents.Title center=true underline=true>
{ReasonReact.string("Page1")}
</StyledComponents.Title>,
};
前面的代码是一个样式化的 ReasonReact 组件,它用一些 CSS 呈现一个h1
。CSS 先前已在StyledComponents.Title
模块中定义。<Title />
组件有两个道具——居中和下划线——默认为false
。
当然,这不是编写样式组件的好方法,但是它在功能上类似于 JavaScript 版本。另一个选择是回到原始的 JavaScript 中,利用熟悉的标记模板文本语法。让我们在Title.re
中举例说明这个例子。
/* Title.re */
%bs.raw
{|const styled = require("styled-components").default|};
let title = [%bs.raw
{|
styled.h1`
text-align: ${props => props.center ? "center" : "left"};
text-decoration: ${props => props.underline ? "underline" : "none"};
color: white;
background-color: coral;
`
|}
];
[@bs.deriving abstract]
type jsProps = {
center: bool,
underline: bool,
};
let make = (~center=false, ~underline=false, children) =>
ReasonReact.wrapJsForReason(
~reactClass=title,
~props=jsProps(~center, ~underline),
children,
);
除了现在<Title />
组件不再是StyledComponents
的子模块之外,用法是相似的。
/* Home.re */
let component = ReasonReact.statelessComponent("Home");
let make = _children => {
...component,
render: _self =>
<Title center=true underline=true> {ReasonReact.string("Page1")} </Title>,
};
个人喜欢使用[%bs.raw]
版本时的开发者体验。我想给亚当·科尔(T1)一个热烈的掌声,因为他提出了两个版本的styled-components
绑定。我也很兴奋看到社区提出了什么。
现在让我们来探索社区中最流行的 CSS-in-JS 解决方案:bs-css
。
使用 bs-css
虽然没有来自理性团队的关于 JS-in-CSS 解决方案的官方推荐,但许多人目前正在使用一个名为bs-css
的库,它包装了 JS-in-CSS 库(版本 9)的情感。bs-css
库提供了一个类型安全的应用编程接口用于推理。通过这种方法,我们也可以让编译器检查我们的 CSS。通过转换我们在 第 3 章中创建的App.scss
,我们将对这个库有所了解。
接下来,克隆本书的 GitHub 存储库,并使用以下代码从Chapter06/app-start
开始:
git clone https://github.com/PacktPublishing/ReasonML-Quick-Start-Guide.git
cd ReasonML-Quick-Start-Guide
cd Chapter06/app-start
npm install
首先,我们将把它作为package.json
和bsconfig.json
的依赖项,如下所示:
/* bsconfig.json */
...
"bs-dependencies": ["reason-react", "bs-css"],
...
通过 npm 安装bs-css
并配置bsconfig.json
后,我们将可以访问库提供的Css
模块。标准的做法是定义自己的子模块Styles
,在这里我们打开Css
模块,并在那里编写我们所有的推理 CSS。由于我们将转换App.scss
,我们将在App.re
中声明一个Styles
子模块,如下所示:
/* App.re */
...
let component = ReasonReact.reducerComponent("App");
module Styles = {
open Css;
};
...
现在,让我们转换以下 Sass:
.App {
min-height: 100vh;
&:after {
content: "";
transition: opacity 450ms cubic-bezier(0.23, 1, 0.32, 1),
transform 0ms cubic-bezier(0.23, 1, 0.32, 1) 450ms;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.33);
transform: translateX(-100%);
opacity: 0;
z-index: 1;
}
&.overlay {
&:after {
transition: opacity 450ms cubic-bezier(0.23, 1, 0.32, 1);
transform: translateX(0%);
opacity: 1;
}
}
}
在Styles
中,我们声明了一个名为app
的绑定,稍后将在<App />
组件的className
道具中使用。我们将绑定到名为style
的bs-css
函数的结果。style
函数接收一个 CSS 规则列表。让我们使用以下代码探索语法:
module Styles = {
open Css;
let app = style([
minHeight(vh(100.)),
]);
};
刚开始有点怪怪的,但是用的越多,感觉越好。所有 CSS 属性和所有单位都是函数。函数有类型。如果类型不匹配,编译器会抱怨。考虑以下无效的 CSS:
min-height: red;
这只是在 CSS,Sass,甚至styled-components
中默默失败。有了bs-css
,我们至少可以防止很多无效的 CSS。编译器还会通知我们任何未使用的绑定,这可以帮助我们维护 CSS 样式表,并且像往常一样,我们有完整的智能感知,这可以帮助我们边走边学习应用编程接口。
就我个人而言,我是通过 Sass 嵌套 CSS 的忠实粉丝,我很高兴我们能用bs-css
做同样的事情。要嵌套:after
伪选择器,我们使用after
函数。要嵌套.overlay
选择器,我们使用selector
功能。就像在 Sass 中一样,我们使用&
符号来引用父元素,如下面的代码所示:
module Styles = {
open Css;
let app =
style([
minHeight(vh(100.)),
after([
contentRule(""),
transitions([
`transition("opacity 450ms cubic-bezier(0.23, 1, 0.32, 1)"),
`transition("transform 0ms cubic-bezier(0.23, 1, 0.32, 1) 450ms"),
]),
position(fixed),
top(zero),
right(zero),
bottom(zero),
left(zero),
backgroundColor(rgba(0, 0, 0, 0.33)),
transform(translateX(pct(-100.))),
opacity(0.),
zIndex(1),
]),
selector(
"&.overlay",
[
after([
`transition("opacity 450ms cubic-bezier(0.23, 1, 0.32, 1)"),
transform(translateX(zero))),
opacity(1.),
]),
],
)
]);
};
Note how we are using the polymorphic variant `transition
for the transition strings. Transitions are not valid otherwise.
您可以在 GitHub 存储库的Chapter06/app-end/src/App.re
文件中找到其余的转换。现在剩下要做的就是将样式应用到<App />
组件的className
道具,如下面的代码所示:
/* App.re */
...
render: self =>
<div
className={"App " ++ Styles.app ++ (self.state.isOpen ? " overlay" : "")}
...
删除App.scss
后,一切看起来大都一样。太棒了。例外的是nav > ul > li:after
选择器。在前面的章节中,我们使用 content 属性来渲染图像,如下所示:
content: url(./img/icon/chevron.svg);
根据Css.rei
,contentRule
函数接受一个字符串。因此,使用url
功能不会进行类型检查,如以下代码所示:
contentRule(url("./img/icon/chevron.svg")) /* type error */
作为逃生路线,bs-css
提供unsafe
功能(如下代码所示),将绕过这个问题:
unsafe("content", "url('./img/icon/chevron.svg')")
然而,虽然我们的 webpack 配置先前将前面的图像作为依赖项引入,但是当使用bs-css
时,它不再这样做。
权衡
在推理中使用 CSS-in-JS 显然是一种权衡。一方面,我们可以获得类型安全的、局部范围的 CSS,并且我们可以将我们的 CSS 与我们的组件放在一起。另一方面,语法有点冗长,可能会有一些奇怪的边缘情况。选择 Sass 而不是 CSS-in-JS 解决方案是完全正确的,因为这里没有明显的赢家。
其他库
我鼓励你尝试其他的 CSS-in-JS 原因库。每当你在寻找一个理由库时,你的第一站应该是 Redex(ReASON Package Index)。
You can find Redex (Reason Package Index) at:
另一个有用的资源是原因不和谐频道。这是询问各种 JS-in-JS 解决方案及其权衡的好地方。
You can find the Reason Discord channel at:
摘要
JS-in-JS 仍然是一个相当新的东西,在不久的将来,理智社区将会对它进行大量的实验。在本章中,我们了解了 JS-in-JS 的一些好处和挑战。你站在哪里?
在第 7 章、推理中的 JSON中,我们将学习如何在推理中处理 JSON,并了解 GraphQL 如何帮助减少样板代码,同时实现一些非常有说服力的保证。
版权属于:月萌API www.moonapi.com,转载请注明出处