Twitterクローンを作ります #23 リファクタリング

直したい部分がいくつか溜まってきているので修正していきましょう。

# カスタム JSON エンコーダー

それぞれのAPIで、`user['_id'] = str(user['_id']` みたいなことをひたすらやっていますがかっこ悪いので自動でやってくれるようにしましょう。

```python:server/main.py
import logging
from bson.objectid import ObjectId


from flask import Flask
from flask.json import JSONEncoder

app = Flask(__name__)
app.secret_key = "DUMMY"
app.config["SESSION_COOKIE_NAME"] = "__session"
app.logger.setLevel(logging.INFO)

class CustomJSONEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, ObjectId):
            return str(obj)
        return super().default(obj)


app.json_encoder = CustomJSONEncoder

from apis import login, posts, users
```

これで、ある程度は解消しましたが、セッションにログインユーザーを保存する部分は依然として str(...) が残ってしまっていますね。

こちらはうまい方法が思いつかなかったので User クラスにまとめておきます。

```python:server/models/user.py
...
    @staticmethod
    def set_session_user(user):
        del user["hashed_password"]
        user["_id"] = str(user["_id"])
        if "following" not in user:
            user["following"] = []
        user["following"] = [str(_id) for _id in user["following"]]
        if "followers" not in user:
            user["followers"] = []
        user["followers"] = [str(_id) for _id in user["followers"]]
        session["user"] = user

```

# エラーレスポンス

現状ちゃんと厳密に従っているわけではないのですが、レスポンスは https://google.github.io/styleguide/jsoncstyleguide.xml に合わせておこうかなと思っているので、エラーレスポンスを `return jsonify({"error": {"message": "投稿が存在しません"}}), 404` のような形で返しています。

が、毎回これを書くのがだるいので関数化しておきましょう。

```python:server/api/__init__.py
...

def error(message, status=400):
    return jsonify({"error": {"message": message}}), status

```

# アクションの引数, 戻り値

アクション（とミューテーション)の引数はオブジェクトで渡すべきだったので修正します。

commit はアクション内で行うべきですが、その後の処理やエラー発生時の処理は呼び出し側で指定すべきだと思うので必ず axios.xxx の戻り値を返すようにします。

ただ、エラーハンドリングは多くの箇所で共通になるはずなので、指定したいときだけ上書きできるようにしましょう。

```javascript:client/src/store/index.js
import axios from "axios";
import UIkit from "uikit";
import { createStore } from "vuex";

import router from "@/router";

function defaultErrorHandler(error) {
  if (error.response.status === 401) {
    router.push("/login");
  } else {
    UIkit.notification(error.response.data.error.message, {
      status: "danger",
    });
  }
}

export default createStore({
  state: {
    userLoggedIn: undefined,
    posts: [],
    profile: {
      user: {
        display_name: "",
        username: "",
      },
    },
  },
  getters: {},
  mutations: {
    setUserLoggedIn(state, args) {
      state.userLoggedIn = args.user;
    },
    setPosts(state, args) {
      state.posts = args.posts;
    },
    prependPost(state, args) {
      state.posts.unshift(args.post);
    },
    updatePost(state, args) {
      const orig = state.posts.find((p) => p._id === args.post._id);
      if (orig) {
        Object.assign(orig, args.post);
      }
      state.posts
        .filter(
          (p) => p.retweeted_post && p.retweeted_post._id === args.post._id
        )
        .forEach((p) => Object.assign(p.retweeted_post, args.post));
    },
    setProfileUser(state, args) {
      state.profile.user = args.user;
    },
  },
  actions: {
    async registerUser({ commit }, args) {
      return axios.post("/api/users", args.user).then((resp) => {
        commit("setUserLoggedIn", { user: resp.data });
      });
    },
    async login({ commit }, args) {
      return axios.post("/api/login", args.credential).then((resp) => {
        commit("setUserLoggedIn", { user: resp.data });
      });
    },
    async logout({ commit }) {
      return axios.post("/api/logout").then(() => {
        commit("setUserLoggedIn", { user: undefined });
      });
    },
    async loginCheck({ commit }) {
      return axios.get("/api/session").then((resp) => {
        commit("setUserLoggedIn", { user: resp.data });
      });
    },
    async createPost({ commit, state }, args) {
      const body = {
        content: args.content,
        posted_by: state.userLoggedIn._id,
      };
      return axios.post("/api/posts", body).then((resp) => {
        commit("prependPost", { post: resp.data });
      });
    },
    async loadPosts({ commit }) {
      return axios
        .get("/api/posts")
        .then((resp) => {
          commit("setPosts", { posts: resp.data });
        })
        .catch(defaultErrorHandler);
    },
    async likePost({ commit }, args) {
      return axios
        .post(`/api/posts/${args.post._id}/like`)
        .then((resp) => {
          commit("updatePost", { post: resp.data });
        })
        .catch(defaultErrorHandler);
    },
    async unlikePost({ commit }, args) {
      return axios
        .delete(`/api/posts/${args.post._id}/like`)
        .then((resp) => {
          commit("updatePost", { post: resp.data });
        })
        .catch(defaultErrorHandler);
    },
    async retweetPost({ commit }, args) {
      return axios
        .post(`/api/posts/${args.post._id}/retweet`)
        .then((resp) => {
          commit("updatePost", { post: resp.data });
        })
        .catch(defaultErrorHandler);
    },
    async unretweetPost({ commit }, args) {
      return axios
        .delete(`/api/posts/${args.post._id}/retweet`)
        .then((resp) => {
          commit("updatePost", { post: resp.data });
        })
        .catch(defaultErrorHandler);
    },
    async loadProfileUser({ commit }, args) {
      return axios
        .get(`/api/users/${args.username}`)
        .then((resp) => {
          commit("setProfileUser", { user: resp.data });
        })
        .catch(defaultErrorHandler);
    },
    async followUser({ commit }, args) {
      return axios
        .post(`/api/users/${args.username}/follow`)
        .then((resp) => {
          commit("setUserLoggedIn", { user: resp.data });
        })
        .catch(defaultErrorHandler);
    },
    async unfollowUser({ commit }, args) {
      return axios
        .delete(`/api/users/${args.username}/follow`)
        .then((resp) => {
          commit("setUserLoggedIn", { user: resp.data });
        })
        .catch(defaultErrorHandler);
    },
  },
  modules: {},
});


```

エラーハンドラを呼び出し側で書き換えるようになってはいませんがいったんこれでいいでしょう。

あと、ホーム画面で投稿しても投稿一覧に変化がなかったので、先頭に表示されるように修正しました。

# ログイン画面, ユーザー登録画面のリンク

ログイン画面に、ユーザー登録がまだなら登録してね、的なリンクを追加します。


重複したコードがかなり整理されましたし、責務が曖昧だったところが明確になりましたね。