GAミント至上主義

Web Monomaniacal Developer.

Vue + PWAの開発環境をvue-cli3 + pwa plugin + yarnを使ってDocker上で構築する

uyamazak.hatenablog.com
2018年の2月ごろ、VueでPWAの履歴書作成アプリを作り始めて、もうすぐ本番公開だけど改めて調べると環境がすでに大きく変わっていた。
作ったのこれ↓
rirekisho.yagish.jp

半年もたってないけどこの変化だからなるべく早く変更する準備をした方がよさそう。
大きいのは下記4点。

vue-cliのテンプレートではなくプラグインになった

以前使ったテンプレート形式のもの
github.com

新しいvue-cliプラグイン形式
vue-cli/packages/@vue/cli-plugin-pwa at dev · vuejs/vue-cli · GitHub

まだrc版なせいもあるのか、ドキュメントちょっとしか書いてない

WebPack3が4に

せっかく分かってきたところで大きく変わった
yarn.lockを見たらwebpack "^4.8.2"で入っていた

Service WorkerがWorkbox使う形に

テンプレートの時はService Workerのスクリプトは直書きされていたが、プラグインGoogleが開発しているWorkboxというService Workerを使いやすくするライブラリを使用している。
developers.google.com

PWAを推進しているGoogleが作ってるし、これは今後使っていった方が良さげ。

yarn使った方がいいかも

npm歴も浅いので違いがよくわかってないが、いろいろ良いという話は見るので、さっそく使ってみる。

ということで、さっそくDockerを使って環境を作ってみる

Dockerfile

FROM node:9-slim
RUN mkdir /pwa/
RUN apt-get update --fix-missing && apt-get -y upgrade
RUN apt-get install -y \
    git \
    bzip2

WORKDIR /pwa/

EXPOSE 8080
CMD ["bash"]

とりあえずnodeをもとにイメージを作る。
gitとbzip2は以前必要になったのでそのまま。今回はいらないかも。
適当なイメージ名をタグつけてビルドする

sudo docker build -t pwa-201807:latest .

run

作ったファイルを残しておくためappディレクトリを作ってマウント。
あと設定ファイルがホームディレクトリにできるようなので、それもマウントしておく。
rootで実行しているので/root。
本番にはビルドしたファイルのみアップし、このイメージはそのまま使わないのでrootで問題ないと思う。

mkdir app
mkdir root
sudo docker run -it --rm \
        -v `pwd`/app:/pwa/app \
        -v `pwd`/root:/root \
        -p 2106:8080 \
        --name pwa-201807 \
        pwa-201807:latest

yarnアップデート

なんかもともとyarn入ってたけど古いのでアップデートする。
1.7.0になった。

root@e8a2e26d5310:/shanyang# yarn
yarn install v1.5.1
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
info Lockfile not saved, no dependencies.
Done in 0.06s.
root@e8a2e26d5310:/shanyang# npm install -g yarn
/usr/local/bin/yarnpkg -> /usr/local/lib/node_modules/yarn/bin/yarn.js
/usr/local/bin/yarn -> /usr/local/lib/node_modules/yarn/bin/yarn.js
+ yarn@1.7.0
added 1 package in 0.439s
root@e8a2e26d5310:/shanyang#

vue/cliインストール

ドキュメント通り
cli.vuejs.org

root@e8a2e26d5310:/shanyang# yarn global add @vue/cli
yarn global v1.7.0
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.4: The platform "linux" is incompatible with this module.
info "fsevents@1.2.4" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "@vue/cli@3.0.0-rc.3" with binaries:
      - vue
Done in 8.34s.

@vue/cli@3.0.0-rc.3が入った

プロジェクト作る

名前は何でもいいけど、マウントしてあるappで作る

root@e8a2e26d5310:/shanyang# vue create app
Please pick a preset:
  default (babel, eslint)
❯ Manually select features

? Please pick a preset: Manually select features
? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◉ Progressive Web App (PWA) Support
 ◉ Router
 ◯ Vuex
 ◉ CSS Pre-processors
 ◉ Linter / Formatter
 ◉ Unit Testing
❯◉ E2E Testing

yarn使うとかいろいろ聞かれるので適当に答える。
デフォルトだとrouterとか入らないので再度やり直して、選択した。
ここにもPWAが出てきたので選択しておく

とりあえず開発サーバー起動

設定したポート(2106)にアクセスすると下記の画面が出た。

f:id:uyamazak:20180709185959p:plain

PWAプラグイン追加

インストールは
各設定をvue.config.jsかpackage.jsonのvueに書き、下記コマンドでいいらしい。

root@e8a2e26d5310:/shanyang/app# vue add @vue/pwa 

逃  Installing @vue/cli-plugin-pwa...

yarn add v1.7.0
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.4: The platform "linux" is incompatible with this module.
info "fsevents@1.2.4" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Saved lockfile.
success Saved 17 new dependencies.
info Direct dependencies
└─ @vue/cli-plugin-pwa@3.0.0-rc.3
info All dependencies
├─ @vue/cli-plugin-pwa@3.0.0-rc.3
├─ babel-runtime@6.26.0
├─ common-tags@1.8.0
├─ json-stable-stringify@1.0.1
├─ lodash.template@4.4.0
├─ lodash.templatesettings@4.1.0
├─ pretty-bytes@4.0.2
├─ workbox-broadcast-cache-update@3.3.1
├─ workbox-build@3.3.1
├─ workbox-cache-expiration@3.3.1
├─ workbox-cacheable-response@3.3.1
├─ workbox-google-analytics@3.3.1
├─ workbox-precaching@3.3.1
├─ workbox-range-requests@3.3.1
├─ workbox-streams@3.3.1
├─ workbox-sw@3.3.1
└─ workbox-webpack-plugin@3.3.1
Done in 3.54s.

✔  Successfully installed plugin: @vue/cli-plugin-pwa

噫  Invoking generator for @vue/cli-plugin-pwa...
逃  Installing additional dependencies...
yarn install v1.7.0
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.4: The platform "linux" is incompatible with this module.
info "fsevents@1.2.4" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 2.24s.

   Successfully invoked generator for plugin: @vue/cli-plugin-pwa
   The following files have been updated / added:

     public/img/icons/android-chrome-192x192.png
     public/img/icons/android-chrome-512x512.png
     public/img/icons/apple-touch-icon-120x120.png
     public/img/icons/apple-touch-icon-152x152.png
     public/img/icons/apple-touch-icon-180x180.png
     public/img/icons/apple-touch-icon-60x60.png
     public/img/icons/apple-touch-icon-76x76.png
     public/img/icons/apple-touch-icon.png
     public/img/icons/favicon-16x16.png
     public/img/icons/favicon-32x32.png
     public/img/icons/msapplication-icon-144x144.png
     public/img/icons/mstile-150x150.png
     public/img/icons/safari-pinned-tab.svg
     public/manifest.json
     src/registerServiceWorker.js
     package.json
     src/main.js
     yarn.lock

   You should review these changes with git diff and commit them.

ところどころ文字がバグってるけどいろいろPWAのファイルができた

src ディレクトリ構成

% tree src
src
├── App.vue
├── assets
│   └── logo.png
├── components
│   └── HelloWorld.vue
├── main.js
├── registerServiceWorker.js
├── router.js
└── views
    ├── About.vue
    └── Home.vue

viewsというディレクトリは所見。router用かな?
今作っているアプリはcomponentsに詰め込んでいるのでrouterで使うのはviewsに入れる形にした方がきれいかもしれない。
でも口で言うとvueと一緒だから紛らわしそう。

build時のディレクトリ構成

root@e8a2e26d5310:/shanyang/app# yarn build
yarn run v1.7.0
$ vue-cli-service build

⠦  Building for production...

 DONE  Compiled successfully in 4395ms                                                                                                                               10:04:03

  File                                      Size             Gzipped

  dist/js/chunk-vendors.217e7126.js         99.37 kb         35.09 kb
  dist/js/app.3bb5f26d.js                   5.93 kb          2.07 kb
  dist/service-worker.js                    0.94 kb          0.53 kb
  dist/precache-manifest.c4b9ea04d7e2938    0.46 kb          0.24 kb
  96813a60dd3863afb.js
  dist/css/app.928b9db9.css                 0.42 kb          0.26 kb

  Images and other types of assets omitted.

 DONE  Build complete. The dist directory is ready to be deployed.

Done in 6.60s.

package.json

root@e8a2e26d5310:/shanyang/app# cat package.json
{
  "name": "app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test:unit": "vue-cli-service test:unit",
    "test:e2e": "vue-cli-service test:e2e"
  },
  "dependencies": {
    "register-service-worker": "^1.0.0",
    "vue": "^2.5.16",
    "vue-router": "^3.0.1"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "^3.0.0-beta.15",
    "@vue/cli-plugin-e2e-cypress": "^3.0.0-beta.15",
    "@vue/cli-plugin-eslint": "^3.0.0-beta.15",
    "@vue/cli-plugin-pwa": "^3.0.0-beta.15",
    "@vue/cli-plugin-unit-mocha": "^3.0.0-beta.15",
    "@vue/cli-service": "^3.0.0-beta.15",
    "@vue/eslint-config-standard": "^3.0.0-rc.3",
    "@vue/test-utils": "^1.0.0-beta.16",
    "chai": "^4.1.2",
    "node-sass": "^4.9.0",
    "sass-loader": "^7.0.1",
    "vue-template-compiler": "^2.5.16"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]
}

vue ui

今のところ必要ないけどGUIでもいろいろできるようになったみたい
f:id:uyamazak:20180709192035p:plain


で、これから設定を開発中のものと揃えたいのだけれど、webpack系の環境変数とかの設定ファイルが見つからず、どこに書いたらいいかわからないので、今日はここまで

Vue.jsで要素の高さを使って入力値バリデーションする

現在、近日公開予定(サービス名も未定)の履歴書作成アプリをVue.jsとFirebaseでつくっている。

2018/07/12追記 公開しました

rirekisho.yagish.jp

その中で志望動機欄のような自由記述のテキストエリアのバリデーションがなんとかできたのでメモ。

こんな感じの欄。文字数や改行で高さがオーバーしたら一段階文字サイズを小さくして、大丈夫ならそのまま、それでもはみ出るならエラーを出したかった
f:id:uyamazak:20180618160130p:plain

単純に文字数、行数だけでは制限できない

テキストエリアなので自由に改行できるので文字数が少なくてもオーバーする場合がある
行数も計算しようと思ったが、文字が多い時は一段階は文字が小さくなるようにもしたいし、いろんなCSSプロパティが絡んできて何行になるかはよくわからない。
またCSSで文字サイズや行の高さを変えるたびにプログラム側で数字の調整を行うのは大変。

欄の高さは履歴書という紙への出力という目的上、そう変わらないからここが一定を超えたらエラーを出すようにしてみる。

例(この場で打った擬似コード)として、表示している箇所は下記のようなコンポーネントにしてある。

<template>
<div>
  <pre ref="reason" v-model="data.reason"></pre>
</div>
</template>

入力は親のコンポーネントでtextareaでやってるけど普通なので省略。

refを使ってclientHeightを取得する

vue側からアクセスできるようにref属性reasonをふる。

jp.vuejs.org


そうすれば、this.$refs.reason.clientHeight でclientHeightを取得できるのでこれを使う。

developer.mozilla.org

clientHeight の代わり?にgetBoundingClientRect()を使っても要素の位置やサイズが取得でき、高さも取れる。

今回試す限り同じ数値だったのでとりあえずclientHeight でいく。

また今回はtextarea内の改行をそのまま表示できるようにpreタグを使ってる。CSSでやってもいい。

$nextTickを使って描画変更後に高さを取得する

文字サイズを小さくするclassをつけても、すぐには反映されないので、$nextTickで次フレームの処理に入れる。
$nextTickは入れ子にもできる。

あとコメントで。

<script>
// 省略
  data: function () {
    return {
      data:{
        reason:''
      },
      classObject: {
        reason: {},
      },
      // 高さをpxで
      textareaMaxHeights: {
        reason: 180
      },
      errorMessages: {
        reason: '',
      }
    }
  },
  methods: {
    adjustFontSize: function (refName) {
      // 文字サイズを小さくするclass用のオブジェクトを初期化
      this.classObject[refName] = {}
      // エラーメッセージをいれるもの。まずは空で初期化
      this.errorMessages[refName] = ''
      // まずclassなしで描画されたものを$nextTickで確認
      this.$nextTick(function () {
        if (!this.$refs[refName]) return
        const maxHeight = this.textareaMaxHeights[refName]
        // 高さがオーバーしてたらfont-size-sm classを付ける
        if (this.$refs[refName].clientHeight > maxHeight) {
          this.classObject[refName] = {
            'font-size-sm': true
          }
        } else {
        // オーバーしてなかったら取る
          this.classObject[refName] = {}
        }
       // 文字サイズが反映されたら再度確認してオーバーしてたらエラーメッセージを入れる
        this.$nextTick(function () {
          if (this.$refs[refName].clientHeight > maxHeight) {
            this.errorMessages[refName] = '入力欄をオーバーしてます'
          }
        })
      })
    }
  },
  // 入力欄をwatchして変更のたびに実行。lodashのdebounceで連続実行を制御してもいいかもしれない。
  watch: {
    'data.reason': function () {
      this.adjustFontSize('reason')
    }
  }
}
</script>

これで入力画面からはerrorMessagesを見てエラーの出し分けなどをする。複雑になってきたらvuexとか使ったほうがいいかも。

$nextTickの使い方で試行錯誤しまくったのでまだバグがありそう。

ちょっとはまったのが、clientHeight がv-if,v-showで隠してしまっていると取れないこと。
v-ifでfalseだとそもそも要素がなく、v-showだとdisplay:noneで0になってしまう。

今回入力画面と、表示確認画面の表示を切り替えるUIだったので同時に表示ができなかった。

そのため表示する場所を隠したい場合は下記CSSで隠した。

visibility: hidden;
overflow: hidden;
height: 0;

FirestoreでDateを保存すると数字になったり独自のTimestampになって困った

Firestoreはjavascriptのデータをだいたいそのままぶち込んで保存ができるので便利。

だけど、Dateを保存していると、数字型(1528444872883みたいな)や独自の下記Timestamp型になって、読み込み時にエラーになって困った。

Timestamp  |  Firebase

このキーだけ、みたいな状況ならTimestampがもってるtoDate()で変換ができるけど、入れ子が深い中にあったり、型を決められないときは全データを回して処理しなきゃいけないので対処がきつい。

今回は深いデータをまるっとバックアップするのが目的だったので保存時に

JSON.stringify(backupData)

して、テキストとして保存し、読み込み時に

JSON.parse(documentSnapshot.data().backupData)

してごまかした。

2018年6月13日追記

入れ子じゃないところでもどこかで変わってしまうらしいので、返ってきたdataのtoDateメソッドがあるかどうかを見て、無かったら文字列だからnew Date()にぶち込むという苦肉の策しか思いつかない。
どこで変更されているのか、または確実にTimestamp型で保存する方法を引き続き調査する。

this.db.collection('path/to/collection')
            .where('uid', '==', this.user.uid)
            .orderBy('timestamp', 'desc')
            .limit(1)
            .get()
            .then(
              function (querySnapshot) {
                querySnapshot.forEach(function (doc) {
                  if (!doc.exists) {
                    console.log('no backup exists')
                    throw new Error('noBackup')
                  }
                  console.log('doc.data().lastSaveDate', doc.id)
                  if (typeof doc.data().lastSaveDate.toDate !== 'undefined') {
                    app.lastBackupSaveDate = doc.data().lastSaveDate.toDate()
                  } else {
                    console.log('lastSaveDate is String', new Date(doc.data().lastSaveDate))
                    app.lastBackupSaveDate = new Date(doc.data().lastSaveDate)
                  }
                })
              }).catch(function (e) {
              console.error(e)
            })