PostCSS

PostCSS

背景

在项目开发过程中,我们使用 rem 作为单位进行 CSS 样式开发。虽然 rem 用于适配各种型号的浏览器十分方便,但是它也有明显的缺点:

  1. 在把设计图上的尺寸转换为 rem 时,我们需要自己进行计算,或者给开发工具安装插件,让开发工具来帮我们进行计算。
    在这里插入图片描述

  2. 在修改和维护已经开发完成的代码时,面对如下代码:

    .container {
    	width: 2.66667rem;
    	height: 1.6rem;
    }
    

    我们很难直观地知道这个 CSS 样式设置的尺寸具体是多少。

那么有没有什么办法,能够让开发人员不用进行计算,只需要按照设计图上的尺寸写 px ,最终在浏览器渲染时,得到的是 rem 呢?

有!使用 PostCSS

1. 什么是 PostCSS ?

我们所说的 PostCSS 其实是由两部分组成的:PostCSS 工具和 PostCSS 插件。

PostCSS 工具有三个功能:

  • CSS 代码解析为抽象语法树 AST

  • 提供对 AST 进行操作的 API

  • AST 转换回 CSS

PostCSS 插件负责调用 API 来改变 AST

工具和插件协同工作,实现了对 CSS 代码的转换。

PostCSS 工作原理,如下图所示:
在这里插入图片描述

红色的 ParserStringifierPostCSS 工具的解析器和字符串化工具,负责 CSS 代码和 AST 之间的转换工作。

黑色的 Plugin 1Plugin 2PostCSS 插件,负责从 old ASTnew AST 的改变。

2. PostCSS 有什么用 ?

PostCSS 可以将 对开发人员友好的 CSS 代码转换为 对浏览器友好的 CSS 代码。

1. 提前使用先进的 CSS 特性。

CSS3 的新特性需要加入浏览器的私有前缀,才能被特定浏览器识别。例如:

.animate {
	transform: rotate(180deg);
  -ms-transform: rotate(180deg); /* IE 9 */
  -moz-transform: rotate(180deg); /* Firefox */
  -webkit-transform: rotate(180deg); /* Safari & Chrome */
  -o-transform: rotate(180deg); /* Opera */
}

每次遇到这样的属性,我们都要写一大段,非常麻烦。

使用 autoprefixer 插件,开发时只需要编写:

/* CSS input */
.animate {
	tranform: rotate(180deg);
}

/* CSS output */
.animate {
	transform: rotate(180deg);
  -ms-transform: rotate(180deg); /* IE 9 */
  -moz-transform: rotate(180deg); /* Firefox */
  -webkit-transform: rotate(180deg); /* Safari & Chrome */
  -o-transform: rotate(180deg); /* Opera */
}

PostCSS 会帮我们为属性加上针对不同浏览器的私有前缀。

2. 解决全局 CSS 问题。

项目的不同模块之间, CSS 选择器难免会出现命名冲突的情况,这会造成样式污染。~~~~

postcss-modules 插件,可以自动以组件为单位隔绝 CSS 选择器。

/* CSS input */
.name {
	color: gray;
}

/* CSS output */
.Logo__name__SVK0g {
  color: gray;
}

Vue 中,在 <style> 标签上加入 scoped 来隔绝组件之间的 CSS 样式,就是通过 PostCSS 来实现的。感兴趣的同学可以到 Vue Loader 官网 了解一下。

3. 避免 CSS 代码中的错误。

stylelint 插件,可以检测错误的 CSS 代码,并给予开发人员提示。 stylefmt 插件,可以根据 stylelint 规则自动优化 CSS 的格式。

/* CSS input */
a {
  color: #d3;
}
# console output
app.css
2:10 Invalid hex color

是不是很像 eslint

4. 其他

除了上面三种主要的功能, PostCSS 还可以把 CSS 代码转换成任意我们期望的形式,只要我们为它提供对应的插件。比如我们在背景中谈到的把 px 转换成 rem :

/* CSS input */
.container {
  width: 200px;
  height: 120px;
}

/* CSS output */
.container {
	width: 2.66667rem;
	height: 1.6rem;
}

还可以转换 @font-face 代码:

/* CSS input */
body {
   font-family: "Alice";
}

/* CSS output */
@font-face {
   font-family: "Alice";
   font-style: normal;
   font-weight: 400;
   src: local("Alice"), local("Alice-Regular"),
        url("http://fonts.gstatic.com/s/alice/v7/sZyKh5NKrCk1xkCk_F1S8A.eot?#") format("eot"),
        url("http://fonts.gstatic.com/s/alice/v7/l5RFQT5MQiajQkFxjDLySg.woff2") format("woff2"),
        url("http://fonts.gstatic.com/s/alice/v7/_H4kMcdhHr0B8RDaQcqpTA.woff")  format("woff"),
        url("http://fonts.gstatic.com/s/alice/v7/acf9XsUhgp1k2j79ATk2cw.ttf")   format("truetype")
}

以及转换资源路径:

/* CSS input */
body {
  background: url('foobar.jpg');
  background: url('icons/baz.png');
}

/* CSS output */
body {
  background: url('http://example.com/images/foobar.jpg');
  background: url('http://example.com/images/baz.png');
}

PostCSS 和不同的插件结合使用,可以成为 CSS 预处理器、后处理器、代码优化工具,或者其他任何工具。

非常灵活。

我们还可以根据需求,开发属于自己的 PostCSS 插件。

3. 怎么使用 PostCSS ?

PostCSS 官方提供了很多种使用方式,而我们最常见的方式是在 Webpack 中用于处理 CSS

  1. 安装 postcsspostcss-loader

    $ npm i -D postcss postcss-loader
    
  2. webpack.config.js 文件中加入配置:

    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/i,
            use: [
              'style-loader',
              'css-loader',
              {
                loader: 'postcss-loader',
                options: {
                  postcssOptions: {
                    plugins: [
                      [
                        'autoprefixer',
                        {
                          // 其他选项
                        },
                      ],
                    ],
                  },
                },
              },
            ],
          },
        ],
      },
    };
    
  3. 也可以使用 PostCSS 的配置文件:

    // postcss.config.js
    module.exports = {
      plugins: [
        [
          'autoprefixer',
          {
            // 其他选项
          },
        ],
      ],
    };
    
    // webpack.config.js
    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/i,
            use: ['style-loader', 'css-loader', 'postcss-loader'],
          },
        ],
      },
    };
    
  4. 这样 webpack 运行时就会使用 PostCSS 来处理 CSS 文件了。

4. PostCSS 原理

node 读取 CSS 文件内容,把内容交给 PostCSS

// node 的 fs 模块读取文件后,再传给 PostCSS
const css = fs.readFileSync("./index.css");
postcss([autoprefixer]).process(css);
1. 解析 CSS

在接到 CSS 代码之后,我们先把它以字符串格式存储起来:

this.css = css.toString();

然后我们需要对字符串进行扫描,把字符串解析成 token 格式:

// token
[
  'word', // 类型
  '.className', // 匹配的内容
  1, // 起始位置
  10 // 结束位置
]
  • 为什么要解析成 token 格式呢?

    因为字符串格式的数据操作起来麻烦而且效率低下,而 token 格式对计算机来说易于理解和操作。

  • 怎么才能解析成 token 格式呢?

    如果你对编译原理有所了解,那么就会知道,这个解析的过程被称为词法分析。

    词法分析:从左向右逐行扫描源程序的字符,识别出各个单词,确定单词的类型。将识别出的单词转换成统一的词法单元 token

那么如何进行词法分析呢?

假设有这样一段 CSS 代码,我们来对它进行词法分析:

.my-font { 
  color: #FFF; 
	font-size: 12px;
}

那么我们首先需要定义一些变量,用来存储扫描时的数据:

class Tokenizer {
  constructor(css) {
    this.css = css.toString(); // 输入的 css 代码
    this.length = css.length; // 字符串长度
    this.pos = 0; // 储存当前位置
    this.next; // 储存下一个位置
    this.code = ""; // 当前字符的 charCode
    this.currentToken; // 当前 token
    this.tokens = []; // 结果 tokens
  }
}

还需要声明一些变量,用来判断符号类型:

const NEWLINE = '\n'.charCodeAt(0); // 换行符
const SPACE = ' '.charCodeAt(0); // 空格
const TAB = '\t'.charCodeAt(0); // 制表符

const OPEN_CURLY = '{'.charCodeAt(0); // 左大括号
const CLOSE_CURLY = '}'.charCodeAt(0); // 右大括号

const SEMICOLON = ';'.charCodeAt(0); // 分号
const COLON = ':'.charCodeAt(0); // 冒号

以及查找符号的正则表达式:

const RE_WORD_END = /[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/g;

接下来,我们再规定几种 token 的类型:

space 空格、换行符、制表符
word 单词
string 字符串
brackets 括号 
; 分号
: 冒号

然后我们需要对 CSS 代码从左向右逐个字符进行扫描,一旦发现满足某一种 token 类型,那就把这段字符串截取,存储到 token 中,如下图所示:

Tokenizer

我们定义一个方法 nextToken 来实现识别 token 的功能:

nextToken() {
  if (this.pos >= this.length) return; // 如果指针位置超过文件长度,终止扫描
    
  this.code = this.css.charCodeAt(this.pos); // 当前字符
  // 识别不同类型的 token
  switch (this.code) {
    // 当前字符是空格、换行符、制表符时,存入类型为 space 的 token 中
    // 如果是连续的空格、换行符、制表符的情况,会一并截取,存入一个 token 中
    case NEWLINE:
    case SPACE:
    case TAB: {
      this.next = this.pos;
        
      do {
        this.next += 1;
        this.code = this.css.charCodeAt(this.next);
      } while (
        this.code === SPACE ||
        this.code === NEWLINE ||
        this.code === TAB
      )
  
      this.currentToken = ['space', this.css.slice(this.pos, this.next)];
      this.pos = this.next - 1;
      break;
    }
      
    // 当前字符如果是大括号,存入类型为 { 或者 } 的 token 中
    case OPEN_CURLY:
    case CLOSE_CURLY: {
      let controlChar = String.fromCharCode(this.code);
      this.currentToken = [controlChar, controlChar, this.pos];
      break;
    }
      
    default: {
      // 从当前位置到下一个符号之间的内容,存入类型为 word 的 token
      RE_WORD_END.lastIndex = this.pos + 1;
      RE_WORD_END.test(this.css);
        
      if (RE_WORD_END.lastIndex === 0) {
        this.next = this.css.length - 1
      } else {
        this.next = RE_WORD_END.lastIndex - 2
      }
  
      this.currentToken = ['word', this.css.slice(this.pos, this.next + 1), this.pos, this.next];
      this.pos = this.next;
        
      break;
      }
    }

    this.pos ++
    return this.currentToken
  }
}

最后定义一个 getTokens 方法,让它循环调用 nextToken 方法,直到文件结尾:

// 使用 while 循环,逐个获取 token ,然后保存到 tokens 中,直到文件结尾
getTokens() {
  let token;

  while (!this.endOfFile()) {
    this.nextToken();
    this.tokens.push(this.currentToken);
  }

  return this.tokens;
}

// 判断是否扫描到文件的结尾
endOfFile() {
  return this.pos >= this.length;
}

调用 getTokens 方法,我们就会得到 token 格式的数据:

[
  [ 'word', '.my-font', 0, 7 ],
  [ 'space', ' ' ],
  [ '{', '{', 9 ],
  [ 'space', ' \n  ' ],
  [ 'word', 'color', 14, 18 ],
  [ 'word', ':', 19, 19 ],
  [ 'space', ' ' ],
  [ 'word', '#FFF', 21, 24 ],
  [ 'word', ';', 25, 25 ],
  [ 'space', ' \n\t' ],
  [ 'word', 'font-size', 29, 37 ],
  [ 'word', ':', 38, 38 ],
  [ 'space', ' ' ],
  [ 'word', '12px', 40, 43 ],
  [ 'word', ';', 44, 44 ],
  [ 'space', '\n' ],
  [ '}', '}', 46 ],
  [ 'space', '\n' ]
]
2. 生成 AST

AST 是由一个个节点 Node 组成的树形数据结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qCDxo0vh-1646982702883)(./images/AST.png)]

在得到 token 格式的数据后,PostCSS 根据不同的 token 的类型,进行不同的处理,生成不同的 Node 节点。

parse() {
  let token;
  
  // 逐个获取 token
  while (!this.tokenizer.endOfFile()) {
    token = this.tokenizer.nextToken();
		
    // 不同的 token 类型,使用不同的处理方式。
    switch (token[0]) {			
      // 评论类型的 token , 生成 commet 节点
      case 'comment':
        this.comment(token);
        break;
			
      // @类型的 token , 生成 atrule 节点
      case 'at-word':
        this.atrule(token);
        break;
        
      // ...

      default:
        // rule 和 decl 节点,在 other 方法里生成
        this.other(token);
        break;
    }
  }
  
  this.endFile();
}

PostCSS 把节点分为 5 种类型:AtRule, Comment, Declaration, Root, Rule ,每种节点存储了详细的描述信息。以Rule 节点为例:

Rule {
  raws: { before: '', between: ' ', semicolon: true, after: ' ' }, // 符号信息
  type: 'rule', // 节点类型
  nodes: [ [Declaration] ], // 子节点列表
  parent: Root { // 父节点信息
  	raws: [Object],
    type: 'root',
    nodes: [Circular *1],
    source: [Object],
    [Symbol(isClean)]: false,
    [Symbol(my)]: true
	},
  source: { start: [Object], input: [Input], end: [Object] }, // 源代码信息
  selector: '.className', // 选择器
  [Symbol(isClean)]: false,
  [Symbol(my)]: true
}

并且 PostCSS 提供了操作 ASTAPI ,例如:walkRulesinsertBeforeremoveChild 等,详细文档可以查看: PostCSS API 文档

3. 调用插件

得到了完整的 AST 之后,PostCSS 会把它交给插件来处理:

// 用 parser 把 css 解析成 AST ,保存到 root
let root = parser(css, opts)

// 实例化一个 Result ,用来保存结果
this.result = new Result(processor, root, opts)

// 遍历插件列表
for(let i = 0; i < this.plugins.length; i ++) {
  let plugin = this.plugins[i]
  let promise = this.runOnRoot(plugin);
}

// 把 AST 交给插件处理
runOnRoot(plugin) {
  return plugin(this.result.root, this.result)
}

插件内部利用 APIAST 进行操作,我们以 postcss-pxtorem 为例:

css => {
  // 遍历 AST 的所有属性节点 decl
  css.walkDecls((decl, i) => {
    // 属性值中没有 px 的不处理
    if (decl.value.indexOf("px") === -1) return;
    
    // 有 px 的,把属性值换算为 rem 
    const value = decl.value.replace(pxRegex, pxReplace);
		decl.value = value;
  });
}
4. 字符串化

在插件处理完 AST 之后,PostCSS 使用 StringifierAST 转换回字符串:

// 注释节点,会在两端拼接 /* */
comment(node) {
  let left = this.raw(node, 'left', 'commentLeft')
  let right = this.raw(node, 'right', 'commentRight')
  this.builder('/*' + left + node.text + right + '*/', node)
}

// at选择器会在前面拼接@
atrule(name, semicolon) {
  let name = "@" + node.name;
  // ...
}

// 属性节点会根据情况在末尾拼接 ; 和 !important
decl(node, semicolon) {
  let between = this.raw(node, 'between', 'colon')
  let string = node.prop + between + this.rawValue(node, 'value')

  if (node.important) {
    string += node.raws.important || ' !important'
  }

  if (semicolon) string += ';'
  this.builder(string, node)
}

最后我们再把字符串写入一个 CSS 文件,就得到了新的 CSS 代码。

fs.writeFile("dest/app.css", result.css);
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值