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

嵌套Custom Element无法渲染TMDB API电影数据求助

嵌套自定义元素无法渲染电影内容的问题

问题描述

尝试通过嵌套自定义元素实现从TMDB API加载并展示电影列表,页面能看到<movie-container>和<movie-item>元素,但movie-item的Shadow DOM为空,没有渲染出电影内容。非自定义元素实现时功能正常。

错误原因分析

  • 渲染函数未执行:movie-container.js的set movies方法中,仅引用了this.render而未调用函数(缺少()),导致渲染逻辑未触发。
  • 属性 setter 不匹配:movie-container中给movie-item设置属性时用了movieItem.item = item;,但MovieItem类定义的setter是set movie(movie),属性名不匹配,无法触发MovieItem的渲染。
  • 变量名不一致:MovieItem的setter中把数据存在this._movie,但render函数里却使用this._data访问数据,导致找不到对应属性。
  • 全局函数作用域问题:getColor是全局函数,Shadow DOM无法直接访问外部全局函数,导致渲染时无法生成评分颜色类名。

修复后的代码

修复后的 movie-container.js

import './movie-item.js';
class MovieList extends HTMLElement{
    constructor(){
        super();
        this.shadowDOM = this.attachShadow({mode : 'open'});
    }
    set movies(movies){
        this._movies = movies;
        this.render(); // 修复:调用渲染函数
    }

    render(){
        this.shadowDOM.innerHTML = '';
        this._movies.forEach(item => {
            const movieItem = document.createElement("movie-item");
            movieItem.movie = item; // 修复:匹配MovieItem的setter名称
            this.shadowDOM.appendChild(movieItem);
        });
    }
}

customElements.define('movie-container', MovieList);

修复后的 movie-item.js

class MovieItem extends HTMLElement {
    constructor(){
        super();
        this.shadowDOM = this.attachShadow({mode : 'open'});
    }
    set movie(movie) {
        this._movie = movie; // 统一存储变量名
        this.render();
    }

    // 将getColor移到类内部,确保Shadow DOM可访问
    getColor(vote) {
        if(vote >= 8){
            return 'green';
        }else if(vote >= 5){
            return 'orange';
        }else {
            return 'red';
        }
    }

    render(){
        this.shadowDOM.innerHTML = `
        <style>
            * {
                margin : 0;
                padding : 0;
            }
            :host {
                display: block;
                margin-bottom: 18px;
                box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
                border-radius: 10px;
                overflow: hidden;
            }
            .movie-info {
                color: white;
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 0.5rem 1rem 1rem;
                letter-spacing: 0.5px;
            }
        
            .movie-info h3 {
                margin-top: 0;
            }
            
            .movie-info span {
                background-color: #145455;
                padding: 0.25rem 0.5rem;
                border-radius: 3px;
                font-weight: bold;
            }
        
            .movie-info span.green {
                    color: lightgreen;
            }
            .movie-info span.orange {
                    color: orange;
            }
            .movie-info span.red {
                    color: red;
            }
            
            .description {
                    position: absolute;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    background-color: white;
                    padding: 1rem;
                    max-height: 100%;
                    transform: translateY(101%);
                    transition: .2s ease;
            }
        </style>

            <img src="${'https://image.tmdb.org/t/p/w500'+this._movie.poster_path}" alt="${this._movie.title}">

            <div class="movie-info">
                <h3>${this._movie.title}</h3>
                <span class="${this.getColor(this._movie.vote_average)}">${this._movie.vote_average}</span>
            </div>

            <div class="description">
                ${this._movie.overview}
            </div>
    `;
    }
}

customElements.define('movie-item', MovieItem);

完整可运行修正代码

class MovieList extends HTMLElement{
    constructor(){
        super();
        this.attachShadow({mode : 'open'});
    }
    set movies(movies){
        this._movies = movies;
        this.render();
    }

    render(){
        this.shadowRoot.innerHTML = `
            <style>
                :host {
                    display: flex;
                    flex-wrap: wrap;
                    justify-content: center;
                }
            </style>
        `;
        this._movies.forEach(item => {
            const movieItem = document.createElement("movie-item");
            movieItem.movie = item;
            this.shadowRoot.appendChild(movieItem);
        });
    }
}

customElements.define('movie-container', MovieList);

class MovieItem extends HTMLElement {
    constructor(){
        super();
        this.attachShadow({mode : 'open'});
    }
    set movie(item) {
        this._item = item;
        this.render();
    }

    getColor(vote) {
        if(vote >= 8){
            return 'green';
        }else if(vote >= 5){
            return 'orange';
        }else {
            return 'red';
        }
    }

    render(){
        this.shadowRoot.innerHTML = `
        <style>
            :host {
                width: 300px;
                margin: 1rem;
                border-radius: 3px;
                box-shadow: 0.2px 4px 5px rgba(0, 0, 0, 0.1);
                position: relative;
                overflow: hidden;
                background-color: #0F3D3E;
            }

            :host img {
                    width: 100%;
            }
            
            :host:hover .description{
                    transform: translateY(0);
            }
            
            .movie-info {
                color: white;
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 0.5rem 1rem 1rem;
                letter-spacing: 0.5px;
            }
        
            .movie-info h3 {
                margin-top: 0;
            }
            
            .movie-info span {
                background-color: #145455;
                padding: 0.25rem 0.5rem;
                border-radius: 3px;
                font-weight: bold;
            }
        
            .movie-info span.green {
                    color: lightgreen;
            }
            .movie-info span.orange {
                    color: orange;
            }
            .movie-info span.red {
                    color: red;
            }
            
            .description {
                    position: absolute;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    background-color: white;
                    padding: 1rem;
                    max-height: 100%;
                    transform: translateY(101%);
                    transition: .2s ease;
            }
        </style>

            <img src="${'https://image.tmdb.org/t/p/w500'+this._item.poster_path}" alt="${this._item.title}">

            <div class="movie-info">
                <h3>${this._item.title}</h3>
                <span class="${this.getColor(this._item.vote_average)}">${this._item.vote_average}</span>
            </div>

            <div class="description">
                ${this._item.overview}
            </div>
    `;
    }
}

customElements.define('movie-item', MovieItem);

// 模拟API数据
const data = [
  {
    "adult": false,
    "backdrop_path": "/y5Z0WesTjvn59jP6yo459eUsbli.jpg",
    "genre_ids": [27,53],
    "id": 663712,
    "original_language": "en",
    "original_title": "Terrifier 2",
    "overview": "After being resurrected by a sinister entity, Art the Clown returns to Miles County where he must hunt down and destroy a teenage girl and her younger brother on Halloween night.  As the body count rises, the siblings fight to stay alive while uncovering the true nature of Art's evil intent.",
    "popularity": 5162.285,
    "poster_path": "/yw8NQyvbeNXoZO6v4SEXrgQ27Ll.jpg",
    "release_date": "2022-10-06",
    "title": "Terrifier 2",
    "video": false,
    "vote_average": 7.4,
    "vote_count": 176
  },
  {
    "adult": false,
    "backdrop_path": "/bQXAqRx2Fgc46uCVWgoPz5L5Dtr.jpg",
    "genre_ids": [28,878,14],
    "id": 436270,
    "original_language": "en",
    "original_title": "Black Adam",
    "overview": "Nearly 5,000 years after he was bestowed with the almighty powers of the Egyptian gods—and imprisoned just as quickly—Black Adam is freed from his earthly tomb, ready to unleash his unique form of justice on the modern world.",
    "popularity": 3835.264,
    "poster_path": "/3zXceNTtyj5FLjwQXuPvLYK5YYL.jpg",
    "release_date": "2022-10-19",
    "title": "Black Adam",
    "video": false,
    "vote_average": 7.1,
    "vote_count": 580
  },
  {
    "adult": false,
    "backdrop_path": "/tIX6j3NzadlwGcJ52nuWdmtOQkg.jpg",
    "genre_ids": [27,53,9648],
    "id": 717728,
    "original_language": "en",
    "original_title": "Jeepers Creepers: Reborn",
    "overview": "Forced to travel with her boyfriend to a horror festival, Laine begins to experience disturbing visions associated with the urban legend of The Creeper. As the festival arrives and the blood-soaked entertainment builds to a frenzy, she becomes the center of it while something unearthly has been summoned.",
    "popularity": 2504.094,
    "poster_path": "/aGBuiirBIQ7o64FmJxO53eYDuro.jpg",
    "release_date": "2022-09-15",
    "title": "Jeepers Creepers: Reborn",
    "video": false,
    "vote_average": 5.8,
    "vote_count": 378
  },
]

// 渲染函数
const movieContainerElement = document.querySelector('movie-container');
const renderAllMovie = (results) => {
    movieContainerElement.movies = results;
}

renderAllMovie(data);

补充说明

原HTML和CSS代码无需修改,保持原样即可。修复后,自定义元素能正常渲染电影内容,评分颜色也能正确显示,鼠标悬停时会展开电影简介。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 17:55:15