patorashのブログ

方向性はまだない

1回の熟読より適当に何度も読み返すほうがよさそう

Kindle積ん読があるにも関わらず、GWのブックオフの20%オフセールに釣られてまた本を増やしてしまった私。今回はテレビでも見たことがあった7回読み勉強法についての本を読んでみた。

東大首席弁護士が教える超速「7回読み」勉強法

東大首席弁護士が教える超速「7回読み」勉強法

著者の経歴

著者の山口真由さんは、東大を首席で卒業、東大在学中に司法試験に合格と、国家公務員試験に合格して財務省に入った後、弁護士になったという方である。

この本では、東大に入るまでの勉強法、入った後の勉強法、司法試験の勉強にどう取り組んだか、社会人になってからの勉強についてや、息抜きやモチベーションの維持などについて書かれていた。

勉強は苦痛なことでもある

「勉強が好きなんですね」とよく言われていたというが、そんなことはなくてむしろ苦痛なこともあるけれど、勉強していた結果、あとで報われる快があるということをちゃんと書いていてくれているので読み始めから好感が持てた。勉強や努力という言葉を嫌う傾向が最近の世の中にあると思うのだけれど、やりたいことの中には、往々にして多少のやりたくないことも含まれているので、そこをどう折り合いをつけていくかってのが大事で、そこを乗り越えるのに勉強や努力のパワーが要ると個人的には思っている。最初は苦痛だけれど、わかるようになったら楽しくなった、ということを表現してくれているのは、とてもいいな思った。

最初の苦痛を乗り越えるための方法論が7回読み

つまりは、苦痛を減らして数をこなしているうちに楽しくなるところまでもっていくのが7回読み勉強法である、と感じた。7回読むことを勉強のルーティーンにしてしまおう、ということだ。最初から7回読むと決めてかかって、3回くらいまでは内容がわからなくてもとりあえずどんどん読んでいくのを繰り返す。その間に、その本を読むための土台が作られていく。それ以降に詳細部分を理解していくように詰めていくという感じだ(詳細は本を読んでほしい)。

3回を超えたあたりで、多少はわかるようになってくるので楽しくなってくるんだと思う。

7回解きも有効

数学など、理論の問題は読むだけでなく、7回解いてみるのだという。これは自分が高校3年生の秋頃に知った勉強法と似ている(マジでもっと早く知りたかったやつ)。といっても私が知った勉強法は7回解くではなく、「問題集の回答を丸々書き移していくのを何度も周回する」というものだった。こんなことをして応用力がつくのか?と思われるかもしれないが、基本がわかっていないと応用もできないので、当初は疑心暗鬼ながらこれをやってみたら急激に理解力が上がった。が、丸々写していくのは時間がかかる作業で、全範囲を書き写していくのは受験に間に合わなかったので、もっと早く知りたかったのだった…。しかし、赤点を取るほど足を引っ張っていた数学が平均点以上取れるようになったときは嬉しかったのは今でも覚えている。

話が自分の過去の経験に脱線してしまったが、この体験を元に資格試験の勉強のときにも同じようなことをやったりしているので、7回解きの有効性はとても同意できる。

戦略的な勉強法

著者が一番伝えたいことは、「努力をするんなら戦略的な勉強法でいこう」ということだなと思った。闇雲に頑張る、努力する、というのは、無駄な努力になってしまう可能性が高まり、なおかつ自分が疲弊してしまうし、卑屈になる(こんなに頑張ってるのに!と)。

戦略は攻略対象によって都度考える必要があるとは思う。なにしろ、締め切りはあるのか、難易度はどうなのかなど、考慮する点がそれぞれ違うからだ。とはいえ、戦略の1つに7回読み勉強法を加えるのは有効だと思う。時間をかけて勉強することになるとなおさらだ。

反復と網羅性

7回読みのいいところは、何度も読むことで頭に定着しやすいことと、とりあえず全部読むことで全体をなんとなくでも網羅できることである。試験の場合によく「ヤマを張る」ということがあったかと思うけれど、ヤマが外れたときのダメージは相当大きいので、リスクが高い。無視していいことは教科書には載ってないので、とりあえずでも全体的に掴んでおくことは、後々に効いてくるのだという。 たしかに、予想問題集をいくらこなしても本試験だと見たこともないような問題は出てくるものだ。

一番心に響いた言葉

とても勉強になった本書であるが、最後に、一番心に響いた言葉を載せておく。

「できないのがいけないのではなく、できないままでいることがいけない」

積ん読が増えているので、頑張って消化します!

PDCAは試行錯誤のフレームワークだと認識した

自分を劇的に成長させる! PDCAノート

自分を劇的に成長させる! PDCAノート

本屋で表紙を見てなんか気になったので買って読んでみた。

読む目的

日々の業務や生活の改善は行われているものの、なんか漠然と進んでいる感を感じられないので、参考になるメソッドはないかなと思ったため。あと、ノートにToDoを書いてそれらを倒していくという方式をとっているのだが、振り返りがあまり行えていないかも…と思い、それにPDCAを取り入れたかった。

なぜPDCAが回らないのか?

これは以前に読んだ本にも書いてあったことだが、行動レベルまで落とし込めてないからだ。

patorash.hatenablog.com

本を読んだにもかかわらず、行動レベルに落とし込むのがまだ苦手である。結果がすぐに見えるものに関しては、動けるのだが。ということは、結果がすぐに見えるようにすれば、行動レベルにまで落とし込めるのではないか?という考えに至った。

ふと、OSS-DB Silverを取得したときのことを振り返ると、Study Plusを使って、参考書の進捗を管理していた。あれも勉強時間の見える化と、本を読むという作業の進捗管理なわけで、前に進んでいるというポジティブ思考になれたので、参考書を読み進めるのにはStudy Plusを使うのがよさそうだ。

大事なのは「フレーム」

学校の授業もフレームなのだが、型に嵌めると習慣化しやすい。逆にフレームがないと、何をしたらいいかわからなくて何もできずに過ごしてしまう、ということになりがちであるということが書かれていた。無意識で動けることはだいたいフレームの力。通勤通学などもそう。考えなくても動ける「いつも通り」にすることで省エネルギーになり、決断疲れを起こさなくて済むようになる。

そして、「PDCAノートは、PDCAを回すためのフレームである」と書かれている。確かにPDCAと思っても、とっかかりがないのでズルズルいくと思うので、フレーム化してあると、書くかーという気持ちになりやすいのではないか。

色々なPDCAノート

PDCAノートは

  • 毎日のPDCAノート
  • プロジェクトPDCAノート
  • 商談PDCAノート

など、色んなパターンに落とし込んで使うことができる。

当面は毎日のPDCAノートとプロジェクトPDCAノートを使ってみようかと思う(商談は自分の場合は少ないので)

PDCAは速く回せ

「振り返りの回数が多ければ多いほど、間違ったゴールに着く可能性は低くなる」という趣旨のことが書いてあった。そして、「軌道修正のコストも少なくて済む」とも。手戻りが少なくて済むというのは、やる気を持続するためにもよさそうに思える。どこかでコストが閾値を超えると、途端に面倒臭くなって、やりたくなくなることって多い。

毎日のPDCAノートっていっても、別に改善案が出ない日もあるだろうけれど、ログとして残しておくことで、後日役に立つこともある。

気づき

タイトル通りなのだけれど、PDCAは試行錯誤のフレームワークであるという認識が浅かった。もっとカジュアルに「計画して実行して振り返って改善!」でいいのだけれど、計画をちゃんとしないとと思い過ぎていたのかもしれない。

あと、読んでいる途中で「これってアジャイルだよな」と思った。1日というイテレーションを回して振り返って改善するわけで、アジャイルPDCAなんやなという当たり前のことに気づいた。

本には、PDCAノートを続けるためのメソッドや、GTDとの組み合わせなども紹介されていたり、人生のビジョンの考え方についても言及されており、子供との接し方や仕事のやり方などのほうは特に「わかる〜」という気持ちになった。

とりあえずToDoを書いていくのは、PDCAノートに変えてみることにする。

RailsアプリにreCAPTCHAを導入した

担当アプリのお問い合わせページが中国からのスパム投稿を行われるようになってしまったので、急遽IP制限を追加して一時的に凌ぐことにしたのですが、完全な対策ではないのでreCAPTCHAを導入することにしました。

reCAPTCHAとは?

reCAPTCHAは、Googleが提供している「人間かロボットか」を判断する仕組みです。最近よく見かけるようになったと思いますが、「私はロボットではありません」というチェックボックスにチェックを入れるだけでいいというやつです。

developers.google.com

人間かロボットかの判断をGoogle側に依存するようになるのですが、まぁ問題ないかと思います。

Railsアプリでの導入について

RailsアプリでのreCAPTCHAの導入は簡単です。recaptchaというgemがあります。

github.com

reCAPTCHAのキーの発行

まずは以下のリンクからGet reCAPTCHAボタンを押して、reCAPTCHAのキーを取得します。

www.google.com

次に、

  1. ラベルにわかりやすいものを設定します。(ドメイン名、何のページか等)
  2. reCAPTCHAのタイプを選択します。今回はreCAPTCHGA v2。
  3. ドメインを設定します。サブドメインも設定することと、開発環境で試す場合はlocalhost127.0.0.1なども登録しておいたほうがいいでしょう。
  4. 利用規約に同意して登録します。

f:id:patorash:20180403163946p:plain

キーが発行されるので、それをRailsアプリに登録していきます。

Railsアプリでの設定

recaptchaのサイトに導入の仕方は書いてありますが、一応書きます。

gem recaptchaのインストール

Gemfileに以下を追加します。

gem 'recaptcha', require: "recaptcha/rails"

そして、bundle installを実行します。

環境変数の設定

環境変数にreCAPTCHAで発行されたキー類を登録しておきます。 開発環境でdotenvを使っている場合は、以下のようにしましょう。

RECAPTCHA_SITE_KEY = '発行されたサイトキーをここに設定'
RECAPTCHA_SECRET_KEY = '発行されたシークレットキーをここに設定'
ViewにreCAPTCHAを表示する

あとは、Viewのformタグの内側にreCAPTCHA用のタグを出力します。人として認証されたらJavaScriptのコールバック関数を呼ぶことができますので、それが成立したら送信ボタンを有効にする、なども可能です。コードはslimで書いていますのであしからず。

= form_for @foo do |f|
  # 他のタグ
  = recaptcha_tags callback: 'successRecaptchaVerified'
  # 他のタグ
Controller側でreCAPTCHAのチェック

最後に、Controllerでチェックします。

def create
  @foo = Foo.new(foo_params)
  if verify_recaptcha(model: @foo) && @foo.save
    redirect_to @foo
  else
    render 'new'
  end
end

これで完了です。が、みなさんテストを書いていますよね?その場合は追加が必要です。

テストでreCAPTCHAの認証をパスする

gem recaptchaはテストの際はverify_recaptchaの結果を常にtrueを返すようになっていますので、引数に環境を渡す必要があります。env: Rails.envを追加してください。

def create
  @foo = Foo.new(foo_params)
  if verify_recaptcha(model: @foo, env: Rails.env) && @foo.save
    redirect_to @foo
  else
    render 'new'
  end
end

これでOKです。

まとめ

reCAPTCHAの導入は比較的簡単で助かりました。ただ、ローカルで何度も実験しようとすると、すぐにロボット扱いされてしまって困りました。ブラウザを変えるなどして凌いだのですが、いい方法はないものか…。どなたかご存知であれば教えてください。

webpackerでfont-awesome5を使ってみた。

Rails5.1.5で趣味アプリを作っていってるんですが、css frameworkはbootstrap4を使っているのですが、bootstrap4からはどうもアイコンフォントは別で準備されることになったっぽいので、安定のfont-awesome5を使うかーと思ったので入れてみました。

それにしても未だにNode.jsをちゃんと勉強したことがないのでrequireとimportとかがごっちゃになって参ります。まぁ自分のせいなんですが。

font-awesomeは5でどうなったのか?

Get Started | Font Awesome

Get startのページを確認したところ、JS版とCSS版とAdvanced Optionsと色々あります。本家のオススメはJS版のようです。Advanced Optionsは、npmでのインストールするパターンとか、ReactやVueで使うパターンのやつみたいです。

CSS版は、古いブラウザに対応するときはこちら、みたいにやや煽った表現になってますね…。

なんでJS版のほうがいいかは、CSSだと環境によってはフォントの読み込みのパスで悩むことが多かったと思うのですが、JS版だとsvgに変換して出力するので、そういう悩みから解放されるのとファイルサイズが小さくなるとか(ちゃんと調べてないけど)。あと、自分はやったことないのですが、Node.jsを使ったサーバサイドレンダリングが可能になるからとか。

今回はyarnでインストールして使ってみたかったので、そうしました。

font-awesomeをインストール

今回は無料版のみをインストールしました。 yarnでfont-awesome系を色々入れていきます。

yarn add @fortawesome/fontawesome @fortawesome/fontawesome-free-solid @fortawesome/fontawesome-free-regular @fortawesome/fontawesome-free-brands

webpackerで使う

webpacker経由で使うには、app/javascript/packs/application.jsにて読み込むようにします。

import fontawesome from '@fortawesome/fontawesome'
import faSolid from '@fortawesome/fontawesome-free-solid'
import faRegular from '@fortawesome/fontawesome-free-regular'
import faBrands from '@fortawesome/fontawesome-free-brands'

fontawesome.library.add(faSolid, faRegular, faBrands)

fontawesome.dom.i2svg()

yarnでインストールするときには、ググったやつをコピペして入れたので全然気づかなかったのですが、jsファイルにimportを書くときに、@fontawesome/fontawesomeと書いていたのですが、railsを起動してアクセスしてみたら読み込めないって怒られてしまって結構悩んでいたのですが、@fortawesome/fontawesomeなんですね。え、nじゃなくてr!?まじで???と思って本家のサイトを参照しにいったのですが、たしかに@fortawesome/fontawesomeと書いてありました。

fontawesome.dom.i2svg()メソッドで、iタグにつけたクラス名からsvgに変換してアイコンを表示してくれますので、我々はいつも通り<i class="fa fa-user"></i>みたいに使っていればいいようです。

一応、サンプルのhtml(erb)。

<h1><i class="fa fa-user"></i><%= user.name %></h1>

結果はこちら。

f:id:patorash:20180309015959p:plain

出ました!

turbolinksも併せて使う

turbolinksで使う場合は、turbolinks:loadイベントで呼び出せばいいかと思います。

import fontawesome from '@fortawesome/fontawesome'
import faSolid from '@fortawesome/fontawesome-free-solid'
import faRegular from '@fortawesome/fontawesome-free-regular'
import faBrands from '@fortawesome/fontawesome-free-brands'

fontawesome.library.add(faSolid, faRegular, faBrands)

document.addEventListener('turbolinks:load', () => {
  fontawesome.dom.i2svg()
})

Herokuのpg:backupsのデータがリストアできない現象が発生した

同僚から、Herokuのpg:backupsの定期バックアップファイルをpg_restoreでリストアしようとしたら、

pg_restore: [archiver] unsupported version (1.13) in file header

でコケるという話がチャットワークで流れてきたので調査してみました。

pg_dumpでできるデータが変わったらしい

ググると、stackoverflowにすでに上がってました。

stackoverflow.com

どうも、PostgreSQLのバージョンが上がってセキュリティパッチが当たったらしいです。 リストアするには、pg_restoreをしようとした環境のpostgresql-clientを最新に更新しろ、ということのようです(現時点では、10.3, 9.6.8, 9.5.12, 9.4.17, 9.3.22が各バージョンでの最新)。

PosgreSQLのバージョンは最新ではないのだが?

Herokuで使っているPostgreSQLのバージョンはまだ最新のものにしていないのだけれど、どうしてこういうことが起きるのか?と思ったのですが、これはどうもHeroku側の定期バックアップを実行するpostgresql-clientのバージョンが最新になっている、ということのようでした。pg_dumpの結果はpostgresql-clientのバージョンに依存するので、冷静に考えたら、今回はサーバのマイナーバージョンはほぼ関係ありません。

heroku pg:pull ならOK

試しに、postgresql-clientのバージョンが9.5.4の環境で、heroku pg:pullを行ってみました。

$ dropdb project_development
$ heroku pg:pull DATABASE project_development --app project

この場合、ローカルのpostgresql-clientによってpg_dump, pg_restoreが実行されるので、問題なくリストアされました。

heroku pg:backups:download だとNG

次に、herokuの定期バックアップをダウンロードしてきて、pg_restoreを実行してみました。

$ dropdb project_development
$ createdb project_development --encoding=UTF8 --locale=C --template=template0
$ heroku pg:backups:download --app project
Getting backup from ⬢ project... done, #1129
Downloading latest.dump... ████████████████████████▏  100% 00:00 189.25MB

$ pg_restore -d project_development latest.dump
pg_restore: [archiver] unsupported version (1.13) in file header

エラーが再現しました。やはり、pg_dumpを実行したpostgresql-clientのバージョンが最新の状態で、古いpostgresql-clientでpg_restoreしたらダメのようです。

解決策

解決策は、

  1. ローカルのpostgresql-clientを最新にする
  2. Herokuの定期バックアップファイルを使わずにpg:pullする

になるかと思いますが、2は暫定的な措置にしか過ぎません。postgresql-clientのバージョンアップができるのであれば、早めにしたほうがよさそうです。

Stimulus Handbookを大体やったので所感を書く

Ruby on Railsを開発したDHHの会社のBasecampがStimulusというJSライブラリを発表したという話があり、気になっていたので試してみました。

Stimulusとは?

Stimulusはいわゆるフロントエンド系のJSライブラリなのですが、JSでゴリゴリにHTMLを操作する系ではなく、あくまでHTMLを拡張するためのものという位置づけのようです。なんといってもTurbolinksとの親和性を重視したものになっているので、Railsエンジニアでフロント系のコーディングをJS側に寄せすぎることに違和感を感じていた人たちにとってはとてもいいものなんじゃないか?という予感がします。

stimulusjs.org

Railsで使うには?

2018-03-05現在、Rails5.1.5にて新たにRailsアプリケーションを作成した場合には、rails newの段階でwebpackで利用するJSフレームワークとしてstimulusを指定することができます。

rails new project_name --webpack=stimulus --skip-action-cable --skip-coffee

webpackを使うため、ES2015で書けるから--skip-coffeeを指定しています。

これで自動的にインストールされます。

layout.html.slimを修正

デフォルトのlayout.html.slimの状態だと、stimulusで定義したcontrollerを自動で読み込まないので、headに= javascript_pack_tag 'application'を追記します。私は普段からテンプレートエンジンはslimを使っているので、HTMLに関してはslim表記にしています。

doctype html
html
  head
    title
      | ProjectName
    = csrf_meta_tags
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
    = javascript_pack_tag 'application'
  body
    = yield

これで、使えるようになりました。

Stimulus Handbookをやってみた

既存のJSライブラリと何がどう違うのか、とりあえず触ってみてから考えます。ちなみに私はReactはやったことありません。仕事ではRails + knockout.jsばっかりやってます。あとは趣味でAngularDartのチュートリアルを少々やったくらい。

stimulusjs.org

hello_controller.jsを読み解く

まず接続確認

HTMLとStimulusのControllerを紐付けるには、data-controller属性を使います。

div data-controller="hello"
  input type="text"
  button type="button"
    | Greet

とりあえず、これで紐付いています。

hello_controller.jsの中身は以下のような感じで、ファイルだけでどのControllerかを判断するみたいです。

import { Controller } from "stimulus"

export default class extends Controller {
  connect() {
    console.log("Hello, Stimulus!", this.element)
  }
}

ブラウザをリロードすると、connectメソッドが呼ばれてコンソールにログが表示されます。ちなみにconnectメソッドはStimulusのコールバック関数で、該当controllerとHTMLが接続したら自動的に呼ばれます。

DOMに紐づくイベントをcontrollerに定義する

次はボタンにイベントをdata-actionで定義します。このボタンをクリックしたらgreetメソッドが呼ばれる、というのがわかりやすいです。

div data-controller="hello"
  input type="text"
  button type="button" data-action="click->hello#greet"
    | Greet

JS側は、greetメソッドを定義します。

import { Controller } from "stimulus"

export default class extends Controller {
  greet() {
    console.log("Hello, Stimulus!", this.element)
  }
  // 省略
}

Greetボタンを押したら、コンソールにログが出るようになりました。

DOMに変数をマッピングする

次に、input要素にJSの変数をマッピングします。

div data-controller="hello"
  input type="text" data-target="hello.name"
  button type="button" data-action="click->hello#greet"
    | Greet

JS側は、targetsという配列の変数に、html側で使われる変数名を定義していきます。targetsにnameを定義すると、nameTargetというプロパティが自動的に作られるようです。nameTargetには、紐付いたDOM要素が入ってきます。以下では、greetメソッドを改変して、nameに入れた値を取得してコンソールに出力しています。

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "name" ]

  greet() {
    const element = this.nameTarget
    const name = element.value
    console.log(`Hello, ${name}!`)
  }
}

プロパティがDOM要素になるというのが、なんか意外でした。

リファクタリング(ES2015)

最後に、ES2015のgetterを使って、リファクタリングしていました。

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "name" ]

  greet() {
    console.log(`Hello, ${this.name}!`)
  }
  
  get name() {
    return this.nameTarget.value
  }
}

シンプルになりましたね。これでStimulusのHello Worldは終わりでした。

クリップボードにコピーするサンプルを読み解く

次は、テキストボックスにある文字列をクリップボードにコピーするサンプルでした。

まずは最初の状態。

div data-controller="clipboard"
  | PIN:
  input(type="text" value="1234" readonly)
  button type="button"
    | Copy to Clipboard

stimulusのControllerも。copyメソッドがあるだけ。

import { Controller } from "stimulus"

export default class extends Controller {
  copy() {
  }
}
DOMに変数とイベントをマッピング

sourceという変数にマッピングさせます。

div data-controller="clipboard"
  | PIN:
  input(data-target="clipboard.source" type="text" value="1234" readonly)
  button type="button"
    | Copy to Clipboard

Controller側

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "source" ]
  // 略
}

次に、ボタンを押したらcopyメソッドを呼ぶようにします。

div data-controller="clipboard"
  | PIN:
  input(data-target="clipboard.source" type="text" value="1234" readonly)
  button type="button" data-action="clipboard#copy"
    | Copy to Clipboard

hello_controllerのときには、data-actionclick->hello#greetとしていたのに、今回はclick->がありません!なんとdata-actionには、デフォルトのアクションが要素タイプ毎に定義されているようです。button要素なので、clickがデフォルトのアクションのため、省略できるということでした。

Controller側

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "source" ]
  
  copy() {
    this.sourceTarget.select()
    document.execCommand("copy")
  }
}

copyメソッドで、sourceに紐付いた要素の内容を選択してクリップボードにコピーしています。

copy機能をサポートしてないブラウザ対策

古いブラウザだと、JSだけではクリップボードにコピーできない場合があるため、その対策をしていました。その対策を通じて、stimulusでどういうふうに見た目をコントロールするかが理解できます。

まず、ボタンに.clipboard-buttonをつけます。

button.clipboard-button type="button" data-action="clipboard#copy"
  | Copy to Clipboard

そして、cssを定義します。クリップボードへのコピーをサポートしている場合は表示されるけれど、そうでない場合は見えないようにするというのがクラス名からわかりますね。

.clipboard-button {
  display: none;
}

.clipboard--supported .clipboard-button {
  display: initial;
}

そして、JS側。connectメソッドで、クリップボードへのコピーをサポートしているかチェックしてます。していたら、.clipboard--supportedをdata-controller="clipboard"と書いたdivに追加しています。結果的に、クリップボードにコピーボタンが表示されますね。

  connect() {
    if (document.queryCommandSupported("copy")) {
      this.element.classList.add("clipboard--supported")
    }
  }

見た目のコントロールcssのクラス名の付与・削除で行い、JSで直に表示・非表示を切り替えるというアプローチではなさそうです。

状態管理のサンプルを読み解く

次はスライドショーのサンプルでした。スライドショーと言っても、🐵🙈🙉🙊というお猿の絵文字をポチポチと切り替えていくというものでした。

まずはHTML。

div data-controller="slideshow"
  button data-action="slideshow#previous" ←
  button data-action="slideshow#next" →

  div data-target="slideshow.slide" class="slide" 🐵
  div data-target="slideshow.slide" class="slide" 🙈
  div data-target="slideshow.slide" class="slide" 🙉
  div data-target="slideshow.slide" class="slide" 🙊

次にcss。現在のスライド以外は非表示にしてますね。

.slide {
  display: none;
}

.slide.slide--current {
  display: block;
}

次にslideshow_controller.jsです。

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "slide" ]

  initialize() {
    this.showSlide(0)
  }

  next() {
    this.showSlide(this.index + 1)
  }

  previous() {
    this.showSlide(this.index - 1)
  }

  showSlide(index) {
    this.index = index
    this.slideTargets.forEach((el, i) => {
      el.classList.toggle("slide--current", index == i)
    })
  }
}

HTML側で、data-target="slideshow.slide"が複数書かれていました。これはどうなるのか?というと、stimulusの中では、this.slideTargetsというふうに複数系になります!最初コードを見たときはtarget被っとるじゃないか!と思ったのですが、問題ありませんでした。

ここで、また新たにinitializeメソッドが出てきました。initializeメソッドも、Stimulusのコールバック関数です。コールバック関数は、

  • initialize(1回のみ呼ばれる)
  • connect(ViewとControllerが接続される度に呼ばれる)
  • disconnect(ViewとControllerが切断される度に呼ばれる)

の3種類があるそうです。Turbolinksで画面が切り替わるとdisconnectとなりますが、戻ってくるとconnectが呼ばれる、という感じなのでしょう(試してない)。このコールバック関数を使って、connectで例えばsetIntervalとかのタイマーを使う処理を呼んだ場合、disconnectでそういうタイマー処理を破棄する、というふうに使うそうです。

ここまで見たら、「もうこのコントローラー完成してるじゃないか。どこをいじるんだ?」となるんですが、ここからStimulusのData APIを触っていくことになります。

Stimulus Data API

スライドショーだったら、ページの途中からリンクされることもあるでしょう、そうでしょう。ということで、初期ページ設定をしていきます。

div data-controller="slideshow" data-slideshow-index="0"

ここで定義した初期ページを、initializeメソッドで設定します。

initialize() {
  const index = parseInt(this.element.getAttribute("data-slideshow-index"))
  this.showSlide(index)
}

とまぁ、これが通常の属性の取得方法なのですが、StimulusのData APIではそこが簡単になっています。

initialize() {
  const index = parseInt(this.data.get("index"))
  this.showSlide(index)
}

Data APIはget, set, hasメソッドがありますから、あまり困ることはなさそうです。

そして、状態の管理ですが、このData APIを使います。どういうことかというと、DOMの属性に状態を持たせておいて、JSの変数では持たないということです。

最終的なslideshow_contoller.jsは以下のようになります。

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "slide" ]

  initialize() {
    this.showCurrentSlide()
  }

  next() {
    this.index++
  }

  previous() {
    this.index--
  }

  showCurrentSlide() {
    this.slideTargets.forEach((el, i) => {
      el.classList.toggle("slide--current", this.index == i)
    })
  }

  get index() {
    return parseInt(this.data.get("index"))
  }

  set index(value) {
    this.data.set("index", value)
    this.showCurrentSlide()
  }
}

indexのgetter, setterで、Data APIを使って取得、代入をしています。HTMLの属性側で状態を管理しています。そのため、turbolinksで画面が切り替わってから戻ってきても、問題ないということでしょう。 状態の管理をJSではなくHTML側に持たせるというアプローチは、jQueryで書いてた頃には多少やってたことがありましたが、それは別にturbolinks云々というよりはデータの置き場がそこしかないからという気持ちでやっていました。しかしStimulusでのアプローチだと、そのほうがいいですね。getter,setterを活かしていくのがよさそうです。

感想

以上を読んだ感じでは、JSで小難しいことをせずにRailsの恩恵を活用しようというという感じですね。当初はループどうすんねんと思ってたのですが、this.slideTargetsのように、data-target="slideshow.slide"を何度も出せばいいわけなので、RailsのView側でのループでこれを使えばいい訳ですし。knockout.jsでもturbolinksとの相性問題があって、現在でも自分の担当プロダクトではturbolinks化できてなかったりします。Rails的なアプローチでは、高速化はturbolinksに乗っかるのが開発コスト的に少なくて済むので、Stimulusは結構魅力的です。

最後のサンプルについてここに書いていませんが、一応読みました。外部リソースを使う話で、fetchでHTMLを取得してcontrollerが紐付いたdivに入れるというものでした。まぁこれまでのサンプルのコールバックの実践的な使い方という感じでした。興味のある人は読んでみましょう。

開発環境でAWS S3の代替としてminioを使う

ほんの最近まで、開発環境でもAWS S3を使っていたのですが、minioっていうのがあるよと同僚に教えてもらっていたので、とりあえずissueに積んだまま数ヶ月が過ぎていました。そしてちょうどこの間、間隙を縫って作業できそうな時間ができたので、minioに移行しました。

minioとは?

minioはAWS S3とAPIの互換性のあるオープンソースのストレージサーバーです。

github.com

minioのメリット

minioの利点としては、対象ディレクトリに同名のファイルがあれば、わざわざRailsアプリを介してDBに登録処理を行わなくてもよいというところです。自分の環境でDBのバックアップデータを作り、minioのディレクトリをごっそり他の開発者に渡してパスを通したら、DBとファイルの紐付けがすでに終わっている状態になるのでGood。AWS S3だと、そんなことをしたら自分以外の人がファイルを削除したら自分のところにはDBにデータ残ってるのにS3からはファイルが消えてる、みたいになるので、個別にバケットを作らないといけなかったため、アップロード処理も大変だしすごく面倒でした。

minioのデメリット

デメリットはほとんどないと思いますが、完全にS3のAPIと互換性があるわけではない、というのが注意点です。後述しますが、これで結構ハマりました。

dockerで導入

minioはDockerHubでイメージが公開されているため、それを使いました。

https://hub.docker.com/r/minio/minio/

docker-composeに含める

自分の環境では、PostgreSQL, Redis, Elasticsearchはdocker-composeで起動するようにしているので、minioもdocker-compose.ymlの中にいれるようにしました。minioの部分だけ抽出して書きます。

version: "3"
services:
  minio:
    image: minio/minio:latest
    ports:
      - "9000:9000"
    command: [server, /data]
    volumes:
      - ~/docker/minio/data:/data
    environment:
      MINIO_ACCESS_KEY: access_key
      MINIO_SECRET_KEY: access_secret
    restart: always

あとは、docker-compose up -dで起動するとOKです。

Railsのpaperclipで使う

paperclipで使う方法は、paperclipのwikiで公開されています。

Paperclip with minio · thoughtbot/paperclip Wiki · GitHub

一応、私のしている設定を書いておきます。

ENV['AWS_ACCESS_KEY']に、minioのアクセスキーを、ENV['AWS_SECRET_ACCESS_KEY']に、minioのシークレットキーを設定します。リージョンとバケット名は自分のところでは環境変数にしていますが(dotenvを使ってる)、固定ならば固定でもいいと思います。

app/config/environments/development.rb

config.paperclip_defaults = {
    storage: :s3,
    s3_protocol: :http,
    s3_host_name: 'localhost:9000',
    s3_credentials: {
        access_key_id: ENV['AWS_ACCESS_KEY'],
        secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
    },
    s3_region: ENV['AWS_REGION'],
    bucket: ENV['S3_BACKET_NAME'],
    s3_options: {
        endpoint: 'http://localhost:9000', # for aws-sdk
        force_path_style: true # for aws-sdk (required for minio)
    },
    s3_permissions: :private,
    url: ':s3_path_url',
    path: ':class/:attachment/:id/:style/:filename'
}

肝な部分は、エンドポイントをminioに向けている点や、protocolをhttpにしている点などですね。

ハマったポイント

ハマったポイントですが、S3とminioはpermissionに関しては完全に互換性があるわけではないです。paperclipでは、あくまで上記の設定はデフォルトなので、Model側で設定すれば上書きできるんですね。それで、一部のファイルのpermissionをpublic-readにしていたのですが、読めませんでした。デフォルトの設定(ここではprivate)が勝ってしまうのです。

今度は逆にデフォルトの設定をpublic-readにしてみたところ、元々public-readにしていたファイルは読めるようになりましたし、privateにしていたものは、expiring_urlメソッドで有効期限付きURLを発行してみたのですが、有効期限が過ぎるとちゃんと見えなくなったのですが、URLのクエリを取り除くと普通に見えてしまいました。これはアカン。

解決策

このままだとS3とminioでpermissionに整合性が取れない状態になるので、

  1. s3_permissionはデフォルトでprivateに設定
  2. 今までpublic-readに設定していたファイルもprivateに変更
  3. 全てのファイルにアクセスする場合にはexpiring_urlメソッドで有効期限付きURLを発行する

という方針に切り替えました。

これで、検証環境で試したところ、うまくいったので、OKとします。