【Nuxt.js】GoogleBooksAPIで読書記録ウェブアプリを実装する

#■目的
**勉強のためNuxt.jsのサンプル集を作る**

##目標5
**GoogleBooksAPIを読書記録ウェブアプリを実装する**

##ざっくり仕様
* 検索はISBN13のみとする（本を探すではなく本を登録する、がメインのため）
* スマホ閲覧時、ISBNをスマホカメラで読み取る
* 検索した結果をコメント付きでローカルストレージに保存する
* コメントは再編集できるようにする
* ローカルストレージに保存したデータは削除できるようにする
* 保存したデータから何冊、何ページ読んだかわかるようにする

##はじめに
これから書く内容を以下2記事がなければ絶対に実装できていなかった。
コードもまるっと拝借したりしている箇所もある。
こうやって記事をまとめてくれている人に感謝してもしきれない。
自分のまとめはあくまで自身の考えを整理するアウトプットなので、
実装方法を知りたい人は以下の参考記事を読むことをおすすめします。

https://qiita.com/dnrsm/items/cb0ca65d3e6f674918ad

https://qiita.com/stella0270/items/0b77d09391c3111e88e5

##default.vue
ベースファイルは[ここ](https://qiita.com/MasayukiAbe/items/c4ed2d788fbd80b85506 "【Nuxt.js】ハンバーガーボタンとグロナビを実装する")で書いたものと同じ。
`components/template/Api.vue`内で処理をしていく。
※コードが長いので分割してまとめる


##Api.vue（ISBN入力枠・抜粋）
```vue:components/template/Api.vue
<MoleculesEtcSearchIsbn v-model="isbn" />
```

**SearchIsbn.vue**

```vue:components/molecules/etc/SearchIsbn.vue
<template>
  <input v-model="inputIsbn" type="text" placeholder="ISBN13">
</template>

<script>
export default {
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  computed: {
    inputIsbn: {
      get () {
        return this.value
      },
      set (newValue) {
        this.$emit('input', newValue)
      }
    }
  }
}
</script>
```

アトミックデザインを意識した設計が目標なので、
コンポネント分けするのが大前提なのですが、
そのせいで、
・親で入力された値をpropsで渡して
・v-modelでリアクティブに
等など、考えることがたくさんあったISBN入力箇所。
子コンポネント側の`computed`で`props`で受け取った値を
別の変数に格納して受け取った値を変更できるようにしています。


##Api.vue（ISBN読み取りカメラ枠・抜粋）
```vue:components/template/Api.vue
<button class="p-api_box02--search-btn-scan" @click="startScan">
  バーコードから探す<fa :icon="faCamera" />
</button>
<div v-show="camera" class="p-api_box02--search-scan" :style="scrollTopPosition">
  <p class="p-api_box02--search-scan-txt">
    「978」から始まるバーコードを映してください。
  </p>
  <div id="cameraArea" />
  <button class="p-api_box02--search-btn-stop" aria-label="close" @click.prevent.stop="stopScan">
    閉じる
  </button>
</div>
```

バーコードのカメラ読み取りは[Quagga.js](https://serratus.github.io/quaggaJS/ "QuaggaJS")を採用。
`@click="startScan"`でスマホのカメラが起動する。
スクリプトは後述。


##Api.vue（ローディング中メッセージ表示枠・抜粋）
```vue:components/template/Api.vue
<template v-if="message">
  <p class="p-api_box02--message">
    {{ message }}
  </p>
</template>
```

ちょっとしたUI。ISBNを感知して検索しているときに、「Now loading」と表示する枠。
`v-if="message"`で判定する。


##Api.vue（検索結果表示枠・抜粋）
```vue:components/template/Api.vue
<div v-for="item in items" :key="item.id" class="p-api_box02--result">
  <MoleculesEtcSearchIsbnResult
    v-model="itemComment"
    :item-link="itemLink"
    :item-img="itemImg"
    :item-title="itemTitle"
    :item-authors="itemAuthors"
    :item-publisher="itemPublisher"
    :item-description="item.volumeInfo.description"
    :item-comment="itemComment"
    @saveBtn="saveBtn"
  />
</div>
```

APIの検索結果から必要なものを`items`に格納して、そこから必要なものを`props`で
`SearchIsbnResult`という子コンポーネントに渡す。
逆にlocalstrageに保存するためのボタンのイベントは`emit`で子から渡してもらう。

**SearchIsbnResult.vue**

```vue:components/molecules/etc/SearchIsbnResult.vue
<template>
  <div class="c-bookinfo">
    <figure class="c-bookinfo_thumb">
      <a :href="itemLink" target="_blank">
        <img :src="itemImg">
      </a>
    </figure>
    <dl class="c-bookinfo_data">
      <dt class="c-bookinfo_data--title">
        {{ itemTitle }}
      </dt>
      <dd class="c-bookinfo_data--detail">
        <ul>
          <li>
            <dl>
              <dt>作者</dt>
              <dd>
                <p v-for="(author, index) in itemAuthors" :key="index">
                  {{ author }}
                </p>
              </dd>
            </dl>
          </li>
          <li>
            <dl>
              <dt>出版社</dt>
              <dd>{{ itemPublisher }}</dd>
            </dl>
          </li>
        </ul>
      </dd>
    </dl>
    <p class="c-bookinfo_data--intro">
      {{ itemDescription }}
    </p>
    <AtomsButtonsTextBtn
      btn-style="outside"
      color="dark"
      :link-path="itemLink"
      link-text="詳細を見る"
      class="c-bookinfo_data--btn"
    />
    <dl class="c-bookinfo_comment">
      <dt>■コメント</dt>
      <dd>
        <textarea
          v-model="inputItemComment"
        />
      </dd>
    </dl>
    <p class="c-bookinfo_save" @click="saveBtn">
      <span>保存する<fa :icon="faStickyNote" /></span>
    </p>
  </div>
</template>

<script>
import { faStickyNote } from '@fortawesome/free-solid-svg-icons'
export default {
  props: {
    itemLink: {
      type: String,
      default: ''
    },
    itemImg: {
      type: String,
      default: ''
    },
    itemTitle: {
      type: String,
      default: ''
    },
    itemAuthors: {
      type: Array,
      default: () => []
    },
    itemPublisher: {
      type: String,
      default: ''
    },
    itemDescription: {
      type: String,
      default: ''
    },
    itemComment: {
      type: String,
      default: ''
    }
  },
  computed: {
    faStickyNote () {
      return faStickyNote
    },
    inputItemComment: {
      get () {
        return this.itemComment
      },
      set (newValue) {
        this.$emit('input', newValue)
      }
    }
  },
  methods: {
    saveBtn () {
      this.$emit('saveBtn')
    }
  }
}
</script>
```

基本的に親から渡された値を表示しているだけ。
強いてポイントをあげると

* 「作者」は複数の場合があるので配列で処理
* 親から受け取った「コメント」を`get` `set`で再処理


##Api.vue（累計情報表示枠・抜粋）
```vue:components/template/Api.vue
<div v-if="booksLength > 0" class="p-api_box03--readedcount">
  <p class="p-api_box03--readedcount-books">
    あなたは<br>
    <span>{{ booksLength }}</span>冊 / <span>{{ pagesCount }}</span>ページ<br>
    読みました
  </p>
</div>
```

localstrageに保存されているオブジェクトの数をカウントしたり、
その中のページ数を合計して計算したり。本読みにはちょっとした楽しい機能。


##Api.vue（保存情報表示枠・抜粋）
```vue:components/template/Api.vue
<ul class="c-booklist">
  <li v-for="(book, index) in books" :key="book.id">
    <MoleculesEtcModalBook
      v-model="modalItem.comment"
      :book-img="book.img"
      :book-img-modal="modalItem.img"
      :book-link="modalItem.link"
      :book-title-caption="book.title"
      :book-title="modalItem.title"
      :book-authors="modalItem.authors"
      :book-publisher="modalItem.publisher"
      :book-comment="modalItem.comment"
      @open-modal="openModal(book, index)"
      @delete-btn="deleteBtn()"
      @resave-btn="resaveBtn()"
    />
  </li>
</ul>
```

ここも基本的に`props`でデータを渡しているだけだけど、

* 配列の中からどの情報をモダルウィンドウで表示するか判定するために、引数を指定
* 削除や再保存ボタンを`$emit`で受け取る

とかこちょこちょやってる。

**ModalBook.vue**

```vue:components/molecules/etc/ModalBook.vue
<template>
  <div class="c-booklist_wrap">
    <figure class="c-booklist_thumb" @click="openModal">
      <img :src="bookImg">
      <figcaption>『{{ bookTitleCaption }}』</figcaption>
    </figure>
    <MoleculesEtcModal
      v-if="isModalState"
      :top-position="$window.pageYOffset"
      @close-modal="$store.commit('Modal/closeModal')"
    >
      <figure class="c-booklist_thumb--modal">
        <a :href="bookLink" target="_blank">
          <img :src="bookImgModal">
        </a>
      </figure>
      <dl class="c-booklist_data">
        <dt class="c-booklist_data--title">
          {{ bookTitle }}
        </dt>
        <dd>
          <ul>
            <li>
              <dl>
                <dt>作者</dt>
                <dd>
                  <p v-for="(bookAuthor, index) in bookAuthors" :key="index">
                    {{ bookAuthor }}
                  </p>
                </dd>
              </dl>
            </li>
            <li>
              <dl>
                <dt>出版社</dt>
                <dd>{{ bookPublisher }}</dd>
              </dl>
            </li>
          </ul>
        </dd>
      </dl>
      <dl class="c-booklist_comment">
        <dt>■コメント</dt>
        <dd class="c-booklist_comment-box">
          <textarea ref="editComment" v-model="getBookComment" :class="(editFlag === true) ? 'is-editing' : ''" />
          <template v-if="editFlag === false">
            {{ bookComment }}
          </template>
        </dd>
        <dd class="c-booklist_comment-edit">
          <AtomsButtonsTextBtn
            btn-style="bookEdit"
            color="white"
            link-text="再編集"
            class="c-booklist_btn-edit"
            @click.native="editComment"
          />
        </dd>
        <dd class="c-booklist_comment-resave">
          <AtomsButtonsTextBtn
            v-if="editFlag"
            btn-style="bookResave"
            color="white"
            link-text="再保存する"
            class="c-booklist_btn-resave"
            @click.native="resaveComment"
          />
        </dd>
      </dl>
      <AtomsButtonsTextBtn
        btn-style="outside"
        color="dark"
        :link-path="bookLink"
        link-text="詳細を見る"
        class="c-booklist_btn"
      />
      <AtomsButtonsTextBtn
        btn-style="trash"
        color="red"
        link-text="本棚から削除する"
        class="c-booklist_btn--trash"
        @click.native="deleteBook"
      />
    </MoleculesEtcModal>
  </div>
</template>

<script>
export default {
  // まずはpropsで必要なデータを受け取る
  props: {
    bookImg: {
      type: String,
      default: ''
    },
    bookImgModal: {
      type: String,
      default: ''
    },
    bookLink: {
      type: null,
      default: ''
    },
    bookTitle: {
      type: String,
      default: ''
    },
    bookTitleCaption: {
      type: String,
      default: ''
    },
    bookAuthors: {
      type: Array,
      default: () => []
    },
    bookPublisher: {
      type: String,
      default: ''
    },
    bookComment: {
      type: String,
      default: ''
    },
    countArray: {
      type: String,
      default: ''
    }
  },
  data () {
    return {
      // コメント再編集状態を判定するためフラグを立てる
      editFlag: false,
      editedComment: ''
    }
  },
  // モダルウィンドウ展開時、<body>にclass="modal-on"を付与
  head () {
    return {
      bodyAttrs: {
        class: this.isModalState ? 'modal-on' : ''
      }
    }
  },
  computed: {
    // storeに書いたモダルウィンドウのフラグ（過去記事）
    isModalState () {
      return this.$store.state.Modal.modalFlag
    },
    // コメントが再編集されたら$emitする
    getBookComment: {
      get () {
        return this.bookComment
      },
      set (newComment) {
        this.$emit('input', newComment)
      }
    }
  },
  // $emitだったりフォーカスだったり
  methods: {
    openModal () {
      this.$emit('open-modal')
    },
    deleteBook () {
      this.$emit('delete-btn')
    },
    editComment () {
      this.editFlag = !this.editFlag
      this.$refs.editComment.focus()
    },
    resaveComment () {
      this.$emit('resave-btn')
      this.editFlag = false
    }
  }
}
</script>
```

ただの子コンポネント。データを表示したり、もらったり渡したり。
スクリプトはコメントの通り。


##Api.vue（スクリプト部分）

これ以上コンパクトに書く方法がわからなくて長い。
コメントで解説。

```vue:components/template/Api.vue
<script>
import axios from 'axios'
import _ from 'lodash' // これを書かないとdebaunceの「_」でエラーが出た
import { faStickyNote, faCamera } from '@fortawesome/free-solid-svg-icons'
export default {
  data () {
    return {
      // とにかく使うもの定義。絶対もっといい書き方がありそう
      items: [],
      isbn: '',
      message: '',
      books: [],
      itemComment: '',
      itemId: '',
      itemTitle: '',
      itemLink: [],
      itemImg: '',
      itemAuthors: [],
      itemPublisher: '',
      itemCount: 0,
      modalItem: '',
      Quagga: null,
      code: '',
      countArray: '',
      camera: false,
      scrollPos: 0,
      readedLength: 0,
      pageMath: []
    }
  },
  computed: {
    faStickyNote () {
      return faStickyNote
    },
    faCamera () {
      return faCamera
    },
    // スクロール量をscssで使う準備
    scrollTopPosition () {
      return {
        '--top': this.$window.pageYOffset + 'px'
      }
    },
    // localstrageに保存されているオブジェクトの数をカウント
    booksLength () {
      return this.books.length.toLocaleString()
    },
    // localstrageに保存されているページ数データの合計を取得
    pagesCount () {
      const booksCount = this.books.reduce(function (sum, element) {
        return sum + element.count
      }, 0).toLocaleString()
      return booksCount
    }
  },
  // 入力されたISBNコードをdebaunce対策を行って取得
  watch: {
    isbn (newIsbn, oldIsbn) {
      this.message = 'Now loading...'
      this.debouncedGetAnswer()
    }
  },
  // localstrageにデータがあったらそれを引っ張ってきて、フラグを立てたり
  // 削除でフラグを変えたり
  mounted () {
    if (localStorage.getItem('books')) {
      try {
        this.books = JSON.parse(localStorage.getItem('books'))
        this.haveBooks = true
      } catch (e) {
        localStorage.removeItem('books')
        this.haveBooks = false
      }
    }
  },
  // debaunce対策して1秒経ってから通信
  created () {
    this.debouncedGetAnswer = _.debounce(this.getAnswer, 1000)
  },
  methods: {
    // ISBNを検知したらGoogleBooksAPIから情報を検索し、必要な変数に格納する
    getAnswer () {
      if (this.isbn) {
        axios.get('https://www.googleapis.com/books/v1/volumes?q=isbn:' + this.isbn).then((response) => {
          this.items = response.data.items
          this.itemId = response.data.items[0].id
          this.itemLink = response.data.items[0].volumeInfo.previewLink
          this.itemTitle = response.data.items[0].volumeInfo.title
          if (response.data.items[0].volumeInfo.imageLinks) {
            this.itemImg = response.data.items[0].volumeInfo.imageLinks.thumbnail
          } else {
            this.itemImg = 'http://books.google.com/books/content?printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api'
          }
          this.itemAuthors = response.data.items[0].volumeInfo.authors
          this.itemPublisher = response.data.items[0].volumeInfo.publisher
          this.itemCount = response.data.items[0].volumeInfo.pageCount
        })
      }
      this.message = ''
    },
    // localstrageに保存するデータをまとめたり、入力されている内容を削除したり
    saveBtn () {
      let saveGroup = {
        id: this.itemId,
        link: this.itemLink,
        title: this.itemTitle,
        img: this.itemImg,
        authors: this.itemAuthors,
        publisher: this.itemPublisher,
        comment: this.itemComment.replace(/\\n/g, '\\n'),
        count: this.itemCount
      }
      this.books.unshift(saveGroup)
      saveGroup = ''
      this.isbn = ''
      this.items = ''
      this.itemComment = ''
      this.saveBook()
    },
    // localstrageに保存する
    saveBook () {
      const parsed = JSON.stringify(this.books)
      localStorage.setItem('books', parsed)
    },
    // モダルウィンドウに渡すデータを判定
    openModal (book, index) {
      this.$store.commit('Modal/openModal')
      this.modalItem = book
      this.countArray = index
    },
    // localstrageから該当のオブジェクトを削除する
    deleteBtn () {
      this.books.splice(this.countArray, 1)
      this.saveBook()
      this.$store.commit('Modal/closeModal')
    },
    // コメントを再編集して保存する
    resaveBtn () {
      const activeBooks = this.countArray
      this.books[activeBooks].comment = this.modalItem.comment
      this.saveBook()
    },
    // 読み終わった本の数
    readedBooks () {
      this.readedLength = this.booksLength
    },
    // 読み終わった本の総ページ数
    readedPages () {
      this.pageMath = this.pagesCount
    },
    // ここからQuaagaJS
    // 基本的に参考記事の通り（一部コメント）
    startScan () {
      this.code = ''
      this.initQuagga()
      this.camera = true
    },
    stopScan () {
      this.Quagga.offProcessed(this.onProcessed)
      this.Quagga.offDetected(this.onDetected)
      this.Quagga.stop()
      this.camera = false
    },
    initQuagga () {
      this.Quagga = require('quagga')
      this.Quagga.onProcessed(this.onProcessed)
      this.Quagga.onDetected(this.onDetected)
      // 設定
      const config = {
        inputStream: {
          name: 'Live',
          type: 'LiveStream',
          target: document.querySelector('#cameraArea'),
          constraints: {
            decodeBarCodeRate: 3,
            successTimeout: 500,
            codeRepetition: true,
            tryVertical: true,
            frameRate: 15,
            width: 640,
            height: 480,
            facingMode: 'environment'
          },
          area: {
            top: '30%',
            right: '0%',
            bottom: '30%',
            left: '0%'
          }
        },
        locator: {
          patchSize: 'medium',
          halfSample: true
        },
        numOfWorkers: navigator.hardwareConcurrency || 4,
        decoder: {
          readers: ['ean_reader'],
          multiple: false
        },
        locate: true
      }
      this.Quagga.init(config, this.onInit)
    },
    onInit (err) {
      if (err) {
        return
      }
      this.Quagga.start()
    },
    onDetected (success) {
      this.code = success.codeResult.code
      // ISBN13は978から始まるので、読み取った数字の最初3文字が978であれば
      // 変数isbnに値を渡すよう限定的に処理
      // こうしないと誤認識が多くて使えなかった
      if (this.code.slice(0, 3) === '978') {
        this.isbn = this.code
        this.Quagga.stop()
        this.camera = false
      }
    },
    onProcessed (result) {
      const drawingCtx = this.Quagga.canvas.ctx.overlay
      const drawingCanvas = this.Quagga.canvas.dom.overlay
      if (result) {
        // 検出中の緑の線の枠
        if (result.boxes) {
          drawingCtx.clearRect(0, 0, drawingCanvas.width, drawingCanvas.height)
          const hasNotRead = box => box !== result.box
          result.boxes.filter(hasNotRead).forEach((box) => {
            this.Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, {
              color: 'green',
              lineWidth: 2
            })
          })
        }
        // 検出に成功した瞬間の青い線の枠
        if (result.box) {
          this.Quagga.ImageDebug.drawPath(
            result.box,
            { x: 0, y: 1 },
            drawingCtx,
            {
              color: 'blue',
              lineWidth: 2
            }
          )
        }
        // 検出に成功した瞬間の水平の赤い線
        if (result.codeResult && result.codeResult.code) {
          this.Quagga.ImageDebug.drawPath(
            result.line,
            { x: 'x', y: 'y' },
            drawingCtx,
            {
              color: 'red',
              lineWidth: 3
            }
          )
        }
      }
    }
  }
}
</script>
```


##Api.vue（全文）

※QuaggaJSのcssなどは割愛

```vue:components/template/Api.vue
<template>
  <main>
    <section id="p-api_box01">
      <div class="c-box">
        <AtomsTitlesTit
          tit-tag="h2"
          tit-class=""
          tit-txt="Api"
        />
        <p class="f-txt">
          GoogleBooksAPIでISBNに限定した検索を行い、その内容をローカルストレージに保存する。
        </p>
      </div>
    </section><!-- /box01 -->

    <section id="p-api_box02">
      <div class="c-box">
        <AtomsTitlesTit
          tit-tag="h3"
          tit-class="middle"
          tit-txt="Search for ISBNs on Google Books"
        />
        <dl class="p-api_box02--search">
          <dt>ISBN検索</dt>
          <dd>
            <MoleculesEtcSearchIsbn v-model="isbn" />
            <button class="p-api_box02--search-btn-scan" @click="startScan">
              バーコードから探す<fa :icon="faCamera" />
            </button>
            <div v-show="camera" class="p-api_box02--search-scan" :style="scrollTopPosition">
              <p class="p-api_box02--search-scan-txt">
                「978」から始まるバーコードを映してください。
              </p>
              <div id="cameraArea" />
              <button class="p-api_box02--search-btn-stop" aria-label="close" @click.prevent.stop="stopScan">
                閉じる
              </button>
            </div>
          </dd>
        </dl>
        <template v-if="message">
          <p class="p-api_box02--message">
            {{ message }}
          </p>
        </template>
        <div v-for="item in items" :key="item.id" class="p-api_box02--result">
          <MoleculesEtcSearchIsbnResult
            v-model="itemComment"
            :item-link="itemLink"
            :item-img="itemImg"
            :item-title="itemTitle"
            :item-authors="itemAuthors"
            :item-publisher="itemPublisher"
            :item-description="item.volumeInfo.description"
            :item-comment="itemComment"
            @saveBtn="saveBtn"
          />
        </div>
      </div>
    </section><!-- /box02 -->

    <section v-if="booksLength > 0" id="p-api_box03">
      <div class="c-box">
        <AtomsTitlesTit
          tit-tag="h3"
          tit-class="middle"
          tit-txt="Your Favorite Books"
        />
        <div v-if="booksLength > 0" class="p-api_box03--readedcount">
          <p class="p-api_box03--readedcount-books">
            あなたは<br>
            <span>{{ booksLength }}</span>冊 / <span>{{ pagesCount }}</span>ページ<br>
            読みました
          </p>
        </div>
        <ul class="c-booklist">
          <li v-for="(book, index) in books" :key="book.id">
            <MoleculesEtcModalBook
              v-model="modalItem.comment"
              :book-img="book.img"
              :book-img-modal="modalItem.img"
              :book-link="modalItem.link"
              :book-title-caption="book.title"
              :book-title="modalItem.title"
              :book-authors="modalItem.authors"
              :book-publisher="modalItem.publisher"
              :book-comment="modalItem.comment"
              @open-modal="openModal(book, index)"
              @delete-btn="deleteBtn()"
              @resave-btn="resaveBtn()"
            />
          </li>
        </ul>
      </div>
    </section><!-- /box03 -->

    <AtomsButtonsTextBtn
      btn-style="rounded back"
      color="dark"
      link-path=""
      link-text="Back to Front"
    />
  </main>
</template>

<script>
import axios from 'axios'
import _ from 'lodash'
import { faStickyNote, faCamera } from '@fortawesome/free-solid-svg-icons'
export default {
  data () {
    return {
      items: [],
      isbn: '',
      message: '',
      books: [],
      itemComment: '',
      itemId: '',
      itemTitle: '',
      itemLink: [],
      itemImg: '',
      itemAuthors: [],
      itemPublisher: '',
      itemCount: 0,
      modalItem: '',
      Quagga: null,
      code: '',
      countArray: '',
      camera: false,
      scrollPos: 0,
      readedLength: 0,
      pageMath: []
    }
  },
  computed: {
    faStickyNote () {
      return faStickyNote
    },
    faCamera () {
      return faCamera
    },
    scrollTopPosition () {
      return {
        '--top': this.$window.pageYOffset + 'px'
      }
    },
    booksLength () {
      return this.books.length.toLocaleString()
    },
    pagesCount () {
      const booksCount = this.books.reduce(function (sum, element) {
        return sum + element.count
      }, 0).toLocaleString()
      return booksCount
    }
  },
  watch: {
    isbn (newIsbn, oldIsbn) {
      this.message = 'Now loading...'
      this.debouncedGetAnswer()
    }
  },
  mounted () {
    if (localStorage.getItem('books')) {
      try {
        this.books = JSON.parse(localStorage.getItem('books'))
        this.haveBooks = true
      } catch (e) {
        localStorage.removeItem('books')
        this.haveBooks = false
      }
    }
  },
  created () {
    this.debouncedGetAnswer = _.debounce(this.getAnswer, 1000)
  },
  methods: {
    getAnswer () {
      if (this.isbn) {
        axios.get('https://www.googleapis.com/books/v1/volumes?q=isbn:' + this.isbn).then((response) => {
          this.items = response.data.items
          this.itemId = response.data.items[0].id
          this.itemLink = response.data.items[0].volumeInfo.previewLink
          this.itemTitle = response.data.items[0].volumeInfo.title
          if (response.data.items[0].volumeInfo.imageLinks) {
            this.itemImg = response.data.items[0].volumeInfo.imageLinks.thumbnail
          } else {
            this.itemImg = 'http://books.google.com/books/content?printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api'
          }
          this.itemAuthors = response.data.items[0].volumeInfo.authors
          this.itemPublisher = response.data.items[0].volumeInfo.publisher
          this.itemCount = response.data.items[0].volumeInfo.pageCount
        })
      }
      this.message = ''
    },
    saveBtn () {
      let saveGroup = {
        id: this.itemId,
        link: this.itemLink,
        title: this.itemTitle,
        img: this.itemImg,
        authors: this.itemAuthors,
        publisher: this.itemPublisher,
        comment: this.itemComment.replace(/\\n/g, '\\n'),
        count: this.itemCount
      }
      this.books.unshift(saveGroup)
      saveGroup = ''
      this.isbn = ''
      this.items = ''
      this.itemComment = ''
      this.saveBook()
    },
    saveBook () {
      const parsed = JSON.stringify(this.books)
      localStorage.setItem('books', parsed)
    },
    openModal (book, index) {
      this.$store.commit('Modal/openModal')
      this.modalItem = book
      this.countArray = index
    },
    deleteBtn () {
      this.books.splice(this.countArray, 1)
      this.saveBook()
      this.$store.commit('Modal/closeModal')
    },
    resaveBtn () {
      const activeBooks = this.countArray
      this.books[activeBooks].comment = this.modalItem.comment
      this.saveBook()
    },
    readedBooks () {
      this.readedLength = this.booksLength
    },
    readedPages () {
      this.pageMath = this.pagesCount
    },
    startScan () {
      this.code = ''
      this.initQuagga()
      this.camera = true
    },
    stopScan () {
      this.Quagga.offProcessed(this.onProcessed)
      this.Quagga.offDetected(this.onDetected)
      this.Quagga.stop()
      this.camera = false
    },
    initQuagga () {
      this.Quagga = require('quagga')
      this.Quagga.onProcessed(this.onProcessed)
      this.Quagga.onDetected(this.onDetected)
      // 設定
      const config = {
        inputStream: {
          name: 'Live',
          type: 'LiveStream',
          target: document.querySelector('#cameraArea'),
          constraints: {
            decodeBarCodeRate: 3,
            successTimeout: 500,
            codeRepetition: true,
            tryVertical: true,
            frameRate: 15,
            width: 640,
            height: 480,
            facingMode: 'environment'
          },
          area: {
            top: '30%',
            right: '0%',
            bottom: '30%',
            left: '0%'
          }
        },
        locator: {
          patchSize: 'medium',
          halfSample: true
        },
        numOfWorkers: navigator.hardwareConcurrency || 4,
        decoder: {
          readers: ['ean_reader'],
          multiple: false
        },
        locate: true
      }
      this.Quagga.init(config, this.onInit)
    },
    onInit (err) {
      if (err) {
        return
      }
      this.Quagga.start()
    },
    onDetected (success) {
      this.code = success.codeResult.code
      if (this.code.slice(0, 3) === '978') {
        this.isbn = this.code
        this.Quagga.stop()
        this.camera = false
      }
    },
    onProcessed (result) {
      const drawingCtx = this.Quagga.canvas.ctx.overlay
      const drawingCanvas = this.Quagga.canvas.dom.overlay
      if (result) {
        // 検出中の緑の線の枠
        if (result.boxes) {
          drawingCtx.clearRect(0, 0, drawingCanvas.width, drawingCanvas.height)
          const hasNotRead = box => box !== result.box
          result.boxes.filter(hasNotRead).forEach((box) => {
            this.Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, {
              color: 'green',
              lineWidth: 2
            })
          })
        }
        // 検出に成功した瞬間の青い線の枠
        if (result.box) {
          this.Quagga.ImageDebug.drawPath(
            result.box,
            { x: 0, y: 1 },
            drawingCtx,
            {
              color: 'blue',
              lineWidth: 2
            }
          )
        }
        // 検出に成功した瞬間の水平の赤い線
        if (result.codeResult && result.codeResult.code) {
          this.Quagga.ImageDebug.drawPath(
            result.line,
            { x: 'x', y: 'y' },
            drawingCtx,
            {
              color: 'red',
              lineWidth: 3
            }
          )
        }
      }
    }
  }
}
</script>
```

##結果

正直、バーコードの読み取り精度に難はあると思う。
でも求めていた機能を実装することができた。