エンジニアの卵の成長日記

ふわっちで配信してますhttps://whowatch.tv/profile/w:kurowasi2525

Riot.js製の画像スライダーカスタムタグを作成した話とその際の知見

背景

僕は、とあるプロダクトの開発メンバーで、

1月からの画面の刷新プロジェクトに入りました。

しかし、設計フェーズになってもデザインは確定せず...

ですが、企画の段階でほぼほぼ画像のスライダーがあることは確定していました。
(結局なくなりましたが...)

なので、使えそうなカスタムタグがありそうか調べてみたのですが、
jQueryやBootstrapを利用していたり、
拡張性がないなど、満足できるものがありませんでした。

無いなら自分で作ってしまおう!ということで、作ったので、 その際の知見?を記載していこうと思います笑

RiotImageSlider

github.com

jQuery, Bootstrapなどは利用せず、Riot.jsだけで動かすことができます。

プロダクトがIE11, Edge, Chromeに対応しているため、それらの環境でも動くようにしました。

自分の望む大きさのタグでimage-sliderタグを囲い、
表示したい画像をimage-sliderタグ内に入れるだけで動きます。

オプションを設定してあげることで、スライド速度やボタンの非表示などもできます。

汎用性の高いタグを作成することでの知見

カスタムタグはblock要素ではない

ユーザ側で大きさを指定できる方が汎用性は高いと思います。

そこで、カスタムタグの親の大きさに合わせて形が変わるように作ろうと思った時に、
最初上手くいきませんでした。

<body>
  <div style="width: 400px; height: 200px;">
    <not-block></not-block> ⇦width: 400px; height: 200px;にはなりませんでした。
  </div>
</body>

<not-block>
  <h2>not-blockタグ</h2>
  <style>
    :scope {
      width: 100%;
      height: 100%;
    }
  </style>
</not-block>

原因はカスタムタグ(not-blockタグ)がBlock要素ではなかったからです。

なので、display: block;を指定してあげると上手く動きます。

  <style>
    :scope {
      display: block;
      width: 100%;
      height: 100%;
    }
  </style>

documentから要素の検索を行わない

カスタムタグ内の要素に対してclickイベントの際の処理を追加する場合、
最初はdocument.querySelector()やdocument.getElementById()などで、
要素検索を行い、その要素にイベントが起きた際の処理を追加していました。

最初は上手く動いているように見えたのですが、
同じタグを複数表示した際に、問題が起きました。

<body>
  <custom-tag></custom-tag>
  <custom-tag>/custom-tag>
</body>

<custom-tag>
  <h2 class="custom-class">カスタムタグ</h2>
  <script>
    var tag = this
    tag.on('mount', function() {
      document.querySelector('.custom-class').addEventListener('click', tag.click)
    })
    tag.click = function() {
      console.log('click')
    }
  </script>
</custom-tagu>

一つ目の<custom-tag>をクリックした際に、
consoleには、clickが2回出てしまいます。

documentから参照をしているため、document.querySelectorは両方とも、
1つ目のcustom-tag内のcustom-classを参照してしまうのです。
(普通に考えると当たり前ですが、1つ表示を行いながら作っていた時は気づきませんでした)

riot.js.org

カスタムタグ内で既に用意しているタグなどであれば、
onclickとテンプレート変数などを利用すれば大丈夫ですが、
後述する<yield/>などを利用する場合、そうもいきません。

riot.js.org

Riot.jsの機能である、名前付き要素を利用することも考えたのですが、
mountイベント内では、まだ参照できませんでした。

結論としては、Riot.jsのリファレンス内でも使われている、
this.rootから検索するようにしました。
this.rootはカスタムタグを参照しているので、
カスタムタグ内でしか検索を行わなくなるので、自分の意図した通りの動きになりました。

<custom-tag>
  <h2 class="custom-class">カスタムタグ</h2>
  <script>
    var tag = this
    tag.on('mount', function() {
      tag.root.querySelector('.custom-class').addEventListener('click', tag.click)
    })
    tag.click = function() {
      console.log('click')
    }
  </script>
</custom-tag>

clearIntervalやcancelAnimationFrameを忘れないように

カスタムタグ内でsetIntervalやrequestAnimationFrameを利用することがあると思います。

カスタムタグをifなどで非表示した際に、カスタムタグの情報がすべて消えてくれる気がしてしまうのですが、
当たり前ですが、setIntervalなどはwindowオブジェクトのメソッドですので、
タグが消えても動いてしまいます。

思わぬバグを生まないためにも、タグが消えた際にはちゃんとクリアするようにしましょう。

<script>
  var tag = this
  tag.on('unmount', function() {
    clearInterval(tag.intervalId)
    window.cancelAnimationFrame(tag.requestAnimationId)
  })
</script>

<yield/>が便利

拡張性が高いカスタムタグを作るには、<yield/>はとても重要な機能です。

軽い説明に関しては、他に記事を書いているので、そちらの参照をお願いいたします。

kurowasi2525.hatenablog.com

最後に

デザインが確定せず、2日ほど暇な時間があったので、
自分が使いやすいと思う、スライダーを作ってみたのですが、
思ったより多くの学びがありました。

僕は、Riot.jsを始めたばかりなので、長年使った人から見ると、
何を当たり前のことを言っているんだと思われるかもしれません。

何か他に良い手法などがあれば、Twitterなどで教えていただけると幸いです。

twitter.com

Riot.jsのyield内でテンプレート変数を利用する方法

yieldとは?

カスタムタグ内で</yield>を入れることで、

タグをカスタムタグで囲うことで、中身が<yield>部分に出力されます。

index.html

<body>
  <custome>
    <p>ここに内容を記載します。</p>
  </custome>
</body>

<custome>
  <h1>タイトル</h1>
  </yield>
</custome>

上記を実行すると、以下のように部分に入ります。

<body>
  <custome>
    <h1>タイトル</h1>
    <p>ここに内容を記載します。</p>
  </custome>
</body>

テンプレート変数が使えない!?

カスタムタグ内で、他のカスタムタグを呼び出すことは、よくあると思います。

そこで、テンプレート変数を使おうとするとうまく動きません。

<custom>
  <h1>タイトル</h1>
  </yield>
</custom>

<caller>
  <custom>
    <p>{ contents }</p>
  </custom>
  <script>
    var tag = this
    this.contents = 'ここに内容を記載します。'
  </script>
</caller>

上手くいきそうな気がしますが、結果は以下の通り

<caller>
  <custom>
    <h1>タイトル</h1>
    <p></p>
  </custom>
</caller>

</yield>に入った場合、テンプレート変数の参照は、そのカスタムタグに

<custom>
  <h1>タイトル</h1>
  </yield>
  <script>
    var tag = this
    tag.contents = 'こっちだよ。'
  </script>
</custom>

<caller>
  <custom>
    <p>{ contents }</p>
  </custom>
  <script>
    var tag = this
    tag.contents = 'ここに内容を記載します。'
  </script>
</caller>

</yield>を宣言している方のJSオブジェクトを参照するみたい

<caller>
  <custom>
    <h1>タイトル</h1>
    <p>こっちだよ。</p>
  </custom>
</caller>

解決策

子要素にデータを渡してあげる

<custom>
  <h1>タイトル</h1>
  </yield>
</custom>

<caller>
  <custom contents={ contents }>
    <p>{ opts.contents }</p>
  </custom>
  <script>
    var tag = this
    tag.contents = 'ここに内容を記載します。'
  </script>
</caller>

子要素のタグにデータをセットcontents={ contents }

テンプレート変数でオプションを参照するように修正opts.contents

キャメルケースにすると参照されない

子要素のタグにデータを渡す際に、キャメルケースで記載すると上手く動かなくなります。

<custom>
  <h1>タイトル</h1>
  </yield>
</custom>

<caller>
  <custom contentsMessage={ contentsMessage }>
    <p>{ opts.contentsMessage }</p>
  </custom>
  <script>
    var tag = this
    tag.contentsMessage = 'ここに内容を記載します。'
  </script>
</caller>

ケバブケースで記載をすれば上手くいきます。

<custom>
  <h1>タイトル</h1>
  </yield>
</custom>

<caller>
  <custom contents-message={ contentsMessage }>
    <p>{ opts.contentsMessage }</p>
  </custom>
  <script>
    var tag = this
    tag.contentsMessage = 'ここに内容を記載します。'
  </script>
</caller>

contentsMessagecontents-messageに変更

最後に

最近、業務でRiot.jsを使うようになったのですが、 (今更...)

本当にシンプルなのに高機能!

Vue.jsに負けず劣らず、触っていて楽しい。

これからちょっとずつRiot.jsの記事を書いていけたらと思います笑

twitter.com

pyinstallerで躓いたところ~Pythonコードをexe化~

はじめに

友達から頂いた案件で、簡単に実行できるようにして欲しいという依頼があり、

PythonコードをPyinstallerでexe化した際に躓いたところを紹介

案件の詳しい話は以下の記事で

kurowasi2525.hatenablog.com

pyinstallerとは

Pythonで書かれたプログラムをexeファイルに変換してくれる。

当たり前?ですが、今回はWindowsの方対象です。

ANACONDAをインストール

なぜか、普通に入れたPythonだと動かなかったため、

ANACONDAを入れたほうがいいと思います。

www.anaconda.com

pyinstallerのインストール

Anaconda Promptを起動

$ pip install pyinstaller
$ cd exe化するPythonファイルがあるディレクトリ
$ pyinstaller hoge.py

successみたいのが表示されると、同じディレクトリに ・buildフォルダ ・distフォルダ ・hoge.spec が作成される

distフォルダ内にPythonファイル名と同じフォルダが作成されている。 今回の場合、hogeフォルダ

そのフォルダ内に、exeファイルがあるのでダブルクリックすれば実行されるはず

躓きポイント

Pythonコード内で他のファイルを参照していると、落ちてしまう

Pythonの標準ライブラリ?や自分で作成したPythonファイルなどは、

ちゃんと参照してくれる。

import系

今回僕は、Seleniumを使っていて、このSeleniumはpipで入れたのだが、

このSeleniumをインポートする部分で落ちてしまう。

解決策としては、インポートしたモジュールを

dist/exe化したフォルダ名内にコピーすると、うまくいく

モジュールを探す方法としては、以下のコマンドを打つことで確認することができる。

$pip show モジュール名

chromedriver, csv, jsonなど

これもimport系と同じように、dist/exe化したフォルダ名内にコピーすれば大丈夫

最後に

質問・ご意見などありましたら、Twitterにご連絡くdさい。

少しでもこの記事が良いと思って貰えたなら、Twitterのフォロー、Kyashお願いします!

twitter.com

f:id:kurowasi2525:20180616200734j:plain

Python with Seleniumで業務自動化した際のTips集

はじめに

2018年5月頃に突然、高校の友達Aから連絡が来て食事をすることになった。

Aさんはとあるお店を持つまで出世していて、業務を自動化して欲しいというお話だった。

1件目はスプレッドシートだったのでGASを書いて納品。

2件目はWebブラウザをimacrosで自動化している作業があるのだが、

そのプログラムの作成者がメンテナンスできなくなったため、対応して欲しいとのこと。

imacrosを触るのも嫌だったし、詳しく話してみて将来性を考えるとSeleniumの方が良いと判断し、

提案して、合意してもらえた。

最初はJavaで書く予定だったのですが、自分が所属している会社でPython with Seleniumの自動化テストを、

外注していたので、これをいじれる人間になれば社内価値があがるかもと思い、Pythonに変更した。(相談済み)

初めてPythonSeleniumでちゃんと納品できるものを作成したので、そこで得たTips集を残そうと思う。

Python with Seleniumを書く際のTips

※以下は成功体験から得たものなので、ベストプラクティスではなく、 間違っていることも多くあると思いますが、何卒ご容赦ください

sleepではなくWebDriverWaitを使おう

Seleniumは所詮プログラムなので、ボタンが読み込み終わる前にボタンを押してしまうことがあります。

僕の場合はラズパイ&Wifiでの納品だったので、読み込み速度は不安定です。

そこでsleepを使いたくなってしまいますが、SeleniumにはWebDriverWaitという便利なものがあります。

WebDriverWait(self.browser, 10).until(
    EC.element_to_be_clickable((By.LINK_TEXT, 'ログイン'))
).click()

詳しいことは以下のサイトを参照してください

softwaretest.jp

WebDriverWaitは一つのメソッドで最初に書き、一回だけにしよう

これもちゃんと仕様を調べてないのでわからないのですが、

一つのメソッド内に2回以上WebDriverWaitを記述すると、上手くいかない場合があります。

class Hoge:
    def do(self):
        WebDriverWait()
        WebDriverWait()

if __name__ == '__main__':
    Hoge().do()

上記の書き方だと2つ目以降が動かず、Timeoutしてしまう時があります。

ですが、別のメソッドに分けるとうまくいきます。

class Hoge:
    def done(self):
        WebDriverWait()

    def do(self):
        WebDriverWait()

if __name__ == '__main__':
    Hoge().done()
    Hoge().do()

別ウィンドウで開いてしまうものがある場合

WebDriverWait(self.browser, 10).until(
    EC.element_to_be_clickable((By.LINK_TEXT, '別ウィンドウで開く'))
).click()
self.browser.switch_to_window(self.browser.window_handles[1])

WebDriverのwindow_handlesは立ち上がってるブラウザを配列として持っている。

switch_to_windowで参照するウィンドウを変えることができる。

close()よりquit()を使おう

selenium-dockerのみの現象かもしれませんが、

自動化実行→閉じる→自動化実行→閉じる→...

のような処理を行いたい場合、close()だと自動化→閉じる→何も起きない。

という現象が起きる。quit()で困ったことはないので、基本quit()でいいんじゃないかと思う。

WebDriverには使う時に接続しよう

WebDriverへの接続をインスタンス生成時に行っていました。

class Hoge:
    __init__(self, config):
        self.browser = webdriver.Remote(
            command_executor = 'http://selenium-hub:4444/wd/hub',
            desired_capabilities = DesiredCapabilities.CHROME
        )
    def main(self):
        # schedule機能を利用
if __name__ == '__main__':
    config_json = json.load(open('config.json'))
    for k, v in config_json.imtes():
        Hoge(v).main()

上記のようなコードだったため、自動化コードを実行するタイミングではなく、

プログラムが起動したタイミングでWebDriverと接続をしていました。

僕の案件の場合は、config.jsonに100以上のプロパティがあったので、

起動時に100件以上インスタンスが作成され、接続を行い重くなってしまいました。

当たり前のことですが、実行するタイミングで接続するようにしましょう。

ブラウザはなるべく大きくした方が良い

※2018年8月12日 追記

Seleniumは、ページ内に表示されていないもの?はクリックできないときがあるみたい。

そこで解決として、JavaScriptでスクロールするという方法がある。

でも、僕の場合は設定ファイルによってクリックする場所が違う。

みたいなコードを書いている部分があったため、

設定によって、初期状態で表示されている場合もあれば、

初期状態で表示されていない場合もある。

なので、スクロールで一番下までみたいのはできない。

場所によってスクロールする場所を決めたりする方法もあるが面倒。

なので僕は、ウィドウサイズを大きくすることで解決した。 (ディスプレイの解像度?などは関係ない)

self.browser.set_window_size(1024, 2000)

自動化した際のPythonのTips

他のPythonファイルをインポートする

/hoge
 ┣ main.py
 ┗ config.py

上記のような階層の場合importするだけで簡単に使える

config.py

CONFIG = {
    'aaa' : 1,
    'bbb' : 2
}

main.py

import config

print(config.CONFIG['aaa'])

JSONファイルをインポートする

/hoge
 ┣ main.py
 ┗ config.json

上記のような構造の場合

import json

config_json = json.load(open('config.json'))

スケジュール実行する

pip install schedule

毎週月曜の21時に実行したい場合

import schedule

schedule.every().monday.at('21:00').do(実行したい関数)

最後に

初めてのPython&Seleniumのため、間違っている部分は多くあると思います。

変なところ、質問・ご意見などありましたら、Twitterにご連絡くdさい。

少しでもこの記事が良いと思って貰えたなら、Twitterのフォロー、Kyashお願いします!

twitter.com

f:id:kurowasi2525:20180616200734j:plain

IT研修でVuePress+Express+Nuxt on Dockerでシステムを作成した話

前回の投稿から1ヵ月...

入社して1ヵ月が経ち,現在はIT研修を受けている日々.

今回は,そのIT研修で作成したブログシステムについて話していく.

最終成果物はこちら

ソースコードGitHubこちら

youtu.be

この記事で話す内容

  • このシステムはどんな構成で,どういう形で動いているのか
  • これを作成するに当たり,ぶつかった大きな4つの壁
    • リアルタイム性のあるDocker環境を作成できない
    • Docker環境で立ち上げたNuxtがホスト側からアクセスできない
    • VuePressのサイドバーは自分で設定しないといけない
    • Windows特有のDockerエラー
  • このシステムを作ってみて,わかったことなど所感

課題

IT研修の課題で,2週間でブログシステムを作成するというものがあった.

条件は以下の2つ

  • 記事を閲覧する画面(ユーザ用)、記事を登録・編集・削除画面(ログイン有の管理者用)
  • HTML/CSS/JavaScriptjQuery・Node.js・MS SQLを利用する

技術構成

f:id:kurowasi2525:20180429172257p:plain

VuePress Express Nuxt
Docker press press nuxt
ポート 8080 3000 8888
機能 記事画面 API 管理画面

VuePressで記事を表示

Nuxtで管理画面を表示し,記事を登録した際に,ExpressにPostして,

ExpressがVuePressのdocsにフォルダと記事の内容が書いてあるREADME.mdを作成.

編集画面を開くと,NuxtからExpressにGetし,ExpressがVuePressのdocsフォルダから情報を取得.

という流れ.

フォルダ構成

root/
├ docs/ ←VuePress
│     ├ .vuepress/
│     │      ├ public/
│     │      └ config.js
│     ├ about/←記事
│     ├ back/  ←記事
│     └ README.md
│ 
├ nuxt/ ←Nuxt
│     ├ Nuxtのフォルダなど(省略)
│     └ Dockerfile
│ 
├ docker-compose.yml
├ Dockerfile
└ index.js ←Express

リアルタイム性のあるDocker環境を作成できない

最初は,DockerfileでホストのrootをDocker環境にCOPYコマンドでコピーを行っていた.

しかし,これだと複数の問題点がある.

  • ホスト側を修正してもDocker側には影響がないため,コンテナを立て直す必要がある

   ※これだと毎回コンテナが立ち上がる時間を待つ必要があり,結果をすぐ見ることができない.

  • Docker環境側のファイルを修正する

   ※これだとコンテナを落とすと編集したものが消えてしまう.

    解決策としてDocker環境のファイルをホスト側にコピーする.

    これはとてもナンセンス

解決策

マウント機能を使う.

docker-compose.yml内で
volums:
 - ホスト側:Docker側

Dockerコマンドの引数で指定できるみたいだが,面倒だったのでDocker Composeを導入した.

Dockerfile

FROM node:9.1.0

RUN useradd --user-group --create-home --shell /bin/false app

ENV HOME=/home/app

COPY package.json $HOME/press/
RUN chown -R app:app $HOME/*

USER app
WORKDIR $HOME/press
RUN yarn

CMD ["yarn", "start"]

docker-compose.yml

version: "3"
services:
  press:
    build: .
    ports:
      - '8080:8080'
      - '3000:3000'
    volumes:
      - .:/home/app/press
      - /home/app/press/node_modules
    container_name: 'press'

Node.jsでの開発の主流?はこういう流れっぽい(適当)

  1. ホスト側のpackage.jsonをDocker環境へコピー
  2. Docker環境内でpackage.jsonに記述されているモジュールをインストール (npm install)
  3. Docker環境下にホストの環境をマウント
  4. node_modulesがホスト側に作成されないように,volumesでDocker環境内に閉じ込める

Docker環境で立ち上げたNuxtがホスト側からアクセスできない

Docker環境でNuxtを立ち上げると,ちゃんと127.0.0.1:3000で立ち上がったと出てくる.

しかし,ホストで立ち上げたようにブラウザにlocalhost:3000にアクセスしても表示されない...

解決策

127.0.0.1は自分のIPを指してる(詳しくは調べてね)ので,Dockerfileに0.0.0.0を設定してあげる.

実際のNuxt用のDockerfile

FROM node:9.1.0

RUN useradd --user-group --create-home --shell /bin/false app

ENV HOME=/home/app

COPY package.json $HOME/nuxt/
RUN chown -R app:app $HOME/*

USER app
WORKDIR $HOME/nuxt
RUN yarn

ENV HOST 0.0.0.0

CMD ["yarn", "dev"]

※追記 2018年5月3日

Nuxt on Docker for Windowsの場合,ホットリロードがうまくいかない.

なので,nuxt.config.jsに記述してあげる必要がある.

  watchers: {
    webpack: {
      poll: true
    }
  }

VuePressのサイドバーは自分で設定しないといけない

VuePressは,サイドバーに表示する内容を,root/docs/.vuepress/config.jsに書き込まないといけない.

でも,今回は管理者画面からフォルダとREADME.mdを作成するというものになっているため,

サイドバーに表示する内容は自動で行う必要がある.

解決策

サイドバーの情報をArray型の変数にする.

その変数に,ディレクトリ情報を取得して,名前を入れていく形

config.js

const fs = require('fs');
const path = require('path');

var dirpath = "./docs"
var dirs = fs.readdirSync(dirpath).filter((f) => {
  return fs.existsSync(dirpath + "/" + f) && fs.statSync(dirpath + "/" + f).isDirectory()
})

let sidebarItems = ((dirs) => {
  let items = ['/']
  dirs.forEach((d, index) => {
    if (d !== '.vuepress' && d !== 'admin') {
      items.push('/' + d + '/')
    }
  })
  return items
})(dirs)

module.exports = {
  title: 'Death March',
  description: '社内ドキュメントを管理',
  themeConfig: {
    sidebar: sidebarItems,
    nav: [
      {
        text: 'about',
        link: '/about/',
      },
      {
        text: 'admin',
        link: 'http://localhost:8888'
      }
    ],
  }
}

Windows特有のDockerエラー

エラー

ERROR: Service 'press' failed to build: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)

解決策

  1. タスクバー?のDockerアイコンを右クリック
  2. Settings...をクリック
  3. Networkタブに移動
  4. DNS ServerをFixedにチェック
  5. 8.8.8.8に設定

他にも,何のエラーが出るのか忘れたが,DaemonタブのExperimental featuresにチェックが入っていると,

これもWindows特有の悪さをするみたいなので,チェックを外しておこう.

所感

Dockerについて

2週間という期間があったが,1週間はDockerの勉強,残りの5日はDocker環境の構築での躓きに費やす形だった.

そのおかげで,Dockerに関する知識は相当付いたと思う.

これからDockerを勉強する人におすすめすることは,とにかく触ること.

Dockerの入門書やサイトを見ても,あまり得られるものはない.

とにかく自分が作りたい環境に近いものを作っているサイトを見つけ,Dockerfileをコピペし,

動く環境を作り,Dockerfileをコメントアウトなどして,動きを確かめる方がいいと,今回の作成を通した感じた.

一度Dockerfileを作成してしまえば,どんな環境でもすぐにコマンド一つで立ち上げることができ,

ホスト環境でやっていた開発環境と全く同じように開発できるので,本当に便利だと感じた.

VuePressについて

vuepressをインストールして,docsディレクトリにREADME.mdファイルを作成するだけで,

それっぽいドキュメントサイトが作れる.

本当に手軽で感動するので,一度は触って欲しい(2時間もあれば,誰でも完全に理解した状態)

英語なのは残念だが,とりあえずVuePressの公式サイトのガイド部分をコピペしまくれば,ある程度動きがわかる.

それでもわからない部分はVuePressの公式サイト自体のソースコードGitHubに上がっているので読む.

これである程度のことはできるようになる.

後,細かいことをしたければConfig ReferenceとDefault Theme Configを頑張って読もう.

Vue.js, Nuxt

ソースコードを見てもらうとわかるが,(root/index.html)

最初は,Vue.jsをトランスパイルせずに使っていた(初めてのこと)

そこから,Nuxtへ移行をしたのだが,とてもすんなり行き感動した.

Vue.jsのProgressive Frameworkの一部分を感じた気がした.

これはとても面白い体験で,未だにjQueryが使われているプロジェクトがあり,

それを今風に変えていきたいなら,Vue.jsをお勧めする.

jQuery→Vue.js→単一ファイルコンポーネント

の順にリファクタリングしていくのが,いいのではないだろうか.

まとめ

今回の課題は,2週間でブログシステムを作成するというものだったが,新しい知識を利用したり,

なぜか,新卒なのに開発フローの改善を提案するために,Slackの運用ルール作成したり,新開発フローを作成したり,

課題とは違うことにも,たくさんの時間を使うことになってしまったが,無事完成させることができた.

新しい知識に時間を使いすぎて,会社の人に怒られてしまったが,今回の課題はDockerfileを使う側から作る側にもなれるようになったし,

VuePressという、まだ情報が少ないものを使うこともできたし,とてもエンジニアらしい感じのことができて満足でした.

どんどん新しい知識を入れていきたいと思います.

このシステムはさくらのDockerホスティングのArukasか,さくらのVPS(いつものやつ)に近いうちにあげたいと思ってます.

質問・ご意見などありましたら,Twitterにご連絡ください.

今回のソースコードはこちらにあります.(jQueryなどを使用しているのは,僕の意志ではありません...)

(僕が自由に最新技術を追うことを許してくれる会社さんはいないのだろうか...)

もしよろしければ、Twitterのフォロー、Kyashお願いします!

twitter.com

f:id:kurowasi2525:20180616200734j:plain

github.com

Nuxt.js+Firebase Cloud Messaging(FCM)を使ったPWA化が簡単で衝撃的だった

※この記事は,3時間書けて作成していたが,ミスで保存できていなかったので,一から作り直したため,
やる気がなくなり雑になっています.


概要

Nuxt.jsで作ったシステムをPWA化させた.

PWA化を実装してみて,PWAの素晴らしさ・Nuxt.jsの素晴らしさが更にわかったので伝えたい!

そして,Nuxt.js+Firebase Clouds Messaging(以下,FCM)に関する記事が少なかったため,実装方法を書いていく.


背景

上記で説明したVIROというシステムは,友達に既に使って貰っている.

そこで出てきた文句として,

  • 毎回ログインすることが面倒
  • ブラウザの上下が邪魔(URLを入力する部分,戻るボタンや他のタブを表示するボタン)
  • プッシュ通知が欲しい

などなど...

スマホアプリのような機能が欲しいというのが基本的だった.

どう対応するか悩んでいたところ,小学校の同級生がPWAについて推していたことを思い出した.

調べてみると簡単に実装できそうだったので,PWA化した.


PWAとは?

Progressive Web Appsの略称で,WebサイトでスマホアプリのようなUXを実現させるというもの.

Web Applicationのその先という感じですかね.

PWAの代表的な例としては,

  • オフライン対応
  • ホーム画面に追加
  • プッシュ通知

などがある.


PWAの利点は?

自分が実装してみての利点は,WebAppを作成するだけで,ある程度アプリ層までも作れるということ.

Web技術でネイティブアプリのようなものを作る技術は前からあったと思う.(Monacaなど)

でも,学習コスト・実装コストは高く,SwiftやKotlinを勉強した方がよくねって思ってしまう.

しかし,PWA化は学習コスト・実装コストが低いためおすすめである.

そして,使ってもらう側の一番の利点は,ダウンロード不要ということが一番だと思う.


※PWAの利点や詳しい内容は以下のサイト見てください.
qiita.com


Nuxt.jsとは?

Vue.jsの環境をいい感じにやってくれて,SSRが簡単にできる.

この記事を見ている方は,Nuxt.jsに詳しい方ばかりだと思うので省略.


オフライン対応・ホーム画面に追加

Nuxt.jsの場合,

PWAモジュールの追加

画像の追加

設定ファイルの修正

を行うだけで,PWA化ができる(あら簡単)

Introduction · Nuxt PWA Module

詳しい実装方法は,素晴らしい記事がたくさんあるので,以下のサイトを見てください.

speakerdeck.com

techblog.scouter.co.jp

www.sho-yamane.me


DBに関しては,Firebase Realtime Databaseを利用していたため,既にオフライン対応している.


プッシュ通知対応

プッシュ通知に関しては,Nuxt.jsがOneSignal用のモジュールがあるため,上記の記事を見てもらうとわかる通り,

OneSignalを使うのが一般的みたい.

Onesignal Module · Nuxt PWA Module

今回は,Firebase Realtime Databaseを利用していることやOneSignalは色々と面倒だったので,Firebase Cloud Messagingを利用することにした.


youtu.be


FCMは,

設定ファイルを書き,

10行ほど書けば,プッシュ通知を受け取る状態にできる.

後は,サーバ側からでもフロント側からでも内容を送信すれば通知が表示されるようになる.

Firebaseの公式から,実装方法の動画があるため,お兄さんの言う通りに書いていけば実装できる.

youtu.be


Firebaseへのプロジェクトの追加は,この記事では省く.

メッセージを受信するようにブラウザを設定する

manifest.jsonにハードコード値を指定するのだが,Nuxt.jsではnuxt.config.jsに記述する.

manifest: {
    "gcm_sender_id": "103953800507"
}

この値は固定なので,どんな人でもこの値になる.

上記の記事で,既にPWAを実装している場合,nameやtitle,lang,theme_colorなどのプロパティが記述されているはず.


Service Workerの設定

Nuxt.jsの場合,staticディレクトリにfirebase-messaging-sw.jsを作成する.

FCMでは,Service Workerのファイル名が決まっているみたい.(Service Workerをちゃんと理解できてない)

まず始めに,コンソールから送信者IDを取得する.

コンソール→プロジェクトの設定→クラウドメッセージングタブ

f:id:kurowasi2525:20180313023029p:plain

f:id:kurowasi2525:20180313023032j:plain

firebase-messaging-sw.js

importScripts('https://www.gstatic.com/firebasejs/4.0.0/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/4.0.0/firebase-messaging.js')

const config = {
    apiKey: "",
    messagingSenderId: "コンソールから取得した送信者ID"
}

if (!firebase.apps.length) {
    const firebaseApp = firebase.initializeApp(config)
}

const messaging = firebase.messaging()


プッシュ通知の受信を実装

今回は,トップ画面で利用しているコンポーネントやpagesで実装した.

僕はcreated()内に実装.

const messaging = firebase.messaging()
messaging.requestPermission()
    .then(() => {
        console.log('Have permission')
        return messaging.getToken() //ユーザにプッシュ通知を表示する権限の許可を表示
    ]).then((currentToken) => {
        if (currentToken) {
            // プッシュ通知を受信し,表示できる状態
        }
    }).catch((err) => {
        console.log('Error Occurred.')
    })


これで実装完了.

後は通知を送るのみ.


プッシュ通知の送信

今回は,Firebaseのみで完結させたかったため,フロント側でデータベースのお知らせ情報が追加された場合,

自分に向けてプッシュ通知を送るという実装にした.

import firebase from 'firebase'

export default {
    created () {
        firebase.database().ref('notifications/' + uid).on('value', (snapshot) => { //この部分はFirebase Realtime Database部分
                const messaging = firebase.messaging()
                messaging.requestPermission()
                    .then(() => {
                        console.log('Have permission')
                        return messaging.getToken() //ユーザにプッシュ通知を表示する権限の許可を表示
                    }).then((currentToken) => {
                        if (currentToken) {
                            // この部分までは上記で実装しました.
                            // この部分はデータベースのお知らせ情報が追加などの修正がおきると呼ばれる
                            // 今回はこの部分に送信部分を記述していく
                        }
                    }).catch((err) => {
                        console.log('Error Occurred.')
                    })
        }
    }
}

プッシュ通知を送る場合,

プッシュ通知を送るためのURLに

受信者のトークンID

通知内容

送信者のサーバーキー(送信者IDの場所と同じ.コンソール→プロジェクトの設定→クラウドメッセージングタブ)

をPOST送信することで送信される.

if (currentToken) {
    let argObj = { // 受信者のトークンIDと通知内容
        to: currentToken,
        notification: {
            body: 'メッセージ内容'
            title: 'タイトル',
            click_action: 'クリックした際に開くURL',
            icon: 'アイコン'
        }
    }
    let optionObj = { //送信者のサーバーキー
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'サーバーキー'
        }
    }
    axis.post('https://fcm.googleapis.com/fcm/send', argObj, optionObj)
}


これで送信部分も実装できたと思う.


所感

今回の実装を通して,

Nuxt.jsやべー!

Firebaseすげー!

PWA最高!!!

ってなりました笑

オフライン対応やホーム画面に追加などは,Nuxt.jsの場合数分で実装できます.

本当にやらなきゃ損ってレベル.

Android端末を持っていないため,実装した時はさほど感動がなかったのですが,

友達のAndroid端末で利用した際に驚きを隠せませんでした.

ホーム画面に追加をすると,ホーム画面に追加されるわけではなく,

Androidアプリをダウンロードすると表示される,アプリ一覧の部分に追加されます.

アプリを消す際もアンインストールと表示され,起動時にはスプラッシュ画面が表示されます.

しかも全画面で表示されるので,見た感じは普通のアプリと変わらず,僕はWebAppだけじゃなく,アプリも作ったんだという錯覚と謎の感動・達成感がありました笑

Firebaseも数行で色々できて本当にすごいです.

通知が来た時の感動はやばかった...


結論

PWAに未来を感じた


※小言
あー

iOS11.3まだかなー

Android端末欲しいなー


もしよろしければ、Twitterのフォロー、Kyashお願いします!

twitter.com

f:id:kurowasi2525:20180616200734j:plain

npm startで複数のNode.jsのアプリケーションを立ち上げたい【Firebaseのバックアップ編】

結論

  • concurrentlyを利用する
  • -prefixを使う

例)package.json

"scripts": {
    "start": "concurrently \"npm -prefix ./1つ目のアプリのディレクトリ/ start\" \"npm -prefix ./2つ目のアプリのディレクトリ/ start\""
}

わからないことがあれば,Twitterに連絡ください.
twitter.com


経緯

Firebase Realtime Databaseを利用したシステムを最近作りました.
kurowasi2525.hatenablog.com

そこで,データベースの内容をバックアップ取りたいなーと思ったのですが,Blazeプラン(有料)じゃないと使えないみたい...
自動バックアップ  |  Firebase

無料で簡単な方法でやりたいと思い,調べてみました.

方法1

まず,初めにこちらの記事を発見.

www.sss3.co.jp

ちょっとやってみたのですが,自分の環境ではうまくいかなかったため,断念

方法2

次に,こちらを発見.

サーバーに Firebase Admin SDK を追加する  |  Firebase

この方法が一番簡単そうなので,これで行くことに決定.

let fs = require('fs')

let admin = require('firebase-admin')
let serviceAccount = require('秘密鍵')

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "https://プロジェクトID.firebaseio.com"
})

setInterval(viroBackup, 5000)

function viroBackup () {
  let db = admin.database()
  db.ref('/').once('value', (snapshot) => {
    let now = new Date()
    let dirName = now.getFullYear() + '-' + (Number(now.getMonth()) + 1) + '-' + now.getDate()
    let fileName = now.getHours() + '_' + now.getMinutes() + '_' + now.getSeconds() + '_bk.json';
    if (!fs.existsSync(dirName)) {
      fs.mkdirSync(dirName)
    }
    fs.writeFileSync('./' + dirName + '/' + fileName, JSON.stringify(snapshot.val(), null, 4))
  })
}

こんな感じで完成.

でも,問題点が...

これをどうやって動かすか.

解決策1

Node.jsで動かせるやつないかなーって調べたら,nowというサービスを発見.

https://codeiq.jp/magazine/2017/10/54570/

使ってみたのだが,ディレクトリを作る権限が無くて失敗.

設定で出来るのかもしれないが,調べるのが面倒なので却下.

解決策2

AWSを利用する.

EC2は,金銭面で怖いので却下(知らないものは怖い).

AWS Lambdaを利用したのだが,バックアップを上手く取得できない.

qiita.com

Lambdaは,ファイル関係は向かないみたい...


解決策3

1月に作成したシステムが動いているさくらVPSで動かす.

でも,どうやって複数のサービスを動かせばよいのだろうか...

調べてみると,こちらの記事を発見.

qiita.com

これでよさそう!

でもどうやって,ディレクトリの違うNode.jsのプロジェクトを実行させれば良いのだろうか...

こちらの記事で解決できました.

npm で package.json がカレントディレクトリにない場合 - Please Sleep