前端开发技巧Devs让前端飞

编写强健可扩展CSS架构的8条规则

2017-12-08  本文已影响204人  乌龟怕铁锤

原文地址 8 simple rules for a robust, scalable CSS architecture 本文仅仅是对原文的中文翻译, 所有权利归原作者 Jarno Rantanen.
Original address 8 simple rules for a robust, scalable CSS architecture. This article is only the Chinese translation, all the rights still belong to original author Jarno Rantanen.

这些是我这些年web开发中从各种大型的复杂的web项目里学到的,已经有很多次别人问我这些知识,所以我觉得是时候把这些心得写成文档了。

我会尽量保持解释的简洁,会涵盖以下几个必要的点:

  1. 尽可能使用类
  2. 按类存放组件代码
  3. 保持类的命名空间一致性
  4. 文件名和命名空间保持一致性
  5. 防止样式泄漏到组件之外
  6. 防止组件内的样式泄漏
  7. 尊重组件的边界
  8. 解耦外部的style依赖

简介

如果你从事前端工作的话,你早晚会遇到样式相关的工作. 虽然各种前端的技术日新月异,但CSS始终坚挺的是前端样式的唯一选择(早晚..各种本地应用也会使用的)。 总体来说,样式的工作方式有两种大类:

如何在以上的两种方式中做出选择是另一个话题了,但就像任何事物的两面性一样,这两种方式都有自己的优缺点,本文中我会更多的讨论第一种方式,如果你在使用第二种方式那么本文可能没那么有意思了.

主要目标

我们说的强健的,可扩展的CSS架构, 具体的说到底体现在什么地方呢?

详细的规则

1. 优先使用类

这只是在陈述一个显然的事实.

不要是用目标ID (例如 #header), 因为无论你有多确定只有一个实例, 在长远看来, 你都会被证明是错的. 我们曾经在一个大型应用上尝试找出所有数据绑定的问题,我们使用了2个UI,用同样的DOM, 同样的数据模型. 来确认所有的数据改动都能够被正确的在UI上显示,任何你以为唯一的组件(例如标题栏),都不在是唯一了, 这是个简单的例子能够证明假设唯一是不靠谱的。好吧,我有点偏题了,我只是想说,永远不会有情况是指向ID会被指向类更好的,永远!

同时你也不应该直接指向元素(例如 p). 通常情况下你可以直接指向属于某个组件的元素(详见下面), 但是对于组件本身而言,如果你这样做的化,你就早晚会需要去为不需要的组件撤销样式. 回到我们的主要目标上,这几乎违反了所有的目标( 非面向组件,未遵循样式层叠,成为了默认的值). 像字体,行高和颜色( 属于继承属性) 这些body中的属性可以在你需要的时候成为例外,但是如果你严肃的对待组件隔离,这些也完全可以被放弃。(参考下面的 解耦外部的style依赖)

所以除了极少的例外, 你的样式应该始终指向类.

2. 按类存放组件代码

当你开发或者使用一个组件的时候,假如所有关于这个组件的内容 - 它的Javascript, 样式,测试, 文档 - 都放在一起, 那会很有帮助。prominent

ui/
├── layout/
|   ├── Header.js              // 组件代码
|   ├── Header.scss            // 组件样式
|   ├── Header.spec.js         // 组件单元测试
|   └── Header.fixtures.json   // 测试代码的模拟数据 (如需要的话)
├── utils/
|   ├── Button.md              // 组件使用文档
|   ├── Button.js               // ..
|   └── Button.scss

当你使用这些代码的时候,只要打开项目的目录, 所有相关的组件文件都触手可得。 这些生成DOM的样式文件和Javascript有着很自然的联系, 所以有理由推断你不会只使用其中的某一个文件。 同理适用于组件和他的测试, 比如你可以把这个当作UI组件的参考地点原则. 我原来也一直一丝不苟地的维护着不同镜像下我的代码,在这些styles/, tests/, docs/ 目录下, 直到我意识到这么做的唯一理由是我习惯那样.

3. 保持类的命名空间一致性

CSS 为类名和其他标识(例如id, 动画名)准备的命名空间是单独的也是扁平的。 就像过去使用PHP的日子,开发社区已经适应了这个规则。通过使用更长的,结构化的名字来枚举名字空间(例子BEM)

例如, 我们使用的类名 myapp-Header-link,其中的3个部分都有具体的指代:

作为一个特殊的例子,'Header'组件的根元素作为一个简单的组件可以直接被myapp-Header 类标记, 这些已经足够用了.

无论采用何种命名空间的方式, 请在项目中保持一致。 就像上面三个部分作为不同的功能,他们同样有明确的意义。只要看见这些类,你就知道他们属于哪里,命名空间本身就是一种对于项目的向导地图。

从这里开始我讲假定命名空间总是循序 '应用-组件-类' 的规则,我自己觉得这样很好,但你也可以有你自己的命名方式。

4. 文件名和命名空间保持一致性

这仅仅是两个规则的合并(按类存放组件代码和保持类的命名空间一致性), 所有的指定组件的样式都需要以这个组件命名,没有例外。

If you're working in the browser, and you spot a component that's misbehaving, you can right-
如果你在浏览器里看到一个组件没有正确的显示,你可以右键点击它并查看, 例如:

<div class="myapp-Header">...</div>

你知道组件名字后,就可以到编辑器里搜索了,“快速打开文件”, 输入名字"head", 你能看到

quick-open-file.png

这样文件名严格的匹配组件名称非常有用,对于团队不熟悉架构的新人而言,TA可以轻松而自然的找到应该工作的代码。

一个很自然的(但不是立即)的推论: 一个单独的样式文件只能包含单独名字空间的样式. 为什么?比如我们有个登陆的表单,只用在顶部组件中. 在Javascript代码中被定义为小组件在Header.js中,而且不暴露在外。也许你会定义类名为myapp-LoginForm,同时把这个定义放入 Header.jsHeader.scss. 但想象一下,如果一个新的项目成员加入后想登陆表单中的一个布局问题,他打开检查后会发现没有LoginForm.jsLoginForm.scss ,那他只能通过grep来查找相关的代码。所以,login form需要不同的命名空间,那么就把他放到不同的控件中去。一致性在重大的项目中和金子一样重要。

5. 防止样式泄漏到组件之外

好了,我们建立了我们的命名空间规则,现在需要对UI控件沙盒化起来了。假如每个控件只使用他们命名空间开头的样式类,那么就可以保证样式不被泄漏到附近的控件中。 这是非常有效的(见下说明), 但不停的输入名字空间前缀也是一件繁琐的事情~

一个强健的,同时也机器简单的解决方案:把整个样式包裹到一个前缀区块内。 如下,我们只需要重复应用和组件名字一次:

.myapp-Header {
  background: black;
  color: white;

  &-link {
    color: blue;
  }

  &-signup {
    border: 1px solid gray;
  }
}

以上的例子来自SASS, 但是这个 &符号,也许让你惊讶,在所有其他的css预处理语言中都有一样的功能 (SASS, PostCSS, LESS and Stylus)。完整的说,下面是SASS编译后的CSS:

.myapp-Header {
  background: black;
  color: white;
}

.myapp-Header-link {
  color: blue;
}

.myapp-Header-signup {
  border: 1px solid gray;
}

所有的模式都能和这个契合,例如不同控件状态下的样式: (考虑 Modifier in BEM terms)

.myapp-Header {

  &-signup {
    display: block;
  }

  &-isScrolledDown &-signup {
    display: none;
  }
}

会编译成:

.myapp-Header-signup {
  display: block;
}

.myapp-Header-isScrolledDown .myapp-Header-signup {
  display: none;
}

甚至媒体的查询都能方便的工作,只要你的预编译支持冒泡(SASS, LESS, PostCSS 和 Stylus 都有可以):

.myapp-Header {

  &-signup {
    display: block;

    @media (max-width: 500px) {
      display: none;
    }
  }
}

编译为:

.myapp-Header-signup {
  display: block;
}

@media (max-width: 500px) {
  .myapp-Header-signup {
    display: none;
  }
}

以上的模式使得使用唯一的长类名变得简单,不用一边又一边的重复。便利是必须的,不然的话人们便会偷懒走捷径。

快速的过下JS这边的情况

这篇文档是关于样式约定的,但样式并不是凭空存在的,JS这边的一样需要创造同样的类命名空间,一样的需要便利性.

无耻的插播一个广告 (译者:对作者就是这么说的), 我创建了一个简单的,0依赖的JS库来说明,叫做 css-ns. 当在其他框架中使用的时候,例如. React, 这允许你在指定文件中强制生成一个名字空间.

// 创造一个名字空间绑定的React拷贝
var { React } = require('./config/css-ns')('Header');

// 创造元素:
<div className="signup">
  <div className="intro">...</div>
  <div className="link">...</div>
</div>

会这样绘制DOM:

<div class="myapp-Header-signup">
  <div class="myapp-Header-intro">...</div>
  <div class="myapp-Header-link">...</div>
</div>

这非常方便,以上实现了JS部分的"默认本地";

我又一次跑题了,让我们回到CSS吧.

6. 防止组件内的样式泄漏

还记得我之前说的每个类名都需要增加组件前缀是一种很“方便”的沙盒化样式的方法? 还记得我说过"说明"?

考虑以下的样式:

.myapp-Header {
  a {
    color: blue;
  }
}

和如下的组件层次:

+-------------------------+
| Header                  |
|                         |
| [home] [blog] [kittens] | <-- 这些是 <a> 元素
+-------------------------+

没问题吧? 只有在 Header中的 <a> 元素 inside Header蓝色 , 因为我们定义的规则是:

.myapp-Header a { color: blue; }

加入这个布局变化为:

+-----------------------------------------+
| Header                    +-----------+ |
|                           | LoginForm | |
|                           |           | |
| [home] [blog] [kittens]   | [info]    | | <-- 这些是 <a> 元素
|                           +-----------+ |
+-----------------------------------------+

这是选择.myapp-Header a 同时符合了LoginForm中的<a>元素,我们所谓的样式独立性就被破坏了。 所以说,把所有的样式都绑定在一个名字空间的区域内是一个有效的方式来实现组件邻居之间的样式独立性,但对子组件不一定成立

以下两种方式可以修复这个问题:

  1. 永远不要在样式中指定元素名字, 加入每个在Header中的<a>元素都是 <a class="myapp-Header-link"> , 那我们就不会有这个问题了。可是,有的时候你已经有很多自定义(语义的)元素名字已经创建好了,而你并不想为他们额外增加类,那么你需要:
  2. 通过 [ > 连接符 ] (https://developer.mozilla.org/en-US/docs/Web/CSS/Child_selectors) 指定命名空间内的子元素样式。(译者:原文的outside感觉并不是作者本意)

对第2种方式, 样式可以写作:

.myapp-Header {
  > a {
    color: blue;
  }
}

这样确保了在更深层的组件树不受当前的样式影响,因为生成的选择表达是:.myapp-Header > a. ( 译者:关于selector可以参考译者原创的文章 JQuery Selector 入门)

加入你还不确定,让我提个更加疯狂的方案,而且这也可行:

.myapp-Header {
  > nav > p > a {
    color: blue;
  }
}

这些年来我们一直被教导不要使用嵌套选择 (当然包括使用 '>') 例如这许多年的有用例子建议, 为什么?三个理由:

  1. 串联样式早晚会出事的。你越用嵌套选择,越有可能从某个不知名的角落出来一个元素刚好符合这个选择。 你能读到这里一定了解了我们在之前的建议都在尽力的避免这种可能(严格的命名空间前缀,需要时使用子选择)
  2. 太多的指定破坏了重复使用的可能。样式目标例如 nav p a是没发在其他地方被使用的,除非有相同的结构。 但我们并不想这样,事实上我们应该禁止类似这样的重用,因为他违背了我们的原则:组件样式需要各自独立。
  3. 太多的指定使得重构艰难。这可是有事实依据的,加入你只是用了 .myapp-Header-link a, 你可以随意的将<a> 在你的组件里移动,样式不会有问题。但对于> nav > p > a来说你需要更新选择来匹配新的位置。但我们说过了,组件需要小而独立,这样的工作显得毫无意义。 当然在重构的时候,你需要考虑应用整体的HTML&CSS,也许挺吓人的。但加入你只是操作若干只有几行的沙盒组件, 而且无需关心沙盒外的任何东西,那事情就变的简单多了。

这是一个理解规则的好例子,让你知道什么时候可以违反。在这个架构中,不要使用嵌套选择, 但有些时候确又是正确的事情,祝你好运.

好奇的另一面: 防止泄漏样式进入到组件里

我们已经能够做到组件的样式沙盒化,那这些组件是否能确保独立于整个其他页面了? 让我们回忆下:

例如,我们有这样一个组件样式:

.myapp-Header {
  > a {
    color: blue;
  }
}

但当我们引入了一个有问题的第三方库,它包含了这样的CSS:

a {
  font-family: "Comic Sans";
}

There is no simple way to protect your components from such external abuse, and this is where we often need to just:
压根就没简单的办法来保护你的组件不受外部滥用样式的影响, 这里你只能:

give-up.gif

放弃吧!

不幸中的万幸,你至少可以控制那些依赖你可以使用,找到那些正确又优雅的替代品吧。

不过,我只是说没有简单的方法来保护你的控件,但这并不意味这没有任何方法了。哥们,有办法的 ,只是任何办法都有代价的.

这次走题有点长了,回到CSS.

7. 尊重组件的边界

就像我们样式 .myapp-Header > a, 当我们嵌套一个组件时候,我们可能要对子组件设置一些样式, 考虑这布局:

+---------------------------------+
| Header           +------------+ |
|                  | LoginForm  | |
|                  |            | |
|                  | +--------+ | |
| +--------+       | | Button | | |
| | Button |       | +--------+ | |
| +--------+       +------------+ |
+---------------------------------+

我们立即可以看到样式 .myapp-Header .myapp-Button 不是个好主意,显然我们应该这样写 .myapp-Header > .myapp-Button。 但什么样式是我们可能会希望子组件也继承的呢?

注意到 登陆表单被锁定在顶部栏的右边,直觉上,下面的样式需要:

.myapp-LoginForm {
  float: right;
}

我们还没违反任何规定,但我们也让 登陆表单变的难以重用了, 如果我们下面的主要需要登陆表单, 但又不是右浮动的,那就没戏了。

实用的解决方法是部分的放松我们之前的规则,只写当前名字控件的组件样式。 我们可以这样做:

.myapp-Header {
  > .myapp-LoginForm {
    float: right;
  }
}

这其实很完美,只要我们不破坏子组件的沙盒性。

// COUNTER-EXAMPLE; DON'T DO THIS
.myapp-Header {
  > .myapp-LoginForm {
    color: blue;
    padding: 20px;
  }
}

看起来我们并不希望这样,因为我们失去了对本地样式的保护,全局样式可以影响了。 同时上面的代码中,LoginForm.scss 不再是唯一一个地方你能看到LoginForm组件的样式了,这听起来挺可怕的,那么,我们到底如何区分什么是可以的什么是不可以的呢?

We want to respect the sandbox inside each child component, as we don't want to rely on its implementation details. It's a black box to us. What's outside the child component, conversely, is the sandbox of the parent, where it reigns supreme. The distinction between inside and outside emerges quite naturally from one of the most fundamental concepts in CSS: the box model.
我们需要尊重每个组件的内部沙盒,这并不应该依赖于他们自己的内部实现。每个组件都是一个嘿嘿。对于那些在的组件, 相反的,是他们父辈节点的沙盒, 自我管理。内部和外部的区别在CSS的最重要的基础知识上能够体现出来: the box model.

box-model.png

我的推理并不好,但详细的说是这样的: 就像在国家内意味这在他的边界内,我们认为父节点只能影响它直接下一代的子节点组件边界外的属性. 这意味着和位置与像素相关的属性(例如position, margin, display, width, float, z-index 等), 但那些涉及到边界之内的属性(例如border本身,padding, color, font等)就不行。

综上,以下这种写法是不允许的:

// 反例; 请不要这样写
.myapp-Header {
  > .myapp-LoginForm {
    > a { // 依赖 LoginForm 的详细实现
      color: blue;
    }
  }
}

但也有一些有趣的边界情况,比如:

对于这些边界条件下的情况,你需要清楚的认识到这没什么大不了的,只是一点点CSS串联进你样式。 比起其他让你烦恼的事情来说,你完全可以接受适度的串联。 例如这个例子指定内容, 就像你期望的那样展现: 当组件可见时,.myapp-LoginForm { display: flex }是指定的, 当需要隐藏时,.myapp-Header-loginBoxHidden > .myapp-LoginBox { display: none }成为了指定的样式。

8. 解耦外部的style依赖

为了避免重复造轮子,有时候你需要在组件间共享样式, 有时候你也需要使用其他人创建的样式。 这样的情况下,就需要避免对代码长生比不要的依赖影响.

一个很实在的例子, 我们来用一些来自 Bootstrap的样式, 作为那些整合起来让人头大的框架的一个完美例子. 考虑我们上面所提到的所有内容,说到把所有的样式放到一个全局的名字空间是个很坏的习惯,然而Bootstrap:

先不管上面这些,假定我们要使用Bootstrap作为我们Button组件的基础.

除了可以在HTML里这么写外:

<button class="myapp-Button btn">

考虑扩展 这个类进入你的样式:

<button class="myapp-Button">
.myapp-Button {
  @extend .btn; // from Bootstrap
}

这样做的话,可没人能够知道你这个组件的样式是荒谬的依赖于btn类的. Button 原始的样式实现是完全没必要在外部展示的。作为一个结果,一旦你放弃bootstrap转用别的框架(或者自己写样式), 这样的改变从代码层面是很难体现出来的,但结果就是.. 哈哈 你的Button样子变了!

同样的原则你应该用自己的帮助类, 这里你也许会选择更合理的名字,例如:

.myapp-Button {
  @extend .myapp-utils-button; // 在项目其他地方定义
}

或者 只是占位类 altogether (主流预编译都支持):

.myapp-Button {
  @extend %myapp-utils-button; // 在项目其他地方定义
}

最后,所有主流的css预编译工具都支持 mixins, 这同样十分的有用.

.myapp-Button {
  @include myapp-generateCoolButton($padding: 15px, $withExplosions: true);
}

It should be noted that when dealing with more civilized style frameworks (such as Bourbon or Foundation), they'll in fact be doing just this: declaring a bunch of mixins for you to use where they're needed, and not emitting any styles on their own. Neat.

必须指出现在很多良好的框架(例如 Bourbon 或者 Foundation),它们就是像上面这样做的,声明一些列的mixins给你按需来使用,才不会散发他们自己的样式. 优雅!

结束语

熟悉规则了,才能更好的知道什么时候打破规则

最后,就像上面提到的,你熟悉了解了你所依赖的这些规则(无论是陌生人告诉你的还是你从网上学的), 你才能合理的做一些例外的事情。 例如,如果你需要直接增加一个帮助类,你可以这么做

<button class="myapp-Button myapp-utils-button">

这个添加的值可以,举例来说,帮助你的测试框架来自动识别这是一个可以被点击的按钮。

Or you might decide that it's OK to break component isolation when the breach is tiny, and the additional work from splitting components would be too great. While I'll want to remind you that it's a slippery slope and that consistency is king et cetera, as long as your team is in agreement, and you get stuff done, you're doing the right thing.

或者你也可以决定偶尔打破下组件隔离的规矩,小小的违背下, 因为如果要完全实现组件分割的代价会很大。 虽然我也许会提醒你这可能会有风险,最好还是保持一致,但只要你的团队ok,你又完成任务了,那也没问题的!

最后

喜欢请转发,谢谢

License

CC BY 4.0

声明: 之前的翻译中增加了译者本人的一些注释,其中部分曲解了作者的原意,在此向原作者标示歉意,同时删除了之前的注释。

上一篇 下一篇

猜你喜欢

热点阅读