Nuxt on Firebase FunctionsでHP作成してみたので躓いたとこをメモ

請負の仕事でHPを作ることになった。
お客様自身でサイトの修正を行えるようにしたいという要望があり、Headless CMS等を調査したが日本語化しているのがなさそうだったので自前で作成してみることに。
タイトルにある通りバックエンドはFirebase Cloud FunctionsとFirebase Realtime Databaseを使いました。
フロントエンドはNuxtで[Quill](https://www.npmjs.com/package/vue-quill-editor)というWYSIWYGエディタを組み込むことにしました。
その中でつまずいた点を今後のためにメモします。

# RSSフィードを読み込みはSSR必須だった
[CORS](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)のためにSPAでRSSからデータ抜き出して表示することが出来なかった。
対応としてはiframeを使ってその部分だけSSRで出力。
ちなみにRSSのパースには[xml2js](https://www.npmjs.com/package/xml2js)を使用しました。
参考までにパース部分のscriptを書くと

```vue:pages/RssReader.vue
<script>
function asyncParseString(xml) {
  return new Promise(function(resolve, reject) {
    const options = {
      trim: true,
      explicitArray: false
    }
    const parseString = require('xml2js').parseString
    parseString(xml, options, function(err, obj) {
      if (err) {
        return reject(err)
      }

      return resolve(obj)
    })
  })
}

export default {
  async asyncData({ store, route, app }) {
    // RSS feedを取得するために料金プランを変更する必要があった
    const rss = await app.$axios.get(
      'http://blog.goo.ne.jp/hoge/rss2.xml'
    )
    const item = await asyncParseString(rss.data)
    const blog = {
      title: item.rss.channel.title,
      item: item.rss.channel.item
        .map(i => {
          return { title: i.title, link: i.link }
        })
    }

    return { blog }
  }
}
</script>
```

また、SSR時に外部サイト(上の例ではblog.goo.ne.jp)にアクセスするにはfirebaseの料金プランをSparkからBlaze or Flameに変える必要がありました。ちなみに固定費がかかるのが嫌だったのでBlazeにしました。

# Quillの設定が難しかった
## Quillで日本語フォントを使えるようにするには
まず、plugins/quill.jsを作ります。

```js:plugins/quill.js
import Vue from 'vue'
import Quill from 'quill'
import VueQuill from 'vue-quill-editor'

// 入力フォント設定
const Font = Quill.import('formats/font')
Font.whitelist = ['yu-gothic', 'yu-mincho', 'meiryo']
Quill.register(Font, true)

const options = {
  modules: {
    toolbar: {
      container: [
        [{ font: ['Sans Serif', 'yu-gothic', 'yu-mincho', 'meiryo'] }]
      ]
    }
  },
  placeholder: '記事を入力してください'
}

export default () => {
  Vue.use(VueQuill, options)
}
```
やってることはQuillからFontを取り出してwhitelistに游ゴシック、游明朝、メイリオを設定してます。
その後toolbarにfontを設定してfont選択ドロップダウンを表示させます。
その後cssまたはvueファイルのscopedではないstyleに以下を記述

```vue:layouts/default.vue

<style>
ql-font-yu-gothic {
  font-family: '游ゴシック Medium', 'Yu Gothic Medium', '游ゴシック体', YuGothic,
    sans-serif;
}
.ql-snow .ql-picker.ql-font [data-value='yu-gothic'].ql-picker-item::before,
.ql-snow .ql-picker.ql-font [data-value='yu-gothic'].ql-picker-label::before {
  content: '游ゴシック';
  font-family: '游ゴシック Medium', 'Yu Gothic Medium', '游ゴシック体', YuGothic,
    sans-serif;
}

.ql-font-yu-mincho {
  font-family: 'Yu Mincho', YuMincho, HG明朝B, 'MS Mincho', serif;
}
.ql-snow .ql-picker.ql-font [data-value='yu-mincho'].ql-picker-item::before,
.ql-snow .ql-picker.ql-font [data-value='yu-mincho'].ql-picker-label::before {
  content: '游明朝';
  font-family: 'Yu Mincho', YuMincho, HG明朝B, 'MS Mincho', serif;
}

.ql-font-meiryo {
  font-family: 'Meiryo';
}
.ql-snow .ql-picker.ql-font [data-value='meiryo'].ql-picker-item::before,
.ql-snow .ql-picker.ql-font [data-value='meiryo'].ql-picker-label::before {
  content: 'メイリオ';
  font-family: 'Meiryo';
}
</style>
```
styleを設定することでQuillの編集結果を参照するときにフォントの設定がされます。
また、エディター表示時にツールバーに游ゴシック、游明朝、メイリオがそれぞれ表示されます。

## Quillではテーブルがデフォルトで扱えない
お客様の要望でテーブルも出せるようにしたかったので方法を調べました。[quill-table-module](https://www.npmjs.com/package/quill-table-module)を使えばよいことが分かったので取り込むことに。
例によってplugins/quill.jsを編集します。

```js:plugins/quill.js
import Vue from 'vue'
import Quill from 'quill'
import VueQuill from 'vue-quill-editor'
import quillTable from 'quill-table-module'

Quill.register(quillTable.TableCell)
Quill.register(quillTable.TableRow)
Quill.register(quillTable.Table)
Quill.register(quillTable.Contain)
Quill.register('modules/table', quillTable.TableModule)

const maxRows = 10
const maxCols = 5
const tableOptions = []
for (let r = 1; r <= maxRows; r++) {
  for (let c = 1; c <= maxCols; c++) {
    tableOptions.push('newtable_' + r + '_' + c)
  }
}

const options = {
  modules: {
    table: true,
    toolbar: {
      container: [
        [
          { table: tableOptions },
          { table: 'append-row' },
          { table: 'append-col' }
        ]
      ]
    }
  }
}

export default () => {
  Vue.use(VueQuill, options)
}
```
やってることはquillTableからTable,Row,TableCell等の要素を取り出してQuillに登録。

## Quillでは画像がBase64で保存されるのでデフォルトではDBに登録されてしまう
画像がBase64で保存されるとRealtime Databaseの料金が上がってしまうのでFirebase Storageに登録できるようにします。
やっぱりplugins/quill.jsを編集します。

```js:plugins/quill.js
import Vue from 'vue'
import Quill from 'quill'
import VueQuill from 'vue-quill-editor'

const InlineBlot = Quill.import('blots/embed')
class ImageBlot extends InlineBlot {
  static create(data) {
    const node = super.create(data)
    node.setAttribute('src', data.src)
    node.setAttribute('width', data.width)
    node.setAttribute('height', data.height)
    return node
  }
  static value(domNode) {
    const { src, width, height } = domNode
    return { src, width, height }
  }
}
ImageBlot.blotName = 'imageBlot'
ImageBlot.className = 'image-blot'
ImageBlot.tagName = 'img'
Quill.register({ 'formats/imageBlot': ImageBlot })

const options = {
  modules: {
    toolbar: {
      container: [
        ['image']
      ],
      handlers: {
        image: function() {
          document.getElementById('getFile').click()
        }
      }
    }
  }
}

export default () => {
  Vue.use(VueQuill, options)
}
```
ソースを見てお気づきでしょうが**document.getElementById('getFile')**と、idがgetFileの要素(input[type=file])に依存した形になっています。ここは後で何とかしたいところです。。。

次に登録・編集画面も変更します。

```vue:pages/create.vue
<template>
  <section>
    <div style="margin: 1em;">
      <quill-editor ref="quillEditor" v-model="formData.content" />
      <input
        id="getFile"
        accept="image/png, image/jpeg, image/gif"
        type="file"
        @change="uploadFunction($event)"
      />
    </div>
  </section>
</template>

<script>
import Vue from 'vue'
import firebase from '~/plugins/firebase'
import moment from '~/plugins/moment'

export default Vue.extend({
  data() {
    return {
      formData: {
        title: '',
        content: '',
        publishAt: new Date(),
        public: null,
        userId: ''
      }
    }
  },
  methods: {
    async uploadFunction(e) {
      // file upload
      const now = moment().format('YYYYMMDD-hhmmss')
      const storageRef = firebase.storage().ref()
      const file = e.target.files[0]
      const fileRef = storageRef.child(`images/${now}/${file.name}`)
      await fileRef.put(file)
      fileRef.updateMetadata({ cacheControl: 'public,max-age=14400000' })

      const downloadRef = firebase.storage().ref(`images/${now}/${file.name}`)
      const url = await downloadRef.getDownloadURL()

      // insert image into quill
      const quill = this.$refs.quillEditor.quill
      const index = quill.getSelection(true).index
      const img = new Image()
      const embed = {}
      img.onload = function() {
        embed.width = this.width
        embed.height = this.height
        embed.src = url
        quill.insertEmbed(index, 'imageBlot', embed)
      }
      img.src = url
    }
  }
})
</script>
```
やっていることはtoolbarの画像ボタンクリック時に非表示にしたgetFileをクリックする。その後ファイル選択時にuploadFunctionメソッドを呼び、Firebase Storageにファイルを登録し、download用URLを取得、Quillに画像タグを登録します。

なんだか書いててNuxt上でのQuillの使い方になってきた気がするのでいったん終了。ほかにも躓いたところあるので別の記事を書くかも。