我正在使用 Alpine.js 通过 API 渲染视频列表。进行 API 调用后,响应数据成功填充我的视频数组,但页面未更新以显示新数据。
以下是所发生事件的详细说明:
我正在发出 Axios API 请求并将生成的视频推送到视频数组。 我已通过控制台日志确认视频填充了正确的数据。 尽管如此,模板不会渲染更新后的数组。
谢谢!
<template x-for="video in videos" :key="video.id">
<!--video content-->
<div>Test Video</div>
</template>
const app = {
triggerElement: null,
page: 1,
lastPage: 24,
itemsPerPage: 24,
observer: null,
isObserverPolyfilled: false,
videos: [],
debug: true,
loaded: false,
init: function () {
window.addEventListener('urlChange', () => {
app.getItems();
});
app.triggerElement = document.querySelector('#infinite-scroll-trigger');
document.addEventListener('DOMContentLoaded', function () {
// app.getItems();
app.loaded = true;
});
app.infiniteScroll();
(app.debug) ? console.log('init' ) : '';
},
infiniteScroll: function () {
(app.debug) ? console.log('infiniteScroll' ) : '';
// Check if browser can use IntersectionObserver which is waaaay more performant
if (!('IntersectionObserver' in window) ||
!('IntersectionObserverEntry' in window) ||
!('isIntersecting' in window.IntersectionObserverEntry.prototype) ||
!('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
// Loading polyfill since IntersectionObserver is not available
this.isObserverPolyfilled = true
// Storing function in window so we can wipe it when reached last page
window.alpineInfiniteScroll = {
scrollFunc() {
var position = app.triggerElement.getBoundingClientRect()
if (position.top < window.innerHeight && position.bottom >= 0) {
if (app.loaded) {
(app.debug) ? console.log('getItems 1' ) : '';
app.getItems();
}
}
}
}
window.addEventListener('scroll', window.alpineInfiniteScroll.scrollFunc)
} else {
// We can access IntersectionObserver
this.observer = new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting === true) {
if (app.loaded) {
(app.debug) ? console.log('getItems 2' ) : '';
app.getItems();
}
}
}, {threshold: [0]})
this.observer.observe(this.triggerElement)
}
},
getItems: function () {
// TODO: Do fetch here for the content and concat it to populated items
// TODO: Set last page from API call - ceil it
let currentURL = new URL(window.location.href);
currentURL.hostname = 'api.' + currentURL.hostname;
axios.post(currentURL, {
page: this.page,
perPage: this.itemsPerPage,
})
.then(response => {
const data = response.data;
// this.lastPage = data.total_pages;
app.videos = data.data.videos;
// this.page++;
(app.debug) ? console.log(response) : '';
})
.catch(error => {
console.error('Error loading videos:', error);
(app.debug) ? console.log(error) : '';
this.loading = false;
});
// Next page
this.page++
// We have shown the last page - clean up
if (this.lastPage && this.page > this.lastPage) {
if (this.isObserverPolyfilled) {
window.removeEventListener('scroll', window.alpineInfiniteScroll.scrollFunc)
(app.debug) ? console.log('alpineInfiniteScroll') : '';
} else if (this.observer && this.triggerElement) {
try {
this.observer.unobserve(this.triggerElement);
} catch (e) {
console.error('Failed to unobserve element:', e);
}
}
if (this.triggerElement && this.triggerElement.parentNode) {
this.triggerElement.parentNode.removeChild(this.triggerElement);
this.triggerElement = null; // Prevent further access to the removed element
}
}
}
};
更新视频后使用 this.$nextTick() 以确保 Alpine.js 更新。 手动触发 Alpine.js 反应性。 直接操作 DOM 作为临时解决方法(并不理想)。 这些方法都没有解决问题。如果您了解 Alpine.js 可能不重新渲染更新后的数据或任何替代解决方案的原因,我将不胜感激。
我尝试在本地计算机上重现您的问题,并最终编写了一个完整的工作示例。我希望它能帮助您与实际代码库进行比较,看看有什么问题:
index.html
确保将所有内容都用
div
和 x-data
包裹到 x-init
中。
您还会注意到,我创建了一个函数,该函数返回一个应用程序对象,并对您的代码进行了一些更改,如下面的 js 示例所示。通过在组件中定义 init 并通过 x-init 调用它,您可以确保您的设置代码在组件初始化时运行。
<div x-data="app()" x-init="init">
<!-- Video List -->
<div id="video-list">
<template x-for="video in videos" :key="video.id">
<div class="video">
<h3 x-text="video.title"></h3>
<p x-text="video.description"></p>
</div>
</template>
</div>
<!-- Infinite Scroll Trigger -->
<div id="infinite-scroll-trigger"></div>
</div>
app.js
function app() {
return {
// Data properties
triggerElement: null,
page: 1,
lastPage: null,
itemsPerPage: 10,
observer: null,
isObserverPolyfilled: false,
videos: [],
debug: true,
loading: false,
// Initialization method
init() {
this.triggerElement = document.querySelector("#infinite-scroll-trigger");
this.infiniteScroll();
if (this.debug) console.log("Initialization complete.");
},
// Method to set up infinite scroll
infiniteScroll() {
if (this.debug) console.log("Setting up infinite scroll.");
const supportsIntersectionObserver = "IntersectionObserver" in window;
if (supportsIntersectionObserver) {
// Use IntersectionObserver for better performance
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
threshold: 0,
}
);
this.observer.observe(this.triggerElement);
} else {
// Fallback for browsers without IntersectionObserver support
this.isObserverPolyfilled = true;
this.scrollFunc = this.handleScroll.bind(this);
window.addEventListener("scroll", this.scrollFunc);
}
},
// Handler for IntersectionObserver
handleIntersection(entries) {
if (entries[0].isIntersecting && !this.loading) {
if (this.debug)
console.log("Trigger element intersected. Loading items...");
this.getItems();
}
},
// Handler for scroll event (polyfill)
handleScroll() {
const position = this.triggerElement.getBoundingClientRect();
if (
position.top < window.innerHeight &&
position.bottom >= 0 &&
!this.loading
) {
if (this.debug)
console.log("Trigger element visible. Loading items...");
this.getItems();
}
},
// Method to fetch items from the mock API
async getItems() {
if (this.loading) return; // Prevent multiple calls
this.loading = true;
try {
const response = await axios.get("http://localhost:3000/videos", {
params: {
_page: this.page,
_limit: this.itemsPerPage,
},
});
const totalItems = response.headers["x-total-count"];
this.lastPage = Math.ceil(totalItems / this.itemsPerPage);
this.videos = this.videos.concat(response.data);
if (this.debug) console.log(`Loaded page ${this.page}.`, response.data);
this.page++;
// Check if we've reached the last page
if (this.page > this.lastPage) {
this.cleanup();
}
} catch (error) {
console.error("Error loading videos:", error);
} finally {
this.loading = false;
}
},
// Cleanup method to remove observers and event listeners
cleanup() {
if (this.isObserverPolyfilled && this.scrollFunc) {
window.removeEventListener("scroll", this.scrollFunc);
if (this.debug) console.log("Removed scroll event listener.");
}
if (this.observer) {
this.observer.disconnect();
if (this.debug) console.log("Disconnected IntersectionObserver.");
}
if (this.triggerElement) {
this.triggerElement.remove();
this.triggerElement = null;
if (this.debug) console.log("Removed trigger element.");
}
},
};
}
JavaScript 文件的关键更新:
this
语句而不是app
引用属性和方法。在 Alpine 组件的方法中, this 指的是组件的数据对象。使用它,您可以访问和修改反应属性。