vue动态渲染svg、添加点击事件

4,956 阅读3分钟

欢迎关注[前端小讴的github],阅读更多原创技术文章

业务需求(vue项目中)

  • 页面展示svg内容
  • 监听svg内部的点击事件
  • 动态改变svg内部元素的属性和值

html标签

经多次实验,用embed、img等标签改变src属性的方式,均无法实现上述全部功能(尤其是svg内部点击事件),最终采用**Vue.extend()**方法完整实现,代码也较为简洁,html结构如下:

<template>
  <div>
    <div id="svgTemplate"></div>
  </div>
</template>

直接将svg文件的内容复制粘贴到.vue文件里,是可以在标签内直接添加@click事件完成需求的,方式简单但会造成文件过长,本文不多陈述

实现思路

1.创建xhr对象

const xhr = new XMLHttpRequest();
this.svgUrl = ...; // svg的绝对地址,在浏览器中打开能看到的那个
xhr.open("GET", this.svgUrl, true);
xhr.send();

2.监听xhr对象(获取svg的dom -> 添加事件 -> 修改dom -> 转成虚拟dom并挂载)

xhr.addEventListener("load", () => {
	// ① 获取svg的dom
    const resXML = xhr.responseXML;
    this.svgDom = resXML.documentElement.cloneNode(true);     // console.log(this.svgDom);
    
    // ② 添加click事件
    let btn = this.svgDom.getElementById("...");
    btn.setAttribute("v-on:click", "this.handleClick()");
    // ↑↑↑ 此处注意:原生事件handleClick此时在window层,解决办法见后文

    // ③ 修改 dom
    this.svgDom.getElementById("...").childNodes[0].nodeValue = ...
	this.svgDom.getElementById("...").setAttribute("style",
	      `....; fill:${this.photoResult.resultColor}; ...`);
    // ↑↑↑ 用js操作dom的语法,动态设置svg部件的属性和值
   
    // ④ 将svgDom对象转换成vue的虚拟dom,创建实例并挂载到元素上
    var oSerializer = new XMLSerializer();
    var sXML = oSerializer.serializeToString(this.svgDom);
    var Profile = Vue.extend({
        template: "<div id='svgTemplate'>" + sXML + "</div>"
    });
    new Profile().$mount("#svgTemplate");
});

3.将methods里要执行的事件绑定到window下面,供外部(刚添加的 handleClick 事件)调用

async mounted() {
    window["handleClick"] = () => {
       this.takePhoto();
    };
},
methods:{
    takePhoto(){ ... }
}

到这里就基本完成需求:动态渲染了svg、用js操作dom的语法修改svg部件的属性和值、给svg部件动态添加了事件 handleClick,最后将 takePhoto() 事件绑定给了 window 对象的 handleClick,可以放心大胆的在 takePhoto() 里写你要执行的内容了!

特殊注意

  • 给svg的dom部件添加事件时: 1.经多次尝试,只有 setAttribute + v-on:click 写法有效 2.setAttribute 不支持 @click(非原生事件),会报语法错误 3.addEventListener 和 onclick 均会被 vue 拦截

  • 将svgDom对象转换成vue的虚拟dom时: 1.如果报错如下

则将 import Vue from "vue" 改为 import Vue from "vue/dist/vue.esm.js" 其原因及其他解决办法本文不做探讨可自行百度。 2.vue.extend() 方法是 vue 的一个构造器,用来动态创建 vue 实例,template 组件模板只能有一个根元素 3.$mount 手动挂载到 id 为 svgTemplate的 元素上,挂载后将替换原本的dom(替换原本的 <div id="svgTemplate"></div>)。由于每次更新 svg 都要重新挂载,没有找到 dom 元素是无法挂载的,因此 template 里面最外层的 div 也要加上 id 的属性:

var Profile = Vue.extend({
     template: "<div id='svgTemplate'>" + sXML + "</div>" 
     // ↑↑↑ 最外层的 id 不能省略,否则首次渲染后找不到 #svgTemplate
});
new Profile().$mount("#svgTemplate"); 
// ↑↑↑ 原本的 #svgTemplate 将被替换成 Profile 的 template

完整代码

<template>
  <div>
    <div id="svgTemplate"></div>
  </div>
</template>
<script>
import Vue from "vue/dist/vue.esm.js";

// window.handleClick = () => {
   // 原本的 handleClick 事件是 window 的
// };

export default {
  name: "svg-drawing",
  data() {
    return {
	  /* 全局 */
      svgUrl: "", // svg的url
      svgDom: null, // 获取到的svg元素
      /* svg的变量 */
      photoResult: {
        resultVal: 0, // 测试结果 - 值
        resultMsg: "未检测", // 测试结果 - 字段
        resultColor: "#dcdee2" // 测试结果 - 字段背景色
      }
    };
  },
  async mounted() {
    // 将takePhoto方法绑定到window下面,提供给外部调用
    window["handleClick"] = () => {
      this.takePhoto();
    };
  },
  created() {
    this.getSvg();
  },
  methods: {
    // 初始化svg
    getSvg() {
	  /* 创建xhr对象 */
      const xhr = new XMLHttpRequest();
      this.svgUrl = this.baseUrl + "/svgs/" + "test.svg";
      xhr.open("GET", this.svgUrl, true);
      xhr.send();

	  /* 监听xhr对象 */
      xhr.addEventListener("load", () => {
	    /* 1. 获取 dom */
        const resXML = xhr.responseXML;
        this.svgDom = resXML.documentElement.cloneNode(true);

        /* 2.SVG对象添加click事件 */
        let btnTakePhotoDom = this.svgDom.getElementById("...");
        btnTakePhotoDom.setAttribute("v-on:click", "this.handleClick()");

        /* 3. 修改 dom */
		this.svgDom.getElementById("...").childNodes[0].nodeValue = ...;
		this.svgDom.getElementById("...").setAttribute("style",
	      `....; fill:${this.photoResult.resultColor}; ...`);

        /* 4.将svgDom对象转换成vue的虚拟dom */
        var oSerializer = new XMLSerializer();
        var sXML = oSerializer.serializeToString(this.svgDom);
        var Profile = Vue.extend({
          template: "<div id='svgTemplate'>" + sXML + "</div>"
        });
        // 创建实例,并挂载到元素上
        new Profile().$mount("#svgTemplate");
      });
    },
    // 事件
    takePhoto() { ... },
  },
  beforeDestroy() {
    this.svgDom = null;
  },
  watch: {
    photoResult: {
      handler(newVal, oldVal) {
        this.getSvg();
      },
      deep: true
    }
  }
};
</script>