Help us understand the problem. What is going on with this article?

新人エンジニアが業務効率化のためにブックマークレット作ってみたよ

More than 5 years have passed since last update.

プログラマの三大美徳に

  • 怠慢(Laziness)
  • 短気(Impatience)
  • 傲慢(Hubris)

ってありますが、大して短気でも傲慢でもないので、とりあえずは怠慢になろうとしている新卒1年目の怠慢エンジニアです。

日々のちょっとしたストレス

チームで開発していると開発メンバーの名前を入力しないといけない場面て多くありませんか?

例えば、

  • プルリクのレビュワーをセットする

20151228174228.png

  • グループチャットツールで送り先を指定する

20151228174538.png

チームが3、4人程度ならまだ苦でもないのですが10人ぐらいのチームになると、

プルリクやレビューをお願いする度に、
名前を入力して送信相手に漏れがないか確認してという作業を、
一日に何度も何度も何度も、、、

(# ゚Д゚)メンドクセェェ!!

となってしまったので、チームメンバーをワンポチでレビュワーや送信相手に指定できるボタンをブックマークレットで作ってやろうと思い立ちました(^ω^#)

ブックマークレットとは、

ブックマーク (お気に入り) を利用してブラウザに便利な機能を追加するものです。(引用:はてなブックマークレット)

作るもの

To付きメッセージを送る時、固定メンバーを一括追加できるブックマークレット

※会社ではChatworkを使用しているのでコードはChatwork用です。

必要最小限の機能のみ

bookmarklet
javascript: (function(d) {
    cw_textarea = d.getElementById('_chatText');
    cw_textarea.value = cw_textarea.value + '[To:0000]hoge [To:0000]fuga [To:0000]foo ';
    cw_textarea.focus();
})(document);

これをブックマークのURLに追加すれば完成ですね。

スクリーンショット 2014-12-28 19.49.57.png

ブックマークをクリックすることで

スクリーンショット 2014-12-28 20.17.39.png

hogeさんとfugaさんとfooさんを追加できましたね。
いい感じです!

これはこれでシンプルでいいのですが、
一つの固定メンバーしか追加できませんし、もう少し高機能にしたいなと、、

ちょっと便利に

さっきのブックマークレットを改良して、以下の機能を追加していきます。

  • 開発メンバーかプロジェクトメンバー全員かを選べる
  • 名前のありorなしを設定できる
  • 名前の最後に改行を追加するかを設定できる
  • デザインいい感じに
bookmarklet
javascript: (function(d) {
  var url = location.href,
    bookmarklet = d.createElement('div');
  bookmarklet.id = 'bookmarklet';
  bookmarklet.innerHTML += '<div class="buttons">' +
      '<div class="cw-wrapper">' +
        '<div class="cw-buttons clearfix">' +
          '<span class="buttons-name">' +
            '<img src="https://www.chatwork.com/image/favicon/favicon00.ico">Chatwork:' +
          '</span>' +
          '<button type="button" onclick="setChatworkMember(\'dev\')">エンジニアメンバーをセット</button>' +
          '<button type="button" onclick="setChatworkMember(\'all\')">すべてのメンバーをセット</button>' +
        '</div>' +
        '<div class="cw-checkboxes" clearfix>' +
          '<input id="delete_newline" type="checkbox" name="delete_newline" checked><label for="delete_newline">改行削除</label>' +
          '<input id="has_name" type="checkbox" name="has_name" checked><label for="has_name">名前あり</label>' +
      '</div>' +
    '</div>' +

    '<div onclick="document.body.removeChild(document.body.firstChild);" class="close">閉じる</div>' +

    '<style>' +
    '#bookmarklet {position: fixed; z-index: 10000; top: 10px; right: 10px; width: 340px; height: 105px; border: solid 6px #C5DCEA; border-radius: 5px; box-shadow: black 1px 1px 8px; font-size: 13px; font-family: sans-serif; color: #4f4f4f; text-align: left; padding: 0; margin: 0; background-color: rgb(223, 244, 255); } #bookmarklet .buttons {margin: 10px 0;padding: 10px; height: 68px; } #bookmarklet .buttons button {width: 200px; float: right; } #bookmarklet .cw-buttons, #bookmarklet .stash-buttons {margin: 5px 10px 5px 0px; } #bookmarklet .cw-checkboxes label {margin-right: 10px; } #bookmarklet .buttons-name {font-size: 15px; font-weight: bold; text-align: right; width: 100px; } #bookmarklet .buttons-name img {width:17px; margin: 0px 3px -1px; } #bookmarklet .close {height:20px; width:80px; background-color:#009fff; color:white; text-align:center; font-size:15px; top:-10px; left:240px; line-height:1; padding:4px 0 0 0; position:absolute; cursor:pointer; } #bookmarklet .clearfix:after {content: ""; clear: both; display: block; }' +
    '</style>';

  setChatworkMember = function(group) {
    if (location.hostname.indexOf('chatwork') == -1) {
      alert('チャットワークのページで実行してください');
      return;
    }
    var cw_textarea = d.getElementById('_chatText');
    cw_textarea.value = cw_textarea.value + getChatworkToMmmbers(group);
    cw_textarea.focus();
  };

  getChatworkToMmmbers = function(group) {
    var members = [
      {'id': 5000000, 'position': 'engineer', 'name': 'Engineer1'},
      {'id': 5000001, 'position': 'engineer', 'name': 'Engineer2'},
      {'id': 5000002, 'position': 'engineer', 'name': 'Engineer3'},
      {'id': 5000003, 'position': 'director', 'name': 'Director1'},
      {'id': 5000004, 'position': 'director', 'name': 'Director2'},
      {'id': 5000005, 'position': 'director', 'name': 'Director3'}
    ];
    var to_texts = {};
    for (var i in members) {
      var to_text = '[To:' + members[i].id + ']';
      if (d.getElementById('has_name').checked) {
        to_text += members[i].name;
      }
      to_text += (!d.getElementById('delete_newline').checked) ? '\n' : ' ';
      to_texts[members[i].id] = to_text;
    }
    var to_members = '';
    switch (group) {
      case 'dev':
        for (var i in members) {
          if (members[i].position === 'engineer') {
            to_members += to_texts[members[i].id];
          }
        }
        break;
      case 'all':
        for (var i in members) {
          to_members += to_texts[members[i].id];
        }
        break;
    }
    return to_members;
  };
  d.body.insertBefore(bookmarklet, document.body.firstChild);
})(document);

登録したブックマークをクリックすることで、

スクリーンショット 2014-12-28 21.12.26.png

ブラウザの右上に上記のようなツールセットが表示されます。

これでボタンをポチるだけで、

スクリーンショット 2014-12-28 21.20.37.png

Engineer1, Engineer2, Engineer3をセットすることができました!

いい感じですね。

ただ、自分一人だけが使うだけならこれで問題ないのですが、せっかく作ったのでメンバーに配布したいなと、

でもメンバーが増えたり減ったりする度にブックマークレットを配布しなおして再度ブックマークに登録してもらうのは手間だなーと、、

メンテナンス性を高めるために

もっと惰性を追求したいので、ブックマークレットをサーバーから配信するかたちに変更します。

bookmarklet
javascript: (function(d) {
    var s = d.createElement('script');
    s.id = 'bookmarklet';
    s.charset = 'UTF-8';
    s.src = 'https://[配信サーバーのホスト]:8443/bookmarklet?time=' + (new Date()).getTime();
    d.body.appendChild(s)
})(document);

ブックマークレットの方はだいぶスッキリしましたね。
続いて、JavaScriptを配信するサーバーサイドです。

サーバサイドで使ったもの

  • 言語:Ruby(2.1.3p242)
  • サーバー:Webrick
  • フレームワーク:Sinatra

SSLを使っているサービス上からは外部のJavaScriptを勝手に読み込むことはできなかったので配信サーバーでもSSLを使用しています。

ちなみに、読み込もうとするとSSL通信じゃないとダメだおってエラーが吐かれます。

Mixed Content: The page at 'https://www.chatwork.com/' was loaded over HTTPS, but requested an insecure script 'http://localhost:8443/bookmarklet'. This request has been blocked; the content must be served over HTTPS.

SSLに関しては証明書をオレオレ証明書の作成を参考に作り、myCAディレクトリ以下に設置しました。

Gemfile
source "https://rubygems.org"

gem "rack"
gem "webrick"
gem "openssl"
gem "net"
gem "sinatra"
gem "sinatra-contrib"
gem "sinatra-cross_origin", "~> 0.3.1"
gem "json"

config.ymlでポートと証明書パスの設定

config.yml
port: 8443
cert_path:
  certificate: ./myCA/server.crt
  private_key: ./myCA/server.key

members.ymlでメンバーの情報を管理

members.yml
- {id: 5000000, position: engineer, name: Engineer1}
- {id: 5000001, position: engineer, name: Engineer2}
- {id: 5000002, position: engineer, name: Engineer3}
- {id: 5000003, position: director, name: Director1}
- {id: 5000004, position: director, name: Director2}
- {id: 5000005, position: director, name: Director3}

以下がメインの配信サーバーです。

bookmarklet_provider.rb
require 'webrick'
require 'webrick/https'
require 'openssl'
require 'sinatra'
require 'sinatra/base'
require 'sinatra/cross_origin'
require 'sinatra/reloader'
require 'net/https'
require 'json'
require 'yaml'

set :environment, :production

conf = YAML.load_file('config.yml')
CURRENT_DIR_PATH = Dir.pwd

class BookmarkletProvider < Sinatra::Base
    register Sinatra::CrossOrigin

    before do
        cross_origin
        content_type :js
    end

    get '/bookmarklet' do
        members = YAML.load_file(File.join(CURRENT_DIR_PATH, 'members.yml'))

        <<-EOS
        (function(d) {
          var url = location.href,
            bookmarklet = d.createElement('div');
          bookmarklet.id = 'bookmarklet';
          bookmarklet.innerHTML += '<div class="buttons">' +
            '<div class="cw-wrapper">' +
            '<div class="cw-buttons clearfix">' +
            '<span class="buttons-name">' +
            '<img src="https://www.chatwork.com/image/favicon/favicon00.ico">Chatwork:' +
            '</span>' +
            '<button type="button" onclick="setChatworkMember(\\'dev\\')">エンジニアメンバーをセット</button>' +
            '<button type="button" onclick="setChatworkMember(\\'all\\')">すべてのメンバーをセット</button>' +
            '</div>' +
            '<div class="cw-checkboxes" clearfix>' +
            '<input id="delete_newline" type="checkbox" name="delete_newline" checked><label for="delete_newline">改行削除</label>' +
            '<input id="has_name" type="checkbox" name="has_name" checked><label for="has_name">名前あり</label>' +
            '</div>' +
            '</div>' +

            '<div onclick="document.body.removeChild(document.body.firstChild);" class="close">閉じる</div>' +

            '<style>' +
            '#bookmarklet {position: fixed; z-index: 10000; top: 10px; right: 10px; width: 340px; height: 105px; border: solid 6px #C5DCEA; border-radius: 5px; box-shadow: black 1px 1px 8px; font-size: 13px; font-family: sans-serif; color: #4f4f4f; text-align: left; padding: 0; margin: 0; background-color: rgb(223, 244, 255); } #bookmarklet .buttons {margin: 10px 0;padding: 10px; height: 68px; } #bookmarklet .buttons button {width: 200px; float: right; } #bookmarklet .cw-buttons, #bookmarklet .stash-buttons {margin: 5px 10px 5px 0px; } #bookmarklet .cw-checkboxes label {margin-right: 10px; } #bookmarklet .buttons-name {font-size: 15px; font-weight: bold; text-align: right; width: 100px; } #bookmarklet .buttons-name img {width:17px; margin: 0px 3px -1px; } #bookmarklet .close {height:20px; width:80px; background-color:#009fff; color:white; text-align:center; font-size:15px; top:-10px; left:240px; line-height:1; padding:4px 0 0 0; position:absolute; cursor:pointer; } #bookmarklet .clearfix:after {content: ""; clear: both; display: block; }' +
            '</style>';

          setChatworkMember = function(group) {
            if (location.hostname.indexOf('chatwork') == -1) {
              alert('チャットワークのページで実行してください');
              return;
            }
            var cw_textarea = d.getElementById('_chatText');
            cw_textarea.value = cw_textarea.value + getChatworkToMmmbers(group);
            cw_textarea.focus();
          };

          getChatworkToMmmbers = function(group) {
            var members = #{members.to_json};
            var to_texts = {};
            for (var i in members) {
              var to_text = '[To:' + members[i].id + ']';
              if (d.getElementById('has_name').checked) {
                to_text += members[i].name;
              }
              to_text += (!d.getElementById('delete_newline').checked) ? '\\n' : ' ';
              to_texts[members[i].id] = to_text;
            }
            var to_members = '';
            switch (group) {
              case 'dev':
                for (var i in members) {
                  if (members[i].position === 'engineer') {
                    to_members += to_texts[members[i].id];
                  }
                }
                break;
              case 'all':
                for (var i in members) {
                  to_members += to_texts[members[i].id];
                }
                break;
            }
            return to_members;
          };
          d.body.insertBefore(bookmarklet, document.body.firstChild);
        })(document);

        EOS
    end

    get '/permission_for_ssl' do
        'You can SSL connection with your browser.'
    end

end

webrick_options = {
    :Port            => conf['port'],
    :ServerType      => WEBrick::Daemon,
    :SSLEnable       => true,
    :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE,
    :SSLCertificate  => OpenSSL::X509::Certificate.new(  File.open(conf['cert_path']['certificate']).read),
    :SSLPrivateKey   => OpenSSL::PKey::RSA.new(          File.open(conf['cert_path']['private_key']).read),
    :SSLCertName     => [ [ 'CN',WEBrick::Utils::getservername ] ],
}
Rack::Handler::WEBrick.run BookmarkletProvider, webrick_options

クロスドメイン対策のためにCrossOriginをbeforeで設定しています。

/bookmarkletには、先ほどのブックマークレットを直書きしてmembersをyamlから読み込むようにした感じですね。

またオレオレ証明書なので、ブラウザから最初にこのサーバーにアクセスするとき警告が出てしまいます。

具体的には、以下みたいな感じです。

Chromeの場合

GET https://localhost:8443/bookmarklet?time=1419733313579 net::ERR_INSECURE_RESPONSE

Safariの場合

Failed to load resource: このサーバの証明書は無効です。“localhost”に偽装したサーバに接続している可能性があり、機密情報が漏えいするおそれがあります。

そのため、初回のみブラウザからアクセス許可を出す必要があり、そのときように/permission_for_sslを追加しました。

webrick_optionsではポート番号とSSL、それとデーモン起動の設定をしています。

これで完成です!

$ ruby bookmarklet_provider.rb

を実行すればサーバーが起動できるので、あとはブックマークレットをポチるだけですね!!

まとめ的なやつ

これで怠慢エンジニアにまた一歩近づくことができました!

ちょっとこだわりすぎてサーバーまで設置してしまいましたが、、

会社のAWSなら月1万円ぐらいまで自由に使えるので、そこに設置してチームで運用してみたいと思います。

コードはGithubに置いておくので興味のある方はご自由にお使いください。

また、拙いコードですので改良点などのご指摘は大歓迎です。

ブックマークレット作成時に参考にさせていただいサイトソーシャルてんこ盛り - actyway -

yoppe
フリーランスとしてZOZOテク、楽天、ベンチャーなどでアプリエンジニアやデータエンジニア、SRE、スクラム支援などしていたオールラウンダーなWebエンジニアです|Python,Django,PHP,RoR,Java,Scala,Play,TypeScript,Angular,GCP(GKE,GAE,Dataflow等),AWS,DDD|自作サービス http://ryutwi.yoppe.net
https://github.com/yoppe/Curriculum-Vitae
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした