JS动态创建元素的CSS Transition过渡动画失效问题
问题:Chrome扩展中HP条过渡动画失效
我正在开发一款用于浏览器游戏的Chrome扩展,用来展示队伍信息,目标是实现hp-bar变化时的平滑过渡动画。之前版本功能正常,但为了实现队员卡片按字母排序重构代码后,hp-bar的宽度和颜色显示都正常,唯独平滑过渡动画触发不了。
尝试过的解决方法:
- 在
appendChild前后调用setHp() - 在JS中为
.hp-container设置transition属性 - 在函数内直接设置过渡
- 将CSS选择器从
.hp-bar改为#cardOneHP, #cardTwoHp...
怀疑问题和JS创建元素的方式有关,但不知道具体怎么解决。
代码片段
JavaScript
const hpBarGreen = "#39ff7b"; const hpBarYellow = "#f3b200"; const hpBarRed = "#fb3041"; chrome.runtime.onMessage.addListener( function(request, sender, sendResponse) { if (request.data) { const party = request.data; const members = []; function createMemberObject(partyMember, index) { if (!partyMember) return null; return { id: `member${index + 1}`, originalName: partyMember.name, sortName: partyMember.name.replace("G-Max ", "").replace("Mega ", ""), form: partyMember.form, level: partyMember.level, typeOne: partyMember.types[0], typeTwo: partyMember.types[1], teraType: partyMember.teraType, status: partyMember.status, currentHp: partyMember.currentHP, maxHp: partyMember.maxHP }; } for (let i = 0; i <= 5; i++) { const memberObject = createMemberObject(party[i], i) if (memberObject) { members.push(memberObject); } }; function displayMembers(members) { const popup = document.querySelector(".popup"); popup.innerHTML = ""; members.forEach(member => { const card = document.createElement('div'); card.className = 'pokemon-card'; card.id = member.id; card.innerHTML = ` <div class="sprite"> <img class="pokemon-sprite" id="${member.id}Sprite" src="./images/pokemon/${ member.form ? `${member.sortName}-${member.form}` : `${member.sortName}` }.png" alt=""> <img id="${member.id}Form" src="" alt=""> </div> <div class="stats"> <div class="line-one"> <div class="name" id="${member.id}Name">${member.sortName.toUpperCase()}</div> <div class="level-container"> <p id="${member.id}LevelText">Lv.</p> <p class="level-number" id="${member.id}Level">${member.level}</p> </div> </div> <div class="line-two"> <div class="types"> <img id="${member.id}TypeOne" src="${ member.teraType ? `./images/tera-types/${member.teraType}.png` : `./images/types/${member.typeOne}.png` }" alt=""> <img id="${member.id}TypeTwo" src="${ member.teraType ? "" : `./images/types/${member.typeTwo}.png` }" alt=""> </div> </div> <div class="line-three"> <img id="${member.id}Status" class="status" src="./images/status/${member.status}.png" alt=""> </div> <div class="line-four"> <div id="${member.id}HpBar" class="hp-container"> <div id="${member.id}ActiveHp" class="hp-bar"></div> </div> </div> </div> `; popup.appendChild(card); }); } members.sort((a, b) => a.sortName.localeCompare(b.sortName)); displayMembers(members); const hpPercentage = (members[0].currentHp / members[0].maxHp) * 100; const hpBar = card.querySelector(`#${members.id}ActiveHp`); console.log(members[0].currentHp, members[0].maxHp); function setHp(percentage, hp) { hp.style.width = (percentage) + "%"; if (percentage <= 25) { hp.style.backgroundColor = hpBarRed; } else if (percentage <= 50) { hp.style.backgroundColor = hpBarYellow; } else { hp.style.backgroundColor = hpBarGreen; } }; setHp(hpPercentage, hpBar); } } );
CSS
@font-face { font-family: False-Positive; src: url(./fonts/falsepos.ttf); } body { margin: 0; padding: 0; border: 0; font-family: False-Positive; color: #333333; } .hide { display: none; } .popup { width: 500px; height: 800px; background-color: #e5e4c5ff; display: grid; grid-template-rows: repeat(6, 1fr); align-items: center; justify-content: center; } .pokemon-card { position: relative; width: 300px; height: 100px; background-color: #ffffcc; display: flex; overflow: hidden; align-items: center; border: #777777 solid 2px; } .sprite { min-width: 100px; width: 100px; height: 100px; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); position: relative; align-items: center; justify-content: center; } .pokemon-sprite { grid-column: 1 / span 3; grid-row: 1 / span 3; max-width: 85%; margin-left: 5px; } #memberOneForm, #memberTwoForm, #memberThreeForm, #memberFourForm, #memberFiveForm, #memberSixForm { grid-column: 1; grid-row: 1; z-index: 1; width: 100%; } .stats { max-width: 100%; height: 100%; display: grid; grid-template-rows: 30px 16px 20px 15px; gap: 3px; padding-top: 5px; } .line-one, .line-two, .line-three, .line-four { display: flex; width: 200px; } .line-one { justify-content: space-between; align-items: center; } .line-three { justify-content: end; } .name { min-width: 0; font-size: 30px; } p { margin: 0; } .level-container { display: flex; flex-direction: column; align-items: end; padding-right: 5px; } #level-text { font-size: 10px; } .level-number { font-size: 18px; } .types { width: 75%; height: 100%; display: flex; gap: 5px; } #type-one, #type-two { max-height: 100%; max-width: 100%; } .status { border-radius: 20px; margin-right: 3px; margin-top: 2px; } .hp-container { width: 100%; height: 100%; background-color: #555555; border-radius: 10px; border: 2px solid black; margin-right: 3px; overflow: hidden; } .hp-bar { width: 100%; height: 100%; transition: width 0.2s linear; }
HTML
<div class='popup'></div>
解决办法
问题出在两个核心点:
1. 元素创建后立即修改样式,浏览器跳过过渡
当JS创建元素并同步修改其width时,浏览器会将这两个操作合并为一次重绘,不会触发过渡动画。需要让浏览器先完成元素的初始渲染,再执行样式修改。
2. HP条获取逻辑错误
原代码中card是displayMembers函数内的局部变量,外部无法访问;且members.id是错误的写法,应该遍历每个成员处理对应的HP条。
修改步骤
步骤1:重构displayMembers和setHp函数
把HP条的处理逻辑移到displayMembers内部,使用requestAnimationFrame等待浏览器渲染完成后再修改样式:
// 把setHp函数移到外部,方便全局调用 function setHp(percentage, hp) { hp.style.width = percentage + "%"; if (percentage <= 25) { hp.style.backgroundColor = hpBarRed; } else if (percentage <= 50) { hp.style.backgroundColor = hpBarYellow; } else { hp.style.backgroundColor = hpBarGreen; } }; function displayMembers(members) { const popup = document.querySelector(".popup"); popup.innerHTML = ""; members.forEach(member => { const card = document.createElement('div'); card.className = 'pokemon-card'; card.id = member.id; card.innerHTML = ` <!-- 保持原有的HTML结构不变 --> `; popup.appendChild(card); // 计算当前成员的HP百分比 const hpPercentage = (member.currentHp / member.maxHp) * 100; const hpBar = card.querySelector(`#${member.id}ActiveHp`); // 等待浏览器完成元素渲染后再修改样式,触发过渡 requestAnimationFrame(() => { setHp(hpPercentage, hpBar); }); }); }
步骤2:删除原代码中错误的HP处理逻辑
移除displayMembers(members);之后的这段无效代码:
const hpPercentage = (members[0].currentHp / members[0].maxHp) * 100; const hpBar = card.querySelector(`#${members.id}ActiveHp`); console.log(members[0].currentHp, members[0].maxHp); function setHp(percentage, hp) { // ...原函数内容 }; setHp(hpPercentage, hpBar);
原理说明
requestAnimationFrame会在下一次浏览器重绘前执行回调,此时元素已经被添加到DOM并完成初始渲染,修改width属性会被浏览器识别为独立的样式变化,从而触发CSS过渡动画。同时修改后会遍历所有成员,每个卡片的HP条都能正确应用过渡效果。
内容的提问来源于stack exchange,提问作者PsychedelicSeal
相关产品推荐
相关产品推荐

