UNPKG

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
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 };