20200324のJavaScriptに関する記事は27件です。

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があまり理解できていなかったので、この機会復習もできました。
理解が間違っていましたらぜひ教えてください。
読んでくださりありがとうございました!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kinx 実現技術 - JIT

JIT Compile - Just In Time コンパイル

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。作ったものの紹介だけではなく実現のために使った技術を紹介していくのも貢献。その道の人には当たり前でも、そうでない人にも興味をもって貰えるかもしれない。

前回のテーマは VM (Virtual Machine)。今回のテーマは JIT。

正直、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」 ボタンとか押してもらえるとモチベーションにつながります。どうぞよろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

商品出品画面を作る

メルカリの商品出品画面を参考にコピーを作る

参考画面メルカリ

使用する機能

  • 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
            &#47;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
        &copy; Mercari, Inc.
items_new.scss
a {
  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.rb
def 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
end
item.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階層以降がまだ未実装でした・・・

とりあえず、後から実装するとして、先ずは画像投稿から

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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オブジェクトからアクセスするようにした。

リファクタリングをしてみて

アプリを動かしても動きは変わらない。というか変わったらリファクタリングではない。
ソースのリファクタリングとは基本的に、既存の動作には一切変更を与えずにソースの保守性を高めることだと思っている。デグレなしが前提のはず

元のファイルと比較しても、そのファイルが何をするためのものなのかがハッキリしたのではないかと思う。

まだまだ、修正の余地がありそうなので思いついたらリファクッていこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.

参考ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.

参考ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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全ての値がオブジェクトのような振る舞いをするということです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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によってのみ変更できる感覚を思い出します?
クロージャはカプセル化されたオブジェクト的な存在というわけですね?

以上、「即時関数」「クロージャ」について簡単なまとめでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでjszipとfilesaver.jsを使ってzipフォルダを作成し、ダウンロードしてみる。

初めに

今回はjavascriptのライブラリの一つ、jszipfilesaver.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を勉強して記事を書こうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[CSS/js]tableの行/列ヘッダーを固定する

はじめに

最近Webアプリ周辺の技術を学び始めた者です。普段は製造業で設計業務を担当しておりますが、社内システム構築用にいろいろと勉強しています。主に使うのは下記。

  • 言語 : python/C#
  • Web framework : django

記事に書き出すことで自身の理解も深まると考え、今回初投稿をさせて頂きます。

動機

休日に妻と共同でアプリ開発をしています(この開発記録もつけられたらなーと思っています)。
tableを多用するのですが、その際列ヘッダーと行ヘッダーを固定してtbodyのデータセルだけスクロールできないかなと考え、いろいろ調べていました。
便利なプラグインもたくさんありましたが、

  • 複数ヘッダーを固定できるものが限られていた
  • なるべく既存のtableに変更を加えたくない

という理由で手を出せず。position:stickyというステキなオプションがあるので、どうにかこれを使ってできないかと思い、やってみました。
なおこちらのstackoverflowの質問を参考にしました→Table with fixed header and fixed column on pure css

html

下記のような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.js
sample.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>

image.png

これで任意の数の行/列ヘッダーをcss/jsのみで固定することができます。

課題

Chromeでは動作確認しましたが、IEだとpolyfillが必要みたいです。
stickyfill

また、globalで変数指定が必要だったり、洗練されてないイメージもあるので、もっとうまいやり方がありましたらご教示頂ければ幸いです。

参考

Table with fixed header and fixed column on pure css
stickyfill

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[CSS/jsのみ]tableの行/列ヘッダーを固定する

はじめに

最近Webアプリ周辺の技術を学び始めた者です。普段は製造業で設計業務を担当しておりますが、社内システム構築用にいろいろと勉強しています。主に使うのは下記。

  • 言語 : python/C#
  • Web framework : django

記事に書き出すことで自身の理解も深まると考え、今回初投稿をさせて頂きます。

動機

休日に妻と共同でアプリ開発をしています(この開発記録もつけられたらなーと思っています)。
tableを多用するのですが、その際列ヘッダーと行ヘッダーを固定してtbodyのデータセルだけスクロールできないかなと考え、いろいろ調べていました。
便利なプラグインもたくさんありましたが、

  • 複数ヘッダーを固定できるものが限られていた
  • なるべく既存のtableに変更を加えたくない

という理由で手を出せず。position:stickyというステキなオプションがあるので、どうにかこれを使ってできないかと思い、やってみました。
なおこちらのstackoverflowの質問を参考にしました→Table with fixed header and fixed column on pure css

html

下記のような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>

image.png

ゴールは上記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.js
sample.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>

image.png

これで任意の数の行/列ヘッダーをcss/jsのみで固定することができます。

課題

Chromeでは動作確認しましたが、IEだとpolyfillが必要みたいです。
stickyfill

また、globalで変数指定が必要だったり、洗練されてないイメージもあるので、もっとうまいやり方がありましたらご教示頂ければ幸いです。

参考

Table with fixed header and fixed column on pure css
stickyfill

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

イベントが発生した時、その兄弟要素を操作する方法

概要

jQueryを使用せずに実装することを目指す。
同じ構造をした要素が複数存在する際に、その中でイベントが発生した兄弟要素だけを操作したい場合の実装方法である。

実装例

クリックしたらその兄弟要素にあたる画像を小さくするボタンを実装。今回は同じ構造の要素を4つ用意。

スクリーンショット 2020-03-24 15.53.49.png

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>

SCSS
button {
    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. 2.の子要素を取得(1.の兄弟要素にあたる)
  4. 3.の要素を操作

という周りクドい手順を踏むことによって、やっとこさ兄弟要素を操作することができる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
クラスなどがあります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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によって定義されているので明確です。
初期化処理も、繰り返しを避けることができました。

記事の内容に不備、誤りなどがある場合はご指摘いただけると幸いです。
また、よりスマートな記法をご存知であれば是非教えていただきたいです。

読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

以下のサイトのコードの完全版欲しいです

GASで作る社内ツール (キャンセル・報告編
https://qiita.com/nyanko-box/items/905d224d3ba8d3d00796

ソースコードそのままコピペしても、
キャンセル、報告ボタンが書かれておらず動きません。完全に動くソースコードが欲しいです。

当方、プログラミング初心者につき、最初は先人のマネから入って、
カスタマイズして、業務に応用させたく、どなたかお力添えはただけると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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(dotAll) flag for regular

正規表現に s オプションが導入されました。
https://github.com/tc39/proposal-regexp-dotall-flag

/./snew 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]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kinx 実現技術 - VM(Virtual Machine)

Virtual Machine

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。作ったものの紹介だけではなく実現のために使った技術を紹介していくのも貢献。その道の人には当たり前でも、そうでない人にも興味をもって貰えるかもしれない。

前回のテーマは Switch-Case、今回のテーマは VM (Virtual Machine)。

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);

ちなみに現在の関数呼び出し時の引数評価は 後ろから だが、一般的にこれをあまり保証したくないので、上記のうちの関数呼び出しで使われる yielda の値はインクリメント後か前かは保証されないことになるだろう。

レキシカル変数

レキシカル変数は関数オブジェクト作成時にリストとして関数オブジェクト自体に設定される。関数呼び出しの際、関数オブジェクトに格納されていたレキシカルスコープへのポインタが、作成されたフレームに設定される。関数内で関数が定義されると、その時のフレームをレキシカル・フレームとして登録することで数珠つなぎの形で参照できるようになる、といった算段。

スタック上のイメージは、フレームごとにレキシカル参照が設定されている状態。

|↑| 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」 ボタンとか押してもらえるとモチベーションにつながります。どうぞよろしくお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コールバックと、ポリモーフィズムと、それからコルーチンを構造的に見る

この記事では、コールバックはポリモーフィズムの特別な場合であるという見方もできること、その意味ではそんなに難しい話ではないことを説明します。
あと、関数呼び出しを視覚的にブロックみたいなもので表現すると、ポリモーフィズムやコルーチンを使ってどういう事をやっているのか何となくわかった気になれるので、そのような表現を試みます。
ちょっと見方に変化をつけることで、難しそうな概念が身近になるといいなというような試みです。

コールバックのおさらいと、ご利益

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()で挟み込むというような事ができています。関数一つよりも器用な処理ですね。模式的に図を描くと、以下のようになります。

スクリーンショット 2020-03-24 4.27.52.png
(図中の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 end

yieldまで実行したら、次にnext()が呼ばれた時は次のyieldまで実行する、というような流れになっています。模式的な図は以下のとおりです。

スクリーンショット 2020-03-24 4.28.01.png

(図では便宜上①でも中断位置から再開としていますが、①の時点では最初からになります)

まとめ

コールバックと、ポリモーフィズムと、それからコルーチン、
みんなちがって、みんないい。

免責事項

  • JavaScriptのfunctionは実際にはデータに相当するものも持っています。
  • 単にオブジェクトのメソッド呼び出しをすればポリモーフィズムと言えるのかというと、非常に微妙なところで、またダックタイピングと言うべきではないかという考え方もあると思いますが、便宜上ポリモーフィズムで統一しています。
  • 深夜テンションで書いているので、その他変なところがあればそっと直してください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】チュートリアルの三目並べをやる #2

前回

【React】チュートリアルの三目並べをやる #1

タイムトラベル機能の追加

前回は通常の三目並べ完成までやりました。
今回はその三目並べに「タイムトラベル機能」なるものを実装していきたいと思います。履歴ですね。

公式チュートリアル

着手の履歴の保存

suquaresの配列をsetStateで毎回新規オブジェクトで更新していたことがここで活きるらしいです。
このオブジェクトを更新のたびに保持していきます。

その履歴を保持する場所は一番TOPのGameにするそうです。
これによりBoardはstateを保持する必要がなくなります。

Game
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{ suares: Array(9).fill(null) }],
      xIsNext: true,
    };
  }

また、statusの更新やonClickの処理もすべてGameに持っていくことができます。

以下がすべてを移動させたバージョン

BoardとGame
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) }],
      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を設けます。

Game
class Game extends React.Component {
  constructor(props) {
    super(props);

    // Gameで履歴を保持する
    this.state = {
      history: [{ squares: Array(9).fill(null) }],
      stepNumber: 0, // 表示したい履歴のインデックスを表す
      xIsNext: true,
    };
  }

renderではstepNumberを用いて表示したい履歴を取得します。(currentのところ)

Game
  render() {
    // 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追記】
likey属性がないため、エラーになっていました。
一意な値を振ることが推奨されるようです。
key を選ぶ


stepはhistory内の1つ1つの要素を表します。
moveは、今のstep要素が配列の中でどの位置にいる要素であるか(インデックス)を表す。

onClickイベントで、表示させたい履歴のインデックスと、xIsNextsetStateします。

ここではhistrotyは更新しません。履歴を保持している配列の中から、指定したインデックスの履歴を表示させるだけなので、更新する必要はありません。

returnのところ
最後に、BoardのHTMLを返すreturn();の所です。
Boardに渡すonClickは、Squareのクリックイベント(つまり、マス目を押された時)の処理です。

ここでは、マス目を押された時点からまた新しく履歴を保持するようにします。
なので、「最初の履歴 ~ 今表示している履歴」までを、履歴の配列全体から抜き出し、その抜き出した物の最後尾に今の状態(履歴)を追加します。

Game
      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>
    );

movesTODOとなっていたところに埋め込みます。

これで完成です。

全文

index.js
import 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();

結果

D1AEZK.gif

感想

ちょっと最後らへんは混乱しました。
数日たってソースみるとおそらく解読できないと思います。

jQueryのようにセレクタをたくさん書かずとも実現できたのはメリットだと思いました。
おそらくjQueryならもっとソースコードが煩雑になるはず。

このチュートリアルはQiitaで別の方々がたくさん試されているので、今更でしたね・・・笑

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのモジュールは変数をエクスポートする

今時のJavaScript開発において、JavaScriptが持つモジュールの機能は欠かすことができません。我々はプログラムをいくつものファイル(モジュール)に分割し、import文とexport文を使ってそれらを繋げています。各モジュールはexport文を用いてそのモジュール内で定義した変数・関数などをエクスポートすることができ、別のモジュールがimport文でそれらの値を取得することができるのです。

皆さんは、このimportexport文がどのように働いているのか正確に説明できるでしょうか。実は、import文やexport文というのは値をインポート・エクスポートしているのではなく、言わば変数そのものをインポート・エクスポートしているのです。これを理解するのがこの記事のゴールです。

※ 本当は変数ではなく「バインディング」といったほうが用語としてより正確なのですが、この記事では分かりやすさのために変数という用語を使用しています。

export文は変数をエクスポートする

モジュールから何かをエクスポートするときに使うのがexport文ですが、これにはいくつかの種類があります。実は、export文は必ず変数をエクスポートしています。

最も代表的なexport const foo = ...;という構文は、const宣言により変数fooを作ると同時にそれをエクスポートしています。export let bar = ...のようにconst以外を使うことも可能です。それどころか、export const {foo, bar} = obj;のように分割代入で作られる変数をエクスポートすることもできます(この場合はfoobarがエクスポートされます)。

ほかにexport function 関数名() { }という関数宣言の形もよく使われます。この場合も、関数宣言を通してその関数が入った変数が作られています。

変数に外向きの名前をつけてエクスポートする

また、export { foo, bar }という構文は、事前に定義された変数foobarをエクスポートするという意味ですから、やはり変数をエクスポートしています。この構文の特徴はexport { foo as foo2 };のように違う名前でエクスポートする機能を持っている点です。これにより「モジュールの中ではfooと呼ばれている変数を、外向きにはfoo2という名前でエクスポートする」ということが可能です。このように、モジュールからエクスポートされている変数は「内向きの名前」と「外向きの名前」を持ちます。export const foo = ...のような宣言の場合は内向きの名前と外向きの名前が同じで、どちらもfooです。

defaultエクスポートの扱い

ところで、export default 値;という構文では変数をエクスポートしていないように見えますね。しかし、実はこれは内向きには*default*という名前の変数を作成し、外向きにdefaultという名前でエクスポートしています。ここで作られた*default*という名の暗黙の変数は、そんな変な名前の変数にアクセスする構文的な手段が存在しないため、モジュール内からアクセスすることはできません。

defaultエクスポートはdefaultという名前でエクスポートする機能です。逆に言えば、export default以外の方法でもdefaultという名前でエクスポートすればdefaultエクスポートと同様の挙動をするということです。次のa.mjsindex.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.mjsfooという名前で変数をエクスポートしています。a.mjs内のexport * from "./b.mjs";b.mjsからエクスポートされているfooを同じfooという名前で再エクスポートするという働きをします。言い換えれば、a.mjsは「b.mjsfoo」を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を表示します。これは、このfooa.mjsからインポートしたfooであり、a.mjsfooという名前でエクスポートしているのは「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のスコープに存在するfooa.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内の変数fooa.mjs内の変数fooのエイリアスであるとの見方もできますね。JavaScriptのモジュール間連携とは、モジュールの間に張られたエイリアスによって成り立つものなのです。

ただし、変数に再代入できるのはその変数を所有するオリジナルのモジュールだけです。index.mjsfoo = 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内の変数aa.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.mjsa.mjsb.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という値が入っています。ここでfoo0が入っている理由は、index.mjsよりも先にa.mjsが実行され、a.mjsの中のexport let foo = 0;が実行されることでa.mjsの変数foo0が代入されたからなのです。

もしa.mjsよりも先にindex.mjsが実行されていたら、console.log(foo)の時点でfooにはまだ何も入っていないことになってしまいます。

このように、インポートした変数がすぐ使えることを保証するするために、依存されている側から先に実行するという実行順序になっています。

しかし、循環参照がある場合はこの保証ができなくなります。循環参照とは、例えばa.mjsb.mjsをインポートし、b.mjsa.mjsをインポートしているというような状態を指します。この場合、どちらを先に実行しても問題が発生してしまいますね。a.mjsを先に実行すれば、a.mjsb.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.mjsb.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.mjsa.mjsからインポートした変数varFromAを評価しましたが、まだa.mjs内でexport const varFromA = "a";が実行されていないため、変数varFromAはまだ初期化されていない変数となっているのです。まだ初期化されていない変数はアクセスすることができません。

ちなみに、「初期化されていない変数」は循環参照に特有の現象ではありません。1ファイル内でも、変数宣言よりも前に変数にアクセスすると同じエラーになります。このことからも先のvarFromAa.mjs内のvarFromAに対するエイリアスであることが分かります。

// ReferenceError: Cannot access 'foo' before initialization
console.log(foo);
const foo = 123;

なお、変数がまだ初期化されていない区間はTemporal Dead Zone (TDZ) と呼ばれています。letconstで宣言された変数はその宣言が評価された際に初期化されるため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.jsOdd.jsが循環参照しています。それぞれ、isEvenisOddという関数をエクスポートしています。また、お互いにお互いがエクスポートしている関数をインポートし、相互再帰の形で利用しています。

実は、これは循環参照があるにも関わらずエラーが発生しません。その理由は、Even.jsOdd.jsが実行された瞬間はそれぞれ関数をひとつ定義されているだけであり、インポートした変数をすぐに参照するわけではないからです。実際にこれらの関数が実行されるのはmain.jsの中でisOdd(2)が実行されたタイミングです。main.jsは、Even.jsOdd.jsの実行が終わってから実行されます。つまり、isOdd(2)が実行されるタイミングではすでにisOddisEvenも定義済みである(TDZを抜けている)ということです。これにより、isOddはその中でisEvenを呼び出すことができ、またisEvenisOddを呼び出すことができます。この用意して、循環参照がある状態で関数を定義することができました。

これは、変数がエイリアスとしてインポートされているからこそ実現できることです。この例ではEven.jsOdd.jsmain.jsの順にモジュールが実行されるので、Even.jsが実行された(isEvenが初期化された)段階ではまだisOddは(これはOdd.jsがエクスポートしているisOddのエイリアスなので)TDZにあります。しかし、isOddisEvenの中身から参照されており、この段階で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の名前空間オブジェクトであり、そのオブジェクトに対してfoosetFooというアクセサプロパティを定義しています。これらのプロパティはゲッタを持ち、アクセスされると実際の変数foosetFooの値を返します。

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.mjsb.mjsfooを再エクスポートしているつもりですが、できていません。a.mjsのローカル変数として別にfooを定義しており、それをb.fooの値で初期化されているからです。このfoob.mjsfooのエイリアスではなく、初期化時にb.fooの値を使っただけでまったく無関係のfooだからです。

よって、この例でindex.mjsを実行すると、2秒後に1と表示されます。index.mjsがインポートしているfooa.mjsfooであり、b.mjsfooが変化しても影響されないからです。

b.mjsfooの変化が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.mjsbaz.mjsutils.mjs経由で他の関数を使用しています。

ところが、そこに行儀の悪いindex.mjsが現れて、baz.mjsから直接veryUsefulFuncを読み込んで使用してしまいました。この瞬間にまずい循環参照が発生します。具体的には、index.mjsbaz.mjsutils.mjsbar.mjsutils.mjsという循環参照の発生により、baz.mjsよりも先にutils.mjsが実行されます。その結果、utils.mjsが実行された段階でbaz.mjsからエクスポートされているveryUsefulFuncはTDZ下にあるため、utils.mjs内で以下のエラーが発生してしまいます。

ReferenceError: Cannot access 'veryUsefulFunc' before initialization

さらに、Webフロントエンド状況が悪化することがあります。フロントエンドではいまだにES5へのトランスパイルが行われることが珍しくなく、その場合constvarに変換されて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のimportexport文の挙動を「何をエクスポートしているのか」という点を中心に解説しました。とくに、export文は常に変数をエクスポートしており、importはその変数へのエイリアスを作るのだという点が重要です(export defaultの場合は変数が暗黙に作成されていましたが)。

この挙動は循環参照が発生したときでもなるべくいい感じに動くするようにするためのものですが、その恩恵を得るためには記事で扱ったようなアンチパターンを避ける必要があります。

また、実際にモジュールを使って書かれたコードを処理するのに現在広く使われているwebpackを例にとり、この記事で説明したような挙動が再現されていることを確かめました。

さらに詳しく知りたい方へ

筆者による以下の記事では、top-level awaitという新たな要素がモジュールシステムに与える影響について解説しています。また、この記事の後半ではECMAScript仕様書を読みながらモジュールの挙動を追っていきます。

モジュールの理解をさらに深いものにしたい方はこちらの記事もぜひご覧ください。


  1. エイリアスという言葉を用いた説明は分かりやすいので使っていますが、公式の用語ではないので注意してください。仕様書上の定義は、最初の述べた「参照されるとインポート元の変数の値を返す変数」というものです。 

  2. 実際にはダウンロードサイズの削減を目的として成果物を複数ファイルに分割したりしますが、それは今回の話題とは無関係であることがお分かりだと思います。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2020年から始めるAzure Cosmos DB - JavaScript SDK (SQL API)を見てみる (Part.2)

th.jpeg

この記事について

本記事は、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.ts
public 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.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);
  }

コンストラクタの第一引数に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);
  }

コンストラクタのパラメータプロパティ宣言を除くと、ContainersUsersのクラスを生成する処理があります。
実際に、この 2 つのコンストラクタの中身をみていきます。

Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/client/Container/Containers.ts
export class Containers {
  constructor(public readonly database: Database, private readonly clientContext: ClientContext) {}
}
Azure/azure-sdk-for-js/sdk/cosmosdb/cosmos/src/client/User/Users.ts
export 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.js
var 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は今後フロントエンドでスタンダードなフレームワークになるのかなぁと思っているのだけど、特にこだわりが無いのであれば、導入を検討しても良いんじゃないかなぁと思う。(むやみに新しい技術に飛びつくのは節操ないけど)。ある程度モダンな技術を採用しておくと採用の面でもプラスに働く可能性は高いですし、在籍しているエンジニアのスキルの向上にもなりそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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らしい。詳細は触れない

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。を格下目線から批判してみた。

はじめに

高階関数を書いたら、中級者になれた気がした。という記事を読んだのち、高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。まで読んだところ、非常にもやもやする部分があったので批判をしてみる、という趣旨の記事になります。
非情に底辺(某記事で言う格下目線)の内容になっていますので、こういう場所もあるんだというネタとしてお読みください。

結論

高階関数にする前のべた書きコードが要件を満たせて安全なほぼ最適解!

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ピクセルにしてその他いろいろな動作をする関数」が求められているのです!

つまりコールバック関数を使うメリットがないので関数の外で書けばいいだけと返しても「要件通り作れ!」と言われるだけではないでしょうか。
顧客が求めているのは要件通りの関数です。

考察

機能の実装ではなく関数を作って納品するのは、おそらく関数を作る人と使う人は別なのでしょう。

同じ画面を触っているよその会社から「○○して××したら△△になる機能を実装しろって言われたんだけど、うちの編集可能範囲(同一ソース内でも飛び地になっていたりする)超えてる部分あるからそこ実装してくれない?こういう引数でこういう動作」のような案件が入ってきたのではないでしょうか。

様々な会社が現在進行形で一つのシステムを作成している時によくある「普通に書けば解決するのに別会社の担当だから(運用的に)そのオブジェクトに触れない状態」です。中途半端ににわか知識持ってる上仕切りたがりで人の話を全く聞かない人が上にいるとてもよくある現場になります。

コールバックで全部任せるのは危険では?

かと言ってコールバックの形がいいとも言えません。
関数を使用するのは全く知らない別の人なので、どんな処理がされるか全くわかりません。
そしてその関数が使われる場所が自社の担当だった場合、コールバック内でどんなことをされても正常に動くように実装しなければいけなくなります。
変なコールバックを渡されたせいでも「バグが起こったのはあなたの関数内でしょ?」と言われて泣きながら修正することになります。なりました。

その点引数と実装を追加していく形なら、関数の想定していること以外は基本的に発生することはなく、誰にも怒られない要件通りの綺麗なコードになります。見知らぬ他人が要素を勝手に削除してバグったりしません。

なので

言われたことは言われたとおりに実装し、関数に要件が追加されたらその都度言われたとおりに引数を追加して言われたとおりの処理を書くのが安全で確実ではないかと思います。
身を守るために自分の知らない人が好き勝手出来るような関数はなるべく作らないようにしましょう。

というのが通して読んで私が至った結論でした。

皆さんがこんな結論に至るプログラマ(と自称したら怒られそうですね)にならないことを祈ります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.js
require('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.js
const Dotenv = require('dotenv-webpack');

module.exports = {
  plugins: [
    // { systemvars: true } を設定するとシステム環境変数も読み込まれるようになる
    new Dotenv({ systemvars: true }),
  ],
};
  • index.jsを修正します
    • dotenvはビルド時に適用されるため設定が消えてスッキリしました
index.js
const 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回していきましょう!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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"と書かれた部分がページのトップにくるよう位置を指定できるのです。

なかなかのツワモノです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブロックチェーンでリモート承認してみる(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,bob
nem = 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のエンドポイントはhttpwsに置き換えたものになります。
listenerは何も通信することが無いと1分ほどで切断されてしまいます。
newBlock()を実行してブロック生成ごとに通信が走るように設定しておきましょう。

申請者アカウントの作成と準備

アカウントの作成と蛇口からの入金、残高確認を申請者タブで実行します。

alice
alice = 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]));

コンソールに申請者のアドレスと蛇口サイトのリンクが出力されます。

承認者アカウントの作成と準備

アカウントの作成と蛇口からの入金、残高確認を承認者タブで実行します。

bob
bob = 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からのトランザクションを発見次第、自動承認する動きにしてみます。

bob
aliceAddress = 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機能を使います。

承認確認

最後に承認者から承認された場合の確認をあらかじめ記述しておきます。

alice
bobAddress = 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")
});

申請

それでは申請を行ってみましょう。

alice
tx = 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エンジンの真骨頂となる機能満載でのリモート承認にチャレンジです!お楽しみに。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む