Djangoでのいいね非同期処理

### はじめに
Djangoで非同期いいね機能を作成しようとする方に参考になるよう説明していこうと思います。
一番の目的は自身のアウトプットとなります。
駆け出しエンジニアですので、より良い方法や間違っている点などありましたらコメントにてご連絡をいただければ幸いです

### そもそも非同期（Ajax）とは？
非同期処理（Ajax）とは、Webページをリロードせずにサーバーとデータをやり取りできる技術のことです。
簡単に言うとページを更新せずにいいねできる機能などに使われます。
X（旧Twitter）やYouTubeっていいねするとページは読み込まれないで、ハートの部分だけが更新されますよね。それと全く同じと思っていただいてOKです。

Ajaxは主にJavaScriptを使い、XMLHttpRequest や fetchAPI を利用してサーバーと通信します。

早速ですが、まずは完成品からお見せします。

### モデル構造について

#### 記事モデル（Article）

```
class Article(models.Model):
    title = models.CharField(verbose_name='タイトル', default='タイトルです。', max_length=30, null=False, blank=False)
    text = models.TextField(verbose_name='テキスト', default='テキストです。', max_length=255, null=False, blank=False)
    author = models.ForeignKey(get_user_model(), verbose_name='作成者', on_delete=models.CASCADE, db_index=True, related_name='articles')
    created_at = models.DateTimeField(verbose_name='作成日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)
```

#### いいねモデル（ArticleLike）

```
user = models.ForeignKey(get_user_model(), verbose_name='投稿者', on_delete=models.CASCADE, db_index=True)
    article = models.ForeignKey(Article, verbose_name='記事', on_delete=models.CASCADE, db_index=True, related_name='article_like')
    created_at = models.DateTimeField(verbose_name='作成日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)
```

### urls.py
```
path('<int:pk>/like/', views.ArticleLikeView.as_view(), name='like_detail'),
```

### views.py

#### ArticleLikeView（非同期）
```
class ArticleLikeView(CustomLoginRequiredMixin, View):

    def post(self, request, pk, *args, **kwargs):

        # javascriptから送られてきたbodyの中身を取得
        data = json.loads(request.body)

        # bodyの中のarticle_pkを取得
        article_pk = data.get('article_pk')

        context = {
            "message": "error",
        }

        article = Article.objects.get(pk=article_pk)
        # article = Article.objects.get(pk=pk)でも可能

        try:
            like_exists = article_like_exists(article, request.user)

            if like_exists:
                article_like = ArticleLike.objects.create(user=request.user, article=article)
                context["method"] = "create"

            else:
                ArticleLike.objects.filter(user=request.user, article=article).delete()
                context["method"] = "delete"
            context['message'] = 'success'
            # いいねのカウント数を集計
            context["like_count"] = article.article_like.count()
        except:
            pass

        return JsonResponse(context)
```

### フロント側（HTML & JS）

```
<div class="d-inline my-1">
    <button type='button' id='like_btn' class="btn btn-outline-danger">
        {% if like_count > 0 %}
            <i class="bi bi-suit-heart-fill text-danger" id="like-for-post-icon"></i>
        {% else %}
            <i class="bi bi-suit-heart" id="like-for-post-icon"></i>
        {% endif %}
        <span id="like_count">{{ like_count }}件のいいね</span>
    </button>
</div>

#### ここまでがhtml

#### ここからJSを使った非同期処理

<script>
    window.addEventListener("DOMContentLoaded", (e) => {
        const likeBtn = document.getElementById('like_btn');

        // いいねボタンがクリックされたら
        likeBtn.addEventListener('click', async(e) => {
            // 送信先URL
            const url = "{% url 'blog:like_detail' article.id %}";
            // csrf_tokenの取得
            const csrfToken = getCookie("csrftoken")
            // 送信したいデータ
            const data = {
                article_pk: "{{ article.id }}",
            }

            try {
                // axiosを使い、非同期でAPI（view）に送信する
                request = await axios.post(url, data, {
                    headers: {
                        "Content-Type": "application/json",
                        "X-Requested-With": "XMLHttpRequest",
                        "X-CSRFToken": csrfToken,
                    },
                })
                const response = request.data

                // viewsから送られてくるJsonResponseを受け取る
                if (response.message == "success") {
                    const LikeCount = document.getElementById("like_count");
                    const icon = document.getElementById('like-for-post-icon')

                    // いいねの数を上書き
                    LikeCount.textContent = response.like_count + '件のいいね'

                    // いいねの色を切り替え
                    if (response.method == "create") {
                        icon.classList.remove('bi-suit-heart');
                        icon.classList.add('bi-suit-heart-fill', 'text-danger');
                    } else {
                        icon.classList.remove('bi-suit-heart-fill', 'text-danger');
                        icon.classList.add('bi-suit-heart');
                    }
                    icon.id = 'like-for-post-icon'
                }
            } catch(err) {
                console.log(err)
            }
        });
    });

    // cookieのキーがあれば取得
    // 指定した名前 (name) のクッキーを探し、その値をデコードして返す
    function getCookie(name) {
        if (document.cookie && document.cookie != "") {
            for (var cookie of document.cookie.split(';')) {
                var [key, value] = cookie.trim().split("=");
                if (key == name) {
                    return decodeURIComponent(value);
                }
            }
        }
    }

</script>
```

はい、何がなんだかわからないですよね
ですが大丈夫です。1つ1つ説明していきます。

### 解説

#### Django側についての解説

まず、urls.pyに関してです。
こちらは、該当のURLに対して、このviewで処理するよという役目をしています。
この辺りはDjangoの基礎になるので、わからない方はまずは基礎から勉強してみましょう。

続いて、viewsですが、こちらも難しい書き方ですよね。
1つづつ見ていきます。

まず、JavaScript（フロント）からどのようにデータを取得するのかということになってきますが、以下のコードでJSから送られてきたデータを取得しています。

```
# javascriptから送られてきたbodyの中身を取得
data = json.loads(request.body)

# bodyの中のarticle_pkを取得
article_pk = data.get('article_pk')
```

まず、```json.loads(request.body)```の意味です。
JavaScriptから送られてきたデータは**JSON**となります。
JSONの中身は以下のようになっています。
```
{
  "name": "太郎",
  "age": 22,
  "email": "taro@example.com",
  "skills": ["Python", "JavaScript", "Django"],
  "ok": true,
  "value": null
}
```
これを```json.loads```を使ってpythonが読み解ける辞書に直します。
辞書とは以下のものを言います。
```
{
    "name": "太郎",
    "age": 22,
    "email": "taro@example.com",
    "skills": ["Python", "JavaScript", "Django"],
    "ok": True,
    "value": None
}

```
いや同じやないかいと思いますが、JavaScriptでの```null```や```true```はpythonでは``None```、```True```と書きます。
こんな感じでJSONからpythonがわかる辞書型に直してくれると思えばOKです。

では続いて```data.get('article_pk')```ですが、これはpythonの辞書から```article_pk```というキーから値を取得しています。
```
{
    "name": "太郎",
    "age": 22,
    "email": "taro@example.com",
    "skills": ["Python", "JavaScript", "Django"],
    "ok": True,
    "value": None,
    "article_pk": "！！これを取得する！！"
}

```

ちなみにgetメソッドはキーが存在しない場合はNoneを返します。


あとは```return JsonResponse(context)```を使ってJavaScriptに**JSON**を返します。
JsonResponseとは、そのままの通りレスポンスがJSONになるよという意味です。
contextの中身はこんな感じになっています。
```
context = {
    "message" : "success"
    "method" : "create",
    "like_count": 3
}
```

これをJavaScript側に返して、contextの中身を元にUIを変更していきます。

#### JavaScript側についての解説

では続いて皆さんが一番知りたいであろうJavaScriptでの書き方についてです。

まずですが、
```
window.addEventListener("DOMContentLoaded", (e) => {
    ~~~~~~~~~
    ~~~~~~~~~
});
```
こちらについてです。
これは「ページのDOM（要素ツリー）が読み込まれたら処理を始めたい」時に使うものです。
ん？と思うかもですが、一種のおまじないだと思ってください。
ちなみに、これは書かなくても大丈夫です。

続いては軽く流しますが、
```
const likeBtn = document.getElementById('like_btn');
```

これはHTML側で「like_btn」というIDを設置している場所を見つけて、その要素（DOM）を定数に格納しますよということを行っています。


続いてView側にAPIリクエストを送るデータの作成箇所になります。

```
// いいねボタンがクリックされたら
likeBtn.addEventListener('click', async(e) => {
    // 送信先URL
    const url = "{% url 'blog:like_detail' article.id %}";
    // csrf_tokenの取得
    const csrfToken = getCookie("csrftoken");
    // 送信したいデータ
    const data = {
        article_pk: "{{ article.id }}",
    }
    ~~~~~~~
    ~~~~~~~
});
```


先ほど、likeBtnに格納したデータの中にあるボタンを押したタイミングを検知しています。
ボタンが押されたら（click）発火する仕組みになっています。

続いて、リクエストを送りたいURLを設定しています。
ここではDjangoの記述が書けるので、```{% url 'blog:like_detail' article.id %}```で送信先のURLを設定しています。

```
const csrfToken = getCookie("csrftoken")
```
上記については、csrf_tokenをCookieから取得しますよということを行っています。
セキュリティ対策ですね、form送信の際はDjango側でも必ず使っていますよね。それと同じでJSから送信するときも使います。
Djangoだと```{% csrf_token %}```これに該当します。

あとは送信したいデータを格納してDjangoに送るだけです。
ここではJSONで書き込んでいきます。
```
const data = {
    article_pk: "{{ article.id }}",
}
```

ん、さっき見たようなと思った方は大正解です。これがDjangoのViewで処理していたものですね。
```
data = json.loads(request.body)
article_pk = data.get('article_pk')
```

こんな感じでJSとDjangoでやりとりしています。


続いてですが、
```
try {
    // axiosを使い、非同期でAPI（view）に送信する
    request = await axios.post(url, data, {
        headers: {
            "Content-Type": "application/json",
            "X-Requested-With": "XMLHttpRequest",
            "X-CSRFToken": csrfToken,
        },
    })
    ~~~~~~~~
    ~~~~~~~~
```

これは```axios```というリクエストを送信してくれるメソッドを使い、DjangoにAPIリクエストを送信しています。
ここが一番難しいかなと思いますが、頑張りましょう。

まず、```axios```ってなにからですよね。
これは簡単に言えばDjango側のViewに接続するためのものです。

で、axiosを使うには```async```と```await```が必要になります。
これもなんぞや！と思うかもですが、この2つはJSの通常の関数を非同期関数に変えてくれるスーパーマンだと思ってください。

asyncが関数 → 非同期関数にしてくれる役割を持ち、
awaitがその関数の中で特にレスポンスを待ちたいものを「a wait」止まってくれ！という役目を持っています。

現在のフロントではめちゃくちゃ使うものなので覚えておきましょう！

で、話は戻りますが、APIリクエストを送信するURLとdateを用意してあるので、これを元にリクエストを送ります。
```
headers: {
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest",
    "X-CSRFToken": csrfToken,
}
```

このheadersの中身はおまじないだと思ってください。
このheaders情報は認証などが入っているものになりますので、書かないと怒られます。

最後に
```
const response = request.data

// viewsから送られてくるJsonResponseを受け取る
if (response.message == "success") {
    const LikeCount = document.getElementById("like_count");
    const icon = document.getElementById('like-for-post-icon')

    // いいねの数を上書き
    LikeCount.textContent = response.like_count + '件のいいね'

    // いいねの色を切り替え
    if (response.method == "create") {
        icon.classList.remove('bi-suit-heart');
        icon.classList.add('bi-suit-heart-fill', 'text-danger');
    } else {
        icon.classList.remove('bi-suit-heart-fill', 'text-danger');
        icon.classList.add('bi-suit-heart');
    }
    icon.id = 'like-for-post-icon'
}
```
ここの部分ですが、
これはあまり難しくないのではないでしょうか？

```const response = request.data```でDjangoから結果を受け取ります。
Django側でJSONResponceした内容が```request.data```に入りますので、レスポンスとして定数に格納しておきます。

中身はこんな感じです。
```
response = {
    "message" : "success"
    "method" : "create",
    "like_count": 3
}
```

このレスポンスを使って現在のいいね数だったり、いいねを付けたのか、それとも解除したのかを判定しています。
あとはresponseの中身によってclassを付けたりしているだけとなります。


### まとめ

以上でDjangoでのいいね非同期処理についての解説を終わります。
最初にも申し上げましたが、これは「未来の自分への記録として残す」ことがメインとなっていますので、適当な部分もあれば、ここわかりにくそうだなと思う箇所はまぁ丁寧に解説してる？？？かと思います。

ちなみに、このいいね機能はJavaScriptだとコード量多くて面倒ですよね

それをカバーしてめっちゃスッキリした書き方に変えてくれるのが「React」や「Vue」になります。
概念は難しいですが、味方になるとめちゃくちゃ頼もしいJSフレームワーク君です！

最後におまけで簡単ないいね機能を書いときます！

#### Reactでいいね機能を実装した場合
```
import { useState } from "react";

function LikeButton() {
  const [liked, setLiked] = useState(false);

  return (
    <button onClick={() => setLiked(prev => !prev)}>
      {liked ? "🩷" : "🤍"}
    </button>
  );
}
```

あら、なんと簡単なんでしょう。
では以上です！！