gulp-build-html
Version:
Used to process html files to automatically concat css and js files, and meanwhile update html references.
256 lines (216 loc) • 8.23 kB
JavaScript
var path = require("path"),
fs = require("fs"),
utils = require("wzh.node-utils"),
Vinyl = require('vinyl'),
BuildBlock = require("./BuildBlock"),
lib = require("./lib"),
constants = require("./constants"),
{ JSDOM } = require("jsdom");
var logger = lib.logger;
/**
* @typedef {Object} HtmlTagGroup 标签群组
* @property {String} [hash] 由群组内元素共同属性计算得出的哈希值
* @property {HTMLElement[]} elements 按顺序出现的元素集合
*/
/**
* 依据给定的 HTML 元素的 标签、属性 生成唯一值,用于辅助确定不同的元素是否可以合并至一个构建块中
* @param {HTMLElement} element HTML 元素
* @param {Object} countingAttributeNameAndDefaultValues 参与计算的属性以及默认属性值
* @param {Boolean} [ifCountTag=true] HTML 标签是否参与计算
*/
var generateElementHash = function(element, countingAttributeNameAndDefaultValues, ifCountTag){
if(arguments.length < 3)
ifCountTag = true;
var segs = [];
if(ifCountTag)
segs.push("@_TAG_@=" + element.tagName);
var attrs = Object.keys(countingAttributeNameAndDefaultValues);
attrs.sort();
attrs.forEach(attr => {
var attrValue = element.getAttribute(attr);
if(null === attrValue || undefined === attrValue)
attrValue = countingAttributeNameAndDefaultValues[attr];
segs.push("attr=" + String(attrValue || "").trim());
});
return segs.join(",");
};
/**
* 执行构建动作
* @param {String} htmlFileContent 文件正文
* @param {String} htmlFileAbsolutePath 文件的绝对路径
* @param {Object} [ops] 控制选项
* @param {FileContentReader} [ops. ] 文件正文读取器
* @param {ConcatFileContentWriter} [ops.fileContentWriter] 文件正文写入器
* @returns {{newFileContent: String}} 生成的新文件内容
*/
var build = function(htmlFileContent, htmlFileAbsolutePath, ops){
var rst = {
newFileContent: "",
};
if(utils.string.isEmptyString(htmlFileContent, true))
return rst;
logger.debug("Processing file: {}", htmlFileAbsolutePath);
var jsdom = new JSDOM(htmlFileContent);
/**
* 样式合并策略:
* 1、按出现顺序,合并 style、link 标签的内容
* 2、无法取得正文的 link 标签(如:href 是 http 外链的情形)将作为分割线,拆分合并群组
* 3、每个群组的正文合并后,均替换至群组内的第一个元素所在的位置上
*/
/**
* 脚本合并策略:
* 1、按出现顺序,合并 script 标签的内容,无论是内联脚本还是外联脚本
* 2、法取得正文的 script 标签(如:src 是 http 外链的情形)将作为分割线,拆分合并群组
* 3、每个群组的正文合并后,均替换至群组内的最后一个元素所在的位置上
*/
/* 提取所有的可合并目标 */
var objs = jsdom.window.document.querySelectorAll("*");
var cssObjs = [], jsObjs = [];
for(var i = 0; i < objs.length; i++){
var obj = objs[i];
var isLinkObj = obj.tagName === "LINK",
isStyleObj = obj.tagName === "STYLE",
isScriptObj = obj.tagName === "SCRIPT";
if(!isLinkObj && !isStyleObj && !isScriptObj)
continue;
/* 跳过不能合并的元素 */
if(
isLinkObj && "stylesheet" !== obj.rel
||
isScriptObj && "text/javascript" !== String(obj.type || "text/javascript").trim().toLowerCase()
)
continue;
if(isLinkObj || isStyleObj)
cssObjs.push(obj);
else if(isScriptObj)
jsObjs.push(obj);
}
var
/**
* @type {HtmlTagGroup[]}
*/
cssGroups = [],
/**
* @type {HtmlTagGroup[]}
*/
jsGroups = [];
/* 将 css 元素分组,以此分别合并 */
var cssGroup = null;
for(var i = 0; i < cssObjs.length; i++){
var obj = cssObjs[i];
if(obj.tagName !== "LINK" || !/^(?:http|https|ftp):\/\//i.test(obj.href)){
if(!cssGroup){
cssGroup = {
elements: []
};
cssGroups.push(cssGroup);
}
cssGroup.elements.push(obj);
}else{
/* 该元素不被构建并迫使后续的加入新群组 */
cssGroup = null;
}
}
/* 将 js 元素分组,以此分别合并 */
var jsGroup = null;
for(var i = 0; i < jsObjs.length; i++){
var obj = jsObjs[i];
if(!/^(?:http|https|ftp):\/\//i.test(obj.src)){
if(!jsGroup){
jsGroup = {
elements: []
};
jsGroups.push(jsGroup);
}
jsGroup.elements.push(obj);
}else{
/* 该元素不被构建并迫使后续的加入新群组 */
jsGroup = null;
}
// // 将连续出现的合并在一起
// if(!jsGroup || jsGroup.elements[jsGroup.elements.length - 1].nextElementSibling !== obj){
// jsGroup = {
// elements: [ obj ]
// };
// jsGroups.push(jsGroup);
// }else{
// jsGroup.elements.push(obj);
// }
}
console.log("-------------", htmlFileContent);
let mapper = group => group.elements.map(obj => obj.tagName + ":" + (obj.href || obj.src || ""));
console.log(cssGroups.map(mapper), jsGroups.map(mapper));
/**************************************************/
var fileContentReader = ops.fileContentReader || function(p){
return fs.readFileSync(p, 'utf8');
};
var concatFileContentWriter = ops.fileContentWriter || function(fileType, fileContent, htmlFileAbsolutePath, groupIndex){
var fileAbsolutePath = htmlFileAbsolutePath + ".allinone." + (groupIndex + 1) + "." + fileType;
utils.fs.mkdirSync(path.dirname(fileAbsolutePath));
fs.writeFileSync(fileAbsolutePath, fileContent, "utf8");
return path.relative(path.dirname(htmlFileAbsolutePath), fileAbsolutePath);
};
/* 合并 css */
cssGroups.forEach(function(cssGroup, cssGroupIndex){
var firstElement = cssGroup.elements[0];
var parentElement = firstElement.parentElement;
/* 拼装正文 */
var concatFileContent = "";
cssGroup.elements.forEach(function(element){
if(element.tagName === "STYLE"){
concatFileContent += "\r\n" + element.innerHTML;
}else if(element.tagName === "LINK"){
concatFileContent +=
"\r\n" + "/* [gul-build-html] merged from: " + element.getAttribute("href") + " */" +
"\r\n" + fileContentReader(element.getAttribute("href"));
}
});
concatFileContent = concatFileContent.trim();
/* 写入文件 */
var href = concatFileContentWriter("css", concatFileContent, htmlFileAbsolutePath, cssGroupIndex);
/* 插入 HTML */
var newLinkObj = jsdom.window.document.createElement("link");
newLinkObj.setAttribute("data-auto-generated-by", "gulp-build-html");
newLinkObj.setAttribute("rel", "stylesheet");
newLinkObj.setAttribute("href", href);
parentElement.replaceChild(newLinkObj, firstElement);
/* 移除其它元素 */
cssGroup.elements.forEach(function(element){
element.parentElement && element.parentElement.removeChild(element);
});
});
/* 合并 js */
jsGroups.forEach(function(jsGroup, jsGroupIndex){
var lastElement = jsGroup.elements[jsGroup.elements.length - 1];
var parentElement = lastElement.parentElement;
/* 拼装正文 */
var concatFileContent = "";
jsGroup.elements.forEach(function(element){
if(!element.src){
concatFileContent += "\r\n" + element.innerHTML;
}else{
concatFileContent +=
"\r\n" + "/* [gul-build-html] merged from: " + element.getAttribute("src") + " */\r\n" +
"\r\n" + fileContentReader(element.getAttribute("src"));
}
});
concatFileContent = concatFileContent.trim();
/* 写入文件 */
var src = concatFileContentWriter("js", concatFileContent, htmlFileAbsolutePath, jsGroupIndex);
/* 插入 HTML */
var newScriptObj = jsdom.window.document.createElement("script");
newScriptObj.setAttribute("data-auto-generated-by", "gulp-build-html");
newScriptObj.setAttribute("type", "text/javascript");
newScriptObj.setAttribute("src", src);
parentElement.replaceChild(newScriptObj, lastElement);
/* 移除其它元素 */
jsGroup.elements.forEach(function(element){
element.parentElement && element.parentElement.removeChild(element);
});
});
rst.newFileContent = jsdom.serialize();
return rst;
};
module.exports = {
build: build
};