NuxtとFirebaseでTwitterまとめサイトを作る！ (ツイートを表示する前編)

## はじめに -  "Vue.js / Nuxt.js / Firebase のアプリ開発体験コース"

🎄🎄 🎅 オープンストリーム Advent Calendar 2018 の 19 日目です  🎁🎄🎄

最近、 **Vue.jsとFirebaseを使ってサービス作ったー** …と流行っていますね！  

この記事では、そんなVue.jsを使ってWebアプリを作る **Nuxt.js** と、 **Firebase** を用いて **ツイートをまとめるアプリ** を作ってみたいと思います！
Vue.js / Nuxt.js / Firebaseの入門としてこの記事を位置づけています🔰

環境はこんな感じです！

```bash
$ node -v
v11.4.0

$ npm -v
6.4.1

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.2
BuildVersion:	18C54
```

### 2019/12/18現在の `create-nuxt-app` (でプロジェクトが作成できるNode.jsの環境)には対応しません

まずは当時のNode.js 11.4.0でプロジェクトを作成しようとするとESLintが設定できなくなっています。

> warning nuxt > @nuxt/webpack > @nuxt/babel-preset-app > core-js@2.6.11: core-js@<3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your dependencies to the actual version of core
-js@3.
> error eslint@6.7.2: The engine "node" is incompatible with this module. Expected version "^8.10.0 || ^10.13.0 || >=11.10.1". Got "11.4.0"
> error Found incompatible module.

次に、 `create-nuxt-app` のESLintが動作できるバージョン(11.10.1)で試したところ、[ツイートの埋め込みコンポーネント(tonickkozlov/vue-tweet-embed)](https://github.com/tonickkozlov/vue-tweet-embed/)が動作しなくなります。

`import Tweet from 'vue-tweet-embed/tweet'` と呼び出す方法ではビルドができず `yarn dev` ができません。

```vue:pages/index.vue
<template>
  <div>
    <h1>🐥まとめページ</h1>
    <hr>
    <div
      v-for="tw in tweets" :key="tw">
      <Tweet :id="tw"/>
    </div>
  </div>
</template>

<script>
import Tweet from 'vue-tweet-embed/tweet'

export default {
  components: {
    Tweet
  },
```

> $ yarn dev
> ...
>
> This dependency was not found:
> * vue-tweet-embed/tweet in ./node_modules/babel-loader/lib??ref--2-0!./node_modules/vue-loader/lib??vue-loader-options!./pages/index.vue?vue&type=script&lang=js&

[importするモジュールを変えた方法](https://github.com/tonickkozlov/vue-tweet-embed/issues/10) では、ページを開いたときに "Unknown custom element: <Tweet> - did you register the component correctly?"と表示されます。

```vue:pages/index.vue
...

<script>
import Tweet from 'vue-tweet-embed'

export default {
  components: {
    Tweet
  },
```

詳しい検証を進めたいところですがまだできていません🙇‍♂️
「NuxtとFirebaseを組み合わせてなにか作りたい」という趣旨の場合はこちらの記事をご確認ください。

Nuxt.js + Vuex + Firebase(Authentication, Cloud Firestore)でTwitterユーザー向けの伝言板を作る - Qiita
https://qiita.com/ysd_marrrr/items/cf7871e7803d7b4919f1

## 作ったモノ

Twitterで **モーメント** の機能があったのはご存じでしょうか？ ~~使ったことないって？~~  
モーメントはツイートを一つのページにまとめられる機能ですが、この前 **スマートフォンアプリからのモーメントの作成ができなくなりました。** ~~不便ですね。~~ 

この記事で作るのは、そんなモーメントのようにツイートをまとめるアプリを作ります 🗒
前編ではここまで作ります。このようにまとめたツイートをTwitterの埋め込み機能で表示するアプリになります。
 ~~HTMLに埋め込みタグ貼り付ければおしまいだろ~~ 

![image.png](https://qiita-image-store.s3.amazonaws.com/0/216963/12a8bc68-a115-13c3-6fc3-45b9a7aa1493.png)

最終的には Vue.js/Nuxt.js/Firebase を習得しながら次を目指していきます😃

 - Twitterのアカウントで認証して自分だけのまとめを作れるようにする
 - スマートフォンでもツイートをまとめられるようにする



## Nuxtプロジェクトの作成

[create-nuxt-app](https://github.com/nuxt/create-nuxt-app) を使います。 **Node.jsの環境は整えましたよね？**
プロジェクトの名前からCSSのフレームワーク、SPAにするかどうかまでこのコマンドで設定でき、素早くプロジェクトが始められます。

```bash
$ npm install -g npx
$ npx create-nuxt-app moments-sub
> Generating Nuxt.js project in ~/Projects/moments-sub
? Project name moments-sub
? Project description My tweet collection project
? Use a custom server framework express
? Use a custom UI framework bulma
? Choose rendering mode Single Page App
? Use axios module yes
? Use eslint yes
? Use prettier yes
? Author name Masataka Yoshida
? Choose a package manager yarn
```

### プロジェクトの動作確認

作ったプロジェクトが動くか `yarn dev` で試してみましょう。

```bash
$ cd moments-sub

$ yarn dev
~/Projects/moments-sub/pages/index.vue
  36:1  error  Delete `⏎`  prettier/prettier

✖ 1 problem (1 error, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.
```

おっと、ファイルに触れていないのにエラーが出ました🤔

**ソースコードが整形された形になっていないと** このエラーが出るようですが、プロジェクト開始直後から出るのはテンションが下がります。  

`./node_modules/.bin/eslint --fix pages/index.vue` とeslintに `--fix` オプションを付けて実行するとこのエラーを解決してくれるそうですが、 **動作を確認するときに毎回このエラーが出て `--fix` オプションを付けて直す…なんてやってられません。**


### 整形に関するエラーを自動で修正する

`yarn dev` を実行したときに `--fix` オプションでeslintを走らせられるといいのですが、この方法が見つかりません。 
そこで、 **エディターでプロジェクト内のファイルを変更したときにこのエラーを直してもらうよう** 設定しましょう。Atomの場合は次を参考にするとAtomとeslintを連携させることができます。

Atom & Vue.js & ESLint で自動整形環境構築 - Qiita
https://qiita.com/taiju59/items/326bb25814f60ecf9c3c

最後に `linter-eslint` パッケージの "Fix errors on save" にチェックを入れると、 **上記の整形に関するエラーが自動で治ります 😃**

![image.png](https://qiita-image-store.s3.amazonaws.com/0/216963/21f6f9d8-7c1d-3cf6-3ac9-0fee93d3b482.png)


`http://localhost:3000` を開いてNuxt.jsの画面が出たら次に進みましょう😃
すこし話がそれましたが、これでNuxtの準備が整いました！


## まとめたツイートを表示する画面を作る

では、ツイートを表示してみましょう。

`create-nuxt-app` で作ったNuxtのプロジェクトを見ると次のような構成になっています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/216963/78e7b7e0-d468-26fd-dc52-849f49029b7e.png)

このNuxt.jsのプロジェクトに画面を作るには `pages` ディレクトリにファイルを追加しますが、 `pages/index.vue` を変更します。
この `pages/index.vue` がVue.jsの **単一ファイルコンポーネント** というものです。 `<style>` に何も書いていませんが、HTML/JS/CSSを書いたことがあれば `<template>`, `<script>`, `<style>` は何を書いているかイメージしやすいですよね☺️

`<template>` の下にある `<div>` は必要です。

```vue:pages/index.vue
<template>
  <div>
    <h1>🐥まとめページ</h1>
    <hr>
    <ul>
      <li> {{ tweets }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data: function() {
    return { tweets: 'ここにツイートが出たらいいなぁ' }
  }
}
</script>

<style>
</style>
```


### まとめたツイートをクライアントに送る

まとめたツイートをクライアントに送る部分を作りましょう。 まず、`server` ディレクトリに `api` と `api/tweets.js` を作ります。これはまとめたツイートのデータを返す部分(API)になります。
次に同じディレクトリに `data` と `data/tweets.json` を作ります。これはまとめたツイートのデータになります。
こんな感じになります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/216963/01e72e64-7945-c484-8a0f-62209620afb1.png)


Twitterのツイートをみると、URLは次の形式になっています。この数値の部分がツイートのIDになるので、これを手作業で抜き出してツイートをまとめましょう。

```
https://twitter.com/Adobe/status/1074067849872900097
                    ~~~~~        ~~~~~~~~~~~~~~~~~~~
  @以降の名前(screen_name)         ツイートのID(id, id_str)
```

まとめたツイートを次の形で先ほどの `server/data/tweets.json` に保存します。

```json:server/data/tweets.json
{
  "tweets": [
    "1073763832869179392",
    "1073909919839997952",
    "1074076529838120960"
  ]
}

```

次に、このまとめたデータを返すAPIを作ります。 `server/api/tweets.js` を次の内容にします。
ここでまとめたツイートのJSONを読み込んでクライアントに送ります。

```js:server/api/tweets.js
const express = require('express')
const router = express.Router()
var path = require('path')
var fs = require('fs')

router.get('/tweets', (req, res, next) => {
  const tweetsPath = path.join(__dirname, '../data/tweets.json')
  var tweetsFile = JSON.parse(fs.readFileSync(tweetsPath, 'UTF-8'))

  res.header('Content-Type', 'application/json; charset=utf-8')
  res.send(tweetsFile)
})

module.exports = router
```

APIを作ってもNuxt側は把握できないため、 `server/index.js` を編集して先ほどのAPIを登録します。

ファイルの中に2行を入れますが、既にコードが書かれているのと、それぞれ挿入する場所が決まっているので注意して入れます。

 1. `const apiRouter = require('./api/tweets')` : APIのJSを読み込みます。 `require` 内には `index.js` から見たAPIのJSまでの場所を **拡張子無しで** 指定します。
 2. `app.use('/api', apiRouter)` : APIを登録します。 `/api` はクライアントから見た場所になります。

```js:server/index.js
const express = require('express')
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')
const app = express()
const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || 3000

const apiRouter = require('./api/tweets')  // <- ここに挿入

app.set('port', port)

// Import and Set Nuxt.js options
let config = require('../nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')

async function start() {
  app.use('/api', apiRouter)  // <- ここに挿入

  // Init Nuxt.js
  const nuxt = new Nuxt(config)

...
```

APIができたら画面に出してみましょう！再び `pages/index.vue` を編集します。
先ほどのIDのリストが `tweets` になって `<template>` 内に入るため、リストの中身でループしています。 `:key` は必要です。

```vue:pages/index.vue
<template>
  <div>
    <h1>🐥まとめページ</h1>
    <hr>
    <ul
      v-for="tw in tweets" :key="tw">
      <li> {{ tw }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data: function() {
    return { tweets: ['ここにツイートが出たらいいなぁ'] }
  },
  mounted: function() {
    this.$axios
      .$get('/api/tweets')
      .then(response => {
        this.tweets = response.tweets
      })
      .catch(error => {
        console.log(error)
      })
  }
}
</script>
```

編集後は `http://localhost:3000` に接続するとツイートのIDが並ぶはずです😀

![image.png](https://qiita-image-store.s3.amazonaws.com/0/216963/82baf8b3-babf-455a-ed6d-4c14b32b226e.png)


### 画面に送られたツイートを表示する

まだIDしか表示されていませんが……これで

 - Nuxt.jsでAPIを作って
 - そのAPIに通信して
 - まとめたツイートの情報を取得できました  

このIDをツイートとして表示するのですが、便利なプラグインを使ってみましょう。

ここでは、Vueのプラグインである [vue-tweet-embed](https://github.com/tonickkozlov/vue-tweet-embed) を使います。
yarnを使ってプラグインを導入するので、 `yarn dev` で `http://localhost:3000` に接続できる状態であれば `Ctrl+c` / `Cmd+c` で終了しましょう。

次に、このコマンドで [vue-tweet-embed](https://github.com/tonickkozlov/vue-tweet-embed) をインストールします。

```
$ yarn add vue-tweet-embed
```

インストールが終わったら `pages/index.vue` を編集します。
コンポーネントを追加するので、 `<script>` 内の `import` と `components` を忘れないようにしましょう。

使用方法は `<Tweet :id="'692527862369357824'"></Tweet>` と文字列を入れるようになっていますが、変数をセットするときは `<Tweet :id="tw"/>` と書くことができます。

```vue:pages/index.vue
<template>
  <div>
    <h1>🐥まとめページ</h1>
    <hr>
    <div
      v-for="tw in tweets" :key="tw">
      <Tweet :id="tw"/>
    </div>
  </div>
</template>

<script>
import Tweet from 'vue-tweet-embed/tweet'

export default {
  components: {
    Tweet: Tweet
  },
  data: function() {
    return { tweets: ['ここにツイートが出たらいいなぁ'] }
  },
  mounted: function() {
    this.$axios
      .$get('/api/tweets')
      .then(response => {
        this.tweets = response.tweets
      })
      .catch(error => {
        console.log(error)
      })
  }
}
</script>
```

`yarn dev` を実行してブラウザで確認すると、ツイートのIDが埋め込みツイートに変化しました😆
これでまとめたツイートが表示できましたね！

![image.png](https://qiita-image-store.s3.amazonaws.com/0/216963/e004f674-d26b-db9a-e8ca-afa1561c6a7b.png)


このままでは自分でツイートを追加したり、新しいまとめページを作るときにファイルを作って、そのファイルを指定して……と**ファイルが必要になってきます。**

 - スマートフォンからまとめページをいじりたいですよね？
 - データベースを用意してツイートやページを追加できるようにしたいですよね？
 - Twitterのアカウントで認証したいですよね？

🔥ここでFirebaseを使います🔥


## 前編おわり

…………とFirebaseの導入を書きたいところですが、Firebaseの部分を書くと **その部分だけで10,000文字を超えてものすごく長くなってしまうので** 後編に分けます 🙇‍♂️
~~だっていろいろハマったんだもん~~

(あれ……とりあえず19日の分は書けたよね……？明日でいいよね……？🛌💤)

明日は @maemori さんです！

~~この記事を書いているときに、ツイートをまとめる [Togetter](https://togetter.com) があったのをすっかり忘れてました。~~


## 参考

Nuxt.js でバックもフロントもこれ一本 - Qiita
https://qiita.com/kawaMk4/items/298f95f751540b96d39b