- 投稿日:2019-12-14T23:50:09+09:00
jQueryでフッターをページの長さに合わせてページ下部に固定する方法
1.はじめに
本記事はフッターをページ長さに合わせてページ下部に固定する方法について自分の勉強記録も兼ねて執筆します。
ちなみにfooterFixed.jsというライブラリを使えば簡単にできるみたいですが(笑)、本記事ではそれは使わずに実装していきます。
footerFixed.jsの使い方はこちら
2.目的
フッターをページ長さに合わせてページ下部に固定する
3.コード
簡単ですが、下がコード例です。
今回は簡略下のためcss、JavaScriptをhtmlファイル内に書き込んでいます。(実際は別ファイルで管理する方が良いと思います)sample.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <style> body { color: #fff; text-align: center; width: 50%; margin: 0; padding: 0; } header { background: blue; height:100px; line-height: 100px; } section { background-color: #444; height: 200px; line-height: 200px; } footer { background: orange; width: 50%; height: 100px; line-height: 100px; } </style> <header>ヘッダー</header> <section>メインセクション</section> <footer id='footer'>フッター</footer> <script src='https://code.jquery.com/jquery-3.4.1.min.js'></script> <script> $(function(){ //フッターを最下部に固定 var $footer = $('#footer'); if(window.innerHeight > $footer.offset().top + $footer.outerHeight() ) { $footer.attr({'style': 'position:fixed; top:' + (window.innerHeight - $footer.outerHeight()) + 'px;' }); } }) </script> </body> </html>ちなみにcssで要素のheightとline-heightの数値を揃えると文字を上下中央揃えに出来ます。
(結構使えます)今回のメインとなるコードはその内、下のコードです。
sample.js<script> $(function(){ //フッターを最下部に固定 var $footer = $('#footer'); if(window.innerHeight > $footer.offset().top + $footer.outerHeight() ) { $footer.attr({'style': 'position:fixed; top:' + (window.innerHeight - $footer.outerHeight()) + 'px;' }); } }) </script>4.コードの解説
では上記のコードを解説していきます。
❶
var $footer = $('footer');
まず
var $footer = $('#footer');
でid='footer'のDOMを取得します。
<footer class='footer'>フッター</footer>
だったらvar $footer = $('.footer');
となります。
id属性とclass属性をつけたセレクタの指定の仕方はcssと同じなのでここでは割愛します。DOMについてはこちら
簡単にいうとfooter要素を取得したと考えてもらえたらOKです。
フッターのDOMを取得したのでフッターをjavaScript(jQuery)で操作する準備が出来ました。
❷
if(window.innerHeight > $footer.offset().top + $footer.outerHeight() )
まず上記コードの意味を簡単に言うと表示画面(window)の長さがフッターの下端までの距離より長い場合です。
なるべく詳しく説明していきます。
上図のように表示画面の高さよりフッター下端までの距離が短い場合、本記事で解説している
<script>〜</script>
のコードがなければ表示画面の途中にフッターが表示されることになります。ですので、このような場合にフッターの位置をページ最下部に固定したいのでif文を使っています。
.innerHeight
は要素のpaddingを含んだ高さを取得するメソッド
.outerHeight
は要素のborder、paddingを含んだ高さを取得するメソッド
.offset().top
はその要素の上端の位置を取得するメソッドです。
高さを取得するメソッドの詳しい説明はこちら
.offset()メソッドの詳しい説明はこちら❸
$footer.attr({'style': 'position:fixed; top:' + (window.innerHeight - $footer.outerHeight()) + 'px;' });
いよいよフッターをページ最下部へ固定していきましょう。
まず、
.attr
は要素の属性を取得、変更、追加することのできるメソッドです。.attrメソッドについてはこちら
style属性を追加して、まず固定するために
position: fixed;
を指定します。
top:〜
で固定場所を指定します。
(window.innerHeight - $footer.outerHeight()) + 'px;'
で表示画面の下端からフッターのheightを差し引いた位置を指定しています。つまり、ページ長さに応じて自動でフッターがページ最下部に固定されるということです。
コード解説は以上です!!
※ちなみにcssファイルを別で準備してhtmlファイルに読みこませるようにしていたら.cssメソッドでも実装できるかもです。
5.positon:fixed;との違い
みなさんに質問です。
「フッター固定したいならヘッダーを上部固定するみたいにcssでposition:fixed;
を指定すればできるんじゃない?」って思いませんでしたか?
僕は当時は思いました。(みなさん思わないようでしたらすみません)ですが、フッターに直接
position: fixed;
を指定した時に困ることがあります。それについて追加で説明していきます。
まず、WEBサイトをつくる時って1ページで完成することもありますが、複数ページで構成されることの方が多いのではないでしょうか?
複数ページで同じフッターをページ下部に表示することになるので、同じcssを適用することになります。
そのページごとにposition:fixed;の座標をcssで指定しなければならなくなります。例えば、
ヘッダー、メインセクション、フッターの高さがそれぞれ50px、300px、50pxの場合、sample.cssfooter { position:fixed; top:350px; }とすれば、以下の通り綺麗にページ下部に固定できますよね。(図は不要と判断しましたのでイメージしてください)
しかし、別のページでメインセクションのheightが500pxになった場合、同じsample.cssを適用すると下の図のようにメインセクションとフッターが重なってしまいますよね。
このような場合
footer要素に
<footer class='footer-1 footer-2'
のように複数のclass属性値をつけてcssを分ける必要があります。これが10ページくらいになるとすると、class属性値も10個つけなればなりません(笑)
控え目に言ってめちゃくちゃメンドクさいです。
プログラミングはめんどくさいことを効率化できるというメリットがあるのにめんどくせいことはできるだけやめましょう。
6.最後に
本記事ではjQueryを用いてページ長さに合わせてフッターをページ最下部に固定する方法について説明しました。
最後にもう一度ソースコードを載せておきます。
sample.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <style> body { color: #fff; text-align: center; width: 50%; margin: 0; padding: 0; } header { background: blue; height:100px; line-height: 100px; } section { background-color: #444; height: 200px; line-height: 200px; } footer { background: orange; width: 50%; height: 100px; line-height: 100px; } </style> <header>ヘッダー</header> <section>メインセクション</section> <footer id='footer'>フッター</footer> <script src='https://code.jquery.com/jquery-3.4.1.min.js'></script> <script> $(function(){ //フッターを最下部に固定 var $footer = $('#footer'); if(window.innerHeight > $footer.offset().top + $footer.outerHeight() ) { $footer.attr({'style': 'position:fixed; top:' + (window.innerHeight - $footer.outerHeight()) + 'px;' }); } }) </script> </body> </html>ご存知の方はたくさんいると思いますが、この機能はかなり便利(と思っています)なので是非使ってみて欲しいなと思います!
今回は以上です。
※コメント、ご指摘等ありましたら何でも受付けておりますので気軽にコメントいただけたらと思います。
- 投稿日:2019-12-14T23:44:33+09:00
Node-RED で黒板機能と操作ノードを実装してみた
Node-RED の右にあるサイドバー領域に表示される黒板と、それを操作するノードを作成してみましたので、ご紹介します。
コードは yamachan/node-red-contrib-rtk-board リポジトリにあります。少しずつ、地味に機能追加していたりします。2019年内に npm に登録するのが目標だったり。
作成した理由
Node-RED Advent Calendar 2019 の投稿ネタとして作成しました。ちょっと趣味に走って、想定を超えて拡張しつつありますが…
昨年の Node-RED Advent Calendar 2018 では「Node-RED パズルで遊んでフローに親しんでもらいたい」 という記事を投稿しました。Node-RED のフロー編集画面を生かし、パズル的なものを用意することで楽しく学んでもらいたい、という感じで。
ただその際、フロー中の正解/不正解をステータスのアイコンだけで表現しているのが地味で、もう少し派手な表現方法が欲しいな、何か作ってみよう、とネタを練っていました。まさか1年も寝かすことになるとは思いませんでしたが…
node-red-dashboard の利用を考えましたが、Node-RED お馴染みのフロー画面とは別ページ(別タブ)になります。そうではなくて、デバッグメッセージのように、フロー画面の右にあるサイドバー領域に何かを表示したいと思っていました。
将来的には Scratchキャット のように、見た目にわかりやすい反応で、プログラミング初心者が学習時に最初に遊んでみるノードのひとつになればいいな、などと妄想しております。
Virtual TJBot から学ぶ
サイドバー領域に何かを表示したい!と資料を探していたところ、発見したのが「How to Train Virtual TJBot in Node-RED」記事を訳して試してみた」の記事で紹介した、node-red-contrib-virtual-tjbot です。これはフロー画面の右にあるサイドバー領域に、TJBot の画像を表示して動かしています。まずはこのリポジトリのコードを読んで、サイドバーの仕組みを理解しました。
パネルを表示する
パネル表示のキーとなるのは /tjbot/config.html ファイル 55 行目あたりにある以下のコードです。
/tjbot/config.htmlonpaletteadd: function () { RED.sidebar.addTab({ // ココ! id: "vtjbot", label: "Virtual TJBot", // the label is displayed on the tab name: "Virtual TJBot", // displayed in the view menu content: '<div id="vtjbot" style="width: 100%; height: 100%"><iframe src="/tjbot" style="min-width: 260px; height: 500px; border: 0px" border="0"></iframe></div>', // content of the tab //toolbar: "toolbar", // content for the footer of the tab closeable: true, // can be closed enableOnEdit: true, iconClass: 'fa fa-android' }); }
RED.sidebar.addTab()
関数を用いてパネルを追加しているのがわかります。ググったら API Reference に載っていましたが、詳細な説明はありませんでした。パネルの中身を生成する
実際に表示されるコンテンツは
<iframe src="/tjbot"
で指定されている以下のページです。
そして、このページを表示するためのhttpサーバー機能を提供しているのが /tjbot/ui.js ファイル 52行目あたりにある以下のコードです。
/tjbot/ui.jsfunction init(server, app, log, redSettings) { tjbotPath = join(redSettings.httpNodeRoot, "tjbot"); const socketIoPath = join(tjbotPath, "socket.io"); const bodyParser = require("body-parser"); app.use(bodyParser.json({ limit: "50mb" })); app.use(tjbotPath, serveStatic(path.join(__dirname, "dist"))); // ココ! io = socketio(server, { path: socketIoPath }); io.on("connection", socket => { socket.emit("config", state); }); }なおこの /tjbot/ui.js は package.json ファイルで定義されていないので、自動的には読み込まれません。各ノードの定義部分の先頭で呼び出すことで、初期化が実施されています。
/tjbot/shine.jsmodule.exports = function (RED) { const ui = require("./ui.js")(RED); // 以下略 }操作ノードとの連携
頭の LED を光らせる shine ノードのコード 41行目あたりを見てみます。
/tjbot/shine.jsswitch (mode.toLowerCase()) { case "shine": if (color == "random") { const randIdx = Math.floor(Math.random() * colors.length); ui.emit("shine", { color: colors[randIdx] }); } else { ui.emit("shine", { color: color }); } break; // 省略 }emit 関数は /tjbot/ui.js ファイル 67行目あたりで定義されています。
/tjbot/ui.jsfunction emit(command, params) { io.emit(command, params); switch (command) { case "shine": state.led.color = params.color; break; case "pulse": state.led.color = "off"; break; case "armBack": case "lowerArm": case "raiseArm": case "wave": state.arm.position = command; break; } }
io
はさきほどのinit()
関数で定義されていました。 WebSopcket 技術を使った socket.io でノードと表示パネルを連携させていることがわかります。開発用の Node-RED 環境を準備
さて、ローカルで開発用の Node-RED 環境を用意しましょう。私の場合、適当な作業用のフォルダで、以下のようにコマンドを実行します。(Windowsコマンドプロンプトの場合)
md node-red-dev cd node-red-dev npm init -y npm install --unsafe-perm --save node-red md .node-red echo 2> .node-red/settings.js node_modules\.bin\node-red -u .node-red -s .node-red/settings.jsまあ普通のやり方だと思いますが、
-u
-s
オプションで設定ファイルも同じフォルダ内に指定することで、既にインストール済みの Node-RED 環境に影響しないようにしています。これなら利用後はフォルダごと削除すればokです。手抜きで setting.js を空にしているので、既存の設定を引き継ぎたい場合にはホームディレクトリにある .node-red フォルダからちゃんとコピーしてください。
そしてノード開発用のリポジトリ (今回は c:\work\GitHub\node-red-contrib-rtk-board) を作成し、管理者権限のコマンドプロンプトからシンボリックリンクを作成すれば開発準備は完了です。
mklink /d node_modules\node-red-contrib-rtk-board c:\work\GitHub\node-red-contrib-rtk-board今回の機能を実際に試してみたい方は、まだ npm に登録されていないので、node_modules フォルダに必要ファイルを用意するため、ローカルで準備したNode-RED のフォルダで以下を実施してください。
cd node_modules git clone https://github.com/yamachan/node-red-contrib-rtk-board.gitなお開発中に動作がおかしくなったら、.node-red 設定フォルダを settings.js ファイルだけにして、Node-RED を再起動すればok。
開発を始めよう
さて、準備ができたところでノード開発を始めましょう。基本的な構成は Virtual TJBot を参考に進めていきます。
設定ノード
まずは基本となる設定ノードから進めましょう。開発ガイド がとても参考になります。
後でいくらでも拡張できるので、最初は最低限の項目だけ設定できるようにします。
- ボードの種類 (いわゆる黒板、ホワイトボード、ブラックボードなど)
- ボードの大きさ (横幅、高さ)
以下が実際に開発した設定画面です。
わりと基本に忠実で、特別なことはやっていません。短いですし、詳細は実際のコードを見てください。Output ノード
これが今回の主役ノードで、黒板への出力を全て担っています。
実はループや条件判断もある、簡易言語として実装されていて、テキストとしてプログラムを与える(payloadに指定する)といろんな操作ができたりします。詳しくは README を参照してください。時間があったら日本語の解説記事を書きますね!入力を payload で受けるだけなので、設定画面は最低限です。「RTK Board Config」は前の章で説明した黒板全体の設定ノードです。
実装コードは以下で、dist フォルダに配置した黒板用の html と、そこで利用している描画用の js コードも含みます。
- rtk-board/output.html
- rtk-board/output.js
- rtk-board/ui.js
- rtk-board/dist/index.html
- rtk-board/dist/rtk-face.js
- rtk-board/dist/rtk-board.js
Output ノードの動作例 (基本的な使い方)
Output ノードの使い方は簡単で、Inject ノードを用意し、ペイロードを文字列に設定して、描画用のコマンドを記載すればokです。
以下はコマンドを記載した Inject ノードを幾つか接続して、上から順にクリックしていった結果です。
cls rect 80 60 50 50 red fillRect 100 80 70 70 blue bp;arc 30 200 40 0 2 yellow;stroke bp;arc 60 200 160 0 2 pink;fill
コマンドは;
文字で繋げることで、複数指定することができます。今回の例ではわかりやすいように Inject ノードを幾つも並べていますが、1つの Inject ノードのテキストに、全部指定してしまってもokです。Output ノードの動作例 (ステートの利用)
Output ノードで処理する際、処理系はステートフルに実装されていて、過去の描画パラメータを覚えています。
各コマンドのパラメータは省略されるか、省略を意味する
_
文字が指定されると、最後に実施した値を再利用します。また color のような命令は、実際に描画を実施せず、この保存された値を更新します。以下の例を参照してください。これも上から順に Inject をクリックした結果です。
cls rect 100 100 50 50 red rect 80 80 color blue rect 40 40 color white; go 10 10 rect 40 40
rect
は四角形を描画する命令で、最初の2つの引数は縦横のサイズを、次の2つの引数は表示位置を、そして次の引数は描画色を指定します。例えば Inject (3) にある
rect
はサイズしか指定されていないため、表示位置と表示色は (2) のrect
の描画と同じになっています。Inject (5) にあるrect
も同様にサイズしか指定されていませんが、その前の (4) でcolor
命令で描画色が青に変更されているため、青い四角形を描画しています。Inject (7) にある最後の
rect
もやはりサイズしか指定されていませんが、その直前 (6) でcolor
で描画色が白に変更されており、またgo
で表示位置も移動されているので、左上に白い四角形として描画されています。Output 処理系のステートを利用して指定を省略することで、指定するコマンドの総量を減らすことができます。また修正箇所が減り、変更も容易になります。
Output ノードの動作例 (ランダム表示)
ステートはランダム系のコマンドとも相性が良いです。
例えば以下はランダムに位置を指定する
goRand
コマンドと、colorRand
コマンドを利用した例になります。cls goRand; colorRand; rect 10 10Inject (2) をクリックすると、ランダムな位置に、ランダムな色で、小さな四角形が表示されます。何度もクリックすると、上記のようにカラフルな画面になりました。
後で説明のある
loop
コマンドと組み合わせると、いろいろな表示が楽しめそうです。Output ノードの動作例 (ステートの演算)
ステートを利用した、もう少し複雑なサンプルを見てみましょう。
まず (2) のコマンドに注目してください。円の半径に45%
を指定されていて、これは表示する黒板のサイズの 45% の長さを指定したことになります。縦横の長さが違う場合には小さいほうの値をもとに計算されます。そして表示位置として
50% 50%
が指定されていて、これはそれぞれ、黒板の横幅の50%、縦幅の 50% を意味しており、結果として黒板の中心を示しています。今回のサンプルでは Inject を以下の順でクリックしています。
(1) → (2) → (3) → (4) → (3) → (4) → (3) → (4) → (3) → (4)
つまりは以下を実行したことになります。
cls bp;arc 45% 50% 50%;stroke let _r $($._r * 0.8) bp;arc;stroke let _r $($._r * 0.8) bp;arc;stroke let _r $($._r * 0.8) bp;arc;stroke let _r $($._r * 0.8) bp;arc;strokeそして鍵となるのが (3) の
let
コマンドです。let _r $($._r * 0.8)
let
コマンドは、ステートの内部値(描画を実施する際の変数の値)をセットするためのコマンドです。そしてパラメータを処理する際に$(
と)
で囲まれた範囲は、式として評価され演算が実行されます。そして式のなかで、内部値は
$
オブジェクトの値として参照できます。今回のlet
コマンドを実施すると、円の半径を意味する_r
という内部値の値に対して0.8
の数を積算していますから、つまりは半径が少し小さくなります。(3) と (4) を繰り返し Inject してコマンドを実行するたび、より半径の小さい円が描画される、というわけです。
Output ノードの動作例 (ループの例)
さきほどは手動で Inject (3) (4) を何回かクリックしました。これをループを使って自動化してみましょう。
cls bp;arc 45% 50% 50%;stroke let l 4 let _r $($._r * 0.8);bp;arc;stroke;loop l -5ここでは
let
コマンドを使って、ループ回数をカウントする内部値(変数)であるl
を定義しています。4回実施したいので、値は4
をセットしています。その後の
loop
コマンドが繰返しを実施する命令で、これは指定された内部値(変数)、今回はl
を使って繰り返しを実施します。具体的にはl
から1を引き、0より大きければその後にある値-5
を実行カウンタに加える、つまり5つ命令を戻しています。昔のアセンブリ言語にあったような、原始的な繰り返し命令です。現時点ではまだエラーチェックが甘く、下手するとブラウザがフリーズしかけるので、注意して扱ってください。
ステートの仕組みや、相対的な移動である
move
コマンドと組み合わせてうまく使えば、記述を大きく減らせる可能性をもっています。まぁ、趣味の機能なのでゆるーく楽しむ程度にしてください。Face ノード
今回の黒板機能は、Output ノードだけで完結しています。この Face ノードは、その機能を簡単に使うための、サポート用のノードになります。
次のサンプルをみてください。
(3) の Face ノードの設定は以下のようになっています。
また (4) の Inject のペイロードは以下の文字列になっていて、face コマンドを記述しています。
face smile rf 80% 80% 10% 10%このサンプルにおいて、(2) の smile を設定した Inject をクリックしても、(4) の face コマンド設定した Inject をクリックしても、描画される画像は同じです。
つまり Face ノードは、Output に実装された face コマンドを簡単に記述するためのノードです。基本的な情報を設定すれば、後は smile(笑顔), ugly(憂鬱顔), sad(悲しい顔), safe(安心した顔), angly(怒り顔), usual(素の顔) などの使いたい表情(face_mode)をペイロードで指定するだけで描画できます。
実際のコードはこちら。テキストのコマンドを並べるのも楽しいですが、やはりこの Face のようなサポート用のノードを使ったほうが簡単に楽しめますし、Node-RED っぽいですよね。今後も、サポート用のノードを増やしていく予定です。
縦長の顔はいやだ
今回の Face ノードのサンプルを見て「縦長の顔は可愛くない」との指摘をいただきました。表示サイズを % で指定しているので、表示する黒板のサイズにあわせて顔が縦長に表示されています。
とりあえずの解決策なのですが、縦のサイズを
80%
という黒板からの相対値ではなく、$_w
という内部値を参照としたものに変更してください。``_w'' は横のサイズを示す内部値で、それを指定することで、表示する際に縦横同じ、つまり正方形の領域に顔が表示されるようになります。
Inject に指定したコマンドのほうも同様に変更しておきます。
というわけで
とりあえずはサイドバー領域に黒板的な表示パネルを追加し、そこに描画するためのノードを定義できました。ノード開発楽しいです。
まだ実装できそうな描画コマンドはありますし、それらを簡単に扱えるようにサポートノードの数も増やしたいところです。しばらくはコードを修正して、機能を拡張し、年内の npm への公開まで頑張ります!
とりあえずはステータスちゃんと表示して、国際化して日本語追加して、あとは画像表示コマンドの実装とラベル機能の完成、はクリスマスまでには終わらせる予定。などと宣言して自分を追いつめてみる。
それではまた!
- 投稿日:2019-12-14T23:30:31+09:00
Web Share API を使ったページ共有を試してみた
はじめまして! 2019年10月フロントエンドエンジニアとして入社した上垣です。
入社してから今まで、 Toreta nowのブラウザ版開発に携わっています。toC サービスを開発していく中でよくある要件として、SNSへのページ共有が挙げられると思います。ただ、実装したことがある方ならおわかりと思いますが、シェアさせたいサービスごとに script を埋め込むのは結構大変です。
代表的なサービスの共有方法
- LINE
Toreta now で共有機能の実装を検討していく中で、Web Share API の存在を知ったので、サービスに導入できるか検証してみました。
Web Share APIとは
Web Share API は、ユーザーが選択した任意の宛先にコンテンツを共有するためのAPIです。
W3Cで仕様が公開されており、現在のステータスは W3C Editors Draft(編集者草案) となっています。仕様
GitHub
GitHub - w3c/web-share: Web API proposal for sharing data from a web page
対応ブラウザ
Web Share API はまだ編集者草案のステータスですが、2019年12月時点では、Safari, iOS Safari と Chrome for Android の最新版で実装されています。
Can I use... Support tables for HTML5, CSS3, etc
使用方法
window.navigator の share メソッドをシェアしたい内容を引数に渡して呼び出すことで、共有用ダイアログが表示されます。
JavaScript で実装してみたコードは下記のようになりました。
async share() { if (!window.navigator.share) { alert("ご利用のブラウザでは共有できません。"); return; } try { await window.navigato.share({ title: 'Share API で共有!', text: 'ご覧の通り、お手軽にSNSにリンクを供することができます。', url: 'https://example.com/hogehoge', }); alert('共有が完了しました。'); } catch (e) { console.log(e.message); } }引数
- data : シェアする URL を表す文字列
- text : シェアするテキストを表す文字列
- title : シェアするタイトルを表す文字列
戻り値
ユーザーがシェア操作を完了させたときに解決される Promise。
実践
実際に、上記コードを Android Chrome, iOS Safari で実行してみました。
(以下の例は私の端末で試したものです。端末にインストールされているアプリによって、シェア対象のアプリは変わります。)Android 9 Chrome 78
iOS 13.2.3 Safari
Android iOS 双方とも、SNSアプリに限らず、メモ帳やGmailなど、思ったよりも多くのアプリに共有が可能でした。Slack にも共有できたので、自分宛にメッセージを POST してみました。
Slack
title は表示できていませんが、指定したメッセージを共有することができました!
![]()
まとめ
Web Share API は、まだドラフトのステータスながら、最新の iOS safari, Android Chrome で十分使えるレベルで実装されていることがわかりました。
仕様変更の可能性を残しているとはいえ、モバイルメインのサービスであれば、十分に利用検討の価値があるのではないでしょうか。
- 投稿日:2019-12-14T23:30:03+09:00
Canvasで作るお絵かきアプリ
この記事はtomowarkar ひとりAdvent Calendar 2019の14日目の記事です。
はじめに
昔に描いたコードを持ってきたので、コードが少し古く、初心者感丸出しですがご容赦を。
スマートフォンに対応させたのがポイントです。
デモサイト
コード全文
index.html
- 描画エリア設定
- カラーパッド設定
- 削除ボタン設定
<!doctype html> <html> <head> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <title></title> </head> <body> <div id="hitarea"><canvas id="canvas"></canvas></div> <input type="color" id="color" value="#ff7f7f"> <input type="button" id="removePaint" value="clear" onclick="removePaint()"> </body> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> </html>style.css
style.cssbody { margin: 0; overflow: hidden; } #color{ position: absolute; margin: 0; width: 54px; height:58px; right: 10px; bottom: 60px; border: none; background: none; } #removePaint{ position: absolute; margin: 0; width: 50px; height:50px; right: 15px; bottom: 10px; background: lightgray; }script.js
script.jsvar canvas = document.getElementById( 'canvas' ); var ctx = document.getElementById("canvas").getContext("2d"); canvas.width = window.innerWidth; canvas.height = window.innerHeight; function drawCoordinates(x,y,mouseX,mouseY){ ctx.beginPath(); ctx.moveTo(mouseX, mouseY); ctx.lineTo(x, y); ctx.lineCap = "round"; ctx.lineWidth = 10; ctx.strokeStyle = defocolor; ctx.closePath(); ctx.stroke(); mouseX = x; mouseY = y; } var $document = $(document); var $hitarea = $('#hitarea'); var $x = $('#x'); var $y = $('#y'); var supportTouch = 'ontouchend' in document;// タッチイベントが利用可能かの判別 var EVENTNAME_TOUCHSTART = supportTouch ? 'touchstart' : 'mousedown'; var EVENTNAME_TOUCHMOVE = supportTouch ? 'touchmove' : 'mousemove'; var EVENTNAME_TOUCHEND = supportTouch ? 'touchend' : 'mouseup'; var mouseX = ""; var mouseY = ""; var updateXY = function(event) { var original = event.originalEvent; var x, y; if(original.changedTouches) { x = original.changedTouches[0].pageX; y = original.changedTouches[0].pageY; } else { x = event.pageX; y = event.pageY; } $x.text(x); $y.text(y); return [x,y]; }; // イベント設定 var handleStart = function(event) { event.preventDefault();// タッチによる画面スクロールを止める [x,y] = updateXY(event); mouseX = x; mouseY = y; bindMoveAndEnd(); }; var handleMove = function(event) { event.preventDefault(); // タッチによる画面スクロールを止める [x,y] = updateXY(event); drawCoordinates(x,y,mouseX,mouseY) mouseX = x; mouseY = y; }; var handleEnd = function(event) { [x,y] = updateXY(event); drawCoordinates(x,y,mouseX,mouseY) mouseX = ""; mouseY = ""; unbindMoveAndEnd(); }; var bindMoveAndEnd = function() { $document.on(EVENTNAME_TOUCHMOVE, handleMove); $document.on(EVENTNAME_TOUCHEND, handleEnd); }; var unbindMoveAndEnd = function() { $document.off(EVENTNAME_TOUCHMOVE, handleMove); $document.off(EVENTNAME_TOUCHEND, handleEnd); }; $hitarea.on(EVENTNAME_TOUCHSTART, handleStart); let defocolor = "#ff7f7f"; function watchColorPicker(event) { defocolor = event.target.value; } document.querySelector("#color").addEventListener("change", watchColorPicker, false); function removePaint() { ctx.beginPath(); ctx.fillStyle = "#FFFFFD"; ctx.fillRect(0, 0, canvas.width,canvas.height); }おわりに
以上明日も頑張ります!!
tomowarkar ひとりAdvent Calendar Advent Calendar 2019参考
- 投稿日:2019-12-14T23:21:21+09:00
Svelte3 横並びのメニューを作る!
Svelteでこんな感じのよくある横並びメニューを作りたい!
そして画面からはみ出た分は横スクロールもできるようにしたい!
と思ったのでその際の実装をメモしておきます。リストタグを横並びにする&スクロールもできるようにする
まずは横並びのメニューを作成&横スクロール可能にする実装です。
スクロールバーも非表示にしています
Chrome/Safari以外のブラウザなどシラヌちなみに
overflow-x: hidden;
を指定するとスクロールルバーは消えるけど横スクロールもできなくなるぞっ!!!!こうすると
<div class="menu"> <ul> {#each menus as menu} <li>{menu}</li> {/each} </ul> </div> <script> const menus = ['menu1', 'menu2', 'menu3', 'menu4', 'menu5']; </script> <style> .menu { width: 100%; } ul { padding: 0; /* 先頭に表示される"・"を消す */ list-style: none; /* 左右の表示領域からあふれる時にスクロールバーを表示する */ overflow-x: auto; /* 横並びにした時に返しを行わせない */ white-space: nowrap; } /* スクロールバーを非表示にする */ ul::-webkit-scrollbar { display: none; } li { /* リストを横並びにする */ display: inline-block; /* 見た目整える用 */ width: 100px; height: 50px; background: #fa8072; margin-right: 5px; } </style>こんな感じになる!
クリックしたメニューのスタイルを変える
クリックした要素が分かるようにスタイルを変えたい!という時の実装です。
class:focus={pos === i}
のように記述することで
pos === i
の条件を満たす場合だけfocus
というclass
をつける事ができるので
クリック時につけたいstyleを追加しておけばクリックされた要素の見た目を変えられます。こうすると
<div class="menu"> <ul> {#each menus as menu, i} <li class:focus={pos === i} on:click={() => pos = i}>{menu}</li> {/each} </ul> </div> <script> let pos; const menus = ['menu1', 'menu2', 'menu3', 'menu4', 'menu5']; </script> <style> .menu { width: 100%; } ul { padding: 0; /* 先頭に表示される"・"を消す */ list-style: none; /* 左右の表示領域からあふれる時にスクロールバーを表示する */ overflow-x: auto; /* 横並びにした時に返しを行わせない */ white-space: nowrap; } /* スクロールバーを非表示にする */ ul::-webkit-scrollbar { display: none; } li { /* リストを横並びにする */ display: inline-block; /* 見た目整える用 */ width: 100px; height: 50px; background: #fa8072; margin-right: 5px; } /* クリックされた要素のスタイル */ .focus { background: #87ceeb; } </style>こんな感じになる!
クリック時にイイ感じの場所にメニューをスクロールさせる
見切れているメニューをクリックした時に見切れたままにするのは気持ちが悪いので
クリックされたメニューをいい感じの場所にスクロールさせたい!という時の実装です。ulタグに
bind:this={menu}
を追加することでulのDOM要素を操作できるようになります。
scrollTo
で水平方向のスクロール位置を指定することでulタグ内の任意の位置にスクロールさせることができるようになります。こうすると
<div class="menu"> <ul bind:this={menu}> {#each menus as menu, i} <li class:focus={pos === i} on:click={() => onClickMenu(i)}>{menu}</li> {/each} </ul> </div> <script> let pos; let menu; const menus = ['menu1', 'menu2', 'menu3', 'menu4', 'menu5']; const onClickMenu = i => { pos = i; menu.scrollTo({ top: 0, left: 80 * i, behavior: 'smooth', }) }; </script> <style> .menu { width: 100%; } ul { padding: 0; /* 先頭に表示される"・"を消す */ list-style: none; /* 左右の表示領域からあふれる時にスクロールバーを表示する */ overflow-x: auto; /* 横並びにした時に返しを行わせない */ white-space: nowrap; } /* スクロールバーを非表示にする */ ul::-webkit-scrollbar { display: none; } li { /* リストを横並びにする */ display: inline-block; /* 見た目整える用 */ width: 100px; height: 50px; background: #fa8072; margin-right: 5px; } /* クリックされた要素のスタイル */ .focus { background: #87ceeb; } </style>こんな感じになる!!
bind:thisを使う時に気をつけること
bind:this={menu}
はコンポーネントがマウントされるまでは変数menu
の値はundefined
にため以下のような実装だとエラーになるので
onMount()
やon:click
で使用する関数の中で使用しなければならないです。<div class="menu"> <ul bind:this={menu}> {#each menus as menu, i} <li class:focus={pos === i} on:click={() => onClickMenu(i)}>{menu}</li> {/each} </ul> </div> <script> let menu; menu.scrollTo({ top: 0, left: 50 * i, behavior: 'smooth', }) </script>おわり
いい感じに横スクロールして動くメニューができた!!!
- 投稿日:2019-12-14T21:07:33+09:00
ChromeExtentionの開発で役に立った知識
ChromeExtensionって、ネット上の情報が少ない気がします。そんな訳で、Javascript&ChromeExtention歴2ヶ月ほどですが役に立った知識を羅列していこうと思います。間違いがあったら教えて下さい。そんなこと知ってるわ!みたいなことがあっても許してください。前提条件として下記のことが挙げられます。
・ECMAScript6環境です。
・JavaScriptの知識もあれば、Extension固有の知識もあります・
・chrome.storageを使ったデータベース風なclassを作っていて、そこで躓きまくったので、classに関する話がほどんどです。Background.jsではexport/importが使えない
Background.jsは裏に見えないウインドウを展開して、その中でJavaScriptの処理を行うらしいので、基本的には普通のブラウザと同じようにプログラミングして問題ありません。故にasync/awaitとかも使えるんですが、export/importだけは使えません。原因はわかりませんが、クラスを作ってそれでデータのやり取りをするときに困りました。
対策
exportはmanifest.jsonで予めexportしたいファイルを読み込みます。ポイントはbackground.jsより先に読み込みたいファイルを書くことです。
"background": { "scripts": ["js/test.js", "js/test.js", "js/background.js"], "persistent": false },importはhtml上で普通に先に読めこめば使えますが、Background.jsでしか使わない関数があったりするので、エラーの原因になりやすくあまりおすすめできません。故に、読み込むよりは共通部分を抜き出してjsファイルにして、先の方法で共通化したjsファイルをbackground.jsに読み込む方が早いと思います。
classのコンストラクタではawait/asyncが使えない。
ChromeAPIsは基本何でもコールバック内で処理をさせようとします。故に、かなりの頻度でasync/awaitを使うことになりますが、コンストラクタ内では使えません。
対策
staticなinit(初期化)関数を作って、擬似的なコンストラクタとして運用します。privateとかのアクセス修飾子がないので、きちんとコメント書いておかないと事故に繋がります。擬似的にprivateにする方法もあるようですが、可読性が下がる気がして個人的にあまり好きではないです。
class Test { //このコンストラクタは使わない constructor() { this.name = "amuza"; }; //コンストラクタの代わりにこちらを使う static async init() { const test = new Test(); this.name = await changeName(); return test; }; async changeName(){ /*中略*/ }; }class内では定数が作れない
ChromeExtensionの情報保存手段として、chrome.storageというAPIがあります。JSON形式で保存するのでkeyが必要になります。でも設定とかを保存するので、毎回決まったkeyを使います。そのためkeyが定数だとタイプミスの可能性が減り、プログラムの完成度が上がります。でも、class内ではconstが使えません。
対策
getterを使うことで読み込み専用のプロパティを作り、定数として運用します。
class Test{ static get GREETING() { return "greeting"; }; saveGreeting(){ const obj = {[Test.GREETING]: "hello"}; chrome.storage.sync.set(obj, () => { if (chrome.runtime.lastError) { return ; } else { resolve(objectPlusKey); } }); }); }最後に
多分今後も色々気づきがありそうなので、その都度加筆するか新しい記事を書きます。あと、1月末くらいまでにはChromeExtentionが一つリリースできそうなので、試してみていただけたら幸いです。リリース時には多分Twitterで告知します。なので、フォローしてくださったらとても嬉しいです。(欲張りな告知)
参考文献
https://qiita.com/guru_taka/items/37a90766f4f845e963e5
https://qiita.com/k7a/items/26d7a22233ecdf48fed8
https://qiita.com/noriaki/items/e7adaaf440020fbf6836
- 投稿日:2019-12-14T20:40:32+09:00
JavaScriptで1を31ビット左シフトするとマイナスになる
JavaScript で
2^32
の値が必要になりました。そこで、1 << 32
を計算したところ、1
になってしまいました。試しに1 << 31
を計算してみると、-2147483648
と負の値が返ることがわかりました。以下のコードを書いてみると、
1 << 30
までは期待通りの結果が得られているようですが、1 << 30
より大きい値は変な結果になってしまいました。for (let i = 1; i <= 32; i++) { console.log(`1 << ${i}: ${1 << i}`); } console.log("1 << 32 << 32 << 32 << 32:", 1 << 32 << 32 << 32 << 32);1 << 1: 2 1 << 2: 4 1 << 3: 8 1 << 4: 16 1 << 5: 32 1 << 6: 64 1 << 7: 128 1 << 8: 256 1 << 9: 512 1 << 10: 1024 1 << 11: 2048 1 << 12: 4096 1 << 13: 8192 1 << 14: 16384 1 << 15: 32768 1 << 16: 65536 1 << 17: 131072 1 << 18: 262144 1 << 19: 524288 1 << 20: 1048576 1 << 21: 2097152 1 << 22: 4194304 1 << 23: 8388608 1 << 24: 16777216 1 << 25: 33554432 1 << 26: 67108864 1 << 27: 134217728 1 << 28: 268435456 1 << 29: 536870912 1 << 30: 1073741824 1 << 31: -2147483648 1 << 32: 1 1 << 32 << 32 << 32 << 32: 1原因
JavaScript ではビット演算は符号付き 32 ビット演算で行われます。
1 << 30
は1 << 30 = 0b010000000000000000000000000000000であり、
2147483648
と解釈されますが、1 << 31
は1 << 31 = 0b100000000000000000000000000000000となります。ここで、負の値は 2 の補数形式で表現されることに注意しましょう。最上位ビットが 1 のときは、負の値であることを表します。よって、
1 << 31
は-2147483648
と解釈されます。また、ビット演算は 31 ビットシフトまでしかサポートされていないようです。そのため、1 << 32
も正しく計算できませんでした。ビット演算子 - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Bitwise_OperatorsJavaScript の数値型は大きな値も保持できる 1 ので適当にビットシフトしても大丈夫だろうと考えていましたが、大丈夫ではありませんでした...。
対策
べき乗演算子
**
を使いましょう。2 ** 32
(1 << 32
) も正しく計算できます。for (let i = 1; i <= 32; i++) { console.log(`2 ** ${i}: ${2 ** i}`); }2 ** 1: 2 2 ** 2: 4 2 ** 3: 8 2 ** 4: 16 2 ** 5: 32 2 ** 6: 64 2 ** 7: 128 2 ** 8: 256 2 ** 9: 512 2 ** 10: 1024 2 ** 11: 2048 2 ** 12: 4096 2 ** 13: 8192 2 ** 14: 16384 2 ** 15: 32768 2 ** 16: 65536 2 ** 17: 131072 2 ** 18: 262144 2 ** 19: 524288 2 ** 20: 1048576 2 ** 21: 2097152 2 ** 22: 4194304 2 ** 23: 8388608 2 ** 24: 16777216 2 ** 25: 33554432 2 ** 26: 67108864 2 ** 27: 134217728 2 ** 28: 268435456 2 ** 29: 536870912 2 ** 30: 1073741824 2 ** 31: 2147483648 2 ** 32: 4294967296C++ では?
ちなみに、C++ でも試したのですが、
1 << 31
は JavaScript と同様に-2147483648
になってしまいます!#include <iostream> int main() { long long i = 1 << 31; std::cout << i << std::endl; }-2147483648これは整数リテラル
1
がint
型であり、32 ビット符号付き整数型のためです。きちんとlong long
型であることを明示しましょう。#include <iostream> int main() { long long i = 1 << 31; std::cout << i << std::endl; long long j = 1L << 31; std::cout << j << std::endl; }-2147483648 2147483648
- 投稿日:2019-12-14T20:12:39+09:00
プリザンターのカレンダーを水の中に入れたけど特に何もなかったお話
何か面白いプラグインでもないかな~と探していたら「jquery.ripples.js」という水たまりを表すプラグインを見つけました!
そこで!
プリザンターを水の中に入れてみた!的な感じなのが実現できると思いきや、「フーン」という感じだったのでそのご報告の記事を書こうと思いやーす。
プリザンターとは?
プリザンターとは株式会社インプリムが開発しているオープンソースのWebデータベースです。
データ管理やら業務アプリがマウス操作だけ(ノンプログラミング)でサクッと作れてしまう超有能なプラットフォーム(しかもOSSなので無料という!)なのですな。
オープンソースで脱エクセル!Pleasanter公式サイト
ということで、早速水の中に入れていきましょか!プリザンターを水の中に入れる方法
まずは公式ページより、「jquery.ripples-min.js」を引っ張ってきます。
そしてプリザンターの「Implem.Pleasanter/Scripts/Plugins」にファイルを入れます。
そしてこのカレンダーを~・・・
水の中へIN.csshtml{ background: #33CCFF; } #Grid { background: #33CCFF; } #CalenderBody { background: #33CCFF; }そしてjquery.ripples.jsをスクリプトタグへ設定。
水の中へIN.jsvar script = document.createElement("script"); script.setAttribute("src", "/scripts/plugins/jquery.ripples-min.js"); document.getElementsByTagName("Canvas")[0].appendChild(script);カレンダーに戻ってDevToolsから以下を実行すると・・・
$("html").ripples(); $("#Grid").ripples();フッw
まあ「フーン」って感じですなww
おわりに
以上、プリザンターのカレンダーを水の中に入れたけど特に何もなかったお話でした。
簡単なエフェクトには使えそうな気もするので、頭の片隅には置いておこうと思いヤース。
- 投稿日:2019-12-14T20:12:39+09:00
プリザンターのカレンダーを水の中に入れてみた!
何か面白いプラグインでもないかな~と探していたら「jquery.ripples.js」という水たまりを表すプラグインを見つけました!
そこで!
プリザンターを水の中に入れてみた!的な感じになったので、そのご報告の記事を書こうと思いやーす。
プリザンターとは?
プリザンターとは株式会社インプリムが開発しているオープンソースのWebデータベースです。
データ管理やら業務アプリがマウス操作だけ(ノンプログラミング)でサクッと作れてしまう超有能なプラットフォーム(しかもOSSなので無料という!)なのですな。
オープンソースで脱エクセル!Pleasanter公式サイト
ということで、早速水の中に入れていきましょか!プリザンターを水の中に入れる方法
まずは公式ページより、「jquery.ripples-min.js」を引っ張ってきます。
そしてプリザンターの「Implem.Pleasanter/Scripts/Plugins」にファイルを入れます。
そしてこのカレンダーを~・・・
水の中へIN.csshtml{ background: #33CCFF; } #Grid { background: #33CCFF; } #CalenderBody { background: #33CCFF; }そしてjquery.ripples.jsをスクリプトタグへ設定。
水の中へIN.jsvar script = document.createElement("script"); script.setAttribute("src", "/scripts/plugins/jquery.ripples-min.js"); document.getElementsByTagName("Canvas")[0].appendChild(script);カレンダーに戻ってDevToolsから以下を実行すると・・・
$("html").ripples(); $("#Grid").ripples();おー!水の中にいるような感じになりましたね!
良い感じでございヤース!おわりに
以上、プリザンターのカレンダーを水の中に入れてみたお話でした。
簡単なエフェクトには使えそうな気もするので、頭の片隅には置いておこうと思いヤース。
- 投稿日:2019-12-14T19:25:35+09:00
フォームの表示非表示の切り替え【JavaScript】
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>表示非表示の切り替え</title> </head> <body> <form> <table border="0" cellspacing="0" cellpadding="0"> <tr> <th>利用方法</th> <td> <label><input type="radio" name="entryPlan" value="hoge1" onclick="entryChange1();" checked="checked" />初めて申し込む</label> <label><input type="radio" name="entryPlan" value="hoge2" onclick="entryChange1();" />2度目以降の利用</label> </td> </tr> <!--表示切替--> <tr id="firstBox"> <th>紹介者</th> <td><input type="text"/> <p>紹介された方のお名前を入力してください。 </p> </td> </tr> <!-- 表示非表示切替--> <tr id="secondBox"> <th>会員番号</th> <td><input type="text"/> <p>会員番号を入力してください</p> </td> </tr> </table> </form> <!-- 表示非表示切替 --> <div id="firstNotice">特典:初めての方は30%オフ!</div> <script src="js/main11.js"></script> </body> </html>function entryChange1() { radio = document.getElementsByName('entryPlan') if (radio[0].checked) { //フォーム document.getElementById('firstBox').style.display = ""; document.getElementById('secondBox').style.display = "none"; //特典 document.getElementById('firstNotice').style.display = ""; } else if (radio[1].checked) { //フォーム document.getElementById('firstBox').style.display = "none"; document.getElementById('secondBox').style.display = ""; //特典 document.getElementById('firstNotice').style.display = "none"; } } //オンロードさせ、リロード時に選択を保持 window.onload = entryChange1;
- 投稿日:2019-12-14T18:03:05+09:00
2019年のWebGLデバッグツール状況
これはThree.js Advent Calendar 2019の14日目の記事です。
3年前に以下の記事を読んでWebGL Inspectorを使い、デバッグ処理でWebGL Inspectorに凄くお世話になりました。
WebGLの開発やデバッグに便利なブラウザ拡張機能をつかってみよう
「Three.jsにBaylon.jsみたいなDebug modeある?」という議論を見つけて気になったので、現状のWebGLデバッグツール状況を調べてみました。
Three.jsフォーラム - Does three.js has some “scene debug mode” or plugin? (like babylon.js DebugLayer)
消失したデバッグツール
- 最終更新日(github):2017/02/22
- 去年はまだChromeウェブストアにあったけど消失した
- 開発者のBen VanikさんはGoogleの中の人
- WebGL FundamentalsやThree.js Fundamentalsを書いてるGreggmanさんがPRしまくってた
![]()
FirefoxのShader editor、Canvas debugger
- 最終更新日:Firefox 67でRemove(2019/05/21に消失)
- ユースケースのニーズに対応してない事、バグ修正されずメンテされてない事が原因
- 個人的にはシェーダーを見たり、レンダリング画像のキャプチャして便利なツールだった
- Shader EditorやCanvasキャプチャ機能の人気が下火なので、Safariも消えるような気がします
更新停止したデバッグツール
以下のツールはWebGLサイトによって動いたり動かなかったりします。
- 最終更新日(Chromeウェブストア):2016/10/05
- three.jsの様々な情報を可視化し、リアルタイムに変更できるツール
- ChromeウェブストアのレビューみてもNot workingが多い
- 最終更新日(Chromeウェブストア):2016/04/07
- Shader EditorはVs Codeのプラグインがあるのでその役割を終えたかも
- 開発者の人は最近はWebVRに注力してそう
- 最終更新日(Chromeウェブストア):2015/07/25
- こちらもthree.jsの様々な情報を可視化し、リアルタイムに変更できるツール
- 開発者の人は最近はWebARに注力してそう
更新あるデバッグツール(少し動作が不安定)
- 最終更新日(Chromeウェブストア):2019/04/26
- シェーダーソースやDepth情報、リソースなど様々な情報が見れるツール
- 更新されてるけど動かない事が多い。ChromeウェブストアのレビューみてもNot workingが多い
- 開発者のAaron Moraisさんは決済サービスで有名なstripeの中の人
オススメのデバッグツール
- 最終更新日(Chromeウェブストア):2019/08/26
- MeshやGeometryの情報が見れたり、Materialの情報を変更できる
- three.js r106以上が必要
- 開発者のJordan SantellさんはTHREE.IKライブラリも作ってる人
- 最終更新日(Chromeウェブストア):2019/09/18
- Babylog.jsが提供してるツール。レンダリング結果や頂点情報など様々な情報が見れる
- WebGL2.0も対応してる
- 最終更新日(Chromeウェブストア):2019/11/25
- XRデバイスを使用せずにエミュレートできる。VRカメラをグリグリ動かしたり、コントローラー操作ができる
- 開発者はMozillaのTakahiroさんで日本の人。Three.jsのMMD周りも担当されたと思います。
![]()
- 最終更新日:2019/12/14
- Chromeウェブストアで公開してるものでなく、Babylon.jsの機能
- 使い方はBabylon.jsのプロジェクトで
scene.debugLayer.show()
を1行いれるだけ- スクショや動画が撮れたり、GLB形式に出力できたりマテリアルを変更したり、バウンディングボックスを表示したり強すぎる!
1行コードを入れると左のSCENE EXPLORER、右のINSPECTORが表示される
まとめ
他にもPixiJS devtoolsやA-Frame Inspector、three.js editorなどもありますが、とりあえずオススメしたいのは以上です。
ちなみにiframeだと動作しないものもあり、three.js examples
ではデバッグツールが動作しないのでご注意下さい(Spector.jsやWebXR API Emulatorは動作可能)
調べた事から学ぶ教訓はメンテされないツールは、バグを抱えて淘汰されていく運命を感じました。あとツールのユースケースは大事だと思いました。また、こんなツールもあるよ!など教えて頂けると助かります。
以上、読んで頂きありがとうございました。
- 投稿日:2019-12-14T18:02:44+09:00
レガシーなJavaScriptの構成をwebpack4を使ってまとめる
レガシーなJavaScriptの構成をwebpack4を使ってまとめる
後述するような形で構成しているWebページがあり、今のままだとファイルの更新はまだ良いのですが、新しいファイルの追加を行う場合にHTMLファイルにscriptタグを追加する作業が必要になってしまうため、
せめてJavaScript部分だけでもなるべく工数をかけずに1つのファイルにまとめたいと思い
調べてみました。bundleする以前の構成
- index.html
- assets/js/foo.js
- assets/js/bar.js
- assets/js/hoge.js
- assets/js/main.js
HTML
index.html<html> <head> <!-- 省略 --> </head> <body> <!-- 省略 --> <script src="assets/js/foo.js"></script> <script src="assets/js/bar.js"></script> <script src="assets/js/hoge.js"></script> <script src="assets/js/main.js"></script> </body> </html>JavaScript
assets/js/foo.jsvar Foo = function() { this.name = 'foo' }assets/js/bar.jsvar Bar = function() { this.name = 'bar' }assets/js/hoge.jsvar Hoge = function(foo, bar) { this.call = function() { console.log(foo.name) console.log(bar.name) } }assets/js/main.jsvar hoge = new Hoge(new Foo, new Bar) hoge.call()bundleするために必要なこと
- 各モジュールを
module.exoports
を使用してエクスポートするassets/js/main.js
ファイル内でrequire()
を使用して各モジュールを読み込む各モジュールを
module.exoports
を使用してエクスポートする作成したモジュール名(function名)をexportsする
assets/js/foo.jsvar Foo = function() { // 省略 } + module.exports = Fooassets/js/bar.jsvar Bar = function() { // 省略 } + module.exports = Barassets/js/hoge.jsvar Hoge = function() { // 省略 } + module.exports = Hoge
assets/js/main.js
ファイル内で各モジュールを読み込む
require()
を使用し、<script>
タグで読み込んでいたファイルを読み込むassets/js/main.js+ var Foo = require('./foo.js') + var Bar = require('./bar.js') + var Hoge = require('./hoge.js') var hoge = new Hoge(new Foo, new Bar) hoge.call()HTML内の
<script>
タグを利用して読み込んでいた各モジュール削除し、bundle後のJavaScriptファイルのみ読み込むようにするindex.html<html> <head> <!-- 省略 --> </head> <body> <!-- 省略 --> - <script src="assets/js/foo.js"></script> - <script src="assets/js/bar.js"></script> - <script src="assets/js/hoge.js"></script> - <script src="assets/js/main.js"></script> + <script src="assets/dist/bundle.js"></script> </body> </html>bundleする
コンフィグファイルの編集
webpack.config.js
const path = require('path'); module.exports = { entry: './assets/js/main.js', output: { path: path.resolve(__dirname, 'assets/dist'), filename: 'bundle.js' } };webpackの実行
webpackのインストールとスクリプトの設定
package.json{ // ...省略... "scripts": { "build": "webpack --config webpack.config.js" }, "devDependencies": { "webpack": "^4.41.2" }, "dependencies": { "webpack-cli": "^3.3.10" } }実行結果
$ yarn build yarn run v1.15.2 $ webpack --config webpack.config.js Hash: 0e4f3138f1f49f11d380 Version: webpack 4.41.2 Time: 361ms Built at: 2019-12-14 17:28:28 Asset Size Chunks Chunk Names bundle.js 1.16 KiB 0 [emitted] main Entrypoint main = bundle.js [0] ./assets/js/main.js 142 bytes {0} [built] [1] ./assets/js/foo.js 65 bytes {0} [built] [2] ./assets/js/bar.js 65 bytes {0} [built] [3] ./assets/js/hoge.js 138 bytes {0} [built] ✨ Done in 1.98s.以上です。
やり方が間違っていたり、もっと良い方法がありましたらご教示ください。
- 投稿日:2019-12-14T17:44:36+09:00
[JavaScript] 小さな日付ライブラリ"Daty"を作りました
背景
JavaScriptには、日付・時刻・タイムゾーンまで扱えるDateというものが存在します。
ライブラリ等が無くても使えるのでサクッと日付をゴニョりたいときには便利です。ただ少し複雑な操作をしようとすると、その特殊な挙動によって無駄に消耗することがあると思います。
特に筆者は、Vue.jsを使った日付選択UIを実装した際、割と面倒なことになりました。そこで、Dateの恩恵を受けつつも、合理的な振る舞いとシンプルなインターフェースを持った、日付のみを表現するclassを実装することにしました。
多分これは車輪の再開発だと思いますが。
Dateの挙動(日付オーバーフロー)
node> d = new Date(2000, 0, 31) > d.toLocaleString() '2000-1-31 00:00:00' > d.setMonth(d.getMonth() + 1) > d.toLocaleString() '2000-3-2 00:00:00'"2000年1月31日"で初期化したDateオブジェクトの"月"をインクリメントすると、2月ではなく、"3月2日"になるというものです。
これは少し考えれば分かりますが、
- 2000-1-31 (初期値)
- 2000-2-31 (月をインクリメント)
- 2000-3-2 (2000年2月は29日までのため、2月31日相当となる3月2日として解釈される)
このようなものと考えられるでしょう。
Dateの挙動(NaN)
Dateオブジェクトをインスタンス化する際、各パラメータ(年月日...)に相当する引数に
NaN
を与えることが可能です。例外は発生しません。
その場合、Date全体で "Invalid Date" という扱いになり、各パラメータを個別に制御できません。
setDate()
等が動かなったり、setFullYear()
により他のパラメータが強制的に埋められたりします。
そもそも日付を表すのにNaNを使うはずもなく、動作未定義でも仕方のないところです。作りたかった日付選択フォーム
Datepickerのような特殊なものではなく、単純な年・月・日の3つのプルダウン(select要素)からなるフォームを作ろうとしました。
<select> <option value="">年</option> <option value="2000">2000年</option> ... </select> <select> <option value="">月</option> <option value="0">1月</option> ... </select> <select> <option value="">日</option> <option value="1">1日</option> ... </select>ここで達成したい要件は以下の通りです。
- 年月日それぞれ独立して未選択状態を表現する。
- 存在しない日付は”日”プルダウンの選択肢に現れない。
- 月プルダウンから月を選択したとき、存在しない日付となった場合は”日”を丸める。
各種リアクティブ系フレームワークの力を借りてゴニョればゴリ押しで達成できそうな気がしますが、実装は当然複雑になるでしょう。
そこで、上記の要件を満たしたDateの代替クラスを作ってしまえば、Vue.jsやその他のフレームワークにおけるデータバインディングの枠組みの上に合理的に乗っかったUI実装ができるのではないかと考えました。Datyの実装
- 実装 https://github.com/ztrehagem/daty/blob/master/src/daty-core.ts
- ドキュメント(ja) https://github.com/ztrehagem/daty/blob/master/README-ja.md#class-datycore
ドキュメント整備は途中でやる気を失いました。すまん。
クラスDatyCoreは上記3要件を満たすためのミニマルな実装です。以下のようなコンストラクタとプロパティ/メソッドを持ちます。
- コンストラクタ
new DatyCore(year, month, date)
- 年月日パラメータを数値で初期化。未指定の場合はNaN扱い。
new DatyCore(jsDate)
- Dateオブジェクトから初期化。
- プロパティ
year
,month
,date
- 年月日パラメータの取得と代入やインクリメントが可能
hasYear
,hasMonth
,hasDate
(readonly)
- 年月日パラメータがNaNかどうか
endOfMonth
(readonly)
hasYear && hasMonth ? 月末の日付 : 31
jsDate
1
- 内部で持っているDateオブジェクトへの参照(不変)。代入した場合はコンストラクタと同様。
- メソッド
clearYear()
,clearMonth()
,clearDate()
- 各パラメータにNaNを代入するのと等しい。
Note: DatyCoreを拡張したDatyというクラスも用意してありますが、本記事の執筆時点ではまだ暫定的な実装なので非推奨としておきます。
Datyの利用
あまり説明はいらないかもしれませんが、
year
,month
,date
を双方向バインディングさせ、加えてendOfMonth
を使って"日"プルダウンの内容を可変させれば良いということになります。また、DatyCoreは単なるclassですので、これを継承して新たな機能を持たせることを検討してみてください。
ヘルパーメソッドを用意すればview層でも使えますし、パースやシリアライズ用メソッドを用意すればAPI層まで持っていくことが可能かと思います。
ここで取得できるDateオブジェクトは直接使わずに、Dateオブジェクトを新たに生成するメソッド
toDate()
等を実装することを推奨します。 ↩
- 投稿日:2019-12-14T17:40:26+09:00
初めてのJavaScript~ペンの色・太さが変更できるお絵かきアプリを作ったよ~
Webアプリを作りたいなと思いながらなんとなーく先延ばしにしてたところ
初心者向けのJavaScript,HTML,CSSを使ったワークショップを見つけたので参加してみました。
その時作ったお絵かきアプリに機能を追加してペンの太さ・ペンの色の変更・画面の全消しができるようにしてます.動作環境
あんまりよくわかってないけどCodeSandboxにsign inしてVanillaを使って作りました笑
おそらく…
・CodeSandbox
・Vanilla
・HTML
・JavaScript
・CSS
があればできるんだと思います.
違ってたらすみません.やること
1.HTMLで部品を作ります
今回はお絵かきする部分とボタンを作成しました.
<!DOCTYPE html> <html> <head> <title>Parcel Sandbox</title> <meta charset="UTF-8" /> </head> <body> <div id="app"></div> <!--お絵かきする画面を消すボタン--> <div> <button id="pen-ss">極細</button> <button id="pen-s">細</button> <button id="pen-m">中</button> <button id="pen-l">太</button> <button id="pen-ll">極太</button> </div> <!--お絵かきするところ--> <div> <canvas id="draw-area" width="400px" height="400px" style="border: 1px solid #000000" > </canvas> </div> <!--色を変えるボタン--> <div> <button id="color-red">赤</button> <button id="color-blue">青</button> <button id="color-green">緑</button> <button id="color-black">黒</button> <button id="eraser">消しゴム</button> <button id="clear-button">全消し</button> </div> <script src="src/index.js"></script> </body> </html>これでは動かないのでJavaScriptさんに動かしてもらいましょう
2.JavaScriptの登場
特にどうってこともないのでコード載せます
import "./styles.css"; const canvas = document.querySelector("#draw-area"); const context = canvas.getContext("2d"); canvas.addEventListener("mousemove", event => { draw(event.layerX, event.layerY); }); canvas.addEventListener("touchmove", event => { draw(event.layerX, event.layerY); }); //パソコンでクリックしてる間だけ描けるようにした機能 canvas.addEventListener("mousedown", () => { context.beginPath(); isDrag = true; }); canvas.addEventListener("mouseup", () => { context.closePath(); isDrag = false; }); //スマホで描けるようにする機能 canvas.addEventListener("touchstart", () => { context.beginPath(); isDrag = true; }); canvas.addEventListener("touchend", () => { context.closePath(); isDrag = false; }); //お絵かきするところをきれいにする機能 const clearButton = document.querySelector("#clear-button"); clearButton.addEventListener("click", () => { context.clearRect(0, 0, canvas.width, canvas.height); }); //ペンの色を変える機能 const colorRed = document.querySelector("#color-red"); colorRed.addEventListener("click", () => { context.strokeStyle = "red"; }); const colorBlue = document.querySelector("#color-blue"); colorBlue.addEventListener("click", () => { context.strokeStyle = "blue"; }); const colorGreen = document.querySelector("#color-green"); colorGreen.addEventListener("click", () => { context.strokeStyle = "green"; }); const colorBlack = document.querySelector("#color-black"); colorBlack.addEventListener("click", () => { context.strokeStyle = "black"; }); //消しゴムの機能 const eraser = document.querySelector("#eraser"); eraser.addEventListener("click", () => { context.strokeStyle = "white"; }); //ぺんの太さを変える機能 const penSS = document.querySelector("#pen-ss"); penSS.addEventListener("click", () => { context.lineWidth = 1; }); const penS = document.querySelector("#pen-s"); penS.addEventListener("click", () => { context.lineWidth = 5; }); const penM = document.querySelector("#pen-m"); penM.addEventListener("click", () => { context.lineWidth = 10; }); const penL = document.querySelector("#pen-l"); penL.addEventListener("click", () => { context.lineWidth = 15; }); const penLL = document.querySelector("#pen-ll"); penLL.addEventListener("click", () => { context.lineWidth = 20; }); let isDrag = false; //線をかく機能 function draw(x, y) { if (!isDrag) { return; } context.lineTo(x, y); context.stroke(); }ここまでできればとりあえず動きます
デザインにも凝りたいって方はCSSをいじってください3.CSSでも書きますか
今回はこだわりがなかったのでとりあえず実装しました程度です
/*お絵かきするところからはみ出た部分を隠してみた*/ body { overflow: hidden; }完成
できました!
一応完成したやつのURL載せときます
パソコンでもスマホでも動くようにしました.
Androidで動くかはわかんないです
完成版
- 投稿日:2019-12-14T16:52:37+09:00
年末まで毎日webサイトを作り続ける大学生 〜57日目 JavaScriptのクロージャーとプロトタイプを学ぶ〜
はじめに
こんにちは!@70days_jsです。
クロージャーとプロトタイプを学びました。
57日目。(2019/12/14)
よろしくお願いします。サイトURL
https://sin2cos21.github.io/day57.html
やったこと
中でクロージャーとプロトタイプを使っています。
html
<body> <div id="text">count数:</div> <input type="button" id="button" value="button" /> <div id="object">object Method:</div> </body>javascript
//クロージャー function displayText() { let text = document.getElementById("text"); let count = 0; return function() { count++; text.innerHTML += "<br>" + count; }; } let count1 = displayText(); let count2 = displayText(); count1(); count1(); count2(); count2(); count2(); //____________プロトタイプ___________ let button = document.getElementById("button"); let object = document.getElementById("object"); button.addEventListener("click", createMyObject); function createMyObject() { let userName = prompt(); if (!userName) { userName = "nanashi"; } function Human(name) { this.name = name; } Human.prototype.greet = function() { object.innerHTML += "<br>greet: Hello!" + this.name; }; Human.prototype.sayName = function() { object.innerHTML += "<br>sayName: I' am " + this.name; }; let user = new Human(userName); user.greet(); user.sayName(); }クロージャー
まずクロージャーの方ですが、これは以下のような形をしています。
function displayText() {
let count = 0;
return function() {count++;}
}関数displayText()のなかに変数countと、無名関数があります。
その後、関数を変数に入れています。↓let count1 = displayText();
let count2 = displayText();
count1();//1
count1();//2
count2();//1
count2();//2
count2();//3こうすることで、countの数はcount1とcount2で別々にカウントされます。
関数displayText()のなかに変数countと、無名関数を入れて操作することで、外部からcountは操作されずに、count1とcount2を使って別々にcountを増やしていくことができます。個人的にクロージャーとは、「自分を囲むスコープ内で宣言された変数に参照できる関数」である、
と解釈しています。
ちょっとまだ完璧に理解できた自信がないので、もし間違った認識でしたらご指摘よろしくお願いいたします。
プロトタイプ
まず前提として、「JavaScriptにおいてオブジェクトとは、連想配列である」という認識です。
さらに、オブジェクトの生成方法にクラスタイプベースと、プロトタイプベースの二種類があるという認識です。それで、JavaScriptはプロトタイプベースの方に属します。
以下の部分がコンストラクタです。
function Human(name) {
this.name = name;
}newを使いオブジェクトを生成しています。
let user = new Human(userName);
メソッドの追加は以下のように行います。
Human.prototype.greet = function() {...};
追加したメソッドはオブジェクトを格納している変数を経由して使えます。↓
user.greet();
感想
低迷中...
飛躍の前のしゃがみこみだと信じて頑張ろう。最後まで読んでいただきありがとうございます。明日も投稿しますのでよろしくお願いします。
参考
参考にさせていただきました。ありがとうございます。
- 投稿日:2019-12-14T16:44:54+09:00
人類にコードレビューは早すぎた〜わんにゃん大集合〜
クソアプリアドベントカレンダー2019の14日目です。
コードレビューって難しい☠️
みなさん
コードレビューちゃんとできてますか?
人格攻撃しないっていうけど本当にできてますか?
貴方のレビューで悲しんだレビュイーは本当にいませんか?
レビュイーによって指摘内容が変わりませんか?
レビューを素直に受け止められてますか?
その日の気分やレビュワーによって受け取り方変わりませんか?人格攻撃じゃない真っ当なレビューでも辛いときもある?
僕は豆腐メンタルなので人格攻撃じゃない真っ当なレビューでも
「あ、オレ全然ダメじゃん…つら…」
ってなることが時々あります?レビュイーによって指摘内容が変わる
強い人のプルリクを見るときは
「あ、この人がこう作ってるのは別パターンより良いってことなんだな」
みたいな忖度、ちょっとしちゃいます。
なぜそうしてるのかは聞くくらいで終わったり。
にんげんだもの。コードレビューは人類には早すぎた
ここまで話せばみなさんも気づいたかもしれない。
そう。
人類にコードレビューは早すぎたのだ。動物さんたちにやってもらおう
自分以外のレビュワー、レビュイーが動物になるChrome拡張機能『プルリクエスト忍者』を作りました。
やったね!動物さんたちなら安心だね!
動物になら優しくなれますよね?
やりましたね。これで全て解決です。
もう人間はいりません。実装
Chrome拡張はブラウザ上で実行したいJsさえ書ければ簡単に実装、公開できます。
今回書いたのはこれくらいcontent.js// アドレスバー横の拡張機能ボタンを押された時のリスナー用意 chrome.extension.onMessage.addListener(function(request, sender, sendResponse) { if (request == 'toggle') { toggleNinja(); } }); ('use strict'); let isHide = false; // 登場する動物さんたち const dummyNames = [ '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?' ]; const currentUserIconUrl = $('.Header-link .avatar')[0].src; const currentUserName = $('.header-nav-current-user strong')[0].textContent; let iconUrls = []; let userNames = []; // ページ上にあるユーザー名とアイコンのURLを取得 $('img.avatar[alt^="@"]').each((_, element) => { // @削除 const userName = element.alt.slice(1); if ( !userName || currentUserName == userName || userNames.indexOf(userName) >= 0 ) { return; } iconUrls.push(element.src); userNames.push(userName); }); toggleNinja(); // ユーザー名とアイコンを動物さんにチェンジ!さよなら人類! function toggleNinja() { isHide = !isHide; if (isHide) { userNames.forEach((userName, index) => { $(`[href$='/${userName}']`).each((_, element) => { if ( element.childElementCount > 0 && element.firstElementChild.tagName == 'IMG' ) { const lastChild = element.lastElementChild; if (lastChild.className == 'dummnyName') { lastChild.style.display = 'inline'; } else { const nameDom = document.createElement('span'); nameDom.textContent = dummyNames[index]; nameDom.className = 'dummnyName'; element.appendChild(nameDom); } element.firstElementChild.style.display = 'none'; } else { element.dataset.originalText = element.text; element.text = dummyNames[index]; } }); }); } else { userNames.forEach((userName, _) => { $(`[href$='/${userName}']`).each((_, element) => { if ( element.childElementCount > 0 && element.firstElementChild.tagName == 'IMG' ) { element.firstElementChild.style.display = ''; element.lastElementChild.style.display = 'none'; } else { element.text = element.dataset.originalText; } }); }); } }その他設定などはリポジトリを参照してください
akinov/pull-request-ninja: Hide Assignees & Reviewers for GitHub反省
これで全て解決はしたのですが少しだけ反省点があります。
機能的にクソアプリ
読み込み時に一瞬元アイコンと真名が見えてしまいます。
英霊としてはこれは致命傷。
宝具がバレてしまいます。アプリ名がイケてない
以前、Facebookメッセンジャーのメッセ相手の名前とアイコンを隠すChrome拡張機能『Messenger Ninja』を作っていたので
「何かを隠す、隠れるなら忍者だろ!」
とプルリク忍者という拡張名にしたけど
『プルリクエスト動物園』とかのほうが絶対可愛かった…
反省…?
- 投稿日:2019-12-14T16:36:10+09:00
DECK.GLを使ってGoogleMapタイムラインをビジュアライズしてみよう!#2
前回に引き続き、DECK.GLを使ってGoogleMapのタイムラインをビジュアル化してみよう!の第2弾の記事です。
前回はPCにReactとDECK.GLの環境構築まで終わりました。
今回は実際にコーディングに入っていきます!不要なファイルを削除しよう!
前回、
create-react-app
コマンドを使ってReactの環境を作成しましたが、まずはシンプルに作りたいので不要なファイルを削除したいと思います。これがデフォルトの状態。
なんかいっぱいファイルがありますね。
src
ディレクトリの中からindex.js
を残してそれ以外は削除しちゃいましょう。
こんな感じ
index.jsファイル
初期の
index.js
はこんな感じだと思います。index.js (デフォルト)import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render(<App />, document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();ここにも不要な記述がいっぱいあるので削除しちゃいましょう。
削除後のindex.js
は下記のような感じ。index.js (修正後)import React from "react"; import ReactDOM from "react-dom"; import App from "./App.jsx"; ReactDOM.render(<App />, document.getElementById("root"));これから
App.jsx
にコードを書いていきます。
※拡張子は.js
でもいいのですが、.jsx
にするとvscodeにReactのアイコンが表示されて気分が上がるので、私はいつもこっちで書いてますw
こんな感じ
App.jsxに基本的なコードを書いていこう!
それでは、ようやくコーディングに入ります!
まずは、index.js
と同じ階層にApp.jsx
を作成しましょう。
できたら、下記のコードを
App.jsx
に書きます。(まだDECK.GLは全く関係ないです)
まずは基本的なReactのコードを書いて動くのを確認してみましょう!App.jsximport React from "react"; export default class App extends React.Component { render() { return <div>Hello, DECK.GL!!</div>; } }動作確認は
yarn start
のコマンドを実行するだけでOK!
create-react-app
で作成したプロジェクトは、babelやwebpack、ESLintの設定が済んでおり、このコマンドを実行だけでサーバを起動しhttp://localhost:3000
でアクセスできるようになります。動いたー!
まとめ
今回もまだDECK.GLに触れることができませんでしたが、次回こそは本題に入れるといいな・・・!
- 投稿日:2019-12-14T16:27:30+09:00
きちんと更新されるブラウザキャッシュの活用方法
この記事は 弁護士ドットコム Advent Calendar 2019 16日目の記事です。
はじめに
弁護士ドットコム というサービスのUXエンジニアをやっている白井と申します。
日々ユーザー体験を向上させるべく様々な開発を行っていますが、Webサイトの速度改善もその1つです。
今回はその一環で実施した、ブラウザキャッシュ戦略の再設計の事例についてご紹介します。
対象リソース
この記事では、以下のようなリポジトリで管理されているリソースを主な対象としています。
- JavaScript, JSX, TypeScriptなどのスクリプト類
- CSSなどのスタイルシート類
- 画像やフォント類
これらは頻繁に変更される傾向にあるため、ブラウザキャッシュを適切に制御する必要があります。
注意深く設計しないと、以下のような問題が発生する場合があります。
ブラウザキャッシュのよくある問題
問題① ユーザーのブラウザに古いキャッシュが残ったままになる
内容に変更があったにも関わらず、ユーザーのブラウザにキャッシュが残ってしまい、古いコンテンツが表示されてしまったり、ページ自体が正常に表示されなくなってしまったりするケースです。
また、自分の開発環境では正常に表示されてしまい、なかなか問題に気づけなかった…といった方も多いのではないでしょうか。
問題② ブラウザキャッシュが全く活用できていない
逆に、適切な設定をしていなかったために、ブラウザキャッシュを全く活用できていないケースも多いと思います。
リソースに変更がないにも関わらず毎回リソースを取得させてしまうのは、ユーザーにとってもWebサーバにとっても好ましい状況ではありません。
この記事では、これらの問題が発生しないブラウザキャッシュ制御方法について検討していきたいと思います。
今回の改善結果
弊社では Speed Index のユーザによる実測値をサイト表示速度のKPIとして採用しています。
Speed Index はWebページのファーストビューが表示されるまでにかかる時間を計測したものです。
対応を行った結果、 Speed Index に約14%の改善 が見られました。
本記事でご紹介する手法によってどのくらいサイト表示速度が改善されるかは、以下のようなWebサイトの特性に依存するため、一概には言えません。
- リソースのサイズや分割単位
- ユーザーの新規・再訪問比率
しかし、何らかの改善のヒントになれば幸いです。
そもそもブラウザキャッシュとは?
ご存知のとおり、Webページを閲覧する際にはHTML文書だけではなく、JavaScript・CSS・画像といった様々なリソースをサーバからネットワーク経由で取得する必要があります。
一度訪れたサイトであれば、リソースの大半は前回の訪問時から変更されていないことが多いと考えられます。その場合、変更されたリソースに限って取得すればWebサイトをより高速に表示することができます。
そのために、前回取得したリソースをブラウザ側で記憶しておく仕組みがブラウザキャッシュです。
ブラウザキャッシュを制御する仕組み
サーバ側からブラウザキャッシュを制御するためには、以下の2つの仕組みが必要です。
- ブラウザにリソースをキャッシュさせる仕組み
- ブラウザにキャッシュを破棄させる仕組み
①ブラウザにリソースをキャッシュさせる仕組み
特定のHTTPヘッダを付与することで、ブラウザに対し、そのリソースがキャッシュ可能か指示することができます。
代表的なHTTPヘッダには以下があります。
Cache-Control
Expires
Age
ETag
Last-Modified
詳しくは RFC 7234 を参照してください。
これらの指示内容に応じて、ブラウザはリソースをキャッシュして良いか判断します。そして、次の訪問時には以下のいずれかの挙動を示します。
- ブラウザキャッシュを使用し、リソースの再取得を行わない
- リクエスト自体が発生しませんので、最も高速な挙動となります。
- リソースに更新があるかどうか確認するリクエストを送り、更新がなければキャッシュを使用する
If-Modified-Since
やIf-None-Match
ヘッダ付きのリクエストを送信します。- Webサーバはそれらの内容から更新の有無を判断し、更新がある場合のみ最新のリソースを含めたレスポンスを返します。更新がなければ、
304 Not Modified
レスポンスを返します。- ブラウザキャッシュを使用せず、必ずリソースを再取得する
- リクエストとリソースの取得が必ず発生しますので、最も遅い挙動です。
Webサイトの表示を高速化するには 1. を目指すことになりますが、この場合 リクエスト自体が発生しないため、リソースに更新が発生してもブラウザが再取得してくれない という問題が生じます。
それを解消するのが次に紹介する仕組みです。
②ブラウザにキャッシュを破棄させる仕組み
リソースが更新された場合に最新のものを取得してもらうためには、キャッシュ破棄 (Cache Busting)と呼ばれる手法が必要です。
ブラウザキャッシュの制御においては、リソースを参照する側のURLを変更するという手法が一般的です。
たとえば、HTML内からCSSファイルへの参照があった場合、そのURLを最新のものに更新します。 (当然、HTMLの方はキャッシュさせないようにしておきます)
以下の例ではURLの一部が
version=1
からversion=2
に変わっているため、ブラウザに新しいリソースとして認識され、再度取得されるようになります。<link rel="stylesheet" href="/css/main.css?version=1">↓
<link rel="stylesheet" href="/css/main.css?version=2">この手法には、細かく分けると以下のような種類があります。
- 実際に新しいファイルを作る
- リソースが更新される度に新しいファイル名 (URL) を発行する
- ファイルは同じだが、名前の一部を変える
- ファイル名の一部に何らかのパラメータを付与する
この「何らかのパラメータ」は通称キャッシュバスター (Cache Buster) とも呼ばれます。
キャッシュバスターを付与する方法には、以下のバリエーションがあります。
キャッシュバスターの「生成元」によるバリエーション
何に基づいてキャッシュバスターを生成するかについてはいくつかの方法があり、それぞれメリット・デメリットがあります。
キャッシュバスターの生成元 キャッシュ破棄のタイミング キャッシュが破棄される単位 説明 ・ビルド日時
・デプロイ日時デプロイ時 全てのリソース Webサーバが複数台ある場合に同一時刻になるように注意が必要 ・アプリケーションのバージョン番号
・GitのコミットIDデプロイ時 全てのリソース ・ファイルの更新日時 リソース更新時 該当リソースのみ デプロイ時のファイルコピーで更新日時が書き換わらないように注意が必要 ・ファイル内容のハッシュ値 リソース更新時 該当リソースのみ ファイルごとにハッシュ値の計算が必要 詳しくは後述しますが、弊社では 「ファイル内容のハッシュ値」と「GitのコミットID」を併用する方式を採用しました。
キャッシュバスターを付与する「場所」によるバリエーション
一方、キャッシュバスターをリソースURLのどこに付与するかについても、以下の方法があります。
ファイル名に付与する
例:image-4d300b8f57bff89d8f4f87b5cc1b3de5.png
クエリパラメータとして付与する
例:
image.png?version=4d300b8f57bff89d8f4f87b5cc1b3de5
ファイル名を毎回変更する場合、ローカル開発時に不要なファイルが蓄積していく原因にもなりますので、Webpack処理の際に先にクリーンナップ処理を行うなどの配慮が必要になります。
なお、デプロイごとにキャッシュバスターを更新する場合は、まとめてディレクトリ名に付与してしまうという方法もあります。
弁護士ドットコムにおけるブラウザキャッシュ戦略
一般論が続いたため、そろそろお腹いっぱいかと思いますが、ようやく本題です!
以前はどうだったか?
改善前の弁護士ドットコムでは、以下のような設計になっていました。
ブラウザにリソースをキャッシュさせる仕組み
HTTPヘッダ (
Expires
ヘッダ) により、1年間のキャッシュを許可していました。ブラウザにキャッシュを破棄させる仕組み
ファイル名にデプロイ日時ベースのキャッシュバスターをクエリパラメータとして付与していました。
- CSSファイルから
url()
関数で読み込まれる画像のURLについては、デプロイ時のWebpack処理によってタイムスタンプが付与されていました- その他のJavaScript・CSS・一部の画像については、PHPの関数により、デプロイ日時のタイムスタンプが付与されていました
しかし、この設計・実装には以下の課題がありました。
課題① デプロイの度にブラウザキャッシュが破棄されてしまう
弊社ではデプロイが完全に自動化されているため、エンジニア・デザイナーを問わず、Slackから簡単にデプロイを行うことができます。
そのため、デプロイは1日に数回、多い時には数十回行われる場合があります。しかし、上記の設計における大きな課題の1つが、デプロイする度にすべてのリソースのブラウザキャッシュが破棄されてしまうということでした。
大半のリソースには変更がないにもかかわらず、その度にブラウザキャッシュが破棄されてしまっていました。
課題② サーバごとにキャッシュバスターが異なる
なんと、デプロイ日時ベースのキャッシュバスターを付けていたはずが、サーバごとにキャッシュバスターの値が異なっていました。
現在のデプロイ方式では、複数台あるアプリケーションサーバ間で、デプロイ日時に数秒〜数十秒のズレが生じてしまっています。
デプロイ日時ベースのキャッシュバスターを各サーバで生成していたため、このような現象が起こってしまっていました。
その結果、せっかくブラウザキャッシュを持っていても、前回と異なるサーバにアクセスするだけでキャッシュが破棄されてしまうという問題が起こっていました。
ついやってしまいがちかとは思いますが、なかなか気付きにくいタイプの問題かと思います。
どのように改善したか?
この仕組みを以下のように改善しました。
ブラウザにリソースをキャッシュさせる仕組み
従来通り、HTTPヘッダにより1年間のキャッシュを許可する設定を踏襲しました。
ブラウザにキャッシュを破棄させる仕組み
以下のロジックを採用しました。隙を生じぬ二段構え。
① 特定のリソースにはコンテンツベースのキャッシュバスターを付与する
具体的には、Webpackで処理する以下のリソースが対象となります。
- JavaScriptファイル (
.js
,.jsx
)- CSSファイル (
.css
,.scss
)- CSSファイルの
url()
関数で読み込まれる画像やフォント類 (.png
,.woff2
など)これらのリソースの内容をハッシュ化した文字列をキャッシュバスターとして付与します。
こうすることで、内容が変更されるまでブラウザキャッシュが破棄されないようになります。
② それ以外のリソースにはコミットIDをキャッシュバスターとして付与する
一方、全てのリソースをWebpackで処理するわけではありません。例えば、以下のようなリソースは対象外です。
- (CSSから読み込まれない) 画像やフォント類
- Webpackを通さない、生のJavaScriptやCSSファイル類
これらについては、デプロイ時のGitのコミットIDをキャッシュバスターとして付与します。
デプロイごとにブラウザキャッシュが破棄されてしまいますが、重要なリソースの多くは何らかの形でWebpackで処理されていたため、問題なしと判断しました。
ようやく実装編
さて、前述の仕組みをどのように実装していったのか見ていきましょう。
Rails やLaravel などのフレームワークでは似たような仕組みが用意されていますが、諸事情により、今回は自前で仕組みを作っていきました。
概要
今回の実装は大きく2つの部分から構成されています。
① Webpackで
manifest.json
ファイルを出力する
manifest.json
とは、サーバ内のリソースのパスと、キャッシュバスター付きのリソースのパスの対応関係を記述したJSONファイルです。{ "/js/lawyer/mypage/pc.bundle.js": "/js/lawyer/mypage/pc.bundle.js?b91d959a9994e8c2fc51" }上記の例では、
?b91d959a9994e8c2fc51
というクエリパラメータが付与されています。② PHPコードから
manifest.json
を読み込む一方、HTML出力時にはPHPの関数で
manifest.json
ファイルを読み込み、キャッシュバスター付きのURLに変換を行います。<script src="<?= asset('/js/lawyer/mypage/pc.bundle.js') ?>"></script>↓
<script src="/js/lawyer/mypage/pc.bundle.js?b91d959a9994e8c2fc51"></script>指定されたパスが
manifest.json
に存在しない場合は、一律でGitのコミットIDを付与します。なお、PHPに限らず、任意のサーバサイド言語で同様の仕組みが実現可能かと思います。
Webpack側の実装
CSSの
manifest.json
ファイルを出力するためのWebpack設定は以下のようなイメージになりました。 (一部パスなどを変更しているため、そのままでは動作しません)import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import ManifestPlugin from 'webpack-manifest-plugin'; export const config = { entry: entries.css, output: { path: '../service/css' }, module: { rules: [ { test: /\.scss$/, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { sourceMap: true, }, }, { loader: 'postcss-loader', options: { sourceMap: true, }, }, { loader: 'resolve-url-loader', options: { sourceMap: true, root: '../service', }, }, { loader: 'sass-loader', options: { sourceMap: true, outputStyle: 'compressed', }, }, ], }, { test: /\.(gif|png|jpe?g|eot|wof|woff|woff2|ttf|svg)$/, use: [ { loader: 'file-loader', options: { name: '[path][name].[ext]?[hash]', // CSSのurl()で読み込まれる画像やフォント類にコンテンツのハッシュ値を付与する context: '../service', publicPath: '/', emitFile: false, }, }, ], }, ], }, plugins: [ // manifest.json ファイルを出力する new ManifestPlugin({ basePath: '/css/', publicPath: '/css/', filter: fileDesc => fileDesc.isInitial, }), new MiniCssExtractPlugin({ filename: '[name].css?[contenthash]', // 出力されるCSSのファイル名にコンテンツのハッシュ値を付与する }), ], };これは以下のような処理を行なっています。
- CSSから読み込まれている画像やフォント類のハッシュ値を計算する
- resolve-url-loader を使い、CSSの
url()
関数で参照されている画像やフォント類をWebpackで処理できるようにします。- それらの画像やフォント類を file-loader で処理し、名前にコンテンツのハッシュ値を付与します。
- このプラグインは本来、ファイルシステム上にファイルを出力するプラグインですが、
emitFile: false
を指定し、出力を行わない設定にしています。- このようにすることで、ファイル名の変更のみを行うことができます。
- CSSファイルのハッシュ値を計算する
- mini-css-extract-plugin を使ってCSSを個別のファイルとして出力します。
- その際、
filename
オプションに[contenthash]
を指定し、ファイル名にコンテンツのハッシュ値を付与します。manifest.json
ファイルを出力する
- webpack-manifest-plugin を利用し、目的の
manifest.json
ファイルを出力します。ここで、1.の処理を忘れないようにご注意ください。CSSから読み込まれる画像やフォント類にも適切にキャッシュバスターを付与する必要があります。
これを忘れてしまうと、以下のような問題が発生します。
- キャッシュバスターを付与していなかったために、それらの画像やフォント類はブラウザキャッシュが破棄されないままになってしまった
- タイムスタンプベースのキャッシュバスターを付与していたため、CSSファイルの内容が毎回変わってしまい、コンテンツのハッシュ値が変わってしまった
PHP側の実装
そろそろ読むのも疲れてきたでしょうから省略します。 (書くのも疲れてきました)
manifest.json
ファイルを読み、指定されたパスに対応するものがあればそれを返し、なければGitのコミットIDを付与するだけです。まとめ
今回は弁護士ドットコムにおけるブラウザキャッシュ活用の改善事例についてご紹介しました。
- ブラウザキャッシュを活用すると、2回目以降のサイト表示速度を高速化できる
- それには以下の2つの仕組みが必要である
- ブラウザにリソースをキャッシュさせる仕組み
- ブラウザにキャッシュを破棄させる仕組み
- リソースのコンテンツのハッシュ値をキャッシュバスターとして付与すると、最も長い期間ブラウザキャッシュを保持させることができる
よい良いユーザー体験を提供しようとする、全てのエンジニアの皆様の参考になれば幸いです。
おまけ:設計時にほかに検討したこと
Q. ローカルでの開発時はどうするの?
DIを使ってキャッシュバスターを生成するクラスを差し替え、メソッドが呼び出された時刻のタイムスタンプを返すようにしています。
こうすることで、常にキャッシュが破棄される状態になります。
Q. CDNを導入すれば良いのでは?
CDNで高速化されるのは1回目の読み込みであり、今回の改善対象は2回目以降の読み込みです。補完的な関係にあると考えています。
Q. Webpackのハッシュアルゴリズムに依存してしまうと、将来ほかのツールに移行できないのでは?
ファイルコンテンツベースのハッシュアルゴリズムであれば何でも良い (新旧のツールで同じハッシュ値になる必要がない) ため、問題ないと考えました。
もし、乗り換え先のツールにそういった機能がない場合でも、性能の劣化を許容すれば良さそうです。
Q. 各ファイルの更新時刻をWebサーバ (Apache/Nginx) に認識させて、
Last-Modified
ヘッダを出力すると簡単なのでは?それが実現できればこんなに複雑な仕組みは必要なくなるため、実装上はシンプルになります。
しかし、以下の理由から今回考えた方式を採用しました。
If-Modified-Since
ヘッダ付きリクエストの発生
Last-Modified
ヘッダを出力させた場合、ブラウザはキャッシュが最新か確認するためにIf-Modified-Since
ヘッダ付きのリクエストを送信します。(更新がない場合はレスポンスのボディが含まれないとはいえ) 通信が発生するよりはしない方が高速にブラウジングできるはずです。
デプロイ処理の運用コストの問題
デプロイ時のファイルコピーにより、ファイルの更新時刻が変わってしまうことはよくあります。
また、複数台のAppサーバ間で同一ファイルの更新時刻がズレてしまうとさらにカオスになります。 (弊社で実際にやらかしてしまっていたのは前述のとおりです。。)これらの問題が発生しないようなデプロイ処理を作り込むことは可能ですが、アプリケーション開発者が普段意識しないレイヤですので、運用の中で意図せず壊れてしまう可能性があります。
- 投稿日:2019-12-14T16:23:00+09:00
WebBluetoothで遊んでみる~ARを添えて~
これは、株式会社ACCESS Advent Calendar 2019 の15日の記事です。
本記事では、私が好きな無線通信のひとつである BLE (Bluetooth Low Energy) で、最近?まともに使えるようになった Web Bluetooth で遊んでみたことをつらつら書いていきます。
もう一度 Web Bluetooth に触ってみる
Web Bluetooth は簡単にいうと ブラウザ上で BLE 通信をする機能です。
仕様は WebBluetooth CG が公開しています。
https://webbluetoothcg.github.io/web-bluetooth/実は昨年の2018 春頃に一度使ったことがあるのですが、通信が途切れやすい、Windows 機器では使えない等々の問題があり、遠ざかっていました。
そして月日がながれ、現在(2019/12/11)においては、上記の問題が解決されたということで使ってみることにしました。
とりあえず動くものを作ってみる
とりあえず動くものを作ろうということで、OpenGL のお勉強でお世話になった あのティーポット(Utah teapot) をブラウザ上でくるくる回してみます。
あのティーポット (引用: wikipedia) デバイスは、9軸計測できるセンサ と BLE がオンボードでついているマイコンを組み合わせたものを用意しました。
加速度の値を BLE で飛ばすようにいい感じにコーディングして完成。
デバイスの傾きでティーポットをくるくる回そうということです。
BLE デバイス ブラウザ上でのデバイスの接続や、ティーポットを表示させるために HTML / JavaScirpt / CSS でいい感じにコーディングします。
GoogleChrome さんが Github にサンプルを上げていた(1)ので簡単に作ることができました。ティーポットは、Three.js で表示しています。
余談ですが、Three.js は公式を見たほうが一番わかりやすいと思います。
ブログ等の使ってみた記事は、バージョンが古いものを使用して実現していることが多いです。
現バージョンでは動かないトラブルが結構な確率であります。
そのため公式をみたほうが良いです。余談はここまでとして、とりあえず動くものが完成しました。
動かしてみます。
デバイスと接続して
ティーポットをくるくる windows で動くようになってる!
通信も途切れない!
すげー!Web Bluetooth × AR
Web Bluetooth めっちゃ動く すげー!! となったところで、これまた個人的に好きな AR ( Augmented Reality ) と組み合わせたらどうなるかなぁということで実験してみました。
せっかくなのでマーカーレス AR に挑戦。
8th Wall でやってみることにしました。
WebBluetooth × ティーポット × 8th Wall をいい感じに組み合わせます。
作ってみた
いい感じに組み合わせて爆誕したものがARのティーポットをデバイスで動かすやつ(タイトル未定)です。
デバイスの傾きでティーポットの座標を動かしています。
遊んでみた もっと凝ったのを作りたかったのですが時間の都合上ここまでになりました。
まとめ
Web Bluetooth と AR をちょこっと触ってみました。
Web Bluetooth は以前よりもだいぶ改善されており、だいぶ使える印象を持ちました。
これからもウォッチしていきたいと思います。また、はじめてマーカーレス AR に挑戦してみました。
8th wall を使用してみましたが、サンプルも充実しており簡単にマーカーレス AR が実現できることがわかりました。
次は Google さんが出している ARCore をいじってみようと思います。参考
- Notifications Sample, GoogleChrome, https://googlechrome.github.io/samples/web-bluetooth/notifications.html
- three.js - Tap to place, 8thwall, https://github.com/8thwall/web/tree/master/examples/threejs/placeground
明日のアドベントカレンダーは、 @hnishi さんです。
また明日もみてくださいね。じゃんけーんぽーん!
- 投稿日:2019-12-14T16:05:59+09:00
memo jsでrequireする時.defaultが必要になる場合がある
この場合
require('object').default
でないと取れないexport default = object;これなら
require('object')
で取れるmodule.exports = object;es6の
import ... from ...
でやればexports.default
でも.default
なしで取れるからそっち使おう!
- 投稿日:2019-12-14T15:04:11+09:00
初学者に「覚える必要はない」という言葉がけは危険
プログラミング塾に通っています
こんにちは。
まーこです。10月から塾で学ぶプログラミングを始めました。(今までは個人勉強で学習していましたが、成長がないので塾に通うことになりました。)
そこで、学ぶなかで、新しい概念や用語や態度に出会うのですが、そこでの考察をメモします。
元々、ファインアーティストとして活動していたため、文章に感覚的な部分があります。
プログラマーのように、ロジカルに書くことができないのですが、努力いたしますのでよろしくお願いします。「覚える必要はない」というけれども
覚える必要がないと経験者の方はおっしゃるのですが、これ初心者の人にとって意味を取り間違える危険性がある気がしています。
「暗記ではなく理解しなさい」というけれども
「理解するって何を理解するの・・・?」
これが私が感じでいた勉強での出来事です。
初学の人にとっては何を理解すればいいか分からないのではないかと思います。ましてや理解する内容はどれが理解するべき事項なのか分からない。。。
そのため、私は自分が何を分かっていなくて、何につまづいているか分析すれば理解できていないところが理解できるのでは???と思って、ググってググってググって・・・。「分からなければググりなさい」というけれども
ググる先にまた分からないことがあって、ググってググって・・・・帰ってこれなくなってしまったんですよね笑
理解するつもりが余計に新しい知識を得て、メタレベルでは理解が深まって行くのですが、先生が期待する表層の”理解すべき内容”に辿りつけなかったんですよね笑
それなら一発で答えを言ってほしい・・・なんて思ったりしていました。(先生の気持ちとしては自分で調べる力をつけてほしいとのことでしたが、初学者には効率が悪すぎると思います。。。日常でスマホを使い検索しているのが習慣化されているはずなので、現代人にとっては検索する能力は弱くないスキルだと思うのですよね。ただ、検索した先の決断能力は別。でもこの能力、全てを一度勉強した後、自然に身についてくる能力では?そんな気がします。)←完全愚痴
覚える必要はあるのでは?
覚える必要はある気がします。
「何を覚えるのか」が実は経験者の方は伝えたいのではないかというのが私の推測です。では、何を覚えさせたいのか
このコマンドが「何ができる」を把握(覚え)させたい
2ヶ月半プログラミングの塾に通ってやっとここに行き着きました。2ヶ月半もかかりました。
実はこの間2人の先生から教わっています。「ふーん」程度でよいということだそうです。
(理解と言われると「なるほど!」だと思うじゃないですか!!!?)←完全愚痴しかし、暗記は最強なのではないか
最終的には暗記は最強なような気がしています。せっかちの人は特に。生産性はすごく上がる気がします。毎度毎度調べるの面倒です。
そこで、頭文字だけ暗記すればよいEmmetを導入しました。これがまた便利!!
そしてvscodeエディタの推測でコードを表示してくれる機能。これがまた便利!!その他、記憶の補助となる拡張ツールを入れました。「覚えてもよい」
覚えるのは決して悪いことではない気がします。コードを描き速さが上がると勉強のモチベーションが上がりますし。
これは個人の能力や性格によるのかもしれません。私はせっかちだし、飽きっぽいし、面倒くさがりなので、覚えた方が効率がよかったです。
覚えて量をこなすなかで、なんとなく分かってきた時間の方が、量をこなさず、理解に費やすためにググったりコードを見直していた時間と比べるととても有意義でした。もし、「覚える必要はない」という一般化されたプログラミング学習お作法言葉にひっかかりを感じている方は、どうか試してみてください。
学習メモをここまでご覧頂きましてありがとうございました!
- 投稿日:2019-12-14T14:42:06+09:00
【raindrops.js】プリザンターに水滴入れようとしたけど特に何もなかったお話。
プリザンターと!水滴のお話や!
raindrops.jsという!jQueryのプラグインを使うと出来るらしいんや!
とりうぁえず!やってみるで!
プリザンターと水滴
まずは!raindrops.jsを公式から引っ張ってくるんや!
場所は!ここや!
水滴の元
これを!「Implem.Pleasanter/Scrpts/Plugins」に入れれ!
そして!それだけではダメやねん!
scriptsタグを追加や!
以下のスクリプトを追加するんや!
AmenoSekai.jsvar script = document.createElement("script"); script.setAttribute("src", "/scripts/plugins/raindrops.js"); document.getElementsByTagName("head")[0].appendChild(script);やり方が!分からん人は!以下の公式ページを見るんや!
Pleasanterユーザーマニュアル-スクリプトの追加方法
出力先はどこでもええ!
入れたらとりあうぇず一覧画面に戻るんや!raindrops.jsの!使い方は!
$('セレクタ').raindrops();でOKや!
$('#MainCommands').raindrops();・・・なんやこれ。
$('.grid-row').raindrops();・・・なんやこれ。。。
なんで一覧が波々せえへんのや。。。挫折や。。。
そや!画像とかなら波々させられるんちゃうか!
ガントチャートを波々させるんや!
もうダメや~。。。
おわりや~。。。おわりに
まあとりあえず!これが!プリザンターに水滴入れようとしたけど特に何もなかったお話や!
次は!次こそは!自由自在に波々させられるようにするんや!
さらばや!
- 投稿日:2019-12-14T13:32:38+09:00
法令APIを利用したリサーチツールを自作してみた【SmartRoppo】
1. はじめに 2. リーガルテックっぽいプロダクトを作ってみた 3. SmartRoppoのコンセプト 4. SmartRoppoの主な機能・特長 5. なぜ自分で作ろうと思ったのか? 6. 今後の課題 7. おわりに1. はじめに
この記事は、じゃんく(@jank_2525)さんからバトンを受け継ぎ、「法務系 Advent Calendar 20191」の14日目エントリーとして執筆しています。
皆さんのエントリー、どれも個性あふれる素敵な内容で、毎日大変興味深く拝見しています。
2. リーガルテックっぽいプロダクトを作ってみた
さて、突然ですが、リーガルテック的なプロダクトを作ってみたので、このエントリーをもってβ版を公開させていただきます。【SmartRoppo】といいます。
SmartRoppo -法令データベースを、もっと賢く-
https://smartroppo.com/SmartRoppo/index.html
ユーザー登録など面倒なことは一切不要で、どなたでも利用できます。
※スマホ・タブレットには対応していないので、PC(ブラウザはできればChrome)からご利用ください。3. SmartRoppoのコンセプト
SmartRoppoのコンセプトは、「法令のUI/UXをアップデートする」というものです(壮大)。
私自身は金融系の案件を扱うことが多いのですが、金商法や銀行法をはじめとした金融系の法令は、極めて複雑で難解なものが多いです。
それゆえ、恥ずかしながら、弁護士になって5年が経とうとしている今でも、「こんな条文があったのか!」と気づいたり、危うく読み方を間違えそうになることがあります(私だけではないはず…)。
ただ、こうした複雑・難解な法令は、裏を返せば、様々なケースを想定して具体的・詳細に書かれている(解釈の幅が狭い)ということでもあります。実際、業界の法規制に精通したクライアント様からの相談であっても、条文の内容だけでズバリ回答できてしまうケースもそれなりにあったりします。
つまり、法令の内容を「正確に」読み解くことができれば、それだけで求めている情報にたどり着けることも少なくないのです。というか、まずそれができないと解釈も何もないですよね。実務書等の文献に当たることももちろん大事ですが、最終的には原典である条文を確認することが不可欠でしょう。
あるイベントでお話させていただいた際のスライドを抜粋します。
こうした課題をテクノロジーの力で解決することがSmartRoppoのコンセプトです。
4. SmartRoppoの主な機能・特長
SmartRoppoの機能・特長は、現時点では主に3つです。
(たぶん、文章で説明するよりも、実際に使っていただくか、上記のデモ動画を見ていただいた方が早いです。)① 法令APIからのデータ取得機能(+法令のリアルタイム検索機能) ② 下位規則の自動レファレンス機能 ③ かっこ書きのハイライト機能① 法令APIからのデータ取得機能(+法令のリアルタイム検索機能)
これまでの電子六法アプリは、自前でデータを保有するデータベース方式が多かったように思います。
これに対し、SmartRoppoでは、総務省が公開している「法令API2」を活用する方式にしました。
つまり、基本的にはアプリ側でデータを保有せず、アクセスの都度、最新のXMLデータをe-govから取得するという設計にしています。API方式には、(e-govが適時に更新される限り)法令データが常に最新の状態に保たれ、アプリ側のメンテナンス(法改正等の反映作業)が基本的に不要である3という点にメリットがあります。
ただ、都度データを取得してくるという仕組みゆえ、表示速度はデータベース方式に比べて劣っているかもしれません。② 下位規則の自動レファレンス機能
・ 下位規則の重要性
複雑な法令の内容を正確に理解するには、各条文で参照されている「政令」や「内閣府令」といった下位規則を併せて読むことが不可欠です。
ただ、こうした各条文に紐付く下位規則をいちいち特定・確認するのは骨が折れる作業です。紙の分厚い法令集を、行ったり来たりしながら(栞代わりにペンを挟んだりして)確認したことがある方も多いのではないでしょうか。そこで、SmartRoppoでは、こうした下位規則を自動で取得し、参照元の法令と一覧表示する機能(自動レファレンス機能)を実装しました。
・ これまでになかった?
これまでの電子六法アプリでも、手作業で(ゆえに限られた法令・条文に対して)レファレンスを付けていると思われるものは少数ながらありました。
一方、少なくとも私の知る限り、「自動で」(ゆえに全法令に対して)レファレンスを付ける機能を備えたものは、これまで見られなかったように思います(違っていたらすみません)。とはいえ、SmartRoppoもまだ精度はイマイチですし、現状全ての法令には対応できていないので4、このあたりは順次改善していきたいと思います。
・ なぜ難しいのか? ー「逆参照」の壁
なぜ下位規則の自動レファレンス機能が実装されないのか、私は以前から疑問に思っていました。なんとなく、割と簡単に実装できそうな気もします。
しかし、実際に作ってみてよく分かりました。実はこれ、法令の構造的な問題ゆえに、技術的にはそれなりにチャレンジング(というか超面倒くさい)なのです。
特に、API方式と両立させようとすると難しく、いろんな方法を試した結果、それなりの精度でワークしそうなものは、今の自分には一つしか見つけられませんでした。
難しい理由は、端的にいうと、下位規則のレファレンスは、「参照元には参照先を特定する情報がなく、参照先にのみ参照元を特定する情報がある5。そうすると、まずは参照先を特定する必要があるが、その参照先をどうやって特定するのかが問題。」という「タマゴとニワトリ」のような構造になっているからです。
私はこれを「逆参照」と呼んでいます。
人間が作業する際は、「ここでの『内閣府令』は『○○府令』のことを指していて、だいたいこの辺に書いてあるはず」といったアタリを付け、その周辺の条文をチェックするといったやり方を採ることが多いように思います。
しかし、こういった「肌感覚」的なものをプログラムに書こうとすると、なかなか難しいわけです。
③ かっこ書きのハイライト機能
これは見たまんまですが、かっこ書きをその階層ごとに色分けする機能です。かっこ書きが長かったり、かっこが多重になっている条文が読みやすくなります。
ただ、この多重かっこ(ネスト)の処理がなかなかくせ者で、現状バグが出てしまっています。
原因は分かっているのですが6、時間が足りずまだ対応できていません。すみません。。5. なぜ自分で作ろうと思ったのか?
SmartRoppoの開発にあたっては、コーディングはもちろん、設計や(イケてない)デザインも含め、とりあえず自分一人で手を動かしてやってみました7。
私は自他ともに認める「超ド文系」の人間です。技術的なバックグラウンドは何一つありません。プログラミング言語も全く触ったことがなく、「HTML…?
」というレベルからのスタートでした。
そんな状態から、どうして自分で作ろう/作れると思い至ったのか。自分でもよく分からない(というか忘れた)のですが、以下のような想いがあったように思います。
こうした経緯で開発を始めたわけですが、弁護士業務の合間を縫って、あーでもないこーでもないと考えながらコードを書いては消し、無限エラー地獄と戦うのは、思っていた以上にハードでした。
特に前述の「逆参照」機能については、正直、めちゃくちゃ悩みました11。いろんな技術を試しては壊してを繰り返す中で、頭がハゲそうになりました12。
でも、エンジニア志望でもない自分がプログラミングを学ぶのであれば、何か形にしないとやる意味がない(Deploy or Die13)。少なくとも自分自身が実務で使いたいと思えるものでなければ作る意味もない。そう思っていたので、気合いで乗り切りました。
基本は怠惰なダメ人間ですが、やると決めたことは何だかんだやるんです。結果、なんとか形になって少しほっとしています(まだまだ課題は山積していますが)。
開発の詳しい経緯(どうやってプログラミングを勉強したか)や技術面の詳細などについては、(もし興味を持ってくださる方がいれば)別の機会に書きたいと思っています。技術面の話は、扱っているデータが「法令そのもの」であるだけに、法務パーソンの皆様にも興味を持っていただけるのではないかと思います。
6. 今後の課題
とりあえず公開はしてみたものの、時間や技術力の制約もあり、まだまだ十分な性能・機能を備えているとはいえません。
今後の課題や追加予定の機能をざっと書き出してみると、こんな感じでしょうか(順不同)。
- 処理速度の向上(API方式と自動レファレンス機能のせいですがそれにしてもちょっと遅い)
- 自動レファレンス機能(逆参照)の精度向上・対象拡大
- 条数検索・用語検索機能(UIはありますがこれも時間が足りず)
- 順参照の自動レファレンス機能(逆参照の前にこっちをやるべきでした)
- 法令外国語訳DBの自動参照機能14
- 判例・パブコメ等の関連資料のレファレンス機能
- メモ・ブックマーク等のパーソナライズ機能
- デザインの改修(絶望的にセンスがないので誰か助けてください…)
- XMLデータ化されていない法令(裁判所規則、告示等)のカバー
- 正規表現による検索機能
- その他細かいバグの修正
7. おわりに
まだまだ未熟なプロダクトですが、是非一度触ってみていただければと思います。
そして、どんな内容でも結構ですので、ご意見ご要望などいただけるととても嬉しいです(@lawyer_alpaca)。長々とお付き合いいただきありがとうございました!
次は10ru(@oga10ru)さんです!よろしくお願いします!
法令APIに対応していない法令もあります(データ容量が大きい等)。とはいえ、それほど数は多くないので、定期的に改正の有無をチェックし、手動で最新のXMLデータに差し替えることで対応可能です。 ↩
レファレンスの処理自体はプログラムで自動的に行っているのですが、プログラムにデータを渡す際の前処理的な作業を一部手作業でで行っているためです。公開までに時間が足りませんでした。。 ↩
例外として、参照元にも参照先にも両者を「明確に」紐付ける情報がないケースもあります。 ↩
いわゆる「読み替え規定」がかっこの対応関係を崩しているためです。例えば、「この場合、●条の『▲▲▲・・・(◆◆◆・・・』という規定は、『△△△・・・(◇◇◇・・・』と読み替えるものとする。」という条文があった場合、「開きかっこ」に対応する「閉じかっこ」がデータ上は存在しないことになります。これにより、かっこの対応関係にズレが生じ、ネストが意図せず深く(あるいは浅く)なってしまうのです。 ↩
もちろん、一般的なフレームワークやライブラリは使用しています。その意味で、「粉からカレーを作る」「牛を育てるところからハンバーグを作る」といったレベルでスクラッチしたわけではありません。念のため。 ↩
代表的な例として、Holmesの笹原さん(@kenta_holmes)、GVA TECH(AI-CON)の山本さん(@gvashunyamamoto)、LegalForceの角田さん(@NT_LegalForce)、Legal Technology(Legal Library)の二木さん(@LEGALLIBRARY_F)などが挙げられます。ゼロから事業を立ち上げられたこれらの方々を、私は心の底からリスペクトしています。自分には到底マネできないので。。 ↩
とはいえ、リーガルテックについては、起業する人、開発する人、使ってみる人、情報発信する人、静観する人など、関わり方は人それぞれだと思います。個々の技術やプロダクトに対する評価も人それぞれだと思いますし、それでいいと思います。 ↩
このときは、プログラミングを学べば技術のことが分かるようになると思っていました。しかし、今はちょっと違って、両者は(相互補完的な関係にはあるものの)基本的には別個のものであり、それぞれ勉強する必要があると考えています。法務に例えるならば、「契約書を何百通レビューしたところで、それだけでは民商法のことが分かるようにはならない。逆もまた然り。両方とも、それはそれとして勉強しなければならない。」という感覚でしょうか。 ↩
全然関係ないですが、バチェラー3面白かったですね。 ↩
もともと髪の毛は多い方なので、まだ大丈夫だと思います。 ↩
日本法令外国語訳データベースシステムで公開されている英訳法令は現在約750(全法令の1割弱)にとどまりますが、重要法令を中心に今後3年間で大幅な拡充が予定されているようです。http://www.moj.go.jp/housei/hourei-shiryou-hanrei/housei03_00013.html ↩
- 投稿日:2019-12-14T13:32:04+09:00
React まとめ②
Reactアプリケーションはelement(要素)という構成ブロックにより構成されています。
コンポーネントは要素によって構成されたものです。
コンポーネントを使うことにより、UIを部品に分割し分離させることができます。コンポーネントはpropsと呼ばれる任意の値を受け取り、画面上に表示すべきものを返すReaxt要素を返します。
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }これはpropsで受け取ったnameを表示させる関数コンポーネントです。
class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }これは等価なコードです、classコンポーネントを使う際はextends React.Componentを記載して下さい。
またファイル冒頭にimport React, { Component } from 'react';を書くことも必要です。function Welcome(props) { return <h1>Hello, {props.name}</h1>; } const element = <Welcome name="Sara" />; ReactDOM.render( element, document.getElementById('root') );このコードではelementと定義された要素を引数としてReactDOM.renderが呼び出されます。
そのあとelementに代入されたwelcomeコンポーネントを呼び出し、その時propsとしてname=’Sara’が渡されます。
その後ReactDOMがHello, Sara
になるようDOMを更新します。Reactでは
<welcome />はDOMタグ <Welcome />はコンポーネント と認識されることを覚えておきましょう。class App extends React.Component { render() { return ( <div> <h1>Hello World</h1> </div> ); } }これはclassコンポーネントですが、特徴として内部にプライベート状態(state)をもたせたり、
生成状況によって呼び出されるライフサイクルメソッドを持っています。
主に親コンポーネントとして使用し関数コンポーネントで作った子コンポーネントに、保持している値を伝える親玉のような存在と言えるでしょう。const App = (props) => { return ( <div> <h1>Hello World</h1> </div> <h2>Hello React</h2> ); };これは同レベルのReact Elementがあるためエラーになります。
const App = (props) => { return ( <div> <h1>Hello World</h1> <h2>Hello React</h2> </div> ); };とdivタグで囲ってあげましょう。
// 親コンポーネント class Parent extends React.Component { //superは必須です constructor(props) { super(props); this.state = { value1: 'foo', value2: [ 'bar', 'baz' ], }; } render() { return ( <div> <Child1 data={this.state.value1} /> <Child2 data={this.state.value2} /> </div> ); } } // Functionalコンポーネントで受け取る場合 const Child1 = (props) => ( <div> {props.data} </div> ); // Classコンポーネントで受け取る場合 class Child2 extends React.Component { render() { return ( <div> {this.props.data} </div> ); } }親で独自の名前を付けて子に流していく流れですね。
クラスコンポーネント間でも受け取ることができます。viewで発生したイベントハンドラはクラスコンポーネントのメソッドで設定して下さい。
その際はキャメルケースで書く必要があります。
またコールバック関数として実行する際にthisが機能するようconstructor()内でthisをbindしておく必要があります。
これはjavascriptにまつわる話題です。ReactDom.render( <App />, document.querySelector('.content') );これは冒頭紹介したコードです、深掘りしていきましょう。
親コンポーネント(App.js)のrender()メソッドで返されたレンダリングの実態は仮想DOMと呼ばれるものです。仮想DOMではブラウザで表示できないので生のDOMに変換する必要があるのですが、そこでこのコードが活躍します。
第一引数に親コンポーネント、第二引数にhtmlで表示・挿入する部分を渡しています。
- 投稿日:2019-12-14T13:00:06+09:00
ゴルフ特有の動きの特徴点をオブジェクト検出で捉える
前回、オブジェクト検出で追加学習を行い、ゴルファーとゴルフボールだけを追い掛けるモデルを作成しました。
しかし、ゴルフのフォームのチェックをAIを使って実施するには、画像からゴルフスイングの動きを捉えることが必要です。良いスイングとは何か?
誰に聞いても明確な答えが得られませんでした。人それぞれ体格や目標が違うから、見てみないとわからない、と。ではなぜ「レッスンプロ」という人が存在するのか?レッスンプロは人のゴルフスイングを見て、無意識に形を認識・分類し、独自の視点で良い点や矯正方法が思い浮かぶのだと思います。
これと同じことをAIに実行させるには、画像に写っている人物からゴルフのスイングを検出し、特徴点を把握して動きを捉える必要があります。さらに、おおよそこういう動きや角度なら「良いスイング」「ダメなスイング」と定義付ける必要もあります。
特徴点を検出する
TensorflowJSには、Posenetという姿勢推定AIがありますが、ゴルフの場合、どうしてもクラブヘッドの位置を捉えなければならないため、また、特に後方からスイング撮影するとPosenetでは検出出来ない部位があり、Posenetは使えません。
なので、後方からのアドレス画像に限定し、頭、腰、右足、グリップ、クラブヘッド、ボールを認識出来るよう、オブジェクト検出で追加学習を行いました。
学習用アノテーション部位は以下の通りです。いろいろなトッププロ100名の後方からのアドレス画像を使ってアノテーション化し、学習を行いました。
精度が上がらない原因
特徴点が悪いのか、そもそも部位の選択の仕方が悪いのか、精度が50%をなかなか超えません。
いろいろ試行錯誤の結果、オブジェクト検出をやっている人には常識なのかもしれませんが、対象となる人物画像やアノテーション部位の縦横サイズ大きさに大きく影響します。
つまり、人物引き気味の画像とアップ気味の画像が混在するアノテーションでは別のものとして認識するようで、なるべく画像の縦横幅を同じにし、映っている人物の大きさが同じになるよう選別し、学習実行すると90%を超える精度が出始めました。エポック数は14,000でした。学習に使っていない後方画像もしっかり認識しました。
以下の画像は、未学習写真です。
TensorflowJSに変換
ここまで出来上がったところで、モデルをJavascriptに変換します。前回苦慮した部分です。
Pythonよりも慣れていること、即WEBサービスとして使ってもらえること、がJavascriptを利用する理由です。特徴点
四角形で囲められれば、あとは中心点を求めて点を描画するだけです。線で繋げてみます。
Javascript・Canvasの操作はお手の物です。
さらに100名分の特徴点を取得し、見て取れる角度の平均や標準偏差・相関を算出し、閾を設定すれば、良いアドレスのモデルは出来そうな気がします。
さらにこれら座標から算出出来るバランス感、分類を行い、自分のアドレスと比較、矯正方法を示唆してくれそうです。
今後連続写真を使えば、スイング全体の良し悪し判断出来るかも。
- 投稿日:2019-12-14T10:58:41+09:00
axiosのヘッダーのconfigでちょっとハマった
某APIを試しててaxiosのconfig指定をミスってたので、自戒の意味を込めて残しておきます。
完全に自分用メモっぽいやつです。
ミスったコードなど
//省略 class Hoge { constructor() { //省略 } //ミスった方 methodA(IMAGE_PATH){ const file = fs.createReadStream(IMAGE_PATH); const form = new FormData(); form.append('image', file); form.append('entrance', 'detection'); const config = { 'X-ClovaOCR-Service-ID': this.SERVICE_ID, ...form.getHeaders() }; return axios.post(this.URL.RECOGNITION, form, config) } //うまくいった方 methodB(IMAGE_PATH){ const file = fs.createReadStream(IMAGE_PATH); const form = new FormData(); form.append('image', file); form.append('entrance', 'detection'); const config = { headers: { 'X-ClovaOCR-Service-ID': this.SERVICE_ID, ...form.getHeaders(), } } return axios.post(this.URL.RECOGNITION, form, config) } } const hoge = new Hoge(); hoge.methodA(); // エラー hoge.methodB(); // 成功axiosのconfigミス
慣れてつかってるうちにconfig=headersっぽいイメージで使ってたみたい。
通常configの中には他にもmethod指定やbodyデータなども入ってくるのでconfig.headers
な指定にしないとですね。const config = { 'X-ClovaOCR-Service-ID': this.SERVICE_ID, ...form.getHeaders() };const config = { headers: { 'X-ClovaOCR-Service-ID': this.SERVICE_ID, ...form.getHeaders(), } }
- 投稿日:2019-12-14T10:44:11+09:00
use-reducer-asyncの紹介
はじめに
先日の記事でreact-trackedの紹介をしました。react-trackedはreactのcontextとhooksを使ったglobal stateのライブラリです。プリミティブな機能を提供しており、必要に応じて拡張(custom hooks化など)して使えます。非同期処理も可能なのですが、ドキュメントサイトでチュートリアルを作成する際には、オススメの非同期処理なども記述する必要があると思いました。react-trackedはreduxなどと違って外部にstoreを持たないため、非同期処理もReactの範囲でやることになります。custom hooksで非同期処理用のcallbackを作る方法もありますが、今回は、useReducerを拡張して非同期処理を記述するライブラリを紹介します。
react-trackedと合わせて使うことを想定して説明しましたが、特にreact-tracked専用のライブラリではなく、普通のReact stateでも使うことができます。
use-reducer-async
リポジトリはこちらです。
https://github.com/dai-shi/use-reducer-async
コードはとても小さいです。ライブラリにしなくても自分でcustom hooksを書いても同じことができます。このライブラリは、その機能より、コーディングパターンを提案することに意味があります。
使い方
一例として、データ取得を行うケースを実装してみたいと思います。
ライブラリをインポートします。一つのhookだけです。
import { useReducerAsync } from 'use-reducer-async';初期ステートを定義します。personというデータを取得するケースを想定しています。
const initialState = { loading: false, person: null, };reducerを定義します。
const reducer = (state, action) => { switch (action.type) { case 'FETCH_STARTED': return { ...state, loading: true }; case 'FETCH_FINISHED': return { ...state, loading: false, person: action.person }; default: throw new Error('no such action type'); } };非同期処理を行うハンドラーを定義します。今回は一つだけです。
const asyncActionHandlers = { START_FETCH: (dispatch, getState) => async (action) => { dispatch({ type: 'FETCH_STARTED' }); const data = await fetchData(); dispatch({ type: 'FETCH_FINISEHD', person: data }); }, };最後にコンポーネントでhookを使います。
const Component = () => { const [state, dispatch] = useReducerAsync(reducer, initialState, asyncActionHandlers); return ( <div> <button type="button" onClick={() => dispatch({ type: 'START_FETCH' })}>Fetch Person</button> {state.loading && 'Loading...'} {state.person && <Person person={state.person} />} </div> ); };これにより非同期処理が実行されて、ステートが更新されます。
ポイント
今回の例では使いませんでしたが、本ライブラリはTypeScriptの型定義があるため、それを使うとより便利です。
ところで、Reduxでは公式ドキュメントでredux-thunkを推奨していますが、型付けが難しいことが課題です。use-reducer-asyncやcustom hooksで非同期処理をする場合は、その課題が軽減されます。ちなみに、本ライブラリのハンドラー定義はredux-thunkのAPIとそっくりです。
また、Reduxの場合はexternal storeを使うため、Concurrent Modeの対応が限定的になる部分があります。use-reducer-asyncはReactのstateを使うだけなので、Concurrent Modeに素直に対応できます。
おわりに
このライブラリを作ろうと思ったきっかけは、use-saga-reducerを見つけたからです。redux-sagaにExternal APIというものがあることを知らず、Redux以外で使えることは驚きでした。redux-sagaはとても強力でいいとは思うのですが1、ドキュメントサイトのチュートリアルで紹介するには向かず、シンプルな非同期処理のみを対象にした本ライブラリを開発しました。
Reduxの公式ドキュメントでも、redux-sagaは標準採用ではなく、redux-thunkが標準になっています。 ↩
- 投稿日:2019-12-14T10:05:58+09:00
A tampermonkey script
// ==UserScript== // @name filter event by aj club name // @namespace http://tampermonkey.net/ // @version 0.1 // @require http://code.jquery.com/jquery-3.3.1.min.js // @description add a club index in the head then you could get by event list by club! // @author Matt // @match https://www.audax-japan.org/* // @grant none // ==/UserScript== (function () { 'use strict'; function onlyUnique(value, index, self) { return self.indexOf(value) === index; } function onlyShowme(event) { $("table > tbody > tr").each(function () { $(this).show(); if ($(this).find("td:nth-child(3)").text() !== event.target.text) { $(this).hide(); } }); } var club_list = []; var only_unique = []; $("table > tbody > tr > td:nth-child(3)").each(function () { club_list.push($(this).text()); only_unique = club_list.filter(onlyUnique).filter(function (e) { return e !== '' }).filter(function (e) { return e !== '主催' }); }); for (var i = 0; i < only_unique.length; i++) { $("#post-21795 > div").prepend("<a id='" + only_unique[i] + "' style='padding:3px;cursor: context-menu;'>" + only_unique[i] + "</a>"); } for (var j = 0; j < only_unique.length; j++) { $("#" + only_unique[j]).click(onlyShowme); } })();
- 投稿日:2019-12-14T04:14:29+09:00
Google翻訳とGrammarlyを1つのアプリにして英作文セルフレビューを効率化する
オフショア先とやりとりをしたり英語でコメントやでコミットログを書くときは、自信がないのでGrammarlyでの文法チェックとGoogle翻訳での対訳でセルレビューをする習慣をつけています。
とはいえ、Webブラウザで毎回GrammarlyとGoogle翻訳を開いたりタブを探したりするのが面倒くさくなっていました。
1画面に2つのWebサービスをまとめたい
どうせ同じ英文を両方のサービスにペーストするなら、1つの画面で両方いい感じに開けて、ブラウザとは別のアプリケーションとしてまとまっていたら便利そうです。(下図)
真っ先に考えたのはiframeを使って複数のWebサイトを1HTML内に埋め込むことでしたが、いずれのサービスもiframeからの使用は禁止されていました。だったらElectronでできるんじゃないかということで、作ってみることにしました。
(開発環境は macOS Mojave、Node.js 12.2.0、Electron 7.1.1 です。)
今のElectronは1つのウィンドウに複数のブラウザを表示できる
最近のElectronのAPIでは、Experimentalですが複数のWebページの表示を同時に配置できます。昔いじったときはそんな機能なかったので、GitHubを調べてみました。どうやら2018年の12月頃に実装されたみたいです。
ひとつのBrowserWindowの中に、複数のBrowserViewを表示させることができます。BrowserViewの中はブラウザのタブのようにそれぞれ他と独立して動いているので、関係ないURLを1画面に表示することも問題ありません。これならやりたいことができますね!
JavaScriptでラフに実装
それでは実装してみます。まずは適当なディレクトリ内に
package.json
を作成します。mkdir qiita-test cd qiita-test npm init -y npm i -D electron touch index.jsこの
index.js
はElectronのメインプロセスの実装になります。今回のアプリは既存のWebサイトを表示するだけなので、作るのはこれだけです!とりあえずゴリゴリ書いたのが以下のコードになります。
index.jsconst { app, BrowserView, BrowserWindow, screen } = require('electron'); app.on('ready', () => { // デスクトップ領域の大きさを取得 const workArea = screen.getPrimaryDisplay().workArea; // 推奨サイズ const initialWidth = 1300; const initialHeight = 940; // BrowserWindowを新規作成 let win = new BrowserWindow({ width: Math.min(workArea.width, initialWidth), height: Math.min(workArea.height, initialHeight), }); // BrowserWindowを閉じたらアプリ終了 win.on('closed', () => { win = null; app.quit(); }); // レイアウト指定用の定数 const { width, height } = win.getBounds(); const gTransHeight = 300; // Grammaly用のBrowserViewを作成 const grammarly = new BrowserView({ webPreferences: { // nodeの機能を無効化 nodeIntegration: false, } }); // BrowserWindowに追加 win.addBrowserView(grammarly); // 配置する位置とサイズを指定 grammarly.setBounds({ x: 0, y: 0, width: width, height: height - gTransHeight }); // URLを開く grammarly.webContents.loadURL('https://www.grammarly.com/signin?allowUtmParams=true'); // Google翻訳画面用に同じことをする… const gTransView = new BrowserView({ webPreferences: { nodeIntegration: false, } }); win.addBrowserView(gTransView); gTransView.setBounds({x: 0, y: height - gTransHeight, width: width, height: gTransHeight }); gTransView.webContents.loadURL('https://translate.google.com/'); // Google翻訳画面にCSSを挿入してツールバーを隠す gTransView.webContents.on("dom-ready", () => { gTransView.webContents.insertCSS(` /* ヘッダー隠す */ body > header { display: none !important; } /* 境界線入れる */ .frame { border-top: 3px solid black !important; padding-bottom: 66px !important; padding-top: 15px !important; } /* 使わないボタンを隠す */ .input-button-container { display: none !important; } `); }); // リサイズ時に配置位置を再計算 win.on('resize', () => { const { width, height } = win.getBounds(); grammarly.setBounds({x: 0, y: 0, width: width, height: height - gTransHeight }); gTransView.setBounds({x: 0, y: height - gTransHeight, width: width, height: gTransHeight }); }); });補足・解説
BrowserViewの自動リサイズはうまく動かない
BrowserViewには
setAutoResize()
というメソッドがあり、これを使うとBrowserWindowの伸縮に合わせてBrowserViewの大きさも自動的に変えてくれる機能があります。https://electronjs.org/docs/api/browser-view#viewsetautoresizeoptions-experimental
しかし、この機能はまだExperimentalで、今回の複数のBrowserViewを配置する使い方ではレイアウトが崩れるケースもありました。このため、
setAutoResize()
は使わずに、ウィンドウリサイズ時にはそれぞれのBrowserViewでsetBounds()
をやり直す実装にしています。// リサイズ時に配置位置を再計算 win.on('resize', () => { const { width, height } = win.getBounds(); grammarly.setBounds({x: 0, y: 0, width: width, height: height - gTransHeight }); gTransView.setBounds({x: 0, y: height - gTransHeight, width: width, height: gTransHeight }); });表示したWebサイトのCSS上書き
webContents
のinsertCSS()
で、CSSを自由に追加することができます。https://electronjs.org/docs/api/web-contents#contentsinsertcsscss-options
ただし、
insertCSS()
を実行する前にWebサイトの読み込みを待つ必要があるので、
今回の例ではdom-ready
イベントで実行しています。// Google翻訳画面にCSSを挿入してツールバーを隠す gTransView.webContents.on("dom-ready", () => { gTransView.webContents.insertCSS(` /* ヘッダー隠す */ body > header { display: none !important; } /* 境界線入れる */ .frame { border-top: 3px solid black !important; padding-bottom: 66px !important; padding-top: 15px !important; } /* 使わないボタンを隠す */ .input-button-container { display: none !important; } `); });作ったアプリを起動する
とりあえずアプリを起動するにはこのコマンドを叩きます。
npx electron . # ctrl + c で終了
起動しました! 目に見えるアプリができるとテンション上がりますね。
Grammarlyは一度ログインしてしまえば、次回はログイン後の画面から立ち上がるようになります。electron-packagerでのパッケージング
このままだと毎回コマンドを叩かないと起動してくれないので、macOSアプリとしてパッケージ化します。パッケージ化するためのツールもいくつかの種類がありますが、今回はelectron-packagerというツールを使ってみます。
npm i -D electron-packager npx electron-packager . QiitaTest --platform=darwin --arch=x64 --electron-version=7.1.1 --out=./build --asarこれで、QiitaTestという名前のアプリケーションが生成されました! ダブルクリックすれば本当にアプリとして使うことができます。
(ただし、本格的に配布するには署名が必要です。この記事では立ち入りません……。)
TypeScriptで少し丁寧に作り直す
このあとTypeScriptを導入してもうちょっと綺麗に作り直しましたが、アプリの動きは全く同じなので割愛します。
https://github.com/hokkey/translators-web単純だけど便利
作ったアプリはなかなかの便利さで、業務でも重宝しています。
GrammarlyとGoogle翻訳の組み合わせ以外にも、
- Slack, Redmine, GitLabなど、ある1つの案件に絡むWebサービスを1アプリにまとめて表示する
- よく使う辞書サイトを1つのアプリにまとめる
など、アイデア次第でほかにも応用できそうです!
- 投稿日:2019-12-14T04:00:22+09:00
React function compornent(FC)とclass compornentの違い
新しく書き始める時にfunction compornent(FC)とclass compornentで書く違い、書き方を学んだので、memo。
React function compornent(FC)とclass compornentの違い
基本的にはfunction compornent(関数コンポーネント)で書いた方が推奨されている。
使い分けるポイントとしては…
FCは、簡潔に書くことができる
class compornentのrender部分だけで書いてる感じ。
デメリットは状態管理によるrender()制御ができない(制御できることといえば、propsを渡せることだけ)class compornentは状態を管理したい時に使う
・ライフサイクルメソッドを適切に使うことにより、render() を制御することが可能。
・propsはもちろんstateも使うことができる書き方の違い
- function compornent(FC)
import React from "react"; const Test = () => { return ( //処理 <div> <h1>{'FCの書き方!'}</h1> </div> ); }; export default Test;
- class compornent
import React from "react"; class Test2 extends React.Component { //なくてもよい constructor(props) { super(props); this.state = {count: 100}; // stateも使える } // render() メソッドは、クラスコンポーネントで必ず定義しなければならない唯一のメソッドなので必要 render() { return ( //処理 <div>{'class compornentの書き方'}</div> ); } } export default Test2;使うときは、、、
import Test from "./FCTest.index"; import Test2 from "./ClassTest.index"; return ( <Test /> <Test2 /> //これで使える );参考
https://ja.reactjs.org/docs/react-component.html
https://teratail.com/questions/118890
https://www.sambaiz.net/article/225/