- 投稿日:2020-03-24T22:40:28+09:00
Railsフリマアプリ payjpを使ってクレジットカードを登録・削除
payjp とは・・・?
クレジットカードを登録、変更、購入を行ってくれる便利なAPIです。
みんさんもフリマアプリ、ECサイトでクレジットカードを使って商品の購入すると思います。その時登録したクレジットカード情報はどこに保存されているでしょうか?私は運営するサイトで暗号化されて管理されているのだと思っていましたが、違うようです。たしかにサイトのデータベースにクレジットカード情報を登録するのはセキュリティ上よろしくないですよね。
このとき登場するのがpayjp
になります。payjpの仕組み
まずpayjpとフリマアプリではどのような流れでクレジットカードの処理が行われているのかざっくりとイメージしましょう!
私はここを飛ばしてすぐコードを書き初めてため概念の理解に時間がかかりました。わかれば非常に単純でした。笑
以下の記事がとてもわかりやすく参考になります。
Pay.jpの仕組みについて!
セキュリティに加えて、payjpで発行されるトークンと顧客IDが紐つけられ永続化することで、ユーザーがスムーズに決済できることがポイントかと思います。実装内容
参考記事
実装においては以下の記事を参考としました。事情にわかりやすく説明されていています。
Payjpでクレジットカード登録と削除機能を実装する(Rails)基本的な実装な上記記事を見れば理解できると思います。今回は実装中につまずいたポイントや復習した内容についてまとめます。
JavaScript(jQuery)を使ってトークンを発行
ここまでの学習でJSについて理解が足りていなかったので、カリキュラムを復習、新しい知識をインプットしながらトークンの発行の仕方を理解しました。payjpに関して記事は結構あるのですが、コードがボーンって書いてあるだけだったので詳しく見ていきましょう!!
payjp.js
document.addEventListener( "DOMContentLoaded", e => { Payjp.setPublicKey("自分の公開鍵を記述する"); var btn = document.getElementById('token_submit'); btn.addEventListener("click", (e) => { e.preventDefault(); var card = { number: $("#number").val(), cvc: $("#cvc").val(), exp_month: $("#exp_month").val(), exp_year: $("#exp_year").val() }; Payjp.createToken(card, (status, response) => { if (status === 200) { $("#number").removeAttr("name"); $("#cvc").removeAttr("name"); $("#exp_month").removeAttr("name"); $("#exp_year").removeAttr("name"); $("#card_token").append( $('<input type="hidden" name="payjp-token">').val(response.id) ); document.inputForm.submit(); alert("登録が完了しました"); } else { alert("カード情報が正しくありません。"); } }); }); }, false );まずpayjpとに入力データを渡すために、公開鍵を記述します。
秘密鍵と公開鍵がありますので間違わないように注意しましょう。秘密鍵はcontrollerに記述します。Payjp.setPublicKey("自分の公開鍵を記述する");viewでカード情報を入力して、登録ボタン(#token_submit)をクリックした時、データを送信するデフォルトの機能を停止し、
var btn = document.getElementById('token_submit'); btn.addEventListener("click", (e) => { e.preventDefault();クレジットカード情報の、
number
,cvc
,exp_month
,exp_year
を.val()で属性の値を取得します。var card = { number: $("#number").val(), cvc: $("#cvc").val(), exp_month: $("#exp_month").val(), exp_year: $("#exp_year").val() };上記で取得した値は
Payjp.createToken()
の引数に渡し管理されます。
status === 200
はリクエストが成功している状況です。
ここでカード情報はサーバーには保存しないため、removeAttr
によって属性を削除します。そしてresponse.idを取得しトークンをデータベースに送るため隠しタグを生成します。Payjp.createToken(card, (status, response) => { if (status === 200) { $("#number").removeAttr("name"); $("#cvc").removeAttr("name"); $("#exp_month").removeAttr("name"); $("#exp_year").removeAttr("name"); $("#card_token").append( $('<input type="hidden" name="payjp-token">').val(response.id) ); document.inputForm.submit(); alert("登録が完了しました");最後に!
便利なAPIを使うと簡単に便利な機能を実装することができますが(結構時間かかったけど)、何でこの記述をしているのか考えるのもいい勉強になりました。特にJSがあまり理解できていなかったので、この機会復習もできました。
理解が間違っていましたらぜひ教えてください。
読んでくださりありがとうございました!!
- 投稿日:2020-03-24T22:33:29+09:00
Kinx 実現技術 - JIT
JIT Compile - Just In Time コンパイル
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。作ったものの紹介だけではなく実現のために使った技術を紹介していくのも貢献。その道の人には当たり前でも、そうでない人にも興味をもって貰えるかもしれない。
前回のテーマは VM (Virtual Machine)。今回のテーマは JIT。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
正直、JIT は道半ばでコード晒すのもイマイチだが、参考にはなるかと思う。コントリビューター募集中というのもあるので興味があればよろしくお願いします。
JIT コンパイラとは
Just In Time、つまり まさにその場で コンパイルして実行する方式。ネイティブコード(機械語)にコンパイルして実行するため実行速度を追い求める人たちの間では欲しくてたまらない機能の一つ。
ただし、プロセッサーごとに個別に実装しないといけない。x64 だけ、とかならまだいいんだけど、どっかで ARM くらいはサポートすっかなー、とか、PowerPC は、SPARC?、とかになると、そもそもテストするのが大変だ。
Ruby 方式
Ruby も最近 JIT サポートをアピールしはじめたが、その辺の大変さからうまく?逃げている。つまり、C ソースを作成して、他のコンパイラでコンパイル したものを自分のプロセスに取り込む、といったことをしている。コンパイラがホストコンピュータ用のコードを出力してくれるし、もともと他人のモノなので Ruby 自体はプロセッサーを意識しなくて良い。
まあ逃げ方としてはそうなるとは思うが、しかしそれでは実行環境にコンパイラが必要になってしまう。しかもなにより Windows 環境で Visual Studio をインストールしてください、とか、Cygwin インストールしてください、とは 言えない というのが私の感覚です。開発者以外にコンパイラをインストールさせるのは ... かなり気が引ける。
sljit
そこで sljit を採用した。sljit はアセンブラを抽象化しており、その抽象化アセンブラで書くことでコード出力時に x64 や ARM などに合わせて出力ができるようになっている。その代わり、レジスタ数とかどのプロセッサーでも共通に使えるようにミニマムに合わせておかなければならない。
ただし、x64 以外ではテストしていないので悪しからず。
尚、ネイティブ・コード用 IR 出力ルーチンは src/ast_native.c にあり、sljit を使ったネイティブ・コード用コンパイラは src/nir_compile.c にある。そして、まだ実装されていないものが沢山あることがわかる。。。
個別の実装(特徴)
いくつか個別の実装の内容を記載してみる。まだまだやらなきゃいけないことが沢山あるなあ。
IR (Intermediate Representation)
ネイティブ・コードに変換する上で扱いやすいように、VM 用の IR と異なるコードを出力する。いやいや本当に違うものを出力する。なぜなら、例外の扱いとかコンパイル時点における型チェックの厳密さとかが全く異なるからで、それに合わせて違う体系になっている。
例えば;
native nfib(n) { if (n < 3) return n; return nfib(n-2) + nfib(n-1); } function fib(n) { if (n < 3) return n; return fib(n-2) + fib(n-1); }これは以下のようになる。
nfib(1, 10): .L0 1: jmp .L1 .L1 2: load r1, $(0,0) 3: lt r2 = r1, 3 4: jz .L4 .L2 5: load r3, $(0,0) 6: ret r3 .L3 8: jmp .L4 .L4 9: load r4, $(0,0) a: sub r5 = r4, 2 b: arg r5 c: call r6 = [rec-call]() d: excpt r7 = check e: jz .L6 .L5 f: excpt off 10: excpt on 11: ret 0 .L6 13: load r7, $(0,0) 14: sub r8 = r7, 1 15: arg r8 16: call r9 = [rec-call]() 17: excpt r10 = check 18: jz .L8 .L7 19: excpt off 1a: excpt on 1b: ret 0 .L8 1d: add r10 = r6, r9 1e: ret r10 .L9 20: ret 0 fib: .L460 d06: enter 23, vars(1), args(1) .L461 d07: lt_v0i $0(0), 3 d08: jz .L463(d0a) .L462 d09: retvl0 $0(0) .L463 d0a: pushvl0 $0(0) d0b: subi 2 d0c: callvl1 $1(34), 1 d0d: pushvl0 $0(0) d0e: subi 1 d0f: callvl1 $1(34), 1 d10: add d11: ret d12: halt長くなるので各命令の意味は省略します。ちょっと無駄なコードも見え隠れ(隠れてない)するが、こんな感じの出力を現時点ではする。しかし、こうやって書くと改善点も客観的に見えるというメリットもあるね。
例外処理
ネイティブコードでも
try-catch-finally
をサポートするにあたり、多少工夫が必要。setjmp/longjmp が使えれば簡単だが、JIT コード内で、しかも複数プロセッサー対応を目指した抽象化アセンブラの範囲内では使えないので実現するのはちょっと骨が折れた。具体的には以下の通りに実装されている。
- 例外が発生したかをチェックできる特別なフラグを用意する。
- 例外発生時、フラグを立て、キャッチ・ルーチンがあればそこにジャンプする。
finally
があればそれを実行する。- 同じ関数内に次の(外側の)キャッチ・ルーチンがあれば、そこにジャンプ。
- 無ければ return する。
- リターンは関数呼び出しの次のコードなので、そこには例外が発生したかのフラグを確認するコードを出力しておく。
- 例外発生を検知したら、上記と同じ形で伝播させていく。
スタックトレースも作れるはずなのだが、他のことを優先しているので今はできていない。どっかできちんとやる。
型チェック
ネイティブコードでは型チェックは重要。関数の入口で引数の型をチェック。レキシカル変数をアクセスする場合もチェック。関数から帰ってきたときにもチェック。型が期待するものと違ってたらすぐさま例外を上げるようになっている。
Switch-Case
sljit のライブラリを色々見たのだが、sljit の世界の中でどうしてもジャンプテーブルの実現方法が分からなかったので、今はサポートできていない(具体的にはレジスタ値にジャンプする方法がインターフェースとして用意されていない模様)。
最悪は
if-else
で実現するくらいだが、ジャンプテーブルの無い Switch-Case は軽自動車並みのエンジンで見た目だけ F1 カーといった感じなので、やる意味があるかどうか。sljit を改造するという手もあるが、x64 以外はすぐに対応できそうもない、といったところ。VM 関数呼び出し
これも現在は未サポート。子 VM を起動してジャンプすれば良いとは思うが、入れ子になった VM 内で例外が発生した場合の処理が大変そうなので、まだ手がつけられていない。おそらく以下のような感じでできるのでは、と思っているが、どうだろう。
- フレームに
native
からのコールであるフラグを立てる。return
時、native
からであれば復帰値をセットして VM からreturn
する。- 例外発生時、スタックを巻き戻している最中に
native
コールされたフラグを見つけた場合、そこでスタックトレース情報の作成を中断し、native
向けの例外情報を設定してnative
にリターン。native
側に帰ってきたら、native
での例外伝搬の仕組みで伝搬させていく。他に気にするところはあるかな。例外のスタックトレースを
native
関数内でも保存するようにしないといけないな。やってみる時間があれば挑戦してみる、という予定。オブジェクトへのアクセス
レキシカル変数へのアクセスはできるので、できるはずだがこれもまだ未サポート。演算で認められていない型同士の演算をきちんと拒絶、もしくはきちんとキャストするようにした上で、オブジェクトへのインデックス・アクセスをできるようにすれば良いだけだが、先にテストコードとドキュメントとライブラリを充実させたいもので。
そのうちやる。
レジスタ割り付け
複数のプロセッサーに対応させようとすると、レジスタ数の縛りが結構キツイ。なので割り切ってレジスタ割り付けは省略。全部ストア・ロードで対応。出力コードは汚いが、マイクロベンチマークである程度パフォーマンスが出ているので、良しとした。
尚、最初の例ではあえて載せていなかったが、x64 向けコードを出力させてみると以下のようになる。うむ、汚い。大半は型チェックと例外チェックなのだが、演算内容に対しても全くレジスタ割り付けしていないのも分かるだろう。毎回ストア・ロードしているし、無駄にストアしたのを再度ロードしたりしているのでその辺は簡単に改善できそうではある。今のところ重要度は高くないのでペンディング中。
return
くらいは一か所に集めるか。nfib: (native-base:0x7f1b2aa50010) 0: 53 push rbx 1: 41 57 push r15 3: 41 56 push r14 5: 41 55 push r13 7: 55 push rbp 8: 41 54 push r12 a: 48 8b df mov rbx, rdi d: 4c 8b fe mov r15, rsi 10: 4c 8b f2 mov r14, rdx 13: 48 81 ec 68 02 00 00 sub rsp, 0x268 1a: 49 8b 47 08 mov rax, qword [r15+0x00000008] 1e: 48 83 c0 01 add rax, 0x01 22: 49 89 47 08 mov qword [r15+0x00000008], rax 26: 48 3d 00 04 00 00 cmp rax, 0x400 2c: 72 29 jb 0x00000057 2e: 48 c7 43 20 01 00 00 00 mov qword [rbx+0x00000020], 0x01 36: 48 c7 43 28 06 00 00 00 mov qword [rbx+0x00000028], 0x06 3e: 48 c7 c0 00 00 00 00 mov rax, 0x00 45: 48 81 c4 68 02 00 00 add rsp, 0x268 4c: 41 5c pop r12 4e: 5d pop rbp 4f: 41 5d pop r13 51: 41 5e pop r14 53: 41 5f pop r15 55: 5b pop rbx 56: c3 ret 57: 49 83 bf 10 01 00 00 01 cmp qword [r15+0x00000110], 0x01 5f: 0f 85 52 02 00 00 jnz 0x000002b7 65: 49 8b 57 10 mov rdx, qword [r15+0x00000010] 69: 48 89 14 24 mov qword [rsp], rdx 6d: eb 00 jmp 0x0000006f 6f: 48 8b 14 24 mov rdx, qword [rsp] 73: 48 89 94 24 18 02 00 00 mov qword [rsp+0x00000218], rdx 7b: 48 c7 84 24 20 02 00 00 01 00 mov qword [rsp+0x00000220], 0x01 87: 00 00 48 83 bc 24 18 02 00 cmp qword [rsp+0x00000218], 0x03 90: 00 03 jb 0x0000009e 92: 72 0c 48 c7 84 24 20 02 00 00 mov qword [rsp+0x00000220], 0x00 9e: 00 00 00 00 48 83 bc 24 20 cmp qword [rsp+0x00000220], 0x00 a7: 02 00 jz 0x000000d3 a9: 00 00 74 2a mov rdx, qword [rsp] ad: 48 8b 14 24 48 89 94 24 mov qword [rsp+0x00000228], rdx b5: 28 02 00 00 48 8b 84 24 mov rax, qword [rsp+0x00000228] bd: 28 02 00 00 48 81 c4 add rsp, 0x268 c4: 68 02 pop r12 c6: 00 pop rbp c7: 00 41 pop r13 c9: 5c 5d pop r14 cb: 41 5d pop r15 cd: 41 pop rbx ce: 5e ret cf: 41 5f jmp 0x0000006d d1: 5b c3 jmp 0x000000d3 d3: eb 9c eb 00 mov rdx, qword [rsp] d7: 48 8b 14 24 48 89 94 24 mov qword [rsp+0x00000230], rdx df: 30 02 00 00 48 8b 94 24 mov rdx, qword [rsp+0x00000230] e7: 30 02 00 00 sub rdx, 0x02 eb: 48 83 ea 02 48 89 94 24 mov qword [rsp+0x00000238], rdx f3: 38 02 00 00 48 8b 94 24 mov rdx, qword [rsp+0x00000238] fb: 38 02 00 00 48 mov qword [rsp+0x00000018], rdx 100: 89 54 24 18 48 c7 84 24 18 01 mov qword [rsp+0x00000118], 0x01 10c: 00 00 01 00 mov rcx, qword [rbx+0x00000018] 110: 00 00 48 mov rax, rbx 113: 8b 4b 18 48 mov rdx, qword [r15+0x00000008] 117: 89 d8 49 8b 57 mov qword [rsp+0x00000010], rdx 11c: 08 48 89 54 24 lea rsi, [rsp+0x00000008] 121: 10 48 8d mov rdi, rax 124: 74 24 call rcx 126: 08 48 89 c7 ff d1 48 89 mov qword [rsp+0x00000240], rax 12e: 84 24 40 02 00 00 48 c7 84 24 mov qword [rsp+0x00000248], 0x01 13a: 48 02 00 00 mov rax, qword [rbx+0x00000020] 13e: 01 00 00 00 cmp rax, 0x00 142: 48 8b jnz 0x00000150 144: 43 20 48 83 f8 00 75 0c 48 c7 mov qword [rsp+0x00000248], 0x00 150: 84 24 48 02 00 00 00 00 00 cmp qword [rsp+0x00000248], 0x00 159: 00 48 83 bc 24 48 jz 0x0000018e 15f: 02 00 00 00 0f 84 2f 00 mov qword [rbx+0x00000020], 0x00 167: 00 00 48 c7 43 20 00 00 mov qword [rbx+0x00000020], 0x01 16f: 00 00 48 c7 43 20 01 00 mov rax, qword [rsp+0x00000210] 177: 00 00 48 8b 84 24 10 add rsp, 0x268 17e: 02 00 pop r12 180: 00 pop rbp 181: 48 81 pop r13 183: c4 68 pop r14 185: 02 00 pop r15 187: 00 pop rbx 188: 41 ret 189: 5c 5d 41 5d 41 jmp 0x0000006d 18e: 5e 41 5f 5b mov rdx, qword [rsp] 192: c3 e9 df fe ff ff 48 8b mov qword [rsp+0x00000248], rdx 19a: 14 24 48 89 94 24 48 02 mov rdx, qword [rsp+0x00000248] 1a2: 00 00 48 8b sub rdx, 0x01 1a6: 94 24 48 02 00 00 48 83 mov qword [rsp+0x00000250], rdx 1ae: ea 01 48 89 94 24 50 02 mov rdx, qword [rsp+0x00000250] 1b6: 00 00 48 8b 94 mov qword [rsp+0x00000018], rdx 1bb: 24 50 02 00 00 48 89 54 24 18 mov qword [rsp+0x00000118], 0x01 1c7: 48 c7 84 24 mov rcx, qword [rbx+0x00000018] 1cb: 18 01 00 mov rax, rbx 1ce: 00 01 00 00 mov rdx, qword [r15+0x00000008] 1d2: 00 48 8b 4b 18 mov qword [rsp+0x00000010], rdx 1d7: 48 89 d8 49 8b lea rsi, [rsp+0x00000008] 1dc: 57 08 48 mov rdi, rax 1df: 89 54 call rcx 1e1: 24 10 48 8d 74 24 08 48 mov qword [rsp+0x00000258], rax 1e9: 89 c7 ff d1 48 89 84 24 58 02 mov qword [rsp+0x00000260], 0x01 1f5: 00 00 48 c7 mov rax, qword [rbx+0x00000020] 1f9: 84 24 60 02 cmp rax, 0x00 1fd: 00 00 01 00 00 00 jnz 0x0000020f 203: 48 8b 43 20 48 83 f8 00 0f 85 mov qword [rsp+0x00000260], 0x00 20f: 0c 00 00 00 48 c7 84 24 60 cmp qword [rsp+0x00000260], 0x00 218: 02 00 00 00 00 00 jz 0x0000024d 21e: 00 48 83 bc 24 60 02 00 mov qword [rbx+0x00000020], 0x00 226: 00 00 0f 84 2f 00 00 00 mov qword [rbx+0x00000020], 0x01 22e: 48 c7 43 20 00 00 00 00 mov rax, qword [rsp+0x00000210] 236: 48 c7 43 20 01 00 00 add rsp, 0x268 23d: 00 48 pop r12 23f: 8b pop rbp 240: 84 24 pop r13 242: 10 02 pop r14 244: 00 00 pop r15 246: 48 pop rbx 247: 81 ret 248: c4 68 02 00 00 jmp 0x0000006d 24d: 41 5c 5d 41 5d 41 5e 41 mov rdx, qword [rsp+0x00000240] 255: 5f 5b c3 e9 20 fe ff ff add rdx, qword [rsp+0x00000258] 25d: 48 8b 94 24 40 02 00 00 mov qword [rsp+0x00000260], rdx 265: 48 03 94 24 58 02 00 00 mov rax, qword [rsp+0x00000260] 26d: 48 89 94 24 60 02 00 add rsp, 0x268 274: 00 48 pop r12 276: 8b pop rbp 277: 84 24 pop r13 279: 60 02 pop r14 27b: 00 00 pop r15 27d: 48 pop rbx 27e: 81 ret 27f: c4 68 02 00 00 jmp 0x0000006d 284: 41 5c 5d 41 5d 41 5e 41 mov rax, qword [rsp+0x00000210] 28c: 5f 5b c3 e9 e9 fd ff add rsp, 0x268 293: ff 48 pop r12 295: 8b pop rbp 296: 84 24 pop r13 298: 10 02 pop r14 29a: 00 00 pop r15 29c: 48 pop rbx 29d: 81 ret 29e: c4 68 02 00 00 41 5c mov rax, 0x00 2a5: 5d 41 5d 41 5e 41 5f add rsp, 0x268 2ac: 5b c3 pop r12 2ae: 48 pop rbp 2af: c7 c0 pop r13 2b1: 00 00 pop r14 2b3: 00 00 pop r15 2b5: 48 pop rbx 2b6: 81 ret 2b7: c4 68 02 00 00 41 5c 5d mov qword [rbx+0x00000020], 0x01 2bf: 41 5d 41 5e 41 5f 5b c3 mov qword [rbx+0x00000028], 0x07 2c7: 48 c7 43 20 01 00 00 mov rax, 0x00 2ce: 00 48 c7 43 28 07 00 add rsp, 0x268 2d5: 00 00 pop r12 2d7: 48 pop rbp 2d8: c7 c0 pop r13 2da: 00 00 pop r14 2dc: 00 00 pop r15 2de: 48 pop rbx 2df: 81 ret最適化
最適化は 全く やってない。JITに関してはそんなに頑張らなくてもいいかなーと。
というか、実は VM コードに対しても現時点では全くやってない。定数の畳み込みすらやってないので、まずはそっちから。
今後
優先順位によってすぐにはできないかもしれないが、上記問題の解決はどこかでやるつもり。
それ以外、せっかく型指定できるようにしているので、
native
以外での部分 JIT はできるようにした方がいいよね。逆にそれができればnative
いらない。というか、単なるint
省略記法になる。JIT とは言っても、一般的なトレーシングとかではなく、コンパイル時にコード出力してしまうのだけどね。この辺の言葉の使い方も難しい。Just In Time の範囲がどこまでかによって変わるが、一般的に 実行時に ということのようなので、これも JIT のうちでしょう。おわりに
こうして記事にしてみると、何気に具体的な課題が見つかっていい感じですね。予定立てます。
ではでは、今回もここまで読んでいただいてありがとうございます。最後はいつもの以下の定型フォーマットです。興味がありましたら ★ とか 「
いいねLGTM」 ボタンとか押してもらえるとモチベーションにつながります。どうぞよろしくお願いします。
- 最初の動機は スクリプト言語 KINX(ご紹介) を参照してください(もし宜しければ「
いいねLGTM」ボタンをポチっと)。- リポジトリは ここ(https://github.com/Kray-G/kinx) です。こちらももし宜しければ★をポチっと。
- 投稿日:2020-03-24T22:31:16+09:00
商品出品画面を作る
メルカリの商品出品画面を参考にコピーを作る
参考画面メルカリ
使用する機能
- ActiveStorage(画像投稿)リンク
- ancestry(多階層カテゴリー)
- active_hash(静的データ作成)
とりあえず出来たコード
new.html.haml.sell %header.sell-header = link_to root_path do = image_tag 'mercari_top_logo.svg', alt: 'mercari', height: '49', width: '185' -#メイン部分 %main %section.sell-container = form_with model: @item do |f| -# 画像部分 .sell-container__content .sell-title %h3.sell-title__text 出品画像 %span.sell-title__require 必須 .sell-container__content__max-sheet 最大10枚までアップロードできます .sell-container__content__upload .sell-container__content__upload__items .sell-container__content__upload__items__box %ul#output-box %div#image-input{tabindex:"0"} = f.label :images, for: "item_images0", class: 'sell-container__content__upload__items__box__label', data: {label_id: 0 } do = f.file_field :images, multiple: true, class: "sell-container__content__upload__items__box__input", id: "item_images0", style: 'display: none;' %pre %i.fas.fa-camera.fa-lg ドラッグアンドドロップ またはクリックしてファイルをアップロード .error-messages#error-image -#商品名部分 .sell-container__content .sell-title %h3.sell-title__text 商品名 %span.sell-title__require 必須 = f.text_field :name, {class:'sell-container__content__name', required: "required", placeholder: '商品名(必須 40文字まで)'} .error-messages#error-name .sell-title %h3.sell-title__text 商品の説明 %span.sell-title__require 必須 = f.text_area :text,{class: 'sell-container__content__description', required: "required", rows: '7', maxlength: '1000', placeholder: text_placeholder} -# placeholderでtems_helperを呼び出す .sell-container__content__word-count %span#word-count 0 /1000 .error-messages#error-text -# 詳細部分 .sell-container__content %h3.sell-sub-head 商品の詳細 .sell-container__content__details .sell-title %h3.sell-title__text カテゴリー %span.sell-title__require 必須 .sell-collection_select = f.label :category_id, {class: 'sell-collection_select__label'} do = f.collection_select :category_id, @category_parent, :id, :name, {prompt: "選択して下さい"},{ class: 'sell-collection_select__input', id: 'category-select', required: "required"} %i.fas.fa-chevron-down .error-messages#error-category .sell-title %h3.sell-title__text 商品の状態 %span.sell-title__require 必須 .sell-collection_select = f.label :condition_id, {class: 'sell-collection_select__label'} do = f.collection_select :condition_id, Condition.all, :id, :condition, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'condition-select', required: "required"} %i.fas.fa-chevron-down .error-messages#error-condition -# 配送部分 .sell-container__content %h3.sell-sub-head %p 配送について = link_to '/delivery',target: '_blank',class: 'sell-sub-head__guides-link' do %i.far.fa-question-circle .sell-container__content__delivery .sell-title %h3.sell-title__text 配送料の負担 %span.sell-title__require 必須 .sell-collection_select = f.label :deliverycost_id, {class: 'sell-collection_select__label'} do = f.collection_select :deliverycost_id, Deliverycost.all, :id, :payer, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'deliverycost-select', required: "required"} %i.fas.fa-chevron-down .error-messages#error-deliverycost .sell-title %h3.sell-title__text 発送元の地域 %span.sell-title__require 必須 .sell-collection_select = f.label :pref_id, class: 'sell-collection_select__label' do = f.collection_select :pref_id, Pref.all, :id, :name, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'pref-select', required: "required"} %i.fas.fa-chevron-down .error-messages#error-pref .sell-title %h3.sell-title__text 発送までの日数 %span.sell-title__require 必須 .sell-collection_select = f.label :delivery_days_id, class: 'sell-collection_select__label' do = f.collection_select :delivery_days_id, DeliveryDays.all, :id, :days, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'delivery_days-select', required: "required"} %i.fas.fa-chevron-down .error-messages#error-delivery_days -# 価格部分 .sell-container__content %h3.sell-sub-head %p 販売価格(300〜9,999,999) = link_to '/price',target: '_blank', class: 'sell-sub-head__guides-link' do %i.far.fa-question-circle .sell-container__content__price .sell-title %h3.sell-title__text 販売価格 %span.sell-title__require 必須 .sell-container__content__price__form = f.label :price, class: 'sell-container__content__price__form__label' do ¥ = f.number_field :price, {placeholder: '0', value: '', autocomplete:"off", class: 'sell-container__content__price__form__box', required: "required"} .error-messages#error-price .sell-container__content__commission .sell-container__content__commission__left 販売手数料 (10%) .sell-container__content__commission__right ー .sell-container__content__profit .sell-container__content__profit__left 販売利益 .sell-container__content__profit__right ー .submit-btn = f.submit '出品する', class: 'submit-btn__sell-btn' = link_to 'もどる', root_path, class: 'submit-btn__return-btn' .attention-box %p 禁止されている = link_to '行為', '/prohibited_conduct', target: '_blank' および = link_to '出品物', '/prohibited_item', target: '_blank' を必ずご確認ください。 = link_to '偽ブランド品', '/counterfeit_goods', target: '_blank' や = link_to '盗品物', '/stolen_goods', target: '_blank' などの販売は犯罪であり、法律により処罰される可能性があります。また、出品をもちまして = link_to '加盟店規約', '/seller_terms', target: '_blank' に同意したことになります。 %footer.sell-footer %nav %ul.clearfix %li = link_to '#' do プライバシーポリシー %li = link_to '#' do メルカリ利用規約 %li = link_to '#' do 特定商取引に関する表記 = link_to root_path, class: 'footer__logo' do = image_tag 'logo-gray.svg', alt: 'mercari', height: '65', width: '80' %p %small © Mercari, Inc.items_new.scssa { color: inherit; text-decoration: none; box-sizing: border-box; } img { vertical-align: middle; box-sizing: border-box; } .error-messages{ color: #ff0211; font-size: 14px; line-height: 1.4em; margin: 16px 0; box-sizing: border-box; } .sell-title{ align-items: center; margin: 0!important; box-sizing: border-box; &__text{ font-size: 14px; font-weight: 600; line-height: 1.4em; } &__require{ margin-left: 8px; font-size: 12px; padding: 0 4px; background-color: #ff0211; color: #fff; border-radius: 2px; display: inline-block; font-style: normal; font-weight: 600; line-height: 1.4em; margin: 0; } } .sell-sub-head{ box-sizing: border-box; color: rgb(136, 136, 136); font-size: 14px; font-weight: 600; line-height: 1.4em; margin-bottom: 24px; display: flex; &__guides-link{ color: rgb(0, 149, 238); margin-left: 4px; } } .sell-collection_select{ box-sizing: border-box; margin-top: 16px; &__label{ display: inline-block; position: relative; width: 100%; .fas.fa-chevron-down{ box-sizing: border-box; pointer-events: none; position: absolute; right: 16px; top: 40%; color: rgb(136, 136, 136); height: 48px; } } &__input{ -webkit-appearance: none; -moz-appearance: none; appearance: none; background-color: #fff; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; color: #222; font-size: 16px; height: 48px; line-height: 1; margin: 0; outline: none; padding: 0 56px 0 16px; width: 100%; } } // ここより上は繰り返し使用するパーツ .sell { box-sizing: border-box; position: relative; color: rgb(51, 51, 51); background-color: rgb(245, 245, 245); font-family: Arial, 游ゴシック体, YuGothic, メイリオ, Meiryo, sans-serif; font-size: 14px; line-height: 1; box-sizing: border-box; .sell-header{ box-sizing: border-box; height: 128px; align-items: center; display: flex; justify-content: center; } .sell-container{ box-sizing: border-box; max-width: 700px; width: 100%; margin: 0px auto; background-color: rgb(255, 255, 255); // 画像部分 &__content{ height: auto; padding: 40px; border-bottom: 1px; border-bottom-color: #efefef; border-bottom-style: solid; &__max-sheet{ margin-top: 16px; } &__upload{ margin-top: 16px; display: flex; flex-wrap: wrap; &__items{ height: auto; width: 100%; &__box{ height: auto; align-content: center; align-items: center; cursor: pointer; display: flex; flex-wrap: wrap; justify-content: center; position: relative; border-width: 1px; #output-box{ box-sizing: border-box; display: flex; flex-wrap: wrap; width: 100%; height: auto; .preview-image{ box-sizing: border-box; height: 150px; width: 20%; padding: 0px 4px; margin-top: 8px; &__figure{ margin:0 auto; height: 118px; background-color: rgb(245, 245, 245); img{ box-sizing: border-box; width: 100%; height: 100%; object-fit: contain; } } &__button{ border-top-width: 1px; border-top-color: rgb(204, 204, 204); border-top-style: solid; background-color: rgb(245, 245, 245); justify-content: space-around; display: flex; align-items: center; height: 32px; color: rgb(0, 149, 238); } } #image-input{ box-sizing: border-box; height: 100%; -webkit-flex: 1; flex: 1; margin-top: 8px; .sell-container__content__upload__items__box__label{ box-sizing: border-box; background-color: rgb(245, 245, 245); height: 150px; border-width: 1px; border-style: dashed; border-color: rgb(204, 204, 204); text-align: center; display: flex; align-items: center; justify-content: center; i{ box-sizing: border-box; margin-bottom: 8px; } } } } } } } } // 商品名部分 &__content{ &__name{ margin-top: 16px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; height: 48px; padding: 0 16px; width: 100%; } &__description{ margin-top: 16px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; padding: 16px; width: 100%; font-size: 16px; display: block; } &__word-count{ text-align: right; color: #888; font-size: 12px; line-height: 1.4em; } } // 詳細は共通パーツのみ // 配送は共通パーツのみ // 価格部分 &__content{ &__price{ -webkit-box-align: center; align-items: center; box-sizing: content-box; display: flex; height: 46px; justify-content:space-between; &form__label{ font-size: 14px; } &__form__box{ border: 1px solid #ccc; border-radius: 4px; height: 48px; margin-left: 8px; padding: 0 16px; width: 300px; align-items: center; display: inline-flex; text-align: right; } } #error-price{ box-sizing: border-box; text-align: right; } &__commission{ display: flex; justify-content:space-between; height: 70px; padding: 12px 0px; align-items: center; border-bottom: 1px; border-bottom-color: #efefef; border-bottom-style: solid; } &__profit{ display: flex; justify-content:space-between; height: 70px; padding: 12px 0px; align-items: center; } } .submit-btn{ box-sizing: border-box; margin: 0 auto; width: 360px; margin-bottom: 32px; &__sell-btn{ background-color: #ea352d; color: #fff; margin-bottom: 24px; width: 100%; font-size: 17px; height: 48px; font-weight: 600; border-radius: 4px; } &__return-btn{ background-color: #ccc; color: #222; width: 100%; font-size: 17px; height: 48px; font-weight: 600; padding: 14px 0; border-radius: 4px; display: inline-block; text-align: center; } } .attention-box{ box-sizing: border-box; font-size: 12px; line-height: 1.4em; word-break: keep-all; a{ box-sizing: border-box; color: #0095ee; } } } .sell-footer{ box-sizing: border-box; padding: 40px 0px; text-align: center; nav{ box-sizing: border-box; .clearfix{ box-sizing: border-box; display: inline-block; font-size: 12px; display: flex; justify-content: center; margin-bottom: 40px; li{ box-sizing: border-box; margin: 0px 8px; } } } } }items_new.js$(document).on('turbolinks:load', function(){ // 画像が選択された時プレビュー表示、inputの親要素のulをイベント元に指定 $('#image-input').on('change', function(e){ //ファイルオブジェクトを取得する let files = e.target.files; $.each(files, function(index, file) { let reader = new FileReader(); //画像でない場合は処理終了 if(file.type.indexOf("image") < 0){ alert("画像ファイルを指定してください。"); return false; } //アップロードした画像を設定する reader.onload = (function(file){ return function(e){ let imageLength = $('#output-box').children('li').length; // 表示されているプレビューの数を数える let labelLength = $("#image-input>label").eq(-1).data('label-id'); // #image-inputの子要素labelの中から最後の要素のカスタムデータidを取得 // プレビュー表示 $('#image-input').before(`<li class="preview-image" id="upload-image${labelLength}" data-image-id="${labelLength}"> <figure class="preview-image__figure"> <img src='${e.target.result}' title='${file.name}' > </figure> <div class="preview-image__button"> <a class="preview-image__button__edit" href="">編集</a> <a class="preview-image__button__delete" data-image-id="${labelLength}">削除</a> </div> </li>`); $("#image-input>label").eq(-1).css('display','none'); // 入力されたlabelを見えなくする if (imageLength < 9) { // 表示されているプレビューが9以下なら、新たにinputを生成する $("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}"> <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]"> <i class="fas fa-camera fa-lg"></i> </label>`); }; }; })(file); reader.readAsDataURL(file); }); }); //削除ボタンが押された時 $(document).on('click', '.preview-image__button__delete', function(){ let targetImageId = $(this).data('image-id'); // イベント元のカスタムデータ属性の値を取得 $(`#upload-image${targetImageId}`).remove(); //プレビューを削除 $(`[for=item_images${targetImageId}]`).remove(); //削除したプレビューに関連したinputを削除 let imageLength = $('#output-box').children('li').length; // 表示されているプレビューの数を数える if (imageLength ==9) { let labelLength = $("#image-input>label").eq(-1).data('label-id'); // 表示されているプレビューが9なら,#image-inputの子要素labelの中から最後の要素のカスタムデータidを取得 $("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}"> <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]"> <i class="fas fa-camera fa-lg"></i> </label>`); }; }); // f.text_areaの文字数カウント $("textarea").keyup(function(){ let txtcount = $(this).val().length; $("#word-count").text(txtcount); }); //販売価格入力時の手数料計算 $('#item_price').keyup(function(){ let price= $(this).val(); if (price >= 300 && price <= 9999999){ let fee = Math.floor(price * 0.1); // 小数点以下切り捨て let profit = (price - fee); $('.sell-container__content__commission__right').text('¥'+fee.toLocaleString()); // 対象要素の文字列書き換える $('.sell-container__content__profit__right').text('¥'+profit.toLocaleString()); } else{ $('.sell-container__content__commission__right').html('ー'); $('.sell-container__content__profit__right').html('ー'); } }); // 各フォームの入力チェック $(function(){ //画像 $('#image-input').on('focus',function(){ $('#error-image').text(''); $('#image-input').on('blur',function(){ $('#error-image').text(''); let imageLength = $('#output-box').children('li').length; if(imageLength ==''){ $('#error-image').text('画像がありません'); }else if(imageLength >10){ $('#error-image').text('画像を10枚以下にして下さい'); }else{ $('#error-image').text(''); } }); }); //送信しようとした時 $('form').on('submit',function(){ let imageLength = $('#output-box').children('li').length; if(imageLength ==''){ $('body, html').animate({ scrollTop: 0 }, 500); $('#error-image').text('画像がありません'); }else if(imageLength >10){ $('body, html').animate({ scrollTop: 0 }, 500); $('#error-image').text('画像を10枚以下にして下さい'); }else{ return false; } }); //画像を削除した時 $(document).on('click','.preview-image__button__delete',function(){ let imageLength = $('#output-box').children('li').length; if(imageLength ==''){ $('#error-image').text('画像がありません'); }else if(imageLength >10){ $('#error-image').text('画像を10枚以下にして下さい'); }else{ $('#error-image').text(''); } }); //商品名 $('.sell-container__content__name').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-name').text('入力してください'); $(this).css('border-color','red'); }else{ $('#error-name').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //商品説明 $('.sell-container__content__description').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-text').text('入力してください'); $(this).css('border-color','red'); }else{ $('#error-text').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //カテゴリー $('#category-select').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-category').text('選択して下さい'); $(this).css('border-color','red'); }else{ $('#error-category').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //状態 $('#condition-select').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-condition').text('選択して下さい'); $(this).css('border-color','red'); }else{ $('#error-condition').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //送料負担 $('#deliverycost-select').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-deliverycost').text('選択して下さい'); $(this).css('border-color','red'); }else{ $('#error-deliverycost').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //発送元 $('#pref-select').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-pref').text('選択して下さい'); $(this).css('border-color','red'); }else{ $('#error-pref').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //発送までの日数 $('#delivery_days-select').on('blur',function(){ let value = $(this).val(); if(value == ""){ $('#error-delivery_days').text('選択して下さい'); $(this).css('border-color','red'); }else{ $('#error-delivery_days').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); //価格 $('.sell-container__content__price__form__box').on('blur',function(){ let value = $(this).val(); if(value < 300 || value > 9999999){ $('#error-price').text('300以上9999999以下で入力してください'); $(this).css('border-color','red'); }else{ $('#error-price').text(''); $(this).css('border-color','rgb(204, 204, 204)'); } }); }); });items.controller.rbdef new @item = Item.new @category_parent = Category.where("ancestry is null") end def create @item = Item.new(item_params) if @item.save redirect_to root_path else render :new end end private def item_params params.require(:item).permit(:name, :text, :category_id, :condition_id, :deliverycost_id, :pref_id, :delivery_days_id, :price, images: []).merge(user_id: current_user.id, boughtflg_id:"1") end enditem.rb(モデル)class Item < ApplicationRecord extend ActiveHash::Associations::ActiveRecordExtensions belongs_to_active_hash :condition belongs_to_active_hash :pref belongs_to_active_hash :deliverycost belongs_to_active_hash :delivery_days belongs_to_active_hash :boughtflg # 上記active_hashのアソシエーション validate :images_presence validates :name, :text, :category_id, :condition_id, :deliverycost_id, :pref_id, :delivery_days_id, :boughtflg_id, presence: true validates :price, presence: true, inclusion: 300..9999999 has_many_attached :images belongs_to :user, foreign_key: 'user_id' # optional: true後で消す belongs_toのnotnull制約解放のため使用している belongs_to :category #imageのバリデーション def images_presence if images.attached? # inputに保持されているimagesがあるかを確認 if images.length > 10 errors.add(:image, '10枚まで投稿できます') end else errors.add(:image, '画像がありません') end end endカテゴリーの2階層以降がまだ未実装でした・・・
とりあえず、後から実装するとして、先ずは画像投稿から
- 投稿日:2020-03-24T22:22:45+09:00
Electronでカレンダーを作る 番外編① ~リファクタリング~
はじめに
どうも、こんにちは。
Electronでカレンダーを作っているのですが、ロジックをつらつら書いているうちにファイルが膨れあがって読みづらくなってきたので、ここらでスッキリさせようと思い、リファクタリングをすることにしました。前回までの記事は、以下を見て下さい。
Electronでカレンダーを作る①
Electronでカレンダーを作る②
Electronでカレンダーを作る③
Electronでカレンダーを作る④
Electronでカレンダーを作る⑤この記事が番外編扱いなのは、Electronにまったく関係ないからです。
作ったソースの問題点
以下が、これまで作ってきたソースとなっております。
index.js'use strict'; //momentモジュール const moment = require("moment"); // カレンダーをキャッシュしておくMap let calendarMap = new Map(); // 画面に表示されている月 let currentDispMonth; window.onload = function() { //URL文字列から初期表示の月を取得 const month = parseURLParam(location.search).month; //直近3か月のカレンダー作成 createLatestCalendar(month); //カレンダーの親要素取得 const root = document.getElementById('calendars'); //子要素としてパネルを追加 const callendar = calendarMap.get(month); root.appendChild(callendar.getPanel()); //カレンダー表示 display(callendar.createContents); //先月ボタン押下時 document.getElementById('preMonth').onclick = function() { const localMoment = moment(currentDispMonth); const preMonth = localMoment.add(-1,'month').format("YYYY-MM"); const beforPanel = calendarMap.get(currentDispMonth).getPanel(); const afterCallendar = calendarMap.get(preMonth); const afterPanel = afterCallendar.getPanel(); changePanel(beforPanel, afterPanel, afterCallendar.createContents); createLatestCalendar(preMonth); }; //来月ボタン押下時 document.getElementById('nextMonth').onclick = function() { const localMoment = moment(currentDispMonth); const nextMonth = localMoment.add(1,'month').format("YYYY-MM"); const beforPanel = calendarMap.get(currentDispMonth).getPanel(); const afterCallendar = calendarMap.get(nextMonth); const afterPanel = afterCallendar.getPanel(); changePanel(beforPanel, afterPanel, afterCallendar.createContents); createLatestCalendar(nextMonth); }; }; /** * URLパラメータを分解してkey:valueの形式で返す。 * @param URL */ function parseURLParam(URL) { // URLパラメータを"&"で分離する const params = URL.substr(1).split('&'); var paramsArray = []; var keyAndValue = null; for(var i = 0 ; i < params.length ; i++) { // "&"で分離したパラメータを"="で再分離 keyAndValue = params[i].split("="); // パラメータを連想配列でセット paramsArray[keyAndValue[0]] = keyAndValue[1]; } // 連想配列パラメータを返す return paramsArray; } /** * パネルを切り替える * @param beforPanel * @param afterPanel * @param callback パネル切り替え後に実行される関数 */ function changePanel(beforPanel, afterPanel, callback) { //カレンダーの親要素取得 const root = document.getElementById('calendars'); //afterPanelでbeforPanelを置き換え root.replaceChild(afterPanel, beforPanel); display(callback); } /** * カレンダーを表示する。 * @param callback 実行される関数 */ function display(callback) { callback(); } /** * 指定した月の直近(当月、先月、来月)のCallendarオブジェクトを作成する。 * 対象月のCalendarオブジェクトがキャッシュされているならそれを使い回す。 */ function createLatestCalendar(month) { //当月分 if(!calendarMap.get(month)) { const callendar = new Callendar(month); callendar.createPanel(); calendarMap.set(month, callendar); } const localMoment = moment(month); //先月分 const preMonth = localMoment.add(-1,'month').format("YYYY-MM"); if(!calendarMap.get(preMonth)) { const preCallendar = new Callendar(preMonth); preCallendar.createPanel(); calendarMap.set(preMonth, preCallendar); } //今月分 const nextMonth = localMoment.add(2,'month').format("YYYY-MM"); if(!calendarMap.get(nextMonth)) { const nextCallendar = new Callendar(nextMonth); nextCallendar.createPanel(); calendarMap.set(nextMonth, nextCallendar); } } /** * カレンダークラス * @param month(YYYY-MM) */ const Callendar = function(month) { //コンストラクタ this._month = month; this._moment = moment(this._month); this._panel = {}; //コールバック関数で利用するのでthisを固定しておく const _this = this; /** * カレンダーのパネルを作成する。 */ this.createPanel = function() { // カレンダーのパネルを作成 const panel = document.createElement('div'); panel.id = 'panel'; // 子要素にテーブルを追加 const table = panel.appendChild(document.createElement('table')); table.id = 'table'; table.classList.add('calendar'); // テーブルにキャプション追加 const caption = table.appendChild(document.createElement('caption')); caption.id = 'caption'; // ヘッダー追加 const header = table.appendChild(document.createElement('thead')); // ヘッダーのカラムを作成する。 const headerRow = header.appendChild(document.createElement('tr')); headerRow.appendChild(document.createElement('th')).innerText = '日'; headerRow.appendChild(document.createElement('th')).innerText = '月'; headerRow.appendChild(document.createElement('th')).innerText = '火'; headerRow.appendChild(document.createElement('th')).innerText = '水'; headerRow.appendChild(document.createElement('th')).innerText = '木'; headerRow.appendChild(document.createElement('th')).innerText = '金'; headerRow.appendChild(document.createElement('th')).innerText = '土'; // body作成 let cellNum = 1; for (let i = 1; i < 7; i++) { const row = table.appendChild(document.createElement('tr')); row.id = 'row' + i; for(let j = 1; j < 8; j++) { const cell = row.appendChild(document.createElement('td')); cell.id = 'cell' + cellNum; cellNum++; } } this._panel = panel; }; /** * カレンダーの内容を作成する。 */ this.createContents = function() { const __moment = _this._moment; //captionを表示 document.getElementById('caption').innerText = __moment.format("YYYY年MM月"); //当月の日数を取得 const daysOfMonth = __moment.daysInMonth(); //月初の曜日を取得(index.htmlと合わせるために+1する) const firstDayOfManth = __moment.startOf('month').day() + 1; //カレンダーの各セルに日付を表示させる let cellIndex = 0; for(let i = 1; i < daysOfMonth + 1; i++) { if(i === 1) { cellIndex += firstDayOfManth; } else { cellIndex++; } document.getElementById("cell" + cellIndex).innerText = i; } //6行目の第1セルが空白なら6行目自体を非表示にする。 if(document.getElementById("cell36").innerText === "") { document.getElementById('row6').style.display = "none"; } currentDispMonth = _this._month; }; /** * パネルを取得する。 */ this.getPanel = function() { return this._panel; }; /** * 月を取得する。 */ this.getMonth = function() { return this._month; }; };一つのファイルに色々詰め込みすぎていて何をしたい(している)のか分かりづらくなっていますね。
そう、1番の問題点は何をするためのJSファイルかがフワフワしてしまっていることです。
ゆえに今回のリファクタリングの指針としては、
そのファイルの責務が何なのかを分かりやすくすることに決めました。ファイルの分割の指針
上記ファイルの内容をざっくりと大枠に分けると以下の二つになります。
①画面の初期表示とボタンのイベントの設定
②カレンダーの作成と内容の表示上記の大枠を実現できるようにファイルを分割・修正していきます。
画面の初期表示とボタンのイベントの設定
①に関しては、既存ファイル(上記のindex.js)を画面(index.html)の操作を責務とさせるファイルとすることで実現します。
カレンダーの作成と内容の表示
②に関しては、index.js内にカレンダーを扱うことを責務としたCaldndarクラスを作成してあるのでそれを上手いこと分割していきたいと思います。
また、
- 月ごとにCalendarオブジェクトを作成するので、それらを管理することを責務とするクラス
- カレンダーを表示するパネル(HTML要素)を作成することを責務とするグローバルな関数
も別々のファイルとして作成していくこととします。
よって分割していくファイルは以下の4つ
- 画面の初期表示とボタンのイベントの設定をするファイル・・・index.js
- カレンダーを扱うクラスに関するファイル・・・calendar.js
- 月ごとのCalendarオブジェクトを管理するクラスに関するファイル・・・calendarManager.js
- カレンダーを表示するパネル(HTML要素)を作成するファイル・・・panel.js
リファクタリングしていく
index.js
index.js'use strict'; /** * カレンダー画面の表示・切り替えの処理 * および各種イベント発火に伴う処理を実行するモジュール * index.htmlに紐づく想定 */ //momentモジュール const moment = require("moment"); // 画面に表示されている月(YYYY-MM) let currentDispMonth; window.onload = function() { //URL文字列から初期表示の月を取得 const month = parseURLParam(location.search).month; //初期表示の月から直近3か月のカレンダー作成 createLatestCalendar(month); //カレンダーの親要素取得 const root = document.getElementById('calendars'); //初期表示の月のCalendarオブジェクト取得 const callendar = calendarManager.getCaldnar(month); //カレンダー表示 displayCalendar(root, callendar); /********** 以下はイベント処理 ***********/ //先月ボタン押下時 document.getElementById('preMonth').onclick = function() { //現在画面に表示されている月からmomentオブジェクト取得 const currentMoment = moment(currentDispMonth); //先月のCalendarオブジェクト取得 const preMonth = currentMoment.add(-1,'month').format("YYYY-MM"); const preCalendar = calendarManager.getCaldnar(preMonth); changeCalendar(root, getCurrentDisplayCalendar(), preCalendar); createLatestCalendar(preMonth); }; //来月ボタン押下時 document.getElementById('nextMonth').onclick = function() { //現在画面に表示されている月からmomentオブジェクト取得 const currentMoment = moment(currentDispMonth); //来月のCalendarオブジェクト取得 const nextMonth = currentMoment.add(1,'month').format("YYYY-MM"); const nextCalendar = calendarManager.getCaldnar(nextMonth); changeCalendar(root, getCurrentDisplayCalendar(), nextCalendar); createLatestCalendar(nextMonth); }; }; /** * URLパラメータを分解してkey:valueの形式で返す関数 * @param URL */ function parseURLParam(URL) { // URLパラメータを"&"で分離する const params = URL.substr(1).split('&'); var paramsArray = []; var keyAndValue = null; for(var i = 0 ; i < params.length ; i++) { // "&"で分離したパラメータを"="で再分離 keyAndValue = params[i].split("="); // パラメータを連想配列でセット paramsArray[keyAndValue[0]] = keyAndValue[1]; } // 連想配列パラメータを返す return paramsArray; } /** * カレンダーを切り替える関数 * @param root 親要素 * @param beforCalendar 切り替え前Calendarオブジェクト * @param afterCalendar 切り替え後Calendarオブジェクト */ function changeCalendar(root, beforCalendar, afterCalendar) { root.replaceChild(afterCalendar.getPanel(), beforCalendar.getPanel()); //切り替え後カレンダーの内容表示 afterCalendar.createContents((month) => { setCurrentDispMonth(month); }); } /** * カレンダーを表示する関数 * 引数に渡されたCalendarオブジェクトが持つパネルの表示と内容の作成を行う * @param root 親要素 * @param calendar 表示させたいCalendarオブジェクト */ function displayCalendar(root, calendar) { root.appendChild(calendar.getPanel()); calendar.createContents((month) => { setCurrentDispMonth(month); }); } /** * 指定した月の直近(当月、先月、来月)のCallendarオブジェクトを作成する関数 * @param month 月 */ function createLatestCalendar(month) { // 先月分 createRangeCalendar(month, -1); // 来月分 createRangeCalendar(month, 1); } /** * 基準月から指定した期間分(基準月を含む)のCalendarオブジェクトを作成する関数 * 第二引数がプラスなら基準月から未来、マイナスなら過去分になる。 * @param {*} month 基準月 * @param {*} term 期間 */ function createRangeCalendar(month, term) { const localMoment = moment(month); let ope = (term < 0) ? -1 : 1; let abs = Math.abs(term); for(let i = 0; i < abs + 1; i++) { const targetMonth = localMoment.add((i * ope), 'month').format('YYYY-MM'); calendarManager.createCalendar(targetMonth); } } /** * 画面に表示する月をセットする関数 */ function setCurrentDispMonth(month) { currentDispMonth = month; } /** * 現在画面に表示されている月のCalendarオブジェクトを取得する関数 */ function getCurrentDisplayCalendar() { return calendarManager.getCaldnar(currentDispMonth); }改修点
①余分な処理を削って、画面の初期表示の作成およびイベントの処理をメインとして扱うようにした。
②画面で使用するCalnedarオブジェクトをCalendarManagerを利用して作成するようにした。画面を扱うためのファイルなので、Calendarオブジェクトの作成に関してはCalendarManagerに委譲して、責務を明確化させた。
calendar.js
calendar.js'use strict'; /** * カレンダークラス * カレンダーの内容の作成を行う * @param month(YYYY-MM) */ let Calendar = function(month) { // eslint-disable-line //コンストラクタ const _month = month; const _moment = moment(_month); const _panel = createPanel(); /** * カレンダーの内容を作成するメソッド * @param callback 画面の表示月を設定するコールバック関数 */ this.createContents = function(callback) { //captionを表示 document.getElementById('caption').innerText = _moment.format("YYYY年MM月"); //当月の日数を取得 const daysOfMonth = _moment.daysInMonth(); //月初の曜日を取得(index.htmlと合わせるために+1する) const firstDayOfManth = _moment.startOf('month').day() + 1; //カレンダーの各セルに日付を表示させる let cellIndex = 0; for(let i = 1; i < daysOfMonth + 1; i++) { if(i === 1) { cellIndex += firstDayOfManth; } else { cellIndex++; } document.getElementById("cell" + cellIndex).innerText = i; } //6行目の第1セルが空白なら6行目自体を非表示にする。 if(document.getElementById("cell36").innerText === "") { document.getElementById('row6').style.display = "none"; } callback(month); }; /** * パネル(HTML要素)を取得するメソッド */ this.getPanel = function() { return _panel; }; /** * 月を取得するメソッド */ this.getMonth = function() { return _month; }; };改修点
カレンダーを扱うクラスを単独のファイルに分割した。
月(YYYY-MM)を引数としてnewをし、createContentsメソッドを呼び出すことでプロパティとして持つパネル(HTML要素)に対して内容の作成を行うことが出来る。
calendarManager.js
calendarManager.js'use strict'; /** * カレンダー管理クラス * Calendarオブジェクトの管理を行う。 */ const CalendarManager = function() { // eslint-disable-line /** * CalendarオブジェクトをキャッシュしておくMap */ const calendarMap = new Map(); /** * Calendarオブジェクトを作成しキャッシュしておくメソッド * すでに生成済みなら何もしない */ this.createCalendar = function(month) { let calendar = calendarMap.get(month); if(!calendar) { calendar = new Calendar(month); calendarMap.set(month, calendar); } }; /** * 指定された月のCalendarオブジェクトを返すメソッド * キャッシュに無ければ作って返す */ this.getCaldnar = function(month) { let r = calendarMap.get(month); if(r) { return r; } else { this.createCalendar(month); return this.getCaldnar(month); } }; }; /** * グローバルなCalendarManager変数 * アプリケション共通で利用する。 */ const calendarManager = new CalendarManager(); // eslint-disable-line新しくCalendarオブジェクトの管理を行うクラスを作成した。
また、グローバル関数としてCalendarManagerオブジェクトを設定しておくことで、画面のどこからでもCalendarオブジェクトの作成・取得を単純化できるようにした。
panel.js
panel.js'use strict'; /** * カレンダーを表示するパネル(HTML要素)を作成する関数 * グローバル関数なのでどこからでも取得できる * @return パネル(HTML要素) */ window.createPanel = function() { // カレンダーのパネルを作成 const panel = document.createElement('div'); panel.id = 'panel'; // 子要素にテーブルを追加 const table = panel.appendChild(document.createElement('table')); table.id = 'table'; table.classList.add('calendar'); // テーブルにキャプション追加 const caption = table.appendChild(document.createElement('caption')); caption.id = 'caption'; // ヘッダー追加 const header = table.appendChild(document.createElement('thead')); // ヘッダーのカラムを作成する。 const headerRow = header.appendChild(document.createElement('tr')); headerRow.appendChild(document.createElement('th')).innerText = '日'; headerRow.appendChild(document.createElement('th')).innerText = '月'; headerRow.appendChild(document.createElement('th')).innerText = '火'; headerRow.appendChild(document.createElement('th')).innerText = '水'; headerRow.appendChild(document.createElement('th')).innerText = '木'; headerRow.appendChild(document.createElement('th')).innerText = '金'; headerRow.appendChild(document.createElement('th')).innerText = '土'; // body作成 let cellNum = 1; for (let i = 1; i < 7; i++) { const row = table.appendChild(document.createElement('tr')); row.id = 'row' + i; for(let j = 1; j < 8; j++) { const cell = row.appendChild(document.createElement('td')); cell.id = 'cell' + cellNum; cellNum++; } } return panel; };カレンダーのベースとなるパネル(HTML要素)を作成する関数をグローバル関数として設定するようにした。
元々Calnedarクラス内のメソッドとして記述していたが、カレンダーの大枠は常に共通(日 ~ 土 の7列 × 6週間分)なのでオブジェクトごとにメソッドが作成されるのは無駄となる。
どこからでもアクセスできる関数を作成してCalendarオブジェクトからアクセスするようにした。リファクタリングをしてみて
アプリを動かしても動きは変わらない。というか変わったらリファクタリングではない。
ソースのリファクタリングとは基本的に、既存の動作には一切変更を与えずにソースの保守性を高めることだと思っている。デグレなしが前提のはず元のファイルと比較しても、そのファイルが何をするためのものなのかがハッキリしたのではないかと思う。
まだまだ、修正の余地がありそうなので思いついたらリファクッていこうと思います。
- 投稿日:2020-03-24T21:11:04+09:00
[Node.js] request モジュール がDeprecated になった
- 投稿日:2020-03-24T21:11:04+09:00
[Node.js] request モジュール がDeprecated になっていた
内容
As of Feb 11th 2020, request is fully deprecated. No new changes are expected to land. In fact, none have landed for some time.
- 2020年2月11日をもってDeprecatedになったようです。
- 下記のモジュールについても
request
と依存関係があることから非推奨となりました。- 代替手段についてはこのissueで議論されています。
参考ページ
- 投稿日:2020-03-24T20:47:48+09:00
【JavaScript】全ての値がオブジェクトに`なり得る`
はじめに
開眼!JavaScriptを読んだまとめ的な記事です。
オブジェクト
「オブジェクトとは、名前と値を持つプロパティを格納するコンテナにすぎない」
著者も言っていますが、オブジェクトとはただのプロパティを持った箱です。
これ以上も以下もありません。プリミティブ型
リテラルを使って値を生成したり、new演算子を使用せずにコンストラクタ関数で値を生成した際には、プリミティブ型となります。
// どちらもプリミティブ型 const myString = 1; const myString2 = String('hello')プリミティブ型はオブジェクトではありません。
ですが、プリミティブ型でもStringオブジェクトで使用できるメソッドを使用することができます。
何故でしょう。// オブジェクトを生成 const myString = new String('hello'); console.log(myString.length); // プリミティブ型を生成 const myString2 = 'hello'; console.log(myString2.length);どちらも結果は「5」となります。
プリミティブ型の振る舞い
プリミティブ型はオブジェクトとして扱われません。オブジェクトのように扱われるまでは。
上の例で、プリミティブ型に対してlengthを使用した時が、プリミティブ型がオブジェクトのように扱われたタイミングです。
このタイミングで何が行われているのでしょう。
console.log(myString2.length);ここが呼び出される前まではプリミティブ型として扱われています。
しかし、ここが呼び出されたタイミングで、このプリミティブ型をオブジェクトのように扱うために、JavaScriptがバックグラウンドで補完するラッパーオブジェクト(new String('hello')のような)を生成します。つまり、下記の2つは同じ意味となります。
const myString = 'hello'; console.log(myString.length) console.log((new String('hello')).length)まとめ
つまり、JavaScriptは全ての値がオブジェクトということではありません。
タイトルの通り、JavaScript全ての値がオブジェクトのような振る舞いをするということです。
- 投稿日:2020-03-24T20:30:02+09:00
【JavaScript】一度は聞いた「即時関数」と「クロージャ」について
概要
一度は耳にしたであろう「即時関数」「クロージャ」について簡易的にまとめました。
即時関数
名前の通り、即実行される関数のことですね?
通常、無名関数を実行する時は、下記のように一旦変数に格納して括弧演算子で実行したりします。var hello = function(name) { return `${name}さん、Hello!!`; }; hello('太郎') // 太郎さん、Hello!!ですが、JavaScriptには即時関数と呼ばれる構文が存在します。
上の例を即時関数で書き換えると、以下のような記述になります。(function(name) { return `${name}さん、Hello!!`;})('太郎'); // 太郎さん、Hello!!上のように括弧(グループ化演算子)で囲むことにより、定義と実行が行われるのですね。
* ちなみに、以下のように即時関数に関数名をつけたりもできます。(function hello(name) { return `${name}さん、Hello!!`;})('太郎'); // 太郎さん、Hello!!さて、即時関数はどういった時に使用するのでしょうか?
これは、グローバルスコープの汚染するを防ぐ方法として利用されたりしています。
ちなみに、ここでの「グローバルスコープの汚染する」は、グローバルに変数や関数を宣言することを指します。
複数人での開発したり複数のライブラリ読みこんだりしている場合に、グローバルスコープが汚れてしまうことで、
変数名や関数名が重複してしまうといったことが起きたりしてしまいます?このグローバルスコープの汚染するを防ぐ方法ですが、即時関数だけではなく他にも方法があります。
オブジェクトを名前空間として用いる方法です。var myHello = myHello || function(name) { return `${name}さん、Hello!!`; }; myHello('太郎') // 太郎さん、Hello!!上のように、
myHello
が既に定義されているときはそっちを優先みたいに名前の衝突は防げますね?
他にも、関数を名前空間として用いる方法もあります。
関数内で宣言される変数は、関数内にスコープを持ちます。(ローカル変数と呼びます。)
これは、関数の中からはアクセスできるけど外からはアクセスできません。
この性質を利用してしまえば名前の衝突をグローバルは汚れません。var user = 'グローバル太郎'; (function(){ var user = 'ローカル太郎'; var user2 = 'ローカル太郎2'; })(); console.log(user); // グローバル太郎 console.log(user2); // ReferenceError見事に、「グローバル太郎」を「ローカル太郎」にしなくて済みました。
ここで紹介した関数内にスコープを持つという性質は結構重要な項目でして、次項で説明するクロージャでも利用されます。クロージャ
クロージャとはMDNによると、下記のとおりです。
クロージャは、関数と、その関数が宣言されたレキシカル環境の組み合わせです。
ソースコードで表すと、以下のような感じです。
function makeCounter() { var count = 0; function f() { return count++; } return f; } var counter = makeCounter();上の
counter
がクロージャにあたります。
意味合い的には、makeCounter関数内のf関数がクロージャとなってcounter変数に格納されたという感じですかね。
ちなみに、クロージャの仕組みについては、ScopeChainとかの説明が必要で長いので詳細は割愛します?
(このサイトとか参考になります!)さて、このクロージャとやらの何がすごいのか。
実は、変数の永続化や隠蔽を実現することができます。前項でも触れましたが、ローカル変数 (関数の中で宣言された変数) というのは、関数の外からアクセスすることができません。
また、本来、ローカル変数はその関数が実行されている間だけ存在します。function makeCounter() { var count = 0; console.log('makeCounterのローカル変数', count) function f() { return count++; } return f; } var counter = makeCounter(); // makeCounterのローカル変数 0 console.log(counter()); // 0 console.log(counter()); // 1上のコードを実行してみると、
var counter = makeCounter();
のタイミングでローカル変数が存在しているのを確認できますが、counter実行時にもローカル変数countの参照が残り続けていることも確認できます。
ローカル変数countがクロージャの内部状態としてメモリ上に残り続けているのですね。
また、counter()
と実行することで変数の状態を変更できていますね。まるで、makeCounterクラスのメンバ変数countがクラス内のメソッドfによってのみ変更できる感覚を思い出します?
クロージャはカプセル化されたオブジェクト的な存在というわけですね?以上、「即時関数」「クロージャ」について簡単なまとめでした。
- 投稿日:2020-03-24T16:58:47+09:00
Vue.jsでjszipとfilesaver.jsを使ってzipフォルダを作成し、ダウンロードしてみる。
初めに
今回はjavascriptのライブラリの一つ、jszipとfilesaver.jsというライブラリの記事を書こうと思います。
jszipというものはzipとして圧縮するためのライブラリで,filesaver.jsはファイルをダウンロードするためのライブラリです。
必要最低限(zipフォルダにファイルやフォルダ又はフォルダの中にファイルを入れる)のやり方を書きます。必要な技術、ライブラリ
- vue.cli(vue.js)
- jszip
- filesaver.js
やり方
必要最低限をやるために必要なメソッドは、file() folder() generateAsync() saveAs() の4つ。
file()の第一引数はファイル名(string型),第二引数は内容(string型,array型etc)です。
folder()の第一引数はファイル名(string型)。
generateAsync()はzipフォルダを作成するためのメソッドで、第一引数には作成するzipのタイプを指定します。
saveAs()はfilesaver.jsのメソッドです。第一引数にはダウンロードするzipフォルダ、第二引数はzip名。practice.vue<template> <div> <a v-on:click="make">ダウンロード</a> </div> </template> <script> import jszip from 'jszip' import saveAs from 'filesaver' export default{ methods:{ make(){ let zip = new jszip(); //インスタンス作成 zip.folder('icon').file('test.txt','hello world') //iconフォルダを作り、その中にtest.txtファイルを作っている。 zip.file('practice.js','hello') // iconフォルダと同じ階層にjsファイルを作っている。 zip.generateAsync({type:'blob'}) //blobタイプのzipを作成。 .then(function(blob){ //zipデータを受け取る。 saveAs(blob,"hello.zip") //第一引数は受け取ったzipデータ,第二引数はzipの名前(.zipは無くてもいい) }) } } </script>これはjszipのページのexampleに載っているコードを少し書き直した物ですが、これだけでzipフォルダを作る事ができます。
終わりに
また時間があったらjszipとfilesaver.jsを勉強して記事を書こうと思います。
- 投稿日:2020-03-24T15:56:47+09:00
[CSS/js]tableの行/列ヘッダーを固定する
はじめに
最近Webアプリ周辺の技術を学び始めた者です。普段は製造業で設計業務を担当しておりますが、社内システム構築用にいろいろと勉強しています。主に使うのは下記。
- 言語 : python/C#
- Web framework : django
記事に書き出すことで自身の理解も深まると考え、今回初投稿をさせて頂きます。
動機
休日に妻と共同でアプリ開発をしています(この開発記録もつけられたらなーと思っています)。
tableを多用するのですが、その際列ヘッダーと行ヘッダーを固定してtbodyのデータセルだけスクロールできないかなと考え、いろいろ調べていました。
便利なプラグインもたくさんありましたが、
- 複数ヘッダーを固定できるものが限られていた
- なるべく既存のtableに変更を加えたくない
という理由で手を出せず。position:stickyというステキなオプションがあるので、どうにかこれを使ってできないかと思い、やってみました。
なおこちらのstackoverflowの質問を参考にしました→Table with fixed header and fixed column on pure csshtml
下記のようなtableと、wrapperとなるdivを用意します。class名はbootstrapを意識しています。
sample.html<div class="table-wrapper"> <table class="table text-nowrap sticky-table table-borderless"> <thead class="thead-light"> <tr class="fixed-header-0"> <th class="fixed-column-0">日付</th> <th>1/1</th> <th>1/2</th> <th>1/3</th> <th>1/4</th> <th>1/5</th> <th>1/6</th> <th>1/7</th> </tr> <tr class="fixed-header-1"> <th class="fixed-column-0">曜日</th> <th>月</th> <th>火</th> <th>水</th> <th>木</th> <th>金</th> <th>土</th> <th>日</th> </tr> </thead> <tbody> <tr> <th class="fixed-column-0 table-light">AM</th> <td>〇</td> <td>〇</td> <td>〇</td> <td>×</td> <td>×</td> <td>〇</td> <td>〇</td> </tr> <tr> <th class="fixed-column-0 table-light">PM</th> <td>×</td> <td>〇</td> <td>〇</td> <td>×</td> <td>〇</td> <td>×</td> <td>〇</td> </tr> <tr> <th class="fixed-column-0 table-light">夜</th> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>×</td> </tr> </tbody> </table> </div>
日付 1/1 1/2 1/3 1/4 1/5 1/6 1/7 曜日 月 火 水 木 金 土 日 AM 〇 〇 〇 × × 〇 〇 PM × 〇 〇 × 〇 × 〇 夜 〇 〇 〇 〇 〇 〇 × ゴールは上記tableの「日付」「曜日」行を固定しつつ、「AM」「PM」「夜」も常に表示することです。
wrapperにはclass=table-wrapper、tableにはclass=sticky-tableを指定します。また、固定したい行ヘッダーとなる各trには上から順にclass=fixed-header-n(nは0始まりの番号)を指定し、固定したい列ヘッダーとなる各thには左から順にclass=fixed-column-m(mは0始まりの番号)を指定します。
tableの左上の場所は行列方向に拘束したいので、tr.fixed-header-nおよびth.fixed-column-mの両方の指定が必要です。ちなみにヘッダーにtable-lightやthead-lightで色を付けているのは、透明のままだとヘッダー固定したときに他のセルと重なってしまうからです。
css/js
下記のようなcssとjsを作成します。
sticky-table.css/*ラッパー*/ div.table-wrapper { overflow: scroll; max-height:500px; /*任意*/ max-width:1000px; /*任意*/ } /*行ヘッダーを固定する。topの値はjsで動的に指定*/ table.sticky-table thead tr[class*="fixed-header-"] th { position: -webkit-sticky; /* for Safari */ position: sticky; /* tbody tdより手前に表示する */ z-index: 1; } /*行ヘッダーと列ヘッダーが重なる部分を固定する。top,leftの値はjsで動的に指定*/ table.sticky-table thead tr[class*="fixed-header-"] th[class*="fixed-column-"] { /* 全てのセルより手前に表示する */ z-index: 2; } /*列ヘッダーを固定する。leftの値はjsで動的に指定*/ table.sticky-table tbody th[class*="fixed-column-"] { position: -webkit-sticky; /* for Safari */ position: sticky; /* tbody tdより手前に表示する */ z-index: 1; }コメントを添えていますが、
div.table-wrapper { overflow: scroll; max-height:500px; /*任意*/ max-width:1000px; /*任意*/ }はwrapperの挙動です。max-height/max-widthは任意の値に設定してください。
続いて下記のような.jsを作成します。結局jqueryで書いてしまった。
sticky-table.js//行ヘッダーに対しtopを設定 height = 0; for (var i = 0; i < fixed_header_num; i++) { $(".fixed-header-" + i + " th").css('top', height); height += $(".fixed-header-" + i + " th").outerHeight(); } //列ヘッダーに対しleftを設定 width = 0; for (var j = 0; j < fixed_column_num; j++) { $("th.fixed-column-" + j).css('left', width); width += $("th.fixed-column-" + j).outerWidth(true); }jsでは固定したい各ヘッダーに対し、「どこまでの位置に達したら上/左方向への移動を拘束するか」の値となるtop/leftの値を動的に設定しています。一番上のヘッダーはtop=0でよいのですが、二番目以降のヘッダーは自身の上にあるヘッダーの累積高さ分の値を設定しています。列ヘッダーも同様。
fixed_header_num, fixed_column_numはそれぞれ、固定したい行/列ヘッダーの数なのですが、これらは使用シーンに合わせて変わると思うので、グローバルで宣言することにします。
使用例
下記のツリー構造を仮定します。
sticky-table/ ├ sample.html └ static/ ├ css/ │ └ sticky-table.css └ js/ └ sticky-table.jssample.html<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8" /> <title>ヘッダー固定</title> <!--bootstrap--> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <!--sticky-table--> <link rel="stylesheet" href="static/css/sticky-table.css"> </head> <body> <!--https://stackoverflow.com/questions/15811653/table-with-fixed-header-and-fixed-column-on-pure-css--> <div class="container"> <div class="table-wrapper"> <table class="table text-nowrap sticky-table table-borderless"> <thead class="thead-light"> <tr class="fixed-header-0"> <th class="fixed-column-0">日付</th> <th>1/1</th> <th>1/2</th> <th>1/3</th> <th>1/4</th> <th>1/5</th> <th>1/6</th> <th>1/7</th> </tr> <tr class="fixed-header-1"> <th class="fixed-column-0">曜日</th> <th>月</th> <th>火</th> <th>水</th> <th>木</th> <th>金</th> <th>土</th> <th>日</th> </tr> </thead> <tbody> <tr> <th class="fixed-column-0 table-light">AM</th> <td>〇</td> <td>〇</td> <td>〇</td> <td>×</td> <td>×</td> <td>〇</td> <td>〇</td> </tr> <tr> <th class="fixed-column-0 table-light">PM</th> <td>×</td> <td>〇</td> <td>〇</td> <td>×</td> <td>〇</td> <td>×</td> <td>〇</td> </tr> <tr> <th class="fixed-column-0 table-light">夜</th> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>×</td> </tr> </tbody> </table> </div> </div> <!--jquery+bootstrap--> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> <!--sticky-table--> <script type="text/javascript"> //固定するヘッダーの数 var fixed_header_num = 2; //固定するカラムの数 var fixed_column_num = 1; </script> <script src="static/js/sticky-table.js"></script> </body> </html>これで任意の数の行/列ヘッダーをcss/jsのみで固定することができます。
課題
Chromeでは動作確認しましたが、IEだとpolyfillが必要みたいです。
stickyfillまた、globalで変数指定が必要だったり、洗練されてないイメージもあるので、もっとうまいやり方がありましたらご教示頂ければ幸いです。
参考
Table with fixed header and fixed column on pure css
stickyfill
- 投稿日:2020-03-24T15:56:47+09:00
[CSS/jsのみ]tableの行/列ヘッダーを固定する
はじめに
最近Webアプリ周辺の技術を学び始めた者です。普段は製造業で設計業務を担当しておりますが、社内システム構築用にいろいろと勉強しています。主に使うのは下記。
- 言語 : python/C#
- Web framework : django
記事に書き出すことで自身の理解も深まると考え、今回初投稿をさせて頂きます。
動機
休日に妻と共同でアプリ開発をしています(この開発記録もつけられたらなーと思っています)。
tableを多用するのですが、その際列ヘッダーと行ヘッダーを固定してtbodyのデータセルだけスクロールできないかなと考え、いろいろ調べていました。
便利なプラグインもたくさんありましたが、
- 複数ヘッダーを固定できるものが限られていた
- なるべく既存のtableに変更を加えたくない
という理由で手を出せず。position:stickyというステキなオプションがあるので、どうにかこれを使ってできないかと思い、やってみました。
なおこちらのstackoverflowの質問を参考にしました→Table with fixed header and fixed column on pure csshtml
下記のようなtableと、wrapperとなるdivを用意します。class名はbootstrapを意識しています。
sample.html<div class="table-wrapper"> <table class="table text-nowrap sticky-table table-borderless"> <thead class="thead-light"> <tr class="fixed-header-0"> <th class="fixed-column-0">日付</th> <th>1/1</th> <th>1/2</th> <th>1/3</th> <th>1/4</th> <th>1/5</th> <th>1/6</th> <th>1/7</th> </tr> <tr class="fixed-header-1"> <th class="fixed-column-0">曜日</th> <th>月</th> <th>火</th> <th>水</th> <th>木</th> <th>金</th> <th>土</th> <th>日</th> </tr> </thead> <tbody> <tr> <th class="fixed-column-0 table-light">AM</th> <td>〇</td> <td>〇</td> <td>〇</td> <td>×</td> <td>×</td> <td>〇</td> <td>〇</td> </tr> <tr> <th class="fixed-column-0 table-light">PM</th> <td>×</td> <td>〇</td> <td>〇</td> <td>×</td> <td>〇</td> <td>×</td> <td>〇</td> </tr> <tr> <th class="fixed-column-0 table-light">夜</th> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>×</td> </tr> </tbody> </table> </div>ゴールは上記tableの「日付」「曜日」行を固定しつつ、「AM」「PM」「夜」も常に表示することです。
wrapperにはclass=table-wrapper、tableにはclass=sticky-tableを指定します。また、固定したい行ヘッダーとなる各trには上から順にclass=fixed-header-n(nは0始まりの番号)を指定し、固定したい列ヘッダーとなる各thには左から順にclass=fixed-column-m(mは0始まりの番号)を指定します。
tableの左上の場所は行列方向に拘束したいので、tr.fixed-header-nおよびth.fixed-column-mの両方の指定が必要です。ちなみにヘッダーにtable-lightやthead-lightで色を付けているのは、透明のままだとヘッダー固定したときに他のセルと重なってしまうからです。
css/js
下記のようなcssとjsを作成します。
sticky-table.css/*ラッパー*/ div.table-wrapper { overflow: scroll; max-height:200px; /*任意*/ max-width:400px; /*任意*/ } /*行ヘッダーを固定する。topの値はjsで動的に指定*/ table.sticky-table thead tr[class*="fixed-header-"] th { position: -webkit-sticky; /* for Safari */ position: sticky; /* tbody tdより手前に表示する */ z-index: 1; } /*行ヘッダーと列ヘッダーが重なる部分を固定する。top,leftの値はjsで動的に指定*/ table.sticky-table thead tr[class*="fixed-header-"] th[class*="fixed-column-"] { /* 全てのセルより手前に表示する */ z-index: 2; } /*列ヘッダーを固定する。leftの値はjsで動的に指定*/ table.sticky-table tbody th[class*="fixed-column-"] { position: -webkit-sticky; /* for Safari */ position: sticky; /* tbody tdより手前に表示する */ z-index: 1; }コメントを添えていますが、
div.table-wrapper { overflow: scroll; max-height:200px; /*任意*/ max-width:400px; /*任意*/ }はwrapperの挙動です。max-height/max-widthは任意の値に設定してください。
続いて下記のような.jsを作成します。結局jqueryで書いてしまった。
sticky-table.js//行ヘッダーに対しtopを設定 height = 0; for (var i = 0; i < fixed_header_num; i++) { $(".fixed-header-" + i + " th").css('top', height); height += $(".fixed-header-" + i + " th").outerHeight(); } //列ヘッダーに対しleftを設定 width = 0; for (var j = 0; j < fixed_column_num; j++) { $("th.fixed-column-" + j).css('left', width); width += $("th.fixed-column-" + j).outerWidth(true); }jsでは固定したい各ヘッダーに対し、「どこまでの位置に達したら上/左方向への移動を拘束するか」の値となるtop/leftの値を動的に設定しています。一番上のヘッダーはtop=0でよいのですが、二番目以降のヘッダーは自身の上にあるヘッダーの累積高さ分の値を設定しています。列ヘッダーも同様。
fixed_header_num, fixed_column_numはそれぞれ、固定したい行/列ヘッダーの数なのですが、これらは使用シーンに合わせて変わると思うので、グローバルで宣言することにします。
使用例
下記のツリー構造を仮定します。
sticky-table/ ├ sample.html └ static/ ├ css/ │ └ sticky-table.css └ js/ └ sticky-table.jssample.html<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <meta charset="utf-8" /> <title>ヘッダー固定</title> <!--bootstrap--> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <!--sticky-table--> <link rel="stylesheet" href="static/css/sticky-table.css"> </head> <body> <!--https://stackoverflow.com/questions/15811653/table-with-fixed-header-and-fixed-column-on-pure-css--> <div class="container"> <div class="table-wrapper"> <table class="table text-nowrap sticky-table table-borderless"> <thead class="thead-light"> <tr class="fixed-header-0"> <th class="fixed-column-0">日付</th> <th>1/1</th> <th>1/2</th> <th>1/3</th> <th>1/4</th> <th>1/5</th> <th>1/6</th> <th>1/7</th> </tr> <tr class="fixed-header-1"> <th class="fixed-column-0">曜日</th> <th>月</th> <th>火</th> <th>水</th> <th>木</th> <th>金</th> <th>土</th> <th>日</th> </tr> </thead> <tbody> <tr> <th class="fixed-column-0 table-light">AM</th> <td>〇</td> <td>〇</td> <td>〇</td> <td>×</td> <td>×</td> <td>〇</td> <td>〇</td> </tr> <tr> <th class="fixed-column-0 table-light">PM</th> <td>×</td> <td>〇</td> <td>〇</td> <td>×</td> <td>〇</td> <td>×</td> <td>〇</td> </tr> <tr> <th class="fixed-column-0 table-light">夜</th> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>〇</td> <td>×</td> </tr> </tbody> </table> </div> </div> <!--jquery+bootstrap--> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> <!--sticky-table--> <script type="text/javascript"> //固定するヘッダーの数 var fixed_header_num = 2; //固定するカラムの数 var fixed_column_num = 1; </script> <script src="static/js/sticky-table.js"></script> </body> </html>これで任意の数の行/列ヘッダーをcss/jsのみで固定することができます。
課題
Chromeでは動作確認しましたが、IEだとpolyfillが必要みたいです。
stickyfillまた、globalで変数指定が必要だったり、洗練されてないイメージもあるので、もっとうまいやり方がありましたらご教示頂ければ幸いです。
参考
Table with fixed header and fixed column on pure css
stickyfill
- 投稿日:2020-03-24T15:51:38+09:00
イベントが発生した時、その兄弟要素を操作する方法
概要
jQueryを使用せずに実装することを目指す。
同じ構造をした要素が複数存在する際に、その中でイベントが発生した兄弟要素だけを操作したい場合の実装方法である。実装例
クリックしたらその兄弟要素にあたる画像を小さくするボタンを実装。今回は同じ構造の要素を4つ用意。
HTMLとcss
参考程度に掲載
HTML<ul class="item-list"> <li class="item-list__item"> <img src="images/1.jpeg" alt="img1" class="img-op"> <button class="popup-btn btn">look</button> </li> <li class="item-list__item"> <img src="images/2.jpeg" alt="img2" class="img-op"> <button class="popup-btn btn">look</button> </li> <li class="item-list__item"> <img src="images/3.jpeg" alt="img3" class="img-op"> <button class="popup-btn btn">look</button> </li> <li class="item-list__item"> <img src="images/4.jpeg" alt="img4" class="img-op"> <button class="popup-btn btn">look</button> </li> </ul>SCSSbutton { overflow : hidden; outline : none; } .text-center { text-align: center; } .img-pc { display:none; } .wrapper { box-sizing: border-box; padding: 0 20px; } .btn { display: block; border-radius: 2px; -webkit-appearance:none; appearance:none; padding: 8px 16px; font-size: 1.2rem; border:none; color: #fff; background-color: $main-color; font-family: 'Montserrat', sans-serif; outline: none; cursor: pointer; } .item-list { //パラメータ $marginLeft: 16px; //プロパティ display: flex; justify-content: center; flex-wrap: wrap; margin-top: 32px; margin-left: (-1 * $marginLeft); padding: 0 16px; &__item { display: block; margin-top: 32px; margin-left: $marginLeft; width: 200px; } } .img-op { width: 240px; height: 96px; display: block; border-radius: 8px; object-fit: cover; &.active { transform: scale(0.5); } } .img-input { display: none; } .popup-btn { display: block; margin: 16px auto; }Javascript
JavaScript//popup-btnクラスの要素を全て取得 let popupBtns = Array.from(document.getElementsByClassName('popup-btn')); popupBtns.forEach(function(popupBtn) { //その中からクリックイベントが発生した要素を取得 popupBtn.addEventListener('click', function() { //親要素に遡り、ymg-opクラスの要素を取得。activeクラスをtoggle。 popupBtn.parentElement.getElementsByClassName('img-op')[0].classList.toggle('active'); }); });実装のヒント
考え方としては以下の通り。
jQueryだと簡単だけど、生jsだと結構面倒臭い。
- イベントが発生した要素を取得
- 1.で取得した要素の親要素を取得
- 2.の子要素を取得(1.の兄弟要素にあたる)
- 3.の要素を操作
という周りクドい手順を踏むことによって、やっとこさ兄弟要素を操作することができる。
- 投稿日:2020-03-24T14:07:54+09:00
GoogleAppsScriptでのMail とUserとSession
だんだん内容が薄くなってきました
先日、Lineに通知を出すやつをつくって、早速稼働してるのだけど、LineにURL送るとクリックしたときにLine内のブラウザーで開いてしまい、ちょっと面倒なことに。(ログインが必要なWebサイドなので)というわけで、メールで送っちゃおう、というのが今日の趣旨です。
IFTTT側でメールを出す方法もありますが、アプレットつくるより早そうだったので。。。1.MailAppクラス
至ってシンプル。リファレンスはこちら
let mailObject = { to:"hogehoge@gmail.com", subject:"hoge", body:"hoge hoge-!" }; MailApp.sendEmail(mailObject);sendEmailは引数の違うメソッドがいくつかあるけど、普段はこれで十分かと。
2.Userクラス
送り先のメールアドレスをそのままスクリプトに書くのもアレだし、ひとまず自分に送るんだからスクリプトユーザーにおくる手があるはず、とリファレンスを探ると
Userクラスというものがあった。
メンバーがgetEmailのみ、というシンプルなクラス(前はLoginIDがあった)で、ユーザー識別子か、今回のようにスクリプトを動かしているユーザーにメールを出すためだけにあるようなもの。let email = user.getEmail();3.Sessionクラス
Userクラスを取得する一つの方法がSessionクラスのgetActiveUser
let user = Session.getActiveUser(); let email = user.getEmail();2と3は順番が逆に書くべきだったかもしれない。
4.BaseService
上記3つのクラスは、GASのクラスの中で、BaseServiceに所属している。
このServiceには
Blob
Menu
UI
console
クラスなどがあります。
- 投稿日:2020-03-24T12:42:03+09:00
Destructuring を用いた関数の引数を初期化する方法
Destructuring とは
Destructuring assignment は ES6 で導入された JavaScript の記法です。
主に、配列やオブジェクトで用いられ、非常にパワフルに活躍します。今回は、関数の引数として用いるケースについて紹介します。
利用例
下記のような二種類の関数を考えます。
const normalFunc = (arg1?, arg2?, arg3?) => { ... } const destructFunc = ({arg1, arg2, arg3}) => { ... }それぞれの関数に対して、
arg3
だけに値を設定して使いたいケースでは、それぞれ、normalFunc(null, null, 'something'); destructFunc({ arg3: 'something' });と書きます。
Destructuring assignment を用いた場合、他の変数を考慮しなくて済みます。引数を初期化する
では、引数を初期化したい場合はどうなるでしょうか。
通常の関数の場合は、以下のように書くことができます。const normalFunc = (arg1 = 1, arg2 = 2, arg3 =3) => { return { arg1, arg2, arg3 }; } console.log(normalFunc()); // Output: {arg1: 1, arg2: 2, arg3: 3} console.log(normalFunc(10)); // Output: {arg1: 10, arg2: 2, arg3: 3} console.log(normalFunc(undefined, undefined, 10)); // Output: {arg1: 1, arg2: 2, arg3: 10}Destructuring を用いた場合、どのように記載できるでしょうか?
const destructFunc = ({arg1 = 1, arg2 = 2, arg3 = 3}) => { return { arg1, arg2, arg3 }; } console.log(destructFunc({arg1:10})); // Output: {arg1: 10, arg2: 2, arg3: 3} console.log(destructFunc({arg3:10})); // Output: {arg1: 1, arg2: 2, arg3: 10}
arg3
を設定するときにundefined
を追加しなくても動作するので良い感じですね。ただ、実はこれだと少し不足があります。引数を取り除いてみます。
console.log(destructFunc()); // Output: TypeError通常の関数では、すべての引数をオプショナルで渡していたので、
normalFunc()
と記載することができました。
一方で、 Destructuring を用いた関数では、オブジェクトを引数として渡さないといけないので、destructFunc({})
と記載する必要があります。
これだと毎回{}
を書かないといけないので少し不便です。
destructFunc()
に対応する引数なしの場合のケースは以下のように書くことができます。
const destructFunc = ( {arg1 = 1, arg2 = 2, arg3 = 3} = { arg1: 1, arg2: 2, arg3: 3, }, ) => { return { arg1, arg2, arg3 }; }初期化の部分で少し可読性が低いですね。
以下の形で書くこともできます。
この場合、return { arg1, arg2, arg3 }
と省略できなくなります。const destructFunc = ({arg1, arg2, arg3} = {}) => { return { arg1: arg1 || 1, arg2: arg2 || 2, arg3: arg3 || 3, }; }TypeScript を用いるとこれらを改善できます。
interface Params { arg1?: number; arg2?: number; arg3?: number; } const destructFunc = ({...props}: Params = {}) => { const { arg1 = 1, arg2 = 2, arg3 = 3 } = props; return { arg1, arg2, arg3, }; }引数は
Params
によって定義されているので明確です。
初期化処理も、繰り返しを避けることができました。記事の内容に不備、誤りなどがある場合はご指摘いただけると幸いです。
また、よりスマートな記法をご存知であれば是非教えていただきたいです。読んでいただきありがとうございました。
- 投稿日:2020-03-24T10:09:52+09:00
以下のサイトのコードの完全版欲しいです
GASで作る社内ツール (キャンセル・報告編
https://qiita.com/nyanko-box/items/905d224d3ba8d3d00796ソースコードそのままコピペしても、
キャンセル、報告ボタンが書かれておらず動きません。完全に動くソースコードが欲しいです。当方、プログラミング初心者につき、最初は先人のマネから入って、
カスタマイズして、業務に応用させたく、どなたかお力添えはただけると幸いです。
- 投稿日:2020-03-24T09:23:19+09:00
ES2018で追加された機能について
はじめに
JavaScript: Everything From ES2016 to ES2019のを読み、本稿はES2018で導入された機能についてわかったことを自分なりにまとめたものになります。
レスト構文とスプレッド構文(Rest/Spread)をオブジェクトにも使用可能
ES2015では、配列にスプレッド構文を使用することができました。
const name = ["taro", "ichiro", "hanako"]; const spread_name = [...name, "jiro"]; console.log(spread_name); //["taro", "ichiro", "hanako", "jiro"]ES2018では、オブジェクトにもスプレッド演算子を使用できるようになりました。
let sampleObj = { a: "1", b: "2", c: "3", d: "4", } let { a, b, ...z } = sampleObj; console.log(a); //1 console.log(b); //2 console.log(z); //{c: "3", d: "4"}スプレッド演算子によって複製されたオブジェクトについて
スプレッド演算子によって複製されたオブジェクトについて、
元のオブジェクトが変更された場合、複製されたオブジェクトには変更がされません。let sampleObj = { a: "1", b: "2", c: "3", d: "4", } let copy = { ...sampleObj }; console.log(copy); //{a: "1", b: "2", c: "3", d: "4"} sampleObj.e = 5; console.log(sampleObj); //{a: "1", b: "2", c: "3", d: "4"} //複製されたオブジェクトには変更されない console.log(copy); //{a: "1", b: "2", c: "3", d: "4"}非同期イテレータ
for-await-of
構文を使用して、非同期に反復可能なオブジェクトを繰り返すループを作ることができます。
for-of
構文では、反復可能なオブジェクト([Symbol.iterator]()
)が利用できました。
for-await-of
構文では、非同期反復可能なオブジェクト([Sysmbol.iterator]()
)が利用できます。
以下のサンプルコードは、async function*
で宣言した非同期ジェネレータを繰り返して処理するコードです。let sleep = (ms) => new Promise((func) => setTimeout(func, ms)); async function* asyncGenerator() { yield 1; await sleep(1000); yield 2; await sleep(1000); yield 3; await sleep(1000); } (async function() { for await (item of asyncGenerator()) { console.log(item); } })();Promise.prototype.finally()
MDNによると、次のように示されています。
finally() メソッドは、Promise を返します。成功・失敗にかかわらず、promise が確立したら指定したコールバック関数が実行されます。これにより、promise が成功裏に実行されたか否かに関わりなく、Promise が処理された後に実行されなければならないコードを提供できます。
let myPromise = new Promise((resolve, reject) => { resolve(); }) myPromise .then(() => { console.log('still working'); }) .catch(() => { console.log('there was an error'); }) .finally(() => { console.log('Done!'); }) // still working // Done!正規表現に関する新しい機能
正規表現に関連する新しい機能について、ここでは以下3つを紹介します。
s
フラグ (s(dotAll) flag for regular)- 名前付きキャプチャグループ (RegExp named capture groups)
- 後読みアサーション(RegExp Lookbehind Assertions)
s(dotAll) flag for regular
正規表現に
s
オプションが導入されました。
https://github.com/tc39/proposal-regexp-dotall-flag
/./s
、new RegExp('.', 's')
のように使用して、
改行コードにマッチするための正規表現を記述できます。//sオプションを使用しない例 /foo[^]bar/.test('foo\nbar'); //true //sオプションを使用する例 /foo.bar.test/s.test('foo\nbar\ntest'); //true名前付きキャプチャグループについて
名前付きキャプチャグループとは、
(?<name>...)
で表現されたキャプチャグループのことをいいます。
例文どおりですが、名前付きキャプチャグループを使用すれば、
exec
メソッドで返されたオブジェクトのgroups.name
プロパティを参照することで、
正規表現にマッチした値を操作することができます。//名前付きなしキャプチャグループ let re = /(\d{4})-(\d{2})-(\d{2})/ let result = re.exec('2020-01-01') console.log(result) //["2020-01-01", "2020", "01", "01", index: 0, input: "2020-01-01", groups: undefined] //index: 0 //input: "2020-01-01" //groups: undefined //名前付きキャプチャグループ let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u; let result = re.exec('2020-01-01'); console.log(result) //["2020-01-01", "2020", "01", "01", index: 0, input: "2020-01-01", groups: {…}] //index: 0 //input: "2020-01-01" //groups: // year: "2020" // month: "01" // day: "01"後読みアサーション
肯定後読みアサーションは
(?<=...)
で表現されます。
直前に...
がある場合にマッチします。否定後読みアサーションは
(?<!...)
で表現され、
直前に...
がない場合にマッチします。let alphabet = 'abcdef'; let result = alphabet.match(/(?<=abc)def/); console.log(result) //["def", index: 3, input: "abcdef", groups: undefined] let result = alphabet.match(/(?<!abc)def/); console.log(result) //null let result = alphabet.match(/(?<!xyz)def/); console.log(result) //["def", index: 3, input: "abcdef", groups: undefined]
- 投稿日:2020-03-24T07:25:55+09:00
Kinx 実現技術 - VM(Virtual Machine)
Virtual Machine
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。作ったものの紹介だけではなく実現のために使った技術を紹介していくのも貢献。その道の人には当たり前でも、そうでない人にも興味をもって貰えるかもしれない。
前回のテーマは Switch-Case、今回のテーマは VM (Virtual Machine)。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
Virtual Machine
仮想機械。Kinx の場合、構文解析した AST から IR (Intermediate Representation) を構築、それを直接実行する。命令数は現時点で 190。include/ir.h に一覧がある。 一応、一通り第二オペランドの型がわかるものに関しては型ごとに命令を用意することで実行時コストを下げるようにはしてある。
出力例
今回は出力例から。以下のフィボナッチ数列のベンチマーク;
function fib(n) { if (n < 3) return n; return fib(n-2) + fib(n-1); } System.println("fib(34) = ", fib(34));これをコンパイルしてみると、以下のようになる。
_startup: .L1 0: jmp .L2(2) 1: halt _main1: .L2 2: enter 37, vars(35), args(1) 3: pushf __anonymous_func58 => .L3(10) 4: call 0 5: pop 6: pushf fib => .L460(d0a) 7: storevx $(0,33) 8: pushi 34 9: callvl0 $0(33), 1 a: pushs "fib(34) = " b: pushvl0 $0(1) c: calls "println", 2 d: pop e: ret null f: halt fib: .L460 d0a: enter 23, vars(1), args(1) .L461 d0b: lt_v0i $0(0), 3 d0c: jz .L463(d0e) .L462 d0d: retvl0 $0(0) .L463 d0e: pushvl0 $0(0) d0f: subi 2 d10: callvl1 $1(33), 1 d11: pushvl0 $0(0) d12: subi 1 d13: callvl1 $1(33), 1 d14: add d15: ret d16: haltアドレスが飛んでいるのは、標準ライブラリの読み込みなど、スタートアップルーチンの表示を省略するようにしているからです(
__anonymous_func58
のあたり)。ダイレクト・スレッデッド・コード
gcc の場合、ラベルに対するジャンプ命令を生成できるので俗にいう ダイレクト・スレッデッド・コード が実現できる。残念ながら Visual Studio では実現できない。
まず、最初にラベルに対するアドレス用のテーブルを作成する。この時、命令コードの番号と配列位置を合わせておくことでアドレスを一発で引けるようにしておく。ちなみに、C のラベルは関数をまたげないので、関数内で閉じてないといけない。
static void *jumptable[] = { &&LBL_KX_HALT, &&LBL_KX_NOP, &&LBL_KX_DUP, &&LBL_KX_IMPORT, &&LBL_KX_ENTER, &&LBL_KX_CALL, &&LBL_KX_CALLV, &&LBL_KX_CALLVL0, ... };次に、命令を一通りスキャンして、命令に対するアドレスを設定する。
for (int i = 0; i < code_len; ++i) { kx_code_t *c = fixcode[i]; c->gotolabel = jumptable[c->op]; ... }準備が整っていれば、命令実行の最後で次の命令に移動して
goto
する。LBL_KX_ENTER: ... cur = cur->next; goto *(cur->gotolabel);この辺が、include/kxexec.h にマクロとして定義してある。マクロは gcc と Visual Studio での共通化用です。
ダイレクト・スレッディングに関しては、結構古い記事だが YARV Maniacs 【第 3 回】 命令ディスパッチの高速化 がやっぱり分かりやすいと思う。
スタック構造
スタックは演算で使用されるが、関数呼び出し時にフレームを作成する。フレームにはローカル変数を格納するバッファが用意されており、フレームが GC されない限り参照できる。関数呼び出し時のスタック構造は以下の通り。
[ 0] frame obj .lex = previous lexical frame. --------------------------------------------------------- [-1] return address [-2] param count [-3] function obj (.lex) [-4] param 1 [-5] param 2 [ ] ... [..] param n [ ] ... [ ] frame obj -- previous frame ---------------------------------------------------------
.lex
はレキシカル・フレームへのポインタ。リンクリストの形でさかのぼることができる。余談だが、ここで演算用のスタックをフレームごとに個別に持たせれば Fiber でスタック状態も復元できると想定しているのだが、そこまで頑張る必要があるかどうかよく分からない。自分自身は困っていない。というのも、スタック状態を復元して良くなる点といえば式の中に
yield
を書くといった以下のようなコードだが、こういうの使えなくても良い気がする。var x = (yield 10)[0] + 50; var y = func(yield a, ++a);ちなみに現在の関数呼び出し時の引数評価は 後ろから だが、一般的にこれをあまり保証したくないので、上記のうちの関数呼び出しで使われる
yield
のa
の値はインクリメント後か前かは保証されないことになるだろう。レキシカル変数
レキシカル変数は関数オブジェクト作成時にリストとして関数オブジェクト自体に設定される。関数呼び出しの際、関数オブジェクトに格納されていたレキシカルスコープへのポインタが、作成されたフレームに設定される。関数内で関数が定義されると、その時のフレームをレキシカル・フレームとして登録することで数珠つなぎの形で参照できるようになる、といった算段。
スタック上のイメージは、フレームごとにレキシカル参照が設定されている状態。
|↑| frame -> lex -> lex -> ... |ス| │ |タ| │ |ッ| ↓ |ク| frame -> lex -> lex -> ... |↓| │尚、レキシカル参照されているフレームは GC で回収されないようにマークを付けなければならない。
スプレッド演算子(
...
)関数呼び出しの際のスプレッド演算子(例:
func(...a)
等)は、呼び出しの段階で個別のパラメータに展開される。その際、その数に合わせてparam count
の値が調整された形で格納される。例えば以下のコード;
b = [1,2,3]; func(a, ...b);これは実行時に以下のように展開されたのと同じ動作をする。
b = [1,2,3]; func(a, 1, 2, 3);もちろんコンパイル時には決定できないので、実行時に展開される。
eval
eval()
は、VM をネストさせるのが大変そうだったので、動的にコンパイルした結果を現在のコードの最後に追記し、同じ VM 上で単にジャンプするように実装している。こうしておくと例外発生時の扱いも単純になる。その他
どうも Visual Studio は Switch-Case の Case 数がある閾値を超えると 最適化をやめてしまう 模様。ちゃんとした公式文書が発見できなかったので、このあたりに詳しい方がいたら教えてください。目に見えてコンパイル時間が短くなり、パフォーマンスが悪くなる。
それにしても最適化が有効な時の src/ir_exec.c の Visual Studioでのコンパイル時間が泣きたいほどに遅い。かといってココを最適化しとかないと実行時速度がやばことになる。(今は WSL 上でやっている)gcc だと全然苦にならないほどコンパイルが速い。それでもダイレクト・スレッデッド・コードのおかげか、gcc でコンパイルしたほうがパフォーマンスも良い。実行ファイルに依存性を持たせない意味で Windows 上でのビルドは Visual Studio がいいんだけどなー。どうにかならんかな。
今後
IR のセーブ・ロード機能を付けたいところ。命令ごとに使うフィールドは決まっているので、それをルール化して書き出せばよいし、同じように読めばよい。やることはわかっているのだが、時間が追い付いていないな。
おわりに
今回も時間を割いて読んでいただいてありがとうございます。最後はいつもの以下の定型フォーマットです。興味がありましたら ★ とか 「
いいねLGTM」 ボタンとか押してもらえるとモチベーションにつながります。どうぞよろしくお願いします。
- 最初の動機は スクリプト言語 KINX(ご紹介) を参照してください(もし宜しければ「
いいねLGTM」ボタンをポチっと)。- リポジトリは ここ(https://github.com/Kray-G/kinx) です。こちらももし宜しければ★をポチっと。
- 投稿日:2020-03-24T05:05:55+09:00
コールバックと、ポリモーフィズムと、それからコルーチンを構造的に見る
この記事では、コールバックはポリモーフィズムの特別な場合であるという見方もできること、その意味ではそんなに難しい話ではないことを説明します。
あと、関数呼び出しを視覚的にブロックみたいなもので表現すると、ポリモーフィズムやコルーチンを使ってどういう事をやっているのか何となくわかった気になれるので、そのような表現を試みます。
ちょっと見方に変化をつけることで、難しそうな概念が身近になるといいなというような試みです。コールバックのおさらいと、ご利益
const someFunction = (someCallable) => { /* nagai syori */ const isContinued = confirm('nagai syori ga owatta yo! tudukeru?') someCallable(isContinued) } someFunction((isContinued) => { if (isContinued) { alert('Yeah!') } else { alert('Boo!') } })これは何の変哲もないコールバックです。someFunctionの引数として、無名の関数を渡してやることで、someFunctionの処理の中で無名の関数が呼ばれるようにします。
技術的に、このような渡し方が便利な場面はいくつかありますが、特に際立つものとしては非同期処理が終わった直後に確実にコールバック関数を呼び出させることができるということでしょうか。
例えば、JavaScriptにはsetTimeout()という、指定した時間がだいたい経過した後に引数に指定した関数を呼ぶような関数があります。このsetTimeout()を使って、以下のようなコードを書くと、0〜1000ミリ秒の間の時間が経過した後にalertが表示されます。
(setTimeout自体も一種のコールバックみたいなもので、適当な時間が経過した後に指定する処理を行わせるという機能性を実現しているのですが、一旦置いておきます。)const baka = () => { alert('Yeah!') } setTimeout(baka, 1000 * Math.random())この乱数を使っている部分は、実際には通信などで微妙な時間が経過する場合のシミュレーションだと思ってください。
では、このbaka()という関数をsetTimeoutで実行した"直後"に処理を行わせるには、どうすればよいでしょうか。これは、baka()に手を加えないとすれば、じつはとても難しいです。一方で、baka()に手を加えれば簡単で、const baka = () => { alert('Yeah!') alert('Hyahha-!!!') // 後続の処理 }このようにしてしまえば、baka()の主処理の後で、無駄な間が無く、かつ確実に後続の処理が行われます。
しかし、一般論としては、例えば通信処理Xが終わった直後に、ある場面では処理Aを行わせたり、別の場面では処理Bを行わせたり、というような事が必要になります。つまり、場面によって行いたい処理が変わるというような場合です。
また、baka()の中に何でもかんでも書こうとしても、baka()はバカなので一つの責務しか果たすことができず、処理を覚えることもできません。このように、前段の処理Xに対して、ある場面ではA、別の場面ではB、という事をXの呼び出し元で手軽に制御できるようにすることが、コールバックの大きなメリットなのでした。Xの処理の中で次の処理を呼び出すことにすれば、その処理は確実に呼び出され、かつその処理を引数で指定すれば、柔軟に処理を行わせる事ができます。
const functionX = (someFunction) => { // 以下の行は非同期的に実行されるが、処理が終わったら必ず直後にコールバックが呼ばれる setTimeout(() => { // nanika syori someFunction('X') }, 1000 * Math.random()) } const functionA = (value) => alert(value + ' no atoni A dayo!') const functionB = (value) => alert(value + ' no atoni B desu!') functionX(functionA) functionX(functionB) // これらの順序は必ずしもAが先ではないが、Xの処理の後にコールバックが確実に呼ばれることは保証されるこの書き方=関数を引数として渡すという考え方は、特に非同期処理で強みを持ちますが、非同期処理以外でも有効です。
オブジェクトを引数に渡すことと、ポリモーフィズムと
さて、さきほどのコード断片において、関数の代わりに、メソッド(=関数のプロパティのこと)を持つオブジェクトを渡して同じことをやってみます。
const functionX = (someObject) => { // 以下の行は非同期的に実行されるが、処理が終わったら必ず直後にコールバックが呼ばれる setTimeout(() => { // nanika syori someObject.someFunction('X') }, 1000 * Math.random()) } const objA = { someFunction(value) {alert(value + ' no atoni A dayo!')} } const objB = { someFunction(value) {alert(value + ' no atoni B desu!')} } functionX(objA) functionX(objB) // これらの順序は必ずしもAが先ではないが、Xの処理の後にコールバックが確実に呼ばれることは保証されるなんと、普通にオブジェクトを引数として渡していますが、やっていることはコールバックと全く同じですね!
というのも、オブジェクトというのは、ざっくり「データと関数を組にしたもの」のことでした。そこで、引数にオブジェクトを渡すという事は、関数に余分なものを付け加えて渡しているというような見方もできるのですね。そのような見方をすると、実はコールバック、もっというと高階関数というのは、意外と簡単なことをやっているようにも見えるのでした。ところで、functionX()の中ではsomeFunctionというメソッド(関数)を呼び出していますが、これはオブジェクトによって一般に内容が異なる関数で、このようにオブジェクトに応じて同じ名前の異なる関数を呼び出させる仕組みを(特に型付き言語の枠組みにおいて)ポリモーフィズムと呼ぶのでした。
そのような観点では、コールバックはポリモーフィズムの特殊な場合(データを持たず、呼び出しが可能なオブジェクトを渡した場合)と思うこともできるのでした。オブジェクトを渡して、より複雑な処理を表現すること
コールバックは、基本的には処理が終わった後に呼ばれるものでした。
一般にある関数Xが一つの関数Yを引数に取るとき、Xの中でYを呼ぶ場所は最初でも最後でも良く、複数回呼ぶこともできますが、Yという一つの関数しか使えません(一つの関数、と言ってしまったので、当たり前なのですが...)
一方で、オブジェクトZを渡すとき、Zが複数のメソッドを持つことを期待する場合があります。
例えば、次のような処理を考えることができます。const functionX = (someObject) => { // begin ... const beginning = someObject.begin() // X begin ... alert('X dayo!') const middle = beginning + ' X ' // ... X end return someObject.end(middle) // ... end } const objZ = { begin() {return 'begin'}, end(value) {return value + 'end'} } console.log(functionX(objZ)) // begin X end引数のsomeObject(=objZ)のbegin(), end()で挟み込むというような事ができています。関数一つよりも器用な処理ですね。模式的に図を描くと、以下のようになります。
(図中のXの処理というのは、主な処理のことで、左側は全体がfunctionXの中です)ところで、これはbegin()とend()を2つの引数として渡しているのと本質的に同じではないかという見方をすることもできます。これは、ある意味その通りだと思います。
しかし、もう少しパターンが増えた一般の場合を想定してみましょう。例えばYとZに増やした時、
beginY()
endY()
beginZ()
endZ()
という4つの関数がバラバラに定義されていて、都度beginYとendY、beginZとendZが対になるようにプログラマが注意するよりは、objYの中にY用のbeginとend、objZの中にZ用のbeginとendを定義して、引数で渡したオブジェクトのメソッドを使うようにした方が、自然に記述することができるということです。でもそんなに複雑な処理ばかりが必要なわけではない
しかし、これまでの話をひっくり返すようですが、単に処理Xを行った後にAやBの部分だけを調整したい、それだけで十分、という事がよくあります。そのような時は、データの無いオブジェクトであるところの関数を渡すだけで十分なのでした。
ポリモーフィズムとは少し違った形の"行き来する処理"コルーチン
じつは、ポリモーフィズムで実現した構造を、上記のbegin()やend()のような複数のメソッドに頼らずに、一つの関数でやってしまう方法があります。
さきほど上の方で、Yという一つの関数しか使えませんと書いた部分について、雑にいうと「関数を途中まで処理させて、yieldという記述があった時にreturnに類似のことを行うが、処理を終了してしまうのではなく、途中のままで保持しておく」というようなやり方があります。
これがコルーチンです。
コルーチンという名前は、サブルーチンとの比較からつけられたもので、「ルーチンXがルーチンYを呼び出すという、言わばYがXのサブというようなあり方ではなくて、XとYが共に同じようなポジションで互いに呼び出し合う」というニュアンスで、コワーカー=coworkerなどの接頭辞であるcoをルーチンにつけたものです。
上でやったことと同じようなことをやってみます。const functionX = (someObject) => { // begin ... const beginning = someObject.next().value // X begin ... alert('X dayo!') const middle = beginning + ' X ' // ... X end return middle + someObject.next().value // ... end } const objY = function* () { yield 'begin' yield 'end' } console.log(functionX(objY())) // begin X endyieldまで実行したら、次にnext()が呼ばれた時は次のyieldまで実行する、というような流れになっています。模式的な図は以下のとおりです。
(図では便宜上①でも中断位置から再開としていますが、①の時点では最初からになります)
まとめ
コールバックと、ポリモーフィズムと、それからコルーチン、
みんなちがって、みんないい。免責事項
- JavaScriptのfunctionは実際にはデータに相当するものも持っています。
- 単にオブジェクトのメソッド呼び出しをすればポリモーフィズムと言えるのかというと、非常に微妙なところで、またダックタイピングと言うべきではないかという考え方もあると思いますが、便宜上ポリモーフィズムで統一しています。
- 深夜テンションで書いているので、その他変なところがあればそっと直してください。
- 投稿日:2020-03-24T01:44:29+09:00
【React】チュートリアルの三目並べをやる #2
前回
タイムトラベル機能の追加
前回は通常の三目並べ完成までやりました。
今回はその三目並べに「タイムトラベル機能」なるものを実装していきたいと思います。履歴ですね。着手の履歴の保存
suquaresの配列を
setState
で毎回新規オブジェクトで更新していたことがここで活きるらしいです。
このオブジェクトを更新のたびに保持していきます。その履歴を保持する場所は一番TOPの
Game
にするそうです。
これによりBoardはstate
を保持する必要がなくなります。Gameclass Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ suares: Array(9).fill(null) }], xIsNext: true, }; }また、
status
の更新やonClick
の処理もすべてGameに持っていくことができます。以下がすべてを移動させたバージョン
BoardとGameclass Board extends React.Component { // constructor削除 renderSquare(i) { return ( <Square value={this.props.squares[i]} //state → props onClick={() => { this.props.onClick(i); // 処理はすべてGameに移動して、GameのonClickを呼び出す }} /> ); } render() { return ( <div> {/* satateは削除。Game側で表示する */} <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> ); } } class Game extends React.Component { constructor(props) { super(props); // Gameで履歴を保持する this.state = { history: [{ squares: Array(9).fill(null) }], xIsNext: true, }; } render() { // statusの表示を移動してきた const history = this.state.history; const current = history[history.length - 1]; // 最新の履歴を取得 const winner = this.calculateWinner(current.squares); let status; if (winner) { status = '勝者:' + winner; } else { status = '次の手番: ' + (this.state.xIsNext ? 'X' : 'O'); } return ( <div className="game"> <div className="gmae-board"> <Board squares={current.squares} // クロージャを使ってcurrentからsquaresを取得して、Boardに渡す // BoardからonClick処理を移動 onClick={i => { const squares = Object.create(current.squares); // 最新のsquare if (this.calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; // 更新用のstateを作る const newState = { // historyに新しい履歴を追加する history: history.concat({ squares: squares }), xIsNext: !this.state.xIsNext, }; this.setState(newState); }} /> </div> <div className="game-info"> <div>{status}</div> <div>{/* TODO */}</div> </div> </div> ); } // 勝敗判定関数(Boardから移動してきた) calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; } }
history.concat
のところはArray.prototype.push
ではなくArray.prototype.concat
を使用します。
pushだと既存のstateを更新してしまうことになります。
concatであれば、新たに配列を作り出すため、安全です。
Array.prototype.concat()過去の着手の表示
※以降はチュートリアルはあまり見ずに自分でやり遂げてみたかったので、ちょっとチュートリアルとは異なります。
履歴はすべて保持しておいて、「○回目の履歴表示」ボタンが押されたら、その履歴の状態を表示させるようです。
これを実現させるためにstate
に「表示したい履歴のインデックス」を表すstepNumber
を設けます。Gameclass Game extends React.Component { constructor(props) { super(props); // Gameで履歴を保持する this.state = { history: [{ squares: Array(9).fill(null) }], stepNumber: 0, // 表示したい履歴のインデックスを表す xIsNext: true, }; }
render
ではstepNumber
を用いて表示したい履歴を取得します。(current
のところ)Gamerender() { // statusの表示を移動してきた const history = this.state.history; const current = history[this.state.stepNumber]; // カレントはstepNumberのインデックスで求める const winner = this.calculateWinner(current.squares);そして、履歴表示ボタンのHTMLを作ります。
履歴の配列をmap
でループして、新しい配列を作ります。
この配列は「履歴表示ボタンのHTML」の配列になります。
Array.prototype.map()Game// 履歴表示ボタン配列を作成 const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( // keyが無いと警告がでる <li key={move}> <button // 履歴ボタン押下イベント onClick={() => { // 対象の履歴インデックスの状態に変更する // xIsNextは2で割ったあまりで求められる this.setState({ stepNumber: move, xIsNext: move % 2 === 0, }); }} > {desc} {/* ボタン表示名 */} </button> </li> ); });【※2020/03/24追記】
li
にkey
属性がないため、エラーになっていました。
一意な値を振ることが推奨されるようです。
key を選ぶ
step
はhistory内の1つ1つの要素を表します。
move
は、今のstep
要素が配列の中でどの位置にいる要素であるか(インデックス)を表す。
onClick
イベントで、表示させたい履歴のインデックスと、xIsNext
をsetState
します。ここでは
histroty
は更新しません。履歴を保持している配列の中から、指定したインデックスの履歴を表示させるだけなので、更新する必要はありません。returnのところ
最後に、BoardのHTMLを返すreturn();
の所です。
Boardに渡すonClick
は、Squareのクリックイベント(つまり、マス目を押された時)の処理です。ここでは、マス目を押された時点からまた新しく履歴を保持するようにします。
なので、「最初の履歴 ~ 今表示している履歴」までを、履歴の配列全体から抜き出し、その抜き出した物の最後尾に今の状態(履歴)を追加します。Gamereturn ( <div className="game"> <div className="gmae-board"> <Board squares={current.squares} // クロージャを使ってcurrentからsquaresを取得して、Boardに渡す // BoardからonClick処理を移動(※これはあくまでマス目押下イベント) onClick={i => { const squares = Object.create(current.squares); // カレントのsquare if (this.calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; // 履歴の最初~直前に押された履歴までを抜き出す const newHistory = history.slice(0, this.state.stepNumber + 1); // 更新用のstateを作る const newState = { // 抜き出した履歴の続きからまた新たに履歴を保持していく history: newHistory.concat({ squares: squares }), stepNumber: newHistory.length, // 最新のインデックス xIsNext: !this.state.xIsNext, }; this.setState(newState); }} /> </div> <div className="game-info"> <div>{status}</div> <div>{moves}</div> </div> </div> );
moves
はTODO
となっていたところに埋め込みます。これで完成です。
全文
index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import * as serviceWorker from './serviceWorker'; function Square(props) { return ( <button className="square" onClick={props.onClick}> {props.value} </button> ); } class Board extends React.Component { // constructor削除 renderSquare(i) { return ( <Square value={this.props.squares[i]} //state → props onClick={() => { this.props.onClick(i); // 処理はすべてGameに移動して、GameのonClickを呼び出す }} /> ); } render() { return ( <div> {/* satateは削除。Game側で表示する */} <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> ); } } class Game extends React.Component { constructor(props) { super(props); // Gameで履歴を保持する this.state = { history: [{ squares: Array(9).fill(null) }], stepNumber: 0, // 表示したい履歴のインデックスを表す xIsNext: true, }; } render() { // statusの表示を移動してきた const history = this.state.history; const current = history[this.state.stepNumber]; // カレントはstepNumberのインデックスで求める const winner = this.calculateWinner(current.squares); // 履歴表示ボタン配列を作成 const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li> <button // 履歴ボタン押下イベント onClick={() => { // 対象の履歴インデックスの状態に変更する // xIsNextは2で割ったあまりで求められる this.setState({ stepNumber: move, xIsNext: move % 2 === 0, }); }} > {desc} {/* ボタン表示名 */} </button> </li> ); }); let status; if (winner) { status = '勝者:' + winner; } else { status = '次の手番: ' + (this.state.xIsNext ? 'X' : 'O'); } return ( <div className="game"> <div className="gmae-board"> <Board squares={current.squares} // クロージャを使ってcurrentからsquaresを取得して、Boardに渡す // BoardからonClick処理を移動(※これはあくまでマス目押下イベント) onClick={i => { const squares = Object.create(current.squares); // カレントのsquare if (this.calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; // 最初の履歴~直前に押された履歴までを抜き出す const newHistory = history.slice(0, this.state.stepNumber + 1); // 更新用のstateを作る const newState = { // 抜き出した履歴の続きからまた新たに履歴を保持していく history: newHistory.concat({ squares: squares }), stepNumber: newHistory.length, // 最新のインデックス xIsNext: !this.state.xIsNext, }; this.setState(newState); }} /> </div> <div className="game-info"> <div>{status}</div> <div>{moves}</div> </div> </div> ); } // 勝敗判定関数(Boardから移動してきた) calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; } } ReactDOM.render( <React.StrictMode> <Game /> </React.StrictMode>, 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();結果
感想
ちょっと最後らへんは混乱しました。
数日たってソースみるとおそらく解読できないと思います。jQueryのようにセレクタをたくさん書かずとも実現できたのはメリットだと思いました。
おそらくjQueryならもっとソースコードが煩雑になるはず。このチュートリアルはQiitaで別の方々がたくさん試されているので、今更でしたね・・・笑
- 投稿日:2020-03-24T01:42:57+09:00
JavaScriptのモジュールは変数をエクスポートする
今時のJavaScript開発において、JavaScriptが持つモジュールの機能は欠かすことができません。我々はプログラムをいくつものファイル(モジュール)に分割し、
import
文とexport
文を使ってそれらを繋げています。各モジュールはexport
文を用いてそのモジュール内で定義した変数・関数などをエクスポートすることができ、別のモジュールがimport
文でそれらの値を取得することができるのです。皆さんは、この
import
・export
文がどのように働いているのか正確に説明できるでしょうか。実は、import
文やexport
文というのは値をインポート・エクスポートしているのではなく、言わば変数そのものをインポート・エクスポートしているのです。これを理解するのがこの記事のゴールです。※ 本当は変数ではなく「バインディング」といったほうが用語としてより正確なのですが、この記事では分かりやすさのために変数という用語を使用しています。
export
文は変数をエクスポートするモジュールから何かをエクスポートするときに使うのが
export
文ですが、これにはいくつかの種類があります。実は、export
文は必ず変数をエクスポートしています。最も代表的な
export const foo = ...;
という構文は、const
宣言により変数foo
を作ると同時にそれをエクスポートしています。export let bar = ...
のようにconst
以外を使うことも可能です。それどころか、export const {foo, bar} = obj;
のように分割代入で作られる変数をエクスポートすることもできます(この場合はfoo
とbar
がエクスポートされます)。ほかに
export function 関数名() { }
という関数宣言の形もよく使われます。この場合も、関数宣言を通してその関数が入った変数が作られています。変数に外向きの名前をつけてエクスポートする
また、
export { foo, bar }
という構文は、事前に定義された変数foo
、bar
をエクスポートするという意味ですから、やはり変数をエクスポートしています。この構文の特徴はexport { foo as foo2 };
のように違う名前でエクスポートする機能を持っている点です。これにより「モジュールの中ではfoo
と呼ばれている変数を、外向きにはfoo2
という名前でエクスポートする」ということが可能です。このように、モジュールからエクスポートされている変数は「内向きの名前」と「外向きの名前」を持ちます。export const foo = ...
のような宣言の場合は内向きの名前と外向きの名前が同じで、どちらもfoo
です。defaultエクスポートの扱い
ところで、
export default 値;
という構文では変数をエクスポートしていないように見えますね。しかし、実はこれは内向きには*default*
という名前の変数を作成し、外向きにdefault
という名前でエクスポートしています。ここで作られた*default*
という名の暗黙の変数は、そんな変な名前の変数にアクセスする構文的な手段が存在しないため、モジュール内からアクセスすることはできません。defaultエクスポートは
default
という名前でエクスポートする機能です。逆に言えば、export default
以外の方法でもdefault
という名前でエクスポートすればdefaultエクスポートと同様の挙動をするということです。次のa.mjs
とindex.mjs
を用意してindex.mjs
を実行すれば、コンソールにsomething is 123
と表示されるでしょう。////////// a.mjs const bar = 123; export { bar as default }; ////////// index.mjs import something from "./a.mjs"; console.log("something is", something);このように、
export default
といったdefaultエクスポートの構文は実は「default
という名前をつけてエクスポートする」という行為を短く書くだけの構文なのです。再エクスポートの構文
export
文は再エクスポートの機能も持っています。まず、export * from "module"
はモジュールからエクスポートされた変数を同じ名前でエクスポートします。面白いのは、この構文は自身のスコープ内にその変数を作らないということです。下の例ではb.mjs
はfoo
という名前で変数をエクスポートしています。a.mjs
内のexport * from "./b.mjs";
はb.mjs
からエクスポートされているfoo
を同じfoo
という名前で再エクスポートするという働きをします。言い換えれば、a.mjs
は「b.mjs
のfoo
」をfoo
という外向きの名前でエクスポートしているのです。再エクスポートが普通のエクスポートと決定的に違う点は、「自身のスコープの変数」をエクスポートするのではなく「他のモジュールの変数」をエクスポートしているという点です。これはつまり、再エクスポートは自身のスコープに何の影響も与えないということです。
a.mjs
の中でfoo
という変数が宣言されていたとしても、再エクスポートされているfoo
とはまったく無関係です。////////// b.mjs export const foo = 123; ////////// a.mjs // b.mjsのfooを再エクスポート export * from "./b.mjs"; // この変数fooはb.mjsがエクスポートするfooとは無関係 const foo = 0; console.log(foo); ////////// index.mjs import { foo } from "./a.mjs"; // このfooはb.mjsのfooなので123が表示される console.log(foo);この例を見ると、
a.mjs
内のconsole.log(foo)
は0を表示します。これは、a.mjs
のスコープにあるfoo
はそのモジュール内で宣言されている変数foo
だからです。一方、index.mjs
内のconsole.log(foo)
は123を表示します。これは、このfoo
がa.mjs
からインポートしたfoo
であり、a.mjs
がfoo
という名前でエクスポートしているのは「b.mjs
がエクスポートするfoo
」だからです。ちなみに、
a.mjs
の中でexport const foo = 0
のように書いた場合はexport *
の方よりも優先されてこちらがfoo
としてエクスポートされます。他に
export * as ns from "module"
構文やexport { foo, bar } from "module"
構文が再エクスポートを行いますが、これらも現在のモジュールのスコープ内には影響を与えません。また、
default
という名前でエクスポートされている変数はexport * from "module"
構文で再エクスポートされません。defaultエクスポートを再エクスポートしたければexport { default } from "module"
という方法が有効です。
import
文は変数をインポートする
export
文が変数をエクスポートするなら、import
文がインポートするのも当然変数です。そのことがたいへんよく分かる例がこれです。////////// a.mjs export let foo = 0; export const setFoo = (value) => { foo = value; } ////////// index.mjs import { foo, setFoo } from "./a.mjs"; // a.mjs内のfooは0なので0が表示される console.log(foo); // a.mjs内のfooが100になる setFoo(100); // a.mjs内のfooは100なので100が表示される console.log(foo);この例では
a.mjs
が変数foo
をエクスポートし、index.mjs
がインポートしています。すると、index.mjs
のスコープに存在するfoo
はa.mjs
に存在するfoo
と同じになります。より正確には、index.mjs
に存在する変数foo
は、「参照されるとa.mjs
の変数foo
の中身を返す変数」となります。これが意味することは、index.mjs
の変数foo
の値は常にa.mjs
の変数foo
の値と同じであるということです。このことは、
a.mjs
が提供するsetFoo
関数を用いてa.mjs
内の変数foo
を書き換えると分かります。setFoo
の呼び出し後は、index.mjs
の変数foo
の中身が勝手に変わっています。これはもちろん、a.mjs
の変数foo
の中身が変わったからです。ここで重要なのは、インポートは値のコピーではないということです。あくまで変数そのものをインポートしているのであり、だからこそ、インポート後に元の変数の値が変わっても追随できるのです。言い方を変えれば、インポートはモジュール間で変数のエイリアスを作る機能であるとも言えます。上の例では、
index.mjs
内の変数foo
はa.mjs
内の変数foo
のエイリアスであるとの見方もできますね。JavaScriptのモジュール間連携とは、モジュールの間に張られたエイリアスによって成り立つものなのです。ただし、変数に再代入できるのはその変数を所有するオリジナルのモジュールだけです。
index.mjs
でfoo = 123;
のようにしてa.mjs
内の変数foo
を書き換えることはできません(ランタイムエラーになります)。インポートされた変数は読み取り専用のエイリアスなのです1。一応、インポートと対比して、明示的に値をコピーする例も用意しておきます(
a.mjs
の中身は同じなので省略)。////////// index.mjs import { foo, setFoo } from "./a.mjs"; // これを実行した時点でのfooの値をmyFooに代入 const myFoo = foo; // 0が表示される console.log(myFoo); // a.mjs内のfooが100になる setFoo(100); // myFooは0のまま console.log(myFoo);こうした場合、当然ながら変数
myFoo
には「代入を実行した時点でのfoo
の値」が入ります。変数myFoo
は変数foo
とは無関係ですから、foo
がどう変化してもmyFoo
の値は変化しません。この例と対比することでも、「
import { foo } from "./a.mjs";
」が「const foo = (a.mjsの変数fooの値);
」のような意味ではないことがお分かりになるでしょう。モジュール名前空間オブジェクト
import
文にはimport * as mod from "module";
のような構文もあります。これは、モジュールからエクスポートされている変数を全部まとめてオブジェクトにしてインポートするという意味です。この構文によって得られるオブジェクトがモジュール名前空間オブジェクトです。長いので以降は名前空間オブジェクトと呼びます。名前空間オブジェクトが持つ各プロパティは、インポート元の変数の値を常に反映します。別の言い方をすれば、名前空間オブジェクトのプロパティがインポート元の変数のエイリアスになっていると言えます。先ほどの
setFoo
の例を少し書き換えることでこれを確かめましょう。////////// a.mjs export let foo = 0; export const setFoo = (value) => { foo = value; } ////////// index.mjs import * as a from "./a.mjs"; // 0 が表示される console.log(a.foo); a.setFoo(100); // 100 が表示される console.log(a.foo);
index.mjs
内の変数a
にa.mjs
の名前空間オブジェクトが入っています。結果から分かるように、a.foo
の値はa.mjs
内の変数foo
の値を常に反映しています。名前空間オブジェクトはこの点で特別なオブジェクトです。例えば、次のようにしても再現できません。
import { foo, setFoo } from "./a.mjs"; // これは名前空間オブジェクトの挙動にならない const a = { foo, setFoo };なぜなら、このように作ったオブジェクトは
a.foo
が「a
を作った瞬間の変数foo
の値」になり、変数foo
の変化に追随しないからです。先ほどの説明の通りこのようにインポートした変数foo
はインポート元のfoo
のエイリアスですが、それは変数自体の性質であり、「変数foo
を評価して得た値」は何の変哲のないただの値でしかありません。そのただの値をa
のプロパティに入れても、名前空間オブジェクトのような挙動にはならないのです。ちなみに、defaultエクスポートは「
default
という名前でエクスポートされている変数」だったので、名前空間オブジェクトのdefault
プロパティとして取得できます。また、dynamic import(
import("./a.mjs")
)の結果として得られるのもやはり名前空間オブジェクトです。よって、index.mjs
を次のように書き換えても同じ結果となります。import("./a.mjs").then(a => { // 0 が表示される console.log(a.foo); a.setFoo(100); // 100 が表示される console.log(a.foo); })なぜ値ではなく変数をエクスポートするのか
ここまで、JavaScriptのモジュールは変数をエクスポートしているのだということを解説しました。しかし、なぜそのような挙動になっているのでしょうか。値をエクスポートした方が単純で分かりやすいような気がします。
その答えは、ECMAScriptの仕様書がホストされているGitHubリポジトリにひっそりと置かれているFAQ.mdというファイルにわざわざ書かれています。ちなみに、これは自慢ですが、筆者はECMAScript仕様書にプルリクエストを送ってマージされたことがあります。
このFAQ.mdには、この挙動の理由について次のように書かれています。
The biggest reason for this is that it allows cyclic module dependencies to work.
つまり、循環参照があるようなプログラムでも動くようにするためというのが最大の理由です。
循環参照の問題点
まず、そもそも循環参照の何が問題なのかを考えてみましょう。これまで見てきたプログラム例は循環参照がありませんでしたが、その場合モジュールは依存されている側から順番に実行されていました。つまり、
index.mjs
→a.mjs
→b.mjs
という依存関係がある場合、まずb.mjs
が実行され、次にa.mjs
が実行され、index.mjs
が実行されました。この実行順序は、import
文でインポートした変数には最初から値が入っているということを保証するためのものです。先ほどの例を再掲します。////////// a.mjs export let foo = 0; export const setFoo = (value) => { foo = value; } ////////// index.mjs import { foo, setFoo } from "./a.mjs"; // a.mjs内のfooは0なので0が表示される console.log(foo); // a.mjs内のfooが100になる setFoo(100); // a.mjs内のfooは100なので100が表示される console.log(foo);この例では、
a.mjs
が先に実行されて、そのあとindex.mjs
が実行されます。これにより、index.mjs
の最初のconsole.log(foo);
を実行した時点でfoo
にはすでに0
という値が入っています。ここでfoo
に0
が入っている理由は、index.mjs
よりも先にa.mjs
が実行され、a.mjs
の中のexport let foo = 0;
が実行されることでa.mjs
の変数foo
に0
が代入されたからなのです。もし
a.mjs
よりも先にindex.mjs
が実行されていたら、console.log(foo)
の時点でfoo
にはまだ何も入っていないことになってしまいます。このように、インポートした変数がすぐ使えることを保証するするために、依存されている側から先に実行するという実行順序になっています。
しかし、循環参照がある場合はこの保証ができなくなります。循環参照とは、例えば
a.mjs
がb.mjs
をインポートし、b.mjs
もa.mjs
をインポートしているというような状態を指します。この場合、どちらを先に実行しても問題が発生してしまいますね。a.mjs
を先に実行すれば、a.mjs
はb.mjs
をインポートしているのにb.mjs
よりa.mjs
が先に実行されてしまいます。逆でも同じ問題が起きます。この問題は、実は根本的にはどうしようもありません。JavaScriptでモジュール間の循環参照があった場合、前述の保証は諦めて一定の順番でモジュールを実行します。試しに、循環参照が原因のエラーを発生させてみましょう。
////////// b.mjs import { varFromA } from "./a.mjs"; export const varFromB = "b"; console.log("varFromA is", varFromA); ////////// a.mjs import { varFromB } from "./b.mjs"; export const varFromA = "a"; console.log("varFromB is", varFromB); ////////// index.mjs import "./a.mjs";この例では、
a.mjs
とb.mjs
が循環参照しています。それぞれが変数をエクスポートし、互いにインポートした変数を利用しています。これを実行すると、次のようなエラーが発生します(Node.js v13.8.0で確認)。console.log("varFromA is", varFromA); ^ ReferenceError: Cannot access 'varFromA' before initializationつまり、
b.mjs
の実行時に、まだ初期化されていない変数varFromA
を読もうとしたことによるエラーです。これは、
a.mjs
よりも先にb.mjs
が実行されたことが原因です。b.mjs
はa.mjs
からインポートした変数varFromA
を評価しましたが、まだa.mjs
内でexport const varFromA = "a";
が実行されていないため、変数varFromA
はまだ初期化されていない変数となっているのです。まだ初期化されていない変数はアクセスすることができません。ちなみに、「初期化されていない変数」は循環参照に特有の現象ではありません。1ファイル内でも、変数宣言よりも前に変数にアクセスすると同じエラーになります。このことからも先の
varFromA
がa.mjs
内のvarFromA
に対するエイリアスであることが分かります。// ReferenceError: Cannot access 'foo' before initialization console.log(foo); const foo = 123;なお、変数がまだ初期化されていない区間はTemporal Dead Zone (TDZ) と呼ばれています。
let
やconst
で宣言された変数はその宣言が評価された際に初期化されるためTDZが存在しますが、var
で宣言された変数は最初からundefined
に初期化されているため、TDZが存在しません。循環参照でもエラーが発生しない場合
本題に戻ると、ここで重要なのはTDZにある変数をインポートするだけではエラーにならないということです。
import
文により変数のエイリアスができても、それはまだTDZにある変数へのエイリアスを作ったというだけです。そのエイリアスを通じて実際にアクセスしなければエラーは起きないのです。つまり、TDZにある変数にアクセスしなければ、モジュールが循環参照していてもエラーは起きないということになります。そして、実際のところ、モジュールが循環参照しているがTDZにある変数にアクセスしないという例は結構あります。最も典型的なのは、モジュールが関数だけエクスポートしている場合です。先ほどのFAQ.mdに載っている例を引用します。
////////// Even.js import {isOdd} from "./Odd.js"; export function isEven(num) { if (num === 0) { return true; } else { return isOdd(num - 1); } } ////////// Odd.js import {isEven} from "./Even.js"; export function isOdd(num) { if (num === 0) { return false; } else { return isEven(num - 1); } } ////////// main.js import {isOdd} from "./Odd"; isOdd(2);この例では、
Even.js
とOdd.js
が循環参照しています。それぞれ、isEven
とisOdd
という関数をエクスポートしています。また、お互いにお互いがエクスポートしている関数をインポートし、相互再帰の形で利用しています。実は、これは循環参照があるにも関わらずエラーが発生しません。その理由は、
Even.js
やOdd.js
が実行された瞬間はそれぞれ関数をひとつ定義されているだけであり、インポートした変数をすぐに参照するわけではないからです。実際にこれらの関数が実行されるのはmain.js
の中でisOdd(2)
が実行されたタイミングです。main.js
は、Even.js
とOdd.js
の実行が終わってから実行されます。つまり、isOdd(2)
が実行されるタイミングではすでにisOdd
もisEven
も定義済みである(TDZを抜けている)ということです。これにより、isOdd
はその中でisEven
を呼び出すことができ、またisEven
もisOdd
を呼び出すことができます。この用意して、循環参照がある状態で関数を定義することができました。これは、変数がエイリアスとしてインポートされているからこそ実現できることです。この例では
Even.js
→Odd.js
→main.js
の順にモジュールが実行されるので、Even.js
が実行された(isEven
が初期化された)段階ではまだisOdd
は(これはOdd.js
がエクスポートしているisOdd
のエイリアスなので)TDZにあります。しかし、isOdd
はisEven
の中身から参照されており、この段階でisOdd
を評価することはないのでTDZによるエラーは起こりません。次にOdd.js
が実行されたタイミングでisOdd
が初期化されると、当然ながらEven.js
から見えるisOdd
も初期化済みになります。このように変数がエイリアスされていることで、インポートした変数が後から初期化されるのでOKというパターンが生まれるのです。バンドラによるインポート・エクスポートの扱い
現在のフロントエンド開発では、モジュールを駆使して書かれたプログラムはWebpackに代表されるバンドラによって処理し、インポート・エクスポートの無い単一のプログラムに変換してから実行されます2。つまり、ここまで説明してきたインポート・エクスポートの挙動を実際に処理しているのはバンドラだということです。
ということで、先ほどから出てきているこの例をWebpackでバンドルしたものを見てみましょう。
////////// a.mjs export let foo = 0; export const setFoo = (value) => { foo = value; } ////////// index.mjs import { foo, setFoo } from "./a.mjs"; // a.mjs内のfooは0なので0が表示される console.log(foo); // a.mjs内のfooが100になる setFoo(100); // a.mjs内のfooは100なので100が表示される console.log(foo);これを
webpack --mode none
でバンドルしたものからindex.mjs
に相当する部分を抜き出すとこのようになります。index.mjs__webpack_require__.r(__webpack_exports__); /* harmony import */ var _a_mjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); // a.mjs内のfooは0なので0が表示される console.log(_a_mjs__WEBPACK_IMPORTED_MODULE_0__["foo"]); // a.mjs内のfooが100になる Object(_a_mjs__WEBPACK_IMPORTED_MODULE_0__["setFoo"])(100); // a.mjs内のfooは100なので100が表示される console.log(_a_mjs__WEBPACK_IMPORTED_MODULE_0__["foo"]);最も注目すべき点は、元々のソースコードで変数
foo
を参照していたところが_a_mjs__WEBPACK_IMPORTED_MODULE_0__["foo"]
というプロパティアクセスに置き換わっている点です。_a_mjs__WEBPACK_IMPORTED_MODULE_0__
はa.mjs
の名前空間オブジェクト(をWebpackがエミュレートしているもの)ですね。さすがに「よそのモジュール(=スコープ外の存在)により変数の中身が勝手に書き換わる」はそのまま実現することができませんが、「よそのモジュールによりオブジェクトのプロパティが勝手に書き換わる」は(よそのモジュールがオブジェクトを参照できれば)実現できそうなので、このような方式が取られています。次に
a.mjs
に相当する部分はこんな感じです。__webpack_exports__
というのが多分a.mjs
の名前空間オブジェクトであり、そのオブジェクトに対してfoo
やsetFoo
というアクセサプロパティを定義しています。これらのプロパティはゲッタを持ち、アクセスされると実際の変数foo
やsetFoo
の値を返します。a.mjs__webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "foo", function() { return foo; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "setFoo", function() { return setFoo; }); let foo = 0; const setFoo = (value) => { foo = value; }このことから、Webpackの方針は
import
を頑張って名前空間オブジェクト経由で変換し、名前空間オブジェクトはゲッタを使って再現するというものであることが分かりますね。モジュールのアンチパターン
この記事の説明を理解すると、モジュールを書く際に避けるべきパターンが見えてきます。基本的には、「エイリアスを作れば済むのにわざわざ値を取得してエクスポートしているもの」がアンチパターンとなります。
1. 変数を経由して再エクスポート
変数を再エクスポートしたい場合は、再エクスポート用の構文を使って再エクスポートすることで、変数のエイリアス性を保って再エクスポートできます。途中でローカルスコープの変数を経由させると、エイリアス性が途切れてしまいます。
悪い例////////// b.mjs // 変数fooは1秒後に9999になる export let foo = 1; setTimeout(()=> { foo = 9999; }, 1000); ////////// a.mjs import * as b from "./b.mjs"; // fooはa.mjsが実行された瞬間のb.fooの値(1)になる export const foo = b.foo; ////////// index.mjs import { foo } from "./a.mjs"; setTimeout(()=> { // 2秒後にfooを表示すると1が表示される console.log(foo); }, 2000);この例では、
b.mjs
がエクスポートしている変数foo
の値が最初は1
で、1秒後に9999
に変化します。a.mjs
はb.mjs
のfoo
を再エクスポートしているつもりですが、できていません。a.mjs
のローカル変数として別にfoo
を定義しており、それをb.foo
の値で初期化されているからです。このfoo
はb.mjs
のfoo
のエイリアスではなく、初期化時にb.foo
の値を使っただけでまったく無関係のfoo
だからです。よって、この例で
index.mjs
を実行すると、2秒後に1
と表示されます。index.mjs
がインポートしているfoo
はa.mjs
のfoo
であり、b.mjs
のfoo
が変化しても影響されないからです。
b.mjs
のfoo
の変化がindex.mjs
に伝わってほしければ、正しく再エクスポートしなければいけません。例えば次のようにすれば再エクスポートできます。こう変更すると、index.mjs
を実行した2秒後に9999
が表示されます。////////// a.mjs import * as b from "./b.mjs"; export { foo } from "./b.mjs";また、次のように「インポートされた変数」を直に
export { }
構文に渡した場合は再エクスポートとして扱ってもらえるので、これでもOKです。こちらの方式だと、a.mjs
内でもb.mjs
からインポートしたfoo
を参照することができるという利点があります。とにかく、ローカル変数を経由してしまうとだめなのです。////////// a.mjs import { foo } from "./b.mjs"; export { foo };2. オブジェクトに入れて再エクスポート
別のアンチパターンとして、次のように親切にも「インポートされたものをまとめたオブジェクト」を作ってエクスポートしている場合があります。
////////// utils.mjs import { someNiceFunc } from "./foo.mjs"; import { otherNiceFunc } from "./bar.mjs"; import { veryUsefulFunc } from "./baz.mjs"; // 変数からエクスポートするパターン export const utils = { someNiceFunc, otherNiceFunc, veryUsefulFunc, }; // default exportのパターン export default { someNiceFunc, otherNiceFunc, veryUsefulFunc, }これもやはり、変数のエイリアス性が途切れるのでアンチパターンです。変数に入れようがdefaultエクスポートだろうがだめです。例えば、
utils.someNiceFunc
の値はこのモジュールが実行された瞬間のsomeNiceFunc
の値であり、その後someNiceFunc
の値が変化しても追随できません。これは、utils.mjs
が実行された瞬間に{ someNiceFunc, otherNiceFunc, veryUsefulFunc }
というオブジェクトリテラルが評価され、その過程で変数someNiceFunc
の値が参照されているからです。実際のところ「モジュールからエクスポートされている便利関数が後から変わる」というのは非現実的なシチュエーションですが、これと循環参照を組み合わせると割と現実的な問題となります。
ちょっと長いですがこんな感じの例で考えてみます。
////////// utils.mjs import { someNiceFunc } from "./foo.mjs"; import { otherNiceFunc } from "./bar.mjs"; import { veryUsefulFunc } from "./baz.mjs"; export default { someNiceFunc, otherNiceFunc, veryUsefulFunc, } ////////// foo.mjs export const someNiceFunc = (arg)=> { return arg * 2; } ////////// bar.mjs import utils from "./utils.mjs"; export const otherNiceFunc = (arg)=> { return utils.someNiceFunc(arg) + 1; } ////////// baz.mjs import utils from "./utils.mjs"; export const veryUsefulFunc = (arg)=> { console.log(utils.otherNiceFunc(arg)); } ////////// index.mjs import { veryUsefulFunc } from "./baz.mjs"; veryUsefulFunc(100);この例では
foo.mjs
,bar.mjs
,baz.mjs
がそれぞれとてもいい感じの関数をエクスポートしており、それらをutils.mjs
がオブジェクトにまとめています。これらの関数を使いたい場合はutils.mjs
を経由して使用する想定です。bar.mjs
やbaz.mjs
もutils.mjs
経由で他の関数を使用しています。ところが、そこに行儀の悪い
index.mjs
が現れて、baz.mjs
から直接veryUsefulFunc
を読み込んで使用してしまいました。この瞬間にまずい循環参照が発生します。具体的には、index.mjs
→baz.mjs
→utils.mjs
→bar.mjs
→utils.mjs
という循環参照の発生により、baz.mjs
よりも先にutils.mjs
が実行されます。その結果、utils.mjs
が実行された段階でbaz.mjs
からエクスポートされているveryUsefulFunc
はTDZ下にあるため、utils.mjs
内で以下のエラーが発生してしまいます。ReferenceError: Cannot access 'veryUsefulFunc' before initializationさらに、Webフロントエンド状況が悪化することがあります。フロントエンドではいまだにES5へのトランスパイルが行われることが珍しくなく、その場合
const
はvar
に変換されてTDZが消えます。その結果、上の例は「TDZによるエラーが発生する」という結果の代わりにutils
が以下のようなオブジェクトになるという結果になります。{ someNiceFunc: [Function: someNiceFunc], otherNiceFunc: [Function: otherNiceFunc], veryUsefulFunc: undefined }つまり、なぜか
utils.veryUsefulFunc
だけundefined
になってしまうのです。上記のソースコードからこの結果が得られたとき、あなたは原因を特定することができますか? ビルドシステムが一因なのでソースコードだけ見ても分からない上に、循環参照が原因なのでimport
文の順番を変えたら直るということすらあり得ます。「JavaScriptはクソ言語」と吐き捨てて投げ出したくなる誘惑に負けずにバグの修正までこぎつけることができるでしょうか。このバグの原因は主に2つあります。ひとつは循環参照を作ったこと、そしてもう一つはインポートしたものをオブジェクトに詰めてエクスポートするというアンチパターンを行なったことです。
後者が100%いつでも避けるべきかは疑問符が付くところですが、避けられるなら避けるべきです。今回の場合は、アンチパターンを避けて
utils.mjs
をこのようにすれば万事解決です。import { someNiceFunc } from "./foo.mjs"; import { otherNiceFunc } from "./bar.mjs"; import { veryUsefulFunc } from "./baz.mjs"; export { someNiceFunc, otherNiceFunc, veryUsefulFunc, }それに合わせて
utils.mjs
を使う側もimport * as utils
にするか、あるいは必要なものだけutils.mjs
から読み込むようにします。皆さんもこの先「インポートしたはずのものが
undefined
だ」という謎のバグに出会うことがあるかもしれませんが、その場合は循環参照+エクスポートのアンチパターンという組み合わせを疑ってみるのも悪くないでしょう。まとめ
この記事では、JavaScriptの
import
・export
文の挙動を「何をエクスポートしているのか」という点を中心に解説しました。とくに、export
文は常に変数をエクスポートしており、import
はその変数へのエイリアスを作るのだという点が重要です(export default
の場合は変数が暗黙に作成されていましたが)。この挙動は循環参照が発生したときでもなるべくいい感じに動くするようにするためのものですが、その恩恵を得るためには記事で扱ったようなアンチパターンを避ける必要があります。
また、実際にモジュールを使って書かれたコードを処理するのに現在広く使われているwebpackを例にとり、この記事で説明したような挙動が再現されていることを確かめました。
さらに詳しく知りたい方へ
筆者による以下の記事では、top-level awaitという新たな要素がモジュールシステムに与える影響について解説しています。また、この記事の後半ではECMAScript仕様書を読みながらモジュールの挙動を追っていきます。
モジュールの理解をさらに深いものにしたい方はこちらの記事もぜひご覧ください。
- 投稿日:2020-03-24T01:28:39+09:00
2020年から始めるAzure Cosmos DB - JavaScript SDK (SQL API)を見てみる (Part.2)
この記事について
本記事は、2020年3月6日 (米国時間) にて、Azure Cosmos DB に新しく Free Tier (無償利用枠) が登場したことに伴い、改めて Azure Cosmos DB を色々と触っていく試みの 4 回目です。
今回も、前回記事 同様、 Microsoft Azure Cosmos JavaScript SDK について見ていきたいと思います。対象読者
- Azure Cosmos DB について学習したい方
- Node.js で Azure Cosmos DB への CRUD 操作を行いたい方
- Microsoft Azure Cosmos JavaScript SDK の動作について理解したい方
- TypeScript のパラメータプロパティ宣言を知らない方
Microsoft Azure Cosmos JavaScript SDK
実際に、Microsoft Docs の内容を元に、JavaScript SDK (SQL API) の中身を見ていきます。
今回はデータベースの読み取り、または削除
を行う Database について確認します。Database
TypeDoc の記載は、以下の通りです。
Operations for reading or deleting an existing database.
既存のデータベースの読み取りまたは削除の操作。const client: CosmosClient = new CosmosClient({ endpoint, key }); const database: Database = client.database(databaseId);実際に使用する際は、CosmosClient クラスを生成した後、CosmosClient クラス内にある
database
メソッドを使用して、Database クラスを生成する手順が必要になります。
CosmosClient クラスのdatabase
メソッドは、どうなっているのか、実際に中身を確認してみます。Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/CosmosClient.tspublic database(id: string): Database { return new Database(this, id, this.clientContext); }はい、Database クラスのコンストラクタが動いています。
まあ Database 型の定数(const)に値を代入しているのだから当たり前ですよね。このコンストラクタ、プログラミング初心者にとってはすごく理解が難しい引数
this
がありますが、一旦置いておいて、実際のコンストラクタの中身を見ていきます。Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/client/Database/Database.tsexport class Database { public readonly containers: Containers; public readonly users: Users; constructor( public readonly client: CosmosClient, public readonly id: string, private clientContext: ClientContext ) { this.containers = new Containers(this, this.clientContext); this.users = new Users(this, this.clientContext); }コンストラクタの第一引数に
CosmosClient
があります。
つまり、先ほどの this とは、このコンストラクタの引数に自分自身への参照(CosmosClient)を渡していた、ということです。
ところで、 (私みたいな) TypeScript 初心者がこのコードをみた際、きっと下記のような疑問を持つのではないかと思います。(実は前回の内容の中にも同じものがあったんですけどね、、)あれ、、先ほど引数で渡してきた CosmosClinet と databaseId、全然使われていないように見えるけど、どうなっているの...!?
安心してください、もちろん
渡した引数には意味があります
。ここで注目するのはコンストラクタの引数に public/private/readonly の修飾子がついているところです。TypeScript には、
パラメータプロパティ宣言
というものがあります。これは、コンストラクタの引数の中でプロパティの作成および初期化が行えるようになるというものです。つまり、上記で記載した Database クラスのコンストラクタの内容は
export class Database { public readonly client: CosmosClient; public readonly id: string; private clientContext: ClientContext; public readonly containers: Containers; public readonly users: Users; constructor( client: CosmosClient, id: string, clientContext: ClientContext ) { this.client = client; this.id = id; this.clientContext = clientContext; this.containers = new Containers(this, this.clientContext); this.users = new Users(this, this.clientContext); }と同じ内容ということになります。
実際にコンパイルされた後の JavaScript ファイルも見てみます。Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/dist/index.js(5988行目付近)class Database { constructor(client, id, clientContext) { this.client = client; this.id = id; this.clientContext = clientContext; this.containers = new Containers(this, this.clientContext); this.users = new Users(this, this.clientContext); } }内容が同じであることを確認できました。
TypeScript って本当に便利ですね。さて、本題のコンストラクタ処理に戻ります。
Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/client/Database/Database.ts(再掲)export class Database { public readonly containers: Containers; public readonly users: Users; constructor( public readonly client: CosmosClient, public readonly id: string, private clientContext: ClientContext ) { this.containers = new Containers(this, this.clientContext); this.users = new Users(this, this.clientContext); }コンストラクタのパラメータプロパティ宣言を除くと、
Containers
とUsers
のクラスを生成する処理があります。
実際に、この 2 つのコンストラクタの中身をみていきます。Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/client/Container/Containers.tsexport class Containers { constructor(public readonly database: Database, private readonly clientContext: ClientContext) {} }Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/client/User/Users.tsexport class Users { constructor(public readonly database: Database, private readonly clientContext: ClientContext) {}また出たな!パラメータプロパティ! ※決してどこかの企業のフレーズを真似したわけではありません笑
ここでもコンストラクタのパラメータプロパティ宣言を使い、
Containers
クラスとUsers
クラスを生成していますね。このあたりのクラスについては、次回以降、確認していきたいと思います。さいごに
今回は、Azure Cosmos DB に JavaScript/Node.js で接続する際に CosmosClient クラスを使って生成する Database クラスについて、中身を確認してみました。
今回の内容も、以前、実際に CRUD アプリを作成 (Qiita 記事) した時はたったの 1 行で終わってしまった内容です。前回、
普段のアプリ開発では、あまり意識しない世界なのかもしれませんが、実際にコンストラクタの中で何が行われているのかを確認することは、Azure Cosmos DB の仕組みや使用するライブラリへの深い知見を得る ためには必要な事かな、と思いました。
と思っていた自分ですが、Microsoft Azure Cosmos JavaScript SDK の中身を見ていくことは、ライブラリの理解だけでなく、
TypeScript の勉強もできる
という一石二鳥なことではないか、と感じました。他のライブラリについてもこれは同じように言えることだと思います。次回は、
Container/Containers
クラスについて見ていこうと思います。関連リンク
前回記事
参考資料
Microsoft Docs
TypeScript Handbook
Qiita
- 投稿日:2020-03-24T01:07:02+09:00
Vue.jsによるバリデーションのサンプルを作ってみた
Vue.jsを使うメリット
導入のしやすさ
Vue.jsが流行っているのは段階的に導入を進めていきやすいからというのが挙げられると思います。また実装の規模によってCDNで導入したりnpmコマンドでVue CLIを入れてがっつりVueを使用して構築することも可能です。
大量のイベントの記述を一掃できる
Vue.jsで実装を行うとjQueryで大量に記述するonclickなどのイベントの記述を一掃することができます。
HTMLに$(.js-selector)のようにクラスの先頭にjs-って付けてJSだけで使用されるクラスををたくさん記述する必要がなくなるのでHTMLの見通しも良くなります。またDOMの構造が変化してもプログラムが動かなくなることが基本的にありません。バリデーションのサンプルを実装した
See the Pen wRmvOO by YusukeIkeda (@YusukeIkeda) on CodePen.
Vue.jsでバリデーションを実装する場合、プラグインを導入する方法もありますが、まずはプラグインを使わず実装を出来た方が理解度が変わると思うので、今回はプラグインを使わないで実装をしてみました。
HTML・CSSについて
CSSに
[v-cloak] {display: none;}
という記述がありますが、これはチラツキ防止です。
Vue.jsが完全に読み込まれる前に、一瞬だけマスタッシュ構文 {{ checkName }} が見えてしまうのを防ぐ為、Vue.jsが完全に読み込むまでdisplay:none;
で見えなくしてくれます。HTML<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Vue.jsによるサンプルバリデーション</title> <style> [v-cloak] { display: none; } table td{ width: 200px; } table input[type="text"]{ width: 100%; } </style> </head> <body> <div id="app"> <div v-cloak> <!--エラーメッセージ--> <p v-show="!this.name.flag">{{checkName}}</p> <p v-show="!this.tel.flag">{{checkTel}}</p> <p v-show="!this.email.flag">{{checkEmail}}</p> </div> <!--入力項目--> <form method="post" action=""> <table> <tr> <th>名前:</th><td><input type="text" v-model="name.text"></td> </tr> <tr> <th>電話番号:</th><td><input type="text" v-model="tel.text"></td> </tr> <tr> <th>メール:</th><td><input type="text" v-model="email.text"></td> </tr> </table> </form> </div> <script src="https://unpkg.com/vue"></script> <script src="script.js"></script> </body> </html>JavaScriptについて
【実装における課題】
if(checkval == 0)
というif文で文字列の長さをチェックして、未入力かどうかの判定をしているけど、このif文が初回アクセス時も動いてしまう。その為画面にアクセスしたらいきなりエラーメッセージが表示される状態。対処療法として初期化の文字列に半角スペースを入れて、エラーメッセージを表示させないようにしたけど、これは根本的な解決策じゃない…。良い解決策をご存じの方、コメントで教えていただけたら幸いです…!
script.jsvar app = new Vue({ el: '#app', data: { name:{ text: ' ', max:'名前は10文字以内で入力してください', require:'名前は必須です', flag : true, }, tel:{ text: ' ', max:'電話番号は10文字以内で入力してください', require:'電話番号は必須です', flag : true, }, email:{ text: ' ', max:'メールアドレスは100文字以内で入力してください', require:'メールアドレスは必須です', flag : true, }, }, computed: { checkName: function() { let checkval = this.name.text.length; if(checkval == 0) { //未入力チェック this.name.flag = false; return this.name.require; } else if(checkval > 10) { //文字の制限チェック this.name.flag = false; return this.name.max; } this.name.flag = true; }, checkTel: function() { let checkval = this.tel.text.length; if(checkval == 0) { //未入力チェック this.tel.flag = false; return this.name.require; } else if(checkval > 10) { //文字の制限チェック this.tel.flag = false; return this.tel.max; } this.tel.flag = true; }, checkEmail: function() { let checkval = this.email.text.length; if(checkval == 0) { //未入力チェック this.tel.flag = false; return this.name.require; } else if(checkval > 100) { //文字の制限チェック this.email.flag = false; return this.email.max; } this.email.flag = true; } } });まとめ
今回のサンプルはとりあえず動くけど、実装方法は絶対に良くないのでもっと理解を深める必要があるなぁと思います。
Vue.jsは今後フロントエンドでスタンダードなフレームワークになるのかなぁと思っているのだけど、特にこだわりが無いのであれば、導入を検討しても良いんじゃないかなぁと思う。(むやみに新しい技術に飛びつくのは節操ないけど)。ある程度モダンな技術を採用しておくと採用の面でもプラスに働く可能性は高いですし、在籍しているエンジニアのスキルの向上にもなりそうです。
- 投稿日:2020-03-24T00:58:02+09:00
js関数まとめ
関数
関数宣言文と関数リテラル式の違い
//関数宣言文 function func_name() { /* 処理 */ }; //関数リテラル式 var f = function func_name() { /* 処理 */ }; var f = function () { /* 処理 */ }; // 関数名func_nameは省略可。関数宣言文は巻き上げ(hoisting ホイスティング:varによる変数宣言と、functionによる関数宣言はスコープ内の一番先頭で"宣言"されたものと見なされる)が起こるため、宣言より前に呼び出してもエラーしない。
引数
関数内から実引数にアクセスするには
arguments
オブジェクトが使える。
arguments.length
として実際に受け取った実引数の数を取得できる。
反対に関数自身の持つlengthプロパティを使うと仮引数の数を取得できる。スコープ
varにはブロックスコープが存在しない。letではこれがある。
if (var i = 0; i < 2; i++){ } //ブロックを抜けてもiは2また、letは以下のようにブロックスコープをもった文や式を作れる。
//let文 let ( x = 1 ) { // 処理 } //let式 let ( x = 1 ) 式;Functionクラス
StringやNumberのように、Function関数・コンストラクタが存在する。
同じく関数宣言文やリテラル表記で定義する方が望ましいらしい。プロパティ(一部)
- apply() : 第一引数にthis参照先、第二引数に渡す要素のリストを指定して関数実行。
- bind() : 第一引数にthis参照先、第二引数に渡す要素を1つ1つ指定し、クロージャを返す。可変長。
- call() : 第一引数にthis参照先、第二引数に渡す要素を1つ1つ指定して関数実行。可変長。
- length : 関数の仮引数の数(上述)
なお、this参照、クロージャについては下記。
this参照
var obj = { x: 10, f: function() { print this.x; } } var fn = obj.f fn() // thisの参照先はグローバルオブジェクトとなる。オブジェクトリテラルとして定義した時にはオブジェクトobjのプロパティであるためthis参照はobjのxを指す。
変数fnにobjの関数オブジェクトfを代入した時、グローバルオブジェクトのプロパティが生成し、fnを呼び出すとthis参照先はグローバルオブジェクトの持つxとなる。apply関数、call関数は第一引数に指定したオブジェクトがthisの参照先となる。
function f() { print(this.x); } var obj = { x: 10 }; f.apply(obj); // 10クロージャ
表層的な理解:関数スコープを抜けてもローカル変数が生き続け、状態を持つことができる関数。
function f() { var x = 0; function g() { return ++x; } return g; } var fn = f(); fn() //1 fn() //2 fn() //3上記のように関数fのローカル変数が生き続けているように振る舞う。
細かなしくみ。
関数(ここではf)がコールされると、その瞬間Callオブジェクトと呼ばれるものが暗黙裡に生成する。Callオブジェクトは自身のプロパティとして関数スコープの変数や関数を持つ、すなわちここではxやgがCallオブジェクトのプロパティとなる。最後に関数gへの参照がreturnされ、グローバルオブジェクトのプロパティであるfnに格納される。
ここで、本来であればCallオブジェクトはその関数が終了し、どこからも参照されなくなった時点で消滅する(=ガベージコレクション)のだが、プロパティである関数gへの参照がfnに渡っているためCallオブジェクトが消滅しない。
そのため、表面上関数fnが状態を持っているように見える。
クロージャの利用
- グローバル変数の回避
- 変数の隠蔽(privateな変数)
var calc = (function() { var private_val = 5; return function (x) { print private_val * x; } } )(); calc(2); //10無名の関数は定義と同時に実行され、プロパティである関数への参照がcalcに渡されるため、Callオブジェクトが生き続け、クロージャとなる。
このcalcは関数としてprivate_valにアクセスできるが、ダイレクトにアクセスして値を書き換えたり読み出したりすることはできない。式クロージャ
Firefoxの独自拡張?deprecatedらしい。詳細は触れない
- 投稿日:2020-03-24T00:48:32+09:00
高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。を格下目線から批判してみた。
はじめに
高階関数を書いたら、中級者になれた気がした。という記事を読んだのち、高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。まで読んだところ、非常にもやもやする部分があったので批判をしてみる、という趣旨の記事になります。
非情に底辺(某記事で言う格下目線)の内容になっていますので、こういう場所もあるんだというネタとしてお読みください。結論
高階関数にする前のべた書きコードが要件を満たせて安全なほぼ最適解!
const setHeight = (element, toRed, width) => { element.style.height = '100px'; if(toRed){ element.style.backgroundColor = 'red'; } // 第三引数のwidthがあったら、要素の横幅を変更する if(width){ element.style.width = width + 'px'; } };そもそも要件変わってません?
上級者になれた方の記事を拝見すると、
[問題]そもそもコールバック関数を使うメリットが無い
コールバック関数なんて複雑で読みにくいものなんか使わずに、以下のようにすればいいだけなのだ。const setHeight = (element) => {
element.style.height = '100px';
}setHeight(box);
box.style.color = "green";
box.innerHTML = "こんにちわ!";コールバック関数を使う適切な動機ではない。
とあります。
しかしちょっと待ってほしい。大元記事で社長はこう依頼し、そこに対して要件を追加しています。社長「今日は↓こんな関数を作ってくれ」
引数として受け取ったHTML要素の高さを100ピクセルにする
つまり必要とされているものは「高さを100ピクセルにしてその他いろいろな動作をすること」ではなく「高さを100ピクセルにしてその他いろいろな動作をする関数」が求められているのです!
つまりコールバック関数を使うメリットがないので関数の外で書けばいいだけと返しても「要件通り作れ!」と言われるだけではないでしょうか。
顧客が求めているのは要件通りの関数です。考察
機能の実装ではなく関数を作って納品するのは、おそらく関数を作る人と使う人は別なのでしょう。
同じ画面を触っているよその会社から「○○して××したら△△になる機能を実装しろって言われたんだけど、うちの編集可能範囲(同一ソース内でも飛び地になっていたりする)超えてる部分あるからそこ実装してくれない?こういう引数でこういう動作」のような案件が入ってきたのではないでしょうか。
様々な会社が現在進行形で一つのシステムを作成している時によくある「普通に書けば解決するのに別会社の担当だから(運用的に)そのオブジェクトに触れない状態」です。中途半端ににわか知識持ってる上仕切りたがりで人の話を全く聞かない人が上にいるとてもよくある現場になります。
コールバックで全部任せるのは危険では?
かと言ってコールバックの形がいいとも言えません。
関数を使用するのは全く知らない別の人なので、どんな処理がされるか全くわかりません。
そしてその関数が使われる場所が自社の担当だった場合、コールバック内でどんなことをされても正常に動くように実装しなければいけなくなります。
変なコールバックを渡されたせいでも「バグが起こったのはあなたの関数内でしょ?」と言われて泣きながら修正することになります。なりました。その点引数と実装を追加していく形なら、関数の想定していること以外は基本的に発生することはなく、誰にも怒られない要件通りの綺麗なコードになります。見知らぬ他人が要素を勝手に削除してバグったりしません。
なので
言われたことは言われたとおりに実装し、関数に要件が追加されたらその都度言われたとおりに引数を追加して言われたとおりの処理を書くのが安全で確実ではないかと思います。
身を守るために自分の知らない人が好き勝手出来るような関数はなるべく作らないようにしましょう。というのが通して読んで私が至った結論でした。
皆さんがこんな結論に至るプログラマ(と自称したら怒られそうですね)にならないことを祈ります。
- 投稿日:2020-03-24T00:37:08+09:00
【Node】dotenvで環境変数を設定する
概要
- Nodeでプログラムを実行する時に環境(dev/stag/prodなど)ごとに値が異なる部分はコードを修正せずに実行するため環境変数として埋め込むことがよくあると思います
- セットする環境変数が1つ2つであればコマンド実行時に設定すればよいですが規模が大きくなってきたらファイルでまとめて定義したくなるでしょう
- そんな時はdotenvを使うと便利です
- dotenvを使うと
.env
ファイルに定義された値を環境変数として使うことができます- また、システムの環境変数として値が設定されていればそちらを優先して使うということもできます
- なので、開発時はローカルで
.env
を配置し、本番ではホスティングサービスの機能で環境変数として設定するといった使い方をすることでリポジトリ内のファイルを変更せずに実行することができますNodeスクリプトの実行時にdotenvを使う場合
- シンプルな形でいうと
index.js
があってnode index.js
で実行するようなケースです- まずはdotenvをinstallします
npm i dotenv
- 次にサンプル用の
index.js
を作成します
require('dotenv').config();
としておくことでdotenvが適用されるようになりますprocess.env
で環境変数を取得できますTEST_VALUE
という環境変数の値を取得しコンソールに出力するサンプルですindex.jsrequire('dotenv').config(); const value = process.env.TEST_VALUE; console.log(value);
- 最後に値を定義する
.env
ファイルを作成します.env# 環境変数を定義 TEST_VALUE=.envで値を設定しています
- 実行してみましょう
node index.js
- ログに
.envで値を設定しています
と出力されているはずです.env
の値をシステム環境変数で上書きできることも確認しておきましょうTEST_VALUE="コマンドで値を設定しています" node index.js
- ログに
コマンドで値を設定しています
と出力されているはずですwebpackでビルド時にdotenvを使う場合
- webpackなどでビルドしてから実行するケースも多くあると思います
- ここではWebpackでdotenvを使う場合の手順を紹介します
- dotenv-webpackというライブラリを使用します
npm i webpack webpack-cli dotenv-webpack
- webpackの設定ファイルを作成します
webpack.config.jsconst Dotenv = require('dotenv-webpack'); module.exports = { plugins: [ // { systemvars: true } を設定するとシステム環境変数も読み込まれるようになる new Dotenv({ systemvars: true }), ], };
index.js
を修正します
- dotenvはビルド時に適用されるため設定が消えてスッキリしました
index.jsconst value = process.env.TEST_VALUE; console.log(value);
- ビルドして実行してみましょう
# ビルド(dist/main.jsが出力される) npx webpack -p index.js # 実行 node dist/main.js
- 実行すると
.envで値を設定しています
とログが出力されるはずです- システムの環境変数で上書く場合ビルド時に環境変数を設定します
# 環境変数を設定してビルド TEST_VALUE="コマンドで値を設定しています" npx webpack -p index.js # 実行 node dist/main.js
- 実行すると
コマンドで値を設定しています
とログが出力されるはずですまとめ
- dotenvを使った環境変数の設定のしかたを紹介しました
- Nodeを使ったアプリでもこういった技術を活用してCI/CD回していきましょう!
- 投稿日:2020-03-24T00:18:20+09:00
vue cliのscroll behavior: アンカーリンクをつくる方法
こんにちは。備忘録です。
ページ内にたくさんの記事があるとします。1ページに表示されている記事数は5つ。ところが一番下にある「もっと読む」というボタンをクリックすると、新たに5記事が表示される。そんな動きをしたいとします。
ところがアンカーリンクを設定しなければ、「もっと読む」をクリックしたときにページのトップに連れて行かれてしまいます。UX的には、「もっと読む」をクリックしたときは、トップに飛ばされないまま、続く5つの記事が見たいです。
こんなとき、vue.jsではscroll behaviorという機能を使って、トップに連れて行かれないよう調整することができます。
(下に来る例が上にくる例と違うので紛らわしいのですが、、、)
Step 1. RouterのJSファイルをいじる。
const router = new Router({ mode: "history", scrollBehavior(to, from, savedPosition) { //ここでscrollを調整する。ここにはto, from, savedPositionの三つの引数が入ります。 if (savedPosition) { return savedPosition; //戻る(?)もしくは進む(?)ボタンを操作したときは、デフォルトのpositionでロードしてください。 } else { //そのほかの場合はこのpositionを効かせてちょうだい。 const position = {}; if (to.hash) { //hashがあれば…… position.selector = to.hash; //positionはhashが指定したところだよ。 if (to.hash === "#animal") { //hashがanimalというidを指定しているのであれば position.offset = { y: 200 }; //offsetを効かせてください。※ここはoffsetを効かせていますが、なんでも指定できます。 } if (document.querySelector(to.hash)) { return position;//hashがあれば、そのpositionを教えてください。 } return false; //そもそもhashがなければ、デフォルトの操作をしてください。 } } }, routes: [ { path: "/", name: "Home", component: Home, props: true } });Step 2. Scroll behavior を効かせたい instance/componentをいじる。
<section class="zoo-information"> <h1>Our Zoo</h1> <p>{{zoo.description}}</p> </section> <section class="animals"> <h2>I think {{ animal.name }} is the best</h2> <div class="cards" id="animal"> <div v-for="animal in animals" :key="animal.slug" class="card"> <router-link :to="{ name: 'animalDetails', params: { animalSlug: animal.slug}, hash: '#animal' }" > //ここにクリックしたとき、トップに行かず、#animalのところで位置をキープしたい <img :src="require(`@/assets/${animal.image}`)" :alt="animal.name" /> <span class="card__text">{{ animal.name }}</span> </router-link> </div> </div> <router-view :key="$route.path" /> </section>何も設定しなければ、
router-link
のリンクをクリックしたとき、.zoo-information
がトップにくるようページが表示されるでしょう。でも、せっかくrouter-link
をクリックしているので、UX的にもrouter-link
をクリックしたときはrouter-link
の中身がページのトップにくるよう表示したいです。そこで
to.hash
をrouter.jsで指定しておくと、router-link
をクリックしたときにid="animal"
と書かれた部分がページのトップにくるよう位置を指定できるのです。なかなかのツワモノです。
- 投稿日:2020-03-24T00:11:17+09:00
ブロックチェーンでリモート承認してみる(2)
今回は申請者と承認者でタブを分けて実運用に近い形で検討してみます。
また、リスナーやフィルターを利用して自動承認なども実装してみましょう。前回の記事をまだ読んでいない方はこちらから先にお読みください。
事前準備(申請者、承認者両方で実行)
申請者をAlice、承認者をBobとします。
chromeブラウザで申請者タブと承認者タブの2つを開いてください。双方ともにF12キーあるいは fn+F12キーを押してデベロッパーツールを起動しておきます。まずはライブラリの読み込みます。
alice,bob(script = document.createElement('script')).src = 'https://s3-ap-northeast-1.amazonaws.com/xembook.net/nem2-sdk/symbol-sdk-0.17.3.js'; document.getElementsByTagName('head')[0].appendChild(script);次に、使用するモジュールのインポートです。前回のsymbol-sdkに加えて今回はrxjsのoperatorsを使用します。Websocketでブロックチェーンをリアルタイムに監視しますので、Listenerクラスも生成しておきます。
alice,bobnem = require("/node_modules/symbol-sdk"); op = require("/node_modules/rxjs/operators"); NODE = "https://sym-test.opening-line.jp:3001"; GENERATION_HASH = "44D2225B8932C9A96DCB13508CBCDFFA9A9663BFBA2354FEEC8FCFCB7E19846C"; txHttp = new nem.TransactionHttp(NODE); accountHttp = new nem.AccountHttp(NODE); wsEndpoint = NODE.replace('http', 'ws'); listener = new nem.Listener(wsEndpoint, WebSocket); listener.open().then(() => { listener.newBlock(); });websocketのエンドポイントは
http
をws
に置き換えたものになります。
listenerは何も通信することが無いと1分ほどで切断されてしまいます。
newBlock()を実行してブロック生成ごとに通信が走るように設定しておきましょう。申請者アカウントの作成と準備
アカウントの作成と蛇口からの入金、残高確認を申請者タブで実行します。
alicealice = nem.Account.generateNewAccount(nem.NetworkType.TEST_NET); console.log(alice.address.plain()); "http://faucet-02.symboldev.network/?recipient=" + alice.address.address +"&amount=10" //15秒後ぐらいに確認 accountHttp.getAccountInfo(alice.address).subscribe(x=>console.log(x.mosaics[0]));コンソールに申請者のアドレスと蛇口サイトのリンクが出力されます。
承認者アカウントの作成と準備
アカウントの作成と蛇口からの入金、残高確認を承認者タブで実行します。
bobbob = nem.Account.generateNewAccount(nem.NetworkType.TEST_NET); console.log(bob.address.plain()); "http://faucet-02.symboldev.network/?recipient=" + bob.address.address +"&amount=10" //15秒後ぐらいに確認 accountHttp.getAccountInfo(bob.address).subscribe(x=>console.log(x.mosaics[0]));コンソールに申請者のアドレスと蛇口サイトのリンクが出力されます。
承認時のふるまい
次に、申請が来た時の承認者のふるまいをあらかじめ記述しておきます。
本来ならファイルハッシュの確認してから承認ですが、今回は申請者Aliceからのトランザクションを発見次第、自動承認する動きにしてみます。bobaliceAddress = nem.Address.createFromRawAddress("{Aliceのアドレス}"); listener.confirmed(bob.address) .pipe( op.filter((x) => x.signer.address.plain() === aliceAddress.plain()), ) .subscribe(x=>{ console.log(x); tx = nem.TransferTransaction.create( nem.Deadline.create(), aliceAddress, [], nem.PlainMessage.create('approved:' + x.transactionInfo.hash), nem.NetworkType.TEST_NET, nem.UInt64.fromUint(100000) ); signedTx = bob.sign(tx,GENERATION_HASH); txHttp.announce(signedTx).subscribe(_ => console.log(_), err => console.error(err)); console.log("https://sym-test.opening-line.jp:3001/transaction/" + signedTx.hash + "/status") });Aliceタブで出力されたAliceのアドレスを使ってAddressクラスを生成します。Bob宛のトランザクションを受信した場合に、署名者がAliceだった場合"Approved:{受信トランザクションのhash値}"をメッセージにして承認トランザクションを発生させます。
pipe 内部で rxjs.operators の filter機能を使います。承認確認
最後に承認者から承認された場合の確認をあらかじめ記述しておきます。
alicebobAddress = nem.Address.createFromRawAddress("{Bobのアドレス}"); listener.confirmed(alice.address) .pipe( op.filter((x) => x.signer.address.plain() === bobAddress.plain()), ) .subscribe(x=>{ console.log("承認されました"); console.log("https://sym-test.opening-line.jp:3001/transaction/" + x.transactionInfo.hash + "/status") });申請
それでは申請を行ってみましょう。
alicetx = nem.TransferTransaction.create( nem.Deadline.create(), bobAddress, [], nem.PlainMessage.create('b7d3e3191d2d2e77ed6e455eeaec147c13e19f0c079f0ca0dcff853f3df46911'), nem.NetworkType.TEST_NET, nem.UInt64.fromUint(100000) ); signedTx = alice.sign(tx,GENERATION_HASH); txHttp.announce(signedTx).subscribe(_ => console.log(_), err => console.error(err)); "https://sym-test.opening-line.jp:3001/transaction/" + signedTx.hash + "/status"事前にHash生成器で作成したファイルハッシュ
b7d3e3191d2d2e77ed6e455eeaec147c13e19f0c079f0ca0dcff853f3df46911
をメッセージに指定して承認者へ送信します。トランザクションがconfirmedになると、承認者(Bob)タブの承認トランザクションが走ります。さらに承認トランザクションがconfirmedになった時点で申請者(Alice)タブにて"承認されました"と表示されます。ぜひお試しください。
次回はNEM Catapultエンジンの真骨頂となる機能満載でのリモート承認にチャレンジです!お楽しみに。