组织CSS文件

这一章我们将使用BEM方法论,去设计项目的CSS样式,这里我们将不会使用Bootstrap或者Tailwind,因为作者觉得不好看,并打算展示一些技巧

如果你喜欢CSS,你可以学习到一些技巧,如果你不喜欢,也可直接复制代码,进入下一章的Turbo学习

我们的CSS风格

CSS是一个比较难掌握的话题,像其他的编程一样,它需要一些风格和约定才能更好的使用,而学习写CSS最好的办法就是写一个小项目去设计样式

BEM方法论

对于命名约定,我们使用BEM方法论,它是简单易懂的,总结为以下三点:

  1. 每个 component(or block) 应有有独立的名字,比如系统中的card.card的CSS类应该定义在card.scss文件中,这就BEM中的B。
  2. 每个 block 可能有多个 elements,比如 card 举例,每个card都有 title 和 body,在BEM中我们应该写为 .card_title.card_body,这样就可以避免命名冲突,如果另有一个block为 .box也有 title 和 body,那么这个就是.box_title.box_body,这就是BEM中的E
  3. 每个 block 可能有多个 modifiers,再用 card 的例子,每个card可能有不同的颜色,那这个命名就该是:.card--primary and .card--secondary这就是BEM中的M

这样就可以避免命名冲突了

组织CSS文件

现在我们有了健壮的命名约定,是时候讨论文件组织了,这个项目很简单,我们也会有一个简单的架构

我们的app/assets/stylesheets/文件夹将会包含4个 elements

  • application.sass.scss导入所有的样式
  • A mixins/ folder where we'll add Sass mixins
  • A config/ folder where we'll add our variables and global styles
  • A components/ folder where we'll add our components
  • A layouts/ folder where we'll add our layouts

components 和 layouts 有什么区别?

components是页面中独立的部分,它不应该关心它会被放到哪里,而是只关心样式,一个好的例子是:按钮,按钮不知道它会被放哪里

layout相反的,不添加样式,只关注与定位,好的例子是:container转载页面内容,如果你好奇样式相关内容,可以看这里:

一旦我们建立了我们的设计系统,我们将能够创建新的页面在没有时间编写额外的CSS组成组件和布局。


注意 components 和 margins

理论上,components不应该有外边距,当我们设计一个独立的components时,我们也不知道他被放到页面哪里。比如按钮,不管是垂直还是水平放置,都没道理增加外边距,出现几个空格的距离。

我们不能提前预知独立的components在哪里被使用,这是layouts的职责,随着 design system 的壮大,如果components更容易与其他组件合作将会更容易的复用。

虽然那么说,但是在本教程中,我们将打破这些规则,我会直接在components上加入margin。因为这个项目不会再扩展,我不希望事情变的太复杂,不过如果你做真实项目时,应该记住上面的规则


足够的理论了,现在我们将要写SASS代码了,让我们开始吧

使用我们自己的CSS在quote编辑器上

The mixins folder

这个文件夹是最小的,只有一个_media.scss文件,我们将定义一个breakpointsmedia queries,在我们的项目只有一个叫做tabletAndUpbreakpoints

// app/assets/stylesheets/mixins/_media.scss

@mixin media($query) {
  @if $query == tabletAndUp {
    @media (min-width: 50rem) { @content; }
  }
}

当写css时,我们先按照移动端写css,例如:

.my-component {
  // The CSS for mobile
}

当需要为大尺寸overrides时,使用我们的 media query 可以让事情变得更简单

.my-component {
  // The CSS for mobile

  @include media(tabletAndUp) {
    // The CSS for screens bigger than tablets
  }
}

这就是 modile first approach,我们先定义小尺寸,再添加override的大尺寸,这是一个使用 mixin 的好的实践,如果后面想增加更多的breakpoints,例如:laptopAndUp or desktopAndUp 就变得很容易了。

tabletAndUp50rem更容易阅读

并且这可以帮我们避免写重复的代码,比如:

.component-1 {
  // The CSS for mobile

  @media (min-width: 50rem) {
    // The CSS for screens bigger than 50rem
  }
}

.component-2 {
  // The CSS for mobile

  @media (min-width: 50rem) {
    // The CSS for screens bigger than 50rem
  }
}

想象一下我们要把50rem改为55rem在一些points中,那会是一场维护噩梦。

最后在一个地方有一个精心策划的断点列表可以帮助我们选择最相关的断点,从而限制我们的选择!

这就是我们第一个css文件要说的内容,但很有用,我们的quote编辑器,必须是响应迅速的,并且这是一个简单强大的breakpoints实现

The configuration folder

其中一个重要的文件夹是 variables files,去构建一个强壮的设计系统,这里就是我们将选择好看的颜色,可读的字体和保证一直的间距性。

首先:让我们开始设计文本,比如字体,颜色,大小和行高

// app/assets/stylesheets/config/_variables.scss

:root {
  // Simple fonts
  --font-family-sans: 'Lato', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;

  // Classical line heights
  --line-height-headers: 1.1;
  --line-height-body:    1.5;

  // Classical and robust font sizes system
  --font-size-xs: 0.75rem;   // 12px
  --font-size-s: 0.875rem;   // 14px
  --font-size-m: 1rem;       // 16px
  --font-size-l: 1.125rem;   // 18px
  --font-size-xl: 1.25rem;   // 20px
  --font-size-xxl: 1.5rem;   // 24px
  --font-size-xxxl: 2rem;    // 32px
  --font-size-xxxxl: 2.5rem; // 40px

  // Three different text colors
  --color-text-header: hsl(0, 1%, 16%);
  --color-text-body:   hsl(0, 5%, 25%);
  --color-text-muted:  hsl(0, 1%, 44%);
}

第一个Variables设置帮助我们确认我们的文本设计在整个系统中是一致的。

保持一致的spacing,padding,margins在系统中也是必要的,让我们开始构建简单的标尺。

// app/assets/stylesheets/config/_variables.scss

:root {
  // All the previous variables

  // Classical and robust spacing system
  --space-xxxs: 0.25rem; // 4px
  --space-xxs: 0.375rem; // 6px
  --space-xs: 0.5rem;    // 8px
  --space-s: 0.75rem;    // 12px
  --space-m: 1rem;       // 16px
  --space-l: 1.5rem;     // 24px
  --space-xl: 2rem;      // 32px
  --space-xxl: 2.5rem;   // 40px
  --space-xxxl: 3rem;    // 48px
  --space-xxxxl: 4rem;   // 64px
}

关于颜色:

// app/assets/stylesheets/config/_variables.scss

:root {
  // All the previous variables

  // Application colors
  --color-primary:          hsl(350, 67%, 50%);
  --color-primary-rotate:   hsl(10, 73%, 54%);
  --color-primary-bg:       hsl(0, 85%, 96%);
  --color-secondary:        hsl(101, 45%, 56%);
  --color-secondary-rotate: hsl(120, 45%, 56%);
  --color-tertiary:         hsl(49, 89%, 64%);
  --color-glint:            hsl(210, 100%, 82%);

  // Neutral colors
  --color-white:      hsl(0, 0%, 100%);
  --color-background: hsl(30, 50%, 98%);
  --color-light:      hsl(0, 6%, 93%);
  --color-dark:       var(--color-text-header);
}

The last part of our variables file will contain various user interface styles such as border radiuses and box shadows. Once again, the goal is to ensure consistency in our application.

// app/assets/stylesheets/config/_variables.scss

:root {
  // All the previous variables

  // Border radius
  --border-radius: 0.375rem;

  // Border
  --border: solid 2px var(--color-light);

  // Shadows
  --shadow-large:  2px 4px 10px hsl(0 0% 0% / 0.1);
  --shadow-small:  1px 3px 6px hsl(0 0% 0% / 0.1);
}

这就是所有了,我们的系统使用统一的样式。现在 apply those variables to global styles i.e。

// app/assets/stylesheets/config/_reset.scss

*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
  padding: 0;
}

html {
  overflow-y: scroll;
  height: 100%;
}

body {
  display: flex;
  flex-direction: column;
  min-height: 100%;

  background-color: var(--color-background);
  color: var(--color-text-body);
  line-height: var(--line-height-body);
  font-family: var(--font-family-sans);
}

img,
picture,
svg {
  display: block;
  max-width: 100%;
}

input,
button,
textarea,
select {
  font: inherit;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  color: var(--color-text-header);
  line-height: var(--line-height-headers);
}

h1 {
  font-size: var(--font-size-xxxl);
}

h2 {
  font-size: var(--font-size-xxl);
}

h3 {
  font-size: var(--font-size-xl);
}

h4 {
  font-size: var(--font-size-l);
}

a {
  color: var(--color-primary);
  text-decoration: none;
  transition: color 200ms;

  &:hover,
  &:focus,
  &:active {
    color: var(--color-primary-rotate);
  }
}

现在我们可以设计独立的components

The components folder

这个文件夹将会包含我们独立的components样式、

Let’s start by, believe it or not, our most complex components: buttons. We will start with the base .btn class and then add four modifiers for the different styles:

// app/assets/stylesheets/components/_btn.scss

.btn {
  display: inline-block;
  padding: var(--space-xxs) var(--space-m);
  border-radius: var(--border-radius);
  background-origin: border-box; // Invisible borders with linear gradients
  background-color: transparent;
  border: solid 2px transparent;
  font-weight: bold;
  text-decoration: none;
  cursor: pointer;
  outline: none;
  transition: filter 400ms, color 200ms;

  &:hover,
  &:focus,
  &:focus-within,
  &:active {
    transition: filter 250ms, color 200ms;
  }

  // Modifiers will go there
}

btn类是一个内联块元素,我们为其添加了默认样式,如padding, border-radius和transition。注意,在Sass中,&号对应于直接嵌套&号的选择器。在我们的例子中&:hover,将被Sass转换为CSS中的:btn:hover。

现在我们为其添加四个样式:

// app/assets/stylesheets/components/_btn.scss

.btn {
  // All the previous code

  &--primary {
    color: var(--color-white);
    background-image: linear-gradient(to right, var(--color-primary), var(--color-primary-rotate));

    &:hover,
    &:focus,
    &:focus-within,
    &:active {
      color: var(--color-white);
      filter: saturate(1.4) brightness(115%);
    }
  }

  &--secondary {
    color: var(--color-white);
    background-image: linear-gradient(to right, var(--color-secondary), var(--color-secondary-rotate));

    &:hover,
    &:focus,
    &:focus-within,
    &:active {
      color: var(--color-white);
      filter: saturate(1.2) brightness(110%);
    }
  }

  &--light {
    color: var(--color-dark);
    background-color: var(--color-light);

    &:hover,
    &:focus,
    &:focus-within,
    &:active {
      color: var(--color-dark);
      filter: brightness(92%);
    }
  }

  &--dark {
    color: var(--color-white);
    border-color: var(--color-dark);
    background-color: var(--color-dark);

    &:hover,
    &:focus,
    &:focus-within,
    &:active {
      color: var(--color-white);
    }
  }
}

还是上面的规则:&--primary 会被转为:.btn--primary通过Sass预处理器。

我们再写关于.quotecomponent

// app/assets/stylesheets/components/_quote.scss

.quote {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-s);

  background-color: var(--color-white);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow-small);
  margin-bottom: var(--space-m);
  padding: var(--space-xs);

  @include media(tabletAndUp) {
    padding: var(--space-xs) var(--space-m);
  }

  &__actions {
    display: flex;
    flex: 0 0 auto;
    align-self: flex-start;
    gap: var(--space-xs);
  }
}

下来是.form 和 .visually-hidden,之前我们使用了simple form,其中定义了:

config.wrappers :default, class: "form__group" do |b|
  b.use :html5
  b.use :placeholder
  b.use :label, class: "visually-hidden"
  b.use :input, class: "form__input", error_class: "form__input--invalid"
end

这里我们开始定义.form

// app/assets/stylesheets/components/_form.scss

.form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);

  &__group {
    flex: 1;
  }

  &__input {
    display: block;
    width: 100%;
    max-width: 100%;
    padding: var(--space-xxs) var(--space-xs);
    border: var(--border);
    border-radius: var(--border-radius);
    outline: none;
    transition: box-shadow 250ms;

    &:focus {
      box-shadow: 0 0 0 2px var(--color-glint);
    }

    &--invalid {
      border-color: var(--color-primary);
    }
  }
}

下来是.visually-hidden component

// app/assets/stylesheets/components/_visually_hidden.scss

// Shamelessly stolen from Bootstrap

.visually-hidden {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border: 0 !important;
}

展示异常信息

// app/assets/stylesheets/components/_error_message.scss

.error-message {
  width: 100%;
  color: var(--color-primary);
  background-color: var(--color-primary-bg);
  padding: var(--space-xs);
  border-radius: var(--border-radius);
}

That’s it; we have all the components we need to complete the CRUD on our Quote model. We just need two layouts now, and we will be done with the CSS!

The layouts folder

// app/assets/stylesheets/layouts/_container.scss

.container {
  width: 100%;
  padding-right: var(--space-xs);
  padding-left: var(--space-xs);
  margin-left: auto;
  margin-right: auto;

  @include media(tabletAndUp) {
    padding-right: var(--space-m);
    padding-left: var(--space-m);
    max-width: 60rem;
  }
}
// app/assets/stylesheets/layouts/_header.scss

.header {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s);
  justify-content: space-between;
  margin-top: var(--space-m);
  margin-bottom: var(--space-l);

  @include media(tabletAndUp) {
    margin-bottom: var(--space-xl);
  }
}

好了这些就是所有内容,现在我们导入到 manifest file 为了 Sass 去处理他们

The manifest file

// app/assets/stylesheets/application.sass.scss

// Mixins
@import "mixins/media";

// Configuration
@import "config/variables";
@import "config/reset";

// Components
@import "components/btn";
@import "components/error_message";
@import "components/form";
@import "components/visually_hidden";
@import "components/quote";

// Layouts
@import "layouts/container";
@import "layouts/header";

赶紧结束吧,我们已经写了太多让人头晕的css代码了。