普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月21日首页

使用uniapp vue2开发微信小程序时,图片处理插件

2026年1月21日 17:47

vue3处理插件

参考juejin.cn/post/738574…

因为上面的文章中提出的例子在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里面中...

❌
❌