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>
问题分析与修复方案
核心错误点
target is undefined直接原因:abcjs.TimingCallbacks初始化时传入了visualObj[0],但abcjs.Editor返回的是单个实例而非数组,visualObj[0]为undefined,导致内部无法找到目标渲染对象。- 循环内重复ID:
v-for生成的多个元素使用了相同的ID(如abcString、paper、audio),ID是全局唯一标识,abcjs只能绑定第一个匹配的元素,后续歌曲实例初始化完全失效。 - 事件监听绑定不精准:
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
相关产品推荐
相关产品推荐

