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

https://blog.toru-takagi.dev/profile/

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

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