7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【SCP】英語勉強用PWAアプリをNuxtで3日+αで作ってみる

Last updated at Posted at 2019-09-22

英語よわよわの大学生です。北海道でIT系の大学生やっているのですが、将来もみすえると英語を最低でも読んでなんとなく理解はできる程度の力を付けたいなと思っています。週イチくらいで

注:この記事は今後身近な人をサーバーレス・Nuxtの道に勧誘してWebプログラミングの道へ引きずり込む際のチュートリアルとする事をサブ目標として書いてます。そのため、この記事には冗長あるいは砕けた表現、不必要な部分を含む長いコードなどがあるため、記事の大部分を折りたたんでおります。ご了承ください。

成果物: SCPを英語で読もう!

なんで英語学習続かないん?

  1. 単語帳の例文つまんない
  2. 単語帳持ち歩きたくない
  3. いい感じの学習アプリはだいたい月額制
  4. つまんない
  5. Googleさん。いつもありがとう
  • 問題点1と4より、英語が読めなさすぎてストレスばかり溜まって直ぐにやめてしまう原因となっているのがわかります。せめて面白ければ……,すぐに日本語訳を参照できたら……
  • 問題点2、紙媒体は重く電車の中で開くのもにめんどくさくなります。スマホで勉強したい
  • 問題点3は致し方ありません。アプリ開発者も「質の高い英文や訳文」やアプリそのものを提供するのにお金がかかります。
    • ただリ英文が無料で利用できて、質の高い訳があれば無料でもそれなりに解決しそうです
  • 問題点5。こればっかりはどうしようもない。

今までの敗因は「続かなかった事」単純に「続ける」事さえできれば私の勝ちです。T○KIO的発想でどうにかします。

制約

どこでも見れるスマホで使いたいですが、ネイティブアプリの開発経験はないのでWebアプリくらいしか私は作れません。(たとえスマホで見なくてもWebアプリくらいしか私は作れません)
しかしその場合、いくら質の高い英文や訳文があっても著作権法の例外である個人使用に引っかからなくなります。
個人使用の範囲であれば比較的ゆるい条件で著作物を使えますが、Webアプリとして公開すれば他の人が使わなくても個人使用にはなりません。
ちゃんと著作権の問題が一切ないコンテンツが必要です。

うーんそんな都合の良い……

面白く無料で利用できる英文と和文のセットそんな都合の良いコンテンツ……あっ

SCP財団SCP Foundation

これならほぼすべてのコンテンツがCC BY-SA 3.0のライセンス下で公開されてます!

SCPとは何?って方もいるかとは思いますので軽く説明させていただきますと、リモートでファイルをコピーするコマンドではなく、
SCP(あるいはSCiP)と呼ばれるような非科学的あるいは超常的で人類の害となったりならなかったりする物品や生物を確保し、収容、人類やその存在を保護するという目的を持った「財団」を舞台とした共同怪奇創作サイトです。面白そうですね。
最近は「収容違反 インシデント」と言うSCP財団オンリー即売会もやっていたりとかなり活発ですので、既に知っているという方も多いことでしょう。

SCP Foundationは英語でその様なコンテンツが日夜投稿され、SCP財団では日本語でその様なコンテンツが投稿されたりSCP Foundationに投稿されたコンテンツの翻訳版が投稿されていたりします。

今回は翻訳版を利用していきましょう。

使う技術。アーキテクチャを考える0日目

AWSしか使ったこと無いので、使うのはAWSです。
CloudFront->S3という王道の方法でSSL化した静的サイトで公開します。
Cognitoを使ってユーザー認証をとりあえず付けときましょう。(2日目私より「間に合わん!諦めろ!」)
S3にアップする静的サイトのファイルはNuxtでサックリ作りましょう。今回はスマホがターゲット操作時間が長いのでPWAを試してみます。(Firebaseも今後試したい)
とりあえず3日ではここまでやっていきます。この3日というのは平日で初心者が色々調べながらというのを想定しているので、イケイケでつよつよな人なら半日でもできると思います。

ただ、少し挑戦もしましょう。脳死でつかってたBootstrap、便利ですがCSSで完結しておらずスマートじゃないですね。Bulumaが最近話題なうえデザインもいい感じなので使ってきましょう。

オプションとして以下の2つも対応してみます。
スワイプで画面を切り替えるVueのプラグイン?があったと思うのでそれで英↔日のコンテンツの切り替えをするとクールですね。v-showをうまく使いましょう。
英単語を選択すると翻訳した単語が出てくるのもやってみたいです。パブリックな英和辞典があったはずなのでそれで実現します。

ここまで0日目、次の日に行きましょう……

Nuxtでの開発とPWA|1日目ここから

**1日目**

インストール - Nuxt.jsを見てセットアップします。
試しにyarn add @nuxtjs/google-analyticsすると、下の様な警告が出る事があるので言われ通りbabelとかを追加していきます。

warning "babel-jest > babel-preset-jest > @babel/plugin-syntax-object-rest-spread@7.2.0" has unmet peer dependency "@babel/core@^7.0.0-0".
warning "vue-jest > @babel/plugin-transform-modules-commonjs@7.6.0" has unmet peer dependency "@babel/core@^7.0.0-0".
ナドナド

4. PWA対応させる

Get Started | ⚡ Nuxt PWAをそのままやってみます。
yarn add @nuxtjs/pwaしてmodulesにpwaを追加。
staticディレクトリに1辺が512px以上のicon.pngを置けって書いててあるので、ロゴとクレジットと背景選んで作ってみました。背景画像はパブリッドメイン。ロゴのクレジットは画像に記載。
scp?.png

iconってこれが良いのかはわからないがとりあえず次へ

Workbox Moduleの設定を見る

Workbox Module | ⚡ Nuxt PWAここ

nuxt.config.js
pwa: {
  workbox: {
    /* workbox options */
  }
}

で色々なオプションを追加したり、上書きできるらしいですが私はしませんでした。

Meta Module

これもデフォルトで良いかな〜と思って眺めてた所、すこし気になる記述ありました。

mobileAppIOS
Default: false
Meta: apple-mobile-web-app-capable
Please read this resources before you enable mobileAppIOS option:

https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
https://medium.com/@firt/dont-use-ios-web-app-meta-tag-irresponsibly-in-your-progressive-web-apps-85d70f4438cb

デフォルトのままだとfalseなのでiOSではPWAの全画面表示的な事ができない感じでしょうか。
デフォルトでfalseの理由はURLで記述されています。
2016年の話ですが、apple-mobile-web-app-capableはバグるよ!みたいなことだと思います。
……私としてはスマートフォンはiOS端末しか持ってないし、最近聞く限りではAppleもPWAに乗り気になり始めたとの事を風のうわさで聞いたので、多分大丈夫だと信じてtrueに変更してみます。
言語もデフォルトはenなので、jpに変更

nuxt.config.js
    meta: {
      mobileAppIOS: true,
      lang: 'jp'
    }

twitterCardの設定とかもあるので、後で変更予定あり。

Manifest Module

作ったら始まるNuxtでPWAものがたりWeb App Manifest
を参考にさせていただいた 。
theme_colorとbackground_colorは後で指定する。

nuxt.config.js
    manifest: {
      name: "SCPを英語で読もう!",
      title: "SCPを英語で読もう!",
      short_name: "SCPと英語!",
      'og:title': 'SCPを英語で読もう!',
      description: 'SCPを気軽に英語で読もう!毎日読んで英語力向上を目指す!',
      'og:description': 'SCPを気軽に英語で読もう!毎日読んで英語力向上を目指す!',
      lang: 'ja',
      theme_color: "#b09b89",
      background_color: "white",
      display: "standalone",
      scope: "/",
      start_url: "/"
    }

OneSignal Module

通知機能っぽい。AWSのSNSで代用できないか考える……TODOに追加し今回は見送ります。

PWAのテスト

yarn devのローカル環境でLighthouseで試してみるとなんかiconが無いよって言われる。少なくてもiconが無いのでPWA認定されていない。
これはyarn devの時にはサイズ別のアイコンが作成されないため。
nuxt generateとかした時にiconが自動で作成されるのが確認できました。他にもオフライン時ステータス200返してほしいとか言う警告がでてますが、その警告が出た状態でもLighthouseはPWAとして認めてるので後回しにします。

次の日に行きましょう。

# 2日目サックリとサイト作ってく
**2日目**

Bulumaの学習をしていきましょう。

Bulumaで注目したクラスなど
### button クラス
  • is-primary
  • is-link
  • is-info
  • is-success
  • is-warning
  • is-danger

大きさ指定は

  • is-small
  • is-medium
  • is-large

などなど。

Columns powered by Flexboxーカラム


<div class="columns">
  <div class="column">
    First column
  </div>
  <div class="column">
    Second column
  </div>
  <div class="column">
    Third column
  </div>
  <div class="column">
    Fourth column
  </div>
</div>

とするとレスポンシブにカラムがならぶ。他にもFlexboxのデザインが色々ある。使うならしっかり読み込むと色々と柔軟にデザインできそう。

levelクラス

水平にブロックを置くのに使うクラス。
level>( level-left || level-right ) > level-itemのという構造で使う。かなり便利そうだが、文字だと伝わりづらいので公式サイトの例を見ると良い。

Form系

Bootstrapの用に何もしなくてもある程度整うというよりは、しっかりクラスをつけていくスタイルっぽいです。

  • fieldクラス 以下の3つのクラスのみを含みます。他のfieldとの間隔を一定に保ちます

    • label
    • control 以下の4つの要素を含みます。
      • input
      • select
      • button
      • icon 以下のクラスを追加できます。Fontawsomeと一緒に使うといい感じ
        • is-left has-icons-left
        • is-right has-icons-right
    • help 最低文字数6文字などの注釈などを入れます
  • has-addonsクラス controlないでinputとボタンなどをくっ付けるために使うっぽいです(attach controlsが読めなかった)

  • is-expandedクラス 画面の横いっぱいにします。色々な組み合わせがあるようです。

  • has-addons-centeredクラス controlの上のdivなどに付与し、controlを中央に配置

  • has-addons-rightクラス has-addons-centeredと同様、controlを右端に配置

  • groupクラス field groupとして使い、controlを1つのグループとします。以下のクラスと一緒に用いることでグループの配置を変更できます

    • is-grouped-centered 中央
    • is-grouped-right 右端

などなど時間がないのでココまで

ふむ……とりあえずスマホ対応したヘッダーはほしいですね。 [Navbar | Bulma: Free, open source, & modern CSS framework based on Flexbox](https://bulma.io/documentation/components/navbar/)を参考に componentsにnavi.vueを作って、カスタマイズしていきます。 Googleでサイト検索と、レスポンシブヘッダー部分開閉用のdataと検索用のdataとmethodを用意しました。
navi.vue
<template>
  <div>
  </div>
</template>
<script>
export default {
  name: 'naviComponent',
  data() {
    return {
      toggle: false,
      searchVal: ''
    }
  },
  methods: {
    googleSearch() {
      const url =
        'https://www.google.co.jp/search?q=a' +
        encodeURIComponent(this.searchVal) +
        '&as_sitesearch='+document.domain
      location.href = url
    }
  }
}
</script>
<style></style>

HTMLとかに入ったのでfontAwsomeについてですが、Nuxtに追加する形で行います。理由はCDNだと遅いからですね。
[Nuxt]FontAwesomeで使いたいアイコンだけを使おう[Vue]さんを参考にさせていただきました。

**Nuxt+FontAwesomeの設定**
yarn add @fortawesome/fontawesome-svg-core
yarn add @fortawesome/free-brands-svg-icons
yarn add @fortawesome/free-solid-svg-icons
yarn add @fortawesome/vue-fontawesome
/plugins/FontAwsome.js
import Vue from 'vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'

/** これで読み込むのだけ選択 **/
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { faTwitter, faFacebookF, faLine } from '@fortawesome/free-brands-svg-icons'
//import { } from '@fortawesome/free-regular-svg-icons'

library.add(faSearch,faTwitter,faFacebookF,faLine)

Vue.component('fa-icon', FontAwesomeIcon)

Vue.config.productionTip = false
nuxt.config.js
plugins: [
    { src: '@plugins/FontAwesome.js', ssr: false }
]
<!--検索などに-->
<fa-icon :icon="['fas', 'search']"/>
### ハンバーガーメニューを開いたり閉じたりしたい Bulumaはなんかis-activeクラスを付与したり付与しなかったりすると、スマホ等で見た時上からヘッダーの内容が垂れ下がってきたり、戻ったりするようです。 例示されてるコードはvueじゃないので:classと@clickでサックリ作り直します。 その工程を経て以下のコードと見た目となりました。
**navi.vueのソース(折りたたみ)**

image.png

navi.vue
<template>
  <div class="header-wrapper">
    <header>
    <nav class="navbar" role="navigation" aria-label="main navigation">
      <div class="navbar-brand nav-border">
        <n-link to="/" class="navbar-item has-text-black font-family-title button is-outlined  is-rounded">
          SCPを英語で読もう!
        </n-link>
        <a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" @click="toggle=!toggle">
          <span aria-hidden="true"></span>
          <span aria-hidden="true"></span>
          <span aria-hidden="true"></span>
        </a>
      </div>

      <div class="navbar-menu" :class="{'is-active': toggle }">
        <div class="navbar-start">
          <a class="navbar-item">
            色々
          </a>

          <div class="navbar-item has-dropdown is-hoverable">
            <a class="navbar-link" @click="LicenseLink=!LicenseLink">
              ライセンス等
            </a>

            <div class="navbar-dropdown" v-show="LicenseLink">
              <a class="navbar-item" href="http://www.scp-wiki.net/">
                SCP Foundation
              </a>
              <a class="navbar-item" href="http://ja.scp-wiki.net/">
                SCP財団
              </a>
              <hr class="navbar-divider">
              <a class="navbar-item">
                ライセンス詳細
              </a>
              <hr class="navbar-divider">
              <a class="navbar-item">
                プライバシーポリシー
              </a>
            </div>
          </div>
        </div>

        <div class="navbar-end">
          <div class="navbar-item">
            <div class="field has-addons">
              <div class="control">
                <input class="input" type="text" v-model="searchVal" placeholder="検索(Google)">
              </div>
              <div class="control">
                <button class="button is-info" @click="googleSearch()" :disabled="searchVal === ''">
                  <fa-icon :icon="['fas', 'search']"/>
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </nav>
    </header>

  </div>
</template>
<script>
export default {
  name: 'naviComponent',
  data() {
    return {
      toggle: false,
      LicenseLink: false,
      searchVal: ''
    }
  },
  methods: {
    googleSearch() {
      const url =
        'https://www.google.co.jp/search?q=a' +
        encodeURIComponent(this.searchVal) +
        '&as_sitesearch='+document.domain
      location.href = url
    }
  }
}
</script>
<style>
# header {
  width: 100vw;
}
.nav-border {
  padding-top:5px;
  padding-bottom:5px;
}
.font-family-title {
  font-family: "M PLUS 1p","Hiragino Kaku Gothic ProN","メイリオ", sans-serif;
  font-weight: bold;
}
</style>

コンテンツの表示について

SCPのコンテンツには大きく分けて2種類あります。
SCPの報告書と、それらを舞台としたTaleです。
また、それぞれが千個単位(?)で存在しているため一つ一つpageを作成するのは非効率的です。なので今回は

  • ルートページ(リスト)
    • 報告書ページ
    • Taleページ

の3ページのみ作成します。
また、これだけではコンテンツの情報がまったくないため

  • JSONフォルダ
    • 報告書
      • JP
      • EN
    • Tale
      • JP
      • EN

にコンテンツをJSONで入れてそれをfetchで引っ張ってくることで表示させようと思います。
Taleも面白いのが多いのですが、スクレイピング
では、まずJSONフォルダにコンテンツを入れましょう。

# 3日目 完成させる|最終日(?) コンテンツを取得してJSONすることは前日決めました。しかし手動ではとても骨が折れるので、私はローカルでnodejsをうごかしてスクレイピングします。 そのスクリプトのソースは一応Githubに挙げてますが、Node.jsは初めてなので……お察しください。 https://github.com/tenzikukanzya/SCPSearchBase

これでTOPページ、コンテンツが出来上がったので残りは記事の表示ページです。

<template>
<div class="wrapper">
    <nuvi-component />
    <div class="tabs is-centered">
      <ul>
        <li :class="{'is-active':isEnglish}"><a @click="isEnglish=true">English</a></li>
        <li :class="{'is-active':!isEnglish}"><a @click="isEnglish=false">日本語</a></li>
      </ul>
    </div>
    <div id="scp-en" class="scp-post post-en" v-show="isEnglish" v-html="enContent.content">
    </div> 
    <div id="scp-jp" class="scp-post" v-show="!isEnglish" v-html="jpContent.content">
    </div>
</div>
</template>
(略)

isEnglishで表示タブの管理をおこなって、コンテンツを読み込んでv-htmlで表示させます。

scp-beta.gif

これでアップロードはまだですが、最低限英語を勉強できる環境ができました

オプション

Mouse Dictionary←すごい

すごい。どんなツールか気になる方はリンクを見てみてください。
このツールを使っていて便利だと感じたのは

  • めちゃ検索早い
  • カーソル乗せるだけ
  • 熟語対応

この2点です。欠点はスマホでは使えないことくらい。
リスペクトして次の機能を組み込んでいきましょう。

  • 英単語・熟語を選択すると訳がでる
  • 画面したで常駐
  • クリックするとポップアップで詳細表示

こういった内容で行きます。
英和辞典はパブリックなEJDictを使います。しかし、このファイルはTXTでも5MB強あるので、それを使いやすいようにJSONにすると容量が膨れ上がる事が予想されます。
今回はSCP記事の英単語のみを抽出してそこからEJDictに訳があるものだけを取り出す事でファイルサイズを抑えることにしました。

ソースはhttps://github.com/tenzikukanzya/SCPSearchBase のcreateENJSON.jsにあるので、興味がる方はみてみてくだい。(Node.jsを触ったのが(略))

工夫点としてはコンテンツのすべての単語を取り出した後に、それらの単語を含む熟語を辞書データからすべて引っ張ってきて、熟語がコンテンツのテキストにあった場合は単語の配列に追加する事で、ある程度の熟語対応ができるようにしました。

ですが、このままだと辞書データの関係上単数や原型でないと訳を出してくれません。選択ということで複数形はどうにかできるかもしれませんが、基本的には~ingとかが出ないのは困ります。あいまい検索を実装しましょう。

あいまい検索、つまりファジーな検索を実装するには大雑把でいいならば正規表現でもできますが、今回はソレに特化したライブラリであるfuzzyset.jsを用います。

yarn add fuzzyset.js

をnuxtのwebpackプラグインに追加して使えるようにしたら使いたいページで

this.translationFuzzy = FuzzySet(Object.keys(ArrayData),true)

の様に検索したい配列を渡してライブラリが扱える様に渡し保存しておきます。
その後は

this.translationFuzzy.get(word,false,0.7)

~~で似ている単語を検索できるのですが、これは検索する文(この場合はword)が20文字とかになると検索がかなり重くなります。適当な値に設定しましょう。~~PCの調子が悪かっただけっぽいです。
ちなみに.getは配列で値を返してくれるのですが.getの第2引数は似た文字が見つからんかったときの返り値です。
そういった事を色々設定してやると……
scp-b.gif
便利になります。モーダルはBulumaのを@clickとかで切り替える形で作ってます。

地味な工夫

SCPのコンテンツには同wikiに対してリンクが貼ってある事もあります。これはそのまま保存しているのですが、 ドメインが異なるためユーザーがコンテンツのリンクを踏むと、私のドメイン+PATHにリンクされ404エラーが出ます。今回は表示した後にリンクのhrefを書き換える事で対処しました。

//id:scp-enはコンテンツの親要素にあたるdivのid
//http://www.scp-wiki.netは書き換え後のBaseURL
    let enHref = document.getElementById("scp-en").getElementsByTagName("a")
    for(let key=0;key<enHref.length;key++){
      if(enHref[key].pathname !== ''){
        enHref[key].href = "http://www.scp-wiki.net" + enHref[key].pathname
      }
    }

余談ですがNode.jsのアプリもスクレイピングなので、ここまで作るだけでJavaScriptのDOM操作に関する知識がめっちゃ増えました。

スワイプ

スマホでの操作の場合、スワイプで記事を切り替えれる様にします。
Nuxt.js/Vue.jsでもスワイプでページ移動したいを参考にさせていただきました。特に特記する事はありませんが。v-touch:swipe.left="flag=!flagみたいな事は出来ません。methodを通してなり、直接記載するなりして関数を渡しましょう。

CloudFront+S3

@nuxtjs/google-analyticsでGoogleAnalyticsを対応さます。詳細省略
そういった事が終わったらnuxt generateをして静的ファイルを作りましょう。
次に、S3バケットを作成します。AWS CLIでやります。

aws s3 mb s3://バケット名

(初めてCLIでバケット作ったので一応公開されてないかマネージメントコンソール見た)

yarn nuxt build --analyze

でライブラリ等の容量確認して
スクリーンショット 2019-09-22 19.53.57.png

yarn nuxt generate

nuxt genereteが静的ホスティング用でnuxt buildがSSR用らしいですね。
nuxt build --spa vs nuxt generateより

ファイルのアップロード

aws s3 sync Nuxtファイル/dist s3://バケット名

次はCloudFrontとRoute53で公開します。
ドメインのSSL証明書取得は既に行っているため省略しますが、やってない場合は先にやっとくとスムーズにできます。
CloudFront

  • Origin Domain Name -> S3バケット
  • Viewer Protocol Policy -> Redirect HTTP to HTTPS
  • Restrict Bucket Access(Yes) //これNOでも良さそう
  • Use Origin Cashe Headers(true)
  • Compress Objects Automatically(YES)
  • CNAMEs->Route 53で設定するドメイン
  • Custom SSL Certificate -> 証明書選択
  • Clients that Support(略) SNI -> true(注:Legacyは月600$なので注意)
    で作成

その後
作成したのを選択->Error->Create Custom Error Response
でHTTP Error Code 403の時 -> Response Page Pathを/index.html(HTTP200)
(これで良いかよくわかってない。)

CloudFronは設定後しばらくはCloudFront側の処理があるので、表記がDeployとなるまでは待機し、なったらRoute53の設定です。設定と言ってもAレコードのエイリアスをCloudFrontにするだけです。

結果・公開 結構いい感じ

SCPを英語で読もう!で公開しました。
Lighthouseの結果を貼るのが習わしっぽいので貼ります。(トップページの結果)
Screenshot 2019-09-22 at 22.22.55.png
Screenshot 2019-09-22 at 22.23.10.png
パフォーマンスはあまり芳しくはないですが妥協点には達しており、他はかなりいい感じです。特に気をつけて作ったわけではありませんが、ページの情報量が少ないのも影響してるのかもしれません。
PWAの値はオールグリーン、これはモジュールの力ですね。

Safariを久々に起動し、ホーム画面に追加してみました。
2019-09-22_22-33-38_000.png
いい感じです。しっかり設定が反映されています。

オフラインでも一度訪れたページは表示されるのも確認できました。
欠点としてはやはり最初は辞書データを読み込むのに時間がかかってしまいますね。今後テキストが増えていったときは辞書の分割も考慮する必要があります。

以上となります。タイトルには3日+αと書いてますが、実際のところとしては(タイトルの通り)最低限のところまでは3日で作り、他にやってみたいことや権利関係の確認のために+3日くらいで作ったので実質6日くらいです。

さらっとソースをあげたGithubのリンク載せて終わっているNode.jsのスクリプトとここに乗せていないCCライセンス関係のページ作りのための知識収集に時間かかってました。
ですが、簡単なもの(スクレイピング等のデータ収集が必要ないナドナド)なら本当に3日で作れると思うので、この記事を片手に身近な人をNuxtとサーバーレスに引きずり込もうと思います。

長い記事にも関わらずここまで読んでいただきありがとうございました。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?