You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

abcjs cursorControl示例目标赋值错误:点击播放报'target is undefined'

问题描述

按照abcjs Example Generator的说明编写代码并部署后,点击播放按钮时控制台报错target is undefined,推测是visualObj与可视化画布的绑定存在问题,寻求abcjs开发者的解决建议。

代码示例

<template>
  <div class="container-sm">
    <div v-for="item in songData" :key="item.id">
      <div>
        <h6>musik > <router-link to="songsearch">genre</router-link><router-link to="songsearch"> > {{ table }}</router-link></h6>
      </div>
      <div class="text-center">
        <ul class="unstyled-list list-inline">
          <li class="list-inline-item"></li>
          <li class="list-inline-item">{{ item.title }}</li>
          <li class="list-inline-item"></li>
        </ul>
      </div>
      <div class="text-center">
        <button class="btn bg-transparent" @click="showAbc">show ABC-Text</button>
      </div>
      <div id="abc-code" class="text-center" style="display: none">
<textarea class="abc-string" id="abcString" v-model="item.chords" spellcheck="false"></textarea>
      </div>
      <div id="audio" class="text-center"></div>
      <div id="midi" class="midi-download"></div>
      <div id="paper" class="text-center"></div>
      <div id="download_btn"></div>
    </div>
  </div>
</template>

<script>
import 'font-awesome/css/font-awesome.min.css'
import 'abcjs/abcjs-audio.css'
import abcjs from 'abcjs'

export default {
  data() {
    return {
      table: this.$route.query.table,
      song: this.$route.query.song,
      songData: []
    }
  },
  mounted() {
    this.getSong().then(() => {
      this.loadABC()
    });
  },
  methods: {
    async getSong() {
      let apiURL = 'https://gracile-software.de/xxxxxxxx/xxxxxx-api/songattic/xxx.php';
      let formData = new FormData();
      formData.append('table', this.table);
      formData.append('song', this.song);
      try {
        let loadSong = await this.axios.post(apiURL, formData);
        this.songData = loadSong.data;
      } catch(e) {
        console.log(e);
      }
    },
    loadABC() {
      let options = {};
      const visualObj = new abcjs.Editor("abcString", {
        canvas_id: "paper",
        abcjsParams: options,
        synth: {
          el: "#audio",
          options: {
            displayLoop: true,
            displayRestart: true,
            displayPlay: true,
            displayProgress: true,
            displayWarp: true
          },
        },
      });
      var cursorControl = new CursorControl("#paper");
      document.querySelector(".abcjs-midi-start").addEventListener("click", startTimer);
      function onEvent(ev) {
        if (ev)
          cursorControl.onEvent(ev);
          else
          cursorControl.onFinished();
      }
      function startTimer() {
        cursorControl.onStart();
        var timingCallbacks = new abcjs.TimingCallbacks(visualObj[0], {
        eventCallback: onEvent
        });
        timingCallbacks.start();
      }
      function CursorControl(rootSelector) {
        var self = this;
        self.cursor = null; // This is the svg element that will move with the music.
        self.rootSelector = rootSelector; // This is the same selector as the renderAbc call uses.

        self.onStart = function() {
            var svg = document.querySelector(self.rootSelector + " svg");
            self.cursor = document.createElementNS("http://www.w3.org/2000/svg", "line");
            self.cursor.setAttribute("class", "abcjs-cursor");
            self.cursor.setAttributeNS(null, 'x1', 0);
            self.cursor.setAttributeNS(null, 'y1', 0);
            self.cursor.setAttributeNS(null, 'x2', 0);
            self.cursor.setAttributeNS(null, 'y2', 0);
            svg.appendChild(self.cursor);
        };

        self.removeSelection = function() {
            var lastSelection = document.querySelectorAll(self.rootSelector + " .abcjs-highlight");
            for (var k = 0; k < lastSelection.length; k++)
                lastSelection[k].classList.remove("abcjs-highlight");
        };
        self.onEvent = function(ev) {

            // This is called every time a note or a rest is reached and contains the coordinates of it.
            if (ev.measureStart && ev.left === null)
                return; // this was the second part of a tie across a measure line. Just ignore it.

            self.removeSelection();

            // Select the currently selected notes.
            for (var i = 0; i < ev.elements.length; i++ ) {
                var note = ev.elements[i];
                for (var j = 0; j < note.length; j++) {
                    note[j].classList.add("abcjs-highlight");
                }
            }

            // Move the cursor to the location of the current note.
            if (self.cursor) {
                self.cursor.setAttribute("x1", ev.left - 2);
                self.cursor.setAttribute("x2", ev.left - 2);
                self.cursor.setAttribute("y1", ev.top);
                self.cursor.setAttribute("y2", ev.top + ev.height);
            }
        };
        self.onFinished = function() {
                self.removeSelection();
            if (self.cursor) {
                self.cursor.setAttribute("x1", 0);
                self.cursor.setAttribute("x2", 0);
                self.cursor.setAttribute("y1", 0);
                self.cursor.setAttribute("y2", 0);
            }
        };
      }
    },
    showAbc() {
      let abcCode = document.getElementById("abc-code");
      if(abcCode.style.display == "none") {
        abcCode.style.display = "block";
      }else {
        abcCode.style.display = "none";
      }
    }
  }
}
</script>

<style>
.abc-string {
  width: 26em;
  height: 18em;
  resize: none;
}
.abcjs-highlight {
fill: #0a9ecc;
}
.abcjs-cursor {
stroke: red;
}
</style>

问题分析与修复方案

核心错误点

  1. target is undefined直接原因:abcjs.TimingCallbacks初始化时传入了visualObj[0],但abcjs.Editor返回的是单个实例而非数组,visualObj[0]为undefined,导致内部无法找到目标渲染对象。
  2. 循环内重复ID:v-for生成的多个元素使用了相同的ID(如abcString、paper、audio),ID是全局唯一标识,abcjs只能绑定第一个匹配的元素,后续歌曲实例初始化完全失效。
  3. 事件监听绑定不精准:document.querySelector(".abcjs-midi-start")只会绑定第一个播放按钮,循环生成的其他按钮点击无响应。

修复步骤

1. 模板中使用动态ID

将循环内的固定ID改为基于item.id的动态值,确保每个歌曲实例的元素唯一:

<template>
  <div class="container-sm">
    <div v-for="item in songData" :key="item.id">
      <!-- ... 其他内容不变 ... -->
      <div :id="`abc-code-${item.id}`" class="text-center" style="display: none">
        <textarea class="abc-string" :id="`abcString-${item.id}`" v-model="item.chords" spellcheck="false"></textarea>
      </div>
      <div :id="`audio-${item.id}`" class="text-center"></div>
      <div :id="`paper-${item.id}`" class="text-center"></div>
      <button class="btn bg-transparent" @click="showAbc(item)">show ABC-Text</button>
      <!-- ... 其他内容不变 ... -->
    </div>
  </div>
</template>

2. 重构ABC初始化逻辑

将原有的loadABC拆分为针对单个歌曲的初始化方法,在获取数据后为每个歌曲单独实例化abcjs:

export default {
  // ... data部分不变 ...
  mounted() {
    this.getSong();
  },
  methods: {
    async getSong() {
      // ... 原有请求逻辑不变 ...
      this.songData = loadSong.data;
      // 为每个歌曲单独初始化abcjs
      this.songData.forEach(item => this.initAbcInstance(item));
    },

    initAbcInstance(item) {
      const abcId = `abcString-${item.id}`;
      const paperId = `paper-${item.id}`;
      const audioSelector = `#audio-${item.id}`;

      const options = {};
      // abcjs.Editor返回单个实例,直接赋值给visualObj
      const visualObj = new abcjs.Editor(abcId, {
        canvas_id: paperId,
        abcjsParams: options,
        synth: {
          el: audioSelector,
          options: {
            displayLoop: true,
            displayRestart: true,
            displayPlay: true,
            displayProgress: true,
            displayWarp: true
          }
        }
      });

      const cursorControl = new CursorControl(`#${paperId}`);
      // 绑定当前歌曲实例的播放按钮事件
      document.querySelector(`${audioSelector} .abcjs-midi-start`).addEventListener("click", () => {
        cursorControl.onStart();
        // 直接传入visualObj实例,而非visualObj[0]
        const timingCallbacks = new abcjs.TimingCallbacks(visualObj, {
          eventCallback: ev => {
            ev ? cursorControl.onEvent(ev) : cursorControl.onFinished();
          }
        });
        timingCallbacks.start();
      });

      // 保留CursorControl逻辑,增加元素存在性判断避免报错
      function CursorControl(rootSelector) {
        const self = this;
        self.cursor = null;
        self.rootSelector = rootSelector;

        self.onStart = () => {
          const svg = document.querySelector(`${self.rootSelector} svg`);
          if (!svg) return;
          self.cursor = document.createElementNS("http://www.w3.org/2000/svg", "line");
          self.cursor.setAttribute("class", "abcjs-cursor");
          self.cursor.setAttributeNS(null, 'x1', 0);
          self.cursor.setAttributeNS(null, 'y1', 0);
          self.cursor.setAttributeNS(null, 'x2', 0);
          self.cursor.setAttributeNS(null, 'y2', 0);
          svg.appendChild(self.cursor);
        };

        self.removeSelection = () => {
          const lastSelection = document.querySelectorAll(`${self.rootSelector} .abcjs-highlight`);
          lastSelection.forEach(el => el.classList.remove("abcjs-highlight"));
        };

        self.onEvent = ev => {
          if (!ev || (ev.measureStart && ev.left === null)) return;

          self.removeSelection();
          ev.elements.forEach(noteGroup => {
            noteGroup.forEach(note => note.classList.add("abcjs-highlight"));
          });

          if (self.cursor && ev.left !== null) {
            self.cursor.setAttribute("x1", ev.left - 2);
            self.cursor.setAttribute("x2", ev.left - 2);
            self.cursor.setAttribute("y1", ev.top);
            self.cursor.setAttribute("y2", ev.top + ev.height);
          }
        };

        self.onFinished = () => {
          self.removeSelection();
          if (self.cursor) {
            self.cursor.setAttribute("x1", 0);
            self.cursor.setAttribute("x2", 0);
            self.cursor.setAttribute("y1", 0);
            self.cursor.setAttribute("y2", 0);
          }
        };
      }
    },

    // 修复showAbc方法,针对单个歌曲元素操作
    showAbc(item) {
      const abcCode = document.getElementById(`abc-code-${item.id}`);
      abcCode.style.display = abcCode.style.display === "none" ? "block" : "none";
    }
  }
}

3. 其他优化

  • 用forEach替代原有的for循环,代码更简洁
  • 增加DOM元素存在性判断,避免空指针报错
  • 简化事件回调的写法,提升可读性

内容的提问来源于stack exchange,提问作者BsrBstiBrln

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 20:55:55