如何用缩短的类名编译 sass
首先编译 sass,然后将其传递给自定义任务以重命名类。
我正在使用css 节点模块来解析css。
让我们从查看自定义 grunt 任务开始。
免责声明:我快速编写了这段代码,所以它可能还没有准备好生产。
var fs = require( 'fs' ),
rename = require( './rename.js' );
// Register the rename_css task.
grunt.registerMultiTask('rename_css', 'Shorten css class names', function () {
var options = this.options(); // Pass all options directly to css.parse
this.files.forEach(function ( file ) {
var renamed = rename.rename(
fs.readFileSync( file.src[ 0 ], 'utf8' ), options );
fs.writeFileSync( file.dest, renamed.text );
fs.writeFileSync( file.map, JSON.stringify( renamed.map, null, 2 ) );
});
});
此任务的配置如下所示:
grunt.initConfig({
rename_css: {
options: { compress: true }, // Minify the output css.
main: {
src: "style.css",
dest: "style.min.css",
map: "map.json"
}
}
});
rename.js 太长,无法在此处全部显示,但您可以在 github 上查看整个文件。这是主要功能:
function rename( s, options /* passed directly to css.parse */ ) {
/**
* Give the css classes short names like a-b instead of some-class
*
* Returns an object in the form {text: `newCss`, map: `partsMap`} whare text is
* the newly generated css and partsMap is a map in the {oldPart: newPart}.
*/
var
ast = css.parse( s, options ),
countMap = walkPass1( ast.stylesheet ), // Walk the first pass.
sortedCounts = [],
map = {}, // Final map.
part,
// List of charictor positions for the short class names.
// Each number corresponds to a charictor in the `chars` string.
charPosSet = [ 0 ];
// Unpack the count map.
for ( part in countMap ) {
sortedCounts.push({
name: part,
count: countMap[ part ],
replacment: undefined
});
}
// Sort based on the number of counts.
// That way we can give the most used classes the smallest names.
sortedCounts.sort(function( a, b ) { return b.count - a.count });
// Generate the small class names.
sortedCounts.forEach(function ( part ) {
var
s = '',
i = charPosSet.length;
// Build up the replacment name.
charPosSet.forEach(function ( pos ) {
s += chars[ pos ];
});
while ( i-- ) {
charPosSet[ i ]++;
// If the current char pos is greater then the lenght of `chars`
// Then we set it to zero.
if ( charPosSet[ i ] == chars.length ) {
charPosSet[ i ] = 0;
if ( i == 0 ) { // Time to add another digit.
charPosSet.push( 0 ); // The next digit will start at zero.
}
} else {
// Everything is in bounds so break the loop.
break;
}
}
part.replacment = s;
});
// Now we pack a basic map in the form of old -> new.
sortedCounts.forEach(function ( part ) {
map[ part.name ] = part.replacment;
});
// Walk the tree a second time actually renameing the classes.
walkPass2( ast.stylesheet, map );
return {
text: css.stringify( ast, options ), // Rebuild the css.
map: map
};
}
它看起来很复杂,但这里是它正在做的事情的分解:
- 解析 css 并获取抽象语法树 (ast)。
- 遍历树,创建类部分映射到计数(css 文档中出现的次数)。
- 将地图打包成一个数组并根据计数对其进行排序。
- 遍历数组创建缩短的类名。
- 以 oldName -> newName 的形式创建最终地图
- 第二次遍历树,实际上用新的类名替换旧的类名。
- 将编译后的 css 与生成的地图一起返回。
值得指出的是,这个函数会给更常用的类提供更短的名称,这将
导致 css 文件稍小。
如何在 php 中包含这些类名。
这可以通过输出缓冲区来完成。它可能看起来像这样(在页面顶部的根 html 标记之前):
<?php
define(DEV_MODE, false);
function build_class( $name, $map ) {
$parts = [];
foreach ( explode( '-', $name ) as $part ) {
$newPart = array_key_exists( $part, $map )? $map[ $part ] : $part;
array_push( $parts, $newPart );
}
return implode( '-', $parts );
}
function class_rename ( $content ) {
$string = file_get_contents( 'map.json' );
$classMap = json_decode( $string, true );
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false; // Remove unnesesary whitespace.
@$doc->loadHTML( $content );
foreach ( $doc->getElementsByTagName( '*' ) as $elem ) {
$classStr = $elem->getAttribute( 'class' );
if ( ! empty( $classStr ) ) { // No need setting empty classess all over the place.
$classes = []; // This is ware we put all the renamed classes.
foreach ( explode( ' ', $classStr ) as $class ) {
array_push( $classes, build_class( $class, $classMap ) );
}
$elem->setAttribute( 'class', implode( ' ', $classes ) );
}
}
return $doc->saveHTML();
}
if (!DEV_MODE)
ob_start( 'class_rename' );
?>
JavaScript(奖励)
虽然不是原始问题的一部分,但该解决方案非常有趣且并非微不足道,因此我决定将其包含在内。
首先注册另一个 grunt 任务:
var fnPattern = /(jQuery|\$|find|__)\s*\(\s*(["'])((?:\\.|(?!\2).)*)\2\s*\)/g;
grunt.registerMultiTask('rename_js', 'Use short css class names.', function () {
this.files.forEach(function ( file ) {
var
content = fs.readFileSync( file.src[ 0 ], 'utf8' ),
map = JSON.parse( fs.readFileSync( file.map ) ),
output = content.replace( fnPattern, function ( match, fn, delimiter, str ) {
var classes, i;
if ( fn == '__' ) {
classes = str.split( ' ' );
i = classes.length;
while ( i-- ) {
classes[ i ] = rename.getClassName( classes[i], map );
}
// We can safly assume that that the classes string won't contain any quotes.
return '"' + classes.join( ' ' ) + '"';
} else { // Must be a jQuery function.
return match.replace( str, rename.getClassSelector( str, map ) );
}
});
// Wrap the output in a function so that the `__` function can get removed by an optimizer.
fs.writeFileSync( file.dest, '!(function(window, undefined) {\n' + output + '\n})(window);' );
});
});
JavaScript 文件可能如下所示:
function __( s ) {
return s;
}
window.main = function () {
var elems = document.getElementsByClassName(__('some-class-name')),
i = elems.length;
while ( i-- ) {
elems[ i ].className += __(' some-other-class-name');
}
}
重要的部分是__ 函数声明。在开发过程中这个函数什么都不做,但是当我们构建
应用程序,这个函数将被编译的类字符串替换。使用的正则表达式将找到所有出现的
__ 以及 jQuery 函数(jQuery、$ 和 jQuery.find)。然后它创建三个组:函数名,
分隔符(" 或 ')和内部字符串。这是一个图表,可帮助您更好地了解正在发生的事情:
(?:jQuery|\$|find)\s*\(\s*(["'])((?:\\.|(?!\1).)*)\1\s*\)
Debuggex Demo
如果函数名是__,那么我们用与 php.ini 相同的方式替换它。如果不是那么它是
可能是一个选择器,所以我们尝试做一个选择器类替换。
(请注意,这不处理输入到 jQuery 函数中的 html 文本。)
你可以得到一个完整的例子here