六、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.jsonbsconfig.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道具中使用。我们将绑定到名为stylebs-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.reicontentRule函数接受一个字符串。因此,使用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:

https://redex.github.io/

另一个有用的资源是原因不和谐频道。这是询问各种 JS-in-JS 解决方案及其权衡的好地方。

You can find the Reason Discord channel at:

https://discord.gg/reasonml

摘要

JS-in-JS 仍然是一个相当新的东西,在不久的将来,理智社区将会对它进行大量的实验。在本章中,我们了解了 JS-in-JS 的一些好处和挑战。你站在哪里?

第 7 章推理中的 JSON中,我们将学习如何在推理中处理 JSON,并了解 GraphQL 如何帮助减少样板代码,同时实现一些非常有说服力的保证。