Vue.js&Nuxt.jsで初めてアプリを自作したときに躓いた点

## はじめに

Vue.js&Nuxt.js超入門（秀和システム）を読んで勉強したのでアプリを作った。

![20190501171617.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48665/5cbf7c3e-fcba-4b15-e42e-b101bf27cb3f.png)
「囀暦」というアプリで「てんれき」と読む。Twitterでつぶやかれている内容の中で「○月○日にxxします！」という告知をカレンダー上にプロットするもの。以前にもRailsでちょこっと作っていたが今回改めて作り直してみた次第。

教科書通り作るのとはわけが違う。エラー&エラーに悩まされましたのでここにまとめます。

## 構成

- Vue.js（フロント）
- Nuxt.js（バック）
- Firebase（認証）

また、ライブラリとして下記を使用。

- Materialize
- fullcalendar

初期設定

```bash
$ npx create-nuxt-app tenreki
> Generating Nuxt.js project in /Users/hoshito/RubymineProjects/tenreki
? Project name tenreki
? Project description My primo Nuxt.js project
? Use a custom server framework none
? Choose features to install (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Use a custom UI framework none
? Use a custom test framework none
? Choose rendering mode spa
? Author name hoshito
? Choose a package manager npm
```

## ディレクトリ構成

初めて扱うフレームワークだったので何をどこに置いたらいいのか、あのファイルどこにいったっけと探すのが大変だった。
![20190502005558.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48665/f49d7e65-4346-4f96-14b5-20a3697eac07.png)

nuxt.config.jsはいろいろ書いたので全部のせておく。

```json:nuxt.config.js
import pkg from './package'
require('dotenv').config();
const {
  CONSUMER_KEY,
  CONSUMER_SECRET,
  ACCESS_TOKEN_KEY,
  ACCESS_TOKEN_SECRET
} = process.env;

export default {
  mode: 'spa',

  /*
  ** Headers of the page
  */
  head: {
    title: pkg.name,
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: pkg.description }
    ],
    script: [
      { src: "https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js" },
      { src: "https://unpkg.com/popper.js"},
      { src: "https://unpkg.com/tooltip.js"}
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      { rel: "stylesheet", href: "https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"}
    ]
  },

  /*
  ** Customize the progress-bar color
  */
  loading: { color: '#fff' },

  /*
  ** Global CSS
  */
  css: [
  ],

  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    { src: '~/plugins/calendar.js', ssr: false }
  ],

  /*
  ** Nuxt.js modules
  */
  modules: [
    '@nuxtjs/dotenv',
  ],

  /*
  ** Build configuration
  */
  build: {
    /*
    ** You can extend webpack config here
    */
    extend(config, ctx) {
      config.node = {
        fs: "empty" // TwitterAPIを使うのに必要
      };
    }
  },
  serverMiddleware: [
    '~/server'
  ],
  env : {
    CONSUMER_KEY,
    CONSUMER_SECRET,
    ACCESS_TOKEN_KEY,
    ACCESS_TOKEN_SECRET
  }
}
```

躓いたところを一つずつ記載。

## 環境変数を使う

- `npm install --save @nuxtjs/dotenv`
- nuxt.config.jsに情報を記載
  - `dotenv`とか`env`とか書かれているところ
  -  この設定だけでけっこう散らばって記載しているけどこれがベストなのかは疑問
- .envファイルに環境変数を記載（このファイルはコミットしない）
  - 下のような感じ
```
CONSUMER_KEY="xxx"
CONSUMER_SECRET="xxx"
ACCESS_TOKEN_KEY="xxx"
ACCESS_TOKEN_SECRET="xxx"
```

サーバ起動前に環境変数を設定する方法もありそうだったが、毎回設定するよりは外部ファイルに置いておく方が好みなのでこの方法を取った。

## localStorage, sessionStorageを使用する際はSPAモードで実行

これは書籍にもあった。SPAとSSRの違いがよく分かっていないので（サーバレンダリングの有無だけ？）もう少し勉強する必要がある。

今回はsessionStorageにTwitterのトークン情報を保存している。

https://developer.twitter.com/en/docs/basics/authentication/guides/securing-keys-and-tokens.html

> We would not normally recommend using LocalStorage, although there may be a few exceptions to this.

> Using SessionStorage is normally more preferable than LocalStorage when thinking in terms of security. 

Twitterのdevelopページにこう書かれていたので、localStorageではなくsessionStorageを使った。

## TwitterAPIを使用するにあたって

- `npm install --save twitter`
- `npm install --save axios fs net tls`（依存するライブラリたち）

fsが使えずに下記のようなエラーが出た。

```
ERROR in ./node_modules/request/lib/har.js Module not found: Error: Can't resolve 'fs' in '/Users/hoshito/RubymineProjects/tenreki/node_modules/request/lib'
```

fsはファイルシステムのことで、SPAつまりクライアント側でファイルシステムモジュールを使おうとしてエラーになっていた。

下のように書くとエラーを回避できた。

```javascript:nuxt.config.js
    extend(config, ctx) {
      config.node = {
        fs: "empty" // TwitterAPIを使うのに必要
      };
    }
```

SPAだとこれが動かずSSRだとこれが動かず、というのが多く混乱してしまう。

## node-sassは（今だけ？）使えない

sassを使いたくて`npm install node-sass`をしたところ脆弱性エラーで進めなかった。調べてみると最近出たエラーっぽい。解決策はいろいろ出てきている。

https://github.com/sass/node-sass/issues/2625
https://zatsuzen.com/nodejs/node-sass/

package.jsonで`node-gyp`を書き換えろ、という内容であるが、私のpackage.jsonは以下のようなものであり、どこをどう書き換えればよいのか分からずsassの導入を断念しました。

```json:package.json
{
  "name": "tenreki",
  "version": "1.0.0",
  "description": "My primo Nuxt.js project",
  "author": "hoshito",
  "private": true,
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate"
  },
  "dependencies": {
    "@fullcalendar/core": "^4.1.0",
    "@fullcalendar/daygrid": "^4.1.0",
    "@fullcalendar/vue": "^4.1.0",
    "@nuxtjs/dotenv": "^1.3.0",
    "axios": "^0.18.0",
    "cross-env": "^5.2.0",
    "firebase": "^5.10.1",
    "fs": "0.0.1-security",
    "net": "^1.0.2",
    "nuxt": "^2.4.0",
    "sass": "^1.19.0",
    "tar": "^4.4.8",
    "tls": "0.0.1",
    "twitter": "^1.7.1"
  },
  "devDependencies": {
    "nodemon": "^1.18.9"
  }
}
```

## ビジネスロジック

serverMiddlewareを使った。
https://ja.nuxtjs.org/api/configuration-servermiddleware/

`page/list.vue`から自身にAPIを叩くように使っている。

```javascript:page/list.vueの一部
    methods: {
      getTweets(token, secret) {
        const self = this;
        const params = {token: token, secret: secret};
        axios.post("/api/tweets", params).then(res => {
          self.events = res.data;
        }).catch(err => {
          console.log(err);
        });
      },
```

```javascript:server/index.jsの全部
import express from 'express'
import bodyParser from 'body-parser'
import {getClient, getTweetInfoForFullCalendar} from '../plugins/twitter'

const app = express()
// requestでjsonを扱えるように設定
app.use(bodyParser.urlencoded({extended: true}))
app.use(bodyParser.json())

app.post('/tweets', (req, res) => {
  let client = getClient(req.body.token, req.body.secret);
  // TODO: リクエストパラメータに入れる
  let params = {owner_screen_name: "hoshitostar", slug: "Love", count: 200};
  client.get('lists/statuses', params).then(tweets => {
    let result = getTweetInfoForFullCalendar(tweets);
    res.send(result);
  }).catch((errors) => {
    // TODO: error処理
    console.log(errors);
  });
})

module.exports = {
  path: '/api',
  handler: app
}
```

これも最初全然リクエストを投げられず、投げることができたと思ったらリクエストパラメータ取れなくて苦労した。

`server/index.js`はRailsでいう（別にRailsじゃなくてもいいけど）Controllerの役割を担っていると思ったので、ロジックは`plugins/twitter`に入れた。本当は`client.get...`の部分も含めて外に出そうと思ったけど、client.getの非同期処理でハマってしまったのでやめにした。

```javascript:plugins/twitterの一部
// TwitterAPIのクライアントクラスを取得
function getClient(token, secret) {
  const Twitter = require('twitter');
  return new Twitter({
    consumer_key: process.env.CONSUMER_KEY,
    consumer_secret: process.env.CONSUMER_SECRET,
    access_token_key: token,
    access_token_secret: secret
  });
}

// --- 省略 ---

export {getClient, getTweetInfoForFullCalendar}
```

## 余談

2019/04/08から技術系のBlogとTwitterを始めてます。このQiitaの記事はブログに記載していたものをまとめたものになります。

Blog: https://hoshitostar.hatenablog.jp/
Twitter: @hoshitostar

Qiita vs ブログの話題はたまに見るけどやっぱり難しいですね。今回はまとまった記事になったのでQiitaに投稿しました。もっと良い書き方があれば教えていただけると嬉しいです。

アウトプットがんばるぞー