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

JS变量加载延迟致添加菜单失效的问题排查

问题描述

开发了一款具备“添加至我的图书馆”功能的应用,点击“+”按钮可通过JS打开添加菜单,但该功能时常无法正常工作。报错信息如下:

main.js:2 Uncaught TypeError: Cannot read properties of null (reading 'value')
    at main.js:2:55

相关代码

JS代码

if (window.location.pathname === '/dashboard/') {
    let booksLen = document.getElementById("booksLen").value;  // takes number of positions
    for (let comp = 1; comp < booksLen; comp++) {
        let bookAddButton = document.getElementById(`add-btn-${comp}`)
        let bookAddComp = document.getElementById(`add-comp-${comp}`)
        bookAddButton.addEventListener('click', () => {
            if (bookAddComp.classList.contains('d-none') === false) {
                bookAddComp.classList.toggle('d-none')
            } else {

                // disable all previously opened submit forms and error msg
                for (let compDisable = 1; compDisable < 5; compDisable++) {
                    let bookAddCompDisable = document.getElementById(`add-comp-${compDisable}`)
                    let errMsgDisable = document.getElementById(`err-msg-${comp}`)
                    if (bookAddCompDisable.classList.contains('d-none') === false) {
                        bookAddCompDisable.classList.toggle('d-none')
                    }
                    if (errMsgDisable.classList.contains('d-none') === false) {
                        errMsgDisable.classList.toggle(('d-none'))
                    }
                }
                // open only one submit form at a time
                bookAddComp.classList.toggle('d-none')
            }

        })

        let submitButton = document.getElementById(`submit-button-${comp}`)

        // show error msg
        submitButton.addEventListener('click', e => {
            let bookInput = document.getElementById(`input-${comp}`).value
            let errMsg = document.getElementById(`err-msg-${comp}`)
            if (bookInput === "") {
                errMsg.classList.remove('d-none')
                e.preventDefault()
            }
        })
    }
}

模板变量传递代码

<input type="hidden" id="booksLen" name="booksLen" value="{{ books_len }}">

base.html脚本引入

<script src="{% static './js/main.js' %}" async defer></script>

Django视图代码

class BooksAvailability(View):
    # displays books related to libraries filtered for proper user. Only records desired by user
    def get(self, request):
        user = request.user
        libraries = models.Libraries.objects.all()
        books = models.Books.objects.filter(user=user.id)
        status = models.BooksLibraries.objects.all()

        books_len = len(books) + 1 # passes variable for js purpose. Book add to my library feature
        return render(request, 'library/dashboard.html',
                      context={'libraries': libraries, 'status': status, 'books': books, 'books_len':books_len})
问题原因及解决方案

核心原因

  1. DOM加载时机不匹配:虽然脚本加了async defer,但defer的执行时机是DOM加载完成后,但如果页面渲染慢、模板中booksLen元素位置靠后,或者Django模板渲染有延迟,脚本执行时该元素还未被渲染,就会导致document.getElementById("booksLen")返回null,触发报错。
  2. 无存在性校验:代码直接获取booksLen的value,没有做元素存在性判断,一旦元素缺失,后续所有按钮绑定逻辑都会中断。
  3. 直接统计元素无效的原因:你尝试统计元素数量无效,是因为统计时机不对——DOM未加载完成时,找不到目标元素,统计结果为0,循环根本不会执行。

解决方案

1. 确保DOM完全加载后执行JS

把代码包裹在DOMContentLoaded事件监听中,同时给每个DOM元素添加存在性校验,避免单个元素缺失导致整个逻辑崩溃:

document.addEventListener('DOMContentLoaded', function() {
    if (window.location.pathname === '/dashboard/') {
        // 先校验核心元素是否存在
        const booksLenEl = document.getElementById("booksLen");
        if (!booksLenEl) return;
        
        let booksLen = parseInt(booksLenEl.value, 10);
        // 校验数值有效性
        if (isNaN(booksLen) || booksLen <= 1) return;

        for (let comp = 1; comp < booksLen; comp++) {
            let bookAddButton = document.getElementById(`add-btn-${comp}`);
            let bookAddComp = document.getElementById(`add-comp-${comp}`);
            // 单个元素缺失则跳过当前循环
            if (!bookAddButton || !bookAddComp) continue;

            bookAddButton.addEventListener('click', () => {
                // 原有点击逻辑
                if (bookAddComp.classList.contains('d-none') === false) {
                    bookAddComp.classList.toggle('d-none')
                } else {
                    for (let compDisable = 1; compDisable < 5; compDisable++) {
                        let bookAddCompDisable = document.getElementById(`add-comp-${compDisable}`)
                        let errMsgDisable = document.getElementById(`err-msg-${comp}`)
                        if (bookAddCompDisable && !bookAddCompDisable.classList.contains('d-none')) {
                            bookAddCompDisable.classList.toggle('d-none')
                        }
                        if (errMsgDisable && !errMsgDisable.classList.contains('d-none')) {
                            errMsgDisable.classList.toggle('d-none')
                        }
                    }
                    bookAddComp.classList.toggle('d-none')
                }
            });

            let submitButton = document.getElementById(`submit-button-${comp}`);
            if (!submitButton) continue;

            submitButton.addEventListener('click', e => {
                let bookInput = document.getElementById(`input-${comp}`);
                let errMsg = document.getElementById(`err-msg-${comp}`);
                if (!bookInput || !errMsg) return;
                
                if (bookInput.value === "") {
                    errMsg.classList.remove('d-none')
                    e.preventDefault()
                }
            })
        }
    }
});

2. 优化模板渲染逻辑

把booksLen的隐藏输入框放在页面靠前位置,确保脚本执行前该元素已被渲染;同时在Django视图中保证books_len始终是有效数值(比如用户无书籍时,books_len为1,此时循环不会执行,避免无效操作)。

3. 替代方案:不依赖hidden字段,直接统计按钮数量

摆脱对booksLen变量的依赖,直接通过选择器获取所有添加按钮,更可靠:

document.addEventListener('DOMContentLoaded', function() {
    if (window.location.pathname === '/dashboard/') {
        // 直接获取所有以add-btn开头的按钮
        const bookAddButtons = document.querySelectorAll('[id^="add-btn-"]');
        
        bookAddButtons.forEach(button => {
            // 从按钮ID中提取comp数值
            const comp = button.id.split('-')[2];
            const bookAddComp = document.getElementById(`add-comp-${comp}`);
            if (!bookAddComp) return;

            button.addEventListener('click', () => {
                // 原有点击逻辑
                if (!bookAddComp.classList.contains('d-none')) {
                    bookAddComp.classList.toggle('d-none')
                } else {
                    for (let compDisable = 1; compDisable < 5; compDisable++) {
                        let bookAddCompDisable = document.getElementById(`add-comp-${compDisable}`)
                        let errMsgDisable = document.getElementById(`err-msg-${comp}`)
                        if (bookAddCompDisable && !bookAddCompDisable.classList.contains('d-none')) {
                            bookAddCompDisable.classList.toggle('d-none')
                        }
                        if (errMsgDisable && !errMsgDisable.classList.contains('d-none')) {
                            errMsgDisable.classList.toggle('d-none')
                        }
                    }
                    bookAddComp.classList.toggle('d-none')
                }
            });

            const submitButton = document.getElementById(`submit-button-${comp}`);
            if (!submitButton) return;

            submitButton.addEventListener('click', e => {
                const bookInput = document.getElementById(`input-${comp}`);
                const errMsg = document.getElementById(`err-msg-${comp}`);
                if (!bookInput || !errMsg) return;
                
                if (bookInput.value === "") {
                    errMsg.classList.remove('d-none')
                    e.preventDefault()
                }
            })
        });
    }
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 03:57:04