- 投稿日:2020-02-18T23:47:58+09:00
RasDashを使ってみた
RasDashは「メモリやCPU使用率、センサの値などを表示するNode.js製ダッシュボード」です。
MOON GIFTさんの「RasDash - Raspberry Pi用のダッシュボード」という記事を見て、面白そうと感じたので、使ってみました。導入
下記の手順で導入。最後の
systemctl enable RasDash.service
は自動起動するために入力しただけなので、実行しなくても問題なし。$ git clone https://github.com/sykeben/RasDash.git ~/RasDash $ cd ~/RasDash/ $ sudo ./install_deps $ sudo ./service_manager install $ sudo ./service_manager start $ sudo systemctl enable RasDash.service表示
http://<Raspberry piのIPアドレス>:5808/dash/0
をブラウザで表示。
各値はリアルタイムに反映される。
ポート変更
デフォルトでは、ポート番号
5808
へ接続している。
~/RasDash/config.json
に記述された数値を変更すれば、他のポート番号に変更できる。{ "port": 5808 }
表示内容変更
ダッシュボードに載せる情報は下記のファイルで変更可能。
表示するボードの順番を修正:~/RasDash/views/pages/dash_0.ejs
さいごに
htmlやjavascript、cssの知識はもちろん、EJSというNode.jsのテンプレートエンジンに関する知識が必要なようです。
RasDashの他の項目をいじれるよう、暇を見つけていろいろ試してみます。
- 投稿日:2020-02-18T23:05:14+09:00
FileListは作れる!~file inputの中身も自由自在~
ふつうに
value
を書くことができない<input type="file">
ですが、ブラウザによっては動的な設定もほぼ自由に行えることが判明しました。
<input type="file">
へ値を設定する入力するものがファイルである以上、
<input type="file">
に対してvalue
は(空にするvalue = ''
を除けば)使えないのですが、別ルートとしてfiles
というプロパティがあります(MDN)。ここには選択したファイルのリスト(multiple
でなければ1つだけ)が入ったFileList
オブジェクトが入っていて、また代入することで設定が可能です。
FileList
って、何?ところが、
files
に代入するものもFileList
オブジェクトでなければならず、代わりにFile
の配列を持ってきてもエラーになります。しかも、new FileList
も動かないため、一見すると他の<input type="file">
から持ってくる、ぐらいの動作しかできないようにも見えます。実際、昔はそうでした。裏口から作る~
DataTransfer
ここで登場するのが
DataTransfer
です。これは、ドラッグアンドドロップで動かしているデータを持つためのオブジェクトです。ファイルを投げ込むような動作も存在する以上、このオブジェクトもfiles
としてFileList
を持っています。そして、どういうわけか、
DataTransfer
はnew
できます。なので、DataTransfer
経由でFileList
をゼロから作ることが可能となります。const dt = new DataTransfer(); dt.items.add(file); const list = dt.files;なお、iOS(とIE)はこの方法に対応していませんので、要注意です(Can I use)。
動作例
<input type="file" multiple>
で一気に選んだファイルを、1つ1つ<input type="file">
に分配するような例を実装しました。See the Pen DataTransfer経由でFilesを作る by Jkr2255 (@jkr2255) on CodePen.
その他
DataTransfer
経由でFileList
の作成が可能となったため、new FileList
ができないことが有害無益となってしまい、これも可能とする動きもあるようです。参考リンク
- 投稿日:2020-02-18T23:04:25+09:00
Amazonで見ている本のタイトルを図書館のサイトで検索するChrome拡張の作り方
概要
Amazonで見ている本のタイトルを図書館のサイトで検索するChrome拡張を作ってみた。
はじめに
ヘビー図書館ユーザーなので、WEB予約を効率化したくて試行錯誤中。ブックマークレットで、効率化しようとしてある程度成功したけれど、ページ遷移を挟んだスクリプトは実行できなかった。なので、Chrome拡張使って試してみることにした。
自分のスキル的には、JavaScriptとHTMLなんとなくならわかる程度。ブックマークレット初心者がWEBサイト入力を自動化するためにやったこと - Qiita
方針
とりあえず、公開はおいといて、自分のために動くものを作る。
Amazonのサイトで気になる本を表示してるときに、Chrome拡張のアイコンを押すと、図書館のサイトで検索した結果を表示するようにしたい。本読むとき、それなりにAmazonのレビューとか評価を気にするタイプなので。やったこと
他の人のマネ
作り方、全然知らないので、調べた。
いくつか見たけど、ややこしそうやった。
一番シンプルそうな、このサイトに書かれてること試してみた。
Chrome拡張の作り方 (超概要) - Qiitaすんなり動いたので、これを真似して作ることにした。
content_scriptsっていうのを使ってる。
特定のサイトを開いたときに動作するスクリプト。manifest.jsonってので、全体的な設定を書いて、JavaScriptのファイルで動作を書いたら良さそう。HTMLとかアイコンはなくても良さそう。
やりたいことするための調査
やりたいのは、Amazonのサイトを開いてるときに、アイコンをクリックしたら動作するイメージ。どうも、アイコンをクリックしたときに動作するpage_actionってのと、バックグラウンドで動作するbackgroundってのを設定したらよさそう。
Chrome拡張の開発方法まとめ その1:概念編 - Qiita
できたもの
試行錯誤して、処理の流れはこんな感じになった。
- バックグラウンドで、クリックを監視
- クリックされたら、Amazonのサイトでタイトルを取得して(
|
でタイトルとか著者名が区切られてる)、タイトルをローカルに保存して、図書館のサイトを開く- 図書館のサイトが開かれたら、ローカルに保存してあるタイトルで検索する。
※ permissionsをエラーとか見ながら、適当に検索して設定した。
※ スクリプト間のデータ受け渡しはchrome.storage.local使った。他にも方法ありそう。
※ page actionのbrowser_styleも、style.jsがどうこうっていうエラーが出たので、適当に設定してみた。ファイルはこれ(↓)で全部。
manifest.json{ "name": "Amazon2KobeLibrary", "version": "1.0.0", "manifest_version": 2, "description": "Amazonで見ている本のタイトルを神戸市図書館のサイトで検索", "background": { "scripts": [ "background.js" ] }, "page_action": { "default_title": "page action", "browser_style": true }, "content_scripts": [ { "matches": [ "https://www.lib.city.kobe.jp/opac/opacs" ], "js": [ "content_lib.js" ] } ], "permissions": [ "storage", "activeTab" ] }background.js(function(){ chrome.tabs.onUpdated.addListener(function(tabId){ chrome.pageAction.show(tabId); }); chrome.pageAction.onClicked.addListener(function(tab){ chrome.tabs.executeScript(null,{ file: "script.js"}); }); })();script.jsvar title = document.title; var title1 = title.split('|'); chrome.storage.local.set({ hold_title: title1[0] }); // alert('タイトル:' + title1[0]); window.open('https://www.lib.city.kobe.jp/opac/opacs')content_lib.jschrome.storage.local.get("hold_title", function (items) { document.forms['search_form'].elements['keywords'].value = items.hold_title; var x = document.getElementsByTagName("form"); var request = document.createElement('input'); request.name = 'btn1'; request.value = '検索'; document.forms['search_form'].appendChild(request); document.forms['search_form'].submit(); });拡張機能として追加
想像以上に簡単やった。
Extensionsを開いて、Load Unpackedをクリックして上記ファイルが入ったフォルダを選択するだけ。たぶん、Developer modeにチェック必要。はじめ、Pack extensionをクリックしてたけど、違った。
こんな感じで登録される。ソースコードを更新したときは、右下の方の更新ボタン押したら勝手に読み込んでくれた。ファイルのパスを覚えてるっぽい。たぶん。
残課題
普通に、図書館のサイトを開いたときも、勝手に遷移するようになった。どうしよ・・・。
Amazonからしか検索できない。微妙すぎる。検索窓にタイトル入れるまでにしといた方がましかも。
公開方法わからない。もうちょい、ちゃんと作ったら公開してもいいのかも。おわりに
とりあえず、動くもの作れてよかった。ちょっと微妙ではあるけれど。
思ったより簡単やった。食わず嫌いよくない。追記
妥協案として、検索窓にタイトルを入れて検索窓にフォーカス(カーソルで選択した状態)するところで止めてみた。悪くない気がする。
content_lib.jschrome.storage.local.get("hold_title", function (items) { document.forms['search_form'].elements['keywords'].value = items.hold_title; document.forms['search_form'].elements['keywords'].focus(); // var x = document.getElementsByTagName("form"); // var request = document.createElement('input'); // request.name = 'btn1'; // request.value = '検索'; // document.forms['search_form'].appendChild(request); // document.forms['search_form'].submit(); });参考:
テキスト入力欄に最初からカーソルを入れる(フォーカスを与える)方法 - JavaScript TIPSふぁくとりー
- 投稿日:2020-02-18T20:36:47+09:00
ブックマークレット初心者がWEBサイト入力を自動化するためにやったこと
概要
自動化したいWEBサイトのHTMLを確認して、Bookmarkletを作ってみた。
検索窓に文字列を入れたり、POSTしたり、Amazonから本のタイトルコピーしたりできるようになった。デバッグツールの使い方も少し知ることができた。
今回、題材にしたサイト以外でも同じように対応できるはず。はじめに
Bookmarkletを自分で作ってみた。今までは、検索して同じことしようとしてる人を見つけてコピペして登録してた。今回、やりたいことが見つからなかったので、自分で作ってみた。HTTPもHTMLもJavaScriptもなんとなく知ってるくらいだったけれど、なんとかなった。
やりたいこと
市の図書館のWEBサイトが微妙なので、もうすこし楽に(クリック少なめで)検索と予約したい。(一応書いておくと、WEBで予約できるの便利で活用してる。とても感謝してる。)
環境
デバッグ環境
ここを参考にした。ちゃんとデバッグできる。すばらしい。
Chrome Dev Toolでブックマークレットを実行する - Qiita`Chromeの
Ctrl+Shift+I
で開くDeveloper Toolsを開く。
Consoleにコピペしてエンターキー。console.log()
とか使える。エラーの行番号もでる。ブレークポイントも設定できる(らしい)。試してみると、すごく簡単。実際に使うときと同じChrome環境で動くので、安心感すごい。自動化したいWEBサイトの中身を調べる
Developer Tools使ったり、
Ctrl+u
でHTMLのソースコード見たりして自動化したい部分のformとかinputを確認する。今回、対象にしたサイトが割と古いからか、HTMLにそのまま色々書かれてた。ただ、自動化したかったサイトは
authenticity_token
ってのが毎回変わる。結構、やっかいやった。
検索がHTTP GETじゃなくてHTTP POSTなのも微妙やった。一度、このサイトを開いてauthenticity_token
ってのを確認してPOSTしないと検索できない。予約もおんなじようなシステム。Bookmarkletじゃ他のサイトから直接検索したり予約できなかった。残念。コードを短くする
GoogleのClosure Compilerってのを使うと短くしてくれる。デバッグもできる(らしい)。
登録
Bookmarklet
いくつか試したものを抜粋。
Amazonの本のタイトルを取得
HTMLを確認するとtitleに著者名とかが入ってて
|
で区切られてる。本のタイトルだけ取得して、コピーする(クリップボードに入れる)。
(ここで取得した本のタイトルを図書館のサイトの検索窓に入れたかったけど、あんまり自動化できなくて結局手動でやることにした。)javascript:( function(){ const title = document.title; var title1 = title.split('|'); console.log('ページタイトル=%s\n', title1[0]); alert('ページタイトル:'+title1[0]); var tmp = document.createElement('p'); var pre = document.createElement('p'); pre.style.userSelect = 'auto'; tmp.appendChild(pre).textContent = title1[0]; document.body.appendChild(tmp); document.getSelection().selectAllChildren(tmp); document.execCommand('copy'); } )();検索窓に文字列を挿入
formの名前を確認して、とりあえず適当にテキストを入力してみた。普通に動いた。
(結局使わなかった。クリップボードからペーストもできそうやったけど、あんまり自動化できてないしやめた。)javascript:( function(){ document.forms['search_form'].elements['keywords'].value = 'test'; } )();まじめにformとinputを設定してsubmit
formに紐づくinputを全部、自分で設定してPOSTするパターン。結構めんどい。
var request
部分をコメントアウトしたら、最後にappendしたものしか送られていなかった。変数を再利用できてなさそう。JavaScriptってそういうもんなんかな?javascript: ( function postForm() { var form = document.createElement('form'); form.method = 'POST'; // form.accept-charset = 'UTF-8'; form.action = 'https://xxx'; var request = document.createElement('input'); request.name = 'y'; request.value = 'z'; form.appendChild(request); var request = document.createElement('input'); request.name = 'a'; request.value = 'b'; form.appendChild(request); form.submit(); } )();formを取得して、inputを追加してsubmit
HTMLにかかれているformを再利用する。一部ユーザ入力によって変わる部分があったので、そこだけ追加。formに名前とかIDがついていなかったので、何番目のformかを指定した。
x[2]
の部分。3番目。javascript: ( function postForm() { var x = document.getElementsByTagName("form"); var request = document.createElement('input'); request.name = 'commit'; request.value = '予約'; x[2].appendChild(request); x[2].submit();// Form submission } )();ページ遷移後のスクリプト
一度、POSTして新しいページに行ってから、別のスクリプトを実行したかったけれど、Bookmarkletじゃできなさそうやった。残念。Chrome拡張でなんとかしようかなぁ。
おわりに
作りたいもの駆動で勉強できてよかった。
デバッグツールがあったので、試しながら作れたのが良かった。
HTTPとかHTMLとかJavaScriptの知識がもうすこしあれば、もっとスムーズに作れるかもしれない。
でも、ちょっと自動化したいだけだったので、ちょうど良い感じで学べた気がする。
複数ページにまたがって自動化したかったけれど、調べた限りできないみたいやった。残念。
次は、Chrome拡張でも作ってみよっかな。
- 投稿日:2020-02-18T17:47:39+09:00
Reactにおけるstateのイミュータビリティ
この記事の目的
公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。関連するQiita記事
Reactのざっくり概要
Reactコンポーネントとは
Reactコンポーネント間の値の受け渡し
Reactコンポーネントでstateをリフトアップ参考資料
ドキュメント
チュートリアル
※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough TutorialReactにおけるstateのイミュータビリティ
イミュータビリティとは
変更可能なデータオブジェクトの値を変更するときは、以下の2つの方法がある。
- 対象のデータオブジェクトを直接変更する → mutable(変異)
- 対象のデータオブジェクトをコピーし、コピーしたデータオブジェクトを変更してから対象のデータオブジェクトをコピーオブジェクトで入れ替える(書き換えではなく入れ替え) → immutable (不変)
Reactにおけるイミュータビリティ
Reactのstateを変更する際は
setState()
という関数を使用するが、この関数はイミュータブルの手法で値を変更する必要がある。コードの例
以下のBoardコンポーネントを例に説明する。
board.jsclass Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), }; } handleClick(i) { const squares = this.state.squares.slice(); //ポイント1 squares[i] = 'X'; //ポイント2 this.setState({squares: squares}); //ポイント3 } renderSquare(i) { return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} /> ); } render() { const status = 'Next player: X'; return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }BoardコンポーネントのhandleClick関数(Squareコンポーネントでマスをクリックされたときに動作する関数)においてsetState関数を使用してstateの置き換えをしているが、その中でイミュータブルな処理が行われている。
注目すべきポイント
見るべきポイントは以下の3点。
state.squares
の値を変更したいが、まずはconst squares = this.state.squares.slice();
でstate.squares
のコピーを作成する- コピーした
squares
に'X'を格納するthis.setState({squares: squares});
で、state.squares
をコピーしたsquares
で置き換える → 書き換えではなく置き換えをしているオブジェクトをコピーする方法
JavaScriptでオブジェクトのコピーを作成する方法はいろいろあるが、「配列をコピーする例」と「オブジェクトのプロパティをコピーする例」を示す。
配列をコピーする例
ArrayCopy.jsconst newSquares = this.state.squares.slice(); newSquares[i] = 'X'; this.setState({squares: squares});オブジェクトのプロパティをコピーする例
ObjectAssign.jsvar player = {score: 1, name: 'Jeff'}; var newPlayer = Object.assign({}, player, {score: 2}); this.setState({player: newPlayer});なぜイミュータブルであることが重要なのか
なぜ、書き換えによるmutable(変異)な値の変更を行わず、オブジェクトの入れ替えによってimmutable (不変)な値の変更を行うのが重要なのか、理由を3つ説明する。
複雑な処理を簡略化できる
stateをimmutable (不変)の方針でオブジェクトによって置き換えると、値の変更は「オブジェクトの置き換えをしていない」か「オブジェクトの置き換えをしている」かの2択になる。
ある一部分だけ値を書き換えたような中途半端な状態にはならない。対して、stateをmutable(変異)の方針で直接書き換えると、ある一部分だけ書き換わった中途半端な状態が存在する可能性がある。
仮に、途中で書き換えの処理が失敗した場合、「一部分だけ書き換わった中途半端な状態」を元に戻す複雑な処理が必要となってしまう。また、immutableで「オブジェクトの置き換えをした結果」を履歴として保持すれば、過去の状態に戻す処理もmutableよりも簡単に実現することができる。
変更の検出が容易
mutableで値を直接書き換えた場合、stateが参照するオブジェクトに変化がないため、state内のどのオブジェクトに変化があったのかをReactは検知することができない。
そうなると、Reactは仮想DOM内の新旧差分を全走査することになり、再描画が遅くなる。対して、imutableでstateのオブジェクトを入れ替えた場合、stateが参照するオブジェクトに変化があるため、state内のどのオブジェクトに変化があったのかをReactは検知できる。
ReactはViewの再描画をする際に、仮想DOMの新旧比較を変化があったオブジェクトでのみ行うため、再描画が高速になる。再描画のタイミングを決めやすい
上記で書いたとおり、imutableなオブジェクトはstate内のどこで変更があったのかを検知できる。
それを利用して、再描画が必要ない変更であれば再描画せず、再描画が必要な変更であれば差描画するといったことが可能となる。
それによって、不要な再描画を押さえて描画パフォーマンスを向上させることができる。
- 投稿日:2020-02-18T17:40:09+09:00
2020年 2月のJestセットアップ
ありとあらゆるリファレンスを読みまくってまとめました。
一番スッキリJestをセットアップしよう。Jestとは
Facebookが開発したJavaScriptの単体テストフレームワーク。
Node上で動作するため、手軽にテストを実行できる事が特徴。作業環境
- Laravel 5.7
- Docker on Mac
- Vue.jsとかはLaravel mixのもの
セットアップ
Jestとかを
npm install
npm install --save-dev jest babel-jest @babel/core babel-core@bridge @babel/preset-env vue-jest @vue/test-utilsそれぞれのひとこと説明
jest
Jest本体babel-jest
babelとjestを連携させるもの@babel/core
Babel本体の最新版
Babelは八百万のJavaScript構文を一定のバージョンと互換のある構文に変換するもの。
Nodeでは最新のJavaScript構文(import / exportなど含め)が動作しません。
そのため、Babelを使ってトランスパイルしてあげます。babel-core@bridge
vue-jest
が@babel/core
ではなく、babel-core
を参照してしまうらしく、これを解決するためにインストール。
Babel 7.x系だけど名前はbabel-core
なBabel本体。@babel/preset-env
山ほどあるBabelの構文変換に関わるライブラリをいい感じに自動的に使ってくれるもの。
参考 : babel-preset-envを簡単にさわってみた。 - Qiitavue-jest
vueをjestでテストできるようにするもの@vue/test-utils
Vueのテストを書きやすくしてくれるユーティリティBabelのバージョンに注意
Babel 7.xからプラグインのプレフィックスとして
@babel/
がつくようになりました。
ECサプリに入ってるBabelは7系なのでプラグインを追加する時、@babel/
から始まっているか注意してください。6.x以前のプラグインを使うとうまく動かない事があるらしい
設定ファイルをつくる
babel.config.js (.babelrc)
Babelの設定ファイルです。
babel.config.js
でも.babelrc
でもいいのでソースルートに配置します。おそらく両方あるのはよろしく無いのでどちらか片方にしましょう。babel.config.jsmodule.exports = { presets: [ ['@babel/preset-env', {modules: false}] ], env: { test: { presets: [ ['@babel/preset-env', {targets: {node: 'current'}}] ] } } };jest.config.js
どんなファイル名のものをテスト対象とするか、どのファイル名をどのモジュールで変換するか、などを設定できます。
jest.config.jsmodule.exports = { transform: { '.*\\.(vue)$': '<rootDir>/node_modules/vue-jest', '^.+\\.js$': '<rootDir>/node_modules/babel-jest' }, moduleFileExtensions: [ 'js', 'vue' ], };package.json (既存のものを編集)
Jestの起動コマンドを書き足します。
NODE_ENV=test
とすることでNodeへテスト環境であることを明示しています。package.json{ "private": true, "scripts": { "dev": "npm run development", "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", "watch": "npm run development -- --watch", ... , "test": "NODE_ENV=test jest" <-追記 }, "devDependencies": { ... }, ... }テストを書く
テストケースを作ってみましょう。
こんなファイルをテスト対象にしてみました。
カリー化してある2つの数字を足すだけの関数です。ExampleSum.jsexport default{ sum(x){ return (y){ return x + y; } } }これをテストするスクリプトはこんな感じ。
example.test.jsimport ExampleSum from "path/to/ExampleSum"; test('test sum()', ()=>{ expect(ExampleSum.sum(1)(2)).toBe(3); }実行してみよう
npm test
とターミナルへタイプしてJestを動かしてみよう。
--
でファイル名を連結することでそのファイル名と部分一致するファイルのみをテスト対象としてくれます。npm test -- example.test.jsそのほかのCLIオプションは Jest CLI Options · Jest へ。
async
/await
もテストできるPromiseExample.jsexport default { sleep() { return new Promise(resolve => { setTimeout(() => { resolve(); }, 500); }); } }test('test async', async () => { await PromiseExemple.sleep(); expect(true).toBe(true); });
Vueのフロントエンドテストもかけます
ExampleComponent.vue<template> </template> <script> export default { name: "ExampleComponent", methods: { sum(x, y) { return x + y; } } } </script> <style scoped> </style>component.test.jsimport ExampleComponent from "./models/ExampleComponent"; const {mount} = require("@vue/test-utils"); test('Vue component test', () => { const component = mount(ExampleComponent); expect(component.isVueInstance()).toBeTruthy(); expect(component.vm.sum(1, 2)).toBe(3); });
component.vm
が僕らのよく知るVueコンポーネントインスタンスです。
DOMを操作してフォーム入力をしたり、クリックしたりもできます。
@babel/preset-env
がびっくりするほど便利だったことがわかった。
- 投稿日:2020-02-18T17:27:45+09:00
[python] コールバック関数(引数に関数を渡す)
javascriptとpython
javascriptconst sayHello = word => console.log(word); const func = callback => { console.log('ここはsayHelloが呼ばれたあとに実行される'); return callback; } func(sayHello('hello'));実行結果hello ここはsay_somethingが呼ばれたあとに実行される
pythondef say_something(word): print(word) def func(callback): print('ここはsay_somethingが呼ばれたあとに実行される') return callback func(say_something('hello'))実行結果hello ここはsay_somethingが呼ばれたあとに実行される
- 投稿日:2020-02-18T15:57:41+09:00
Reactコンポーネントでstateをリフトアップ
この記事の目的
公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。関連するQiita記事
Reactのざっくり概要
Reactコンポーネントとは
Reactコンポーネント間の値の受け渡し参考資料
ドキュメント
チュートリアル
※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough TutorialReactコンポーネントでstateをリフトアップ
stateを子コンポーネントに持たせる場合の問題点
Reactコンポーネント間の値の受け渡しでは、Squareコンポーネントのstateで値を保持していたが、このままだとBoardコンポーネントで9つあるSquareコンポーネントの値を取得しようとした場合に、9つのSquareコンポーネントにそれぞれ問い合わせをする必要がある。
そのようなコードもReactは許容するが、コードがわかりにくくなりバグを発生しやすく、リファクタリングも難しくなるので、Reactは「stateのリフトアップ」を推奨している。
stateのリフトアップとは?
各Squareコンポーネントで持っていたstateをBoardコンポーネントに移し、各Squareコンポーネントのpropsに対してstateの値を渡すようにする。
このように、SquareコンポーネントにあったstateをBoardコンポーネントに移すようなリファクタリングを、「stateのリフトアップ」と呼ぶ。
stateのリフトアップをするコード例
Squareコンポーネントのstateをリフトアップすると、以下のようなコードになる。
コードの例
board.jsclass Square extends React.Component { //ポイント2 render() { return ( <button className="square" onClick={() => this.props.onClick()} > {this.props.value} //ポイント4 </button> ); } } class Board extends React.Component { constructor(props) { super(props); this.state = { //ポイント1 squares: Array(9).fill(null), }; } handleClick(i) { const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); } renderSquare(i) { return ( <Square value={this.state.squares[i]} //ポイント3 onClick={() => this.handleClick(i)} /> ); } render() { const status = 'Next player: X'; return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)}{this.renderSquare(1)}{this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)}{this.renderSquare(4)}{this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)}{this.renderSquare(7)}{this.renderSquare(8)} </div> </div> ); } }注目すべきポイント
stateのリフトアップを見る上でのポイントは以下の4点。
- Boardコンポーネントにstateを持たせる
- Squareコンポーネントからstateを除去
- Boardコンポーネントのstateを、Squareコンポーネントのpropsに渡す
- Squareコンポーネントは、propsで受け取ったBoardコンポーネントのstateを表示する
子コンポーネントから親コンポーネントのstateを変更する例
Boardコンポーネントから見て、Squareコンポーネントは子コンポーネントになるので、BoardコンポーネントとSquareコンポーネントには親子関係があることになる。
Squareコンポーネント(子コンポーネント)からBoardコンポーネント(親コンポーネント)のstateを変更したい場合、子コンポーネントから親コンポーネントのstateを直接変更することはできない。
なぜなら、コンポーネントのstateはプライベートなフィールドなので外部から直接値を変更することはできないためである。
そのため、SquareコンポーネントからBoardコンポーネントのstateを変更する場合は、以下のように親コンポーネントから子コンポーネントにstateを変更する関数をpropsとして渡すことになる。
子コンポーネントでは、propsで受け取った親の関数を通して、親のstateを変更する。コードの例
board.jsclass Square extends React.Component { render() { return ( <button className="square" onClick={() => this.props.onClick()} //ポイント3 > {this.props.value} </button> ); } } class Board extends React.Component { constructor(props) { super(props); this.state = { squares: Array(9).fill(null), }; } handleClick(i) { //ポイント1 const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); } renderSquare(i) { return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} //ポイント2 /> ); } render() { const status = 'Next player: X'; return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)}{this.renderSquare(1)}{this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)}{this.renderSquare(4)}{this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)}{this.renderSquare(7)}{this.renderSquare(8)} </div> </div> ); } }注目すべきポイント
子コンポーネントから親コンポーネントのstateを変更する上でのポイントは以下の3点。
- 親コンポーネント(Boardコンポーネント)にstateを変更する処理(今回の場合は
handleClick
関数)を持たせる- 子コンポーネント(Squareコンポーネント)にstateを変更する処理(今回の場合は
handleClick
関数)をpropsとして渡す- 子コンポーネント(Squareコンポーネント)から親コンポーネント(Boardコンポーネント)のstateを変更するときは、propsで渡された親コンポーネント(Boardコンポーネント)の処理(今回の場合は
handleClick
関数)を呼び出すstateのリフトアップを検討するときのポイント
以下の流れで実装とリファクタリングを区別し、リファクタリングでstateのリフトアップを行うのが好ましい。
実装
- 親子関係にあるコンポーネントであっても、子コンポーネントに”state”と”stateを変更する処理”がある状態で実装を進める
- 実装が完了し、テストで動作を確認する
リファクタリング
- 子コンポーネントにある”state”を、親コンポーネントにリフトアップする
- 子コンポーネントにある”stateを変更する処理”を、親コンポーネントにリフトアップする
- 子コンポーネントのpropsに、親コンポーネントの”state”と”stateを変更する処理”を渡す
- 子コンポーネントは、propsで渡された”state”と”stateを変更する処理”を使って、子コンポーネント側の変更を親コンポーネントのstateに格納する
- リファクタリングが完了したら、テストで動作を確認する
- 投稿日:2020-02-18T15:08:21+09:00
Reactコンポーネント間の値の受け渡し
この記事の目的
公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。関連するQiita記事
参考資料
ドキュメント
チュートリアル
※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough TutorialReactコンポーネント間の値の受け渡し
Reactコンポーネント間で値の受け渡しをするには、propsとstateというものを使用する。
ReactコンポーネントとはのSquareコンポーネントとBoardコンポーネントにpropsとstateを加えて説明する。propsの追加
propsとは、コンポーネントのプロパティ(引数)のことである。コンポーネントに外から値を引き渡したい場合、propsを使用する。
コードの例
Squareコンポーネントにpropsを追加すると以下のようなコードになる。
board.jsclass Square extends React.Component { render() { return ( <button className="square"> {this.props.value} //ポイント2 </button> ); } } class Board extends React.Component { renderSquare(i) { return <Square value={i} />; //ポイント1 } render() { return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }注目すべきポイント
ポイントは以下の2点。
- BoardコンポーネントのrenderSquare関数でSquareコンポーネントを返す際に、Squareコンポーネントに
value={i}
を渡している- Squareコンポーネントは渡されたvalueを
this.props.value
で取り出しているこれによって、Squareコンポーネントを描画するときに
value={i}
で0〜9の値が渡されることになり、渡された値はSquareコンポーネントのタグに挟まれる形で{this.props.value}
の部分で表示されることになる。実行結果
実行結果としては以下のようになる。
(実際にブラウザで表示させたい場合はReact公式チュートリアルのCode Penで確認)stateの追加
stateとは、コンポーネント内で値を保持するための記憶領域を指す。コンポーネント内で値を保持したい場合にstateを使用する。
コードの例
Squareコンポーネントにstateを追加すると以下のようなコードになる。
board.jsclass Square extends React.Component { constructor(props) { super(props); this.state = { //ポイント1 value: null, }; } render() { return ( <button className="square" onClick={() => this.setState({value: 'X'})} //ポイント2 > {this.state.value} </button> ); } } class Board extends React.Component { renderSquare(i) { return <Square />; } render() { return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }注目すべきポイント
ポイントは以下の2点。
- Squareコンポーネントのコンストラクタで、
this.state
にvalue: null
を含むオブジェクトを格納している(コード上で明示されていないが、Squareコンポーネントはthis.state
というフィールドを持っている)- SquareコンポーネントのボタンがクリックされたときにonClickイベントが発生し、
this.setState({value: 'X'})
が実行される。これによってSquareコンポーネントのthis.state
に{value: 'X'}
が格納される実行結果
実行結果としては以下のようになる。
(実際にブラウザで表示させたい場合はReact公式チュートリアルのCode Penで確認)
Squareコンポーネントをクリックする前
Squareコンポーネントのstateには、
value: null
が格納されている。
Squareコンポーネントをクリックした後
- 投稿日:2020-02-18T14:44:29+09:00
Reactコンポーネントとは
この記事の目的
公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。関連するQiita記事
参考資料
ドキュメント
チュートリアル
※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough TutorialReactコンポーネントとは
ReactはUIを「コンポーネント」という部品単位で構築する。
1つのコンポーネントを描画する例
これをReactコンポーネントで定義すると、以下のようになる。(スタイルは省略している)
square.jsclass Square extends React.Component { render() { return ( <button className="square"></button> ); } }SquareというReactコンポーネントを定義しており、render関数で描画内容をreturnしている。
このときというHTMLタグをreturnしているように見えるが、これはJSX記法であり、以下のようにJSXを使わずにReact.createElementで記述した場合と等価である。
square.jsclass Square extends React.Component { render() { return ( React.createElement( 'button', {className: 'square'} ); ); } }複数のコンポーネントを描画する例
上記のSquareを更に3×3のマス目でボタンを並べて描画する場合を考える。
これをReactコンポーネントで定義すると、以下のようになる。(スタイルは省略している)
board.jsclass Square extends React.Component { render() { return ( <button className="square"></button> ); } } class Board extends React.Component { renderSquare(i) { return <Square />; } render() { return ( <div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }新たに加わったBoardというReactコンポーネントは、Squareコンポーネントを9つ組み合わせて3×3のマス目を実現している。
- 投稿日:2020-02-18T14:01:15+09:00
Ruby と JavaScript でライフゲームを書いた
ハッカーになろうや模造クリスタル『ゲーム部』2で言及されてたライフゲームが面白そうだったので書いてみた。ライフゲームはセル平面上(要は方眼紙のこと)で生命をシミュレートするモデルです。
ライフゲームのルール
- セルには生と死の二つの状態がある。
- 時間(世代)が経過すると、あるルールに従ってセルの状態が変わる。
- あるルールとは次の4つ。
- 過疎による死: 自身が生で、周りの8セルのうち1セル以下が生のとき、次の世代で死ぬ。
- 生存: 自身が生で、周りの8セルのうち2〜3セルが生のとき、次の世代で生き続ける。
- 過密による死: 自身が生で、周りの8セルのうち4セル以上が生のとき、次の世代で死ぬ。
- 誕生: 自身が死で、周りの8セルのうち3セルが生のとき、次の世代で生になる。
コードで表現しやすそうなルールだ。疑似コードで書いてみるとこんな感じ?
class Cell def next_alive? alive_count = 周囲.count { |item| item.alive? } # 過疎 return false if self.alive? && alive_count <= 1 # 生存 return true if self.alive? && 2 <= alive_count && alive_count <= 3 # 過密 return false if self.alive? && 4 <= alive_count # 誕生 return false if self.dead? && alive_count == 3 false end endで、これを1世代ごとに全てのセルに対してチェックすればいけそう。
Ruby で実装
https://github.com/oieioi/lifegame.rb
こんな感じに動きます。JoyDivisionっぽい見た目になった。
上に示した次世代の生死判定はこんな感じのコードになりました。
lib/lifegame/game.rb#L47-L78# あるセルが次のターンに生きてるか確認する def next_alive?(x, y) target = self[x, y] # 隣接セルの座標 adjoining = [ [x - 1, y - 1], [x , y - 1], [x + 1, y - 1], [x - 1, y], # [x , y], [x + 1, y], [x - 1, y + 1], [x , y + 1], [x + 1, y + 1], ] dead_or_live = adjoining.map { |point| n_x, n_y = point self[n_x, n_y]&.alive? }.compact live_count = dead_or_live.count { |live| live } if target.dead? # 3の時のみ誕生 live_count == 3 else # 2,3の時のみ生き残る (2..3).include?(live_count) end endJavaScript (with React)で実装
CLI だとインタラクティブなのがやりづらいのでJSでも書いてみた。
https://github.com/oieioi/lifegame.js
デモはこちら: https://dreamy-lumiere-0f384d.netlify.com/こちらは全てのセルを二次元配列で受け取って新しい生死の二次元配列を返すようにした。
src/lib/LifegameLogic.js#L1-L18function nextCells(cells) { return cells.map((line, x)=> { return line.map((alive, y) => { // 周囲を調べる const aliveCount = getAdojoiningPositions(x, y).filter((position) => { const [x,y] = position; if (!cells[x]) return false; return cells[x][y] }).length if (alive) { // 周囲の生き残りが2,3のとき生存 return aliveCount === 2 || aliveCount === 3; } else { return aliveCount === 3 } }); }); }
- 投稿日:2020-02-18T13:28:58+09:00
Enumerable.Linq の各メソッドを JavaScript ではどう書くか?(メソッドの対応)
はじめに
内容は随時補完されます!
.NET の Linq に慣れ親しんでいる方向けに、JavaScriptでの似たメソッドへの対応を一覧化したものです。Linq でいう「シーケンス」を、Javascript でいう「索引付きコレクション(配列など、インデックス値で順序付けされたデータのコレクション)」「配列風オブジェクト1」「キー付きコレクション(反復可能オブジェクト2)」のすべて、またはいずれかとほぼ同じとみなして対応させています。
一覧
まったく同じ機能とは限らないので注意です。(元となる配列自体を書き換える(*マークつき)、メソッドチェーンにならない、同値判定・False判定法が違うなど)
Linq JavaScript JSでの機能 Aggregate reduce
reduceRight配列の各要素に対してreducer関数を実行して単一の値を返す
配列の各要素に対して右から左へreducer関数を実行して単一の値を返すAll every 配列のすべての要素が、与えられた関数によって実行されるテストに通るかテストする Any some 配列の少なくとも1つの要素が、渡された関数によって実施されるテストに通るかテストする Append *push
concat配列自体の末尾に1つ以上の要素を追加する
配列に他の配列や値をつないだ新しい配列を返すAverage reduce で代用など Cast map で代用など Concat concat 配列に他の配列や値をつないだ新しい配列を返す。2以上の配列や値も一度に指定可能 Contains includes 配列に特定の要素が含まれているかを返す Count length Distinct Set オブジェクト生成など Empty [ ] First find 配列内の、提供されたテスト関数を満たす最初の要素の値を返す。配列すべての要素にテスト関数が呼び出されるので注意。 FirstOrDefault ↑ Last sort と find で代用など LastOrDefault ↑ Max Math.max
reduce で代用など引数として与えた複数の数の中で最大の数を返す Min Math.min
reduce で代用など引数として与えた複数の数の中で最小の数を返す OrderBy *sort 配列自体の要素をソートする OrderByDescending ↑ Prepend *splice 配列自体の既存の要素を取り除いたり、置き換えたり、新しい要素を追加したりする Reverse *reverse 配列自体の要素を反転させて返す Select map 配列のすべての要素に対して与えられた関数を実行し、その結果からなる新しい配列を生成して返す SelectMany flat
flatMapすべてのサブ配列の要素を指定した深さで再帰的に結合した新しい配列を生成して返す
マッピング関数を使用してそれぞれの要素をマップした後、結果を新しい配列内にフラット化して返すSkip *splice 配列自体の既存の要素を取り除いたり、置き換えたり、新しい要素を追加したりする SkipLast *splice 配列自体の既存の要素を取り除いたり、置き換えたり、新しい要素を追加したりする SkipWhile filter で代用など Sum redure で代用など Take subarray
slice指定される範囲から同じ要素の型をもつ新しい配列を生成して返す
配列の一部をシャローコピーして新しい配列オブジェクトを返すTakeLast ↑ TakeWhile filter で代用など ToArray from 「配列風オブジェクト1」や「反復可能オブジェクト2」から新しい浅いコピーのArrayインスタンスを生成して返す Where filter 各配列要素に対して、与えられたテスト関数を実行し、合格したすべての配列要素からなる新しい配列を生成して返す 対応例
Select - map (配列添字情報つき)
C#var req = new[] { 160, -2, 0, 5 }; var res = req.Select((s, i) => s * i); // -> { 0, -2 , 0, 15 }JavaScriptlet req = [16, -2, 0, 5]; var res = req.map(function(s, i) { return s * i; }); // -> [ 0, -2, 0, 15 ]補足
linq.js
便利なライブラリがあるようです。
ジェネレーター関数
.NET でいう IEnumerable<T>を戻り型とする関数と似た概念があり、function*と記載することで、yield (C# の yield return) や yield* を使用できます。
JavaScriptfunction* OneTwoThree(s) { yield "1" + s; yield "2" + s; yield "3" + s; } for (let v of OneTwoThree("個")) { console.log(v); } for (let v of OneTwoThree("本")) { console.log(v); } // -> // 1個 // 2個 // 3個 // 1本 // 2本 // 3本さいごに
間違いのご指摘、便利な書き方、ぜひぜひお寄せ下さいませ!
参考情報 ありがとうございます!
配列風オブジェクト (length プロパティおよびインデックス付けされた要素を持つオブジェクト (Array.from - MDN より) ↩
Map や Set のような要素を取得するオブジェクト(Array.from - MDN より) ↩
- 投稿日:2020-02-18T13:04:22+09:00
【Vue.js】vue-image-lightbox で画像ギャラリーを作成!
はじめに
Googleドライブの画像表示みたいなUIを作りたいと思っていたところ、vue-image-lightbox を使うと
簡単に実現できたので、最低限の実装方法を残したいと思います。vue-image-lightboxとは?
サムネイルから、画像を拡大表示したり、スライドショーにしたりするための機能を提供しているライブラリです。
参考情報
- vue-image-lightbox https://www.npmjs.com/package/vue-image-lightbox
- ソースコード https://github.com/ryuckel/vue-playground/tree/vue-image-lightbox
今回の記事で実現した内容
以下の画像のようになるところをゴールとします。
記事中は主にライブラリに関係した部分を書いてます。ソースは参考情報から見てみてください。
環境
- Vue.js 2.5.10
- vue-cli 3.7.0
実装(準備編)
- 環境構築
vue createコマンドでプロジェクトを作成していきます。
プロジェクト名は任意で入れてください。
セットアップにあたり聞かれる質問も任意で答えていただいて構いません。vue create image-app(プロジェクト名を入れてください) cd image-app
- 必要なライブラリのインストール
yarn add vue-image-lightbox vue-lazyload※画像の遅延読み込み(Webページを先に表示して画像は読み込んだら表示させること)が必要になるため
vue-lazyloadも併せてインストールしています。
- CSSのインポート
まず、app.scssにvue-image-lightboxのCSSプロパティを読み込みます。app.scss@import '../../../node_modules/vue-image-lightbox/dist/vue-image-lightbox.min.css';
- vue-lazyloadのインポート
画像の遅延読み込みのため、main.jsにvue-lazyloadをインポートしていきます。main.jsimport VueLazyLoad from 'vue-lazyload'; Vue.use(VueLazyLoad);実装(コンポーネント作成)
vue-image-lightboxを使用したコンポーネントを作成します。
CSSはBuefy(BulmaベースのUIコンポーネントライブラリ)を使い、サムネイルだけCSSを当てています。
要点は以下の通りです。
- import文でvue-image-lightboxを読み込みます。
- 画像表示は配列(images)を用意して
v-for
でimg要素を生成し、v-lazyで遅延読み込みしています。- img要素をクリックすると
openGallery
メソッドを発火させることでクリックした画像がギャラリー表示されます。 openGalleryメソッドはvue-image-lightboxのshowImageメソッドに配列(images)のインデックスを渡しています。- ImageLightBox要素で指定しているプロパティ
show-light-box
がtrueだとブラウザにページを読み込んだタイミングでギャラリー表示されます。show-caption
がtrueにすると、ギャラリー表示時に画像オブジェクトに指定されたcaptionが表示されます。LightBox.vue<template> <div> <div class="media"> <div class="column"> <div class="columns is-multiline"> <div v-for="(image, index) in images" :key="image.id" class="column is-one-quarter has-text-centered"> <img v-lazy="image.src" @click="openGallery(index)" class="thumbnail" /> <p>{{ image.title }}</p> </div> </div> <ImageLightBox ref="lightbox" :images="images" :show-light-box="false" :show-caption="true"></ImageLightBox> </div> </div> </div> </template> <script> import ImageLightBox from 'vue-image-lightbox'; export default { name: 'LightBox', components: { ImageLightBox }, data() { return { images: [ { thumb: 'https://www.pakutaso.com/shared/img/thumb/YAMAhokkaido015_TP_V.jpg', src: 'https://www.pakutaso.com/shared/img/thumb/YAMAhokkaido015_TP_V.jpg', title: '朝日が昇る摩周湖(北海道川上郡弟子屈町)', caption: '朝日が昇る摩周湖(北海道川上郡弟子屈町)', id: 1 }, { thumb: 'https://www.pakutaso.com/shared/img/thumb/YAMAhokkaido032_TP_V.jpg', src: 'https://www.pakutaso.com/shared/img/thumb/YAMAhokkaido032_TP_V.jpg', title: '水鏡の青い池(北海道川上郡美瑛町白金)', caption: '水鏡の青い池(北海道川上郡美瑛町白金)', id: 2 }, { thumb: 'https://www.pakutaso.com/shared/img/thumb/KMKC428D354_TP_V.jpg', src: 'https://www.pakutaso.com/shared/img/thumb/KMKC428D354_TP_V.jpg', title: '悪天候の上高地と大正池', caption: '悪天候の上高地と大正池', id: 3 } ] }; }, methods: { openGallery(index) { this.$refs.lightbox.showImage(index); } } }; </script> <style lang="scss" scoped> img.thumbnail { height: 100px; cursor: pointer; } </style>最後に
画像のギャラリー表示をやりたくなった時になかなかvue-image-lightboxに行きつけなかったのですが、
vue-image-lightboxを見つけてから実装まではスムーズにいったので記事にしてみました。
ぜひお試しください!
- 投稿日:2020-02-18T12:48:43+09:00
Electronでの印刷時に余白をピッタリ調整する
モチベーション
Electronでは
webContents.print()
関数やwebContents.printToPDT()
関数を使って簡単に印刷やPDF作成ができます。この時、CSSで印刷サイズをmm等の実物長さで指定することで、実際の印刷時の各種要素の大きさを指定できるので便利です。が、注意しないと余分な余白が入ってしまいます。経験則からくる注意点を情報共有します。注意点
- CSSで
padding
やmargin
を正しく設定する。特に設定しない時は、明示的に0mm
を設定すること。デフォルトでは0mm
でないことがある。- body要素にも
padding
とmargin
を明示的に設定する事。実際に余分な余白が消えずに悩みましたが、body要素のmargin
がデフォルトで0mm
ではなかったことが原因でした。- 印刷時だけ設定したいCSSは
@media print
項目で設する例
body要素の中にdiv要素があり、その中に文章が書かれている場合のCSS記述は次のようになります。
@media print{ body{ padding: 0mm; margin: 0mm; /* これが無いと余分な余白が入る */ } div{ padding: 0mm; width:180mm; /* 用紙幅はA4(210mm)を仮定して余白を引いたもの*/ margin-left: 15mm; /*左右の余白15mm*/ margin-right: 15mm; margin-top: 20mm; /*上下の余白20mm*/ margin-bottom: 20mm; } }雑感
Electronの印刷はとても楽でいいですね。CSSで微調整できるところも素敵です。すごい時代になってますね。
子供の頃、親父にPCを買ってもらった際、弟と協力して年賀状印刷ソフトを作ったのを思い出しました。当時はVisual Basic6で印刷物の位置合わせをするのに苦労しました。
- 投稿日:2020-02-18T12:48:43+09:00
モダンブラウザとElectronでの印刷時に余白をピッタリ調整する
モチベーション
Electronでは
webContents.print()
関数やwebContents.printToPDT()
関数を使って簡単に印刷やPDF作成ができます。この時、CSSで印刷サイズをmm等の実物長さで指定することで、実際の印刷時の各種要素の大きさを指定できるので便利です。が、注意しないと余分な余白が入ってしまいます。経験則からくる注意点を情報共有します。※2020/2/19:色々試行錯誤して、
@page
との関連、print()
とprintToPDF()
の違いなど大幅に改定しました。最重要注意点
- ChromeおよびChromium系ブラウザ、Electron(7.1.9~8.0.1で確認)において、
@page
内に記述したmargin
,margin-left
,margin-right
,margin-top
,margin-bottom
については、単位の変換におそらくbugがあり、設定値よりも1.5~1.8倍程度大きく設定されてしまう。
- 確認したversion(これ以外も確認していないだけでおそらくbugがある)
- Chrome: 80.0.3987.106
- Electron: 7.1.9~8.0.1
- Edge (Chromium): 80.0.361.54
- 回避策として、
@page
内ではmargin
系の設定値の 単位として%
を使う。- Firefox、Edge(Legacy)では
%
以外の単位も問題なく動く。margin-top
、margin-bottom
に%
を使う際は、ブラウザによって解釈が異なる
- Chrome, Firefox, Edge(Chromium):ページ横幅に対する割合を設定する
- Edge(Legacy):ページ縦幅に対する割合を設定する
注意点(試行錯誤の結果)
- CSSで
padding
やmargin
を正しく設定する。特に設定しない時は、明示的に0mm
を設定すること。デフォルトでは0mm
でないことがある。- body要素にも
padding
とmargin
を明示的に設定する事。実際に余分な余白が消えずに悩んだが、body要素のmargin
がデフォルトで0mm
ではなかったことが原因だった。- 印刷時だけ設定したいCSSは
@media print
項目で設する。- ブラウザ上の表示はどれだけ長くても縦に長い1枚ページであるのに対し、印刷時は縦幅も固定で複数ページに分割される。
- 印刷時のページ毎のマージンは、HTML要素のプロパティーではなく、
@page
のプロパティーとして設定するべき。さもなくば、 改ページ部分での上下マージンが反映されない。webContents.print()
はElectron 7.xではmarginType:"custom"
を設定するとクラッシュする。webContents.print()
のmarginType
に"default"以外の値を設定しても、"default"と表示が変わらない。(バグとも思える。Electron version 8.0.1)。webContents.printToPDF()
でmarginsType:0
(デフォルトマージン)にすると CSSの@page
に設定したmargin
,margin-left
,margin-right
,margin-top
,margin-bottom
が適用される。marginsType:1
(マージンなし)にすると、これらのCSS設定も無視される。webContents.printToPDF()
のmarginsType:0
と、webContents.print()
のmarginType: "default"
はCSSの適用と表示に関して(おそらく)同じ挙動。例
body要素の中にdiv要素があり、その中に文章が書かれている場合のCSS記述は次のようになります。
@page{ size: A4; /* 本来は以下の記述で良いはずが、Chromium系バグで設定値より大きくなってしまう margin-left: 15mm; margin-right: 15mm; margin-top: 20mm; margin-bottom: 20mm; 回避策として以下のように%で指定する */ margin-left: 7.143%; /* = 15mm / 210mm */ margin-right: 7.143%; margin-top: 9.524%; /* = 20mm / 210mm "横幅"に対する割合*/ margin-bottom: 9.524%; } @media print{ body{ padding: 0mm; margin: 0mm; /* これが無いと余分な余白が入る */ } div{ padding: 0mm; margin: 0mm; width:100%; } }上記の様なCSSに対して、印刷のJavascriptコードは
print()
関数のoptionとして次のようにmarginType:"default"
を指定する。const wc = browserWindow.webContents; wc.print({margins:{marginType:"default"}}, (success, error) => { if(success){ console.log('Print successfully.'); }else{ console.log(error); } });また、PDF出力時のJavascriptコードは
printToPDF()
関数のoptionとして次のようにmarginsType:0
を指定する。const wc = browserWindow.webContents; wc.printToPDF({pageSize:"A4", marginsType:0}).then(data => { fs.writeFile(result.filePath, data, (error) => { if (error) throw error; console.log('Write PDF successfully.'); }) }).catch(error => { console.log(error); });雑感
Electronの印刷はとても楽でいいですね。CSSで微調整できるところも素敵です。すごい時代になってますね。
子供の頃、親父にPCを買ってもらった際、弟と協力して年賀状印刷ソフトを作ったのを思い出しました。当時はVisual Basic6で印刷物の位置合わせをするのに苦労しました。
雑感(改定時)
Chrome,Chromiumのバグと気づくまで今回は時間が掛かりました。Firefoxがあってよかった。Edge(Legacy)が無くなってしまった弊害として、今回の様なブラウザエンジンのバグに気づきにくくなりますね。そういう意味で復活して欲しいなぁ、EHTMLエンジン。
- 投稿日:2020-02-18T12:25:00+09:00
forEachをIE11で使う方法
ES2015
JavascriptのES2015(ES6)からは配列を便利に扱えるメソッドが増えましたが、
IE11ではサポートされていないものが多々あります。詳しい対応状況はこちらが分かりやすいですね↓
http://kangax.github.io/compat-table/es6/forEachもその一つで、Can I useを確認してみても、
サポートされていない事が分かります。
ですが、実際はひと工夫することでIE11でも使えます。forEachのIE11対応
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>forEachのIE対応実験</title> </head> <body> <ul id="box"> <li>りんご</li> <li>みかん</li> <li>バナナ</li> </ul> </body> </html>//Chromeなどのモダンブラウザではこれで動く const fruits = document.querySelectorAll('#box li'); //Nodelist; fruits.forEach(fruit => { console.log(fruit.innerText); // りんご, みかん, バナナ }); //IEだとエラーで動かない オブジェクトは 'forEach' プロパティまたはメソッドをサポートしていません。原因は...
IE11はquerySelectorAll等が作るNodelistのforEachに対応していないそうです。
対策としては、このNodelistを配列に変換します。
ですので、元から配列として定義されているオブジェクトに対しては、IE11でもforEachが使えます。//IEでも動いた! const fruits = document.querySelectorAll('#box li'); //Nodelist const fruitsArr = Array.prototype.slice.call(fruits); //Array fruitsArr.forEach(function(fruit) { //アロー関数はIEでは使えません console.log(fruit.innerText); // りんご, みかん, バナナ });//こう書いてもOK const fruits = document.querySelectorAll('#box li'); //Nodelist Array.prototype.forEach.call(fruits, function(fruit) { console.log(fruit.innerText); // りんご, みかん, バナナ });参考文献
https://lealog.hateblo.jp/entry/2014/02/07/012014
Array.prototype - JavaScript | MDN
Array.prototype.slice() - JavaScript | MDNまとめ
他にも通常のfor文にお置き換えるという対応方法もありますが、
今回の対策の方が記述が少なくて済むので、今後も積極的に使っていきたいと思います。
- 投稿日:2020-02-18T12:21:12+09:00
【Ajax+Rails+Carrierwave】個別削除可能な画像複数(10枚まで)投稿
はじめに
初投稿になります。syomaと申します。よろしくお願いいたします。
某プログラミングスクールの最終課題で某フリマアプリのクローンにて、商品出品における画像の複数投稿が大変だった件について投稿いたします。
参考記事が幾つかございましたので、そちらをを参考にさせていただきました。この場をお借りして感謝申し上げます。ただ、どの記事にもscssの記載が無く、クラスに当たるscssの記述に苦戦したので画像複数(10枚まで)投稿の記述とscssをセットで記載してみます。実現したいこと
1.画像をアップロードし、結果を送信する前にプレビューを表示する。(ドラッグ&ドロップは非対応です。すみません。)
2.そのほかのフォームの結果と一緒に画像をコントローラに送信する。
3.画像のプレビュー時に、削除ボタンで個別にアップロードする画像を変更する。
環境
Rails 5.2.4.1
ruby 2.5.1注意点
1.Rails初学者のため、間違っている部分が多々ある可能性がありますので、参考程度にご覧ください。
2.gemのCarrierwaveで1枚画像投稿ができている。
3.モデル側については、解説ありません。コントローラ、ビュー、javascript部分になります。モデル側は、good(商品)に対して複数のphotosテーブルのレコードを登録できるような中間テーブルを作成しています。テーブル構造
モデル同士の関係
userテーブル(誰が)、goodテーブル(何を)、categoryテーブル(どんな→Gemのancestryを使用)、photo(画像)と分けておりますが、今回はgood,photoテーブルのみ記載しておきます。
good(商品).rbclass Good < ApplicationRecord belongs_to :user belongs_to :category has_many :photos, dependent: :destroy accepts_nested_attributes_for :photos, allow_destroy: true endphoto(画像).rbclass Photo < ApplicationRecord belongs_to :good , optional: true mount_uploader :image, ImageUploader endcontroller
goods_controller.rbclass GoodsController < ApplicationController def new @good = Good.new @good.photos.build() end def create @good = Good.new(good_params) respond_to do |format| if @good.save! params[:good_photos][:image].each do |image| @good.photos.create(image: image, good_id: @good.id) end format.html{redirect_to root_path} else @good.photos.build format.html{render action: 'new'} end end end def good_params params.require(:good).permit(:category_id, :brand, :name, :condition, :discription, :size, :delivery_type, :prefecture, :day, :fee, photos_attributes: [:image]).merge(user_id: current_user.id) end end.merge(user_id: current_user.id)は誰が出品したかをgoodデーブルに保存してくれる記述です。もし、user登録機能または出品を単体でしたい場合はこの一文を消してくださいまし。
haml
出品なのでnewになります。
new.html.haml= form_for @good , html: {id: "item-dropzone"} do |f| .upload-box .upload-box__head %h3.bigger 出品画像 %span 必須 %p.discription 最大10枚までアップロードできます -# ここからが複数画像出品の部分です = f.fields_for :photos do |image| .dropzone-container #preview .dropzone-area = image.label :image, class: "dropzone-box", for: "upload-image" do .input_area = image.file_field :image, multiple: true, name: 'good_photos[image][]', id: "upload-image", class: "upload-image", 'data-image': 0 %p ここをクリックしてください .dropzone-container #preview2 .dropzone-area2 = image.label :image, class: "dropzone-box", for: "upload-image" do %p ここをクリックしてください -# 複数画像出品終わり、以下は様々なフォームがある想定10枚の画像を取り込むためにinputタグを10個用意すんのかい、せんのかいと不安でしたが、どうやら
= image.file_field :image, multiple: true
の一文で対応してくれそう!やったね!
クラスについてですが.dropzone-areaに画像が放り込まれると#previewに表示されるイメージです。
おいおいsyomaさん。消去・編集ボタンないけど!?(キレ気味)
ご安心を!javascript側で画像が投稿された瞬間に消去・編集ボタンを#previewに差し込むという記述にしております。scss
new.scss//image投稿欄のCSS .dropzone-container{ display: block; margin: 16px auto 0; display: flex; //プレビュー表示欄のCSS #preview , #preview2{ display: flex; .img_view { height: 162px; width: 112px; margin: 0 15px 10px 0; img{ width: 112px; height: 112px; } } .btn_wrapper { display: flex; text-align: center; .btn.edit { color: #00b0ff; width: 50%; height: 50px; line-height: 50px; border: 1px solid #eee; background: #f5f5f5; cursor: pointer; } .btn.delete { color: #00b0ff; width: 50%; height: 50px; line-height: 50px; border: 1px solid #eee; background: #f5f5f5; cursor: pointer; } } } //投稿クリックエリアのCSS .dropzone-area { margin-bottom: 10px; width: 620px; .dropzone-box { display: block; border: 1px dashed #ccc; position: relative; background: #f5f5f5; width: 100%; height: 162px; cursor: pointer; p { position: absolute; top: 50%; left: 16px; right: 16px; text-align: center; font-size: 14px; line-height: 1.5; font-weight: bold; -webkit-transform: translate(0, -50%); transform: translate(0, -50%); pointer-events: none; white-space: pre-wrap; word-wrap: break-word; } .input_area { display: none; } } } //投稿クリックエリアのCSS .dropzone-area2{ display: none; margin-bottom: 10px; width: 620px; .dropzone-box { display: block; border: 1px dashed #ccc; position: relative; background: #f5f5f5; width: 100%; height: 162px; cursor: pointer; p { position: absolute; top: 50%; left: 16px; right: 16px; text-align: center; font-size: 14px; line-height: 1.5; font-weight: bold; -webkit-transform: translate(0, -50%); transform: translate(0, -50%); pointer-events: none; white-space: pre-wrap; word-wrap: break-word; } .input_area { display: none; } } } }.dropzone-area2はdisplay: none;にしております。javascript側で5枚目が選択された時点で表示される仕組みにするためです。
javascript
new_good.js$(document).on('turbolinks:load', function(){ var dropzone = $('.dropzone-area'); var dropzone2 = $('.dropzone-area2'); var dropzone_box = $('.dropzone-box'); var images = []; var inputs =[]; var input_area = $('.input_area'); var preview = $('#preview'); var preview2 = $('#preview2'); $(document).on('change', 'input[type= "file"].upload-image',function(event) { var file = $(this).prop('files')[0]; var reader = new FileReader(); inputs.push($(this)); var img = $(`<div class= "img_view"><img></div>`); reader.onload = function(e) { var btn_wrapper = $('<div class="btn_wrapper"><div class="btn edit">編集</div><div class="btn delete">削除</div></div>'); img.append(btn_wrapper); img.find('img').attr({ src: e.target.result }) } reader.readAsDataURL(file); images.push(img); if (images.length <= 4) { $('#preview').empty(); $.each(images, function(index, image) { image.data('image', index); preview.append(image); }) dropzone.css({ 'width': `calc(100% - (20% * ${images.length}))` }) // 画像が5枚のとき1段目の枠を消し、2段目の枠を出す } else if (images.length == 5) { $("#preview").empty(); $.each(images, function(index, image) { image.data("image", index); preview.append(image); }); dropzone2.css({ display: "block" }); dropzone.css({ display: "none" }); preview2.empty(); // 画像が6枚以上のとき } else if (images.length >= 6) { // 1〜5枚目の画像を抽出 var pickup_images1 = images.slice(0, 5); // 1〜5枚目を1段目に表示 $('#preview').empty(); $.each(pickup_images1, function(index, image) { image.data('image', index); preview.append(image); }) // 6枚目以降の画像を抽出 var pickup_images2 = images.slice(5); // 6枚目以降を2段目に表示 $.each(pickup_images2, function(index, image) { image.data('image', index + 5); preview2.append(image); }) dropzone.css({ 'display': 'none' }) dropzone2.css({ 'display': 'block', 'width': `calc(100% - (20% * ${images.length - 5}))` }) // 画像が10枚になったら枠を消す if (images.length == 10) { dropzone2.css({ display: "none" }); } } var new_image = $(`<input multiple= "multiple" name="good_photos[image][]" class="upload-image" data-image= ${images.length} type="file" id="upload-image">`); input_area.prepend(new_image); }); $(document).on('click', '.delete', function() { var target_image = $(this).parent().parent(); $.each(inputs, function(index, input){ if ($(this).data('image') == target_image.data('image')){ $(this).remove(); target_image.remove(); var num = $(this).data('image'); images.splice(num, 1); inputs.splice(num, 1); if(inputs.length == 0) { $('input[type= "file"].upload-image').attr({ 'data-image': 0 }) } } }) $('input[type= "file"].upload-image:first').attr({ 'data-image': inputs.length }) $.each(inputs, function(index, input) { var input = $(this) input.attr({ 'data-image': index }) $('input[type= "file"].upload-image:first').after(input) }) if (images.length >= 5) { dropzone2.css({ 'display': 'block' }) $.each(images, function(index, image) { image.attr('data-image', index); preview2.append(image); }) dropzone2.css({ 'width': `calc(100% - (135px * ${images.length - 5}))` }) if(images.length == 9) { dropzone2.find('p').replaceWith('<i class="fa fa-camera"></i>') } if(images.length == 8) { dropzone2.find('i').replaceWith('<p>ココをクリックしてください</p>') } } else { dropzone.css({ 'display': 'block' }) $.each(images, function(index, image) { image.attr('data-image', index); preview.append(image); }) dropzone.css({ 'width': `calc(100% - (135px * ${images.length}))` }) } if(images.length == 4) { dropzone2.css({ 'display': 'none' }) } if(images.length == 3) { dropzone.find('i').replaceWith('<p>ココをクリックしてください</p>') } }) });画像の枚数によってif文で条件分岐をしております。ドチャクソ長いですがご理解ください。。。
参考文献
https://qiita.com/shinnosuke960801/items/66f2a511803d7dac53a3
https://qiita.com/yamayu_504/items/bdde3eeb9ae06a3876bc
https://kolosek.com/carrierwave-upload-multiple-images/まとめ
ご指摘がございましたら編集リクエストをよろしくお願いいたします!
このままeditも書きたいところですが、時間との兼ね合いもあり少し先になりそうです(すまんの)。。。
参考になった方は是非「いいね」していただけると幸いです!
最後までご覧いただきありがとうございました。
- 投稿日:2020-02-18T11:48:19+09:00
タッチイベント(マルチ対応)JavaScript サンプルコード
タッチイベント(マルチ対応)JavaScript サンプルコード
簡潔で理解しやすいタッチイベント(マルチ)のコードが見当たらなかったので、
以下のページを参考にリファクタリングしてみた。https://developer.mozilla.org/ja/docs/Web/API/Touch_events
https://mdn.mozillademos.org/ja/docs/Web/API/Touch_events$samples/Example
タッチイベントで渡ってくる情報って、ev.pageX, _ev.pageY なのね、、、使いにくいなぁ。
具体的には「そのまま canvas に描くとマージンの分描画位置がズレる」という問題がある(元のコードから)。どうしたものか、、、。
(以下のコードはそのズレの対策はしていないです。なぜ、ev.offsetX, _ev.offsetY をくれないのか、、、)リファクタリングした結果
リファクタリングした結果がこちら。
ご注意:
・スマホのブラウザで確認しないとタッチイベントは発生しないことにご注意ください
・PCのブラウザで起動した場合でも動作が確認できるように mousemove イベントのみ拾っていますが、あくまで確認用(メインのコードではない)なのでこちらもご注意くださいhttps://codepen.io/yamazaki3104/pen/QWbbBOK
See the Pen Javascript-touch-event-test by yamazaki.3104 (@yamazaki3104) on CodePen.
コードはこちら、参考まで
参考にしたコードの原型をとどめていないのは、、、、あえて触れないでおこう。いや、すでに「同じ機能」ではなくなっているから「リファクタリング」とは言えないのではないか??
<!DOCTYPE html> <html><body> <canvas id="canvas" width="600" height="600" style="border:solid black 1px;"> Your browser does not support canvas element. </canvas> <script> class PointTable // 描画用の色と座標を覚えるためのクラス { constructor( _table_max = 10 ) { this.color = [] this.x = [] this.y = [] this.table_max = _table_max for ( let i=0 ; i<_table_max ; i++ ) { this.color.push( `hsla( ${ i / this.table_max * 360 }, 100%, 33%, 0.8 )` ) this.x.push( 0 ) this.y.push( 0 ) } } get_idx( _i ) { const i = ( _i < 0 ? -_i : _i ) // abs return i % this.table_max } get_color( _tc ) { const r = this.get_idx( _tc.identifier ) return this.color[r] } get_xy( _tc ) { const r = this.get_idx( _tc.identifier ) return { x: this.x[r], y: this.y[r] } } set_xy( _tc ){ const r = this.get_idx( _tc.identifier ) this.x[r] = _tc.pageX this.y[r] = _tc.pageY } } const xy_tbl = new PointTable() const elm = document.querySelector( 'canvas#canvas' ) const ctx = elm.getContext( '2d' ) // changedTouches を回す。 Array ではなく Object なんですよ。 Why? const for_touches = ( _evt, _fnc ) => { for ( let i=0 ; i<_evt.changedTouches.length ; i++ ) _fnc( _evt.changedTouches[i] ) } const ctx_draw_line = ( _t ) => { // ctxに線を描く const p = xy_tbl.get_xy( _t ) ctx.beginPath() ctx.moveTo( p.x, p.y ) ctx.lineTo( _t.pageX, _t.pageY ) ctx.lineWidth = 4 ctx.strokeStyle = xy_tbl.get_color( _t ) ctx.stroke() xy_tbl.set_xy( _t ) } elm.addEventListener( "touchstart", ( _evt ) => { _evt.preventDefault() for_touches( _evt, ( _t ) => xy_tbl.set_xy( _t ) ) }, false ) elm.addEventListener( "touchmove", ( _evt ) => { _evt.preventDefault() for_touches( _evt, ( _t ) => ctx_draw_line( _t ) ) }, false ) elm.addEventListener( "touchend", ( _evt ) => { _evt.preventDefault() for_touches( _evt, ( _t ) => { ctx_draw_line( _t ) ctx.fillStyle = xy_tbl.get_color( _t ) ctx.fillRect( _t.pageX - 4, _t.pageY - 4, 8, 8 ) }) }, false ) //---------------- // PC用 mousemove イベントでの動作確認コード elm.addEventListener( "mousemove", ( _evt ) => { _evt.preventDefault() ctx_draw_line( { identifier: 0, pageX: _evt.pageX, pageY: _evt.pageY } ) }, false ) </script> </body> </html>canvas でできること
このサンプルコードでは、タッチイベントの結果をシンプルに表現するために、canvas に描画している。
canvas でできることは、このあたりにまとまっています。参考まで。https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D
まとめ
コードを見ていただければ、かなり短いし、特に解説するほどではないいけど、
・changedTouches とかに複数の情報がまとまって渡っていくるところがタッチイベントのポイント
・で、changedTouches は Array ではなく Object なところもポイント(Arrayにしてほしいなぁ もっと簡潔に書きたいから)
・あとは、offsetX, offsetY が「無い」ところもポイントかな(いつかサポートされるのでしょうか、、、遠い目)・情報保存用のクラス、PointTable の _table_max は 10 にしてるけど、もっと少なくてもいいかもしれない。3点以上来てもどう処理していいかわからないしね。
このコードが、みなさまのプログラミングライフの一助になれば幸いです。
- 投稿日:2020-02-18T11:13:36+09:00
Vuetifyのv-comboboxで自由に入力された文字列を取得したい
v-comboboxを使うということは自由入力させたいということなのに、自由入力した文字をうまく取得できない!
なぜか微妙にハマったのでメモっておきます。例のとおりリスト要素から選択された際は
selecteditem
にオブジェクトがバインドされるまではいいが、では自由に入力された値は?
オフィシャルのドキュメントをよく読むと、、valueプロパティは The input's value、つまり入力された値はvalue
プロパティに入る、と記載されております。そこでselecteditemのハンドリングとも併せて検証してみました。結果、
:value="inputedvalue"
のバインドだと入力された値は取得できず、ref="comb"
で$refs経由の取得がうまくいきました。<v-combobox v-model="selecteditem" :items="items" item-text="name" item-value="id" return-object ref="comb" :value="inputedvalue"> </v-combobox>selecteditem: null, items : [ { id:"001", name:"牛丼" }, { id:"002", name:"ピザ" }, { id:"003", name:"ラーメン" }, ]<script> var val = ""; // バインド経由では取れない val = this.inputedvalue; // $refs経由でプロパティ直見すると取れる val = this.$refs.comb.value; // itemsの中の選択肢から選ぶ判定はv-modelがobjectになってることを確認すればよい if(this.selecteditem && this.selecteditem.id) { val = this.selecteditem.id + " / " + this.selecteditem.name; } alert(val); </script>
- 投稿日:2020-02-18T11:10:50+09:00
JavaScriptで日付の判定をする
- 投稿日:2020-02-18T10:54:21+09:00
IM-FormaDesignerでテキストボックスの入力可否をチェックボックスで制御する
はじめに
intra-martのIM-FormaDesignerでは、ローコード開発を行うことが可能です。
画面上の制御が必要な場合は、JavaScriptでスクリプトを実行することも可能です。やったこと
IM-FormaDesignerでテキストボックスの入力可否をチェックボックスで制御した。
if( document.getElementsByName("checkbox")[0].value == 1){ // テキストボックスの背景色をグレーに変更する $('input[name="textbox"]').css({ 'background-color': '#CCCCCC' }); // 透明の箱をテキストボックスに重ねて入力不可にする document.getElementById('clearbox').style.left = '20px'; } else { // テキストボックスの背景色を白に変更する $('input[name="textbox"]').css({ 'background-color': '#FFFFFF' }); // 透明の箱を元の位置に戻して入力可にする document.getElementById('clearbox').style.left = '520px'; }わかったこと
JavaScriptを使用してCSSの要素を制御することで、パッケージ製品やフレームワークの制約がある中でも、カスタマイズなしで画面制御を行うことが可能である。なぜならば、要素の取得と書き換えがプログラムで制御できれば、自由に制御ができるからです。
つぎにやること
パッケージ製品やフレームワークとの結合度を意識しながら、他の実装を検討する
メリット
バージョンアップによる影響を最低限に抑えられる
- 投稿日:2020-02-18T10:35:57+09:00
Javascriptで動的にファイルを読み込む
はじめに
ウェブサイトで外部ライブラリを読み込むというのはよくやる行為だが、それが多くなってきたり、外部ライブラリを踏み台に自作ライブラリを作る(&同じサイト内で使い回す)となると読み込みの管理も大変になってくる。
ので、その辺を自動化する関数を作ってみた。headに読み込みリンクを生成する
二重読み込みの防止装置もつけてある。
javascriptfunction insert_link(tagname, obj, raw_url){ // 差し込む要素の生成 var target_tag = document.createElement(tagname); var keylist = Object.keys(obj); var currentkey; for(let int = 0; int < keylist.length; int++){ currentkey = keylist[int]; target_tag[currentkey] = obj[currentkey]; } // その要素がhead内にすでに存在するかどうかチェックし、存在しない場合のみ読み込みを行う var head = document.querySelector('head'); var headtext = head.innerHTML; var processed_url = raw_url.replace(/\//g, '\\/'); processed_url = processed_url.replace(/\./g, '\\.'); var regexstr = new RegExp( '=["\']' + processed_url + '["\']', 'g'); if(!regexstr.test(headtext)){ head.appendChild(target_tag); } return target_tag; }使用例
html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>無題</title> </head> <body> <script type="text/javascript"> (function(){ function insert_link(tagname, obj, raw_url){ // 差し込む要素の生成 var target_tag = document.createElement(tagname); var keylist = Object.keys(obj); var currentkey; for(let int = 0; int < keylist.length; int++){ currentkey = keylist[int]; target_tag[currentkey] = obj[currentkey]; } // その要素がhead内にすでに存在するかどうかチェックし、存在しない場合のみ読み込みを行う var head = document.querySelector('head'); var headtext = head.innerHTML; var processed_url = raw_url.replace(/\//g, '\\/'); processed_url = processed_url.replace(/\./g, '\\.'); var regexstr = new RegExp( '=["\']' + processed_url + '["\']', 'g'); if(!regexstr.test(headtext)){ head.appendChild(target_tag); } return target_tag; } // 使うときは、キーを属性名、値を属性値としたオブジェクト(下のやつ)に設定を書き込みます // CSSファイルの場合 var list01 = { rel: "stylesheet", href: "https://example.com/css/style.css", } // Jsファイルの場合 var list02 = { type: "text/javascript", charset: "UTF-8", src: "https://example.com/js/testscript.js", } // 引数は左から順にタグ名、設定オブジェクト名、読込先です。 insert_link('link', list01, list01.href); // 上で挙げた通り、既に読み込まれているときは自動で処理中止してくれます。 insert_link('link', list01, list01.href); // Jsの場合 insert_link('script', list02, list02.src); // 上で挙げた通り、既に読み込まれているときは自動で処理中止してくれます。 insert_link('script', list02, list02.src); })(); </script> </body> </html>実行例
読み込みが完了したのを確認してから処理を実行
実は、上のコードだけだとある問題が発生する。
ライブラリの読み込みが、本命のjavascriptよりも後になってしまうのだ。
これでは、せっかく読み込んだライブラリの意味がない。これを防ぐため、実際のコードでは以下の文を付け足して使う。
javascript// insert_linkが実行された時点で、ライブラリの読み込み処理は終わっています。 var madedom = insert_link('script', list02, list02.src); madedom.onload = function(){ /* ~~ライブラリの読み込み完了後に行いたい処理をここに書く~~ */ }onloadを使うことで、読込みの完了を待つことができるのだ。
多少速度は落ちるが、エラーが出るよりかはマシだろう。全体のコード
html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>無題</title> </head> <body> <script type="text/javascript"> (function(){ function insert_link(tagname, obj, raw_url){ // 差し込む要素の生成 var target_tag = document.createElement(tagname); var keylist = Object.keys(obj); var currentkey; for(let int = 0; int < keylist.length; int++){ currentkey = keylist[int]; target_tag[currentkey] = obj[currentkey]; } // その要素がhead内にすでに存在するかどうかチェックし、存在しない場合のみ読み込みを行う var head = document.querySelector('head'); var headtext = head.innerHTML; var processed_url = raw_url.replace(/\//g, '\\/'); processed_url = processed_url.replace(/\./g, '\\.'); var regexstr = new RegExp( '=["\']' + processed_url + '["\']', 'g'); if(!regexstr.test(headtext)){ head.appendChild(target_tag); } return target_tag; } // Jsファイルの場合 var list02 = { type: "text/javascript", charset: "UTF-8", src: "https://example.com/js/testscript.js", } // Js var madedom = insert_link('script', list02, list02.src); madedom.onload = function(){ /* ~~ライブラリの読み込み完了後に行いたい処理をここに書く~~ */ } })(); </script> </body> </html>おわりに
間違いなどありましたら編集リクエストおねがいします。
- 投稿日:2020-02-18T09:25:38+09:00
Javascriptの関数で可変長引数を利用する方法 | Array.prototype.slice.call()
*この記事は、「Javascriptで可変長引数をスグにでも使いたい」と言う人を対象に書いております。細かいことは省いていますので、突っ込んだことを知りたい方は他のページを調べてください。
Rubyには普通に存在する可変長引数ですが、Javascriptで利用するには少し工夫が必要になります。
と言っても、Array.prototype.slice.call()を使うだけです。
以下、例をあげます。
hatsumori.jsvar test = function() { var args = Array.prototype.slice.call(arguments); console.log(args); } test('ななまる', 'イマドキ', 'ショパン', 'アカデミー') // ['ななまる', 'イマドキ', 'ショパン', 'アカデミー']関数内でargumentsは渡された引数を渡してくるのですが、そのままでは「配列っぽい何か」であり、配列として加工できません。
Array.prototype.slice.call(arguments)で、配列として加工できるようになります。
- 投稿日:2020-02-18T09:04:04+09:00
JavaScriptの日付フォーマットにうるさいあの人も満足? formatDate関数つくりました
概要
- とある事情で、JavaScriptで日付(Dateオブジェクト)を
yyyy-MM-dd'T'HH:mm:ssXXX
のようにパターン指定でフォーマットする関数を作りました- 外部ライブラリ不要で、そこそこ欲張っています
- ISO8601形式、RFC1123形式、タイムゾーンRFC822など表現可能
- パターン仕様はJavaのSimpleDateFormatライクにしています
コード
以下が、作成した関数formatDate
function formatDate(formatStr, date, opts) { if (!date) { date = new Date(); } opts = opts || {}; let _days = opts.days; if (!_days) { _days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; } let _months = opts.months; if (!_months) { _months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; } const pad = (number, strDigits, isUnpad) => { const strNum = number.toString(); if (!isUnpad && strNum.length > strDigits.length) { return strNum; } else { return ('0000' + strNum).slice(-strDigits.length); } }; const timezone = (date, letter) => { const chunk = []; const offset = -date.getTimezoneOffset(); chunk.push(offset === 0 ? 'Z' : offset > 0 ? '+' : '-');//add Z or +,- if (offset === 0) return chunk; chunk.push(pad(Math.floor(offset / 60), '00'));//hour if (letter === 'X') return chunk.join(''); if (letter === 'XXX') chunk.push(':'); chunk.push(pad((offset % 60), '00'));//min return chunk.join(''); }; const DELIM = '\0\0'; const escapeStack = []; const escapedFmtStr = formatStr.replace(/'.*?'/g, m => { escapeStack.push(m.replace(/'/g, '')); return `${DELIM}${escapeStack.length - 1}${DELIM}`; }); const formattedStr = escapedFmtStr .replace(/y{4}|y{2}/g, m => pad(date.getFullYear(), m, true)) .replace(/M{3}/g, m => _months[date.getMonth()]) .replace(/M{1,2}/g, m => pad(date.getMonth() + 1, m)) .replace(/M{1,2}/g, m => pad(date.getMonth() + 1, m)) .replace(/d{1,2}/g, m => pad(date.getDate(), m)) .replace(/H{1,2}/g, m => pad(date.getHours(), m)) .replace(/h{1,2}/g, m => { const hours = date.getHours(); return pad(hours === 0 ? 12 : hours > 12 ? hours - 12 : hours, m); }) .replace(/a{1,2}/g, m => date.getHours() >= 12 ? 'PM' : 'AM') .replace(/m{1,2}/g, m => pad(date.getMinutes(), m)) .replace(/s{1,2}/g, m => pad(date.getSeconds(), m)) .replace(/S{3}/g, m => pad(date.getMilliseconds(), m)) .replace(/[E]+/g, m => _days[date.getDay()]) .replace(/[Z]+/g, m => timezone(date, m)) .replace(/X{1,3}/g, m => timezone(date, m)); const unescapedStr = formattedStr.replace(new RegExp(`${DELIM}\\d+${DELIM}`, 'g'), m => { const unescaped = escapeStack.shift(); return unescaped.length > 0 ? unescaped : '\''; }); return unescapedStr; }サンプルコード
以下のように使う
const date = new Date(); //ISO8601 console.log(formatDate("yyyyMMdd'T'HHmmssXX",date)); //->20200218T015020+0900 //ISO8601 Ex. console.log(formatDate("yyyy-MM-dd'T'HH:mm:ssXXX",date)); //->2020-02-18T01:50:20+09:00 //RFC1123(RFC822) console.log(formatDate("E, dd MMM yyyy HH:mm:ss Z",date)); //->Tue, 18 Feb 2020 01:50:20 +0900 //曜日を日本語に console.log(formatDate("yyyy年MM月dd日(E曜日) HH時mm分ss秒SSS",date,{ days:['日','月','火','水','木','金','土'] })); //->2020年02月18日(火曜日) 01時50分20秒789 //エスケープ(対象をシングルクォート"'"で囲む。シングルクォート自体は"''"で表現) console.log(formatDate("'Hello!' It'''s' H:m:s.",date)); //->Hello! It's 1:50:20.ライブデモ(pen)
See the Pen ZEGWdKE by Tom Misawa (@riversun) on CodePen.
パターン指定方法の詳細
- パターンとして
H:m:s
のように1文字指定した場合、時間が7時なら7
のみ、17時なら17
のように表示されるが、HH:mm:ss
のように2文字指定した場合には7時なら07
のように左から0埋め(zero padding)される。- パターン内に文字列を含めたい場合は
'Hello' hh:mm:ss
のように文字列部をシングルクォート(')で文字列を囲むとエスケープされる。- 記法はJavaのSimpleDateFormatライク
パターン指定方法一覧
文字 内容 例 yyyy 年 2018 yy (年を2桁表示にする) 18 M 月 7 MM 月(0埋め) 07 MMM 月名(英語圏等向け) Jul d 日付 17 dd 日付(0埋め) 17 a AM/PMのマーカー PM H 時(0-23) 12 HH 時(0-23)(0埋め) 12 E 曜日 Tue h 時(午前午後を1-12で) 12 hh 時(0埋め) 12 m 分 8 mm 分(0埋め) 08 s 秒 56 ss 秒(0埋め) 56 S ミリ秒 789 Z RFC822形式のタイムゾーン +0900 X ISO8601形式のタイムゾーン(1) +09 XX ISO8601形式のタイムゾーン(2) +0900 XXX ISO8601形式のタイムゾーン(3) +09:00 ' ' シングルクォートで囲むと中の文字をエスケープする '' シングルクォート自体を表示したいとき パターン指定いろいろな例
パターン 例 yyyyMMdd'T'HHmmssXX 20180717T120856+0900 yyyy-MM-dd'T'HH:mm:ssXXX 2018-07-17T12:08:56+09:00 E, dd MMM yyyy HH:mm:ss Z Tue, 17 Jul 2018 12:08:56 +0900 yyyy.MM.dd 'at' hh:mm:ss Z 2018.07.17 at 12:08:56 +0900 EEE, MMM d, ''yy Tue, Jul 17, '18 h:mm a 12:08 PM hh 'o''''clock' a, X 12 o'clock PM, +09 yyyy年M月d日 H時m分s秒 2018年7月17日 12時8分56秒 yyyy年MM月dd日 HH時mm分ss秒 2018年07月17日 12時08分56秒 yyyyMMddHHmmssSSS 20180717120856789 おまけ
間違いやすいAM,PMの概念
アメリカなど12時制が慣用されている国は
hh:mm:ss
(hは1-12を表すパターン)を使うことが多い。
以下のようにAM/PMは夜(midnight)の12時が12:00AM。昼(noon)の12時が12:00PMとなるので、実装にもそれを反映した。
24時制 12時制 0 12AM 1 1AM 2 2AM 3 3AM 4 4AM 5 5AM 6 6AM 7 7AM 8 8AM 9 9AM 10 10AM 11 11AM 12 12PM 13 1PM 14 2PM 15 3PM 16 4PM 17 5PM 18 6PM 19 7PM 20 8PM 21 9PM 22 10PM 23 11PM まとめ
- JavaScriptでDateをフォーマットする自作関数をご紹介しました
- ソースコード
- もう少し使い勝手を考えライブラリ化したバージョンを以下のリポジトリにあります
- https://github.com/riversun/simple-date-format
- 投稿日:2020-02-18T09:04:04+09:00
JavaScriptの日付フォーマットにうるさいあの人も満足させたいformatDate関数
概要
- とある事情で、JavaScriptで日付(Dateオブジェクト)を
yyyy-MM-dd'T'HH:mm:ssXXX
のようにパターン指定でフォーマットする関数を作りました- 外部ライブラリ不要で、そこそこ欲張っています
- ISO8601形式、RFC1123形式、タイムゾーンRFC822など表現可能
- パターン仕様はJavaのSimpleDateFormatライクにしています
コード
以下が、作成した関数formatDate
function formatDate(formatStr, date, opts) { if (!date) { date = new Date(); } opts = opts || {}; let _days = opts.days; if (!_days) { _days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; } let _months = opts.months; if (!_months) { _months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; } const pad = (number, strDigits, isUnpad) => { const strNum = Math.abs(number).toString(); if (!isUnpad && strNum.length > strDigits.length) { return strNum; } else { return ('0000' + strNum).slice(-strDigits.length); } }; const timezone = (date, letter) => { const chunk = []; const offset = -date.getTimezoneOffset(); chunk.push(offset === 0 ? 'Z' : offset > 0 ? '+' : '-');//add Z or +,- if (offset === 0) return chunk; chunk.push(pad(Math.floor(offset / 60), '00'));//hour if (letter === 'X') return chunk.join(''); if (letter === 'XXX') chunk.push(':'); chunk.push(pad((offset % 60), '00'));//min return chunk.join(''); }; const DELIM = '\0\0'; const escapeStack = []; const escapedFmtStr = formatStr.replace(/'.*?'/g, m => { escapeStack.push(m.replace(/'/g, '')); return `${DELIM}${escapeStack.length - 1}${DELIM}`; }); const formattedStr = escapedFmtStr .replace(/y{4}|y{2}/g, m => pad(date.getFullYear(), m, true)) .replace(/M{3}/g, m => _months[date.getMonth()]) .replace(/M{1,2}/g, m => pad(date.getMonth() + 1, m)) .replace(/M{1,2}/g, m => pad(date.getMonth() + 1, m)) .replace(/d{1,2}/g, m => pad(date.getDate(), m)) .replace(/H{1,2}/g, m => pad(date.getHours(), m)) .replace(/h{1,2}/g, m => { const hours = date.getHours(); return pad(hours === 0 ? 12 : hours > 12 ? hours - 12 : hours, m); }) .replace(/a{1,2}/g, m => date.getHours() >= 12 ? 'PM' : 'AM') .replace(/m{1,2}/g, m => pad(date.getMinutes(), m)) .replace(/s{1,2}/g, m => pad(date.getSeconds(), m)) .replace(/S{3}/g, m => pad(date.getMilliseconds(), m)) .replace(/[E]+/g, m => _days[date.getDay()]) .replace(/[Z]+/g, m => timezone(date, m)) .replace(/X{1,3}/g, m => timezone(date, m)); const unescapedStr = formattedStr.replace(new RegExp(`${DELIM}\\d+${DELIM}`, 'g'), m => { const unescaped = escapeStack.shift(); return unescaped.length > 0 ? unescaped : '\''; }); return unescapedStr; }サンプルコード
関数の呼び出し方は以下のとおり
const date = new Date(); //パターンを指定してフォーマットできる console.log(formatDate("yyyy年MM月dd日 HH時mm分ss秒SSS",date)); //->2020年02月18日 01時50分20秒789 //曜日も表示。日本語で曜日表示するときは曜日名を与える console.log(formatDate("yyyy年MM月dd日(E曜日) HH時mm分ss秒SSS",date,{ days:['日','月','火','水','木','金','土'] })); //->2020年02月18日(火曜日) 01時50分20秒789 //ISO8601形式(1)でフォーマット console.log(formatDate("yyyyMMdd'T'HHmmssXX",date)); //->20200218T015020+0900 //ISO8601形式(2)でフォーマット console.log(formatDate("yyyy-MM-dd'T'HH:mm:ssXXX",date)); //->2020-02-18T01:50:20+09:00 //RFC1123(RFC822)形式でフォーマット console.log(formatDate("E, dd MMM yyyy HH:mm:ss Z",date)); //->Tue, 18 Feb 2020 01:50:20 +0900 //2019を「'19」みたいにフォーマットするときはyyにする console.log(formatDate("EEE, MMM d, ''yy",date)); //->Tue, Feb 18, '20 01:50:20 +0900 //かぶる文字列を混ぜる場合はエスケープ(対象をシングルクォート"'"で囲む。シングルクォート自体は"''"で表現) console.log(formatDate("'Hello!' It'''s' H:m:s.",date)); //->Hello! It's 1:50:20.See the Pen eYNzpBO by Tom Misawa (@riversun) on CodePen.
パターン指定方法の詳細
- パターンとして
H:m:s
のように1文字指定した場合、時間が7時なら7
のみ、17時なら17
のように表示されるが、HH:mm:ss
のように2文字指定した場合には7時なら07
のように左から0埋め(zero padding)される。- パターン内に文字列を含めたい場合は
'Hello' hh:mm:ss
のように文字列部をシングルクォート(')で文字列を囲むとエスケープされる。- 記法はJavaのSimpleDateFormatライク
パターン指定方法一覧
文字 内容 例 yyyy 年 2018 yy (年を2桁表示にする) 18 M 月 7 MM 月(0埋め) 07 MMM 月名(英語圏等向け) Jul d 日付 17 dd 日付(0埋め) 17 a AM/PMのマーカー PM H 時(0-23) 12 HH 時(0-23)(0埋め) 12 E 曜日 Tue h 時(午前午後を1-12で) 12 hh 時(0埋め) 12 m 分 8 mm 分(0埋め) 08 s 秒 56 ss 秒(0埋め) 56 S ミリ秒 789 Z RFC822形式のタイムゾーン +0900 X ISO8601形式のタイムゾーン(1) +09 XX ISO8601形式のタイムゾーン(2) +0900 XXX ISO8601形式のタイムゾーン(3) +09:00 ' ' シングルクォートで囲むと中の文字をエスケープする '' シングルクォート自体を表示したいとき パターン指定いろいろな例
パターン 例 yyyyMMdd'T'HHmmssXX 20180717T120856+0900 yyyy-MM-dd'T'HH:mm:ssXXX 2018-07-17T12:08:56+09:00 E, dd MMM yyyy HH:mm:ss Z Tue, 17 Jul 2018 12:08:56 +0900 yyyy.MM.dd 'at' hh:mm:ss Z 2018.07.17 at 12:08:56 +0900 EEE, MMM d, ''yy Tue, Jul 17, '18 h:mm a 12:08 PM hh 'o''''clock' a, X 12 o'clock PM, +09 yyyy年M月d日 H時m分s秒 2018年7月17日 12時8分56秒 yyyy年MM月dd日 HH時mm分ss秒 2018年07月17日 12時08分56秒 yyyyMMddHHmmssSSS 20180717120856789 ライブデモ(pen)
See the Pen ZEGWdKE by Tom Misawa (@riversun) on CodePen.
おまけ
間違いやすいAM,PMの概念
アメリカなど12時制が慣用されている国は
hh:mm:ss
(hは1-12を表すパターン)を使うことが多い。
以下のようにAM/PMは夜(midnight)の12時が12:00AM。昼(noon)の12時が12:00PMとなるので、実装にもそれを反映した。
24時制 12時制 0 12AM 1 1AM 2 2AM 3 3AM 4 4AM 5 5AM 6 6AM 7 7AM 8 8AM 9 9AM 10 10AM 11 11AM 12 12PM 13 1PM 14 2PM 15 3PM 16 4PM 17 5PM 18 6PM 19 7PM 20 8PM 21 9PM 22 10PM 23 11PM まとめ
- JavaScriptでDateをフォーマットする自作関数をご紹介しました
- ソースコード
- もう少し使い勝手を考えライブラリ化したバージョンを以下のリポジトリにあります
- https://github.com/riversun/simple-date-format
- 投稿日:2020-02-18T06:20:47+09:00
Cloud FunctionsでFirestoreの更新を特定のデバイスにpush通知する
Cloud Functions × Firestore × Firebase Cloud Messaging
Cloud Functionsはサーバレスコンピューティングサービスです。様々なイベントをトリガーにしてアクションを実行することができ非常にコストパフォーマンスに優れています。
今回はこのCloud Functionsを用いてFirestoreにデータが追加されたら特定のデバイスにpush通知が届くようにします。
Structure
通知を送信したいDBを設計します。今回は簡略化のために
notifications
というコレクションにしています。
ここであらかじめテーブルにデバイスのidが紐付いているとやりやすいと思います。notifications │ ├── notification │ ├── text │ └── users . ├── bJi8iJLw0ufF1HCO17v3b7UTVS53 . . . .
Architecture
Steps
以下のことが既に完了している前提で実装していきます。多分この記事にたどり着くような人はコーディングで行き詰まった人だと思います。Firestoreのサンプルがここしかなかった。。。
- Firebaseプロジェクトは作成済み
- 必要なパッケージはインストール済み
- クライアント側のリモート通知設定済み
1. Cloud Functionsをスタートする
まずプロジェクトを作成します
$ Firebase init
functionsディレクトリ内にある
index.js
にコードを記述していきます。2. コードを記述
index.jsconst functions = require('firebase-functions'); const admin = require('firebase-admin'); admin.initializeApp(); exports.sendNotifications = functions.firestore.document('notifications/{notificationId}').onCreate( async (snapshot) => { // Notification details. const newValue = snapshot.data(); const payload = { notification: { title:"新規ライブ", body: newValue.text + "の新しいライブが追加されました!", content_available: 'true', sound: "default", click_action: `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com`, } }; const userIDs = newValue.users // Get the list of device tokens. userIDs.forEach(function (value) { admin.firestore().collection('fcmTokens').doc(value).get() .then(function (querySnapshot) { let fcmToken = querySnapshot.data().fcmToken admin.messaging().sendToDevice(fcmToken, payload); return; }).catch(error => { return }); }); });なんやこれポイント
サンプルに
const allTokens = await admin.firestore().collection('fcmTokens').get();
ってあったのでてっきりFirestoreのfcmToken勝手にここに格納されてるのかと思いきや自分でDBつくるっぽかった。なので、userIdと紐付けて識別できるようにしました。fcmTokens │ ├── fcmToken(id: uid) │ └── fcmToken . .まず
functions.firestore.document()
の引数にコレクションを指定します。今回はテーブルに新たなドキュメントが追加されたときに発火するようにしたいのでonCreate()
を呼び出しています。
その他にもonUpdate()
、onDelete()
などさまざまなトリガーがあります。詳しくはこちら。そして、
payload
に通知の設定をします。
fcmTokens
というコレクションからfcmtokenをゲットすることができます。RealtimeDBより便利な気がする。最後に
sendToDevice()
で取得したユーザのトークンにまとめてpush通知を送信しています。3. Deploy
$ firebase deployなんか死んでたらログを確認してちょこちょこ直していきましょう。
4. Test
ためしにPythonからFirestoreに新しいドキュメントを追加してみます
import firebase_admin from firebase_admin import credentials from firebase_admin import firestore cred = credentials.Certificate() firebase_admin.initialize_app() db = firestore.client() notification_data = { "text": "MY FIRST STORY", "users": ["gHrdAxQhFYdTmoFjgi8RX8PljXv1"] } db.collection(u'notifications').document().set(notification_data)
任意のデバイスにpush通知が来ればオッケー。たっくーTV見てたのは内緒。
コードはこちら
- 投稿日:2020-02-18T02:35:00+09:00
pupetterを使ってAmazonの欲しいものリストの情報を取得する
概要
pupetterを使ってAmazon.co.jpから欲しい物リストをスクレイピングする手法のメモです。
下記のように欲しいものリストのIDを指定する事で、商品名と価格、商品IDを取得できる形にします。
実装// 欲しい物リストのIDを指定 const itemList = await amazonWishScraper.getProductInfo('hogehoge'); console.log('itemList', JSON.stringify(itemList));itemListの中身[ { "title" : "ProductA", "price": 3278, "productID": "429711111X" }, { "title" : "ProductB", "price": 1234, "productID": "429711112X" }, ]背景
Amazonの欲しいものリストをスクレイピングで取得する例は、何点か見つかりましたが、紹介されているものの多くは、現在は使えないものでした。
というのも以前は、一定数商品が存在する場合にはページの切り替え行なっていましたが、現在はAjaxで同じページ内で画面をスクロールする事で非同期に読み込む方法に変更されたためです。こうなると、GETリクエストを投げてHTMLの解析だけではなく、ブラウザの操作をエミュレートして、スクロールを行うようなヘッドレスブラウザが必要になります。
今回は、puppeteerというヘッドレスブラウザを使用して実現します。前提
MacOS 10.15.3(19D76)
VS Code 1.42.1
node.js v12.14.1
puppeteer 2.1.0実装
実装方法の検討
画面スクロール
前述の通り、一定数以上の商品が登録されている場合にはページのスクロールを行いデータを読み出す必要があります。
puppeteerにはブラウザ内で任意のjsが実行可能なので、画面をスクロールさせる処理を実行します。スクロールの停止(全商品読み込み完了の検知)
スクロールを続けた後に、全ての商品が読み込み終わった事を検知する必要があります。
欲しい物リストの末尾には、下図のようなリストの末尾を示す情報が表示されます。
この部分のタグが検知されたら、全ての商品の読み込みが終わったと判断します。該当箇所のタグ<div class="a-row center-align-text full-width-element"> <div id="no-items-section-anywhere" class="a-section a-spacing-none"> リスト最後 </div> </div>商品情報の取得
全ての情報が画面に描画されたのならば、後は通常のスクレイピングと同様にHTMLタグを解析するだけです。
実装コード
欲しいものリストサイトへのアクセス
puppeteerを初期化して、欲しいものリストのIDからURLを特定してアクセスします。
今回は表示される情報量を減らし、スクレイピングの効率を上げるためにデスクトップPCではなく、iPhone(スマホ)をシミュレートします。この部分の処理は、puppeteerの基本的な動作方法ですので詳細は割愛します。
const puppeteer = require('puppeteer'); const devices = require('puppeteer/DeviceDescriptors'); const iPhone = devices['iPhone 8']; const urlbase = 'https://www.amazon.co.jp/hz/wishlist/ls/'; async function getProductInfo(wishListId) { const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); try { const page = await initPage(browser); // 欲しいものリストのURLを開く await page.goto(urlbase + wishListId); // スクレイピング(中身は後述) return await scrapePage(page); } catch (err) { console.error(err); throw err; } finally { browser.close(); } } async function initPage(browser) { const page = await browser.newPage(); await page.emulate(iPhone); return page; }欲しいものリストサイトへのアクセス
実装方法の検討で説明した通り、
- リストの末尾が見つかるまで画面をスクロールする
- HTMLをquerySelectorで解析する
といったことを実施します。
querySelectorでのデータの切り出しは、生のHTMLを確認しながら商品ブロック単位で情報を取得するように対応しています。async function scrapePage(page) { return await page.evaluate(async () => { // スクロールでの移動距離と待機間隔ms const distance = 500; const delay = 100; // リストの末尾が検知されない限りループする while (!document.querySelector('#no-items-section-anywhere')) { // 500pxずつスクロール移動して、100ミリ秒待機する document.scrollingElement.scrollBy(0, distance); await new Promise(resolve => { setTimeout(resolve, delay); }); } // 全ての商品の表示が終わったらスクレイピングを実施 const itemList = []; // 商品の情報のBOX単位でデータを切り出す [...document.querySelectorAll('a[href^="/dp/"].a-touch-link')].forEach( el => { const productID = el .getAttribute('href') .split('/?coliid')[0] .replace('/dp/', ''); const title = el.querySelector('[id^="item_title_"]').textContent; let price = -1; const priceEle = el.querySelector('[id^="itemPrice_"] > span'); if (priceEle && priceEle.textContent) { price = Number(priceEle.textContent.replace('¥', '').replace(',', '')); } itemList.push({ price: price, title: title, productID: productID }); } ); return itemList; }); }まとめ
簡単にはですが、puppeteerを使ってAmazonの欲しいものリストから情報を一覧取得する法法を紹介しました。
スクロールをシミュレートし、非同期で読み込まれるデータの取得を実現しています。
こういったユーザの操作が必要となる処理に関しても、puppeteerを使う事で簡単に実現ができました。
- 投稿日:2020-02-18T00:33:27+09:00
axiosでエラーになった時のリトライ処理をinterceptorsで実行する
TL;DR
エラーオブジェクトにリクエスト時の設定が含まれているので、これを
axios.request
メソッドに渡す事で再実行することが出来ますimport axios from 'axios' const client = axios.create({ baseURL: 'https://hoge.example/api/v1/' }) client.interceptors.response.use( (response) => { return Promise.resolve(response) }, (err) => { // json文字列になっているままだとエラーになる為 if (typeof config.data === 'string') { config.data = JSON.parse(config.data) } return axios.request(err.config) } )リトライの無限ループ対策
configにプロパティを追加して、リトライ回数を見るように実装しました
import axios from 'axios' const LIMIT_RETRY_COUNT = 2 client.interceptors.response.use( (response) => { return Promise.resolve(response) }, (err) => { if (config.retryCount > LIMIT_RETRY_COUNT) { return Promise.reject(err) } if (Number.isInteger(config.retryCount)) { config.retryCount += 1 } else { config.retryCount = 1 } return axios.request(err.config) } )ただしaxiosのv0.19.0ではこのやり方(custom config)が効かなくなっているので、もしこのバージョンを使用している場合はバージョンを上げる必要があります。
0.18系は問題ありません。