最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手寫一個簡單的CSS預處理器

    正文概述 掘金(式溪)   2020-11-28   517

    看拉勾教育的 前端高手进阶 教人怎样写CSS预处理器,觉得学到不少知识,打算写篇笔记记录下来。

    目标

    CSS预处理器要实现的功能如下:

    1. 用空格和换行符替代花括号丶冒号和分号;
    2. 支持选择器的嵌套组合;
    3. 支持以“$”符号开头的变量定义和使用。

    举一个例子以更好了解要实现的功能。我们希望能把下面的样式表:

    $ib inline-block
    
    $borderColor lightgreen
    
    div
    
      p
    
        border 1px solid $borderColor
    
      color darkkhaki
    
      .a-b
    
        background-color lightyellow
    
        [data]
    
          padding 15px
    
          font-size 12px
    
    .d-ib
    
      display $ib
    
    

    转换成普通CSS样式表:

    div {color:darkkhaki;}
    
    div p {border:1px solid lightgreen;}
    
    div .a-b {background-color:lightyellow;}
    
    div .a-b [data] {padding:15px;font-size:12px;}
    
    .d-ib {display:inline-block;}
    

    因此我们需要编写一个 编译器,所谓编译器就是把一种语言转换为另一种语言,例如gcc把c语言转换为exe文档等。

    不同语言的编译器的工作流程有些差异,但大体上可以分成三个步骤:解析(Parsing)丶转换(Transformation)及代码生成(Code Generation)。

    解析: 词法分析

    解析分为两步,词法分析和语法分析。

    词法分析就是把字符串分成一个个单词,然後把它们分类,例如高级语言的编译器中,把单词分为变量,常量和运算符等等。这些分类好的单词叫 令牌 (token) 。 把样式表分类好的单词如下:

    {
      type: "variableDef" | "variableRef" | "selector" | "property" | "value", //枚举值,分别对应变量定义丶变量引用丶选择器丶属性丶值
      value: string, // token字符值,即被分解的字符串
      indent: number // 缩进空格数,需要根据它判断从属关系
    }
    

    现在的问题是要怎样用代码实现? 词法分析比较简单,把字符串拆分一个个单词,用正则表达式判断单词属於哪一类。当然,因为这个CSS预处理器比较简单,所以可以用正则表达式,如果类别非常多,可能需要用其他方式实现。 把判断条件列出来:

    1. variableDef,以 $ 符号开头,该行前面无其他非空字符串,即$hello: world$hello
    2. variableRef,以 $ 符号开头,该行前面有非空字符串,即 background-color: $hello$hello
    3. selector,独占一行,该行无其他非空字符串;
    4. property,以字母开头,该行前面无其他非空字符串,即 background-color: $hellobackground-color
    5. value,非该行第一个字符串,且该行第一个字符串为 property 或 variableDef 类型。

    实现代码如下:

    function tokenize(text) {
      // 去除多余的空格,逐行解析
      return text.trim().split(/\n|\r\n/).reduce((tokens, line, idx) => {
        // 计算缩进空格数
        const spaces = line.match(/^\s+/) || ['']  //匹配开头的空格
        const indent = spaces[0].length //找出匹配到的空格数
        // 将字符串去首尾空给
        const input = line.trim()
        // 通过空格分割字符串成数组
        const words = input.split(/\s/)
        let value = words.shift()
        // 选择器为单个单词
        if (words.length === 0) {
          tokens.push({
            type: 'selector',
            value,
            indent
          })
        } else { //不是单词
          // 这里对变量定义和变量引用做一下区分,方便后面语法分析
          let type = ''
          if (/^\$/.test(value)) { //单词首字符为 $
            type = 'variableDef'
          } else if (/^[a-zA-Z-]+$/.test(value)) {
            type = 'property'
          } else {
            throw new Error(`Tokenize error:Line ${idx} "${value}" is not a vairable or property!`)
          }
          tokens.push({
            type,
            value,
            indent
          })
          // 为了后面解析方便这里对变量引用和值进行区分
          while (value = words.shift()) {
            tokens.push({
              type: /^\$/.test(value) ? 'variableRef' : 'value',
              value,
              indent: 0
            })
          }
        }
        return tokens;
      }, [])
    }
    
    

    解析: 句法分析

    句法分析就是把之前的令牌之间的关系抽象表示出来,通常就是把它们转换成 抽象语法树 (AST),前端中使用AST最典型的例子就是 Babel。CSS的句法中,最需要关注的是 选择器的属性父子关系,因此需要rule和children表示:

    1. rules,存储当前选择器的样式属性和值组成的对象,其中值以字符串数组的形式存储;
    2. children,子选择器节点。

    AST的数据结构如下:

    {
      type: 'root',
      children: [{
        type: 'selector',
        value: string
        rules: [{
          property: string,
          value: string[],
        }],
        indent: number,
        children: []
      }]
    }
    

    以上是引用至课程的段落,文字有点复杂,代码更复杂 (=_=),我在代码里用注释慢慢解释,尽可能说清楚:

    // token 参数是之前解析的令牌
    function parse(tokens) {
      var ast = {  // ast是要生成的树,最初的根节点为root
        type: 'root',
        children: [],
        indent: -1
      };
      // 记录当前遍历路径
      let path = [ast]
      // 指针,指向上一个选择器结点
      let preNode = ast
      // 便利到的当前结点
      let node
      // 用来存储变量值的对象
      let vDict = {}
      while (node = tokens.shift()) {
        // 对于变量引用,直接存储到vDict中
        if (node.type === 'variableDef') {
          if (tokens[0] && tokens[0].type === 'value') {
            const vNode = tokens.shift()
            vDict[node.value] = vNode.value
          } else { // 如果不是变量定义,即为变量定义的值是对另一变量的引用
            preNode.rules[preNode.rules.length - 1].value = vDict[node.value]
            // 先把之前缓存变量对应的值取出,然後赋值给之前节点rules的值
          }
          continue;
        }
        // 对于属性,在指针指向的结点rules属性中添加属性
        if (node.type === 'property') {
          if (node.indent > preNode.indent) { // 当前节点的缩进空格大於之前节点的空格,即当前节点为子节点
            preNode.rules.push({
              property: node.value,
              value: []
            })
          } else { // 小於的情况,即不为子节点
            let parent = path.pop() // 需要找出当前节点的父节点
            while (node.indent <= parent.indent) {
              parent = path.pop()
            }
            parent.rules.push({
              property: node.value,
              value: []
            })
            preNode = parent
            path.push(parent) 
          }
          continue;
        }
        // 对于值,添加到value数组中
        if (node.type === 'value') {
          try {
            preNode.rules[preNode.rules.length - 1].value.push(node.value); // 然後赋值给之前节点rules的值
          } catch (e) {
            console.error(preNode)
          }
          continue;
        }
        // 对于变量引用,直接替换成对应的值,道理与之前 'variableDef'的情况类似
        if (node.type === 'variableRef') {
          preNode.rules[preNode.rules.length - 1].value.push(vDict[node.value]);
          continue;
        }
        // 对于选择器需要创建新的结点,并将指针
        if (node.type === 'selector') {
          const item = {
            type: 'selector',
            value: node.value,
            indent: node.indent,
            rules: [],
            children: []
          }
          if (node.indent > preNode.indent) { // 当前节点的缩进空格大於之前节点的空格,即当前节点为子节点
            path[path.length - 1].indent === node.indent && path.pop() // 缩进空格相等,即为兄弟节点
            path.push(item)
            preNode.children.push(item);
            preNode = item;
          } else { // 小於的情况,即不为子节点
            let parent = path.pop()
            while (node.indent <= parent.indent) {
              parent = path.pop()
            }
            parent.children.push(item)
            path.push(item)
          }
        }
      }
      return ast;
    }
    

    转换

    之後开始比较简单,在这个过程中,AST 中的节点可以被修改和删除,也可以新增节点。根本目的就是为了代码生成的时候更加方便。它算是一个 中转点,把AST转变成目标样式表的结构。数据结构如下:

    {
     selector: string,
     rules: {
       property: string,
       value: string
     }[]
    }[]
    

    实现代码主要方法是递回遍历AST,先把相应资料抽取,合并成新的对象,然後找一下有没有子节点,有的话,遍历子节点,递归调用函数。代码如下:

    function transform(ast) {
    let newAst = [];
    /**
     * 遍历AST转换成数组,同时将选择器和值拼接起来
     * @param node AST结点
     * @param result 抽象语法数组
     * @param prefix 当前遍历路径上的选择器名称组成的数组
     */
    function traverse(node, result, prefix) {
      let selector = ''
      if (node.type === 'selector') {
        selector = [...prefix, node.value]; //  把选择器合并
        result.push({
          selector: selector.join(' '),
          rules: node.rules.reduce((acc, rule) => {  
            acc.push({
              property: rule.property,
              value: rule.value.join(' ')
            })
            return acc;
          }, [])
        })
      }
      for (let i = 0; i < node.children.length; i++) {  // 有子节点,递归处理
        traverse(node.children[i], result, selector) 
      }
    }
    traverse(ast, newAst, [])
    return newAst;
    }
    

    代码生成

    把刚才转换的新的抽象语法结构变成目标代码:

    function generate(nodes) {
    // 遍历抽象语法数组,拼接成CSS代码
    return nodes.map(n => {
      let rules = n.rules.reduce((acc, item) => acc += `${item.property}:${item.value};`, '') // 先把对象rules属性变成css样式
      return `${n.selector} {${rules}}` // 返回数组形式的css样式表
    }).join('\n') 
    }
    

    起源地下载网 » 手寫一個簡單的CSS預處理器

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元