弊社アドベントカレンダーで一番いいね！を獲得したユーザは誰か？

## この記事は

[FORK Advent Calendar 2019](https://qiita.com/advent-calendar/2019/fork) の 23日目の記事です。
Vue.js (Vue CLI with TypeScript) と Qiita API で Oraganization ビューアを作ったお話です。

### モチベーション

ふと先日**「アドベントカレンダーでいちばん多くいいね！を獲得したユーザに、サンタが何かをプレゼントしてくれれば、良質な記事が社内のエンジニアたちからわしゃわしゃ集まる」**ことに気が付いてしまいました。そこで [Qiita API](https://qiita.com/api/v2/docs) と [Vue CLI](https://cli.vuejs.org/) を使って、弊社の Qiita メンバーの月毎の投稿記事の「いいね！」数を表示する Qiita ビューアを作りました。


### おことわり

- 突貫で開発を行ったため、アプリの挙動が若干あやしい**（言い訳）**
- 一般公開すると Qiita API のリクエスト制限数（後述）を超える可能性がある**（多分ないけど）**

以上の理由から完成版の URL は開示しておりません。
開発のポイントと併せて、サンプルのコードを記載しておりますので、なんとなく実装の内容をお伝えできれば幸いです。



### 完成したアプリのキャプチャ

こんな感じのアプリです。本日までのランキングでは @yoh_zzzz さんの Nuxt + Firebase の記事がトップですね。
<img width="928" alt="スクリーンショット 2019-12-23 10.37.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37412/dfc28259-c7eb-23cd-dcc2-c0d43139041a.png">




## 開発のポイント

開発の要所を 3 分間クッキング :egg: でお届けします。

### 1. Qiita API のインターフェースを定義する

開発は TypeScript で行います。（しれっと言い切るスタイル）
ブラウザで API を叩いたときのレスポンスや、[JSON Schema](https://qiita.com/api/v2/schema) を確認しながら、API のインターフェースを定義します。型定義ファイルはどこに配置しても良いのですが、僕は Vuex のモジュールディレクトリに用意しました。余談ですが、個人的にこの作業がとても楽しく、不安になります。


▼ ユーザ周りインターフェース

```ts:/src/store/user/types.ts
export interface QiitaUser {
  description: string | null
  facebook_id: string | null
  followees_count: number
  followers_count: number
  github_login_name: string | null
  id: string
  items_count: number
  linkedin_id: string | null
  location: string | null
  name: string | null
  organization: string | null
  permanent_id: number
  profile_image_url: string
  team_only: boolean
  twitter_screen_name: string | null
  website_url: string | null
}

export interface UserState {
  list: QiitaUser[]
}
```


開くとソースコードが表示されます。:point_down:
<details><summary>投稿記事周りインターフェース</summary><div>

```ts:/src/store/post/types.ts
export interface QiitaGroup {
  created_at: string
  id: number
  name: string
  private: boolean
  updated_at: string
  url_name: string
}

export interface QiitaTag {
  name: string
  versions: string[]
}

export interface QiitaPost {
  rendered_body: string
  body: string
  coediting: boolean
  comments_count: number
  created_at: string
  group: QiitaGroup | null
  id: string
  likes_count: number
  private: boolean
  reactions_count: number
  tags: QiitaTag[]
  title: string
  updated_at: string
  url: string
  user: QiitaUser
  page_views_count: number | null
}

export interface PostState {
  list: QiitaPost[]
}
```
</div></details>



### 2. API 呼び出し時にリクエストトークンを含める

Qiita API は get リクエストの場合はトークンなしでも呼び出しが可能なのですが、トークンの有り/無しで [利用制限](https://qiita.com/api/v2/docs#%E5%88%A9%E7%94%A8%E5%88%B6%E9%99%90) （1 時間あたりに呼び出しが可能な回数）に大きな差があります。60 回 / 1 時間以上のリクエストが必要な場合はトークンが必要にります。トークンの取得は OAuthを利用した認可フローか、[ユーザの管理画面](https://qiita.com/settings/applications) から発行できます。今回は時間もなかったため後者を選択しました。
以下は、Vuex store モジュールの action として定義した、投稿記事取得 API のリクエストです。


<details><summary>投稿記事取得 action </summary><div>

```ts:/src/store/post/actions.ts
const actions: ActionTree<PostState, RootState> = {
  async fetchPosts(context: ActionContext<PostState, RootState>, payload: { userId: string, page?: number, perPage?: number }): Promise<QiitaPost[]> {
    return new Promise(async (resolve, reject) => {
      const url: string = `https://qiita.com/api/v2/users/${payload.userId}/items?page=${payload.page}&per_page=${payload.perPage}`
      const accessToken: string = 'XXXXXXXXX'
      const config: AxiosRequestConfig = {
        params: { time: Math.floor(Date.now() / 1000 / 60 / 60) }, // キャッシュ対策（1時間毎に異なる文字列を設定）
        headers: { Authorization: `Bearer ${accessToken}` }, // リクエストヘッダにトークンを含める
      }
      const response: AxiosResponse<QiitaPost[]> = await axios.get<QiitaPost[]>(url, config)
      if (response.status === 200) {
        const posts: QiitaPost[] = response.data
        if (posts.length) context.commit('add', posts)
        resolve(posts)
      } else {
        reject(response.status)
      }
    })
  },
}
export default actions
```
</div></details>

### 3. リクエスト回数を最小限にする

一意の Ogranization に所属している Qiita ユーザを取得する API が用意されていないため、弊社の Organization 所属ユーザは、ハードコーディングで用意した静的 JSON ファイルにユーザ ID を列挙することにしました。ユーザ情報は、[/api/v2/users/:user_id](https://qiita.com/api/v2/docs#get-apiv2usersuser_id) から取得できるのですが、この API から各ユーザの投稿記事の合計いいね！数は取得することはできず、すべての投稿記事から各々のいいね！数を合算する必要があります。また投稿記事一覧を取得する API ([/api/v2/users/:user_id/items](https://qiita.com/api/v2/docs#get-apiv2usersuser_iditems)) の戻り値に、記事のユーザ情報がそのまま含まれているため、全所属ユーザの記事一覧を取得し、そこからユーザリストを抽出する Vuex の getters 関数を用意しました。

<details><summary>ユーザリスト取得 getters </summary><div>

```ts:/src/store/post/getters.ts

export const getters: GetterTree<PostState, RootState> = {
  getUsers(state: PostState): QiitaUser[] {
    const users: QiitaUser[] = []
    state.list.forEach((post: QiitaPost) => {
      const user: QiitaUser = post.user
      if (!users.some((usr: QiitaUser): boolean => usr.id === user.id)) {
        users.push(user)
      }
    })
    return users
  },
}

export default getters
```
</div></details>


なお記事取得 API では、1 回のリクエストで最大 100 件の投稿記事を取得することができます。投稿記事数が 100 件を超えるユーザについては、未取得の残りの記事を追加でリクエストするようにしました。
一連の流れを整理すると以下になります。

1. Organization 所属のユーザ ID を自分で用意した JSON から読み込み
2. 全ユーザの投稿記事を 100 件ずつ API から取得 （Vuex action)
3. 取得した投稿記事からユーザー情報を取得 (Vuex getters)
4. 投稿記事が 100 件を超えるユーザについて、残りの記事を取得 (Vuex action)

## おわりに

駆け足での解説となりましたが、ざっくりと要所の雰囲気だけでも伝わったでしょうか。
おそらく今年最後の投稿になるであろう本記事にて Vue のことを書きましたが、実際の仕事でも 2019 年は Vue が活躍してくれました。来年は Vue 3.0 と React を学ぼうと思ってます。

それではみなさま良い年末年始をお過ごしください。:sunrise:

---
[:christmas_tree: FORK Advent Calendar 2019](/advent-calendar/2019/fork)
:arrow_left: 22日目 [正規表現にマッチした文字列を replace を使わずにハイライトさせる](/yshrkn/items/9142acd03e9ab65a13b4) @yshrkn
:arrow_right: 24日目 [Nuxt.jsとmysqlを連携してデータを表示してみた](/ktn/items/a53a791a95315c36f933)  @ktn