vue3处理插件
因为上面的文章中提出的例子在vue2中并不生效, 因此单独写了一个针对vue2使用的loader.
实现1: 通过字符串替换方式处理
这个方式的缺点是较为死板, 无法处理模板字符串和表达式相关, 但是对于src=xxx的类型有较好的匹配
module.exports = function (source) {
console.log("----customLoader original content----", source);
function replaceImageSrcInVue(content) {
content = content.replace(
/(<template[\s\S]*?>)([\s\S]*?)(<\/template>)/,
(match, start, middle, end) => {
// 替换 <image ... src="..." ...>
const replaced = middle.replace(
/(<image\b[^>]*?\bsrc=)(['"])([^'"]+)\2/gi,
(imgMatch, prefix, quote, src) => {
// 只替换非 http/https 开头的 src
if (/^https?:\/\//.test(src)) return imgMatch;
console.log(
"----customLoader src----",
imgMatch,
" prefix:",
prefix,
" src:",
src,
);
return `${prefix}${quote}${"https://www.xxx.com/"}${src}${quote}`;
},
);
return start + replaced + end;
},
);
return content;
}
return replaceImageSrcInVue(source);
};
实现2: 基于ast
这个模式的优点是可以精确匹配到image对应的src属性, 还可以对于绑定src的属性中的模板字符串和字符串类型进行处理, 比如说以下代码, 同时也可以很方便扩展到其他类型的元素中, 比如video等.
:src="isActive ? `${activeHost}/logo.png` : '/staticHost/logo.png'"
依赖编译器版本为yarn add -D @@vue/compiler-sfc@3.5.26
详细实现方式如下:
const compiler = require("@vue/compiler-sfc");
module.exports = function (source) {
const options = this.getOptions();
let { publicPath: staticHost, sourceDir } = options || {};
if (staticHost.endsWith("/")) {
staticHost = staticHost.slice(0, -1);
}
try {
const sfc = compiler.parse(source, {
templateParseOptions: { parseMode: "sfc" },
});
if (!sfc.descriptor.template) {
return source;
}
let content = sfc.descriptor.template.content;
const ast = sfc.descriptor.template.ast;
const tempLen = "<template>".length; // 10, loc是基于整个文件的偏移量,需要减去前面的长度
const traverseAst = (node) => {
if (!node) return;
if (node.children && node.children.length) {
for (let i = node.children.length - 1; i >= 0; i--) {
traverseAst(node.children[i]);
}
}
const doReplace = (loc, oldValue) => {
if (oldValue.startsWith(sourceDir)) {
const newValue =
'"' + oldValue.replace(sourceDir, `${staticHost}/`) + '"';
content =
content.slice(0, loc.start.offset - tempLen) +
newValue +
content.slice(loc.end.offset - tempLen);
}
};
if (node.type === 1 && node.tag === "image") {
// console.log("Found <image> node:", node);
const srcAttr = node.props.find(
(prop) => prop.name === "src" && prop.type === 6,
);
if (srcAttr) {
console.log("Original src value:", srcAttr);
const srcValue = srcAttr.value.content;
const loc = srcAttr.value.loc;
doReplace(loc, srcValue);
} else {
const bindSrcAttr = node.props.find(
(prop) =>
prop.name === "bind" &&
prop.type === 7 &&
prop.rawName === ":src",
);
// console.log("Bind src attribute:", bindSrcAttr);
if (!bindSrcAttr) return;
const ast = bindSrcAttr.exp.ast;
const loc = bindSrcAttr.exp.loc;
// 处理简单的模板字符串情况, 只需要遍历处理template和字符串类型就可以
// 这里可能包含的类型为三目预算符和逻辑运算符
const traverseBindAst = (bindNode, loc) => {
if (!bindNode) return;
// 逻辑运算符|| 或者 &&
if (bindNode.type === "LogicalExpression") {
traverseBindAst(bindNode.right, loc);
traverseBindAst(bindNode.left, loc);
} else if (bindNode.type === "ConditionalExpression") {
// 三目运算符
traverseBindAst(bindNode.alternate, loc);
traverseBindAst(bindNode.consequent, loc);
traverseBindAst(bindNode.test, loc);
} else if (bindNode.type === "TemplateLiteral") {
// 模板字符串类型
if (bindNode.quasis && bindNode.quasis.length > 0) {
const indexLoc = bindNode.quasis[0].loc;
const value = bindNode.quasis[0].value.cooked;
if (value.startsWith(sourceDir)) {
const newValue = value.replace(sourceDir, `${staticHost}/`);
content =
content.slice(
0,
loc.start.offset - tempLen + indexLoc.start.index - 1,
) + // -1 是因为模板字符串的 ` 符号占位
newValue +
content.slice(
loc.start.offset - tempLen + indexLoc.end.index - 1,
);
}
}
} else if (bindNode.type === "StringLiteral") {
// 字符串类型
const indexLoc = bindNode.loc;
const value = bindNode.value;
if (value.startsWith(sourceDir)) {
const newValue = value.replace(sourceDir, `${staticHost}/`);
content =
content.slice(
0,
loc.start.offset - tempLen + indexLoc.start.index, // 这里不减是需要保留 "" 符号
) +
newValue +
content.slice(
loc.start.offset - tempLen + indexLoc.end.index - 2,
); // -2 是因为字符串的 "" 符号占位
}
}
};
traverseBindAst(ast, loc);
}
}
};
traverseAst(ast);
// 替换 template 内容
const loc = sfc.descriptor.template.loc;
const newSource = source.slice(0, loc.start.offset) + content + source.slice(loc.end.offset);
return newSource;
} catch (err) {
console.error("Error parsing SFC:", err);
return source;
}
}
在vue.config.js中的用法
chainWebpack: (config) => {
config.module
.rule("vue")
.use("vue-loader")
.end()
.use("customLoader")
.loader(path.resolve(__dirname, "./customLoader.js"))
.options({
publicPath: "https://xxx.com",
sourceDir: '/staticHost/',
})
.end();
}
ps
如果遇到报错this.getConfig不存在, 则可以把config配置项写到load.js里面中...