明日使えるかもしれない Nuxt 上で唱えるプチ闇魔法３連発

この記事は [Sansan Advent Calendar 2019](https://adventar.org/calendars/3924) の12日目の記事です。 

今年に入ってから Nuxt.js を使い始めましたがとっても便利で、今まで React 派でしたがこれがあるから Vue っていいなと思いました。
みなさんも Nuxt 使っているでしょうか。そして、モダンにかっこよく使えているでしょうか。

僕はあんまりモダンじゃ無い、泥臭くやることを強いられた部分があったので、もしも同じことで悩む人のためにいくつか紹介したいと思います。

コンテンツは

- [Component (Typescript) 内で外部 jQuery を利用する](#1-component-内で外部-jquery-を利用する)
- [SPA + Producitonモードでエラーthrow時にエラー画面へ飛ばす](#2-spa-モードでもエラーが起きた時にエラーページへ飛ばす)
- [ServerMiddlewareでCORSを無視してAPIリクエストする](#3-cors-を無視して-api-にリクエストするための-proxy-サーバーを立てる)

の３点です。

## 前提

以下で動作確認

- Nuxt.js は v2.10
- Typescriptを使用
- Componentの作成には [vue-property-decorator](https://github.com/kaorun343/vue-property-decorator) を使用

また、今回のコード全部入りは[GitHubで公開しています](https://github.com/suzukenz/nuxt-typescript-demo/tree/sansan-advent-2019)。

## 1. Component 内で外部 jQuery を利用する

これは比較的普通に使えるネタかもしれません。
Component 内で jQuery を import して、`$`で使用します。

### 型定義を準備

1. package をインストール

```bash
npm i -D @types/jquery
```

2. `tsconfig.json`の`types`を以下のように追記します。

```json:tsconfig.json
{
  // "@types/jquery" を追加！
  "types": ["@types/jquery", "@types/node", "@nuxt/types"]
}
```

### Component を書く

こんな感じ

```vue:jquery-example.vue
<template>
  <div>
    <button @click="buttonClick" class="continue">
      Click me
    </button>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

// Ref: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jquery#authoring-type-definitions-for-jquery-plugins
declare const $: JQueryStatic

// このページだけで外部jQueryを利用する書き方
// Ref: https://ja.nuxtjs.org/faq/#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%AA%E8%A8%AD%E5%AE%9A
@Component({
  head() {
    return {
      script: [{src: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'}]
    }
  }
})
export default class extends Vue {
  buttonClick() {
    $('button.continue').html('Next Step...')
  }
}
</script>
```

これで使えます。

## 2. SPA モードでもエラーが起きた時にエラーページへ飛ばす

**これは今回書く中でも特にやってみたら動いた系のネタなので Production レベルでは使わないか、使うにしても Nuxt のバージョンをしっかり固定しましょう**

Nuxt では、`layout/error.vue` を置くことで、404 にアクセスされた時や予期せぬエラーが起きた時に、そのエラーページを表示してくれる機能があります。 ([参考](https://ja.nuxtjs.org/guide/views/#%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%9A%E3%83%BC%E3%82%B8))

ですが、SPA モードかつ production build の場合、Component でエラーが throw されるとこのエラーページへ行かなかったので、無理やり制御しました。

### エラーページを準備

これは `layout/error.vue` を追加するだけです。

```vue:layout/error.vue
<template>
  <div>
    <h1>エラーが発生しました</h1>
    <nuxt-link to="/">
      ホーム
    </nuxt-link>
  </div>
</template>
```

### error を制御する plugin を追加する。

Vue.config.errorHandler を使ってエラーをキャッチして、nuxt インスタンスの`error()`を呼び出します。
この部分が、ソースを追っかけていて「これで行けるんじゃね？」ってやったら動いた系なので取り扱い注意です。

```typescript:plugins/error-handler.js
import Vue from 'vue'
import { NuxtApp } from '@nuxt/types/app'

Vue.config.errorHandler = (err, vm) => {
  const $nuxt = vm.$root as NuxtApp
  $nuxt.error(err)
}
```

nuxt.config.ts にも忘れず追加します。

```typescript:nuxt.config.ts
plugins: ['~/plugins/error-handler.ts'],
```

これで、Component 内でエラーが起きた時に error.vue へ飛ぶようになります。
ちなみに SPA Production モードの挙動は build した後に`nuxt-ts start` で確認できます。

エラーを起こすためのお試しComponentは[こちら](https://github.com/suzukenz/nuxt-typescript-demo/blob/sansan-advent-2019/pages/exception.vue)

## 3. CORS を無視して API にリクエストするための Proxy サーバーを立てる

backend がまだ準備できてない等の理由で、開発中、どうしてもクロスオリジンな API にリクエストしたい場合がないでしょうか。まあ、普通は無いと思います。

Nuxt には ServerMiddleware という機能があり、例えば `/api` という serverMiddleware を定義すると
`http://localhost:3000/api` というエンドポイントを作ることができます。

これを活用して、あたかも同じサーバーのエンドポイントなのに、裏では外部 API をリクエストする、みたいなことを実現できます。

### (追記) proxy moduleを使う

これですが、[公式ドキュメントにある通り@nuxtjs/proxyを使う](https://ja.nuxtjs.org/faq/http-proxy/)ことで簡単に実現できます。

```nuxt.config.ts
  proxy: {
    '/api': {
      target: 'http://example.com/', // Nuxtを起動しているサーバーから見たエンドポイントを記載する
      pathRewrite: {
        '^/api': '/'
      }
    }
  }
```

上記の記載をすると、 `/api`へのアクセスでexample.comへリクエストが飛びます。


以下は、追記前の冗長なやり方です。

### 外部 API へリクエストするサーバーを書く

1. リクエストを受けて
2. パスやパラメータを解析して
3. そのパスとパラメータそのまま外部 API にリクエストする

みたいなサーバーを node.js で書きます。これは正直どんな実装でも良いのですがサンプルを載せておきます。

```javascript:server/index.js
const express = require('express')
const request = require('request')

const app = express()

// この場合 localhost:8080/api/~~ とリクエストするとこのserverが受ける
const rootPath = '/api'
// 実際のエンドポイントを入力
const actualEndpoint = process.env.ENDPOINT || 'https://example.com/v1'

const requestWrapper = async (options) => {
  const result = await new Promise((resolve, reject) => {
    request(options, (err, response) => {
      if (err) {
        reject(err)
      }
      if (!response) {
        reject(err, 'Nothing response')
      }
      resolve(response)
    })
  })
  return result
}

// 全部のリクエストを受けて外部APIへ飛ばす
app.all('/*', async (req, res) => {
  console.log(`original url: ${req.originalUrl}`)
  const options = {
    url: actualEndpoint + req.originalUrl.replace(rootPath, ''),
    method: req.method,
    qs: req.query,
    json: req.body
  }

  console.log('request with following options.')
  console.log(options)

  const response = await requestWrapper(options)

  res.status(response.statusCode).send(response.body)
})

// for Nuxt.js server middleware
module.exports = {
  path: rootPath,
  handler: app
}
```

express とか依存関係がある場合は、`npm install` も忘れずに！

### nuxt.config に serverMiddleware を追加する

serverMiddleware に先ほど追加したパスを指定します。

```javascript:nuxt.config.ts
  mode: 'spa',
  serverMiddleware: ['~/server/'],
```

これで、以下のように書くと、上のサーバーへリクエストし、レスポンスを受け取ることができます。

```typescript
this.response = await this.$axios.$get('/api/hoge')
```

こんなの２度と使うのか・・・わからないけど以上です！
取り扱いにはくれぐれも注意しましょう。