Riot.js製の画像スライダーカスタムタグを作成した話とその際の知見
背景
僕は、とあるプロダクトの開発メンバーで、
1月からの画面の刷新プロジェクトに入りました。
しかし、設計フェーズになってもデザインは確定せず...
ですが、企画の段階でほぼほぼ画像のスライダーがあることは確定していました。
(結局なくなりましたが...)
なので、使えそうなカスタムタグがありそうか調べてみたのですが、
jQueryやBootstrapを利用していたり、
拡張性がないなど、満足できるものがありませんでした。
無いなら自分で作ってしまおう!ということで、作ったので、 その際の知見?を記載していこうと思います笑
RiotImageSlider
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つ表示を行いながら作っていた時は気づきませんでした)
カスタムタグ内で既に用意しているタグなどであれば、
onclickとテンプレート変数などを利用すれば大丈夫ですが、
後述する<yield/>
などを利用する場合、そうもいきません。
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/>
はとても重要な機能です。
軽い説明に関しては、他に記事を書いているので、そちらの参照をお願いいたします。
最後に
デザインが確定せず、2日ほど暇な時間があったので、
自分が使いやすいと思う、スライダーを作ってみたのですが、
思ったより多くの学びがありました。
僕は、Riot.jsを始めたばかりなので、長年使った人から見ると、
何を当たり前のことを言っているんだと思われるかもしれません。
何か他に良い手法などがあれば、Twitterなどで教えていただけると幸いです。
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>
contentsMessage
をcontents-message
に変更
最後に
最近、業務でRiot.jsを使うようになったのですが、 (今更...)
本当にシンプルなのに高機能!
Vue.jsに負けず劣らず、触っていて楽しい。
これからちょっとずつRiot.jsの記事を書いていけたらと思います笑
Python with Seleniumで業務自動化した際のTips集
はじめに
2018年5月頃に突然、高校の友達Aから連絡が来て食事をすることになった。
Aさんはとあるお店を持つまで出世していて、業務を自動化して欲しいというお話だった。
1件目はスプレッドシートだったのでGASを書いて納品。
2件目はWebブラウザをimacrosで自動化している作業があるのだが、
そのプログラムの作成者がメンテナンスできなくなったため、対応して欲しいとのこと。
imacrosを触るのも嫌だったし、詳しく話してみて将来性を考えるとSeleniumの方が良いと判断し、
提案して、合意してもらえた。
最初はJavaで書く予定だったのですが、自分が所属している会社でPython with Seleniumの自動化テストを、
外注していたので、これをいじれる人間になれば社内価値があがるかもと思い、Pythonに変更した。(相談済み)
初めてPython、Seleniumでちゃんと納品できるものを作成したので、そこで得たTips集を残そうと思う。
友達からSeleninmでWeb操作の自動化案件を頂いた(元々はimacrosだったのを無理やり変えた)
— 高木徹 (@TTrpbm) 2018年5月28日
貧弱なPCで並列処理をしなければならないのだが、ブラウザと言語は何が良いのだろう
Chrome+ Pythonが一番環境整えやすくて軽そうなイメージ
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()
詳しいことは以下のサイトを参照してください
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でスクロールするという方法がある。
anker linkもこちらの方法で解決できた
— 高木徹 (@TTrpbm) August 12, 2018
seleniumにてButtonがクリックできない時の対処法 on @Qiita https://t.co/KQ0UQBYVhf
でも、僕の場合は設定ファイルによってクリックする場所が違う。
みたいなコードを書いている部分があったため、
設定によって、初期状態で表示されている場合もあれば、
初期状態で表示されていない場合もある。
なので、スクロールで一番下までみたいのはできない。
場所によってスクロールする場所を決めたりする方法もあるが面倒。
なので僕は、ウィドウサイズを大きくすることで解決した。 (ディスプレイの解像度?などは関係ない)
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お願いします!