## tinycss 功能
css文件被引入了那些html页面
css选择器在这些html页面的使用情况
最后生产压缩后的css、以及map
### 技术栈
glob、postcss、vue-template-compiler
### 开发准备
1、git clone本项目~
2、cnpm i
### 使用说明:
1、src目录放入html文件(一个以上)、css文件(一个以上)
2、npm run test //执行命令,输出dist目录
3、demo.css是压缩后的css文件,demo.map是矩阵数据(记录css的命中情况)
### 注意点
不支持所有伪类(除了:root),例如:div.class:first-child 等同于 div.class
### 不足:
不过滤动画选择器:keyframes
不支持去重,不支持选择器、属性去重
app,js
// 1、输入:所有的html、css, 遍历css文件。 // 2、拿到a.css,找出存在a.css的htmlFileArr、htmlTextArr数组。 // 3、将a.css、htmlFileArr、htmlTextArr放入TinyCss,生产优化后的a.css、a.map // 4、遍历2、3,得到优化后的所有css const TinyCss=require('./utils/TinyCss'); const fs=require('fs'); const path=require('path'); const glob=require('glob'); const mkdir=require('./utils/mkdir'); function getText(filepath){ return fs.readFileSync(filepath).toString(); } const srcDir='./src/'; //多个html文件 const htmlFileArr=glob.sync(srcDir+'**/*.html'); if(htmlFileArr.length==0){return;} const cssFileArr=glob.sync(srcDir+'**/*.css'); if(cssFileArr.length==0){return;} console.log(cssFileArr) const toCssFileArr=cssFileArr.map(function (filepath) { return filepath.replace(srcDir,'./dist/'); }) //统计 const vacancyArr=[]; function build(htmlFileArr2,htmlTextArr,cssFile,cssText){ //启动 const app=new TinyCss(htmlTextArr,cssText); //输出 const toText={ vacancy:null, nouseKeyFrames:app.getEmptyKeyFrames(), nouseCss:app.getEmptyCss(), htmlFileArr:htmlFileArr2, map:app.showMap(), } toText.vacancy=[toText.nouseCss.length,toText.map.length] console.log('build:'+cssFile.replace('./dist/',''),'无用selector比率',toText.nouseCss.length+"/"+toText.map.length,'页面引用率',htmlFileArr2.length+"/"+htmlFileArr.length) vacancyArr.push([cssFile.replace('./dist/',''),'无用selector比率',toText.nouseCss.length+"/"+toText.map.length,'页面引用率',htmlFileArr2.length+"/"+htmlFileArr.length]) mkdir(cssFile); fs.writeFileSync(cssFile,app.getTinyAst().toString()); fs.writeFileSync(cssFile.replace(/css$/,'map'),JSON.stringify(toText,null,2)) } cssFileArr.forEach(function (cssFile2,i) { const cssname=path.basename(cssFile2) const cssText2=getText(cssFile2) const htmlFileArr2=[] const htmlTextArr2=[] htmlFileArr.forEach(function (filepath,i) { const html=getText(filepath) if(html.indexOf(cssname)>-1){ htmlFileArr2.push(htmlFileArr[i]) htmlTextArr2.push(getText(htmlFileArr[i])) } }) build(htmlFileArr2,htmlTextArr2,toCssFileArr[i],cssText2) }); const tinyMap=vacancyArr.map(function (item) { return item.join(',') }) fs.writeFileSync('./dist/tiny.map',JSON.stringify(tinyMap,null,2));
TinyCss.js
//TinyCss.js const Api=require('./Api'); //解析成语法树 const compiler = require('vue-template-compiler'); const postcss = require('postcss'); const querySelectorList=require('./querySelectorList') //构建出一个css语法树和多个html语法书,分析css的使用率。 class TinyCss{ constructor(htmlTextArr,cssText){ //多个html书法树 this.htmlTextArr=htmlTextArr; //一个css书法树 this.cssAst=postcss.parse(cssText); this.cssList=Api.depthSearch(this.cssAst,'nodes').filter(function (node) { return node.type==='rule'&&!/keyframes/.test(node.parent.name); }) //输出的部分 this.bigMap=null; this.map=null; this.data=null; this.emptyCss=null; this.emptyKeyFrames=null; } //移除数组中的子元素 removeObj(item,arr){ for(let i=0;i<arr.length;i++){ if(arr[i]===item){ arr.splice(i,1) break; } } } //获取矩阵数据 getBigMap(){ if(this.bigMap){ return this.bigMap; } let map=[]; for(let i=0;i<this.htmlTextArr.length;i++){ const htmlAst=compiler.compile(this.htmlTextArr[i]).ast; const ccRect=new querySelectorList(htmlAst,this.cssList); const rect=ccRect.analysis(); map.push(rect) } this.bigMap=map; return map; } //获取小数据,矩阵数据 getMap(){ if(this.map){ return this.map; } let map=[]; for(let i=0;i<this.htmlTextArr.length;i++){ const htmlText=this.htmlTextArr[i]; const htmlAst=compiler.compile(htmlText).ast; const ccRect=new querySelectorList(htmlAst,this.cssList); const arr=ccRect.analysis().map(function (item) { return item.reduce((x,y)=>x+y); }); for(let j=0;j<arr.length;j++){ if(!map[j])map[j]=[]; map[j].push(arr[j]) } } this.map=map; return map; } //获取展示数据 showMap(){ const cssList=this.cssList; const map=this.getMap(); for(let i=0;i<map.length;i++){ map[i]=cssList[i].selector+","+map[i].join(','); } return map; } //显示无用的css getEmptyCss(){ if(this.emptyCss){ return this.emptyCss; } const cssList=this.cssList; const data=[]; const map=this.getMap(); for(let i=0;i<map.length;i++){ //存在比0大的就是用到的,都是0就是无用的css if(map[i].every(function (n) { return n===0 })){ //从ast中移除节点 this.removeObj(cssList[i],cssList[i].parent.nodes); data.push(cssList[i].selector); } } this.emptyCss=data; return data; } getEmptyKeyFrames(){ if(this.emptyKeyFrames){ return this.emptyKeyFrames; } const keyframesList=Api.depthSearch(this.cssAst,'nodes').filter(function (node) { return node.type==='atrule'&&/keyframes/.test(node.name); }) const vals=Api.depthSearch(this.cssAst,'nodes').filter(function (node) { return node.type==='decl'&&/animation/.test(node.prop); }) const delArr=keyframesList.filter(function (node) { return !vals.some(function (node2) { return node2.value.split(' ').indexOf(node.params)>-1 }) }) const emptyKeyFrames=[]; delArr.forEach( (node) =>{ //从ast中移除节点 this.removeObj(node,node.parent.nodes); emptyKeyFrames.push('@'+node.name+' '+node.params) }) this.emptyKeyFrames=emptyKeyFrames; return emptyKeyFrames; } getTinyAst(){ this.getEmptyCss(); this.getEmptyKeyFrames(); return this.cssAst; } } module.exports=TinyCss;
querySelectorList.js
const Api=require('./Api'); //命中规则 /*css rule矩阵,3*6 行对应selector['.id','.class1','.class2'] 列对应html节点 ['body','body div','body div div','body div p','body div span','body div span a'] [ [0,0,0,0,1,0], [0,0,0,0,1,0], [0,0,0,0,1,0] ] */ class querySelectorList{ constructor(htmlAst,cssList){ //记录selector查找历史 this.selectotCache={}; //构建html语法树和矩阵bitmap this.htmlAst=htmlAst; this.htmlList=Api.depthSearch(this.htmlAst).filter(function (node) { return node.type===1; }) //构建css语法树和矩阵bitmap this.cssList=cssList; } //分析 analysis(){ const cssList=this.cssList; const map=[] for(let i=0;i<cssList.length;i++){ map[i]=this.querySelector(cssList[i].selector); } return map; } //可能是多选择器 querySelector(selector){ if(/,/.test(selector)){ const arr=selector.split(','); const data=this.queryOneSelector(arr[0]); for(let i=1;i<arr.length;i++){ const item=this.queryOneSelector(arr[i]); for(let k=0;k<data.length;k++){ if(data[k]==0){ data[k]=item[k]; } } return data; } }else{ return this.queryOneSelector(selector) } } //查询css_rule,返回[array astNode] queryOneSelector(selector){ selector=selector.trim();//去掉左右空格 //解析css rule const selectorArr=[] selector.replace(/(.+?)([ >~\+]+(?!\d)(?! *:)|$)/ig,function (m,p1,p2) { selectorArr.push(p1,p2); }) // console.log(selectorArr) this.selectorArr=selectorArr; // console.log(selectorArr) //设置缓存 let preSelector=''; for(let i=0;i<selectorArr.length;i=i+2){ const exec=selectorArr[i-1]||''; const curSelector=selectorArr[i]; this.setSelectotCache(preSelector,exec,curSelector); preSelector=preSelector+exec+curSelector } const arr=new Array(this.htmlList.length).fill(0); // if(/ ::/.test(selector)) // console.log(selector,selectorArr) this.selectotCache[selector].forEach( (node) =>{ arr[this.htmlList.indexOf(node)]=1; }) return arr; } //记录selector查询html语法树 setSelectotCache(preSelector,exec,curSelector){ const nextSelector=preSelector+exec+curSelector; //已有缓存 if(this.selectotCache[nextSelector]){return;} if(!preSelector&&!exec){ this.selectotCache[curSelector]=this.breadthHit(curSelector,this.htmlAst) return; } const arr=this.selectotCache[preSelector]; this.selectotCache[nextSelector]=[]; if(/^ +$/.test(exec)){ arr.forEach((node)=>{ this.selectotCache[nextSelector]=this.selectotCache[nextSelector].concat(this.breadthHit(curSelector,node)); }) }else if(/^ *> *$/.test(exec)){ arr.forEach((node)=>{ this.selectotCache[nextSelector]=this.selectotCache[nextSelector].concat(this.childHit(curSelector,node)); }) }else if(/^ *\+ *$/.test(exec)){ arr.forEach((node)=>{ this.selectotCache[nextSelector]=this.selectotCache[nextSelector].concat(this.sublingHit(curSelector,node)); }) }else if(/^ *~ *$/.test(exec)){ arr.forEach((node)=>{ this.selectotCache[nextSelector]=this.selectotCache[nextSelector].concat(this.sublingsHit(curSelector,node)); }) }else{ console.log('exec异常:'+exec) } } //css_rule:element+element sublingHit(tag,astNode){ if(!astNode.parent){ return [astNode].filter( (node) =>{ return this.hitNode(tag,node); }) } return Api.nextSublingSearch(astNode,astNode.parent).filter( (node) =>{ return this.hitNode(tag,node); }) } //css_rule:element~element sublingsHit(tag,astNode){ return Api.nextSublingsSearch(astNode,astNode.parent).filter(function (node) { return this.hitNode(tag,node); }) } //css_rule:element element breadthHit(tag,astNode){ return Api.breadthSearch(astNode).filter( (node)=> { return node.type===1&&this.hitNode(tag,node); }) } //css_rule:element>element childHit(tag,astNode){ return Api.childSearch(astNode).filter( (node)=> { return node.type===1&&this.hitNode(tag,node); }) } //tag是否命中ast节点,返回true、false hitNode(selector,astNode) { //分割字符串 (tag)、(id、class)(val) if(selector==='*'){ return true; }else if(/:root/.test(selector)){ return astNode.tag==='html'; }else{ const arr=[]; //tag if(/(^[a-z]+)/i.test(selector)){ const tag=RegExp.$1; arr.push(astNode.tag===tag) } //class if(/\.([\w-]+)/.test(selector)){ const val=RegExp.$1; arr.push(astNode.attrsMap.class&&astNode.attrsMap.class.split(' ').indexOf(val)>-1); } //id if(/#(\w+)/.test(selector)){ const val=RegExp.$1; arr.push(astNode.attrsMap.id===val); } //属性 if(/\[([\w-]+)(~=|=||=)?(\w+)?\]/.test(selector)){ const key=RegExp.$1; const exec=RegExp.$2; const val=RegExp.$3; // console.log(selector,'属性选择器,只判断是否存在属性') arr.push(astNode.attrsMap[key]===true); } //伪类选择器 if(/(\:.+)/.test(selector)){ const key=RegExp.$1; // console.log(selector,'解析->',selector.replace(/\:.+$/,'')) // arr.push(true) // arr.push(astNode.attrsMap.id===val); } if(arr.length==0){ // console.log(this.selectorArr) console.log(selector,this.selectorArr,'css 解析异常') } return arr.every((item)=>item); } } } module.exports=querySelectorList;
//Api.js const treeSearch=require('./treeSearch'); //遍历子节点 function childSearch(node,childProp='children'){ return node[childProp]; } //遍历兄弟节点 function nextSublingsSearch(node,pnode,childProp='children'){ const parr=pnode[childProp].filter((node)=>{ return node.type===1 }); return parr.slice(parr.indexOf(node)+1); } //遍历下一个兄弟节点 function nextSublingSearch(node,pnode,childProp='children'){ return nextSublingsSearch(node,pnode).slice(0,1); } module.exports={ childSearch, nextSublingsSearch, nextSublingSearch, ...treeSearch }
//treeSearch.js //广度遍历html节点 function breadthSearch(item, childProp='children'){ const nodeList=[item] let index=0; while (index<nodeList.length){ const node=nodeList[index++]; if(node[childProp]){ for(let k in node[childProp]){ nodeList.push(node[childProp][k]); } } } return nodeList; } //深度遍历html节点 function depthSearch(node,childProp='children'){ const nodeList=[] const depthEach=function(item){ nodeList.push(item); if(item[childProp]){ for(let k in item[childProp]){ depthEach(item[childProp][k]); } } } depthEach(node); return nodeList; } module.exports={ breadthSearch,depthSearch }