CSS 工程化
css 的问题
类名冲突的问题,当你写一个 css 类的时候,你是写全局的类呢?还是写多个层级选择后的类呢?
你会发现,怎么都不好!
-
过深的层级不利于编写、阅读、压缩、复用
-
过浅的层级容易导致类名冲突
一旦样式多起来,这个问题就会变得越发严重,其实归根结底,就是类名冲突不好解决的问题。
重复样式
这种问题就更普遍了,一些重复的样式值总是不断的出现在 css 代码中,维护起来极其困难。
比如,一个网站的颜色一般就那么几种:
-
primary
-
info
-
warn
-
error
-
success 如果有更多的颜色,都是从这些色调中自然变化得来,可以想象,这些颜色会到处充斥到诸如背景、文字、边框中,一旦要做颜色调整,是一个非常大的工程。
css 文件细分问题
在大型项目中,css 也需要更细的拆分,这样有利于 css 代码的维护。
比如,有一个做轮播图的模块,它不仅需要依赖 js 功能,还需要依赖 css 样式,既然依赖的 js 功能仅关心轮播图,那 css 样式也应该仅关心轮播图,由此类推,不同的功能依赖不同的 css 样式、公共样式可以单独抽离,这样就形成了不同于过去的 css 文件结构:文件更多、拆分的更细
而同时,在真实的运行环境下,我们却希望文件越少越好,这种情况和 JS 遇到的情况是一致的,因此,对于 css,也需要工程化管理。
从另一个角度来说,css 的工程化会遇到更多的挑战,因为 css 不像 JS,它的语法本身经过这么多年并没有发生多少的变化(css3 也仅仅是多了一些属性而已),对于 css 语法本身的改变也是一个工程化的课题
如何解决
这么多年来,官方一直没有提出方案来解决上述问题,一些第三方机构针对不同的问题,提出了自己的解决方案。
解决类名冲突
一些第三方机构提出了一些方案来解决该问题,常见的解决方案如下:
命名约定
就是提供一种命名的标准,来解决冲突,常见的标准有:
-
BEM
-
OOCSS
-
AMCSS
-
SMACSS
-
其他
我主要以BEM
为例说下:
BEM
是一套针对 css 类样式的命名方法。其他命名方法还有:OOCSS、AMCSS、SMACSS 等等
BEM 全称是:Block Element Modifier
一个完整的 BEM 类名:block__element_modifier,例如:banner__dot_selected,可以表示:轮播图中,处于选中状态的小圆点
实现原理
在 webpack 中,作为处理 css 的 css-loader,它实现了 css module 的思想,要启用 css module,需要将 css-loader 的配置 modules 设置为 true。
css-loader 的实现方式如下:
如何应用样式:
css module 带来了一个新的问题:源代码的类名和最终生成的类名是不一样的,而开发者只知道自己写的源代码中的类名,并不知道最终的类名是什么,那如何应用类名到元素上呢?
为了解决这个问题,css-loader 会导出原类名和最终类名的对应关系,该关系是通过一个对象描述的
目前,最流行的预编译器有 LESS 和 SASS,它们两个特别相似,这里主要说 less
看上去是不是和 LESS、SASS 一样呢?
但 PostCss 和 LESS、SASS 的思路不同,它其实只做一些代码分析之类的事情,将分析的结果交给插件,具体的代码转换操作是插件去完成的。
这一点有点像 webpack,webpack 本身仅做依赖分析、抽象语法树分析,其他的操作是靠插件和加载器完成的。
-
官网地址:https://postcss.org/
-
github 地址:https://github.com/postcss/postcss
安装
PostCss 是基于 node 编写的,因此可以使用 npm 安装
npm i -D postcss
postcss 库提供了对应的 js api 用于转换代码,如果你想使用 postcss 的一些高级功能,或者想开发 postcss 插件,就要 api 使用 postcss,api 的文档地址是:https://api.postcss.org/
不过绝大部分时候,我们都是使用者,并不希望使用代码的方式来使用 PostCss
因此,我们可以再安装一个 postcss-cli,通过命令行来完成编译
npm i -D postcss-cli
postcss-cli 提供一个命令,它调用 postcss 中的 api 来完成编译
命令的使用方式为:
postcss 源码文件 -o 输出文件
配置文件
和 webpack 类似,postcss 有自己的配置文件,该配置文件会影响 postcss 的某些编译行为。
配置文件的默认名称是:postcss.config.js
例如:
module.exports = { map: false, //关闭source-map }
插件
光使用 postcss 是没有多少意义的,要让它真正的发挥作用,需要插件
postcss 的插件市场:https://www.postcss.parts/
下面罗列一些 postcss 的常用插件:
-
postcss-preset-env
过去使用 postcss 的时候,往往会使用大量的插件,它们各自解决一些问题,这样导致的结果是安装插件、配置插件都特别的繁琐。
于是出现了这么一个插件 postcss-preset-env,它称之为 postcss 预设环境,大意就是它整合了很多的常用插件到一起,并帮你完成了基本的配置,你只需要安装它一个插件,就相当于安装了很多插件了。
安装好该插件后,在 postcss 配置中加入下面的配置
module.exports = { plugins: { "postcss-preset-env": {} // {} 中可以填写插件的配置 } }
这个插件里面有很多功能,比如说:
-
自动厂商前缀
可以实现某些新的 css 样式需要在旧版本浏览器中使用厂商前缀
例如:
::placeholder { color: red; }
这个功能在不同的旧版本浏览器中需要书写为
::-moz-placeholder{ color: red; } :-ms-input-placeholder{ color: red; } ::placeholder{ color: red; }
要完成这件事情,需要使用autoprefixer库
。而postcss-preset-env
内部包含了该库,自动有了该功能。
如果需要调整兼容的浏览器范围,可以通过下面的方式进行配置:
【方式 1】:添加 .browserslistrc
文件
创建文件.browserslistrc
,填写配置内容
last 2 version > 1%
【方式 2】:在package.json
的配置中加入browserslist
"browserslist": [ "last 2 version", "> 1%" ]
browserslist 是一个多行的(数组形式的)标准字符串。
它的书写规范多而繁琐,详情见:https://github.com/browserslist/browserslist
一般情况下,大部分网站都使用下面的格式进行书写
last 2 version > 1% in CN not ie <= 8
-
last 2 version: 浏览器的兼容最近期的两个版本
-
1% in CN: 匹配中国大于 1%的人使用的浏览器, in CN 可省略
-
not ie <= 8: 排除掉版本号小于等于 8 的 IE 浏览器☛默认情况下,匹配的结果求的是并集。
可以通过网站:https://browserl.ist/ 对配置结果覆盖的浏览器进行查询,查询时,多行之间使用英文逗号分割。
☛browserlist 的数据来自于 CanIUse 网站,由于数据不是实时的,所以不会特别准确
-
未来的 CSS 语法
CSS 的某些前沿语法正在制定过程中,没有形成真正的标准,如果希望使用这部分语法,为了浏览器兼容性,需要进行编译
过去,完成该语法编译的是 cssnext 库,不过有了 postcss-preset-env 后,它自动包含了该功能。
我们可以通过 postcss-preset-env 的 stage 配置,告知 postcss-preset-env 需要对哪个阶段的 css 语法进行兼容处理,它的默认值为 2
"postcss-preset-env": { stage: 0 }
一共有 5 个阶段可配置:
-
Stage 0: Aspirational – 只是一个早期草案,极其不稳定
-
Stage 1: Experimental – 仍然极其不稳定,但是提议已被 W3C 公认
-
Stage 2: Allowable – 虽然还是不稳定,但已经可以使用了
-
Stage 3: Embraced – 比较稳定,可能将来会发生一些小的变化,它即将成为最终的标准
-
Stage 4: Standardized – 所有主流浏览器都应该支持的 W3C 标准了解了以上知识后,接下来了解一下未来的 css 语法,尽管某些语法仍处于非常早期的阶段,但是有该插件存在,编译后仍然可以被浏览器识别
① 变量
未来的 css 语法是天然支持变量的
在:root{}
中定义常用变量,使用--
前缀命名变量
:root{ --lightColor: #ddd; --darkColor: #333; } a{ color: var(--lightColor); background: var(--darkColor); }
编译后,仍然可以看到原语法,因为某些新语法的存在并不会影响浏览器的渲染,尽管浏览器可能不认识如果不希望在结果中看到新语法,可以配置postcss-preset-env
的preserve
为false
② 自定义选择器
@custom-selector :--heading h1, h2, h3, h4, h5, h6; @custom-selector :--enter :focus,:hover; a:--enter{ color: #f40; } :--heading{ font-weight:bold; } :--heading.active{ font-weight:bold; }
编译后
a:focus,a:hover{ color: #f40; } h1,h2,h3,h4,h5,h6{ font-weight:bold; } h1.active,h2.active,h3.active,h4.active,h5.active,h6.active{ font-weight:bold; }
③ 嵌套
与 LESS 相同,只不过嵌套的选择器前必须使用符号 &
.a { color: red; & .b { color: green; } & > .b { color: blue; } &:hover { color: #000; } }
编译后
.a { color: red } .a .b { color: green; } .a>.b { color: blue; } .a:hover { color: #000; }
2、postcss-apply
这个插件可以支持在 css 中书写属性集,类似于 LESS 中的混入,可以利用 CSS 的新语法定义一个 CSS 代码片段,然后在需要的时候应用它。
:root { --center: { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }; } .item{ @apply --center; }
编译后
.item{ position: absolute; left: 50%; top: 50%; -webkit-transform: translate(-50%, -50%); transform: translate(-50%, -50%); }
☛实际上,该功能也属于 cssnext,不知为何 postcss-preset-env 没有支持
3、 postcss-color-function
该插件支持在源码中使用一些颜色函数
body { /* 使用颜色#aabbcc,不做任何处理,等同于直接书写 #aabbcc */ color: color(#aabbcc); /* 将颜色#aabbcc透明度设置为90% */ color: color(#aabbcc a(90%)); /* 将颜色#aabbcc的红色部分设置为90% */ color: color(#aabbcc red(90%)); /* 将颜色#aabbcc调亮50%(更加趋近于白色),类似于less中的lighten函数 */ color: color(#aabbcc tint(50%)); /* 将颜色#aabbcc调暗50%(更加趋近于黑色),类似于less中的darken函数 */ color: color(#aabbcc shade(50%)); }
编译后
body { /* 使用颜色#aabbcc,不做任何处理,等同于直接书写 #aabbcc */ color: rgb(170, 187, 204); /* 将颜色#aabbcc透明度设置为90% */ color: rgba(170, 187, 204, 0.9); /* 将颜色#aabbcc的红色部分设置为90% */ color: rgb(230, 187, 204); /* 将颜色#aabbcc调亮50%(更加趋近于白色),类似于less中的lighten函数 */ color: rgb(213, 221, 230); /* 将颜色#aabbcc调暗50%(更加趋近于黑色),类似于less中的darken函数 */ color: rgb(85, 94, 102); }
4、stylelint
在实际的开发中,我们可能会错误的或不规范的书写一些 css 代码,stylelint 插件会实时的发现错误。
由于不同的公司可能使用不同的 CSS 书写规范,stylelint 为了保持灵活,它本身并没有提供具体的规则验证。
你需要安装或自行编写规则验证方案
通常,我们会安装tylelint-config-standard库
提供标准的 CSS 规则判定
安装好后,我们需要告诉 stylelint 使用该库来进行规则验证,告诉的方式有多种,比较常见的是使用文件.stylelintrc
//.styleintrc { "extends": "stylelint-config-standard" }
此时,如果你的代码出现不规范的地方,编译时将会报出错误
body { background: #f4; }
发生了两处错误:
-
缩进应该只有两个空格
-
十六进制的颜色值不正确
如果某些规则不是我们所期望的,可以在配置中进行设置:
{ "extends": "stylelint-config-standard", "rules": { "indentation": null } }
设置为null
可以禁用该规则,或者设置为 4,表示一个缩进有 4 个空格。具体的设置需要参见 stylelint 文档:https://stylelint.io/
但是这种错误报告需要在编译时才会发生,如果我希望在编写代码时就自动在编辑器里报错呢?如果想在编辑器里达到该功能,安装 vscode 的插件stylelint
就可以了,它会读取你工程中的配置文件,按照配置进行实时报错。
解决 css 文件细分问题
这一部分,就要依靠构建工具,例如 webpack 来解决了,利用一些 loader 或 plugin 来打包、合并、压缩 css 文件。
利用 webpack 拆分 css
要拆分 css,就必须把 css 当成像 js 那样的模块;要把 css 当成模块,就必须有一个构建工具(webpack),它具备合并代码的能力,而 webpack 本身只能读取 css 文件的内容、将其当作 JS 代码进行分析,因此,会导致错误。
于是,就必须有一个 loader,能够将 css 代码转换为 js 代码
css-loader
css-loader 的作用,就是将 css 代码转换为 js 代码,它的处理原理简单到令人发指:就是将 css 代码作为字符串导出。
例如:
.red{ color:"#f40"; }
经过 css-loader 转换后变成 js 代码:
module.exports = `.red{ color:"#f40"; }`
上面的 js 代码是经过我简化后的,不代表真实的 css-loader 的转换后代码,css-loader 转换后的代码会有些复杂,同时会导出更多的信息,但核心思想不变。
再例如:
.red{ color:"#f40"; background:url("./bg.png") }
经过 css-loader 转换后变成 js 代码:
var import1 = require("./bg.png"); module.exports = `.red{ color:"#f40"; background:url("${import1}") }`;
这样一来,经过 webpack 的后续处理,会把依赖./bg.png 添加到模块列表,然后再将代码转换为
var import1 = __webpack_require__("./src/bg.png"); module.exports = `.red{ color:"#f40"; background:url("${import1}") }`;
再例如:
@import "./reset.css"; .red{ color:"#f40"; background:url("./bg.png") }
会转换为:
var import1 = require("./reset.css"); var import2 = require("./bg.png"); module.exports = `${import1} .red{ color:"#f40"; background:url("${import2}") }`;
总结,css-loader
干了什么:
-
将 css 文件的内容作为字符串导出
-
将 css 中的其他依赖作为 require 导入,以便 webpack 分析依赖
style-loader
由于 css-loader 仅提供了将 css 转换为字符串导出的能力,剩余的事情要交给其他 loader 或 plugin 来处理。
style-loader 可以将 css-loader 转换后的代码进一步处理,将 css-loader 导出的字符串加入到页面的 style 元素中
例如:
.red{ color:"#f40"; }
经过 css-loader 转换后变成 js 代码:
module.exports = `.red{ color:"#f40"; }`
经过 style-loader 转换后变成:
module.exports = `.red{ color:"#f40"; }` var style = module.exports; var styleElem = document.createElement("style"); styleElem.innerHTML = style; document.head.appendChild(styleElem); module.exports = {}
以上代码均为简化后的代码,并不代表真实的代码,style-loader 是有能力避免同一个样式的重复导入
抽离 css 文件
目前,css 代码被 css-loader 转换后,交给的是 style-loader 进行处理。style-loader 使用的方式是用一段 js 代码,将样式加入到 style 元素中。而实际的开发中,我们往往希望依赖的样式最终形成一个 css 文件,此时,就需要用到一个库:mini-css-extract-plugin
该库提供了 1 个plugin
和 1 个loader
-
plugin:负责生成 css 文件
-
loader:负责记录要生成的 css 文件的内容,同时导出开启 css-module 后的样式对象
使用方式:
const MiniCssExtractPlugin = require("mini-css-extract-plugin") module.exports = { module: { rules: [ { test: /.css$/, use: [MiniCssExtractPlugin.loader, "css-loader?modules"] } ] }, plugins: [ new MiniCssExtractPlugin() //负责生成css文件 ] }
配置生成的文件名
同output.filename
的含义一样,即根据 chunk 生成的样式文件名。
配置生成的文件名,例如[name].[contenthash:5].css
默认情况下,每个 chunk 对应一个 css 文件。
以上就是我今天的分享,希望对大家有所帮助,如果哪里有问题,希望大家多多指正,谢谢大家!
本文文字及图片出自 InfoQ
你也许感兴趣的:
- 【外评】电脑从哪里获取时间?
- 【外评】为什么 Stack Overflow 正在消失?
- Android 全力押注 Rust,Linux 却在原地踏步?谷歌:用 Rust 重写固件太简单了!
- 【外评】哪些开源项目被广泛使用,但仅由少数人维护?
- 【外评】好的重构与不好的重构
- C 语言老将从中作梗,Rust for Linux 项目内讧升级!核心维护者愤然离职:不受尊重、热情被消耗光
- 【外评】代码审查反模式
- 我受够了维护 AI 生成的代码
- 【外评】Linux 桌面市场份额升至 4.45
- 【外评】作为全栈开发人员如何跟上 AI/ML 的发展?
你对本文的反应是: