## 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
### 不足:
不支持去重,不支持选择器、属性去重
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;
}
getUiMap(selector){
if(this.uiMap){
return this.uiMap;
}
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 uiArr=ccRect.querySelectorAndChild(selector)
const arr=ccRect.analysis().map(function (item) {
let index=0;
for(let k=0;k<item.length;k++){
if(item[k]===1&&uiArr[k]===1){
index++;
}
}
return index;
});
for(let j=0;j<arr.length;j++){
if(!map[j])map[j]=[];
map[j].push(arr[j])
}
}
this.uiMap=map;
return map;
}
//移除无用的css
getEmptyCss(selector){
if(this.emptyCss){
return this.emptyCss;
}
const cssList=this.cssList;
const data=[];
const map=selector?this.getUiMap(selector):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;
}
//移除注释
removeComment(){
const commentArr=Api.depthSearch(this.cssAst,'nodes').filter(function (node) {
return node.type==='comment';
})
commentArr.forEach((node)=>{
this.removeObj(node,node.parent.nodes);
})
}
getTinyAst(selector){
this.getEmptyCss(selector);
this.getEmptyKeyFrames();
this.removeComment();
return this.cssAst;
}
}
module.exports=TinyCss;
querySelectorList.js
//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;
}
//获取选择器和它得子元素
querySelectorAndChild(selector){
const arr=this.querySelector(selector);
for(let i=0;i<arr.length;i++){
if(arr[i]===1){
const cLen=Api.depthSearch(this.htmlList[arr[i]]).filter(function (node) {
return node.type===1;
}).length;
for(let k=1;k<cLen;k++){
i++;
arr[i]=1;
}
}
}
return arr;
}
//可能是多选择器
querySelector(selector){
if(/,/.test(selector)){
const arr=selector.split(',');
const data=[];
for(let i=0;i<arr.length;i++){
const item=this.queryOneSelector(arr[i]);
for(let k=0;k<item.length;k++){
if(item[k]===1){
data[k]=1;
}else{
data[k]=0;
}
}
}
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.hasOwnProperty(key));
}
//伪类选择器
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 }