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

如何用JavaScript在类Twitter Web应用中异步切换点赞/取消点赞按钮

问题:CS50W类Twitter应用点赞按钮异步切换失效

我正在修读哈佛大学CS50W在线课程,开发类Twitter Web应用。需求是根据用户是否点赞过帖子,显示「点赞」或「取消点赞」按钮,同时异步更新点赞计数器。目前已实现点赞数的无刷新更新,但点击按钮后必须刷新页面才能切换按钮状态,无法异步切换。

相关代码

models.py

class Post(models.Model):
    """ Model representing a post. """
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    content = models.TextField()
    timestamp = models.DateTimeField(auto_now_add=True)
    no_of_likes = models.IntegerField(default=0)

    def __str__(self):
        return f"Post {self.id} by {self.user.username} on {self.timestamp}"
    
class Like(models.Model):
    """ Model representing a like. """
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    timestamp = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.user} likes {self.post}"

urls.py

path("", views.index, name="index"),
path("like/<int:post_id>", views.like, name="like"),
path("unlike/<int:post_id>", views.unlike, name="unlike"),

views.py

def index(request):
    """ Home page. """
    posts = Post.objects.all().order_by('-timestamp')
    paginator = Paginator(posts, 5)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)
    likes = Like.objects.all()  
    
    # Make a list of liked posts.
    liked_posts = []
    try:
        for like in likes:
            if like.user.id == request.user.id:
                liked_posts.append(like.post.id)
    except:
        liked_posts = []

    return render(request, "network/index.html", {
        "posts": posts,
        "page_obj": page_obj,
        "likes": likes,
        "liked_posts": liked_posts,             
    })

@login_required
def like(request, post_id):
   
    post = Post.objects.get(pk=post_id)
    user = User.objects.get(pk=request.user.id)
    like = Like.objects.create(user=user, post=post)
    like.save()

    # Update no of likes.
    post.no_of_likes = Like.objects.filter(post=post).count()
    post.save()
    return JsonResponse({"message": "successfully liked", "no_of_likes": post.no_of_likes})

@login_required   
def unlike(request, post_id):
    
    post = Post.objects.get(pk=post_id)
    user = User.objects.get(pk=request.user.id)
    like = Like.objects.filter(user=user, post=post)
    like.delete()

    # Update no of likes.
    post.no_of_likes = Like.objects.filter(post=post).count()
    post.save()
    return JsonResponse({"message": "successfully unliked", "no_of_likes": post.no_of_likes})

index.html(含JavaScript)

{% if post.id not in liked_posts %}
    <button type="button" class="btn btn-primary" id="like{{ post.id }}" onclick="like('{{ post.id }}')">Like</button>
{% else %}
    <button type="button" class="btn btn-primary" id="unlike{{ post.id }}" onclick="unlike('{{ post.id }}')">Unlike</button>
{% endif %}

<script>
function like(id) {
        fetch(`/like/${id}`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "X-CSRFToken": "{{ csrf_token }}",
            },
        })
        .then(response => response.json())
        .then(data => {
            document.querySelector(".no-of-likes").innerHTML = data.no_of_likes + " likes";
        })
        .then(() => {
            document.getElementById("unlike" + id).style.display = "block";
            document.getElementById("like" + id).style.display = "none";
        });
    }

function unlike(id) {
        fetch(`/unlike/${id}`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "X-CSRFToken": "{{ csrf_token }}",
            },
        })
        .then(response => response.json())
        .then(data => {
            document.querySelector(".no-of-likes").innerHTML = data.no_of_likes + " likes";
        })
        .then(() => {
            document.getElementById("unlike" + id).style.display = "none";
            document.getElementById("like" + id).style.display = "block";
        });
    }   
</script>

问题表现

  • 点击点赞按钮后,点赞数更新,但必须刷新页面才能切换为取消点赞按钮;
  • 点击取消点赞按钮后,点赞数更新,但必须刷新页面才能切换为点赞按钮。

解决方案

核心问题

页面渲染时只会生成「点赞」或「取消点赞」中的一个按钮,另一个按钮不存在于DOM中,导致JS无法通过display属性切换状态。

修复步骤

1. 修改HTML模板,同时渲染两个按钮

把原有的if-else逻辑改为同时渲染两个按钮,通过初始CSS状态控制显示:

<!-- 同时渲染两个按钮,根据初始点赞状态控制显示 -->
<button type="button" class="btn btn-primary" id="like{{ post.id }}" onclick="like('{{ post.id }}')"
        {% if post.id in liked_posts %}style="display:none;"{% endif %}>
    Like
</button>
<button type="button" class="btn btn-primary" id="unlike{{ post.id }}" onclick="unlike('{{ post.id }}')"
        {% if post.id not in liked_posts %}style="display:none;"{% endif %}>
    Unlike
</button>

<!-- 给每个帖子的点赞计数器添加唯一ID,避免全局选择错误 -->
<div class="no-of-likes" id="likes-count-{{ post.id }}">{{ post.no_of_likes }} likes</div>

2. 修正JavaScript中的选择器问题

原代码中document.querySelector(".no-of-likes")会选中页面第一个匹配元素,导致所有帖子点赞数同步错误,需改为针对当前帖子的计数器:

function like(id) {
    fetch(`/like/${id}`, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "X-CSRFToken": "{{ csrf_token }}",
        },
    })
    .then(response => response.json())
    .then(data => {
        // 更新当前帖子的点赞数
        document.getElementById(`likes-count-${id}`).innerHTML = `${data.no_of_likes} likes`;
        // 切换按钮显示状态
        document.getElementById(`like${id}`).style.display = "none";
        document.getElementById(`unlike${id}`).style.display = "block";
    })
    .catch(error => console.error('点赞失败:', error));
}

function unlike(id) {
    fetch(`/unlike/${id}`, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "X-CSRFToken": "{{ csrf_token }}",
        },
    })
    .then(response => response.json())
    .then(data => {
        // 更新当前帖子的点赞数
        document.getElementById(`likes-count-${id}`).innerHTML = `${data.no_of_likes} likes`;
        // 切换按钮显示状态
        document.getElementById(`unlike${id}`).style.display = "none";
        document.getElementById(`like${id}`).style.display = "block";
    })
    .catch(error => console.error('取消点赞失败:', error));
}

3. 优化views.py中的点赞列表生成逻辑(可选)

原代码遍历所有Like对象效率低下,改为直接过滤当前用户的点赞记录:

def index(request):
    """ Home page. """
    posts = Post.objects.all().order_by('-timestamp')
    paginator = Paginator(posts, 5)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)
    
    # 直接获取当前用户点赞的帖子ID列表
    liked_posts = []
    if request.user.is_authenticated:
        liked_posts = Like.objects.filter(user=request.user).values_list('post__id', flat=True)

    return render(request, "network/index.html", {
        "page_obj": page_obj,
        "liked_posts": liked_posts,             
    })

额外优化建议

  • 移除Post模型中的no_of_likes字段,通过post.like_set.count()实时获取点赞数,避免数据不一致;
  • 给Like模型添加唯一约束,防止用户重复点赞:
class Like(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    timestamp = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('user', 'post')  # 确保同一用户不能重复点赞同一帖子

    def __str__(self):
        return f"{self.user} likes {self.post}"

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 14:20:02