前端进阶(1) - CSS 模块化

CSS 模块化

CSS(Cascading Style Sheets),从诞生之初就决定了它没法编程,甚至连解释性语言都算不上,只能做为一种简单的层叠样式表,对 HTML 元素进行格式化。css

但随着前端的发展,前端项目已经变得愈来愈庞大和复杂,社区也一直在探索如何以一种有效的方式去管理前端的代码(js/css/html)和资源(images, fonts, ...)。html

在这个过程当中,社区探索出了 js 的模块化(amd, commonjs, es6),如今用 js 开发大工程已经游刃有余,而 css 的模块化却尚未特别的深刻人心。前端

1. 分组式模块化

这是最先对 css 模块化的实现,也是最主要的一种方式,包括如今不少组件和开发者都是用这种方式开发的。vue

分组式模块化就是用命名的方式,以不一样的前缀表明不一样的含义,实现样式分组,文件分块,达到模块化的目的。react

好比:webpack

# 目录结构
|-- one/page/css/ 某个页面的 css 目录
    |-- common.css 通用的 css
    |-- page1/ 单页面1
        |-- section1.css 区域1 css
        |-- section2.css 区域2 css
    |-- page2/ 单页面2
    |-- ...
    
# common.css 文件
.c-el-1 {
    ...
}
.c-el-2 {
    ...
}    
...    
    
# page1/section1.css 文件
.page1-section1 {
    ...
}
.page1-section1 .el-1 {
    ...
}    
.page1-section1 .el-2 {
    ...
}    
...

# page1/section2.css 文件
.page1-section2 {
    ...
}
.page1-section2 .el-1 {
    ...
}    
.page1-section2 .el-2 {
    ...
}    
...

这种方式并非真正意义上的模块化,由于没法避免全局冲突的问题,但原生 css 并不具有编程的能力,因此这个问题是没法避免的。尽管分组式不算真正意义上的模块化,可是这种方式没有脱离 css 原生的机制,因此尤为是第三方组件在导出 css 文件时,不少都使用的是这种方式。git

好比,ant-design 导出的 css 中使用 ant- 前缀标识,mui 导出的 css 中使用 mui- 前缀标识等等。es6

1.1 最佳实践

css 命名分组实践的时间很长,从 css 诞生之初就有了,因此社区已经发展很成熟了,好比网易的 css 规范框架 NECH-uigithub

补充:
  • 一个 css 文件不宜过大,可使用 @import 进行文件分块;
  • 样式渲染尽可能不要使用 #id [attr],应尽可能使用 .class
  • 使用 js 库操做 dom 时,尽可能不要用 .class,应尽可能用 #id data-set,如 $('#main'), $('[data-tab="1"]')
<ul>
    <li data-tab="1">tab1</li>
    <li data-tab="2">tab2</li>
</ul>
<div data-tab-container="1"></div>
<div data-tab-container="2"></div>

1.2 css 语言扩充

由于 css 不是编程语言,因此不能声明变量、函数,不能作判断、循环和计算,也不能嵌套,因此这就使得写样式是一个效率底下且又枯燥的活儿。web

为了解决这个问题,社区在探索中主要衍生出了两种拓展语言 lesssass,它们兼容 css,而且拓展了编程的功能,主要是带来了如下的特性:

  • 能够声明变量、函数,能够进行一些简单的计算、判断、循环;
  • 能够嵌套选择器,这样节省了书写的内容,也更具阅读性;
.page1-section1 {
    ...
    
    .el-1 {
        ...
        
        .el-1-1 {
            ...
        }
    }
        
    .el-2 {
        ...
    }   
}
  • @import 避免重复导入问题,所以能够放心大胆的导入其余文件。

从模块化的角度来说,lesssass 只是扩充了 css 的功能,但并无在语言的层面作模块化,由于全局命名冲突的问题依然还在。

2. 模块化(导出为 js 对象)

想要让 css 具有真正意义上的模块化功能,暂时还不能从语言的层面来考虑,因此只能从工具的角度来实现。

目前比较好的方式是使用 js 来加载 css 文件,并将 css 的内容导出为一个对象,使用 js 来渲染整个 dom 树和匹配相应的样式到对应的元素上,在这个过程当中,咱们便有机会对 css 作额外的处理,来达到模块化的目的。

好比:

源文件

# style.css 文件
.className {
  color: green;
}

# js 文件
import styles from "./style.css";

element.innerHTML = '<div class="' + styles.className + '">Hello!</div>';

实际效果

# style.css 文件
._23_aKvs-b8bW2Vg3fwHozO {
  color: green;
}

# DOM
<div class="_23_aKvs-b8bW2Vg3fwHozO">Hello!</div>

在这个转换过程当中,根据文件的位置、内容生成一个全局惟一的 base64 字符串,替换原来的名称,避免了全局命名冲突的问题,这样便达到了模块化的目的。因此,开发的过程当中便无全局样式冲突的问题。

# common.css 文件
.container {
    ...
}
.el1 {
    ...
}
.el2 {
    ...
}    
...    
    
# page1/section1.css 文件
.container {
    ...
}
.title {
    ...
}    
.content {
    ...
}    
...

# page2/section1.css 文件
.container {
    ...
}
.title {
    ...
}    
.content {
    ...
}
...

对 css 模块化的定义参见 css-modules,其中对 css 书写需求主要是:

  1. 应当用 .class,而非#id [attr](由于只有 .class 才能导出为对象的属性);
  2. 推荐用 .className 书写,而非 .class-name(前者能够经过 styles.className 访问,后者须要经过 styles['class-name'] 才能访问)。

更多功能能够查看 css-modules

固然这个功能须要构建工具的支持,若是你是使用 webpack 构建工程的话,可使用 css-loader,并设置 options.modulestrue, 即可使用模块化的功能了。

3. 模块化(内置 js,绑定组件)

随着前端组件化的发展,组件化框架的更新,如 reactvue,慢慢的发展为把整个组件的资源进行封装,并只对外暴露一个对象,而调用者无需关心组件的内部实现和资源,直接调用这个对象就够了。

好比(以 react 为例),一个 Welcome 组件,包括一个 js 文件、一个 css 文件、图片:

# Welcome 组件
|-- welcome.js
|-- welcome.css
|-- images/

welcome.js 中即可以下加载(使用“导出为 js 对象”的 css 模块化):

import styles from './welcome.css';
import image1 from './images/1.jpg';

其实,还有另一种思路,就是将 css 内置 js 中,成为 js 的一部分。

这样作的目的,一是 css 的模块化,二是直接绑定到组件上。

好比,material-uistyled-jsxjssvue style scoped 即是使用的这种方式。

这种方式的实现有不少种,这里主要介绍一下 styled-jsx

3.1 styled-jsx

styled-jsx 的原理是根据当前文件的位置、内容生成一个全局惟一的标识,而后把这个标识追加到组件每个元素上,每个样式选择器上,达到模块化的目的。

能够参考官方文档,查看详细的用法,我在这里给个例子:

3.1.1 安装工具(babel 转码所需)

npm install --save styled-jsx

3.1.2 配置 babel plugins(如 .babelrc

{
  "plugins": [
    "styled-jsx/babel"
  ]
}

3.1.3 添加源文件代码

hello.js

export default () => (
    <div className={'container'}>
        <p className={'hello'}>Hello! Hello!</p>
        <div id={'hi'}>Hi!</div>
        <style jsx>{`
          .container {
            color: blue;
          }
          p:first-child {
            color: red;
          }
          .hello {
            color: yellow;
          }
          #hi {
            color: green;
          }
        `}</style>
    </div>
)

3.1.4 转码

babel path/to/hello.js -d target/dir

转码后的文件

import _JSXStyle from 'styled-jsx/style';

export default () => (
    <div className={'jsx-234963469' + ' ' + 'container'}>
        <p className={'jsx-234963469' + ' ' + 'hello'}>Hello! Hello!</p>
        <div id={'hi'} className={"jsx-234963469"}>Hi!</div>
        <_JSXStyle styleId={"234963469"} css={".container.jsx-234963469{color:blue;}p.jsx-234963469:first-child{color:red;}.hello.jsx-234963469{color:yellow;}#hi.jsx-234963469{color:green;}"} />
    </div>
);

3.1.5 运行

实际渲染效果

<style type="text/css" data-styled-jsx="">
.container.jsx-234963469{
  color:blue;
}
p.jsx-234963469:first-child{
  color:red;
}
.hello.jsx-234963469{
  color:yellow;
}
#hi.jsx-234963469{
  color:green;
}
</style>



<div class="jsx-234963469 container">
  <p class="jsx-234963469 hello">Hello! Hello!</p>
  <div id="hi" class="jsx-234963469">Hi!</div>
</div>

4. 后续

更多博客,查看 https://github.com/senntyou/blogs

做者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证