ボクココ

サービス開発を成功させるまでの歩み

Heroku ログと Webhook を使った応用例

ども、@kimihom です。

f:id:cevid_cpp:20180906210024j:plain

先日の Heroku Meetup で、Heroku ログ処理について語ってきた。今回はそれについて語らせてもらう。

speakerdeck.com

Heroku ログ処理の便利さ

実際に自前でサーバー運用していると、当然複数のサーバーがあるわけで、ログはそれら全部のサーバーに吐かれているログを一括管理できる形にしないといけない。ログ収集専用のソフトウェアを各サーバーにインストールしたり、それぞれ定期的に自動で取ってきてどっかに保存するような処理を書いている方もいることだろう。それのインストールや学習、セットアップにどれだけ時間を使わなければならないのだろうか。最終的にやりたいことは、ログを一括で管理して、対象のログが出た時に通知したり、特定のコードを実行させたいといったゴールのはずである。

Heroku の Addon である Papertrail 等を使えば、これらの処理をインストール(heroku addons:add)し、Web 上のインターフェスで簡単に設定するだけで実現できる。手間の排除と、管理が不要になることのメリットは Heroku を利用することの魅力の一つである。

せっかく Heroku を利用するのなら、こうした既に実現された便利なテクノロジーを有効活用すべきだ。自前でサーバー構築して自由自在にできるようになるのと比べて、すぐに使ってすぐに活用できる Heroku のメリットを存分に享受したいところである。

AWS Lambda 上で処理するコード

Heroku じゃなくて Node.js のお話になるけど、補足として以下に記しておく。

本資料に書かれている Node.js のソースコードは、以下のような形となっている。

exports.handler = (event, context, callback) => {
  let body = event.body;
  // Papertrail 通知から JSON 以外の文字列を削除
  let log = JSON.parse(decodeURIComponent(body.slice(8, body.length)));
  
  log.events.forEach(evt) => {
    // Papertrail 側での文字対応
    let json = JSON.parse(evt.message.replace(/^[^{]*|¥+$/g, ""));

    // Enjoy!
  }
}

ログにマッチして Papertrail から送られてくる Webhook のデータは1行のみとは限らない。例えば、1分間にマッチするログが5件くることもある。そのため、Papertrail からくる Webhook データは配列形式で送られてくることになる。

このサンプルでは、シンプルに forEach を使って回している。このサンプルコードの forEach 内で非同期処理を実装してしまった時点で、AWS Lambda の終了条件である callback を呼ぶことは不可能になる点に注意してほしい。あくまで上記サンプルは、forEach 内で同期処理だけをした場合のサンプルコード である。でもログ処理において、Node.js で非同期じゃない処理だけで終わるなんてケースはほとんどないよね。

てことでもし実践的にやるなら、Promiseasync/awaitAsync などを使う必要がある。Node.js 経験者なら当たり前の話なんだけど、フロントエンドでくらいしか JavaScript を触ったことのない方は確実にはまるので気をつけよう。

例えば、以下のような記事が今回のケースに似ている。 async/awaitを、Array.prototype.forEachで使う際の注意点、という話

上記の記事を読めばわかるとは思うが、forEach での非同期処理ってだけでも Promise や Asnyc/Await だと複雑で読みづらい感じになってしまう。んで、私自身 AWS Lambda x Node.js を使った経験から、ぶっちゃけ今でも Async ライブラリは便利だと思っている。Async ライブラリを使えば、waterfall ってのをつかって配列の関数を定義するだけで実現できてしまう。実際には慣れの問題かもしれないが、コードの読みやすさ的に私は Async が気に入っている。

async.waterfall([
  (next) => {
     // some codes..
     // 第一引数はエラーオブジェクト。以降は次の関数に渡す任意の引数
     next(null, arg1, arg2); 
  }, (arg1, arg2, next) {
    // some codes..
    next();
  }
], (err) => {
  console.log("done!");
  callback(err);
})

終わりに

今回の Meetup では、Heroku を使った新しい活用方法の話だったり、そもそも Heroku の何がいいのかについての話もあった。引き続き、Heroku Meetup は Heroku を使っている方やこれから使おうとする方にとって有益な情報共有の場にしていきたいと思っている。

それでは、また次の Heroku Meetup で。 ciao !

Chrome WebRTC でスクリーンシェアする方法まとめ

ども、@kimihom です。

f:id:cevid_cpp:20180218143002p:plain

WebRTC なサービス開発をしていると、デスクトップのスクリーンシェアをしたくなる時がある。その方法について一括でまとめてみる。

2018/8/28 追記

Chrome Inline Install が廃止されるらしい。そのため、以下のインラインインストールは実装できなくなるのでご注意を。

Google Developers Japan: Chrome 拡張機能の透明性向上について

スクリーンシェア概要

まずスクリーンシェアをするには、専用の Chrome 拡張 をマーケットに公開し、ユーザーがその Chrome 拡張をダウンロードしてもらう必要がある。その Chrome 拡張 で作成された Track を最終的に WebRTC に乗っける形となる。

Chrome 拡張のインストールを手軽にしてくれる、Chrome Extension inline installation ってのがある。本記事は、こちらのリンク先の情報がベースとなっているので、今回のスクリーンシェアの実装のイメージなどは上記ページの説明を参照いただきたい。以下に理想のスクリーンシェアの方法の全体像を記述していく。

Chrome 拡張の用意

Chrome 拡張機能は、Chrome ウェブストアからダウンロードできるアプリのようなものだ。読者の方も一度はこのページで何かしらの拡張をダウンロードしたことがあることだろう。

まずはここに Chrome 拡張を乗っけるための準備が必要となる。

Chrome 拡張のプログラムを用意

今回の機能を Chrome 拡張として実装するには、 chrome.js, content.js, manifest.json の3つのファイルを用意する必要がある。最終的にこれらファイルを zip化してアップロードする形だ。 まずは manifest.json を定義しよう。

{
  "manifest_version": 2,
  "name": "screen sharing",
  "short_name": "screen sharing",
  "description": "Screen sharing app",
  "version": "0.0.1",
  "background": {
    "scripts": ["chrome.js"]
  },
  "content_scripts": [
    {
      "matches": ["*://www.my-awesome-app.com/*"],
      "js": ["content.js"],
      "run_at": "document_start"
    }
  ],
  "externally_connectable": {
    "matches": ["*://www.my-awesome-app.com/*"]
  },
  "permissions": [
    "desktopCapture",
    "tabs",
    "https://www.my-awesome-app.com/*"
  ]
}

詳しい JSON の定義については Background PagesContent Scripts などのページを参照いただきたい。必要に応じて適宜値を変えてほしい。

さて、続いて Background Pages で定義した chrome.js のソース例をご紹介する。

chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
  switch (message && message.type) {
    // Our web app sent us a "getUserScreen" request.
    case 'getUserScreen':
      handleGetUserScreenRequest(message.sources, sender.tab, sendResponse);
      break;
    // Respond with the version number
    case "checkExtension":
        var version = chrome.runtime.getManifest().version;
        sendResponse({ version: version });
        break;
    // Our web app sent us a request we don't recognize.
    default:
      handleUnrecognizedRequest(sendResponse);
      break;
  }
  return true;
});

function handleGetUserScreenRequest(sources, tab, sendResponse) {
  chrome.desktopCapture.chooseDesktopMedia(sources, tab, streamId => {
    // The user canceled our request.
    if (!streamId) {
      sendResponse({ type: 'error', message: 'Failed to get stream ID' });
    }
    // The user accepted our request.
    sendResponse({ type: 'success', streamId: streamId });
  });
}

function handleUnrecognizedRequest(sendResponse) {
  sendResponse({type: 'error', message: 'Unrecognized request'});
}

// obtain target tabs to inject content script
chrome.tabs.query({
  "status": "complete",
  "currentWindow": true
}, function(tabs) {
  tabs.forEach(function(tab){
    // inject script assuming that content script is 'content.js'
    chrome.tabs.executeScript(tab.id, {
      "file": "content.js",
      "runAt": "document_start"
    });
  });
});

上記コードで getUserScreencheckExtension メソッドを、通常の Web ページの JavaScript から呼び出すことができるようになる。

content.js はとてもシンプルで以下のような形となる。

window.postMessage({ type: 'ContentScriptInjected' }, '*');

chrome.js の最後のブロックで実行している chrome.tabs.query は、Chrome 拡張のインストール完了後にスクリーンシェアを直接起動するために必要なコードだ。ここでcontent.js が呼び出されて、ContentScriptInjectedイベントを発火し、フロントエンドでスクリーンシェアインストール後のイベントを受け取ることができるようになる。chrome.js から直接フロントエンドへ通知を送りたいところだが、それだとうまく動かないとのことのようだ。

上記をひとまず Zip 化して保存しておこう。

Chrome 拡張の公開

まず この Chrome 拡張が所有するウェブサイトの公式アイテムであることを証明するために、Search Console にウェブサイト登録する必要がある。Search Console でウェブサイト登録を行おう。ここから吐かれた aaa.txt ファイルをhttps://www.my-awesome-app.com/aaa.txt からアクセスできるようにする必要がある。

続いて デベロッパーダッシュボードでアプリ登録を行う必要がある。これで公開するには確か $10 くらいのお金がかかるので注意。先ほど作成した zip をアップロードし、"ウェブサイト" の項目を埋め、インライン インストール: このアイテムではインライン インストールを使用する。 にチェックを入れる。公開設定は限定公開で問題ない。

Chrome 拡張を公開すると、 https://chrome.google.com/webstore/detail/my-awesome-app/nolkcoggbpmcgplklppcfkfgljmkmeba って感じの URL が割り当てられる。この最後の nolkcoggbpmcgplklppcfkfgljmkmeba が今後使う Chrome 拡張の ID となる。この公開の適用は、10分くらいかかるので気長に待とう。

無事公開できたら、ようやく Web ページ側の フロントエンドのコーディングに移っていく。

フロントエンドのコーディング

さて、ここも一気にソースコードを紹介する形で進めていく。

まずは HTML のヘッダに以下を追記。

<link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/nolkcoggbpmcgplklppcfkfgljmkmeba">

続いて JavaScript。

// ページ起動時に呼ぶ
chrome.runtime.sendMessage("nolkcoggbpmcgplklppcfkfgljmkmeba", { type: 'checkExtension', sources: ['window', 'screen', 'tab'] }, function(response) {
  if (response && response.version) {
    // Chrome 拡張が有効であるフラグを立てる
  }
});

// HTML 上の Screen share ボタンが click された時に呼ぶ
if ("Chrome 拡張が有効であるフラグ == true") {
  enableScreen();
} else {
  // install chrome app
  chrome.webstore.install("", function(){
    // set listener called in chrome extension
    window.addEventListener('message', function(ev) {
      // executed in extension/content.js
      if (ev.data.type === "ContentScriptInjected") {
        enableScreen();
      }
    }, false);
  }, function(err, errCode) {
    console.log(err);
  });
}

function enableScreen() {
  getUserScreen(['window', 'screen', 'tab'], "nolkcoggbpmcgplklppcfkfgljmkmeba").then(function(stream) {
    // スクリーンシェアの Stream が得られる。
    // この stream を WebRTC の Track に乗せる
  }, function(err) {
    console.error(err);
  });
}

function getUserScreen(sources, extensionId) {
  return new Promise(function(resolve, reject) {
    chrome.runtime.sendMessage(extensionId, { type: 'getUserScreen', sources: sources }, function(response) {
      switch (response && response.type) {
        case 'success':
          resolve(response.streamId);
          break;
        case 'error':
          console.error(response);
          break;
        default:
          reject(new Error('Unknown response'));
          break;
      }
    });
  }).then(function(streamId) {
    return navigator.mediaDevices.getUserMedia({
      video: {
        mandatory: {
          chromeMediaSource: 'desktop',
          chromeMediaSourceId: streamId
        }
      }
    });
  });
}

上記コードの概要は以下のようなものだ。

  • ページに来た時点で専用のスクリーンシェア Chrome 拡張がインストールされているかをチェック
  • インストールされていなくて、かつスクリーンシェアのボタンをクリックした場合、スクリーンシェア拡張 のダウンロードをしてからスクリーンシェアを実行
  • インストールされていればそのままスクリーンシェアを実行

最終的にスクリーンシェア用の Stream さえ取ってこれれば、あとは WebRTC に乗っけるだけなので、それぞれの方法に応じて実装してほしい。

おわりに

今回は理想的なスクリーンシェアを Chrome の WebRTC で実現するために必要な流れを解説した。最近は WebRTC の記事ばかり書いてるけど、こうして WebRTC に挑戦する方が増えていくことを願っている。

今回の方法は 本当に Chrome 限定のコードなので、 Firefox アドオンとかにも適用したいってなると面倒な if 文でなんとかしないといけなくなる。

スクリーンシェアがアプリとしてインストールする必要があること自体、面倒なことなので、ここら辺は今後の HTML の発展に期待したいところでもある。

Happy WebRTC Coding!

rails-ujs と form_with の使い方

ども、@kimihom です。

Rails 5.0 までは jquery-rails を使ってフォームやリンクの Ajax 通信を可能にしていたけど、Rails 5.1 からは rails-ujs として切り出され、晴れて jQuery からの脱却を可能にした。

そこで、本記事ではこの rails-ujs と関連深い form_with の使い方や注意点についてまとめる。

rails-ujs

rails-ujs は、Ajax の送受信の "送" の部分を JavaScript で実装せずに、よしなにやってくれるライブラリだ。私たちは Rails 5.1 から導入された form_with を使ってフォームを構築し、その form でデータを送った後のレスポンスのハンドリングだけを JavaScript で書けば良くなる。

rails-ujs を使わない場合は、フォーム送信ボタンを押した時のイベントをハンドリングし、各フォームの値を詰めて Ajax で送るみたいな実装が必要になる。それらの実装を全部 rails-ujs と form_with が肩代わりしてくれると考えるといいだろう。実際は form_with だけでなく、View の button_to や link_to などに remote: true をつけるだけで Ajax 化することもできる。

rails-ujs の魅力てのは、やはり Rails のレールに乗ることができることだ。お分かりの通り、フォームの実装はフロントエンドの鬼門だ。各種項目のバリデーションや 各項目のI18n、デフォルト値の設定、値の送信など必要なことがたくさんある。これら実装を React や Angular で実装してもいいだろうけど、それらを使う場合には先の実装を全部 フロントエンドフレームワーク側に寄せる必要が出てきてしまう。しかし、実際には Rails 側のデータベースのモデルと関連づけることで、フォームの実装はより手軽に、より密で効果的に実装が可能である。

rails-ujs のインストール

さて、そんな素敵な rails-ujs を早速インストールしよう。Rails 5.1 ではデフォルトで rails-ujs が入っているので、require するだけだ。

$ cat app/assets/javascripts/application.js

//= require rails-ujs

これで読み込みが完了する。

form_with

Rails 5.1 から導入された form_with であるが、今までの form_for と同じように考えると手痛い思いをすることがあるので気をつけよう。

form_with のデフォルトは remote: true の状態である

form_with の場合、何もオプションを指定しなくても Ajax 送信となる。そのため、form_with でフォームを作って submit した時に何も動作しない?ってなることだろう。一般的なページ遷移するフォーム送信をしたい場合には local: true を明示する必要がある。

form_with で構築した form や text_field には id などが付与されない

これは大きな変化なので注意が必要だ。例えば以前のバージョンで form_for を使って構築していた場合、勝手に form や text_field にはモデルの属性名などに応じて id が割り振られていた。この id をベースに JavaScript 側で処理を実装していた方もいることだろう(私もそうだった)。

しかし、form_with で構築した form や text_field には id や class などが自動で生成されない。そのため、明示的に id: "name" のように割り当てる必要がある。てことで、各フォーム項目に明示的に id や class を割り当てたあと、JavaScript 側で処理しているコードがある場合は修正する必要がある。

rails-ujs の使い方

さて、ここからが rails-ujs の出番だ。フォームを送った後のイベントを定義してあげよう。そのためには、送り元の <form><a> タグから発火されるイベント ajax:success を定義してあげれば良い。

Controller と View, そして JavaScript の関連を知る必要があるので、以下にまとめて紹介する。本来なら jQuery 使う必要がないけども、今までの差分って意味で jQuery で実装した場合でコードを記す。

class UsersController < ApplicationController

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_param)
    if @user.save
      render json: {result: "ok", user: @user}
    else
      render json: {result: "ng", msg: @user.errors.messages}
    end
  end

  private
  def user_param
    params.require(:user).permit(:name)
  end
end
<%= form_with model: @user, url: users_path, id: "user-create-form" do |f| %>
  <%= f.text_field :name %>
  <%= f.submit %>
<% end %>
$(document).on("ajax:success", "#user-create-form", function(e) {
  console.log(e.detail[0]);
});
$(document).on("ajax:error", "#user-create-form", function(e) {
  console.log(e.detail[2]);
});

JavaScript 側で Ajax を送る必要がなく、終わった後の UI の変更だけをすればいいのでフロント側で書かなければならないコードが減っているのがおわかりいただけたことだろう。 rails-ujs からの変更点として何よりも大事なのが、ajax:success 時の処理だ。コールバック関数の引数は1つにまとめられ、Ajax 送信した後のレスポンスは、e.detail[0] に格納されている。この保存場所はちょっとわかりづらい気もするけど、rails-ujs ではそういう実装になっているので従うしかないだろう。ajax:error 時には detail[2] に status や statusText が格納されている。ここら辺は console.log で出力しながら確認するといい。

ちなみに、フォーム送信前に何かしらチェック処理を挟んでエラーの場合は送信キャンセルをしたいこともあるだろう。その場合は、ajax:beforeSend ではなく、ajax:before 内で return false; をしてあげると実装できる。ajax:beforeSend もあるんだけど、送る直前に呼ばれるコールバックなので return false しても Ajax 通信が走ってしまう。

JavaScript コードから form を明示的に送信したい場合

今までの話は、ユーザーが submit を HTML 上でクリックなり Enter してくれた場合に動くというシンプルなケースだった。しかし、実際には JavaScript 側でごにょごにょしてから JavaScript 側で form 送信をしたいことが出てくるはずだ。

その時に、何も考えずに JavaScript 側から submit() を呼ぶと、Ajax 送信ではなく普通のフォーム送信となってしまう 問題を見つけた。ページ遷移してしまうのである。これに対して JavaScript 側からの submit() を Ajax で送信したい場合にどうすればいいかググったが全く出てこないので、仕方なくソースコードを読んだ。

rails-ujs には Rails.fire というメソッドが提供されており、ここで submit イベントを発火させることで対象フォームを JavaScript からリモート送信することが可能になるようだ。

Rails.fire($("#user-create-form")[0], "submit");

rails_ujs の考え方

今回の例は 純粋にレスポンスを json で返して JavaScript 側でその後の UI 処理を任せる方法だったけど、もっと簡単な方法として create メソッドのレスポンスを json ではなく html.erb を返してあげて、JavaScript 側で一括でレスポンスの HTML へ書き換える方法がある。この方法はかなりおすすめで、ちゃんとやれば JavaScript 側で書かなければならないコードを激減させることができる。

Ajax 終わったあとちょっとしたメッセージ出したい程度だったら JSON、がっつり HTML を書き換えたい場合には HTML を返すって思っておくと、今後の実装が楽になることだろう。

HTML を Ajax のレスポンスとして返した場合は、e.detail[2].response に HTML が入っている。

終わりに

Rails 5.1 から導入された form_with だが、以前の form_for と同じ感じで使うとつまづく点が出てくるので気をつけよう。私が遭遇した問題は上記で全てだが、他に問題が起きた際には yarn から落としてきた node_modules/rails-ujs/lib/assets/compiled/rails-ujs.js のソースを読むことになるだろう。そこまでコード量は多くないので、ググって詰まるより実際にソースを読むことをお勧めする。

そんなわけで快適なレールに乗った Rails プログラミングをこれからも楽しんでいこう!

Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法 (Web Engineer's Books)

Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法 (Web Engineer's Books)

  • 作者: 太田智彬,寺下翔太,手塚亮,宗像亜由美,株式会社リクルートテクノロジーズ
  • 出版社/メーカー: 翔泳社
  • 発売日: 2018/01/24
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

Rails 5.1 のフロントエンド周りの所感

ども、@kimihom です。

常に話題に上がってくる Rails のフロントエンド事情だけども、今回 Rails5.1 を色々みた中で自分が感じたことについて書いていく。予め断っておくと、自分もまだそこまでフロントエンドをマスターしている身ではないので間違った考え方の部分もあるかと思う。その場合はぜひコメントなどいただけると幸いだ。

Rails Guide は今でも rails-ujs

jquery-ujs が rails-ujs に変わったなどで、脱 jQuery を果たした Rails5.1。この時点で他のフロントエンドフレームワークに移ることを検討した方も多いかもしれない。

そもそも、jquery-ujs って Rails において結構大事な役割を果たしていると思うので軽く説明すると、要は <%= form_for @article, remote: true %> なフォームで Submit した時に、 Ajax でリクエストを送って対象 Form のイベントに ajax:success などの形式でコールバックが呼ばれる仕組みだ。これが今までは jQuery 依存だったけど、その依存が取っ払われた感じになる。Rails 5.1 では form_with ってのが登場した。

この rails-ujs についてよくよく考えてみると、その他のフロントエンドフレームワークを使おうと思った時点で、rails-ujs のような仕組みは不要になっちゃう気がする。なぜなら、その他のフロントエンドフレームワークでも Form を作る機能を兼ねそろえていて、そっちを使っていくことになるはずだからだ。特にモデルを フロントエンド側で管理しているなら、完全に form_with などは機能が重複してしまうので rails-ujs 自体要らない子になる訳だ。

でも Rails Guide には今でも rails-ujs を使ったサンプルがあるし、残り続けている。それはつまり モデルのデータバインディングの仕組みのない何か を使うことが今でもベースになっているように思う。これはとても理にかなっている。なぜならサーバー側でフォームを生成すれば、フォーム生成は form_with を使って生成できて、I18n は Rails 側で管理された言語ファイルから読み出すことができ、バリデーションやエラーメッセージの表示など、全て Rails の ActionView 側に一任することができるからである。Rails を使っているのに、これらの処理をフロントエンドフレームワーク側でやるってのは機能の使い方として重複しており、それやるんだったら ActionView 自体の機能をごっそり削ぎ落とした RailsAPI 使ったほうがよくね、となるのは当然のことだ。だから Rails API が Rails5.1 から標準で使えるようになったのだろう。

link_toform_withremote: true な Ajax 通信を行い、フロントエンドではレスポンス時の UI 更新だけをする。こうすればフロントエンド側の責務はシンプルになる。Gmail や Facebook 並みのガリガリのリッチな Web アプリケーション作りたいってなら話は別だけど、たいていの Web アプリケーションは "検索して詳細見てフォーム投稿" といったオーソドックスな仕組みだろう。それをわざわざフロントエンドフレームワークに適用するのは、やはり Too Much だと考える。ちょっとしたページ遷移を素早くしたいってなら Turbolinks を使えばうまく History API をラップしてくれるし、Rails はあくまでもそうした用途向きに作られているのだと以下の動画で教えてくれる。

RailsConf 2016 - Turbolinks 5: I Can’t Believe It’s Not Native! by Sam Stephenson - YouTube

f:id:cevid_cpp:20171027225345p:plain

そう、あなたは Google でもないし Facebook でもないのだ。 個人的には Turbolinks は使わないで History API を自前で作ってるのだけど、基本的な Rails のフロントエンドの考え方について上記の Rails Conf での動画は参考になる。

で、お前は Rails 5.1 のフロントエンドでどうするの

まだ模索中である。少なくともバインディングがあるような Too Much な機能は必要とせず、素の JavaScript が簡素化されたくらいのものでちょうどいいと考えている。素の JavaScript で書くのが面倒だったら、もしかしたら jQuery 3.0 の採用もありうる。

そんで ActionView をちゃんと使って form_with でフォームを作成する。Ajax 通信時に断片的な HTML もしくは JS を返してあげて、jQuery でいう $().html()$().append() などで動的に HTML を変えていくシンプルな実装にしたい。部分的なところだけをアップデートするみたいなことをするのは正直複雑になるだけだと思ってて、だったらページごと body.html() で変えたり、大枠の div だけをごっそりと変えるような Turbolinks 的な方法で問題ないように思う。こうすることで、ちゃんと Rails のレールに乗って開発効率を高めて無駄な時間を省き、良いプロダクトを作ることに専念できるはずだ。

もちろん Gmail 並みのガリガリのプロダクトを自分が作るってなら話は別だ。でも、そもそもそういうのを作るんだったら、Rails 自体を採用するのがナンセンス だと私は思う。

終わりに

Rails 5.1 フロントエンド周りの所感についてまとめてみた。当然それぞれ考え方はあるだろうから、1つの考えくらいな感じで参考にしていただければと思う。

少なくとも私は Rails の提唱する実装方法に素直に従って開発し、Rails と共に 良いサービスを作ることに専念していきたい。そういう考えのもとで得た答えが、上記のような開発スタイルである。

今後 Rails 5.1 でちょいちょい開発していく予定なので、何か新しいことがわかったら記事にしていく予定だ。

ContentEditable のハマりどころと対処法

ども、@kimihom です。

f:id:cevid_cpp:20171008154936p:plain

前回の記事で、ContentEditable についての概要をざっと書いた。ContentEditable の持つ魔力については以下の記事を参照いただきたい。

www.bokukoko.info

さて、本記事では実際に ContentEditable を使って実装しようと思った際に気をつけておいたほうがいい点をざっと挙げていく。もし今後 ContentEditable を使った実装をする方の参考になれば幸いだ。

タグ挿入後の罠

ContentEditable を使うなら、何らかのテキスト入力でリアルタイムに HTML タグの挿入をする(document.execCommand('insertHTML'))といった実装をしていくことになるだろう。 実際上記コマンドを実行するだけなら問題なく動作するが、問題はその後にやってくる。例えば <span> タグを動的に挿入した後、その直後に日本語入力をしようとすると、数文字打った後で変換カーソルから外れるという謎バグが発生する(Chrome version 61)。また、ContentEditable 内の<span>タグのの後に何も文字入力がない状態だと、キャレット( | 点滅の文字入力カーソル)が ContentEditable 外に飛ぶというバグも発生した。

ContentEditable の実装で例に挙げた Twitter のタイムラインの ContentEditable は、タグを入力するたびに ContentEditable 内を全てを書き換えるという何とも大胆な実装を施している。 Twitter の投稿文字が140字までという制約があるからこそ成せる芸当だ。実際、Twitter のテキストエリアにタグを100個以上挿入すると、Twitter テキストエリアの動作が極端に重くなる。

本問題に対し、私は<span>タグで何かを入れた後には必ず半角スペースを入れるという実装で対応した。つまりは keydown イベントで全ての対象 <span>タグを走査し、タグ直後にスペースがなければ document.execCommand('insertText', false, " "); を実行している。意図しないスペースが入るということでユーザーにはあまりそのことを気づかないような UX にすることに苦労した。他の方法で対応できた方がいれば是非教えていただきたい!

keydown と keyup の罠

Twitter のように # 入力でタグの候補が出てくるような何かを実装したい場合、keyup でイベント補足をしたいと思うことだろう。そうすれば、#を入力した後のイベントとしてコードを書くことができるので、動的な HTML の書き換えが楽に対応できるためである。

しかし、keyup イベントで # の入力を保障しようとすると問題が発生する。なぜか keyup では #を素早く入力した場合に keyup# の入力を取ってこれないことがあるのだ! この致命的な問題に対応するには、 keydown の時点で # 入力を補足するようにするしかない。この問題により、keyup 時点での実装とは異なった実装の工夫が必要になってしまった。

このように、keydown と keyup, keypress などのイベントの違いを理解した上で、適切な実装が必要になってくる。当然素早いキー入力や記号の入力など、実際に入力されるであろう文字列を全て考慮した上で実装しなければならない。

キャレットの罠

テキストエリア入力なら当然のようにあるキャレット (| で現在のフォーカスを示すやつ) だが、ContentEditable で最も苦戦する機能実装の1つといっても過言ではないだろう。

例えば Twitter のタグ # 入力後に 選択肢が出てきて、そのうちの1つを選択した後にはタグを確定して入力カーソルをそのタグの外から始めるという実装にしたいという場合を考えてみる。この場合、キャレットは新規で作ったタグ外側に合わせるという実装をすることになる。

そこで Range ってのを使うことになる。例えば以下のようなコードだ。

      var range = document.createRange();
      range.setStartAfter(tag);
      var sel = window.getSelection();
      sel.removeAllRanges();
      sel.addRange(range);

この Range は、キャレットを調整するための色々なメソッドを用意しているので試行錯誤しながら調整をしていくことになるだろう。ここら辺の実装をしている時の、ContentEditable でドツボにハマっている感はなかなかエキサイティングなのでぜひ楽しんでいただければ幸いだ。この Range の動作を工夫することで、違和感のない心地よい UX を ContentEditable で実現できる。

ContentEditable 内の子タグで keydown等のイベントを補足できない

色々と ContentEditable の実装を試行錯誤していると、例えば ContentEditable 内の特定の <span> タグでのみ keyup イベントを補足したい みたいなアイディアが浮かんでくる。残念ながらこの方法は実質不可能である。(ContentEditable 内に ContentEditable=false タグを入れてさらにその中にある ContentEditable=true の要素の keyup イベントは取れるが、実装の解として適切ではない)

そのため、ContentEditable 自体の keyupkeydown イベントだけを使って、キー入力を最適化していくことになる。その事実を知った時の絶望感は半端なかった。子タグ内でイベント捕捉できたら実装がどんなに楽になっていたことか。つまり、やるなら Twitter のように、ContentEditable 内の対象タグを毎回チェックして実行する というようなコードを書くことになるだろう。この実装をするのには躊躇したが、検討した結果この方法しかなかった。本件に関しても、他に方法があれば是非教えていただきたい。

そして最後の砦、 IME

英語だったらこんな問題が起きないのだけど、日本語入力の場合は変換という作業がある。ご想像の通り、keydown 等のイベントは、日本語入力時にも全て取ってきてしまう。ここで何が問題になるのかというと、Enter の挙動だ。

日本語入力時の確定Enter では何も起こさず、その後の確定 Enter の際にのみ実行したいといったことが出てくる。全ブラウザでこの挙動に対応するには、結構面倒な手続きが必要だ。これはもう日本語を扱う我々にとって宿命だと思ってやるしかない。以下のリンクを参考にしていただきたい。jQuery じゃなくても参考になる。

jQueryでIME入力確定時にイベントを発行する - Qiita

当然、IME 以外にもクロスブラウザの問題は当然のように出てくる。ブラウザごとのバージョンアップなども追従する必要があるので、この実装がどんなに大変かは簡単にご想像できることだろう。

終わりに

今回は私が遭遇した ContentEditable の実装の一部だけではあるがご紹介した。

前回の記事でも書いたが、改めて ContentEditable を利用する際には、本当に今やりたい実装は ContentEditable が必要か? を考え直してみることをオススメする。以前、私はこんなツイートをしていた。

実際にContentEditable を実装してみて、Markdown の手軽さを痛感した次第である。

本当に ContentEditable が必要なタイミングで、この苦難を乗り越えた先に、最高の UX を提供できるサービスを実現できるはずだ。

拡張テキストエリアを ContentEditable で実現しよう

ども、@kimihom です。

今回は ContentEditable という知る人ぞ知る HTML5 で導入された HTML 属性についてご紹介する。

まず始めに、 大いなる力には大いなる責任が伴うということを伝えておこう。ContentEditable を本気でやろうとすると確実にどハマりし、長い開発期間を要することになる。そして本当にイメージする機能を実現できるかの保証はできない。しかし、その険しい道の行く末に、誰も経験したことのないような、拡張テキストエリアの UX を実現することができる!

ContentEditable で実装できること

まず ContentEditable の簡単な概要をご紹介する。最も一般的に ContentEditable が利用されているのは、おそらく Twitter だろう。公式 Web サイト上のテキストエリアに # の文字を入力してタグ付けをしてみよう。

f:id:cevid_cpp:20171002184710p:plain

ご覧の通り、テキストエリアなのにまず色付けが行われている。そもそも普通の <textarea> ではこれすらも実現不可能だ。また、 # を押した後にはタグの候補が表示されたり、対象のメンションをマウスオーバーするとユーザー情報が表示されたりする。

マークダウンとはなんだったのか。そんなふうに思ってしまうような拡張テキストエリアを、ContentEditable なら実現できるのである。

そして、ContentEditable の原理としては至ってシンプルだ。

<div contentEditable='true'></div>

なんとこれだけで編集可能なテキストエリアを実現できる。Twitter のテキストエリアの部分のHTMLソースを見てもらえばわかる通り、<textarea>ですらないのがポイントだ。CSS や JavaScript を駆使して、 Twitter のツイートエリアのような拡張テキストエリアを作り上げるのである。

ContentEditable を始めてみる

ContentEditable にワクワクした読者の皆様、ようこそ茨の道へ。これの実現には本当にたくさんの罠をくぐり抜ける必要がある。

そもそも、ContentEditable は よくあるブログの編集画面のような、ツールボタンをクリックして文字を太字にしたり色付けしたり画像を挿入したりといった使われ方を想定した設計になっている。これであれば、ContentEditable は容易に実現できる。

f:id:cevid_cpp:20171002185433p:plain

具体的には、 document.execCommand を呼び出すことで書式変更をする。つまり、書式変更ボタンをクリックしたら、document.execCommand('bold') といったようなコマンドを実行するだけで、書式変更のかかった文字を ContentEditable 内で入力させることができるようになる。

しかし、今回私たちが最終的に実現したいのは、Twitter のようなリアルタイムで変わるクールなインタフェースだ。これの実現は半端なく難しいので、チャレンジできる人だけ来て欲しい。

まず、 Twitter ライクな実装を実現するためには、個々のキー入力イベント "keydown" , "keyup" を補足し、その文字によって対応する処理を変えていく必要がある。「"スペース+@"の文字が入力されたら、候補となるユーザー一覧をテキストエリア下部にリスト表示する」 といった実装となる。ご想像の通り、あらゆる文字入力が考えられる中で、その全てのキー入力に対応して違和感を起こさないような実装が必要となってくる。単なる文字だけでなく、Enter, Space, 上下左右、Shift, Control とった文字まで、keydown, keyup イベントは全てを補足するので細かな実装とテストが必要だ。 一応記しておくと、keydownは文字入力される前に呼ばれるイベントで、keyup は文字入力された後に呼ばれるイベントだ。ContentEditable ではこのイベントの使い分けも非常に重要になる。

最終的に生成される文字列は、当然 HTML だ。改行なども<div>タグで表現されたり、書式は <span> で登録されたりする。そのためちゃんとしたテキストとしてデータベースに保存したいのなら、そのHTMLを解析して最終的に整形した文字列を保存しなければならない。そのまま保存して表示みたいなことをすれば、簡単にXSSの脆弱性を生み出してしまうことだろう。

また、キー入力イベントのハンドリングだけではなく、ContentEditable 内のカーソル移動も検討する余地がある。例えば自動入力でテキストエリアに文字を挿入した場合、カーソルはそのテキストエリアの前や後ろに持っていきたいなど、機能の特性によってカーソル位置を変えたいことが出てくる。そこで登場するのが、Range という概念。これも ContentEditable の実装をすると確実に必要になって来るのでドキュメントを見て学んでいただきたい。また、ExecCommand の一部が正しく動作しないとか普通に起こるので、替わりに Range を使って代用する方法など、数多くの地雷が待ち受けていることだろう。

やがてあらゆるハードルを乗り越えれば、リアルタイムに編集しながら自由に HTML を作り上げることができるという夢のような機能を実現できる。

そこまで苦労して ContentEditable を実現したいか。この点に関してはきっとサービス特性によって異なることだろう。

終わりに

私は ContentEditable の持つ魔の力を利用して、拡張テキストエリアを実現することができた。Twitter のタグを見ればわかる通り、普通ならタグ入力の別の検索ボックスのようなものを用意しなければならなかったのが、1つのテキストエリアで全てが表現できるようになった。ContentEditable はそこに価値を感じるか感じないかの世界だ。シンプルで柔軟性のあるサービスを本気で目指しているのなら、ContentEditable の実装を検討してみてはいかがだろうか。

大いなる力には大いなる責任が伴う。ContentEditable で拡張テキストエリアを実現するかしないかを検討してみていただきたい。

続編書きました。

www.bokukoko.info

Node.js Express で非同期処理を next で対応する方法

ども、@kimihom です。

今回は Node.js の Express を使った場合の非同期処理のスマートな対応方法をご紹介する。

Node.js の非同期処理の重要性

簡単な比較をすると、Ruby では非同期処理をほとんどしない代わりに、それぞれのリクエストの一連の処理がが終わるまでサーバーはそのために仕事をする。例えば、DB の読み書きの処理は ActiveRecord を使うが、そこで DB との接続をして処理が終わるまでサーバーの一つのリソース(プロセス)は占有されることになる。これはこれでシンプルなコーディングができるので良いのだけど、処理効率という観点ではあまりスマートな方法ではないと言われている。

Node.js でプログラムを書いていると、DB処理を含めたあらゆる処理が非同期となる。これによってより多くの処理を1つのサーバーで処理することが可能になる。しかし、JavaScript 特有の大量 function ネストが続くコールバック地獄が頻繁に発生する。

さて、Express ではこのコールバック地獄を防ぐために next という便利な引数が存在する。今回はこの next についてご紹介しよう。

使い方

まず Express の基本的なルーティング処理は以下のような感じだ。

router.get('/', function(req, res, next) {
  res.render('index');
});

"/" へアクセスが来たら、index の View をレンダリングせよという処理が書かれている。同じようにして、その下に router.get('/foo'... と書けば、/foo にアクセスした時の処理を書くことができる。んで、Express で書くときに意識したいのが、「リクエストが来たら、コードの上から順番にリクエスト処理がマッチするかを探しに行く」ということだ。そしてこれが next を使う上で大切となる。

早速、先ほどの index に非同期処理を噛ませてみよう。今回は JavaScript で定番の setTimeout でレスポンスを遅らせてみる。

router.get('/', function(req, res, next) {
  setTimeout(next, 3000);
  // setTimeout(function() { next(); }, 3000);  上と同じ
});

router.get('/', function(req, res, next) {
  res.render('index');
});

Express はリクエストが来たらコードが上から評価されるということを把握しておけば、"/" へアクセスが来たらまずは上のコードが実行され、next によってその次にマッチする下のコードが実行されるという流れを理解できるはずだ。

Express の app.js に 404500 の処理が最後にあるのはそのためである。最後の404の部分でマッチしてしまったら対応する定義が見つからなかったということで 404 をエラーを含めてnextに渡す。next の第一引数に エラーオブジェクトを入れておけば、他にマッチせず最終的に一番最後に定義されたエラーハンドリングの処理にマッチすることになる。

/// app.use ~~でルーティング定義

// 一番下に...
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};
  console.error(err);

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

終わりに

今回は Express を使う上で大事なリクエストが来た際のコードが実行される順番について解説した。

next の仕組みを理解することが、Express のコード実行の流れを理解することに繋がってくるので、これから Express を触ろうと思っている方は是非マスターしておいていただきたい。

Express をうまく使いこなせれば、多くの処理をハンドリングする高性能な Web アプリケーションを作ることができるので、シンプルな Web アプリケーションは Express で作ってみても良いかもしれない。

いまどきのJSプログラマーのための Node.jsとReactアプリケーション開発テクニック

いまどきのJSプログラマーのための Node.jsとReactアプリケーション開発テクニック

Heroku の Papertrail でログからコードを実行する方法

ども、@kimihom です。

今回は Heroku アドオンの Papertrail の活用方法についてご紹介。

このアドオンが単なるブラウザ上で綺麗にログを見れるだけのアドオンかと思っていたら、それは Papertrail の 3分の1の魅力しか知っていないことになる。Papertrail の本当の威力を発揮するのは、Alert 機能だ。今回は、この Alert 機能を使って、ログから得られたデータを下に任意のコードを実行する方法についてご紹介する。

概要図

今回は特定のログが吐き出された時に、任意のコードを実行するような想定を考える。以下のような構成図だ。

f:id:cevid_cpp:20170608221458p:plain

“Heroku は AWS で動いているだろ” という 細かいツッコミは置いておいていただいて、AWS Lambda と API Gateway の部分は自前の AWS で用意する必要がある。なので今回はその流れについてもご案内する。

STEP 1. JSON のログを書き出す

まずは Papertrail のイベントをキャッチできるようにするために、アプリケーションから特定のログを出力する必要がある。JSON 形式で書き出すようにすると、あとで処理が楽なのでオススメだ。一応 Ruby (Rails) でのサンプルコードを書いておく。

user = User.find_by(params[:id])
log_info = {
  tag: "PTLog",
  name: user.name,
  id: user.id,
  email: user.email
}
logger.info(log_info.to_json.to_s)

ここで任意の “タグ” を用意することが肝心だ。この値を下に Papertrail の Alert として設定するようにする。

STEP 2. Papertrail で Webhook の設定を実施

さて、特定のログをHeroku から出力したら、いよいよ Papertrail の出番だ。Papertrail アドオンをインストールした後の手順をご紹介する。

f:id:cevid_cpp:20170608222342p:plain

  1. 出力した “タグ” にマッチするログだけを出力するために、 “PTLog” と入力し、Search をクリック
  2. Save Search をクリックし、検索を保存
  3. 名前をつけて Save & Setup an Alert

そしたら今度は Alert をセットする方法について選択する項目が出てくる。 Slack や HipChat を使っている場合は、このまま重要なログであれば通知すれば済むだけの話だけど、今回はコードを実行したいので、"Webhook" を選択。

f:id:cevid_cpp:20170608222838p:plain

Frequency に関してはしょっちゅう起きるイベントなら 1min、そうでなければ 10min とかお好みで問題ない。同じログが短期間で一気に来たとしても、 Papertrail は 配列として データを送ってくれるため、心配ご無用だ。

さて、肝心の Webhook の URL が今回のキモだ。

STEP 3. AWS Lambda と API Gateway のセットアップ

最近は便利になったもので、 AWS Lambda に色々とデフォルトで用意されたテンプレートがあるのでそれを利用しよう。

  1. ランタイムの選択で Node.js 6.10 を選択
  2. microservice-http-endpoint を選択
  3. API 名を適当に作り、セキュリティをオープンで Next
  4. 関数名と名前を適当に編集
  5. コードを下のように修正
  6. ロールは無ければ作って次へ
  7. 最終確認して作成ボタンをクリック

Node.js コードのサンプルは以下のようなイメージだ。

exports.handler = (event, context, callback) => {
  console.log('Received event:', JSON.stringify(event, null, 2));

  const done = (err, res) => callback(null, {
    statusCode: err ? '400' : '200',
    body: err ? err.message : JSON.stringify(res),
    headers: {
      'Content-Type': 'application/json'
    }
  });

  let body = event.body;
  let log = JSON.parse(decodeURIComponent(body.slice(8, body.length)));
  console.log(log);

  log.events.forEach((evt) => {
    let json = JSON.parse(evt.message.slice(0, evt.message.length - 1));
    // ログで出力した JSON が処理できる!
  });

  done();
}

ここで decodeURIComponent したり slice したりしているのは Papertrail が Webhook で書き出すデータを微調整するために行っているもので、きっと誰もが共通の処理をすることになるだろう。これで晴れて Heroku のログに出力した関数を AWS Lambda 内で好きに扱うことができるようになった!

おっと、最後に Papertrail の Webhook URL を API Gateway が生成した URL にセットするのをお忘れなく。 https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/yyyyyyyy のような URL になってることかと思う。

あとは煮ても焼いても・・・。お好みに。きっと運用フェーズで例えば外部サービスの API を呼んだりすることで、データ連携や重要な通知をしたりといったことができるはずだ。

終わりに

運用フェーズではいかに効果的にデータを集積し、サービスの改善に生かせるかが課題になってくるかと思う。しかし、通常のアプリケーション内でそのような情報を外部に送るようなコードを書いてしまうと、そのぶんユーザーへのレスポンス速度が遅くなり、ユーザー体験を悪化させてしまう。そんな時はログからイベントをトリガとして発生させ、それに応じてコードを実行させるようにすることでユーザー体験を損ねることなくデータを蓄積していくことが可能だ。

最後に宣伝になるがこのような Heroku の開発/運用 Tips をシェアする Heroku Meetup があるので、よければ参加してほしい!今回は Heroku をガチで使っている方々からの運営事例と、Heroku 最新 Tips をお届けする予定だ。

herokujp.doorkeeper.jp f:id:cevid_cpp:20170608230018p:plain

WebRTC の Media, Stream, Track について

ども、@kimihom です。

最近の週末は Twilio Video を使ってビデオ通話アプリケーションを作成している。Twilio Video は今どんどん進化していて、単なる2,3 人でのビデオ通話をするにとどまらず、面白いことができるようになっている。特にスクリーンシェアの機能を Chorme 拡張機能を使うことで Twilio Video でもデスクトップ画面共有のアプリケーションが作れてしまう。ちなみにデスクトップ画面共有のドキュメントは1週間前に出たばかりなので、最新情報だ。

さて、この デスクトップ画面共有の概念を理解しようとすると、途端に WebRTC の Media, Stream, Track というそれぞれわかりづらい概念をそれなりに理解しなければならなくなる。今回は自分なりに勉強して理解したことをまとめようと思う。

Media, Stream, Track

最初に自分が理解した 図を貼り付けよう。少々見づらいかもしれないがこの点は勘弁してほしい。

f:id:cevid_cpp:20170305215612j:plain

今回は、ローカルのメディア、つまり自分のブラウザ環境だけに焦点を当てている。実際は LocalMedia での Track だけでなく、 Remote つまり相手の Track と合い重なってそれぞれのビデオ通話を可能にしている。

さて、まず全体を取り巻くのが Local Media だ。Media には複数の Stream を持つことができる。WebRTC といえばのメソッド getUserMedia で取ってこれるのは、デスクトップカメラとマイクの2つだ。 これらはそれぞれ VideoTrack, AudioTrack がある。てことで、大抵の WebRTC でのビデオ通話では、1つの Stream で 2つの Track を持った状態で相手と通信をすることになるだろう。

そしてその下、getUserScreen と書いたが、これが Chrome の画面共有などのメソッドとして仮に定義したものである。実際は Chrome 標準で作られているものではないので、Chrome 拡張機能 として getUserScreen を実装しなければならない。んで、getUserScreen を呼ぶと、もう一つの Stream が出来上がり、その中に VideoTrack が作られることになる。こんな感じで一つの Local Media に 2つの Stream が存在することも可能だ。

ここまで説明すれば、というより図を見た時点で Media, Stream, Track が理解できてのではないかと思う。ただ実際に Twilio Video のドキュメントを読んでみると、これらが複雑に絡み合ったようにそれぞれがそれぞれの参照を持つようになるので、何が何だかわからなくなる。こういう図を最初に見ておけば、混乱することもないかと思う。

Twilio Video でのサンプルコード

さて、ここまでくれば、Twilio Video のサンプルコードも理解できるだろう。 Twilio Video でのビデオの初期化はこんな感じで実装できる。

    _localMedia = new Twilio.Video.LocalMedia();
    Twilio.Video.getUserMedia().then(function(mediaStream){
      _localMedia.addStream(mediaStream);
      _localMedia.attach('#video-view');
    });

    getUserScreen(['window', 'screen', 'tab'], "your chrome app id").then(function(stream) {
      _localMedia.addStream(stream);
    }).catch(function(error) {
      console.error("Screen Capture is not instaled!");
    });
  }

2つの MediaStream を add して追加し、Media を div 要素に Attach することで その中に Video や Audio 要素が追加されるようになる。

それぞれ LocalMedia を addStream すると、先ほどの図のように Track ができあがるので、Media#event:trackAdded イベントが呼ばれる。これでそれぞれの Track を管理して、例えば停止したりミュートしたりの操作も可能になるだろう。

終わりに

今回は Twilio Video でデスクトップ共有ができるようになったので、 Media, Stream, Track の3つについて勉強し、学んだことをシェアした。もし誤りなどあれば、指摘してくれると私の勉強にもなるので大変嬉しい。

個人的に Twilio はこのビデオ分野が今アツいと思ってるので、今後も追っかけていく次第である。

Rails assets 内の JavaScript のメソッドを View 内から呼び出す方法

ども、@kimihom です。

Rails で開発していると、assets/javascripts 内で定義したメソッドを View 内に書かれた <script>タグから呼び出したい時がある。Rails は基本的に assets/javascript 内のコードを全てひとまとめにして一つのapplication.jsってのを作るから、特定のページのロード完了後に特定の JavaScript コードを呼び出したい時にちょっと困ることになる。そんな時は View 内に <script>タグを入れる必要が出てくるだろう。

View 内と Assets 内の JavaScript の混在による課題

だが、View と Assets 内に JavaScript を混在させるといろいろな問題が出てくる。そう簡単にはいかない問題だ。

外部ライブラリの読み込みの前にメソッドを呼んでしまう

jQuery のような JavaScript ライブラリは、より早く HTML を表示させるために <head>タグではなく、<body>の一番下に書くことがあると思う。そうなると、レイアウト内の yield の後に jQuery が読み込まれることになる。つまり、View 内でベタ書きした <script> タグの方がライブラリよりも先に呼ばれてしまうことになる。これはよろしくない。


app/views/layouts/application.html.erb

<html>
<head>
  <title>Sample App</title>
  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= csrf_meta_tags %>
</head>
<body>
   <%= yield %>
   <%= javascript_include_tag 'application' %>
</body>
</html>


app/views/users/index.html.erb

<div id="users">
  .....
</div>
<script>
  $("#users")....   // <- "$" が呼び出せない!!
</script>

ソースコードのカオス化

View 内に <script> タグをどんどん入れて行ってしまったり、ロジックをその中にベタ書きしてしまうと、assets/javascripts にある JavaScript ファイルとごちゃごちゃになってメンテナンス困難なソースコードが出来上がってしまう。

だからできれば assets/javascripts 内に JavaScript のコードを集約させたい。

View から Assets 内の JavaScript を呼び出せない

かといって、assets/javascripts 内で functions を定義しても、Rails の AssetPipeline による JavaScript の Minify によってメソッド名を指定することができない。

app/assets/javascripts/users.js

function setUser() { 
  ......
}


app/views/users/index.html.erb

<div id="users">
  .....
</div>
<script>
$(function() {
  setUser(); // <- "setUser" が呼び出せない!!
});
</script>

content_fortrigger によって解決しよう

さて、本記事のメインテーマである解決策を掲示しよう。まずは1点目の <script>タグの読み込み順について。これは content_for を使うことで解決できる。

app/views/layouts/application.html.erb

<html>
<head>
  <title>Sample App</title>
  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= csrf_meta_tags %>
</head>
<body>
   <%= yield %>
   <%= javascript_include_tag 'application' %>
   <%= yield :footer %>
</body>
</html>


app/views/users/index.html.erb

<div id="users">
  .....
</div>

<%= content_for :footer do %>
<script>
  $("#users")....   // <- "$" が呼び出せる!
</script>
<% end %>

これで、 jQuery が読み込まれた後に、 <script> タグを読み込ませることができる。

続いて JavaScript の Minify によって指定のメソッドが呼び出せない問題。これは trigger を定義してあげよう。

app/assets/javascripts/users.js

$(function() {
  $(document).on("users:loaded", "#users", function() {
      //.....
  });
});


app/views/users/index.html.erb

<div id="users">
  .....
</div>

<script>
<%= content_for :footer do %>
<script>
  $("#users").trigger("users:loaded");
</script>
<% end %>

trigger には引数を与えることもできるので、とても柔軟に View と Assets の JavaScript を通信することが可能だ。

Rails の content_for と jQuery の triggerによって、個別 View のロード時に特定の JavaScript を実行することができるようになった。

終わりに

trigger はいろいろと応用が利くとても便利な jQuery メソッドだ。より詳しく知りたい方は以下の記事も参照されるといいだろう。

今更ながら jQuery の trigger の魅力について語らせてもらう

Rails のレールに乗った美しいコーディングを目指していこう。

Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法 (Web Engineer's Books)

Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法 (Web Engineer's Books)

  • 作者: 太田智彬,寺下翔太,手塚亮,宗像亜由美,株式会社リクルートテクノロジーズ
  • 出版社/メーカー: 翔泳社
  • 発売日: 2018/01/24
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

独学での Web プログラミング学習まとめ

ども、@kimihom です。

自分で何かサービスを出したいという方は多いかと思う。今回はそんな方へ最低限知らなければならないリストを上げようと思う。ターゲットとしては Web アプリケーションということにしたい。

UNIX コマンド

最初は黒い画面に慣れる必要がある。いつまでも Mac の Finder でダブルクリックしてるようではプログラミングの効率が悪すぎる。UNIX に関してひとまず知らなきゃいけないのはディレクトリ/ファイル操作閲覧やエディタ(Vim or Emacs)、各種プログラミングコードの実行といった基本的なことで十分だ。次第にリダイレクトやパイプとかを知っていくと、より効率的な操作ができるようになる。最初から一気にマスターする必要はないが、シェルスクリプトとかも書けるとさらに良い。興味の度合いに応じて UNIX を学んでみよう。

これらはとても基本となる操作であり、UNIXの基礎を知らないと、これから学ぶことに対してことごとく UNIX コマンドから調べなきゃいけなくなったりと地味にきつく挫折する原因にもなりうるので最初にがっつりやってみよう。

例えば以下のような本。読んでないけどおそらく正しいこと書いてると思う。

MacOS XユーザのためのUNIX入門―ターミナルから覗くUNIXの世界

Ruby

ひとまず Web アプリケーションなら Ruby でいいだろう。なぜならあらゆるドキュメントが整備されていて、いろいろな問題にあたってもネットにたくさん情報があり、そして何より書いていて楽しい言語だからだ。複雑でぐちゃぐちゃになるような言語(何とは言わないが)を使うと、プログラミング自体がつまらないものになってしまう。考えてみてほしい。これからあなたはプログラミングをすることになるが、そのコードと四六時中付き合わないといけない。 楽しく学べる Ruby がいいと私は思う。(ここら辺はいろいろと論争を生むところだからあまり突っ込みはしないでいただければ幸いだ)

Ruby でターミナル上で動く簡単なサンプルプログラムを書けるようになろう。ruby test.rb とかを実行して色々と学んでいくのだ。Ruby と UNIX は当然のことながら深く結びついているので、UNIX を深く知れば知るほど、Ruby とのつながりを感じて感動することもある。

これに関しては 2冊くらい読んでおいたほうがいいと思う。王道がこの2冊。

たのしいRuby 第5版

プログラミングRuby 1.9 −言語編−

後者は Ruby 1.9 とちょっと古いんだけど、Ruby 2.x でも十分動くと思う。ちょっと古くてもお勧めしたい本だ。

HTTP

HTTP プロトコルはあらかじめ知っておいてたほうがいい。これを勉強しないでいると、今後の Rails の勉強の時に意味わからないことがたくさん出てくる。本当に基本だけでいいので、さらっとだけでも本を読んでおくといいだろう。

特に GET/POST などの HTTP メソッド、200, 400 などの レスポンスコード、Cookie、セッション などは知っておいたほうがいい。これはWeb標準なので今後絶対に知らないといけない知識になってくる。

HTTPの教科書

HTML/CSS/JavaScript

Web ならこの基本を押さえておかないといけない。この3つに関してはいろいろな本が出回っているから、自分にあったのを読めばいいと思う。最近は HTML5 とか CSS3とかもでてきて情報が錯乱しつつあるので、ググりながら実力をつけるってのがちょっと難しくなってきてる感じもする。

こういうのは必要な時に必要なコードが書ければいいと思うので、辞典っぽいのを買って一通りさらっと読んで気になるのがあればコード書いてみてブラウザで確認するみたいなことをすればいいんじゃないかな。

例えば好きなサイトの完全コピーを HTML/CSS/JavaScript で作ってみよう。そこで「これどうやってやるんだろう?」ってのが出てくることかと思う。そんな時は Chrome Console を開いて、HTML要素やCSSを見て理解できるようになろう。

詳解HTML&CSS&JavaScript辞典 第6版

Web開発でよく使う、特に使えるChromeデベロッパー・ツールの機能 - Build Insider

jQuery, jQuery プラグイン

今でも大活躍の jQuery。初学者は jQuery を知ることは今でも有効だと思う。 DOM 操作によってページの要素が動的に切りけるような基本を身につけることが重要だ。最初は汚いコードでも構わないので、jQuery で色々と遊べるようになるとサービス作成の幅が広がると思う。また、それにあわせて ajax も早いうちに学んでおくと、なお良い。

jQuery プラグインでさらに自分の欲しい機能を早く実現できるような仕組みも学んでみよう。DOM操作ができれば、そのjQueryプラグインをさらに自分好みにカスタマイズできるようにもなるだろう。大事なのはとにかく自分で書いて JavaScript を好きになること。それこそがさらなる勉強の意欲と成長を生むからね。

jQuery レッスンブック jQuery2.X/1.X対応

データベース

Web アプリケーションを作るなら、データベースの知識は必須だ。今でも現役バリバリのリレーショナルデータベースを理解しよう。テーブル設計を正しく行い、SQL が発行できるようにならないといけない。

これも勉強してみればパズルみたいなもので案外面白いものだ。ビビらずに軽い気持ちでチャレンジしてみてほしい。

【改訂第3版】 SQLポケットリファレンス (POCKET REFERENCE)

まずは最低限の SQL さえかければいいんじゃないかな。今後ゼロからがっつり Web アプリケーションを作るならテーブル設計も正しくできるようにならないといけない。

Git, Github

Git はソースコードを管理するためのバージョン管理システムだ。あなたが書いたコードを前の状態に戻したかったり、他のメンバーと開発したコードを組み合わせたり、そんな作業を Git はしてくれる。これも絶対に学んでおかないといけない技術になってきている。

最初のうちはそこまで詳しく知る必要はない。add, commit, branch, checkout, push, fetch, merge, log, diff くらいで最初は十分だろう。

Gitが、おもしろいほどわかる基本の使い方33

この時点で自分の Github アカウントも作っておこう。

Ruby on Rails

ついにだ。やっとここで Ruby on Rails を学習できるようになる。ここまでの基礎がないと、Rails を学んでも自分の作りたいアプリは作ることはできない。決してここまでの勉強は無駄ではないので安心してほしい。

Ruby on Rails は今までの総まとめみたいなものだ。今まで学んだ知識をうまく組み合わせてくれるのが Ruby on Rails だと思ってもいいかもしれない。ここでも何冊か本を読んでしっかりとマスターしたいところだ。

RailsによるアジャイルWebアプリケーション開発 第4版

パーフェクト Ruby on Rails

Heroku

作ったアプリを公開するときにはサーバーが必要だ。 Heroku。この神がかったサービスをぜひ使おう。あなたの書いた Ruby on Rails コードが数行のコマンドを叩くだけで世界中に公開できる。こんな素晴らしいことが他にあろうか。

ここまでくると Qiita とかでたくさん記事が出回っているので、それらを読んでデプロイしてみよう。なぁに、ここまで勉強してきたあなたなら難しいことは何もない。一番簡単な記事はこれかな。

railsアプリを5分でherokuにデプロイする - Qiita

独自ドメインとか SSL とかそういうのもみんなネット上に情報はたくさんある。問題なくできるはずだ。

終わりに

ここまで学べば、ある程度自分の力で Web アプリケーションが作れるようになるだろう。ここから先の技術は、自分でどんどん作って壁にぶち当たり、その壁を突破していくことで体得し、成長することができる。

そのぶち当たった壁はあなたのブログに書き残していってほしい。あなたの貴重な時間の記録を残すという意味で重要だ。その情報が次なるプログラマーを助けるヒントになるだろう。

本気で Web アプリケーションを作りたいあなたなら、きっとここまで勉強してくれることだろう。ぜひこのレベルにまで来てほしい。そしていつかたくさんの人に使われる Web アプリケーションをあなたの手で作り上げていただきたい。その日が来るのを私は心待ちにしている。

Cookie とログインと remember_me のカラクリ

ども、@kimihom です。

Cookie のすごい基本的なところなんだけど、どハマりしたのでメモ。何時間もかけて原因を探って解決した先に、成長ってのはあるものだな。いい経験をした。

デスクトップアプリで発覚した問題

んでどういう話なのかっていうと、運営している Web サービスで、基本的にログインしたら使い続けられる機能がある。そこで、Electron を使ってデスクトップアプリ化をしようとしていた。

f:id:cevid_cpp:20161109232915p:plain

もともとシングルページっぽいアプリケーションだったので、デスクトップアプリ自体としてうまく機能していた(ログイン周りを除いては)。なぜかデスクトップアプリを起動するたびにログインしなければならない状態になっていた。毎回デスクトップアプリを閉じるたびにログアウトされてしまっているような感じになってしまっていて、相当不便な状態だった。

普段の Web アプリケーションとデスクトップアプリとでは何かが違う。何が違うのか。

デスクトップアプリの Cookie の管理

そもそも Electron はブラウザではないので Cookie に該当するようなことは自分で用意しないといけない。Electron での Cookie は、アプリのデータとして保存されている。 Mac の場合は ~/Library/Application\ Support/myapp-170c4a/Cookies。ここの情報を読み込んで、Web サーバーに Cookie を送っている。

これ自体は全く問題ないのだけど、どうやら Electron で Webアプリを埋め込む場合は、起動の度に別のセッションとしてリクエストが発生するっていう特徴があるようだ。これが今回ハマった原因。

普段私たちが使っているブラウザは、session_idとして Cookie に保存される。Cookie を閲覧するツールを見ると確認できる。

f:id:cevid_cpp:20161109232147p:plain

さて、ここで気になるのが セッションっていうチェック。Cookie には Session Cookie と Persistent Cookie の2つがあるらしく、Session Cookie を指定しているということのようだ。これにより、セッションごとに有効な Cookie であるということを示しているらしい。

Session Cookie はセッションごとにしか存在できない Cookie なので、割とすぐに有効でなくなるとお考えかもしれない。しかし、普通にブラウザを使っている場合は、ページをタブで開き続けていたりセッションを残している場合があったりして、毎回アクセスのたびにログインといった感覚ではなくなる。だから、あまり Session Cookie について意識しなくてもいいような感じになってしまっている。

だけど Electron を使うと、これが顕著に大切になってくる。Electron を起動するたびに Session は入れ替わっているので、Session Cookie として session_idにセッションを保存しただけではダメなのである。

Remember Me の役割

ここでログイン時にたまに見る Remember Me(ログインを保持する) のチェックだ。これは、Session が入れ替わるたびにログインしなおさなければならない問題を解決してくれる。これにチェックを入れると、以下のような Cookie が新しく付与されていることに気づく。

f:id:cevid_cpp:20161109232158p:plain

今度はセッションにチェックが入っていないことが確認できる。先ほどの session_id が異なってログアウト状態になったとしても、この remember_user_token が存在して正しいものであれば session_id を発行し直す的なことをしてくれるようだ。

Electron で Web アプリを埋め込む場合は、この処理が実装されていることが絶対条件になる。

これにより、無事 Electron でもセッションを保持することが可能になった。

終わりに

Cookie と Session ってのは割と Web アプリケーションの基本ではあるが、実装を詳しく見てみると新しい発見があるものだ。今作のブラウザではセッションを割と長く持つ傾向があって、あまり恩恵を感じない ログイン状態を保持するチェック。まさか Electron で埋め込む時に問題に直面するとは思わなかった。

何はともあれ、一つまた成長ができてよかったよかった。

クラウドを活用した WebRTC コールセンターの最前線

ども、@kimihom です。

掲題のタイトルで Open Cloud Innovation Festa 2016 で発表してきた。資料は以下。補足的な資料も含めたらめちゃ多くなってしまったw

speakerdeck.com

発表の裏話的な

今回は参加者がエンジニアだけじゃなくて一般の方も来るってことだったので、ごく一般的な内容を中心に解説していった。本記事で裏話的なことをちょいちょい書いていこうと思う。

まず、私と WebRTC は実はちょっと遠い存在だ。 WebRTC メインでプロダクトを1年以上運営しているが、WebRTC コアについて深く学び始めたのは最近のことだ。なぜなら、Twilio の ClientVideo を使えば、 WebRTC の知識はほとんどなくても利用可能だからである。Twilio を使えば、 WebRTC に関して知らなければならないことはほんの少しで、WebRTC の知識がちょっと必要になるのはデバッグの時くらいになる。

そんな折、私はもっと WebRTC について詳しくなりたいと思った。以下の書籍だけが日本語であったので購入して読んだ。

WebRTC ブラウザベースのP2P技術

WebRTC ブラウザベースのP2P技術

NAT越えや STUN/TURN、ICE など Twilio を使って開発していた時にあまりよく把握していなかったワードが解説されていて、WebRTC への理解が深まった。でも実際にゼロから WebRTC を使って何かを実装するにはやっぱりハードルが高そうな感じはする。WebRTC のコアは Web と言うよりネットワークの領域だ。もっとコアの部分を知っていかなければ、 WebRTC を完全に理解することなんてできない。奥の深い技術テーマだ。

私は WebRTC は本当にすごいと思っている(そうじゃなきゃ WebRTC サービス運営はしていない)。WebRTC のすごいところって、Webやスマホ、電話網、SIPフォン、その他デバイス が WebRTC を通じて直接繋がることだ。今まで接続が難しかったことが 全て共通化されてリアルタイムコミュニケーションの幅が広がる。私たちのサービスでは Web と 一般電話公衆網を WebRTC を通じて繋いでいる。今回の発表を通して、ビデオ通話だけが WebRTC じゃないよってところが伝わればよかったかな。

WebRTC とそのほかの HTML5

私たちが開発しているサービスは、音声通話を利用した WebRTC サービスだ。音声通話をしている時に、普通にページ遷移してしまうと通話が終了してしまう。だからこそ Ajax を通じてページ切り替えをしないといけないし、戻るボタンを動作させるために History API を用いて履歴管理をしなければならない。

電話の着信フローの設定をよりわかりやすく表現するために Canvas が必要だった。着信時にスマホ通知を実現するために ServiceWorker が必要だった。電話を保留しているときに他のオペレーターがそれに気づけるように WebSocket が必要だった。最近の HTML5 のあらゆる技術は、私たちのサービスをより使いやすくするために必要なものだった。だからこそ WebRTC 中心にサービスを作れば、必然と他の HTML5 技術が必要になり、自然と SPA っぽくなっていく。

実はスマホ(Android Chrome) でも WebRTC で電話の受発信ができる。これをどんどん突き詰めれば、ソフトウェアをインストールすることなく Web アプリとしてスマホを携帯電話として利用できるようになるだろう。私はそんな未来を予想している。

組み合わせの大切さ

最後のまとめで話したことである「組み合わせの大切さ」。何か固有の技術が出てきた時に、それだけで爆発的なヒットを生むサービスを作ることは難しい。 WebRTC の例で言えば、ビデオ通話だけできるようなサービスを作っても今の時代ユーザーに価値を与えることはできないだろう。

サービスはあらゆるものの組み合わせだ。WebRTC と他の何かを組み合わせ、今までのコミュニケーションを劇的に改善することができてユーザーに価値を認めてもらって初めてサービスは成功する。そこで必要なのは WebRTC 以外の引き出しだ。今流行りの技術を組み合わせる例で言えば、人工知能を組み合わせたり、ビッグデータを活用して戦略的に改善できる通話サービスを作ったり、IoT と組み合わせたり。。日頃からいろんな技術を触ってみて、自分の引き出しを増やすことが大事になる。

Web 1.0でインターネットが使えるようになってポータルサイトが出来上がり、より多くの負荷に耐えられるようになって Web 2.0でSNSが広がってきた。そして次の時代の基盤とも言える HTML5 の登場ってのは何らかのチャンスと言えるのではないだろうか。

終わりに

思っていることをダラダラと書いてまとまりのない感じになってしまったが、今回の登壇で伝えたかったことはそんな感じ。発表を聞いてくれた方、この記事を読んでくれた方に何か響くものがあれば幸いだ。

Rails の render で部分的に動的な HTML を生成する

ども、@kimihom です。

Rails で CSS フレームワークとかを使っていると、例えばモーダルウィンドウを出すためにヘッダやフッタで共通の HTML を使うことになるだろう。これらは大抵の場合、共通の UI となる。それでも当然、モーダル内の body の部分はそれぞれ別々の HTML を書けるようにしたい。そんな場合にどうしたらいいのかをご紹介しよう。

共通部分を 別 html で切り出そう

Rails といえば DRY(Don't Repeat Yourself)だ。同じコードをコピペするようなことはしてはならない。それは HTML でも同様のことだ。同じコードが出てくるようなら、まずは app/views/home/_modal.html.erbのように切り出そう。今回のHTML断片は Bulma を使ったモーダルの例。

modal.html.erb

<div class="modal <%= class_name %>">
  <div class="modal-background"></div>
  <div class="modal-card">
    <header class="modal-card-head">
      <p class="modal-card-title"><%= title %></p>
      <button class="delete"></button>
    </header>
    <section class="modal-card-body">
      <%= yield %>
    </section>
    <footer class="modal-card-foot">
      <a class="button is-primary action"><%= action_name %></a>
    </footer>
  </div>
</div>

そんで、呼び出し元の HTML を <%= render %> の引数とブロックで値とHTMLを引数として渡すことができる。

<%= render layout: "modal", locals: {
  class_name: "hello",
  title: "Hello World",
  action_name: "Agree"
} do %>
  <p> Are you agree? </p>
<% end %>

こうすれば、柔軟なモーダルがどんどんと量産することが可能だ。注意が必要なのは、renderlayout オプションで指定してあげること。こうしないとブロックを利用することができなかった。

あとは JavaScript 側で Modal を表示するような処理を書けばOKだ。

モーダル内容を JavaScript で動的に書き換える

まぁたいていの場合ってのはこのモーダル内容をさらに JavaScript で動的に表示したいということがあるだろう。 Rails の View 内でそれが完結できればいいが、 JavaScript 側の値で動的に表示したい場合は、modal 表示前のイベントをキャッチして、HTMLを書き換える必要がある。

  $(document).on("click", "#console-ctrl .open-modal", function() {
    var target = $(this).attr("data-modal");
    var attr = $(this).attr("data-modal-attribute");
    $target = $("#console-ctrl .modal." + target);
    $target.trigger("modal:before-open", attr);
    $target.addClass("is-active");
  });
  
  $(document).on("click", "#console-ctrl .modal .delete", function() {
    $("#console-ctrl .modal").removeClass("is-active");
  });

  $(document).on("modal:before-open", ".modal.hello", function(attr) {
     console.log("hello modal before open");
  });

こんな感じにしてあげれば、 モーダルを表示する HTML 側で共通で書くことができる。

<i class="fa fa-plus-circle add-person open-modal" data-modal="hello"></i>

このフォントをクリックすると、さっきの hello なモーダルが表示されるようになる。そしてその描画前に、modal:before-openのイベントが発火するようになる。

終わりに

特にCSSフレームワークとかを使っていると、共通な部分ってのはよく出てくる。これらをうまく共通化して、テンプレートとして保存するようにすればより美しく汎用性の高いコードを書くことが可能だ。

今回は jQuery の trigger を利用した。この trigger については以下の記事でご紹介しているので、もし知らなかった場合は調べておくと良いだろう。

今更ながら jQuery の trigger の魅力について語らせてもらう

Service Worker でスマホ Web 通知を実現する

ども、@kimihom です。

Service Worker といえば、最近出てきたバックグラウドでごにょごにょできて通知できるアレでしょ?程度にしか思ってなかったが、思いもよらず使う場面が出てきたのでメモ。本記事はおそらく Service Worker の中でも最もシンプルなサンプルコードの部類に入ると思う。

要件

Web Notification を スマホ Web でも実現したい。ページにとどまっている前提で、そのページで何かしらのイベントを発火した時に通知できればOK。ページにいない時にプッシュ通知を送るとかはしない。

Web Notification と スマホ Web

さて、普通の PC ブラウザでプッシュ通知を実現するのは、割と簡単にできる。以下のページから引用してきただけだが貼っておく。

Web Notifications を使用する - WebAPI | MDN

  if (Notification.permission === "granted") {
    var notification = new Notification("Hi there!");
  } else if (Notification.permission !== 'denied') {
    Notification.requestPermission(function (permission) {
      if (permission === "granted") {
        var notification = new Notification("Hi there!");
      }
    });
  }

PCのブラウザでは正しく通知が届くのだけども、これを スマホの Chrome とかで実行すると通知が出てこないんだよね。これを何とかしたいってのが今回の問題。

Service Worker を使う

Service Worker を使うことでスマホ Web でも同等のことが実現できた。

// 初期化
navigator.serviceWorker.register('/sw.js');

// 通知を送りたいタイミングで
navigator.serviceWorker.ready.then(function(registration) {
  registration.showNotification('CallConnect', {
    icon: '/icon.png',
    body: '通知です。'
  });
});

showNotification の細かいオプションはドキュメントを参照していただきたい。

んで/sw.js にはこんな感じの JavaScript をパブリックな場所に置いておこう。

self.onnotificationclick = function(event) {
  // close notification
  event.notification.close();

  event.waitUntil(
    clients.matchAll({includeUncontrolled: true}).then(function(clientList) {
      for (var i = 0; i < clientList.length; i++) {
        var client = clientList[i];
        if (client.url == "/" && 'focus' in client) {
          return client.focus(); 
        }
      }
    })
  );
};

これでスマホでの通知をタップしたら、その通知を消して通知を出したページに飛ぶことができる。(client.url == の部分は変える必要あるかも)

もちろんこれは通常の PC Web でも同じ動きになるので、統一したければ統一しても良いだろう。個人的には まだ出たばかり感の強い Service Worker の利用は最小限にとどめたいので スマホ Web だけ Service Worker を使って通知するようにした。

Debug Tips

一度 Service Worker にブラウザを登録すると、キャッシュ的な感じでページを更新しても 古い Service Worker の JavaScript のまんまだったりする。

その時は、chrome://serviceworker-internals/ を叩いて開き、そこにある該当の Service Worker を消すことで対応できる。

f:id:cevid_cpp:20160726212834p:plain

終わりに

Service Worker を動かすページにいる前提で通知を出すくらいだったら、手軽に Service Worker で スマホ通知を使えることをお分かりいただけたかと思う。

これがページにいないときにでもプッシュ通知を送るとかいう仕様にした途端に面倒なことが増えるので注意。てかページにいないのにプッシュ通知を送りまくるみたいなそんなページは滅びて欲しいので、それは知らなくていいことのように思える。

Service Worker をはじめとした最新の Web 技術は、モバイルアプリとモバイルウェブの垣根をなくしつつある。ゆくゆくは全ての モバイルアプリは HTML をベースとした Web アプリで置き換えられる日が来るのもそう遠くはないのかもしれない。