关于svg icon 在vite上的应用

343 阅读2分钟

Vue-cli项目,通常会用到svg-sprite-loader插件对 svg 图标进行一个预处理,再通过require.context方法实现对图标的自动引入,然后进行一个全局的icon组件封装,实现优雅的封装使用效果。

详情请看大佬的手摸手系列之icon

到了vite,由于缺少类似svg-sprite-loader的插件,为了实现类似的封装方式,借用了iconfont 关于 svg symbol的实现方法。

实现

通过 viteimport.meta.globEager实现svg icon的自动引入,然后遍历把每个svg iconpath标签和相关路径属性插入到symbol标签里,再由函数return一个被 svg标签 包裹的多个symbol的dom结构字符串即可,具体代码如下。

代码

用于输出字符串dom的函数

function domToString(node) {
  let tmpNode = document.createElement("div");

  tmpNode.appendChild(node);

  let str = tmpNode.innerHTML;

  tmpNode = node = null; // 解除引用,以便于垃圾回收

  return str;
}

遍历path,生成path标签和相关路径属性的函数

function createEl(children) {
  let frag = document.createDocumentFragment();
  children.map((item) => {
    let node = document.createElement(item.type);
    for (const key in item.props) {
      if (key && key !== "fill") node.setAttribute(key, item.props[key]);
    }
    if (item.children) {
      node.appendChild(createEl(item.children));
    }
    frag.appendChild(node);
  });
  return frag;
}

实现过程

//引入svg图标
const modules = import.meta.globEager("./svg/*.svg");
let svgEl = document.createElement("svg");

for (const path in modules) {
   //用于赋值symbol的id
  let id = path.split("/")[2].split(".svg")[0];
  // dom节点不一定是svg, 还可能包含 icon作者信息
  let tmp = modules[path].render();
  let node;
  if (tmp.type !== "svg") {
    tmp.children.map((item) => {
      if (item.type === "svg") node = item;
    });
  } else {
    node = tmp;
  }
  let width = node.props.width;
  let height = node.props.height;
  let viewBox = node.props.viewBox
    ? node.props.viewBox
    : `0 0 ${width} ${height}`;

  let symbol = document.createElement("symbol");
  symbol.setAttribute("viewBox", viewBox);
  symbol.setAttribute("id", id);

  if (node.children[0].type !== "symbol") {
    symbol.appendChild(createEl(node.children));
  } else {
    symbol.appendChild(createEl(node.children[0].children));
  }
  svgEl.appendChild(symbol);
}

let svgStr = domToString(svgEl);

把dom字符串挂载到iconfont函数上

(window._iconfont_svg_string_3788124 = svgStr),
  (function (l) {
    var t = (t = document.getElementsByTagName("script"))[t.length - 1],
      a = t.getAttribute("data-injectcss"),
      t = t.getAttribute("data-disable-injectsvg");
    if (!t) {
      var e,
        h,
        c,
        o,
        n,
        i = function (t, a) {
          a.parentNode.insertBefore(t, a);
        };
      if (a && !l.__iconfont__svg__cssinject__) {
        l.__iconfont__svg__cssinject__ = !0;
        try {
          document.write(
            "<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>"
          );
        } catch (t) {
          console && console.log(t);
        }
      }
      (e = function () {
        var t,
          a = document.createElement("div");
        (a.innerHTML = l._iconfont_svg_string_3788124),
          (a = a.getElementsByTagName("svg")[0]) &&
            (a.setAttribute("aria-hidden", "true"),
            (a.style.position = "absolute"),
            (a.style.width = 0),
            (a.style.height = 0),
            (a.style.overflow = "hidden"),
            (a = a),
            (t = document.body).firstChild
              ? i(a, t.firstChild)
              : t.appendChild(a));
      }),
        document.addEventListener
          ? ~["complete", "loaded", "interactive"].indexOf(document.readyState)
            ? setTimeout(e, 0)
            : ((h = function () {
                document.removeEventListener("DOMContentLoaded", h, !1), e();
              }),
              document.addEventListener("DOMContentLoaded", h, !1))
          : document.attachEvent &&
            ((c = e),
            (o = l.document),
            (n = !1),
            s(),
            (o.onreadystatechange = function () {
              "complete" == o.readyState &&
                ((o.onreadystatechange = null), d());
            }));
    }
    function d() {
      n || ((n = !0), c());
    }
    function s() {
      try {
        o.documentElement.doScroll("left");
      } catch (t) {
        return void setTimeout(s, 50);
      }
      d();
    }
  })(window);

最后,再进行svg-icon 的组件封装, 并全局注册

// src/components/SvgIcon.vue
<template>
  <svg>
    <use :xlink:href="`#${symbolId}`" />
  </svg>
</template>

<script setup>
defineProps({
  symbolId: {
    type: String,
    default: "",
    require: true,
  },
});
</script>

<style lang="scss" scoped>
svg {
  width: 1em;
  height: 1em;
  fill: currentColor;
  overflow: hidden;
}
</style>
// main.js
import "./icons/index.js";
import SvgIcon from "@/components/SvgIcon.vue";
app.component("SvgIcon", SvgIcon);

至此,就已经能实现vue2上对于icon的优雅使用效果

image.png image.png

总结

查阅了一些文章,发现大多使用vite-svg-loader,但对于webpack上的svg-sprite-loader还是难受了点,如果有更好的方案,还请留言分享下,感谢。

完整代码

// src/
const modules = import.meta.globEager("./svg/*.svg");
let svgEl = document.createElement("svg");
for (const path in modules) {
  let id = path.split("/")[2].split(".svg")[0];
  // dom节点不一定是svg, 还可能包含 icon作者信息
  let tmp = modules[path].render();
  let node;
  if (tmp.type !== "svg") {
    tmp.children.map((item) => {
      if (item.type === "svg") node = item;
    });
  } else {
    node = tmp;
  }
  let width = node.props.width;
  let height = node.props.height;
  let viewBox = node.props.viewBox
    ? node.props.viewBox
    : `0 0 ${width} ${height}`;

  let symbol = document.createElement("symbol");
  symbol.setAttribute("viewBox", viewBox);
  symbol.setAttribute("id", id);

  if (node.children[0].type !== "symbol") {
    symbol.appendChild(createEl(node.children));
  } else {
    symbol.appendChild(createEl(node.children[0].children));
  }
  svgEl.appendChild(symbol);
}

let svgStr = domToString(svgEl);
/**
 *
 * @param {Dom} node dom节点
 * @returns dom结构的字符串
 */
function domToString(node) {
  let tmpNode = document.createElement("div");

  tmpNode.appendChild(node);

  let str = tmpNode.innerHTML;

  tmpNode = node = null; // 解除引用,以便于垃圾回收

  return str;
}

/**
 *
 * @param {Array} children 用于遍历的children
 * @returns
 */
function createEl(children) {
  let frag = document.createDocumentFragment();
  children.map((item) => {
    let node = document.createElement(item.type);
    for (const key in item.props) {
      if (key && key !== "fill") node.setAttribute(key, item.props[key]);
    }
    if (item.children) {
      node.appendChild(createEl(item.children));
    }
    frag.appendChild(node);
  });
  return frag;
}

(window._iconfont_svg_string_3788124 = svgStr),
  (function (l) {
    var t = (t = document.getElementsByTagName("script"))[t.length - 1],
      a = t.getAttribute("data-injectcss"),
      t = t.getAttribute("data-disable-injectsvg");
    if (!t) {
      var e,
        h,
        c,
        o,
        n,
        i = function (t, a) {
          a.parentNode.insertBefore(t, a);
        };
      if (a && !l.__iconfont__svg__cssinject__) {
        l.__iconfont__svg__cssinject__ = !0;
        try {
          document.write(
            "<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>"
          );
        } catch (t) {
          console && console.log(t);
        }
      }
      (e = function () {
        var t,
          a = document.createElement("div");
        (a.innerHTML = l._iconfont_svg_string_3788124),
          (a = a.getElementsByTagName("svg")[0]) &&
            (a.setAttribute("aria-hidden", "true"),
            (a.style.position = "absolute"),
            (a.style.width = 0),
            (a.style.height = 0),
            (a.style.overflow = "hidden"),
            (a = a),
            (t = document.body).firstChild
              ? i(a, t.firstChild)
              : t.appendChild(a));
      }),
        document.addEventListener
          ? ~["complete", "loaded", "interactive"].indexOf(document.readyState)
            ? setTimeout(e, 0)
            : ((h = function () {
                document.removeEventListener("DOMContentLoaded", h, !1), e();
              }),
              document.addEventListener("DOMContentLoaded", h, !1))
          : document.attachEvent &&
            ((c = e),
            (o = l.document),
            (n = !1),
            s(),
            (o.onreadystatechange = function () {
              "complete" == o.readyState &&
                ((o.onreadystatechange = null), d());
            }));
    }
    function d() {
      n || ((n = !0), c());
    }
    function s() {
      try {
        o.documentElement.doScroll("left");
      } catch (t) {
        return void setTimeout(s, 50);
      }
      d();
    }
  })(window);