angular-html-inline-loader
2024年12月21日
一、认识
angular-html-inline-loader
解析 HTML
源码, 查找 <link rel="import">
的标签, 将包含 .html?__inline
的 href
链接对应的文件内容嵌入到原 HTML
文件中。它会递归便利 HTML
节点,并拼接所有相关文件内容,最终返回修改后的源码。适用于将外部 HTML
资源嵌入到主 HTML
文件中。
二、实现
const fs = require('fs');
const path = require('path');
const parse5 = require('parse5');
const loaderUtils = require('loader-utils');
const defaultOptions = {
debug: false,
};
const fileCache = new Map();
/**
* 解析 HTML 源代码,获取所有内联文件的引用
* @param {string} source - HTML 源代码
* @returns {Array} 内联文件的引用信息
*/
function extractInlineFileReferences(source) {
const document = parse5.parse(source, { sourceCodeLocationInfo: true });
const references = [];
traverseHtmlNodes(document, references);
return references;
}
/**
* 遍历 HTML 节点,查找所有符合条件的内联文件引用
* @param {Object} node - 当前节点
* @param {Array} references - 存储内联文件引用信息的数组
*/
function traverseHtmlNodes(node, references) {
if (node.nodeName !== 'link') return;
const attributes = new Map(node.attrs.map(attr => [attr.name, attr.value]));
const href = attributes.get('href');
const rel = attributes.get('rel');
if (rel === 'import' && href?.includes('.html?__inline')) {
const filePath = href.split('?__inline')[0];
references.push({
filepath: filePath,
startOffset: node.sourceCodeLocation.startOffset,
endOffset: node.sourceCodeLocation.endOffset,
});
}
// 递归遍历子节点
node.childNodes?.forEach(childNode => traverseHtmlNodes(childNode, references));
}
/**
* Webpack Loader 处理函数
* @param {string} source - 当前文件的源代码
* @param {Object} map - source map
* @param {Object} meta - loader 元数据
* @returns {void}
*/
module.exports = function (source, map, meta) {
const callback = this.async();
const options = { ...defaultOptions, ...loaderUtils.getOptions(this) };
// 解析 HTML 获取内联文件引用信息
const inlineFileReferences = extractInlineFileReferences(source);
// 按照文件在 HTML 中的顺序对引用进行排序
inlineFileReferences.sort((a, b) => a.startOffset - b.startOffset);
let transformedSource = '';
let lastProcessedOffset = 0;
for (const { filepath, startOffset, endOffset } of inlineFileReferences) {
const targetFilepath = path.resolve(path.dirname(this.resourcePath), filepath);
// 从缓存中获取文件内容,避免重复 I/O
let cachedFileContent = fileCache.get(targetFilepath);
if (!cachedFileContent) {
try {
cachedFileContent = fs.readFileSync(targetFilepath, 'utf8');
fileCache.set(targetFilepath, cachedFileContent); // 缓存文件内容
} catch (err) {
return callback(new Error(`Failed to read the file: ${targetFilepath}. ${err.message}`));
}
}
transformedSource += source.slice(lastProcessedOffset, startOffset) + cachedFileContent;
lastProcessedOffset = endOffset;
}
// 追加剩余的源代码部分
transformedSource += source.slice(lastProcessedOffset);
// 返回最终的 transformedSource 和相关信息
callback(null, transformedSource, map, meta);
};
三、配置
{
test: /\.html$/,
include: xxx,
exclude: yyy,
use: [
{
loader: require("./angular-html-inline-loader.js"),
options: {
interpolate: true
}
}
]
}
四、效果
a.html
编译前
<link rel="import" href="test-inline-html.html?__inline">
a.html
编译后