React css自动原子化

1,079 阅读3分钟

开篇

闲来无事,开发一个css自动原子化工具玩玩。

本文仅为实现思路,如有改进地方希望各位积极评论交流。

背景

现有的css原子化方案对于开发者不算友好。

  1. 开发者很难记得所有原子化样式。
  2. 对于不常用的样式需要进行配置。

效果

打包效果对比

原子化前->原子化后 (gzip压缩之前,由于css原子化之后gzip压缩比率降低,导致效果没有压缩前明显)

以下为原子化完成后webpack打包出的代码片段

打包后部分js片段

打包后部分css片段

实现原理

  1. 处理js,使用babel将js转化为ast,对ast不同结构进行处理,将其转化为js树。
  2. 处理css,使用postcss将css转为ast,将其转化为css block。
  3. 合并,css规则匹配js上的class,进行替换。

处理JS

  1. 使用babel将js代码转为ast。
babel.parse(code);
  1. 对不同类型结构进行不同处理。

重点说下对函数结构和类函数的处理。

定义varMap记录函数内部使用的变量和其值,使用returnArr记录函数可以能的返回。

function getFunctionStruc(node) {
    if (!node) return null;
    const obj = {
        varMap: new Map(),
        returnArr: [],
    }
    const {
        id,
        body
    } = node;
    getNodesStruc(body, obj);
    return {
        ...obj,
        name: getNodesStruc(id, obj),
        type: node.type,
    };
}

处理CSS

  1. 使用postcss将css转化为ast
function getCssAst(cssCnt) {
    return postcss.parse(cssCnt).nodes;
}
  1. 循环ast结构,对每个css块进行处理,得到新的结构cssBlock,大致如下
{
   oriSelector,
   oriBlock,
   handleSelector,
   attr,
   isEligible,  // 判断这个css块是否可以被转换
}
  1. 对每个cssBlock attr进行处理,转化为原子化形式, 并将新的class 添加到cssblock对象上。

css 属性原子化

// {height: 20px, width: 20px} -> _hx{height: 20px}, _hy{width: 20px}
function getHashCnt(cnt, length = 2, cssMap) {
  const hashCnt = '_' + hash(cnt).str().substring(0, length);
  if (cssMap[hashCnt] && cssMap[hashCnt] !== cnt) {
      return getHashCnt(cnt, length + 1, cssMap);
  } else {
      cssMap[hashCnt] = cnt;
      return hashCnt;
   }
}
  1. 最后的css block结构如下
{
   oriSelector,
   oriBlock,
   handleSelector,
   attr,
   notConvert,
   newCssSelector,
   newCssBlocks,
}

合并

  1. 合并之前需要分配function的owner

如果使用的变量在此function的varMap中没有找到,则需要去父辈找

大致逻辑如下

function assignOwner(obj) {
    const {
        varMap
    } = obj;
    if (varMap instanceof Map) {
        varMap.forEach((value, key) => {
            if (value) {
                if (functionType.includes(value.type)) {
                    value.parent = obj;
                    assignOwner(value);
                }
            }
        })
    }
};
  1. 深度遍历函数类型结构,每次遍历都需要记录父辈class,这样形成chain来匹配css规则。

大致逻辑如下

const recursion = (returnArr, parentClsChain = [], owner) => {
    for (const returnItem of returnArr) {
        const htmlItemStruc = getHtmlStruc(returnItem, owner);
        const {
            attr,
            child,
        } = htmlItemStruc;
        let realAttrs = getAttrList(attr, owner)
        matchCssObj({
            parentClsChain,
            realAttrs,
            cssBlockList,
        });
        const clsClain = attrList.map((v) => v.value);
        recursion(child, clsClain.length ? [...parentClsChain, clsClain] : parentClsChain, owner);
    }
}
  1. class chain和css规则进行匹配

匹配规则: 判断cssArr(css 规则)是否为class chain的有序子集

function isMatch(classArr, cssArr) {
    let cssArrIndex = cssArr.length - 1;
    let classArrIndex = classArr.length - 1;
    while (cssArrIndex >= 0) {
        if (!classArr[classArrIndex]) return false;
        const cssArrList = cssArr[cssArrIndex];
        const classArrList = classArr[classArrIndex];
        if (cssArrList.filter((v) => classArrList.includes(v)).length === cssArrList.length) {
            cssArrIndex -= 1;
        }
        classArrIndex -= 1;
    }
    return true;
}

如果匹配成功,记录需要替换的class位置,并为匹配成功的css block打一个标记(后面没有标记的css block将不会被转换)。

  1. 获取新的css和js内容

获取新的css内容。

循环cssblocklist, 对可以转换与不能转换的进行不同处理。

function getnewCssFileCnt(cssBlockList) {
    let cnt = '';
    const newCssBlock = {};
    for (let i = 0; i < cssBlockList.length; i++) {
        const {
            oriSelector,
            oriBlock,
            notConvert,
            matched,
            newCssBlocks,
            isAnimation,
            isEligible
        } = cssBlockList[i];
        if (!matched || isAnimation || !isEligible) {
            cnt += oriBlock;
            continue
        }
        if (notConvert) {
            cnt += `${oriSelector}{${notConvert}}`;
        }
        for (let newCssBlockSelector of Object.keys(newCssBlocks)) {
            const newCssBlockAttr = newCssBlocks[newCssBlockSelector];
            if (!newCssBlock[newCssBlockSelector]) {
                newCssBlock[newCssBlockSelector] = newCssBlockAttr
            }
        }
    }
    cnt += cssObjToRealCss(newCssBlock);
    return cnt;
}

获取新的js内容

使用babel循环一遍,匹配记录的loc,修改内容,然后直接生成code

babel.traverse(ast, {
    enter(path) {
        for (let modifyItem of modifyItemList) {
            if (path.node.loc === modifyItem.loc) {
                path.node.value = modifyItem.newCls;
            }
        }
    }
});
const getNewCode = (ast) => {
    const {
        code: newCode
    } = babel.transformFromAst(ast);
    return newCode
}

结语

此次原子化之旅学到了不少知识。

希望以后分享更多有趣的东西。