开篇
闲来无事,开发一个css自动原子化工具玩玩。
本文仅为实现思路,如有改进地方希望各位积极评论交流。
背景
现有的css原子化方案对于开发者不算友好。
- 开发者很难记得所有原子化样式。
- 对于不常用的样式需要进行配置。
效果
打包效果对比
原子化前->原子化后 (gzip压缩之前,由于css原子化之后gzip压缩比率降低,导致效果没有压缩前明显)
![]()
![]()
以下为原子化完成后webpack打包出的代码片段
打包后部分js片段
![]()
打包后部分css片段
![]()
实现原理
- 处理js,使用babel将js转化为ast,对ast不同结构进行处理,将其转化为js树。
- 处理css,使用postcss将css转为ast,将其转化为css block。
- 合并,css规则匹配js上的class,进行替换。
处理JS
- 使用babel将js代码转为ast。
babel.parse(code);
- 对不同类型结构进行不同处理。
![]()
重点说下对函数结构和类函数的处理。
定义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
- 使用postcss将css转化为ast
function getCssAst(cssCnt) {
return postcss.parse(cssCnt).nodes;
}
- 循环ast结构,对每个css块进行处理,得到新的结构cssBlock,大致如下
{
oriSelector,
oriBlock,
handleSelector,
attr,
isEligible, // 判断这个css块是否可以被转换
}
- 对每个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; } }
- 最后的css block结构如下
{
oriSelector,
oriBlock,
handleSelector,
attr,
notConvert,
newCssSelector,
newCssBlocks,
}
合并
- 合并之前需要分配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); } } }) } };
- 深度遍历函数类型结构,每次遍历都需要记录父辈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); } }
- 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将不会被转换)。
- 获取新的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 }
结语
此次原子化之旅学到了不少知识。
希望以后分享更多有趣的东西。