CSS 模块化管理

昨天看了一道面试题,说如何管理 CSS 代码;它谈到了君子协定BEM 规范(然后楼主就说自己马上就拿到 offer 了😅)。但是,BEM 规范说实在已经不够接地气了,现代 CSS 开发的基调是模块化。这期就借此谈谈在 vue 项目中管理 CSS 的几种常用技术。

全局样式表

CSS 的初衷就是全局样式,并通过不同优先级的特征值覆盖其他样式。在 vue 开发中,我们一般会把全局的 CSS 样式表放在/asserts/css/main.css里,然后在入口文件 index.html 上加样式表的 link:

<!-- index.htm -->
<link rel="stylesheet" type="text/css" href='/asserts/css/main.css'>

不过,在工程中一般会使用 webpack。Webpack 会帮我们把 css link 添加到 index.html 里;所以 vue 模版通常就直接在 main.js 里导入 main.css 了。

//main.js
import Vue from 'vue'
import App from './App.vue'

// Importing the global css file
import "@/assets/css/main.css"

new Vue({
  render: h => h(App)
}).$mount('#app')

上面是最传统的 CSS 管理方式,但是我自己很少在 main.css 里添加代码,主要原因还是影响面太大;通常的做法还是在 vue 组件里添加局部样式(即便复用率不高,我也忍了)。

<style> in vue

除了在全局样式文件里编写 CSS,Vue 原生组件还支持在组件内部定义样式表,也就是在 vue 的<style>标签里添加 CSS 选择器。这里科普一下:工程上,vue 文件通常有三种标签,<template><script><style>,分别盛放 html,js 和 css。但事实上 vue 文件只是一个模版,不可以直接运行,我们是依靠了 webpack 的vue-laoder将 vue 模版转换成 js 文件才得以运行;而<style>还需要vue-style-loadercss-loader等加载器才能最终编译到全局的 css 文件中。

通常,我们还会给<style>加一个叫scoped的属性;效果是:这里的 CSS 样式将只作用于当前组件(相当于组件级样式表)。当然,这些样式最终还是会作用到全局,只是耍了个小花招。我们看看样例:

<template>
  <div class="content">Onion</div>
</template>

<style scoped>
.content {
  width: 300px;
}
</style>

scoped 属性会给涉及的 DOM 标签自动添加一个唯一属性——data-v-{componentHash}——为组件内的 CSS 指定作用域;接着 webapck 再把 scoped style 里的选择器改名为.{className}[data-v-{componentHash}];最后,利用组合选择器(如,.content[data-v-b52c41])的特性实现了所谓的 CSS 模块化管理了。

<div class="content" data-v-b52c41>Onion</div>

<style>
.content[data-v-b52c41] {
  width: 300px;
}
</style>

CSS modules

CSS modules 也是现在很流行的一种模块化管理技术,在 react 社区里应用得很多。Vue 里也可以作为 scoped 样式的替代方案。和 scoped style 相比,它也没啥神秘感的,写法上略有不同罢了:

  • <style>里换一个叫module的属性
  • <template>调用时,通过$style绑定 CSS 对象
  • 在 webpack 里给 css-loader 加一个modules:true的参数
<!-- Component.vue -->
<template>
  <div class="$style.content">Onion</div>
</template>

<style module>
.content {
  width: 300px;
}
</style>
// webpack.config.js
rules: [
  {
    test: /\.css$/,
    use: [
      {
        loader: 'css-loader',
        options: {
          // enable CSS Modules
          modules: true,
          // customize generated class names
          localIdentName: '',
        }
      }
    ]
  }
]

我这里顺便定制了 class 类名:[name]__[local]__[hash],也即.{componentName}__{className}__{randomHash}。最后生成的文件会是如下所示:

<div class="Component__content__2Kxy9sid">Onion</div>

<style>
.Component__content__2Kxy9sid {
  width: 300px;
}
</style>

scoped style 和 CSS modules 都是利用 HASH 把组件内的 CSS class 唯一化;这样各个模块内的 CSS 就不会互相覆盖了。除此之外,CSS modules 比 scoped style 再多一个功能——可以使用数组或是对象语法:

<template>
  <div>
    <p :class="{ [$style.red]: isRed }">
      Am I red?
    </p>
    <p :class="[$style.red, $style.bold]">
      Red and bold
    </p>
  </div>
</template>

CSS in JS

所谓 CSS in JS,就是使用了一个叫 styled-components 的库;它把样式表定义写在了 JS 文件里(最后也会被抽取成某 CSS 文件,类名 hash 处理)。CSS in JS 最早也是在 React 社区里活跃起来的,后来有团队为 vue 也写了一个库,叫vue-styled-components,很快这个概念也迅速蔓延到了 vue 实战中。所谓 styled-components(样式化组件)就是写一个只包含样式,不包含业务逻辑的组件;这与我之前介绍过的renderless components恰巧相反。

用法如下:

  • 安装 vue-styled-components

    yarn add vue-styled-components
    
  • 写一个 styled 控件(JS 文件)

    // @/components/content.js
    import styled from 'vue-styled-components';
    
    export const Content = styled.div`
      width: 300px;
    `;
    

    p.s. 这里styled.div是函数,之后跟一个模板字符串,用到了 ES6 的Tagged templates语法

  • 调用该 styled component

     <!-- Component.vue -->
    <template>
      <content>Onion</content>
    </template>
    
    <script>
    import { Content } from '@/components/content.js'
    export default {
      components: { Content },
    }
    </script>
    

OK,基本用法如上所示,但写成样式化组件有什么好处呢?很明显嘛,一是组件复用性更高;二是 vue 组件可以传 props 呀。

比如我们希望传一个 color 属性,定制字体颜色。传统方案基本只能在行内样式:style上做文章。但是行内样式的特征优先级太高,过多使用不利于维护。

<template>
  <div :style="{color: customizedColor}">content</div>
</template>

<script>
export default {
  data: () => ({ customizedColor: 'red' })
}
</script>

这时候,样式化组件的优势就出来了:它修改的是 CSS class 的某个属性。我们看看怎么写带 props 的 styled component:

// @/components/content.js
import styled from 'vue-styled-components';

export const Content = styled('div', {
  color: {
    type: String,
    default: 'black',
  }
})`
  width: 300px;
  color: ${{color} => color}
`;

写法有点变化,调用的是一个柯里化的函数styled,第一个参数是 DOM 标签,第二个参数就是 Vue 组件里常用到的props了;之后继续接模板字符串,这样在${}里就可以调用props值了(所以,你理解为什么要用模版字符这种写法了吧?)。使用如下:

<template>
  <content :color="customizedColor">Onion</content>
</template>

<script>
import { Content } from '@/components/content.js'
export default {
  components: { Content },
  data: () => ({ customizedColor: 'red' }),
}
</script>

该组件渲染出来的 html 代码如下所示。相比原生组件只能绑定行内样式:style——直接作用到 DOM 标签上,样式化组件是将属性直接嵌入到 CSS 里,在代码易维护方面更进一步;此外,相比于原生组件使用组合选择器(如,.content[data-v-7ba5bd90]),样式化组件生成的是一个随机类名(如,fGdyfT),只有一个选择器,在渲染效率方面无形中也拉开了一点距离。

<div class="fGdyfT">Onion</div>

<style>
.fGdyfT {
  width: 300px;
  color: red;
}
</style>

小结

这期介绍了现代前端技术中比较常见的四种 CSS 管理方式。从全局样式表,到 CSS modules,再到 CSS in JS,CSS 进化的趋势就是模块化。那为什么需要 CSS 模块化呢?CSS 本身的规则是全局的,任何一个样式变化,都对整个页面起效。于是,样式冲突(污染)的问题一直是 CSS 解不开的难题。传统的做法无非是把类命写长一点,多加几个选择器覆盖之前的样式表等等;加多了之后发现,这种代码根本无助于可读性,还不如直接使用 hash 避免冲突,再依据 source map 寻找模块代码来得有实在。

最后,至于项目中该使用哪一种技术,还是要视情况而定。客观上讲,CSS in JS 和 CSS modules 更强大更新颖,但在写法上很多人可能还转不过来;scoped style 虽然功能弱一些,倒也能满足基本需求。所以,还是需要项目决策者自行斟酌。

相关

文章同步发布于an-Onion 的 Github。码字不易,欢迎点赞

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容