在前端开发中一直有个原则,叫做"关注点分离",意思就是各种技术只负责自己的领域,不要混合在一起,形成耦合,这种原则比较直观的体现就是不要写"行内样式"(inline style)和"行内脚本"(inline script),HTML、CSS、JavaScript各干各的事,避免混用(此处参考CSS in JS 简介)
现在由于各类前端框架以及打包工具的使用,使我们之前的关注点分离变的策略,现在其实都是在 js 中进行。
相比 HTML,框架对 CSS 都没进行什么特殊处理,也没有形成类似 JSX 的解决方案,不过这其中倒是有一个比较有意思的解决方案:css-in-js(不过在 js 中写 css,怎么都感觉有些别扭)。
另外 css 本身编程能力薄弱,社区也形成各种方案来提升 css 编程能力。相比其他方案,我感觉 Postcss 更为优雅,所以这里特别介绍下
目前是基于 postcss 7.0.32 版本
基本概念
- Postcss是一个用 js 插件转换成 css 的工具
- Postcss 不是预处理器
预处理器是指对 css 能力增强的功能,添加一些一些本身不是css的功能(比如嵌套、变量),通过处理后能转成普通的CSS,
- Postcss 不是后处理器
通过一些规则把已有的css进行完善,比如添加浏览器前缀
- Postcss 是作为一个平台的存在,利用 Postcss 提供的插件可以组合各种不同模块,来构建更为复杂的功能
Demo
- 任意目录 npm init -y
使用这种方式,项目的名词一定不要和某些库的名词冲突,比如叫webpack,postcss,如果叫这个名词安装相关库时就会报ENOSELF的错误
- 安装 相关依赖
- css-loader 是可以在页面中使用 import 引入 css 的能力
- style-loader 是把 css 代码生成 style 标签,放到 head 标签中
- mini-css-extract-plugin 提取css用的插件
npm i --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin mini-css-extract-plugin css-loader style-loader @babel/cli @babel/core @babel/preset-env vue-loader vue-template-compiler
npm i --save vue
- 新建两个文件夹
src、dist,以及babel.config.js、webpack.config.js和其他相关文件
/// babel.config.js
const presets = [["@babel/env"]];
module.exports = {
presets
}
...
/// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
entry: './src/index.js',
resolve: {
extensions: ['.js', '.vue']
},
output: {
filename: '[name].js',
publicPath: '/'
},
mode: 'development',
module: {
rules: [
{test: /\.js$/, use: 'babel-loader', exclude: /node_modules/},
{test: /\.vue$/, use: 'vue-loader', exclude: /node_modules/},
{test: /\.css/, use: ['style-loader', 'css-loader']}
]
},
devServer: {
port: '8111'
},
plugins: [
new HtmlWebpackPlugin({
template: './src/tpl/index.html'
}),
new VueLoaderPlugin()
]
}
...
/// 假定有个 app.vue 按如上设置
import '../style/app.css';
这时在 js 文件中,就可以通过 import 方式把样式文件导入到页面中。不过此时因为用的是 style-loader ,所以样式会写入到 <head> 下的 <style> 标签中
可使用 mini-css-extract-plugin ,这会把相关样式拆分成一个个独立的文件
/// webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
...
{test: /\.css/, use: [MiniCssExtractPlugin.loader, 'css-loader']} // 替换 style-loader
...
plugins: [
...
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
})
]
如果 import 多个样式文件,按目前的设置,会合并成一个样式文件导出。不过这些文件中的内容,只是简单的合并在一起,完全依赖样式编写人员控制每个样式文件的内容
/// app.css
.info {
font-size: 24px;
}
...
/// color.css
.info {
color: red;
}
...
/// app.vue
import '../style/app.css';
import '../style/color.css';
...
/// 合并出的样式文件如下
.info {
font-size: 24px;
}
.info {
color: red;
}
编程化
安装相关相关依赖
npm i -D postcss postcss-loader
通过 .pcss 引入样式文件
.pcss是 Postcss 的专用格式文件
- 安装 postcss-import,使在
.pcss文件可以使用 @import 引入样式文件
npm i -D postcss-import
需要先添加一个对 Postcss 的运行配置文件,可以叫 .postcssrc.js 或 postcss.config.js
// 暂时没有其他配置,可以把留空
module.exports = {
plugins: []
}
postcss.config.js除了plugins还具有如下参数:- syntax: 提供语法分析器和字符串化器的对象
- parser: 特殊的语法解析器(例如,SCSS)
- stringifier: 特殊语法输出生成器(例如Midas)
- map: 对map文件的设置
- from: 输入文件名
- to: 输出文件名
不过常用的就是 plugins (定义使用的插件)
/// 在webpack.config.js中做如下修改
{test: /\.pcss$/, use: [
MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader"
]},
...
/// mian.pcss
@import './app.css';
@import './color.css';
...
/// app.vue 引入 main.pcss
import './main.pcss';
如例子 PostCSS 的编程能力,是通过各种插件实现的,这点是其与lass sass stylus不同的地方,多了一个自己配置的过程,所以相对自由度高些。这些差距可以自己编写,也可以直接使用社区现有插件,插件在 PostCSS 的配置文件中设置使用参数
相关插件
autoprefixer
这应该是postcss中使用最为广泛的插件了,自动识别设定的浏览器兼容范围,添加浏览器样式前缀
npm i -D autoprefixer
/// postcss.config.js
let postcssConfig = {};
postcssConfig.autoprefixer = {
browsers: ['> 1%', 'ff 3']
}
module.exports = {
plugins: postcssConfig
}
- 修改main.pcss内容如下
b {
border-radius:5px;
}
编译运行后结果为
b {
-moz-border-radius:5px;
border-radius:5px;
}
autoprefixer 的参数,使用默认配置即可。这里只说 browsers 参数的设定,因为这关系到最终添加前缀的内容。
browsers
browsers 是利用 browserslist 功能来决定是否需要添加某些浏览器前缀,在browserslist的文档里我们可以找到详细设定,可以设定针对浏览器、国家、指定平台、年份做设置
> 5%
cover 99.5%
> 5% in US
node 10 and node 10.4
since 2015
ie 6-8
not ie <= 8
下面列举下在项目中最有可能碰到的浏览器
- Android: Android webview浏览器
- iOS: ios的Safari浏览器
- Chrome: 谷歌浏览器
- ChromeAndroid: 谷歌浏览器安卓版
- Edge: 微软的Edge浏览器
- ie: ie浏览器
- Safari: Safari桌面浏览器
- ff: firefox浏览器
- and_ff: firefox安卓浏览器
- and_qq: QQ浏览器安卓版
- and_uc: UC浏览器安卓版
browsers 接收的是一个数组,所以可以像例子中那样分开设置,下面的话的意思就是为了适配 安卓2.3,ios3.2,Safari3.1,IE10 浏览器 要添加相关前缀
'> 0%','Android 2.3','iOS 3.2','Safari 3.1','IE 10'
-
> 0%: 是指当你不想像上面设置那么繁琐的指定浏览器时,可以直接指定个大概,就是我要支持市面上多少多少比例的浏览器,这个数字前面可以添加普通的运算符>、>=、<、<= -
前缀修饰符 not: 表示不在某个范围中,还可以使用
coverextends或since进行更细化的设置,包括道指定从哪年开始的什么版本。
圆角功能是 ff4 才加的功能,如上的配置,由于指定适配某个浏览器版本,才会有针对特定浏览器的前缀
/// 如此设定后,生成的样式就不会有针对 火狐浏览器 的前缀
browsers: ['> 1%', 'ff > 4']
设置范围时需要指定范围,不能直接设置 ff 或者 not ff,这样编译会报错,你需要明确指明版本才行
在 package.json 中设置 browserslist
在当前的版本,这么设置运行 webpack 时,编译会有段提醒,建议不要在配置文件中设置浏览器范围。要么加到 package.json中,要么就设置在 .browserslistrc
/// .browserslistrc
> 1%
ff > 4
...
/// package.json
"browserslist": ["> 1%", "ff > 4"] // 这个优先级低于插件设置
postcss-preset-env
这个插件允许开发人员在当前项目中使用 css 将来版本可能会加入的新特性,这个就非常类似于写 ES6+ 的代码,但是使用 babel 转成 ES5 的代码。
这个插件中包含了autoprefixer,另外一个类似的插件,postcss-cssnext 已经不再维护
npm i --save-dev postcss-preset-env
const PostCssEnv = require("postcss-preset-env");
const postCssEnvConfig = PostCssEnv({
stage: 2
});
module.exports = {
plugins: [postCssEnvConfig]
};
插件参数说明
下面根据官方的文档介绍相关设置参数:
-
stage: 根据现行web标准的进程(主要就是 w3c 组织认定的标准,所以这个插件中具备的功能都是未来很有可能直接加到 css 标准中的内容)来决定某些 css 功能需不需要通过垫片的方式添加,可以设置 0~4 的任意数字,如果没有设置这个值,这个值默认为 2
- 0: 这个阶段处在
非官方草案或编辑草案阶段,很有可能会被删除 - 1: 这个阶段处于
实验阶段,有可能会被设置为标准 - 2: 这个阶段处于
待定阶段,这也是插件默认的数值,处于这个阶段的功能,基本可以认为会被加到未来的标准中 - 3: 这个阶段处于
稳定阶段,基本上已经有浏览器厂商实现了,可以直接使用 - 4: 这个阶段处于
标准阶段
- 0: 这个阶段处在
-
features: 针对特定css功能进行单独设置,比如设置了 stage 为 3,可以通过这个参数来特定使用还处于 stage为 2 的功能
postcssPresetEnv({
/* 使用stage为3的标准,同时允许嵌套规则(嵌套是stage 1的标准) */
stage: 3,
features: {
'nesting-rules': true
}
})
-
browsers: 参考上面
browserslist的介绍,无须设置这个参数 -
insertBefore / insertAfter: 允许你在该插件运行之前或者之后运行某些插件,可以是一个或者多个
import postcssSimpleVars from 'postcss-simple-vars'; // 这是类sass的变量处理插件
postcssPresetEnv({
insertBefore: {
'all-property': postcssSimpleVars
}
})
- autoprefixer: 设为 false 可以禁用该 autoprefixer,该插件包含的 autoprefixer 和 browsers 就可以在这里设置,不过一般情况下使用默认即可
const postCssEnvConfig = PostCssEnv({
stage: 2,
autoprefixer: {} // 留空就是使用默认,这个默认会把 autoprefixer、browsers 都开启
});
- preserve: 决定所有插件是否应接收同一个 preserve 选项,该选项可以保留或删除以其他方式填充的 CSS
/// 假定现在把 state 设为 1,然后在样式文件中使用 custom selectors(自定义选择器)
@custom-selector :--heading h1, h2, h3, h4, h5, h6;
article :--heading + p {
color: red;
}
...
/// 如果在配置项中设置 preserve: true 编译后的代码会保留一些元素源码
@custom-selector :--heading h1, h2, h3, h4, h5, h6;
article h1 + p,article h2 + p,article h3 + p,article h4 + p,article h5 + p,article h6 + p {
color: red;
}
article :--heading + p {
color: red;
}
...
/// 设为 false 的时候,就不会保留一些多余的源码
article h1 + p,article h2 + p,article h3 + p,article h4 + p,article h5 + p,article h6 + p {
color: red;
}
- importFrom: 从外部导入相关变量信息(如自定义媒体、自定义属性、自定义选择器和环境变量),这个导入的数据源可以是 css、js 或者 json (还支持函数和直接对象传值)。这个功能完全可以
/// style/theme.css
@custom-media --small-viewport (max-width: 30em);
@custom-selector :--heading h1, h2, h3, h4, h5, h6;
:root {
--color: red;
}
...
PostCssEnv({
...
importFrom: "src/style/theme.css" // 设置导入变量相关的样式文件
})
如果使用的是 postcss.config.js 的方式进行配置,在Node运行环境下,可以根据运行时的 ENV 和某些自己设置的参数,来决定最基础的变量参数,轻松实现换肤的功能
- exportTo: 与 importFrom 功能相反,导出相关变量(这里应该是鼓励我们把变量都放在一个公共的文件下,这样便于维护)
stage 2及以上的css语法
就是开启默认配置可以直接使用的css语法
all用于重置所有属性 [stage 3 下面以数字简写]
a {
all:initial ;
}
| 值 | 描述 |
|---|---|
| initial | 修改所有元素属性或父元素的值为其初始化值 |
| inherit | 修改所有元素属性或父元素的值为其父元素的值 |
| unset | 修改所有元素属性或父元素的值为其父元素的值(如果有继承)或其初始值 |
:any-linkCSS 伪类选择器,会匹配到:link或:visited[2]
nav:any-link> span {
background-color:yellow ;
}
...
// 编译为
nav :link > span,nav :visited > span {
background-color: yellow;
}
- break属性:用于定义多列布局中中断行为的属性 [3]
- break-inside: 描述了在多列布局页面下的内容盒子如何中断
- break-after: 描述在生成的盒子之后的页面,列或区域中断行为
- break-before: 描述列或区域在生成的盒子之前应如何处理中断
- case-insensitive attributes: 定义属性选择器不区分大小写 [2]
[title="hs" i] {
border-style: solid none;
}
...
/// 转换为所有可能性的组合,少用。。转换出来的太多了
[title="hs"],[title="Hs"],[title="hS"],[title="HS"] {
border-style: solid none;
}
- custom properties:用于定义CSS属性接受的变量值 [3]
/// 运行代码时并没有转换,因为这是3级语法,高级一点的浏览器都已经实现了这部分功能
img {
--some-length: 32px;
height: var(--some-length);
width: var(--some-length);
}
:dir(rtl|ltr)匹配特定文字书写方向的元素,中文语系无需关注 [2]- double-position-gradients: 圆锥渐变[2]
.pie_chart {
background-image: conic-gradient(yellowgreen 40%, gold 0deg 75%, #f06 0deg);
}
其他还有,线性渐变(Linear Gradients)、径向渐变(Radial Gradients)
:focus-visible键盘访问触发的元素聚焦(通过 tab 切换选择元素) [2]
/// 任意一个 css 文件
.name:focus { // :focus就是获取焦点,可以时鼠标点击触发,也可以是通过tab切换触发
outline: none;
}
.name:focus-visible { // 对获取焦点进行细化,这个只针对tab切换时的获取焦点
outline: #00ff00 dotted thick;
}
...
/// DOM
<div class="name" tabindex="0">name</div>
在页面中当你使用鼠标点击这个元素,和通过tab键切换时会有不同的体验。鼠标点击时由于设定的 outline 为 none,所以什么都看不到。但通过tab切换时元素外层明显有个边框,这对提升无障碍访问有很大的帮助。
由于内部使用的是 postcss-focus-visible 插件,插件作者是假定需要引入 Polyfill 解决兼容问题,所以如果实在 .pcss 文件中编写相关样式代码,会把 .name:focus-visible 转为 .name.focus-visible,想要避免这种转换,可以把相关代码放到 .css 的文件中
:focus-within当前元素或者当前元素的子元素处于focus状态 [2]
.tab-content:focus-within {
background: #f00;
}
...
/// 假定在页面中有这么一个 DOM 元素标签,按上面的样式代码,只要这其中的input获取焦点,整块区域背景就会变红,这个属性可以提升交互体验
<div class="tab-content">
<input placeholder="请输入姓名" type="text" />
</div>
- font-variant: 设置小型大写字母的字体显示文本,这意味着所有的小写字母均会被转换为大写,但是所有使用小型大写字体的字母与其余文本相比,其字体尺寸更小。又是一个对中文区没什么用的属性 [3]
- gap属性 [3]
该属性是用来设置网格行与列之间的间隙(gutters),是 row-gap 和 column-gap的简写形式,是 Css Grid 中的概念
.grid-1 {
gap: 20px;
}
.grid-2 {
column-gap: 40px;
row-gap: 20px;
}
...
/// 转为
.grid-1 {
grid-gap: 20px;
}
.grid-2 {
grid-column-gap: 40px;
grid-row-gap: 20px;
}
gray(): 用于指定完全去饱和颜色的功能 [2]
p {
color: gray(50);
}
...
/// 转为
p {
color: rgb(119,119,119);
}
- alpha:十六进制颜色表示法 比一般的3/6表示法,多1/2个字符,可以指定透明度 [2]
section {
background-color: #f3f3f3f3;
color: #0003;
}
...
/// 转为
section {
background-color: rgba(243,243,243,0.95294);
color: rgba(0,0,0,0.2);
}
lab()使用lab表示颜色 [2]lch()使用lch表示颜色 [2]hwb()使用hwb表示颜色[2]
color: lch(53 105 40);
color: lab(240 50 20);
color: hwb(120 44% 50%);
一般这些颜色在设计领域或者排版印刷领域使用比较多,前端一般用 rgb rgba
- image-set(): 根据用户分辨率指定引用不同的图像源 [2]
.foo {
background-image: image-set(
"https://www.baidu.com/img/flexible/logo/pc/result@2.png" 1x,
"https://www.baidu.com/img/flexible/logo/pc/result@2.png" 2x,
"https://www.baidu.com/img/flexible/logo/pc/result@2.png" 600dpi
);
}
...
// become
.foo {
background-image: https://www.baidu.com/img/flexible/logo/pc/result@2.png;
}
@media (-webkit-min-device-pixel-ratio: 2), (min--moz-device-pixel-ratio: 2), (min-resolution: 192dpi){
.foo {
background-image: https://www.baidu.com/img/flexible/logo/pc/result@2.png;
}
}
@media (-webkit-min-device-pixel-ratio: 6.25), (min--moz-device-pixel-ratio: 6.25), (min-resolution: 600dpi){
.foo {
background-image: https://www.baidu.com/img/flexible/logo/pc/result@2.png;
}
}
- CSS逻辑属性 [2]
CSS逻辑属性,指的是*-start,*-end 以及 *-inline-start,*-inline-end,*-block-start, *-block-end 这类CSS属性,其最终渲染的方向是有逻辑性在里面的,是与padding-left这种有明确方向的属性相对。这个css逻辑属性有很多值,具体可以参看w3c标准
p:first-child {
float: inline-start;
margin-inline-start: 10px; // 可以等同理解成 margin-left
}
margin-inline和margin-block 是 CSS 逻辑属性,前者是 margin-inline-start 和 margin-inline-end的缩写,后者是 margin-block-start 和 margin-block-end 的缩写。margin-inline 指的是水平方向的 margin 控制,而 margin-block 指的是垂直方向的 margin 控制
:matches匹配伪类,可以一次设置多个属性值 [2]
p:matches(:first-child, .special) {
margin-top: 1em;
}
...
p:first-child, p.special {
margin-top: 1em;
}
:not否定伪类,设置不在当前范围 [2]
p:not(:first-child, .special) {
margin-top: 1em;
}
...
p:not(:first-child):not(.special) {
margin-top: 1em;
}
- 使用更为容易理解的方式定义媒体查询的范围 [3]
@media (width < 480px) {
b { font-size: 14px}
}
@media (480px <= width < 768px) { // 这么写比下面的写法更易理解
b { font-size: 18px}
}
@media (width >= 768px) {
b { font-size: 20px}
}
...
@media (max-width: 479px) {
b {
font-size: 14px;
}
}
@media (min-width: 480px) and (max-width: 767px) {
b {
font-size: 18px;
}
}
@media (min-width: 768px) {
b {
font-size: 20px;
}
}
overflow针对overflow-x和overflow-y的简写 [2]
html {
overflow: hidden auto;
}
...
html {
overflow-x: hidden;
overflow-y: auto;
}
overflow-wrap设置或检索当内容超过指定容器的边界时是否断行 [2]- normal:允许内容顶开或溢出指定的容器边界。
- break-word:内容将在边界内换行。如果需要,单词内部允许断行。
CSS3中将 word-wrap 改名为 overflow-wrap;所以这个会监控你的浏览器兼容范围,低版本浏览器会将 overflow-wrap 的值,同步设给 word-wrap
- css grid 中
place-属性的简写形式 [2]place-items属性是align-items属性和justify-items属性的合并简写形式place-content属性是align-content属性和justify-content属性的合并简写形式place-self属性是align-self属性和justify-self属性的合并简写形式
.example {
place-content: flex-end;
place-items: center / space-between;
place-self: flex-start / center;
}
...
.example {
-ms-flex-line-pack: end;
align-content: flex-end;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
justify-items: /, space-between;
-ms-flex-item-align: start;
align-self: flex-start;
justify-self: /, center;
}
- rebeccapurple 一个特殊的颜色值 [2]
html {
color:rebeccapurple ; // #663399
}
- system-ui 匹配通用字体 [2]
body {
font-family: system-ui;
}
stage 1 的css语法
因为设定的stage为2,如果要启用需要 stage 1的语法,要在features配置中开启。对应的 id 可以通过点击相关 api,查看 URL 对应的hash,比如 https://preset-env.cssdb.org/features#custom-media-queries,对应的id就是custom-media-queries,有感觉对不上的可以去对应github的js文件中查看
postcssPresetEnv({
stage: 2,
features: {
'nesting-rules': true,
'custom-media-queries': true,
'custom-selectors': true
}
})
- custom-media-queries (相关ID)
- 自定义媒体查询
@custom-media --small-viewport (max-width: 30em);
@media (--small-viewport) {
h1 {font-size: 16px}
}
...
// 转码为
@media (max-width: 30em) {
h1 {font-size: 16px}
}
最大最小宽度,可以使用>= <=代替
@custom-media --small-viewport (width >= 500px) and (width <= 1200px);
@media (--small-viewport) {
h1 {font-size: 16px}
}
...
// 转为
@media (min-width: 500px) and (max-width: 1200px) {
h1 {font-size: 16px}
}
- custom-selectors
- 自定义选择器
CSS 扩展规范(CSS Extensions)中允许创建自定义选择器,可以使用@custom-selector”来定义自定义选择器
@custom-selector :--heading h1, h2, h3, h4, h5, h6;
:--heading {
font-weight: bold;
}
运行后变为
h1,h2,h3,h4,h5,h6 {
font-weight: bold;
}
- nesting-rules
- 嵌套,减少重复的选择器声明,通过两种方式进行嵌套:第一种方式要求嵌套的样式声明使用
&作为前缀,&只能作为声明的起始位置;第二种方式的样式声明使用@nest作为前缀,并且&可以出现在任意位置
- 嵌套,减少重复的选择器声明,通过两种方式进行嵌套:第一种方式要求嵌套的样式声明使用
// 嵌套只能使用&开头,除非前缀有@nest
.message {
font-weight: normal;
& .header {
font-weight: bold;
}
@nest .body & {
color: black;
}
}
...
.message {
font-weight: normal
}
.message .header {
font-weight: bold;
}
.body .message {
color: black
}
- prefers-color-scheme-query
- 暗黑模式下媒体查询
- light 浅色主题
- dark 暗色主题
- 暗黑模式下媒体查询
body {
background-color: white;
color: black;
}
@media (prefers-color-scheme: dark) { // 暗黑模式下面代码生效
body {
background-color: black;
color: white;
}
}
cssnano
webpack4+ 的版本,已经集成了cssnano,mode 设为生产模式就会自动启用这个插件
npm install --save-dev cssnano
// 如果单独使用,可以这么配置
module.exports = {
plugins: [
require('cssnano')({
preset: 'default',
}),
],
};
使用 postcss 实现一个 sass
postcss 可自主定义相关插件的使用,组合出适合自己使用的功能,借助 postcss 的插件来实现一个类 sass(大体功能)
相关插件介绍
- postcss
- 基础功能包
- postcss-cli
- 提供了终端运行的能力
- postcss-advanced-variables
- 提供嵌套类似 sass 的
变量、@if、@else、@for、@each、@mixin、@include、@content
- 提供嵌套类似 sass 的
- postcss-scss
- 可以正常使用 sass 中的
#{$var-name}变量形式
- 可以正常使用 sass 中的
- postcss-apply
- postcss-assets
- postcss-import
- 使在文件中可以使用 @import 引入
.pcss文件
- 使在文件中可以使用 @import 引入
- postcss-preset-env
@import
使用 postcss-import 插件
const atImport = require("postcss-import");
module.exports = {
plugins: [
atImport(),
元素嵌套
使用 postcss-preset-env 插件 stage:1 中的 nesting-rules
const PostCssEnv = require("postcss-preset-env");
const postCssEnvConfig = PostCssEnv({
stage: 2,
features: {
"nesting-rules": true
}
});
具体写法上和sass嵌套的写法有些不一样,不过这种写法是未来css的标准,以这个为主要写法
变量、@if、@else、@for、@each、@mixin、@include、@content
使用 postcss-advanced-variables 这个插件,这个插件能实现sass变量的大部分功能,不过一个特殊的语法#{$var-name}需要使用到 postcss-scss 这个插件,并且在postcss.config.js 中设置 parser 为 postcss-scss
const advanced = require("postcss-advanced-variables");
module.exports = {
parser: "postcss-scss",
plugins: [
advanced(),
...
变量
$font-size: 1.25em;
$font-stack: "Helvetica Neue", sans-serif;
$primary-color: #333;
body {
font: $font-size $(font-stack);
color: #{$primary-color};
}
...
// 转为
body {
font: 1.25em "Helvetica Neue", sans-serif;
color: #333;
}
@if、@else
$type: monster;
p {
@if $type == ocean {
color: blue;
} @else {
color: black;
}
}
...
// 转为
p {
color: black
}
@for
@for $i from 1 through 5 by 2 {
.width-#{$i} {
width: #{$i}0em;
}
}
...
// 转为
.width-1 {
width: 10em;
}
.width-3 {
width: 30em;
}
.width-5 {
width: 50em;
}
@each
@each $animal in (red, yellow, black, white) {
.#{$animal}-icon {
color: #{$animal};
}
}
...
// 转为
.red-icon {
color: red;
}
.yellow-icon {
color: yellow;
}
.black-icon {
color: black;
}
.white-icon {
color: white;
}
@mixin @include
@mixin heading-text {
color: #242424;
font-size: 4em;
}
h1, h2, h3 {
@include heading-text;
}
...
// 转为
h1,
h2,
h3 {
color: #242424;
font-size: 4em;
}
属性嵌套
postcss-nested-props
const nestedProps = require("postcss-nested-props");
module.exports = {
plugins: [
nestedProps(),
.funky {
font: {
family: fantasy;
size: 30em;
weight: bold;
}
}
...
// 转为
.funky {
font-family: fantasy;
font-size: 30em;
font-weight: bold
}
Extend/Inheritance(扩展/继承)
postcss-extend
const extend = require("postcss-extend");
module.exports = {
plugins: [
extend(),
// 这里和sass有点不同,使用 @define-placeholder 而非 % 导出样式块
@define-placeholder message-shared {
border: 1px solid #ccc;
padding: 10px;
color: #333;
}
.message {
@extend message-shared;
}
.error {
border: 1px #f00;
background-color: #fdd;
}
.seriousError {
@extend .error;
border-width: 3px;
}
...
// 转为
.message {
border: 1px solid #ccc;
padding: 10px;
color: #333;
}
.error, .seriousError {
border: 1px #f00;
background-color: #fdd;
}
.seriousError {
border-width: 3px;
}
添加calc()计算
postcss-calc
const calc = require("postcss-calc");
module.exports = {
plugins: [
calc(),
@mixin columns_calc($count) {
width: calc(100% / $count);
@if $count > 1 {
float: left;
}
}
.column_calculated {
@include columns_calc(2);
}
...
// 转为
.column_calculated {
width: 50%;
float: left;
}
添加了这些插件,基本就可以完全可以满足正常开发需求
完整配置如下:
const PostCssEnv = require("postcss-preset-env");
const AtImport = require("postcss-import");
const advancedVariables = require("postcss-advanced-variables");
const nestedProps = require("postcss-nested-props");
const extend = require("postcss-extend");
const calc = require("postcss-calc");
const postCssEnvConfig = PostCssEnv({
stage: 2,
features: {
"nesting-rules": true
}
});
module.exports = {
parser: "postcss-scss",
plugins: [
AtImport(),
advancedVariables(),
nestedProps(),
extend(),
calc(),
postCssEnvConfig
]
};
独立使用 postcss
可以通过 postcss-cli 这个插件构建一个独立使用的 css 处理平台,可在 package.json 中添加如下命令
"scripts": {
"build:pc": "npx postcss src/main.pc.css -o dist/main.pc.css",
"build:h5": "npx postcss src/main.h5.css -o dist/main.h5.css"
// src/main.pc.css
@import 'normalize.css';
@import 'reset.css';
@import 'variables.css';
@import 'common.css';
@import 'common.mixin.css';
@import 'layout.mixin.css';
@import 'layout.css';
@import 'layout-flex.css';
@import 'button.mixin.css';
@import 'button.css';
@import 'button-group.css';
@import 'skeleton.mixin.css';
@import 'skeleton.css';
@import 'breadcrumb.css';
@import 'dropdown.css';
@import 'menu.css';
@import 'pagination.mixin.css';
@import 'pagination.css';
@import 'step.css';
@import 'checkbox.css';
@import 'cascader.css';
@import 'form.css';
@import 'calendar.css';
@import 'input-number.css';
@import 'rate.css'
这里由一个统一入口文件,控制针对PC,H5不同的样式表的输出,在入口文件中,可以设置引入不同的变量(比如上面例子中的 variables.css 正常开发中,可以细化成variables.pc.css或者variables.orange.css 这类的样式),让换肤类的功能也比较容易实现
避免编译器对pcss文件报错
我使用的是我使用的是vscode,引用插件后写的样式,编译器基本是不认识的,会给你报错,所以这里说下如何避免对pcss的报错,其他编译器应该也有类似的方法
对vscode添加对.pcss文件的支持
- 安装postcss-sugarss-language插件
- 进入setting,搜索 files.associations ,打开 settings.json 其中添加如下内容
"files.associations": {
"*.css": "postcss"
},
"postcss.validate": false // 避免检查器对 pcss 进行检查
css module
css module 和 css in js 都是社区针对 css 作用域提出的解决方案,从实际开发过程中,感觉 css module 的方式,更适合分工合作的要求。
现在开发中,不管你使用什么框架,基本的流程大概都是:先做静态页面,再接动态数据。这里很容易就会形成两条工作线:一类专门进行重构,与设计师、交互对接完成视觉实现;另一类专门与产品经理、后端对接业务需求(体量大的公司会拆分成两个工种,小公司自然是全干了,但是工作场景多半如此)。
css in js 的方式,更符合独立组件的封装型。从样式、逻辑到内容展现,在一个 js 中都实现,不需要再引入其他文件,如果是写 ui 组件这种方式是极好的。
但是如果是正常的开发需求,你用这种方式,就表示样式问题的改动、修复,业务逻辑的实现,都是在一个文件中进行,不利于分工合作。
个人感觉 css in js 方式会让 js 文件变得混乱,可读性下降。而且没办法单独输出样式文件,没办法把样式文件丢在某个 cdn 服务器下。同时也会影响组件的复用性。如果要复用某个组件(业务组件)就表示要接受这个组件自带的样式,稍微调整点字号颜色啥的,就要单独加个接收参数,或者新建个类似 list-red 的新组件(如果是引入样式的方式,完全可以通过再引入这个组件的外层加个类似 <div class='red-list'><list /></div>,在外层样式文件中直接控制,也很容易扩展多个不同样式、换肤啥的)。
css in js 方式对重构人员也不友好,他们之前查看样式问题,直接修改样式文件部署后就能看出问题能不能解决,现在需要修改 js 文件等 js 文件部署后才能查看改动是否生效(如果碰到重构分支和你开发的分支不同,版本不同的情况,这就会变得很麻烦。。。)
启用
参考了 CSS Modules 用法教程
配合 css-loader 使用,在 webpack 配置项中如下设置:
{test: /\.pcss/, use: [MiniCssExtractPlugin.loader, 'css-loader?modules', 'postcss-loader']},
这样就开启了 css module,它的使用会把对应的样式名变成一种有规律,但不可预期的名称,比如
/// main.pcss
/// 由于样式文件本身并没有要求 .classname 不能重复,所以要自己保证不要重复,否则针对同一类名会转出多个变量名,在使用时会有问题
.box {
border-radius:5px;
}
.info {
color: red;
font-size: 24px;
}
...
/// app.vue
<template>
<div :class="$style.info">{{ msg }}</div>
</template>
import style from '../style/main.pcss';
...
computed: {
$style () {
return style;
}
}
$style的使用方式参考了 vue-loader 的 CSS Modules 方案,vue loader 会使用$style的计算属性,向组件注入 CSS Modules 局部对象
style 会输出为(value 值是一个动态值)
{box: "_2sI8WybUY_1NGPVWmXjdbV", info: "_hA0iOLbXZy9PpOuCjpkc"}
组件内直接使用其中的定义的样式名,会自动替换成这些名称,样式文件也会自动转成这些名称,这样可以解决 css 的样式冲突(污染)的问题,全局污染的问题,也可以算是解决依赖的问题,组件只需要引入自己相关的样式,在这个相关样式文件中定义自己需要使用的样式,再通过 $style 的形式给相关组件使用。
vue 的写法稍微麻烦点(如果用 JSX 会简单些),如果是在 react 中可以直接使用
import style from '../style/main.css';
class App extends Component {
render () {
return <div className={style.info}>app info</div>
}
}
全局作用域
在引入的样式文件中,默认会对所有样式进行转换,如果你的样式只是想通过普通方式使用,可以有两种方案:
- 只针对
.pcss开启css module,.css不开启
/// webpack.config.js
{test: /\.css/, use: [MiniCssExtractPlugin.loader, 'css-loader']},
{test: /\.pcss/, use: [MiniCssExtractPlugin.loader, 'css-loader?modules', 'postcss-loader']}
...
/// global.css
body {
font-size: 18px;
background: #cccccc;
}
...
/// app.vue
import '../style/global.css';
import style from '../style/main.pcss'; // 这里虽然是分开导入,但是最终会合并和一个样式文件
...
/// main.css 导出的样式文件
body {
font-size: 18px;
background: #cccccc;
}
._2sI8WybUY_1NGPVWmXjdbV {
border-radius:5px;
}
._hA0iOLbXZy9PpOuCjpkc {
color: red;
font-size: 24px;
}
- 在待编译的样式文件使用
:global(.className)(也可以省略为:global .className)的写法,这样这个对应的样式也不会编译
:local 可以设置哪些需要转,因为默认就是转换,没必要再加一层说明其需要转换
**对于需要转换的,请一直使用 .classname,不要使用 id **
/// main.pcss
...
:global(body) {
font-size: 18px;
background: #cccccc;
}
两种方法相对,我感觉第一种更好些
Composing(组合) 及 Importing(导入)
- 组合的意义并不是把样式代码进行混合,而是在于使用了组合的类名,在引用时会包含其组合的子类
.box {
border-radius:5px;
}
.info {
color: red;
font-size: 24px;
}
.foo {
composes: box;
composes: info;
padding: 10px;
}
/// 经过转换后对应的json信息如下
box: "src-style-main__box--2sI8W",
info: "src-style-main__info--_hA0i",
foo: "src-style-main__foo--LiN2X src-style-main__box--2sI8W src-style-main__info--_hA0i",
/// 如果我们使用 $style.foo 还会包含其他两个子类的内容
composes还可以从其他样式模块中导入样式进行组合,这种使用方式,就不会同步包括混合的类(上面因为在一个文件中,必然会包含,而这种模式不一定会包含)
.foo {
composes: main-title header-title from './color.css';
padding: 10px;
}
@value
css 变量的解决方案很多,这个是 css-loader 的方案,并且是在开启 css modules 的情况下才能使用
官方有个命名建议:v- 定义value值, s- 选择器 m- 定义 media 规则
@value v-primary: #BF4040;
@value s-black: black-selector;
@value m-large: (min-width: 960px);
.header {
color: v-primary;
padding: 0 10px;
}
:global .s-black {
color: black;
}
@media m-large {
.header {
padding: 0 20px;
}
}
...
._2Q-2IY8XIGQEwnpoQr8qIF {
color: #BF4040;
padding: 0 10px;
}
.black-selector {
color: black;
}
@media (min-width: 960px) {
._2Q-2IY8XIGQEwnpoQr8qIF {
padding: 0 20px;
}
}
定制编译后的类名
css-loader 默认的哈希算法是 [hash:base64],转出的就是这种 _2sI8WybUY_1NGPVWmXjdbV,这个转换后的名称是可以定制的
// 参数比较长,再使用拼接的写法,看起来不美观
loader: 'css-loader',
options: {
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]'
} // 注意是在 modules 下设置 localIdentName
}
按官方建议
- 开发环境使用
[path][name]__[local]--[hash:base64:5] - 生产环境使用
[hash:base64]
这样转出的类名就类似 src-style-main__box--2sI8W,能看出路径和使用模块,方便定位样式问题所在的
css loader 参数
| 名称 | 类型 | 默认 | 描述 |
|---|---|---|---|
| url | {Boolean|Function} | true | 启用/禁止 url() 功能 |
| import | {Boolean|Function} | true | 启用/禁用 @import 处理 |
| modules | {Boolean|String|Object} | false | 启用/禁用 CSS Modules 以及相关配置 |
| sourceMap | {Boolean} | false | 启用/禁用 map 功能 |
| importLoaders | {Number} | 0 | 在 css-loader 之前使用多少个加载器(默认,别去动这个值,按 webpack 中的设置的 use 顺序去执行使用 loader) |
| localsConvention | {String} | 'asIs' | 导出的 JSON 对应的 Key 的规则 |
| onlyLocals | {Boolean} | false | 影响打包时的 loader 顺序,一些 SSR 场景下可以需要设置这个值,一般情况下别用 |
| esModule | {Boolean} | false | 是否启用 es module |
url
/// 先安装 url-loader 处理图片,并添加相关配置信息
{
test: /\.(png|svg|jpg|jpeg|gif)$/,
use: "url-loader"
},
...
/// main.pcss
.foo {
color: red;
font-size: 14px;
background: url("../image/timg.jpeg"); // 此处设定了背景图
}
按如上设置,转换出的 css 会对图片进行处理,但如果设置这个值为 false
{
loader: "css-loader",
options: {
modules: true,
url: false // 这个值默认是 true
}
},
转换出的 css 就不会对 url() 进行处理,原样输出
/// 不如处理 background: url("../image/timg.jpeg");
/// 这个 url 还可以设置为函数,在这里可以控制,只有指定图片名会转,进行差异化处理
url: (url, resourcePath) => {
// url ../image/timg.jpeg
// resourcePath - css 的绝对路径
// 不处理 `img.png` urls
if (url.includes('img.png')) {
return false;
}
return true;
},
import
这个值和 url 功能类似,只不过这里是控制文件的导入导出,这个值也一样可以设置 true 或 false,如果这样直接设置,那这肯定有问题,这个参数要么不设置,要设置一定是要对url进行过滤
/// @import "./color.css";
import: (parsedImport, resourcePath) => {
// parsedImport { url: './color.css', media: '' }
// parsedImport.url - `@import` 文件时对应的路径
// parsedImport.media - `@import` 媒体查询时对应的路径
// resourcePath - css 文件的绝对路径
// 包含 `style.css` 不进行合并处理
if (parsedImport.url.includes('style.css')) {
return false;
}
return true;
},
@import 是一个广泛被支持的 css 属性,大于 IE9 的浏览器就可以正常使用
modules
true 或 false 启用/禁止(还可以通过设置 modules 的值为 'local' 或 'global'启用/禁止) css modules
前面提到的 :global :local、Composing Importing 、@value、localIdentName 都是 modules 的配置信息,modules 可以接收一个对象进行其他配置
modules: {
mode: 'local', // 设置 local 启用 global 禁用 css 模块
exportGlobals: true,
localIdentName: '[path][name]__[local]--[hash:base64:5]', // 编译后的类名
context: path.resolve(__dirname, 'src'),
hashPrefix: 'my-custom-hash',
},
mode除了local global外,还有一个值是pure,使用这个值就要求样式文件中必须是纯的选择器,不能使用global local进行包裹
/// 在 mode 为 pure 时,会提示编译出错
:local(.zoo)
...
/// mode 还可以接受一个函数形式,对指定路径的样式文件进行特殊处理(什么文件开启转换,什么文件不开启),返回值只能是这三个值
mode: (resourcePath) => {
if (/pure.css$/i.test(resourcePath)) {
return 'pure';
}
if (/global.css$/i.test(resourcePath)) {
return 'global';
}
return 'local';
},
-
exportGlobals按描述应该是控制输出类名的名称,不过没没查出有什么特别的用处,设不设都不影响使用 -
context生成hash值相同,参考那个 GitHub bug 说明,是在某些情况下,生成的hash值出现重复的情况,然后借助这个参数,解决此问题,不过我试了很多情况,没试出来生成 hash 相同的情况 -
hashPrefix设定组自己的 hash 值规则,一般无须设置 -
getLocalIdent设置编译后的规则名,同localIdentName只不过这是个函数,能设置的更细,一般用localIdentName -
localIdentRegExp没试出干什么用
localsConvention
这个设定导出 JSON 时,key值和类名如何映射,默认值是 'asIs'
loader: 'css-loader',
options: {
modules: {
mode: 'local',
localIdentName: '[path][name]__[local]--[hash:base64:5]'
},
localsConvention: 'asIs'
}
...
.infoNews {
font-size: 35px;
}
.info-old {
color: white;
}
.info_dashes {
color: salmon;
}
.info {
color: red;
}
...
/// 默认 asIs,类名是啥,导出的就是啥
info: "src-components-page1-index__info--10GK2"
info-old: "src-components-page1-index__info-old--k163w"
infoNews: "src-components-page1-index__infoNews--1uma1"
info_dashes: "src-components-page1-index__info_dashes--FVVUn"
...
/// camelCase 驼峰,会把非驼峰的命名转为驼峰,并保留之前的类名
info: "src-components-page1-index__info--10GK2"
info-old: "src-components-page1-index__info-old--k163w"
infoDashes: "src-components-page1-index__info_dashes--FVVUn" // 值同 info_dashes
infoNews: "src-components-page1-index__infoNews--1uma1"
infoOld: "src-components-page1-index__info-old--k163w" // 值同 info-old
info_dashes: "src-components-page1-index__info_dashes--FVVUn"
...
/// camelCaseOnly 与驼峰类似,只不过不会保留非驼峰的转换
info: "src-components-page1-index__info--10GK2"
infoDashes: "src-components-page1-index__info_dashes--FVVUn"
infoNews: "src-components-page1-index__infoNews--1uma1"
infoOld: "src-components-page1-index__info-old--k163w"
...
/// dashes 一样也是转驼峰,只不过这里限制只转 ``-`` 号
info: "src-components-page1-index__info--10GK2"
info-old: "src-components-page1-index__info-old--k163w"
infoNews: "src-components-page1-index__infoNews--1uma1"
infoOld: "src-components-page1-index__info-old--k163w"
info_dashes: "src-components-page1-index__info_dashes--FVVUn" // 不转换
...
/// dashesOnly 不保留转换前的
info: "src-components-page1-index__info--10GK2"
infoNews: "src-components-page1-index__infoNews--1uma1"
infoOld: "src-components-page1-index__info-old--k163w"
info_dashes: "src-components-page1-index__info_dashes--FVVUn"
之所以会有这么个参数,是因为我们在使用 css modules 时,最终可能会动态绑定到某个组件上,这是为了方便在 js 中使用