20200210のJavaScriptに関する記事は29件です。

divに対してconsole.logのようなことをする

CodePen で出力結果を貼るとき、コンソールの代わりに div に出力したかったので簡易的に実装しました。簡易的なので %d などのフォーマットは利用できません。

See the Pen output for <div> by 七誌 (@7shi) on CodePen.

コード

出力関係のコードを JavaScript 側に入れたくなかったので、HTML 側に入れました。

HTML
<!-- CC0 http://creativecommons.org/publicdomain/zero/1.0/ -->
<div id="result"></div>
<script>
  function log() {
    let str = "";
    for (let i = 0; i < arguments.length; i++) {
      if (i > 0) str += " ";
      let arg = arguments[i];
      if (typeof arg == "string") str += arg;
      else str += JSON.stringify(arg);
    }
    result.appendChild(document.createTextNode(str));
    result.appendChild(document.createElement("br"));
  }
</script>

console.log() を置き換えてはいないので、log() として使います。

log("hello");

文字列化

単純に .toString() とするだけでは配列や連想配列がうまく出力されません。

ブラウザのコンソール
> [1,2,[3,4]].toString()
"1,2,3,4"
> ({a:1, b:2}).toString()
"[object Object]"

.toSource() というのを見掛けましたが、残念ながら Firefox の独自仕様でした。

Firefoxのコンソール
> [1,2,[3,4]].toSource()
"[1, 2, [3, 4]]"
> ({a:1, b:2}).toSource()
"({a:1, b:2})"

JSON.stringify() で文字列化するのが手軽で良いようです。

ブラウザのコンソール
JSON.stringify([1,2,[3,4]])
"[1,2,[3,4]]"
JSON.stringify({a:1, b:2})
"{\"a\":1,\"b\":2}"

参考

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

初心者がJavaScriptでAtCoder(初参戦)【AtCoder Beginner Contest 154】

はじめに

  • プログラミング歴半年の素人が書いています。
  • 不正確な内容、間違いがあったら申し訳ございません。

Hello, 競プロ!

初めて競技プログラミングなるものに挑戦してみます。
言語はJavaScriptです。調べたところあまりJSでAtcoderする人はいないようですね。今回参戦したコンテストの提出状況をみても、JSの提出は数える程でした。

それでも、初心者がやってみる分には言語の差なんて大したことないと思いますので、JSで今後もやって行こうと思っています。

なにより、私のように初心者の方にとってはJSはとっつきやすくていいのではないでしょうか。

今回の結果

AtCoderのコンテストでは、難易度別に複数の問題が出題されます。
今回参加した【AtCoder Beginner Contest 154】では、A問題〜F問題まで6問出題されました。

今回、私が正解できたのは

  • A問題
  • B問題
  • C問題

の3つでした。

ABC154A - Remaining Balls

A問題は初歩中の初歩。与えられた文字列が一致しているか調べるだけでした。

"use strict";
const main = arg => {

    const input = arg.trim().split("\n");
    const strngs = input[0].split(" ");
    const A = Math.floor(input[1].split(" ")[0], 10);
    const B = Math.floor(input[1].split(" ")[1], 10);
    const U = input[2];

    if ( U === strngs[0]) {
        console.log((A - 1), B);
    } else {
        console.log(A, (B - 1));
    }
}

main(require('fs').readFileSync('/dev/stdin', 'utf8'));  

ABC154B - I miss you...

与えられた文字列の文字数分、'x'を出力するだけのシンプルな問題ですが、なぜかWA(不正解)に。
調べたところ、標準入力で受け取った文字列には、見えないスペースや改行が含まれている場合もあるとのこと。

入力された文字列の長さを.lengthで取得するまえに.trim()処理することで、ACになりました。

"use strict";
const main = arg => {

    const inputLength = arg.trim().length; // .trim() する!
    const letter = "x";
    const answer = letter.repeat(inputLength).trim().replace(/\r?\n/g, '');

    console.log(answer);
}

main(require('fs').readFileSync('/dev/stdin', 'utf8'));  

ABC154C - Distinct or Not

標準入力として与えられる整数列に、重複する値があるかどうかを調べる。

配列をSetオブジェクト化することで、重複を取り除く処理ができる。
Setオブジェクトをもう一度Arrayオブジェクトに戻し、要素数が変わったかどうかを調べた。

"use strict";
const main = arg => {

    const input = arg.trim().split("\n");
    const nums = input[1].split(" ");

    const set = new Set(nums);
    const uniqueNums = Array.from(set);

    nums.length === uniqueNums.length ? console.log("YES") : console.log("NO");
}

main(require('fs').readFileSync('/dev/stdin', 'utf8'));  

ABC154D - Dice in Line

D問題は不正解。
動作するプログラムは書けたが、TLE(Time Limit Exceeded:処理時間かかりすぎエラー)だった。

コードが動くかどうかだけでなく、どうやって処理を減らして効率よく実装するかが重要らしい。

このD問題は、累積和というテクニックをつかうことで処理を大幅に減らすことができると解説に書いてあった。コンテスト終了後に、ACするプログラムを作り上げた。

  • 「配列内のある区間」を特定する問題では、累積和が必要になることがある

  • それと、何故かいままで文字列を整数に変換するときにMath.floor()を使っていたけどparceInt()のほうが高速らしい。

  • メモリを使わないほうが高速に動くっぽい。つまり、コンピュータにたくさん記憶しないようにするってことだろうか?

"use strict";
const main = arg => {

    const input = arg.trim().split("\n");
    const N = parseInt(input[0].split(" ")[0],10);
    const K = parseInt(input[0].split(" ")[1],10);
    const pArray = input[1].split(" ").map(n => parseInt(n,10));

    // サイコロの期待値を得る関数
    const kitaichi = i => (i + 1) / 2;
    const kitaichiArray = pArray.map(p => kitaichi(p));

    // 累積和を作成する関数
    const cumulativeSum = (array) => {
    let arr = [0];
    array.reduce((a, i) => {
        a === 0 ? arr.push(i) : arr.push(a + i);
        return a + i
    }, 0)
    return arr;
    }
    // 期待値の累積和配列を作成
    const cumulativeSumArray = cumulativeSum(kitaichiArray);

    // 累積和配列の要素をループしながら最も大きい組を見つける
    let answer = 0;
    for (let i=K; i<N+1; i++) {
        if (answer < (cumulativeSumArray[i] - cumulativeSumArray[i-K])) {
            answer = (cumulativeSumArray[i] - cumulativeSumArray[i-K]);
        } else {
            continue;
        }
    }
    console.log(answer);
}

main(require('fs').readFileSync('/dev/stdin', 'utf8'));  

今回学んだこと

  • .trim()でデータを綺麗にする

  • 「配列内のある区間」を特定する問題では、累積和が必要になることがある

  • 何故かいままで文字列を整数に変換するときにMath.floor()を使っていたけどparseInt()のほうが高速らしい。

  • 処理を減らすことが、ACのカギ!

なんかメモリをうまく使えとどこかに書いてあったけど、それって例えばどんなことなんでしょう...?

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

コードの難読化について

コードの難読化について

暗号化との違い

暗号化

  • ソースコードを完全に読めないものにする

暗号化

  • ソースコードを読みにくくする

難読化の目的

  • ソースコードを厳重に管理したい
  • 盗難防止のためにソースコードの公開を避けたい
  • コードの目的の隠蔽や、リバースエンジニアリングの阻止
  • 頭の体操
  • など

最後に

近年では、多くのWebサービスのJavaScriptやCSSのソースコードで難読化がされています。
フロントエンドでは、JavascriptやCSSを難読化して管理するためにwebpackを用いた方法がよく使われています。

また、「難読化」という言葉も、新人のエンジニアでは聞いたことのない方もいるのではないでしょうか。
実際に使われている場面の多いものだと思うので、難読化という言葉と、どうしてそれが使われるのかをぜひ覚えてみてください。

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

【JavaScript】編集内容が消えて困るページに離脱前の確認をはさむ

Web ページで何かを編集していて、間違ってブラウザバックしてしまったりタブを閉じてしまうと「あっ」てなりますよね。こんなとき、いくつかのサイトでは以下:arrow_down:のような確認ダイアログが出てきて救われることがあります(Qiita の投稿画面でも出てきますね)。

このサイトを離れますか? 行なった変更が保存されない可能性があります。

この実装方法を紹介します。

コード

離脱前に確認ダイアログを出したいページに、以下の JavaScript を置いてください。

window.addEventListener('beforeunload', function (event) {
  event.preventDefault() // (1)
  event.returnValue = '' // (2)
})

コードを見るとわかりますが、表示するメッセージを指定している部分はありません。確認ダイアログのメッセージは、ブラウザ標準の固定メッセージから変更できないため注意してください1

解説

windowbeforeunload のイベントリスナを設定しています。このイベントリスナで特定の操作をすると、:arrow_up:のスクショのような確認ダイアログを出せます。「特定の操作」というのは、以下のどちらかです:

  • (1) イベントオブジェクトの preventDefault メソッドを呼ぶ。HTML の仕様ではこちらが正当らしいが、すべてのブラウザではサポートされていないらしい(参考文献[1]参照)。
  • (2) イベントオブジェクトの returnValue 属性に文字列をセットする。文字列は何でも良い(この例では空文字列に設定)。

上記のコードでは、いろいろなブラウザに対応するため両方の操作をやっています。

なお、この操作を行ったとしても、本当にダイアログが出るかどうかはブラウザに任せられているようです。たとえば Chrome では、フォームの入力値を変更していない場合はダイアログが出ませんでした。濫用を防ぐためでしょう。

参考文献


  1. 参考文献[2]に詳しいですが、かつてはイベントリスナの戻り値やreturnValueの設定値が確認ダイアログのメッセージとして使われるブラウザもあったようです。(2)のパターンで確認ダイアログが出る実装になっているのはその名残のようですね。 

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

Jestでカスタムイベントのdispatchを検証する方法

Jestでカスタムイベントのdispatchを検証する方法です。
調べてあまり情報出てこなかったのでメモ。

テスト対象

単純にセレクタと、カスタムイベント名を受け取ってイベントを発行する関数を対象とします。

trigger-custom-event.ts
export const triggerCustomEvent = (
  selector: string,
  eventName: string
): void => {
  const target = document.querySelector(selector);
  target?.dispatchEvent(new Event(eventName));
};

テストコード

dispachEventで指定のカスタムイベントが発行されたことを検証するので、対象のDOMのdispachEventをモックに差し替えます。
そして、mockのcallsで発行回数、発行イベント名を検証します。

trigger-custom-event.test.ts
import { triggerCustomEvent } from "../src/trigger-custom-event";

describe("custom actions", () => {
  let spyDispatchEvent: jest.Mock;

  beforeEach(() => {
    spyDispatchEvent = jest.fn();
    const target = document.querySelector("body");
    target!.dispatchEvent = spyDispatchEvent;
  });

  it("カスタムイベントが発行される", () => {
    triggerCustomEvent("body", "customEventName");
    expect(spyDispatchEvent.mock.calls.length).toBe(1);
    expect(spyDispatchEvent.mock.calls[0][0].type).toBe("customEventName");
  });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】手軽に綺麗な「要素を矢印で繋いだグラフ」を描く方法(画像付き)

概要

JavaScriptでグラフ(ノードとエッジの集合の方)を作りたい。
スクリーンショット 2020-02-10 16.42.15.png

JavaScriptでグラフを描画できるd3-graphvizの使い方や、色変更などできることを調査した。

基本の使い方

1.インストールする
簡単な方法はnpm install d3-graphviz(yarnでも可)
自分でビルドする場合は以下のコードを実行する。

$ git clone https://github.com/magjac/d3-graphviz.git
$ cd d3-graphviz
$ npm install
$ npm run build

また、unpkg.comを使うこともできる。その際、d3.jsviz.jsが必要であるためこれらも読み込む。

2.htmlファイルを作成する
必要なjsファイルを読み込み、グラフを描画する要素を用意する。
<script>タグ内にグラフ描画スクリプトを記述する。
renderDot()の引数に描画したいグラフを記述している。
最も簡単なデモのコードを以下に示す。このコードでは以下のようなグラフが描画される。

index.html
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/viz.js@1.8.1/viz.js" type="javascript/worker"></script>
<script src="https://unpkg.com/d3-graphviz@2.6.1/build/d3-graphviz.js"></script>
<div id="graph" style="text-align: center;"></div>
<script>

d3.select("#graph").graphviz()
    .fade(false)
    .renderDot('digraph  {a -> b}');

</script>

スクリーンショット 2020-01-21 13.03.11.png

以降、グラフ描画スクリプトのみ記述する。

ノードをカスタマイズする

何も指定せずにグラフを描画すると、上の画像のようなノードになる。

形を変更する

ノードの形を変えたい場合は、shapeを指定する。
グラフのすべてのノードに適用することも、特定のノードのみに適用することも可能。

d3.select("#graph").graphviz()
    .renderDot(`digraph  {
        node [shape="polygon"] // グラフのすべてのノードの形を変える場合
        a [shape="polygon"] // ノード「a」の形を変える場合
        a -> b
    }`);

スクリーンショット 2020-01-21 13.15.21.pngスクリーンショット 2020-01-21 13.15.42.png
d3-graphviz Shape Tweening Demoに様々な形の例がある。

背景の色・透明度を変更する

ノードの背景色や透明度を変更する場合はfillcolorを指定する。また、ノードにstyle="filled"を指定しておく(これを指定するとデフォルトのノード背景色はグレーになる)。
透明度は不透明度指定でRGBの後に記述する。ex. #FFFFFF80 // 半透明の白

d3.select("#graph").graphviz()
    .renderDot(`digraph  {
        node [style="filled" fillcolor="#FF000080"] // グラフのすべてのノードの背景色を変える場合
        a [fillcolor="#0000FF80"] // ノード「a」の背景色を変える場合
        b [fillcolor="#FFFF00"] // ノードごとに別の色にすることもできる
        a -> b
    }`);

スクリーンショット 2020-01-21 13.24.52.pngスクリーンショット 2020-01-21 13.25.27.pngスクリーンショット 2020-01-21 13.26.20.png

枠線の色・透明度・太さを変更する

ノードの枠線の色や透明度を変更する場合はcolorを指定する。透明度指定の方法は背景色と同じである。
また、枠線を太くする場合はstyle="bold"を指定する。

d3.select("#graph").graphviz()
    .renderDot(`digraph  {
        node [color="#FF000080" style="bold"] // グラフのすべてのノードの枠線の色・太さを変える場合。複数指定する場合はスペース区切りで記述する
        a [color="#0000FF80"] // ノード「a」の枠線の色を変える場合
        b [color="#FFFF00"] // ノードごとに別の色にすることもできる
        a -> b
    }`);

スクリーンショット 2020-01-21 13.33.43.pngスクリーンショット 2020-01-21 13.30.39.pngスクリーンショット 2020-01-21 13.30.52.png

文字の色・透明度・太さを変更する

透明度なしで文字色を変更する場合はfontcolorを指定する。透明度や太さを変更する場合は.attributer()で変更できる。

d3.select("#graph").graphviz()
    .attributer(function(d) { // 透明度・太さを変更する場合
        if (d.tag == "text") { // <text>タグ
            if (d.parent.attributes.id == "node-a") {
                d3.select(this).attr("font-weight", "bold") // 太字にする
            } else if (d.parent.attributes.id == "node-b") {
                d.attributes.fill = "#00000080" // 半透明の黒にする
            }
        }
    })
    .renderDot(`digraph  {
        node [fontcolor="#FF0000"] // 透明度を指定しない場合の文字色変更
        a [id="node-a"] // 要素のidを指定する
        b [id="node-b"]
        a -> b
    }`);

スクリーンショット 2020-01-21 13.38.52.pngスクリーンショット 2020-01-21 13.39.38.png

また、<ellipse>タグの時にfillに色を指定するとノードの色の変更もできる。

エッジをカスタマイズする

ノードと同様にエッジの色や太さを変更することもできる。
色・透明度の変更: colorを指定する。透明度指定はノードの場合と同じである。
太さの変更: style="bold"で太くできる。

d3.select("#graph").graphviz()
    .renderDot(`digraph  {
        a -> b [color="#FF000080"] // 色・透明度指定
        a -> c [style="bold"] // 太くする
    }`);

スクリーンショット 2020-01-21 13.44.53.png

その他

アニメーションさせる

d3-graphvizの機能でグラフをアニメーションをさせることもできる。
デモを参考にグラフを切り替えるコードを簡易的に記述したものを以下に示す。アニメーションさせたいグラフを配列で用意し、順番に描画している。
以下のコードではdelay等を指定していないため、点滅のようになる。

var dotIndex = 0;
var graphviz = d3.select("#graph").graphviz()
    .on("initEnd", render);

function render() {
    var dotLines = dots[dotIndex];
    var dot = dotLines.join('');
    graphviz
        .renderDot(dot)
        .on("end", function () {
            dotIndex = (dotIndex + 1) % dots.length;
            render();
        });
}

var dots = [
    [
        'digraph  {',
        '    a -> b',
        '}'
    ],
    [
        'digraph  {',
        '    a -> b [color="#FF0000"]',
        '}'
    ],
    [
        'digraph  {',
        '    a -> b [color="#0000FF"]',
        '}'
    ],
];

ログを出力する

.logEvents(true)でコンソールにログを出力することができる(デフォルトがtrue)。
d3-graphvizの各イベントについて、1つ前のイベントからの経過時間・スタートからの経過時間を知ることができる。

d3.select("#graph").graphviz()
    .logEvents(true)
    .renderDot('digraph  {a -> b}');

スクリーンショット 2020-02-10 15.03.15.png

まとめ

  • d3-graphvizを用いてJavaScriptで簡単にグラフを作成・表示できる。
  • ノード・エッジ共に簡単に色や透明度などを変更できる。
  • 複数のグラフをアニメーションで遷移させられることがわかった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】プラグイン「Jump.js」のコードを読んでみた 

はじめに

スキルアップのためにGithubに公開されているプラグインのコードを読み、そこで学んだことをまとめました。
想定する読者は基本的な構文などを学んだけど次に学習する内容や学習方法に悩んでいる初学者の方になります。
実践的なコードの書き方や学習方法、次に作るもの等の参考にしていただければ幸いです。

読んだコードはcallmecavsさんの「Jump.js」というプラグインで、webサイトでスクロールアニメーションを実現できます。

基本的な使い方

基本的な使い方から見ていきます。
第一引数にスクロールのターゲットになる要素を指定することで、その要素の位置までスクロールさせることができます。指定する要素はノードかセレクタを渡すことができ、セレクタから複数の要素が見つかった場合は1つ目の要素がターゲットになります。また、数値を渡すことで現在のスクロール位置から相対の位置にスクロールします。

Jump(node)
Jump('.target')
Jump(100)

  
第二引数にはオプションを指定することができ、以下のように記述します。

Jump('.target', {
  duration: 1000, // スクロール時間
  offset: 0, // ターゲット要素の位置にoffsetの値を加えた数値がスクロール終了位置になります
  callback: undefined, // スクロール終了後に呼び出されるコールバック関数
  easing: easeInOutQuad, // スクロールに使用されるイージング関数
  a11y: false // trueを指定するとスクロール終了後にターゲット要素のfocus属性をtrueにします
})

  
使用例:
ページ内リンクがクリックされた時にhref属性の値をターゲットに指定してスクロールさせる

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Test Jump.js</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jump.js/1.0.2/jump.js"></script>

</head>
<body>
  <a href="#target">trigger</a>
  <div id="target" style="margin-top: 600px; margin-bottom: 2000px;">target</div>
  <script src="script.js"></script>
</body>
</html>

script.js

window.addEventListener('load', function() {
  const trigger = document.querySelectorAll('a[href^="#"]')
  for (let i = 0; i < trigger.length; i++) {
    trigger[i].addEventListener('click', function(e) {
      const targt = e.target.getAttribute('href')
      Jump(target)
    })
  }
})

全体的なコードの内容

まずはざっくりとコードの内容を見ていきます。
関数内の具体的な処理は省略しています。

import easeInOutQuad from './easing.js'

const jumper = () => {
  // private variable cache
  // no variables are created during a jump, preventing memory leaks

  let element         // element to scroll to                   (node)

  let start           // where scroll starts                    (px)
  let stop            // where scroll stops                     (px)

  let offset          // adjustment from the stop position      (px)
  let easing          // easing function                        (function)
  let a11y            // accessibility support flag             (boolean)

  let distance        // distance of scroll                     (px)
  let duration        // scroll duration                        (ms)

  let timeStart       // time scroll started                    (ms)
  let timeElapsed     // time spent scrolling thus far          (ms)

  let next            // next scroll position                   (px)

  let callback        // to call when done scrolling            (function)

  // scroll position helper
  function location () {/* ・・(中略)・・*/}

  // element offset helper
  function top (element) {/* ・・(中略)・・*/}

  // rAF loop helper
  function loop (){/* ・・(中略)・・*/}

  // scroll finished helper
  function done (){/* ・・(中略)・・*/}

  // API
  function jump (target, options = {}) {/* ・・(中略)・・*/}

  // expose only the jump method
  return jump
}

// export singleton
const singleton = jumper()

export default singleton

いろいろと省略しましたが、大まかな内容としてはjumperという関数内で
1.処理に必要な変数を宣言
2.関数jump内で使用するヘルパー関数を定義
3.ユーザーが実際に使うことになるメインの関数jumpを定義
といったことをしています。
そして、最後に関数jumperを実行して、返り値の関数jumpをエクスポートしています。

イージング関数

はじめにインポートされているイージング関数は滑らかなアニメーションを実現するために使用されます。
以下を引数にあてることで、経過時間からアニメーション位置を求めることができます。
・アニメーションの経過時間
・アニメーションさせたい値の初期値
・アニメーションさせたい値が初期値から変動する値
・アニメーションにかかる時間

変数宣言

変数の宣言の箇所ではコメントに
no variables are created during a jump, preventing memory leaks
「ジャンプ中に変数が作成されないため、メモリリークが防止されます」と書かれています。
繰り返し呼び出される処理の外に変数を定義することで、処理の度に変数が宣言されないように工夫されていました。
また、変数ごとにコメントが書かれていてますが、用途や単位が整列されていて読みやすく、コメントの書き方として非常に参考になりました。

ヘルパー関数

具体的な処理の内容にはここでは触れず、コメントから大まかな用途を把握します。

location

アニメーション開始時点のスクロール位置を取得

top

スクロール目的地となる要素のトップ位置を取得

loop

アニメーション時に繰り返し実行される処理

done

スクロール終了時に実行される処理

エクスポート

最後のほうでは、jumperを実行して返り値のjumpオブジェクトをsingletonという変数に代入してエクスポートしています。jumpをそのままエクスポートせずにわざわざjumperで処理を包んでいるのは、変数やヘルパー関数をスコープ内に閉じ込め、jumpだけを返すことで、余計な変数や関数を外部からアクセスできないようにするためです。
シングルトンという聞きなれない言葉が変数名で使用されていますが、Wikpediaでは以下のように説明されています。
そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのインスタンスが1つしか生成されないことを保証するデザインパターン
つまり、使用するときにnew Jump()というような、新しくインスタンスを作成する記述が不要な関数です。
以下の記事で詳しい実装例などが説明されていて参考になります。
【JSでデザインパターン】シングルトン編

jump関数

処理の流れを把握するため、処理のメインとなるjump関数を見ていきます。
おおまかな流れとしては以下になります。

  1. オプションに値が設定されていない項目にはデフォルト値を設定
  2. location関数でスタート位置を取得してstartに代入
  3. 第一引数のtargetから終了位置を取得してstopに代入
  4. スタート位置から終了位置までの距離を取得してdistanceに代入
  5. loop関数を実行してスクロールアニメーションを開始
  function jump (target, options = {}) {
    // resolve options, or use defaults
    duration = options.duration || 1000
    offset = options.offset || 0
    callback = options.callback                       // "undefined" is a suitable default, and won't be called
    easing = options.easing || easeInOutQuad
    a11y = options.a11y || false

    // cache starting position
    start = location()

    // resolve target
    switch (typeof target) {
      // scroll from current position
      case 'number':
        element = undefined           // no element to scroll to
        a11y = false                  // make sure accessibility is off
        stop = start + target
        break

      // scroll to element (node)
      // bounding rect is relative to the viewport
      case 'object':
        element = target
        stop = top(element)
        break

      // scroll to element (selector)
      // bounding rect is relative to the viewport
      case 'string':
        element = document.querySelector(target)
        stop = top(element)
        break
    }

    // resolve scroll distance, accounting for offset
    distance = stop - start + offset

    // resolve duration
    switch (typeof options.duration) {
      // number in ms
      case 'number':
        duration = options.duration
        break

      // function passed the distance of the scroll
      case 'function':
        duration = options.duration(distance)
        break
    }

    // start the loop
    window.requestAnimationFrame(loop)
  }

jump関数の第二引数にoptions = {}と記述されています。
これは引数が指定されなかった場合に適応されるデフォルト値を設定しています。
第二引数が指定されなかった場合は空のオブジェクトがoptionsに代入されます。

ヘルパー関数

ヘルパー関数の具体的な処理を一つずつ見ていきます。

location

現在のスクロール位置を取得する関数になります。

  // scroll position helper

  function location () {
    return window.scrollY || window.pageYOffset
  }

window.scrollYwindow.pageYOffsetはどちらも現在のスクロール位置を示します。
window.scrollYがブラウザで対応されていない場合はwindow.pageYOffsetが返ります。
それぞれのブラウザの対応状況ですが、window.scrollYはIE以外のブラウザで対応されて、widnow.pageYOffsetはIEを含む全てのブラウザで対応されています。
pageYOffsetのほうがscrollYより対応範囲が広く、わざわざscrollYを優先して使用する理由については調べても明確な答えが出てきませんでしたが、こちらのサイトhttps://codeday.me/jp/qa/20190204/216459.htmlでは、「scrollYpageYOffsetのエイリアスであり、読みやすさを優先してscrollYが使用されているのでは」と説明されていました。

top

引数で指定した要素のトップの位置を取得します。

  // element offset helper

  function top (element) {
    return element.getBoundingClientRect().top + start
  }

startはスクロール開始時のスクロール位置が代入されます。
getBoundingRectで要素のビューポートからの相対位置を取得し、windowのスクロール量を加算することで、要素の位置をwindowトップからの絶対位置で取得することができます。

loop

スクロールアニメーション時に繰り返し呼び出される関数です。

  function loop (timeCurrent) {
    // store time scroll started, if not started already
    if (!timeStart) {
      timeStart = timeCurrent
    }

    // determine time spent scrolling so far
    timeElapsed = timeCurrent - timeStart

    // calculate next scroll position
    next = easing(timeElapsed, start, distance, duration)

    // scroll to it
    window.scrollTo(0, next)

    // check progress
    timeElapsed < duration
      ? window.requestAnimationFrame(loop)       // continue scroll loop
      : done()                                   // scrolling is done
  }

スクロールアニメーション時にrequestAnimationFrameのコールバック関数として繰り返し実行されます。
アニメーション開始時からの経過時間でスクロール位置を求めてスクロールさせる処理を、requestAnimationFrameで高速に繰り返すことで滑らかなアニメーションを実現しています。

引数にはミリ秒単位で計測された現在時刻の値を受け取ります。
初回実行時に受け取った現在時刻をグローバル変数timeStartに代入して、二回目以降に呼び出された際の時刻と比較することでアニメーションの経過時間を求めることができます。経過時間はtimeElapsedという変数に代入されています。

その後、イージング関数でスクロール位置を求めてスクロールさせ、アニメーション経過時間がdurationに設定した時間を経過していれば後述するdone関数を呼び出し、経過していなければ再度loop関数を実行します。

done

スクロール終了後に呼び出される関数です。

  function done () {
    // account for rAF time rounding inaccuracies
    window.scrollTo(0, start + distance)

    // if scrolling to an element, and accessibility is enabled
    if (element && a11y) {
      // add tabindex indicating programmatic focus
      element.setAttribute('tabindex', '-1')

      // focus the element
      element.focus()
    }

    // if it exists, fire the callback
    if (typeof callback === 'function') {
      callback()
    }

    // reset time for next jump
    timeStart = false
  }

はじめにWindow.scrollToでスクロール終了位置までスクロールしています。
これは、requestAnimationFrameで繰り返し実行する間隔が処理する端末の処理能力などに依存していて、ぴったしdurationの値とおなじタイミングでスクロールアニメーションが終了するとは限らず、微妙にスクロール終了位置がずれてしまうからです。
そのずれを修正するために改めてターゲット要素の位置を指定してスクロールさせています。

例:durationの値が5000の場合
N回目のloop実行
 timeElapsedの値 4999 → 再度loopを実行
N+1回目のloop実行
 timeElapsedの値 5004 → doneを実行 
結果4ミリ秒分の終了位置がずれる

次に、オプションでa11yをtrueに設定した場合の処理が記述されています。アクセシビリティを向上させるためにスクロール後、ターゲット要素をフォーカスしています。
要素にtabindex属性を指定することで、input要素やtextarea以外の要素でもフォーカスできるようになります。0以上の値をしていするとタブキーで順番にフォーカスされ、マイナスの値を指定するとタブキーではフォーカスされなくます。

次に、オプションでコールバック関数を設定して場合は、その関数を実行しています。
コールバック関数を使用することで、Jump.jsの利用者はアニメーション終了時に様々な処理を差し込むことができます。

最後に、次にJump関数が呼びだれた時のためにtimeStartを初期化して終了です。

まとめ

変数名やコメントのつけ方、ブラウザ対応の方法など、書籍だけでは習得しづらい知見が多々あり勉強になりました。
オープンソースのコードは数えきれないほどあるので、これからも理解できるところ勉強していこうと思います。
勉強法に悩んでいる方にもおすすめです!

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

【GAS】Google Custom Search APIを使って検索順位のチェックツールを作る

普段SEOのコンサルを行う中で、「検索順位を定点観測できるツールは何を使えば良いか?」という質問をよくいただきます。

いつもは無難にGRCやSERPOSCOPEなどのツールをオススメしていますが、それらは有料であったり、順位取得のエラーが頻繁に出たりと気軽に使えるかどうかと言えば、必ずしもそうではありません。

もっと気軽に検索順位のチェックをできないかということで、「Google Apps Script(以下GAS)」と「Google Custom Search API」を使ってスプレッドシートに日次で書き込む方法をご紹介します。

※以下は完成形のイメージです。
スクリーンショット 2020-02-10 1.55.04.png

システム概要

だいぶシンプルですが、作成するシステムの全体像になります。
スクリーンショット 2020-02-10 15.58.22.png

要件

  • 検索結果50件までを取得し、その中に該当のドメインが含まれているかどうかチェックできる
  • 該当のドメインが含まれている場合はその順位の値を返し、含まれない場合は0を返す
  • 検索したいキーワードを追加できるようにし、同時に複数のキーワードの検索結果取得と順位チェックを行う
  • 1日1回、自動で実行できるように

Google Custom Search APIとは

「Google Custom Search API」はGoogleカスタム検索から実際の検索結果を取得できるAPIです。APIを叩くことでWeb検索または 画像検索の結果をJSON形式で取得できます。

検索結果を取得できるようにする

それでは実際に導入の流れを説明していきます。
まずはAPIを使える状態にして、検索結果を取得できるところまでを行いましょう。

必要なもの

Googleアカウントを持っており、かつログインしているという前提で、以下の準備をします。

  • Custom Search APIのAPIキー(認証情報)の作成
  • Custom Search APIの有効化
  • Custom Search Engine(CSE)の作成

Custom Search APIのAPIキー(認証情報)の取得

Google Developer Console」もしくは「Google Cloud Platform」にアクセスしてください。
そこからGoogle APIの管理画面に移動します。
左メニューから「認証情報」を選択します。

スクリーンショット_2020-02-10_2_22_27.jpg

続いて右上「+認証情報を作成」→「APIキー」を選択。

スクリーンショット_2020-02-10_2_46_57.jpg

そうすると「APIキー」が作成されますので、大事に保管してください。
※必要に応じてキーに制限をかけると良いでしょう。

スクリーンショット_2020-02-10_2_50_08.jpg

Custom Search APIの有効化

上記のAPIキー取得に続いて、Google API管理画面のダッシュボードに戻ります。
そして右上ボタンから「APIとサービスを有効化」を選択

スクリーンショット_2020-02-10_2_22_27.jpg

Googleが提供するAPI ライブラリの一覧に移動しますので、その中から一番下にある「Custom Search API」を選択しましょう。

スクリーンショット_2020-02-10_2_29_12.jpg

ここで、APIを有効化します。
以上で「Custom Search API」の設定は終わりです。

Custom Search Engine(CSE)の作成

次に「Custom Search Engine(CSE)」を作成して、そのIDを取得します。
Custom Search Engine」にアクセスして、「検索エンジンの編集」から「追加」を選択します。

スクリーンショット_2020-02-10_3_00_14.jpg

「検索するサイト」を任意のURLで埋め(後で削除します)、検索エンジンの名前をさらに任意の名前にします。
そして「作成」を選択。

スクリーンショット_2020-02-10_3_03_19.jpg

するとカスタム検索エンジンが作成されますので、「コントロールパネル」から細かい設定を行っていきます。

スクリーンショット_2020-02-10_3_06_01.jpg

「コントロールパネル」を開き、まず「検索エンジンID」を大事に保管してください。
そして先ほど設定した「検索するサイト」のURLは必要無いので削除します。
最後に「ウェブ全体を検索」をオンの状態にします。

スクリーンショット_2020-02-10_3_14_13.jpg

以上で「Custom Search Engine」の設定は終わりです。
ここまで終えることで、APIが使えて検索結果を取得できる状態になりました。

リクエストを投げてみる

それでは、上記で作成したAPIキーと検索エンジンIDを使ってリクエストを飛ばしてみましょう。
※「Postman」などのAPIクライアント、もしくはブラウザでURLを直叩きするかで試してみてください。

すると指定したクエリの検索結果がJSONで返ってくるかと思います。

https://www.googleapis.com/customsearch/v1?key=**APIキー**&cx=**検索エンジンID**&q=**任意の検索クエリ**

各パラメータの詳細は以下になります。

Parameter name Value Description
key String APIキー
cx String カスタム検索エンジンID
q String 検索クエリ

他のパラメータについては、公式リファレンスを参照を参照。

GASで検索結果を取得し、順位をチェックする

GASの方の実装に移ります。

新規のスプレッドシートを開く

新しいスプレッドシートを開き、シートを次のような形式に変えます。
A列に検索したいキーワードを記入し、そのキーワードに対する順位がB列以降に日次で取得されていくようにします。

スクリーンショット_2020-02-10_15_36_23.jpg

スクリプトエディタ

続いてスプレッドシートメニュー「ツール」→「スクリプトエディタ」からスクリプトエディタを開き、コードを完成させます。

プロパティ変数を設定する

先に取得したAPIキーと検索エンジンID、検索するサイトのドメインを環境変数として設定します。
スクリプトエディタ上のメニュー「ファイル」→「プロジェクトのプロパティ」→「スクリプトのプロパティ」

スクリーンショット 2020-02-10 16.58.19.png

このようにGASにはプロパティ変数の機能があって、KEY・VALUEで設定しておくと、コード内から呼び出せて使えるので便利です。

コードを書く

プログラムの基となるコードをスクリプトエディタ上に書いていきます。

スクリーンショット 2020-02-10 16.05.16.png

基本的には、以下のコードをそのままコピペでご利用いただければ問題無いです。

///検索結果を取得
function searchResult(query,domain){
  var apiKey = PropertiesService.getScriptProperties().getProperty('apiKey');
  var searchId = PropertiesService.getScriptProperties().getProperty('searchId');
  var items = [];

  for (var i = 0; i < 5; i++){
    var start =(i*10)+1;
    var apiUrl = "https://www.googleapis.com/customsearch/v1?key="+apiKey+"&cx="+searchId+"&q="+query+"&start="+start;
    var apiOptions = {
      method : 'get'
    };
    var responseApi = UrlFetchApp.fetch(apiUrl, apiOptions);
    var responseJson = JSON.parse(responseApi.getContentText());
    for (var v = 0; v < 10; v++){
      items.push(responseJson["items"][v]["link"]);
    }
  }
  return rankloop(items,domain);
}

///順位をチェック
function rankloop(items,domain){
  var rank = 0;  
  for (var i = 0; i < items.length; i++){
    if(items[i].match(domain)){
      rank = i + 1;
      break;
    }
  }
  return rank;
}

///シートに順位を書き込む
function getRank() {
  var book = SpreadsheetApp.getActiveSpreadsheet();
  var sheetData = book.getSheetByName("シート1");
  var colQuery = 1;
  var rowStartData = 2;
  var rowEndData = sheetData.getDataRange().getLastRow();
  var columnEndData = sheetData.getDataRange().getLastColumn();
  var date =  Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/dd');

  sheetData.getRange(1, (columnEndData+1)).setValue(date);

  var domain = PropertiesService.getScriptProperties().getProperty('domain');
  var start = 1;

  for (var i = rowStartData; i <= rowEndData; i += 1) { 
    var query = sheetData.getRange(i, colQuery).getValue(); 
    var rank = searchResult(query,domain);
    sheetData.getRange(i,(columnEndData+1)).setValue(rank);
  }
}

これを保存すれば、もう完成です。あとは指定した日時でgetRank()関数を定期実行するようにすれば、先ほどのスプレッドシートのB列に順位が取得されてきます。

トリガーを設定

トリガー設定画面から「日付ベースのタイマー」でgetRank()関数を実行するように設定しておきましょう。

完成形

デイリーで定期実行され、自動で日付と順位が列ごとに追加されていきます。

スクリーンショット 2020-02-10 17.12.15.png

条件付き書式などで、ビジュアルをわかりやすくするのもアリでしょう。

またグラフ化して定点観測したい場合は、データスタジオ等のBIツールと連携させると良いでしょう。
そこはスプレッドシートを利用する大きな利点かなと思います。

問題点

問題点と言うほど大げさでは無いのですが、Custom Search APIには無料枠の制限があって、1日100クエリまでという制限があります。
1回の検索で1ページ10件までしか取得できませんので、今回の実装では50件(50位)までを対象としている為、1つのキーワードにつき5クエリを費やす形になります。
ですので、無料で順位チェックできるキーワードの数としては20までということになります。

もしそれ以上のキーワードの順位をチェックしたいということであれば、課金が必要です。

具体的にはこちら
Custom Search JSON API > pricing

課金による追加のリクエストは、1000クエリあたり5ドル、1日あたり最大10,000クエリがかかります。
キーワード数で換算すると200キーワードあたり5ドルです。(安いのか、、高いのか、、)

最後に

いかがでしたでしょうか。
20キーワードまでとはいえ、インストールといった煩わしさも無く、無料で手軽に順位チェックできるのは魅力的ですよね。

何千、何万というキーワードを観測する場合には不向きかもしれませんが、重要なキーワードだけに絞って運用するのであれば、これで全然問題無いかと思います。

運営するサイト・コンテンツの検索順位を把握することは、SEOを行う上でとても重要な作業です。
もし導入する検索順位のチェックツールが定まらないというお悩みがあるのであれば、この記事のやり方を参考にして、実際に取り入れてみてはいかがでしょうか。

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

[HowTo]Pay.jpを用いたクレジットカードの登録機能実装について

某スクールのチーム開発にてpay.jpを活用したクレジットカード登録と商品購入機能実装を担当させていただくことになりました!
色々と苦戦しましたがなんとか実装できましたので、以下にまとめてみたいと思います。
今回はクレジットカード登録機能実装までまとめており、商品購入機能に関しては、準備出来次第、別記事として投稿します!

そもそもPay.jpとは

シンプルなAPI・多彩な機能、分かりやすい料金形態でクレジットカード決済をかんたんに導入できる決済サービスです。手数料も比較的リーズナブル(2.59%〜)であることと、導入が簡単ということもあり、スタートアップ企業などに多く採用されているようです。

クレジットカード登録について

クレジットカード登録に関しては、カードの登録のフォーマットによってやり方が異なります。
- チェックアウト:pay.jp社にて用意されているフォーマットを利用する方法。
- カスタムフォーム:ご自身でフォーマットを作成し、そちらに準じてカード登録を行う方法。

今回は既に入力フォームを作成していたので、カスタムフォームにて実装を行うことにしました!

全体の流れ

1. クレジットカード番号などの情報登録:view
2. ”トークン”作成:Javascript
3. ”トークン”をキーとしてPayjpに顧客情報として登録:controller
4. 登録情報の確認:view+controller

今回の記事では、まず、Viewの実装内容を確認いただき、JSを用いたトークンの作成をご確認いただきます。
そして、最後にコントローラでそのトークンをキーとしてPayjpに顧客登録をする動きを確認いただければと思います。

1.クレジットカード番号などの情報登録:view

今回は以下のようなviewで登録画面を作ってます。

612316d11d112f7fb4f670ef83a440ec.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to root_path do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          支払い方法
          .single-header__progress__round--red
            .single-header__progress__round--red-long{ id: "long" }
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        支払い方法

.single-main__container__form
  .single-main__container__form__frame
    = form_for(@creditcard, url: creditcards_path,method: :post,html: {id: "form" }) do |f|
      = render "devise/shared/error_messages", resource: @creditcard
      .form-group
        = f.label :カード番号
        %span.form-group__require 必須
        = f.text_field :card_number, {placeholder: "半角数字のみ", class: "form-group__input",maxlength:"16"}
      .form-group
        = f.label :カード会社
        %span.form-group__require 必須
        = f.select :card_company, Creditcard.card_companies.keys, {}, {class: 'form-group__input'}
        %ul.signup__card--list
          %li.icon--visa
            = image_tag("visa.svg", id:"icon--visa")
          %li.icon--master
            = image_tag("master-card.svg", id:"icon--master")
          %li.icon--saison
            = image_tag("saison-card.svg", id:"icon--saison")
          %li.icon--jcb
            = image_tag("jcb.svg", id:"icon--jcb")
          %li.icon--americanexpress
            = image_tag("american_express.svg", id:"icon--americanexpress")
          %li.icon--diners
            = image_tag("dinersclub.svg", id:"icon--diners")
          %li.icon--discover
            = image_tag("discover.svg", id:"icon--discover")
      .form-group
        = f.label :有効期限
        %span.form-group__require 必須
        %br
        = f.select :card_month, options_for_select(["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
        -# = f.select :card_year, options_for_select(["20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]), {}, {class: "form-group__input--half"}
        = f.select :card_year, options_for_select((2020..2030)), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
      .form-group
        = f.label :セキュリティコード, class: "label"
        %span.form-group__require 必須
        = f.text_field :card_pass, placeholder: "カード背面4桁もしくは3桁の番", class: "form-group__input"

      .form-group__add
        .form-group__add--question ?
        %p.form-group__text--right--blue
          カード裏面の番号とは?
      .form-group
        = f.submit "登録する", class: "btn-default btn-red", url: "creditcards_path",id:"charge-form",method: :post
  = render "/registration/registration_footer"

  f.submit "Sign up"

2.”トークン”作成:Javascript

今回はPay.jpの機能を利用するために、入力された値で”トークン”を作成し、その”トークン”をキーとしてクレジットカード情報などを登録します。
そのためにはクレジットカード情報を入力してもらった後に、その情報を元に”トークン”を作成するためには、Javascriptを活用して実装します。

今回はJqueryにて実装を行いました。
ポイントは以下の通りです。
- Payjpの公開鍵の記述を忘れずに。
- e.preventDefault()の記述を忘れずに。submitする前にトークンを作成します。
- jsにHTML情報を追加するときはバッククオーテーション``
- 入力エラーのときはprop('disabled', false)でボタンのdisabledを解除。2回以上押せるようにします。
- $("#form").get(0).submit();最後の送信はformの情報をとばす
*上記ミスで、当方はエラー地獄にはまりました。。笑

jquery
$(function() {
  Payjp.setPublicKey('pk_test_57c5bfaa1f1d1f2acd058a77');
  $("#charge-form").on('click', function(e){
    e.preventDefault();
    let card = {
        number: $('#creditcard_card_number').val(),
        cvc:$('#creditcard_card_pass').val(),
        exp_month: $('#creditcard_card_month').val(),
        exp_year: $('#creditcard_card_year').val()
    };

    Payjp.createToken(card, function(status, response) {
      if (response.error) {
        $("#charge-form").prop('disabled', false);
        alert("カード情報が正しくありません。");
      }
      else {
        $(".number").removeAttr("name");
        $(".cvc").removeAttr("name");
        $(".exp_month").removeAttr("name");
        $(".exp_year").removeAttr("name");
        let token = response.id;
        $("#card_token").append(`<input type="hidden" name="payjpToken" value=${token}>`);
        $("#form").get(0).submit();
        alert("登録が完了しました");
      }
    });
  });
});

3.”トークン”をキーとしてPayjpに顧客情報として登録:controller

最後に”トークン”をキーとしてPayjpに顧客情報として登録するために、コントローラに記述が必要となります。
*今回はセッションを用いたユーザー情報を登録しおりますため、ごちゃごちゃしてますが、クレジットカードの登録だけであれば、クレジットカードのコントローラを作成し、対応いただけますと幸いです。
*セッションを用いた登録の詳細記述は以下URLよりご確認いただけますと幸いです。
https://qiita.com/Tatsu88/items/7447a669b788b011e96b

今回のクレジットカードに関する記述は、「def create_creditcard」に記載ございます。
”#顧客情報をPAY.JPに登録。”という記述で、"トークン”をPay.jpに飛ばしてます。

controller
class Users::RegistrationsController < Devise::RegistrationsController
  def new
    super
  end

  # POST /resource
  def create
    if params[:sns_auth] == 'true'
      pass = Devise.friendly_token
      params[:user][:password] = pass
      params[:user][:password_confirmation] = pass
    end
    params[:user][:birthday] = params[:birthday]
    @user = User.new(sign_up_params)
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @address = @user.build_address
    render :new_address
  end

  def create_address
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(address_params)
    unless @address.valid?
      flash.now[:alert] = @address.errors.full_messages
      render :new_address and return
    end
    @user.build_address(@address.attributes)
    session["address"] = @address.attributes
    @creditcard = @user.build_creditcard
    render :new_credit_card
  end

  def create_creditcard
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(session["address"])
    Payjp.api_key = 'sk_test_be263def71d21c8f58b223e3'
    if params['payjpToken'].blank?
      redirect_to action: "new"
    else
      # 顧客情報をPAY.JPに登録。
      customer = Payjp::Customer.create(
        description: 'test', 
        email: @user.email,
        card: params['payjpToken'], 
      )
    end
    @creditcard = Creditcard.new(creditcard_params)
    @creditcard[:customer_id]=customer.id
    @creditcard[:card_id]=customer.default_card
    unless @creditcard.valid?
      flash.now[:alert] = @creditcard.errors.full_messages
      render :new_credit_card and return
    end
    binding.pry
    @user.build_address(@address.attributes)
    @user.build_creditcard(@creditcard.attributes)
    if @user.save
      sign_in(:user, @user)
    else
      render :new
    end
  end

  protected
  def address_params
    params.require(:address).permit(:address,:postal_code, :prefecture,:city,:apartment)
  end

  def creditcard_params
    params.require(:creditcard).permit(:card_number,:card_year, :card_month, :card_pass,:card_company)
  end

4.登録情報の確認:view+controller

最後にPay.jpに登録した情報を取得できるようにしましょう。
まずは、情報をpay.jpから取得するための記述をコントローラに行います。

view
=render "home/header_login"
.mypage_a  
  %main.mypage-contents.clearfix
    .main-content
      .payment
        .payment-content
          .payment-content__title
            %h1.payment-header 支払い方法
          .payment-content__main
            .payment-content__creditcards
              %h2.payment-title クレジットカード一覧
            .payment-content__creditcards__list
              %figure
                = image_tag "#{@card_src}",alt: @card_brand, id: "card_image"
              .payment-content__creditcards__list__number
                = "**** **** **** " + @creditcard_information.last4
              .payment-content__creditcards__list__number
                - exp_month = @creditcard_information.exp_month.to_s
                - exp_year = @creditcard_information.exp_year.to_s.slice(2,3)
                = exp_month + " / " + exp_year
    .side-content
      %nav.mypage-nav
        %ul.mypage-nav-list
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              マイページ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お知らせ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              やることリスト
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              いいね!一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品する
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 出品中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 売却済み
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 過去の取引
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ニュース一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              評価一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ガイド
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お問い合わせ
              %i.icon-arrow-right
        %h3.mypage-nav-head-merpay メルペイ
        %ul.mypage-nav-list-merpay
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              売上・振込申請
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              ポイント
              %i.icon-arrow-right
        %h3.mypage-nav-head-setting 設定
        %ul.mypage-nav-list-setting
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              プロフィール
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              発送元・お届け先住所変更
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              支払い方法
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              メール/パスワード
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              本人情報
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              電話番号の確認
              %i.icon-arrow-right
          %li
            =link_to destroy_user_session_path, method: :delete, class: "mypage-nav-list-setting-item" do
              ログアウト
              %i.icon-arrow-right
=render "home/footer"
controller
class CardsController < ApplicationController
  require "payjp"
  before_action :set_creditcard

  def show
    Payjp.api_key = "sk_test_be263def71d21c8f58b223e3"
    customer = Payjp::Customer.retrieve(@creditcard.customer_id)
    @creditcard_information = customer.cards.retrieve(@creditcard.card_id)
    @card_brand = @creditcard_information.brand 
    case @card_brand
    when "Visa"
      @card_src = "visa.svg"
    when "JCB"
      @card_src = "jcb.svg"
    when "MasterCard"
      @card_src = "master-card.svg"
    when "American Express"
      @card_src = "american_express.svg"
    when "Diners Club"
      @card_src = "dinersclub.svg"
    when "Discover"
      @card_src = "discover.svg"
    end
  end

完成イメージは以下のようになっております。
此の情報はpay.jpでも同じ情報が登録できてます。
creditcard.info.png

注意点(テストする時のクレジットカード番号について)

テストを行う時のクレジットカードの番号が定められており、この番号以外で適当な番号を入れるとエラーとなります。
下記URLに詳細載ってますので、ご確認の上、対応ください。
https://pay.jp/docs/testcard

メモ

-function(e) {} のeって何?
function(e)の「e」。これはイベントハンドラ、イベントリスナとして設定したコールバック関数が受け取ることができるイベントオブジェクトです。
JavaScriptの関数は引数を指定しなくてもOKなのでイベントオブジェクトを省略してもエラーとはなりません。

-get(index)
DOMエレメントの集合からインデックスを指定して、ひとつのエレメントを参照する。
これによって、特にjQueryオブジェクトである必要のないケースで特定のDOM Elementそのものを操作することが可能。例えば$(this).get(0)は、配列オペレータである$(this)[0]と同等の意味になる。

参照

JavaScriptをしっかり勉強 vol.6 Eventオブジェクト
http://brush-clover.com/program/js-study6/

jQuery日本語リファレンス
http://semooh.jp/jquery/api/core/get/index/

payjpリファレンス
https://pay.jp/docs/api/#payjp-api

トークン作成
https://pay.jp/docs/cardtoken

カード情報非通過化対応のお願い
http://payjp-announce.hatenablog.com/entry/2017/11/10/182738

顧客を作成
https://pay.jp/docs/api/#%E9%A1%A7%E5%AE%A2%E3%82%92%E4%BD%9C%E6%88%90

【Rails5】簡単便利!PAY.JPでクレジットカードのオンライン決済機能を導入!
https://qiita.com/emincoring/items/ce29dbbd182aa3c49c6b

payjp.jsの導入方法<Rails>
https://qiita.com/tripoodle/items/57d1cf9aef74ac5c9ab6#payjp%E3%81%A8%E3%81%AF

Payjpでクレジットカード登録と削除機能を実装する(Rails)
https://qiita.com/takachan_coding/items/f7e70794b9ca03b559dd

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

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

[HowTo]Pay.jpを用いたクレジットカードの登録機能実装について(カスタムフォーム使用)

某スクールのチーム開発にてpay.jpを活用したクレジットカード登録と商品購入機能実装を担当させていただくことになりました!
色々と苦戦しましたがなんとか実装できましたので、以下にまとめてみたいと思います。
今回はクレジットカード登録機能実装までまとめており、商品購入機能に関しては、準備出来次第、別記事として投稿します!

そもそもPay.jpとは

シンプルなAPI・多彩な機能、分かりやすい料金形態でクレジットカード決済をかんたんに導入できる決済サービスです。手数料も比較的リーズナブル(2.59%〜)であることと、導入が簡単ということもあり、スタートアップ企業などに多く採用されているようです。
イメージとしては以下のようになっており、一時的なトークンを作成し、取引をすることで、加盟店はクレジットカード情報などの重要な情報を扱う必要がないまま、取引ができます。

payjpimage.png

クレジットカード登録について

クレジットカード登録に関しては、カードの登録のフォーマットによってやり方が異なります。
- チェックアウト:pay.jp社にて用意されているフォーマットを利用する方法。
- カスタムフォーム:ご自身でフォーマットを作成し、そちらに準じてカード登録を行う方法。

今回は既に入力フォームを作成していたので、カスタムフォームにて実装を行うことにしました!

全体の流れ

1. クレジットカード番号などの情報登録:view
2. ”トークン”作成:Javascript
3. ”トークン”をキーとしてPayjpに顧客情報として登録:controller
4. 登録情報の確認:view+controller

今回の記事では、まず、Viewの実装内容を確認いただき、JSを用いたトークンの作成をご確認いただきます。
そして、最後にコントローラでそのトークンをキーとしてPayjpに顧客登録をする動きを確認いただければと思います。

1.クレジットカード番号などの情報登録:view

今回は以下のようなviewで登録画面を作ってます。

612316d11d112f7fb4f670ef83a440ec.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to root_path do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          支払い方法
          .single-header__progress__round--red
            .single-header__progress__round--red-long{ id: "long" }
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        支払い方法

.single-main__container__form
  .single-main__container__form__frame
    = form_for(@creditcard, url: creditcards_path,method: :post,html: {id: "form" }) do |f|
      = render "devise/shared/error_messages", resource: @creditcard
      .form-group
        = f.label :カード番号
        %span.form-group__require 必須
        = f.text_field :card_number, {placeholder: "半角数字のみ", class: "form-group__input",maxlength:"16"}
      .form-group
        = f.label :カード会社
        %span.form-group__require 必須
        = f.select :card_company, Creditcard.card_companies.keys, {}, {class: 'form-group__input'}
        %ul.signup__card--list
          %li.icon--visa
            = image_tag("visa.svg", id:"icon--visa")
          %li.icon--master
            = image_tag("master-card.svg", id:"icon--master")
          %li.icon--saison
            = image_tag("saison-card.svg", id:"icon--saison")
          %li.icon--jcb
            = image_tag("jcb.svg", id:"icon--jcb")
          %li.icon--americanexpress
            = image_tag("american_express.svg", id:"icon--americanexpress")
          %li.icon--diners
            = image_tag("dinersclub.svg", id:"icon--diners")
          %li.icon--discover
            = image_tag("discover.svg", id:"icon--discover")
      .form-group
        = f.label :有効期限
        %span.form-group__require 必須
        %br
        = f.select :card_month, options_for_select(["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
        -# = f.select :card_year, options_for_select(["20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]), {}, {class: "form-group__input--half"}
        = f.select :card_year, options_for_select((2020..2030)), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
      .form-group
        = f.label :セキュリティコード, class: "label"
        %span.form-group__require 必須
        = f.text_field :card_pass, placeholder: "カード背面4桁もしくは3桁の番", class: "form-group__input"

      .form-group__add
        .form-group__add--question ?
        %p.form-group__text--right--blue
          カード裏面の番号とは?
      .form-group
        = f.submit "登録する", class: "btn-default btn-red", url: "creditcards_path",id:"charge-form",method: :post
  = render "/registration/registration_footer"

  f.submit "Sign up"

2.”トークン”作成:Javascript

今回はPay.jpの機能を利用するために、入力された値で一時的な”トークン”を作成し、その”トークン”をキーとしてクレジットカード情報などを登録します。
そのためにはクレジットカード情報を入力してもらった後に、その情報を元に”トークン”を作成するためには、Javascriptを活用して実装します。

今回はJqueryにて実装を行いました。
ポイントは以下の通りです。
- Payjpの公開鍵の記述を忘れずに。
- e.preventDefault()の記述を忘れずに。submitする前にトークンを作成します。
- jsにHTML情報を追加するときはバッククオーテーション``
- 入力エラーのときはprop('disabled', false)でボタンのdisabledを解除。2回以上押せるようにします。
- $("#form").get(0).submit();最後の送信はformの情報をとばす
*上記ミスで、当方はエラー地獄にはまりました。。笑

jquery
$(function() {
  Payjp.setPublicKey('pk_test_57c5bfaa1f1d1f2acd058a77');
  $("#charge-form").on('click', function(e){
    e.preventDefault();
    let card = {
        number: $('#creditcard_card_number').val(),
        cvc:$('#creditcard_card_pass').val(),
        exp_month: $('#creditcard_card_month').val(),
        exp_year: $('#creditcard_card_year').val()
    };

    Payjp.createToken(card, function(status, response) {
      if (response.error) {
        $("#charge-form").prop('disabled', false);
        alert("カード情報が正しくありません。");
      }
      else {
        $(".number").removeAttr("name");
        $(".cvc").removeAttr("name");
        $(".exp_month").removeAttr("name");
        $(".exp_year").removeAttr("name");
        let token = response.id;
        $("#card_token").append(`<input type="hidden" name="payjpToken" value=${token}>`);
        $("#form").get(0).submit();
        alert("登録が完了しました");
      }
    });
  });
});

3.”トークン”をキーとしてPayjpに顧客情報として登録:controller

最後に”トークン”をキーとしてPayjpに顧客情報として登録するために、コントローラに記述が必要となります。
*今回はセッションを用いたユーザー情報を登録しおりますため、ごちゃごちゃしてますが、クレジットカードの登録だけであれば、クレジットカードのコントローラを作成し、対応いただけますと幸いです。
*セッションを用いた登録の詳細記述は以下URLよりご確認いただけますと幸いです。
https://qiita.com/Tatsu88/items/7447a669b788b011e96b

今回のクレジットカードに関する記述は、「def create_creditcard」に記載ございます。
”#顧客情報をPAY.JPに登録。”という記述で、"トークン”をPay.jpに飛ばしてます。

controller
class Users::RegistrationsController < Devise::RegistrationsController
  def new
    super
  end

  # POST /resource
  def create
    if params[:sns_auth] == 'true'
      pass = Devise.friendly_token
      params[:user][:password] = pass
      params[:user][:password_confirmation] = pass
    end
    params[:user][:birthday] = params[:birthday]
    @user = User.new(sign_up_params)
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @address = @user.build_address
    render :new_address
  end

  def create_address
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(address_params)
    unless @address.valid?
      flash.now[:alert] = @address.errors.full_messages
      render :new_address and return
    end
    @user.build_address(@address.attributes)
    session["address"] = @address.attributes
    @creditcard = @user.build_creditcard
    render :new_credit_card
  end

  def create_creditcard
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(session["address"])
    Payjp.api_key = 'sk_test_be263def71d21c8f58b223e3'
    if params['payjpToken'].blank?
      redirect_to action: "new"
    else
      # 顧客情報をPAY.JPに登録。
      customer = Payjp::Customer.create(
        description: 'test', 
        email: @user.email,
        card: params['payjpToken'], 
      )
    end
    @creditcard = Creditcard.new(creditcard_params)
    @creditcard[:customer_id]=customer.id
    @creditcard[:card_id]=customer.default_card
    unless @creditcard.valid?
      flash.now[:alert] = @creditcard.errors.full_messages
      render :new_credit_card and return
    end
    binding.pry
    @user.build_address(@address.attributes)
    @user.build_creditcard(@creditcard.attributes)
    if @user.save
      sign_in(:user, @user)
    else
      render :new
    end
  end

  protected
  def address_params
    params.require(:address).permit(:address,:postal_code, :prefecture,:city,:apartment)
  end

  def creditcard_params
    params.require(:creditcard).permit(:card_number,:card_year, :card_month, :card_pass,:card_company)
  end

4.登録情報の確認:view+controller

最後にPay.jpに登録した情報を取得できるようにしましょう。
まずは、情報をpay.jpから取得するための記述をコントローラに行います。

view
=render "home/header_login"
.mypage_a  
  %main.mypage-contents.clearfix
    .main-content
      .payment
        .payment-content
          .payment-content__title
            %h1.payment-header 支払い方法
          .payment-content__main
            .payment-content__creditcards
              %h2.payment-title クレジットカード一覧
            .payment-content__creditcards__list
              %figure
                = image_tag "#{@card_src}",alt: @card_brand, id: "card_image"
              .payment-content__creditcards__list__number
                = "**** **** **** " + @creditcard_information.last4
              .payment-content__creditcards__list__number
                - exp_month = @creditcard_information.exp_month.to_s
                - exp_year = @creditcard_information.exp_year.to_s.slice(2,3)
                = exp_month + " / " + exp_year
    .side-content
      %nav.mypage-nav
        %ul.mypage-nav-list
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              マイページ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お知らせ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              やることリスト
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              いいね!一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品する
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 出品中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 売却済み
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 過去の取引
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ニュース一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              評価一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ガイド
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お問い合わせ
              %i.icon-arrow-right
        %h3.mypage-nav-head-merpay メルペイ
        %ul.mypage-nav-list-merpay
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              売上・振込申請
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              ポイント
              %i.icon-arrow-right
        %h3.mypage-nav-head-setting 設定
        %ul.mypage-nav-list-setting
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              プロフィール
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              発送元・お届け先住所変更
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              支払い方法
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              メール/パスワード
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              本人情報
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              電話番号の確認
              %i.icon-arrow-right
          %li
            =link_to destroy_user_session_path, method: :delete, class: "mypage-nav-list-setting-item" do
              ログアウト
              %i.icon-arrow-right
=render "home/footer"
controller
class CardsController < ApplicationController
  require "payjp"
  before_action :set_creditcard

  def show
    Payjp.api_key = "sk_test_be263def71d21c8f58b223e3"
    customer = Payjp::Customer.retrieve(@creditcard.customer_id)
    @creditcard_information = customer.cards.retrieve(@creditcard.card_id)
    @card_brand = @creditcard_information.brand 
    case @card_brand
    when "Visa"
      @card_src = "visa.svg"
    when "JCB"
      @card_src = "jcb.svg"
    when "MasterCard"
      @card_src = "master-card.svg"
    when "American Express"
      @card_src = "american_express.svg"
    when "Diners Club"
      @card_src = "dinersclub.svg"
    when "Discover"
      @card_src = "discover.svg"
    end
  end

完成イメージは以下のようになっております。
此の情報はpay.jpでも同じ情報が登録できてます。
creditcard.info.png

注意点(テストする時のクレジットカード番号について)

テストを行う時のクレジットカードの番号が定められており、この番号以外で適当な番号を入れるとエラーとなります。
下記URLに詳細載ってますので、ご確認の上、対応ください。
https://pay.jp/docs/testcard

メモ

-function(e) {} のeって何?
function(e)の「e」。これはイベントハンドラ、イベントリスナとして設定したコールバック関数が受け取ることができるイベントオブジェクトです。
JavaScriptの関数は引数を指定しなくてもOKなのでイベントオブジェクトを省略してもエラーとはなりません。

-get(index)
DOMエレメントの集合からインデックスを指定して、ひとつのエレメントを参照する。
これによって、特にjQueryオブジェクトである必要のないケースで特定のDOM Elementそのものを操作することが可能。例えば$(this).get(0)は、配列オペレータである$(this)[0]と同等の意味になる。

参照

JavaScriptをしっかり勉強 vol.6 Eventオブジェクト
http://brush-clover.com/program/js-study6/

jQuery日本語リファレンス
http://semooh.jp/jquery/api/core/get/index/

payjpリファレンス
https://pay.jp/docs/api/#payjp-api

トークン作成
https://pay.jp/docs/cardtoken

カード情報非通過化対応のお願い
http://payjp-announce.hatenablog.com/entry/2017/11/10/182738

顧客を作成
https://pay.jp/docs/api/#%E9%A1%A7%E5%AE%A2%E3%82%92%E4%BD%9C%E6%88%90

【Rails5】簡単便利!PAY.JPでクレジットカードのオンライン決済機能を導入!
https://qiita.com/emincoring/items/ce29dbbd182aa3c49c6b

payjp.jsの導入方法<Rails>
https://qiita.com/tripoodle/items/57d1cf9aef74ac5c9ab6#payjp%E3%81%A8%E3%81%AF

Payjpでクレジットカード登録と削除機能を実装する(Rails)
https://qiita.com/takachan_coding/items/f7e70794b9ca03b559dd

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

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

[HowTo]Pay.jpを用いたクレジットカードの登録機能実装について/カスタムフォーム版

某スクールのチーム開発にてpay.jpを活用したクレジットカード登録と商品購入機能実装を担当させていただくことになりました!
色々と苦戦しましたがなんとか実装できましたので、以下にまとめてみたいと思います。
今回はクレジットカード登録機能実装までまとめており、商品購入機能に関しては、準備出来次第、別記事として投稿します!

そもそもPay.jpとは

シンプルなAPI・多彩な機能、分かりやすい料金形態でクレジットカード決済をかんたんに導入できる決済サービスです。手数料も比較的リーズナブル(2.59%〜)であることと、導入が簡単ということもあり、スタートアップ企業などに多く採用されているようです。
イメージとしては以下のようになっており、一時的なトークンを作成し、取引をすることで、加盟店はクレジットカード情報などの重要な情報を扱う必要がないまま、取引ができます。

payjpimage.png

クレジットカード登録について

クレジットカード登録に関しては、カードの登録のフォーマットによってやり方が異なります。
- チェックアウト:pay.jp社にて用意されているフォーマットを利用する方法。
- カスタムフォーム:ご自身でフォーマットを作成し、そちらに準じてカード登録を行う方法。

今回は既に入力フォームを作成していたので、カスタムフォームにて実装を行うことにしました!

全体の流れ

1. クレジットカード番号などの情報登録:view
2. ”トークン”作成:Javascript
3. ”トークン”をキーとしてPayjpに顧客情報として登録:controller
4. 登録情報の確認:view+controller

今回の記事では、まず、Viewの実装内容を確認いただき、JSを用いたトークンの作成をご確認いただきます。
そして、最後にコントローラでそのトークンをキーとしてPayjpに顧客登録をする動きを確認いただければと思います。

1.クレジットカード番号などの情報登録:view

今回は以下のようなviewで登録画面を作ってます。

612316d11d112f7fb4f670ef83a440ec.png

view
.single-container
  %header.single-header
    %h1.single-header__logo
      = link_to root_path do
        =image_tag("fmarket_logo_red.svg")
    %nav.single-header__progress
      %ol
        %li.single-header__progress__text{ id: "first" }
          会員情報
          .single-header__progress__round--red
        %li.single-header__progress__text
          お届け先住所入力
          .single-header__progress__round--red
        %li.single-header__progress__text--active
          支払い方法
          .single-header__progress__round--red
            .single-header__progress__round--red-long{ id: "long" }
        %li.single-header__progress__text{ id: "end" }
          完了
          .single-header__progress__round
  %main.single-main
    %section.single-main__container
      %h2.single-main__container__title
        支払い方法

.single-main__container__form
  .single-main__container__form__frame
    = form_for(@creditcard, url: creditcards_path,method: :post,html: {id: "form" }) do |f|
      = render "devise/shared/error_messages", resource: @creditcard
      .form-group
        = f.label :カード番号
        %span.form-group__require 必須
        = f.text_field :card_number, {placeholder: "半角数字のみ", class: "form-group__input",maxlength:"16"}
      .form-group
        = f.label :カード会社
        %span.form-group__require 必須
        = f.select :card_company, Creditcard.card_companies.keys, {}, {class: 'form-group__input'}
        %ul.signup__card--list
          %li.icon--visa
            = image_tag("visa.svg", id:"icon--visa")
          %li.icon--master
            = image_tag("master-card.svg", id:"icon--master")
          %li.icon--saison
            = image_tag("saison-card.svg", id:"icon--saison")
          %li.icon--jcb
            = image_tag("jcb.svg", id:"icon--jcb")
          %li.icon--americanexpress
            = image_tag("american_express.svg", id:"icon--americanexpress")
          %li.icon--diners
            = image_tag("dinersclub.svg", id:"icon--diners")
          %li.icon--discover
            = image_tag("discover.svg", id:"icon--discover")
      .form-group
        = f.label :有効期限
        %span.form-group__require 必須
        %br
        = f.select :card_month, options_for_select(["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
        -# = f.select :card_year, options_for_select(["20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]), {}, {class: "form-group__input--half"}
        = f.select :card_year, options_for_select((2020..2030)), {}, {class: "form-group__input--half"}
        = f.label :, class: "form-group__card--year-and-month"
      .form-group
        = f.label :セキュリティコード, class: "label"
        %span.form-group__require 必須
        = f.text_field :card_pass, placeholder: "カード背面4桁もしくは3桁の番", class: "form-group__input"

      .form-group__add
        .form-group__add--question ?
        %p.form-group__text--right--blue
          カード裏面の番号とは?
      .form-group
        = f.submit "登録する", class: "btn-default btn-red", url: "creditcards_path",id:"charge-form",method: :post
  = render "/registration/registration_footer"

  f.submit "Sign up"

2.”トークン”作成:Javascript

今回はPay.jpの機能を利用するために、入力された値で一時的な”トークン”を作成し、その”トークン”をキーとしてクレジットカード情報などを登録します。
そのためにはクレジットカード情報を入力してもらった後に、その情報を元に”トークン”を作成するためには、Javascriptを活用して実装します。

今回はJqueryにて実装を行いました。
ポイントは以下の通りです。
- Payjpの公開鍵の記述を忘れずに。
- e.preventDefault()の記述を忘れずに。submitする前にトークンを作成します。
- jsにHTML情報を追加するときはバッククオーテーション``
- 入力エラーのときはprop('disabled', false)でボタンのdisabledを解除。2回以上押せるようにします。
- $("#form").get(0).submit();最後の送信はformの情報をとばす
*上記ミスで、当方はエラー地獄にはまりました。。笑

jquery
$(function() {
  Payjp.setPublicKey('pk_test_57c5bfaa1f1d1f2acd058a77');
  $("#charge-form").on('click', function(e){
    e.preventDefault();
    let card = {
        number: $('#creditcard_card_number').val(),
        cvc:$('#creditcard_card_pass').val(),
        exp_month: $('#creditcard_card_month').val(),
        exp_year: $('#creditcard_card_year').val()
    };

    Payjp.createToken(card, function(status, response) {
      if (response.error) {
        $("#charge-form").prop('disabled', false);
        alert("カード情報が正しくありません。");
      }
      else {
        $(".number").removeAttr("name");
        $(".cvc").removeAttr("name");
        $(".exp_month").removeAttr("name");
        $(".exp_year").removeAttr("name");
        let token = response.id;
        $("#card_token").append(`<input type="hidden" name="payjpToken" value=${token}>`);
        $("#form").get(0).submit();
        alert("登録が完了しました");
      }
    });
  });
});

3.”トークン”をキーとしてPayjpに顧客情報として登録:controller

最後に”トークン”をキーとしてPayjpに顧客情報として登録するために、コントローラに記述が必要となります。
*今回はセッションを用いたユーザー情報を登録しおりますため、ごちゃごちゃしてますが、クレジットカードの登録だけであれば、クレジットカードのコントローラを作成し、対応いただけますと幸いです。
*セッションを用いた登録の詳細記述は以下URLよりご確認いただけますと幸いです。
https://qiita.com/Tatsu88/items/7447a669b788b011e96b

今回のクレジットカードに関する記述は、「def create_creditcard」に記載ございます。
”#顧客情報をPAY.JPに登録。”という記述で、"トークン”をPay.jpに飛ばしてます。

controller
class Users::RegistrationsController < Devise::RegistrationsController
  def new
    super
  end

  # POST /resource
  def create
    if params[:sns_auth] == 'true'
      pass = Devise.friendly_token
      params[:user][:password] = pass
      params[:user][:password_confirmation] = pass
    end
    params[:user][:birthday] = params[:birthday]
    @user = User.new(sign_up_params)
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    @address = @user.build_address
    render :new_address
  end

  def create_address
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(address_params)
    unless @address.valid?
      flash.now[:alert] = @address.errors.full_messages
      render :new_address and return
    end
    @user.build_address(@address.attributes)
    session["address"] = @address.attributes
    @creditcard = @user.build_creditcard
    render :new_credit_card
  end

  def create_creditcard
    @user = User.new(session["devise.regist_data"]["user"])
    @address = Address.new(session["address"])
    Payjp.api_key = 'sk_test_be263def71d21c8f58b223e3'
    if params['payjpToken'].blank?
      redirect_to action: "new"
    else
      # 顧客情報をPAY.JPに登録。
      customer = Payjp::Customer.create(
        description: 'test', 
        email: @user.email,
        card: params['payjpToken'], 
      )
    end
    @creditcard = Creditcard.new(creditcard_params)
    @creditcard[:customer_id]=customer.id
    @creditcard[:card_id]=customer.default_card
    unless @creditcard.valid?
      flash.now[:alert] = @creditcard.errors.full_messages
      render :new_credit_card and return
    end
    binding.pry
    @user.build_address(@address.attributes)
    @user.build_creditcard(@creditcard.attributes)
    if @user.save
      sign_in(:user, @user)
    else
      render :new
    end
  end

  protected
  def address_params
    params.require(:address).permit(:address,:postal_code, :prefecture,:city,:apartment)
  end

  def creditcard_params
    params.require(:creditcard).permit(:card_number,:card_year, :card_month, :card_pass,:card_company)
  end

4.登録情報の確認:view+controller

最後にPay.jpに登録した情報を取得できるようにしましょう。
まずは、情報をpay.jpから取得するための記述をコントローラに行います。

view
=render "home/header_login"
.mypage_a  
  %main.mypage-contents.clearfix
    .main-content
      .payment
        .payment-content
          .payment-content__title
            %h1.payment-header 支払い方法
          .payment-content__main
            .payment-content__creditcards
              %h2.payment-title クレジットカード一覧
            .payment-content__creditcards__list
              %figure
                = image_tag "#{@card_src}",alt: @card_brand, id: "card_image"
              .payment-content__creditcards__list__number
                = "**** **** **** " + @creditcard_information.last4
              .payment-content__creditcards__list__number
                - exp_month = @creditcard_information.exp_month.to_s
                - exp_year = @creditcard_information.exp_year.to_s.slice(2,3)
                = exp_month + " / " + exp_year
    .side-content
      %nav.mypage-nav
        %ul.mypage-nav-list
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              マイページ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お知らせ
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              やることリスト
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              いいね!一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品する
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 出品中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              出品した商品 - 売却済み
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 取引中
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              購入した商品 - 過去の取引
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ニュース一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              評価一覧
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              ガイド
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-item" do
              お問い合わせ
              %i.icon-arrow-right
        %h3.mypage-nav-head-merpay メルペイ
        %ul.mypage-nav-list-merpay
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              売上・振込申請
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-merpay-item" do
              ポイント
              %i.icon-arrow-right
        %h3.mypage-nav-head-setting 設定
        %ul.mypage-nav-list-setting
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              プロフィール
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              発送元・お届け先住所変更
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              支払い方法
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              メール/パスワード
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              本人情報
              %i.icon-arrow-right
          %li
            =link_to "#", class: "mypage-nav-list-setting-item" do
              電話番号の確認
              %i.icon-arrow-right
          %li
            =link_to destroy_user_session_path, method: :delete, class: "mypage-nav-list-setting-item" do
              ログアウト
              %i.icon-arrow-right
=render "home/footer"
controller
class CardsController < ApplicationController
  require "payjp"
  before_action :set_creditcard

  def show
    Payjp.api_key = "sk_test_be263def71d21c8f58b223e3"
    customer = Payjp::Customer.retrieve(@creditcard.customer_id)
    @creditcard_information = customer.cards.retrieve(@creditcard.card_id)
    @card_brand = @creditcard_information.brand 
    case @card_brand
    when "Visa"
      @card_src = "visa.svg"
    when "JCB"
      @card_src = "jcb.svg"
    when "MasterCard"
      @card_src = "master-card.svg"
    when "American Express"
      @card_src = "american_express.svg"
    when "Diners Club"
      @card_src = "dinersclub.svg"
    when "Discover"
      @card_src = "discover.svg"
    end
  end

完成イメージは以下のようになっております。
此の情報はpay.jpでも同じ情報が登録できてます。
creditcard.info.png

注意点(テストする時のクレジットカード番号について)

テストを行う時のクレジットカードの番号が定められており、この番号以外で適当な番号を入れるとエラーとなります。
下記URLに詳細載ってますので、ご確認の上、対応ください。
https://pay.jp/docs/testcard

メモ

-function(e) {} のeって何?
function(e)の「e」。これはイベントハンドラ、イベントリスナとして設定したコールバック関数が受け取ることができるイベントオブジェクトです。
JavaScriptの関数は引数を指定しなくてもOKなのでイベントオブジェクトを省略してもエラーとはなりません。

-get(index)
DOMエレメントの集合からインデックスを指定して、ひとつのエレメントを参照する。
これによって、特にjQueryオブジェクトである必要のないケースで特定のDOM Elementそのものを操作することが可能。例えば$(this).get(0)は、配列オペレータである$(this)[0]と同等の意味になる。

参照

JavaScriptをしっかり勉強 vol.6 Eventオブジェクト
http://brush-clover.com/program/js-study6/

jQuery日本語リファレンス
http://semooh.jp/jquery/api/core/get/index/

payjpリファレンス
https://pay.jp/docs/api/#payjp-api

トークン作成
https://pay.jp/docs/cardtoken

カード情報非通過化対応のお願い
http://payjp-announce.hatenablog.com/entry/2017/11/10/182738

顧客を作成
https://pay.jp/docs/api/#%E9%A1%A7%E5%AE%A2%E3%82%92%E4%BD%9C%E6%88%90

【Rails5】簡単便利!PAY.JPでクレジットカードのオンライン決済機能を導入!
https://qiita.com/emincoring/items/ce29dbbd182aa3c49c6b

payjp.jsの導入方法<Rails>
https://qiita.com/tripoodle/items/57d1cf9aef74ac5c9ab6#payjp%E3%81%A8%E3%81%AF

Payjpでクレジットカード登録と削除機能を実装する(Rails)
https://qiita.com/takachan_coding/items/f7e70794b9ca03b559dd

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

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

PyWebViewで複数のJavaScriptファイルを読み込む

PyWebViewで複数のJavaScriptを使いたい

PyWebViewで、ウィンドウにJavaScriptを読み込ませたいときには、window#evaluate_js()というメソッドを利用します。

このメソッド、複数回呼び出したときは、それぞれのJavaScriptを別々のファイルと認識して処理します、このため次のようなメソッドを作っておくと、HTML5アプリケーション開発により近い感覚でPyWebViewのGUIを作れます。

def webview_load_elems(window):
  """
  引数で指定した任意のウィンドウに、次のCSS、JSライブラリを読み込む
  ・bootstrap.css
  ・bootstrap.js/popper.js/jquery.js
  ・独自開発用の、index.css/index.js
  """
  css = ["index.css"]
  js = ["index.js", "classes.js"]
  nm = "node_modules"
  dist = nm / "bootstrap-honoka" / "dist"
  distjs = dist / "js"

  css.insert(0, dist / "css" / "bootstrap.css")
  js.insert(0, nm / "popper.js" / "dist" / "popper.js")
  js.insert(0, distjs / "bootstrap.js")
  js.insert(0, nm / "jquery" / "dist" / "jquery.js")

  for file in css:
    with open(file, mode="r", encoding="utf-8") as f:
      window.load_css(f.read())
  for file in js:
    with open(file, mode="r", encoding="utf-8") as f:
      window.evaluate_js(f.read())
index.js
let test = TestClass()
/* ... */
classes.js
class TestClass {
  /* ... */
}

上記コードの問題

上記のようにファイルを読み込むと、classes.jsで宣言したクラスをindex.jsで使うことはできません。ReferenceErrorが発生してしまいます。

最近のJavaScriptであればexport文などもありますが、PyWebView上で扱われるファイルは(初期設定では)ローカルファイルとして扱われること、そもそもファイルを読み込んでいるわけではなく、JavaScriptのコードを直接ブラウザエンジンに読み込んでることから、export文は使えません

グローバルな変数のプロパティとして追加する

このようなときは、jQueryなどのファイルの先頭にヒントがあります。

jquery.js
( function( global, factory ) {

    "use strict";

    if ( typeof module === "object" && typeof module.exports === "object" ) {

        // For CommonJS and CommonJS-like environments where a proper `window`
        // is present, execute the factory and get jQuery.
        // For environments that do not have a `window` with a `document`
        // (such as Node.js), expose a factory as module.exports.
        // This accentuates the need for the creation of a real `window`.
        // e.g. var jQuery = require("jquery")(window);
        // See ticket #14549 for more info.
        module.exports = global.document ?
            factory( global, true ) :
            function( w ) {
                if ( !w.document ) {
                    throw new Error( "jQuery requires a window with a document" );
                }
                return factory( w );
            };
    } else {
        factory( global );
    }

// Pass this if window is not defined yet
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {

jQueryはJavaScriptの実行環境毎に、グローバル変数やエクスポート領域に自分自身を登録することで、他のJavaScriptファイルからjQueryを呼び出すことを可能にしています。

PyWebViewの実行環境において、使えるグローバル変数はwindowです。ですので、windowのプロパティとして、宣言したクラスを登録すればOK。

classes.js
class TestClass {
  /* ... */
}

window.TestClass = TestClass;

これでindex.jsからでもclasses.jsのクラスが利用できるようになります。

おまけ:PyWebViewウィンドウの開発者コンソールから、JavaScriptファイルを見たい

webview.start()メソッドのguiに開発者コンソールが利用できるブラウザエンジン(cefなど)、debugTrueに設定すると、ブラウザウィンドウの右クリックメニューから開発者コンソールが開けるようになります。

この開発者コンソールから読み込んだJavaScriptファイルを見たい場合。読み込んだJavaScriptコードはファイルの形式ではないので、Sourceタブから中身を見ることができません。

そんなときは、JavaScriptファイルの先頭行にconsole.log("ファイル名")などと入れておくと良いです。

image.png

画面右の「VMnn:行数」というリンクをクリックすると、読み込んだJavaScriptコードを閲覧することができます。

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

【GAS】V8 runtimeに切り替えるとき、claspの人はマニフェストを手動で書き換えること!

あらすじ:

  • GAS (Google Apps Script) はJavaScript 1.6〜1.8という古い構文だった
  • 最近それがV8 runtimeというエンジンに変わってECMAScript準拠のナウい構文になった
  • Webのスクリプトエディタを使っている人はRunメニューから切り替えられる
  • claspを使っている人はローカルのappsscript.jsonを手動で書き換える必要がある

これでわかった人は読む必要なし。

詳細:

GASのランタイムがRhinoというのからV8というのに変更になった。
これまでのGASのJavaScriptは1.6〜1.8とかの古いものだったが、ECMAScript準拠のナウいものになったらしい。
めでたい。

参考記事: https://tonari-it.com/gas-v8-runtime/

上の「隣にIT」さん(いつもお世話になります!)の記事の執筆時点では、本来「実行>Enable new Apps Script runtime powerd by V8」で変更できるが、まだ「表示>マニフェストファイルの変更」でマニフェストファイルを表示して変更しなければならなかった、と書かれているが、2020-02-10では、ちゃんと実行メニューに「Enable...」と表示されるようになった。

ちなみにこれ、トグルになっていて、Enable状態で実行メニューを見るとDisable、Disable状態で見るとEnableが表示されていて、RhinoとV8が交互に切り替わるようになっている。
(Rhinoってオライリーのサイ本のサイだね!w)

また、ぼくの場合スクリプトエディタを開いただけで「V8が使えますがどうしますか」的なメッセージが出てきて(スクショ撮りはぐった><)、はい、と答えるだけでV8に切り替わった。

ただ、ぼくはclaspを使っていて、ローカル上にRhinoのレガシーソースが溜まっていた(※)。
これをpushしようとすると

? Manifest file has been updated. Do you want to push and overwrite?(y/N)

と言われる。

「y」と入力すると、マニフェストファイルも置き換わってしまい、レガシー状態に戻ってしまう。
デフォルトを受け入れてEnterを押下するか「N」と入力すると、マニフェストファイル以外のソースもアップロードされない。

この場合は、claspのローカル環境にあるマニフェストファイル、appsscript.jsonを手動で書き換える。
末尾だけ掲げる。

appsscript.json書き換え前
  "exceptionLogging": "STACKDRIVER"
}

=>

  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

jsonはjsと違ってケツカンマを許さないので注意。
https://qiita.com/daijinload/items/149410c975d6c008101e

これでpushするとV8状態のまま開発中のソースも正しくアップロードされる。
とりあえずスプレッド構文が使えてハッピーだ。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax

(この項終わり)

※過去のぼくのclaspとの格闘はコチラ:
https://qiita.com/query1000/items/259a60ba1be8743b858b
https://qiita.com/query1000/items/b25ae9deb700c7bf3212

追記:
2020-02-10現在、HtmlService.getUserAgent()がnullを返すという問題がある。
https://ja.stackoverflow.com/questions/62926/gas%e3%81%aev8-runtime%e3%81%a7htmlservice-getuseragent%e3%81%8c%e4%bd%bf%e3%81%88%e3%81%aa%e3%81%84

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

javascript知見

シリーズ

物理・数学・プログラムのページについて

概要

javascriptについてわかったことなどをまとめていこうと思います。
参考

関数について


normalFunc()とarrowFunc1()は全く同じということではなくてthisの振る舞いが異なる点に注意。
参考
// 普通の関数の書き方1
function normalFunc1(x, y){
  return x * y;
}

// 普通の関数の書き方2
const normalFunc2 = function(x, y){
  return x * y;
}

// アロー関数1
const arrowFunc1 = (x, y) => {
  const ret = x * y;
  return ret;
}

// アロー関数2 --中身が1行の時
const arrowFunc2 = (x, y) => return x * y;

// アロー関数3 --中身が1行 &amp; 引数が1つの時
const arrowFunc3 = x => return 10 * x;

// 実行
console.log(normalFunc1(1,2));
console.log(normalFunc2(1,2));
console.log(arrowFunc1(2,3));
console.log(arrowFunc2(3,4));
console.log(arrowFunc3(1));

promiseの例

参考

この、resolve()reject()の意味がわからない。
どうしてここに書くのか...
function hidouki2() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      //成功時
     resolve('非同期成功');
      // 失敗時
     // reject(new Error('Error'));
    }, 5000);
  });
}

hidouki2().then(function (value) {
  console.log(value);
}).catch(function (error) {
  console.log(error);
});

promiseとjqueryを用いたajaxの方法

参考1参考2
今回の勉強を通して、ショックを受けました。いかに自分が勉強不足だったか。そしてこんなにも楽に綺麗に非同期処理が書けるのかと!
もっと勉強しよう!!!と思いました。

$.ajax({
  オプション
})
.then(成功時の処理, 失敗時の処理);

$.ajax({
  url: "http://jsrun.it/assets/E/H/Z/t/EHZt3"
}).then(
  // 1つめは通信成功時のコールバック
  function (data) {
    $("#results").append(data);
  },
  // 2つめは通信失敗時のコールバック
  function () {
    alert("読み込み失敗");
});

または

$.ajax({
  url: 'http://jsrun.it/assets/E/H/Z/t/EHZt3'
})
.then(
  data => $('#results').append(data),
  error => alert('読み込み失敗')
);

then()をメソッドチェーンすると、ajaxで1つ目の結果を用いて2つ目のajaxを行うといったことがコールバック地獄なしで行える。これはすごい!!!!!!!!!!!(これはpromise()then()!?)
var hoge;
var fuga;
$.ajax("a.html")
.then(function(_hoge) {
  hoge = _hoge;
  return $.ajax("b.html");
})
.then(function(_fuga) {
  fuga = _fuga;
  return $.ajax("c.html");
})
.then(function(piyo) {
  console.log(hoge + piyo + fuga);
});

次は並列処理。複数のajaxを同時に行い、全て揃ったら実行される(これはpromiseall()!?)
$.when($.ajax('a.html'), $.ajax('b.html'), $.ajax('c.html'))
  .done(function(hoge, fuga, piyo) {
  // do something
});

jqueryのajaxで画像を送信する方法

サーバー側

if($_FILES) {
    $input_file = $_FILES;
    // echo json_encode($input_file);
    // exit();

    $result = [];
    for ($i=0; $i<count($_FILES['upload_file']['name']) ; $i++) { 
        $img_name = $this->login_member['id'].date("YmdHis").$_FILES['upload_file']['name'][$i];
        $result[] = ['name'=>$img_name];

        // $_SESSION['upload_img'][] = $img_name;
        //画像を仮保存
        move_uploaded_file($_FILES['upload_file']['tmp_name'][$i], DIR_IMG_TMP.$img_name);

    }

    echo json_encode($result);
    exit();

} 

フロント側

$(document).on('click', '#submit', function(e){
    e.preventDefault();

    let formData = new FormData($('#imgForm').get()[0]);
    $.ajax({
        url:URL,
        type: 'post',
        processData: false,
        contentType: false,
        data: formData,
        success: console.log('send!')
    })

    .done( (data) => {
        console.log('success');
        console.log(data);
    })

    .fail( (data) => {
        console.log('faild');
        console.log(data);

    })

    .always( (data) => {
        console.log('always');
        {* console.log(data); *}

    });
});

よく使うひな形

$.ajax({
    url:'',
    type: 'post',
    //processData: false,
    //contentType: false,
    data: formData,
    dataType: 'json',
    success: console.log('send!')
})
.done( (data) => {

})
.fail( (data) => {
})
.always( (data) => {
});
}

Observable, promise例

参考

<p>observable</p>
const locationsSubscription = locations.subscribe({
  next(position) { console.log('Current Position: ', position); },
  error(msg) { console.log('Error Getting Location: ', msg); }
});

<p>promise</p>
hidouki2().then(function (value) {
  console.log(value);
}).catch(function (error) {
  console.log(error);
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【5分で動く】Reactで作るイケてるWEBアプリケーション

react.gif

背景

社内でReactを使ったWEBアプリケーションを開発するにあたって、UIフレームワークの導入から認証までやってくれるようなサンプルが見当たらなかったので、今回作成したものをテンプレっぽくして共有します。
これさえあれば、とりあえず簡単なWEBアプリは作れるのでぜひ活用していただければと思います。

概要

https://github.com/tonio0720/React-App

起動方法

git clone https://github.com/tonio0720/React-App

cd React-App

# react 起動
cd frontend
npm i
npm start

# express 起動
cd backend
npm i
npm start

今回利用したもの

  • React (Frontend Framework)
  • Create React App
  • Ant Design (UI Framework)
  • Axios (HTTP client)
  • echarts (Chart Library)
  • Express (Backend Framework)

解説

Reactアプリ自体はcreate react appを使って作成しました。
ただ色々と拡張する必要があったので、react-app-rewiredcustomize-craを使いました。
UIフレームワークにはAnt Designを使用しています。

バックエンドは認証処理をしたかったのでおまけ程度に書いています。
express-generatorを使って作りました。
認証にはexpress-jwtを使用しています。

空の画面だけでさみしかったので、echartsでダッシュボードっぽくしてみました。
データは僕のQiitaのダッシュボードから持ってきました。(APIではなく直書きです。)
グラフにはechartsを利用しています。

↓で詳細について説明してみます。

Ant Designの導入

Ant DesignはLESSで作られているので、Webpackで読み込める状態にする必要があります。
webpackのバージョン次第では、localIdentNameが云々とエラーが出てしまうので、そちらの対応もしています。

config-overrides.js
const path = require('path');
const {
    override,
    disableEsLint,
    fixBabelImports,
    addLessLoader,
    addWebpackAlias,
} = require('customize-cra');
const theme = require('./src/theme');

const modifyVars = {};
Object.keys(theme).forEach((key) => {
    modifyVars[`@${key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`] = theme[key];
});

const config = {
    webpack: override(
        disableEsLint(),
        addWebpackAlias({
            '@': path.resolve(__dirname, 'src')
        }),
        fixBabelImports('import', {
            libraryName: 'antd',
            libraryDirectory: 'es',
            style: true,
        }),
        // ★ここから
        addLessLoader({
            javascriptEnabled: true,
            modifyVars
        }),
        ((config) => {
            config.module.rules.forEach((rule) => {
                if (!rule.oneOf) {
                    return;
                }
                rule.oneOf.forEach((rule) => {
                    if (!rule.use) {
                        return;
                    }
                    rule.use.forEach((loader) => {
                        if (loader.options && loader.options.localIdentName) {
                            const { localIdentName } = loader.options;
                            delete loader.options.localIdentName;
                            loader.options.modules = { localIdentName };
                        }
                    });
                });
            });

            return config;
        }),
        // ★ここまで
    ),
    devServer: (configFunction) => {
        return (proxy, allowedHost) => {
            const config = configFunction(proxy, allowedHost);
            config.proxy = {
                '/api': {
                    target: 'http://localhost:3030',
                    pathRewrite: { '^/api': '' }
                }
            };
            return config;
        };
    },
};

module.exports = config;

JWT認証

Reactの認証処理はContextProviderを使って実装しています。
ContextProviderは下の階層にプロパティを引き渡すことができます。

ページ遷移の度に、バックエンドの/user/infoというところにリクエストを送り、検証をします。
成功の場合、useridとトークンを保存します。
失敗の場合、ログインページに戻します。

./src/contexts/Auth.js
import React, { useEffect, useState } from 'react';
import useReactRouter from 'use-react-router';

import { getToken, setToken, removeToken, gotoLogin } from '@/utils/auth';
import request from '@/utils/request';

export const AuthContext = React.createContext({});

export const AuthProvider = ({
    children
}) => {
    const { location: { pathname } } = useReactRouter();
    const [isLoggedIn, setIsLoggedIn] = useState(false);
    const [userid, setUserid] = useState(null);

    const checkAuth = () => {
        return request.post('/user/info', {});
    };

    useEffect(() => {
        setIsLoggedIn(false);

        if (pathname === '/login') {
            return;
        }

        const token = getToken();

        if (token) {
            checkAuth().then(({
                token,
                userid
            }) => {
                setToken(token);
                setIsLoggedIn(true);
                setUserid(userid);
            }).catch(() => {
                removeToken();
                gotoLogin();
            });
        } else {
            gotoLogin();
        }
    }, [pathname]);

    return (
        <AuthContext.Provider
            value={{
                isLoggedIn,
                userid
            }}
        >
            {(isLoggedIn || pathname === '/login') && children}
        </AuthContext.Provider>
    );
};

ログインページからは/user/loginにリクエストを送り、usernameとpasswordを検証します。
成功した場合は、tokenが返ってくるのでCookieに保存します。

./src/pages/Login/LoginForm.js
import React, { useState } from 'react';
import useReactRouter from 'use-react-router';
import {
    Form,
    Icon,
    Input,
    Button,
    Checkbox,
    Alert
} from 'antd';

import { setToken } from '@/utils/auth';
import request from '@/utils/request';

import styles from './index.module.less';

const LoginForm = ({
    form
}) => {
    const { history } = useReactRouter();
    const [error, setError] = useState(false);

    const handleSubmit = (e) => {
        e.preventDefault();
        form.validateFields((err, values) => {
            setError(false);
            if (!err) {
                request.post('/user/login', values).then(({ token }) => {
                    setToken(token);
                    history.push('/');
                }).catch(() => {
                    setError(true);
                });
            }
        });
    };

    const { getFieldDecorator } = form;
    return (
        <Form onSubmit={handleSubmit}>
            {error && (
                <Alert
                    description="Password Incorrect."
                    type="error"
                    showIcon
                    style={{ marginBottom: 16 }}
                />
            )}
            <span>username: admin, password: admin</span>
            <Form.Item>
                {getFieldDecorator('username', {
                    rules: [{ required: true, message: 'Please input your username!' }],
                })(
                    <Input
                        autocomplete="off"
                        prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
                        placeholder="Username"
                    />,
                )}
            </Form.Item>
            <Form.Item>
                {getFieldDecorator('password', {
                    rules: [{ required: true, message: 'Please input your Password!' }],
                })(
                    <Input
                        prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
                        type="password"
                        placeholder="Password"
                    />,
                )}
            </Form.Item>
            <Form.Item>
                {getFieldDecorator('remember', {
                    valuePropName: 'checked',
                    initialValue: true,
                })(<Checkbox>Remember me</Checkbox>)}
                <Button
                    type="primary"
                    htmlType="submit"
                    className={styles.loginFormButton}
                >
                    Log in
                </Button>
            </Form.Item>
        </Form>
    );
};

export default Form.create({ name: 'login' })(LoginForm);

終わりに

Reactは便利ですが、部分的なサンプルが多くまとまったものが少ないので不便に感じている人も多いのではないでしょうか。
Reactを始めるきっかけにしてもらえればうれしいです!

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

React + And Design + Expressで作るWEBアプリテンプレ―ト

react.gif

背景

社内でReactを使ったWEBアプリケーションを開発するにあたって、UIフレームワークの導入から認証までやってくれるようなサンプルが見当たらなかったので、今回作成したものをテンプレっぽくして共有します。
これさえあれば、とりあえず簡単なWEBアプリは作れるのでぜひ活用していただければと思います。

概要

https://github.com/tonio0720/React-App

起動方法

git clone https://github.com/tonio0720/React-App

cd React-App

# react 起動
cd frontend
npm i
npm start

# express 起動
cd backend
npm i
npm start

今回利用したもの

  • React (Frontend Framework)
  • Create React App
  • Ant Design (UI Framework)
  • Axios (HTTP client)
  • echarts (Chart Library)
  • Express (Backend Framework)

解説

Reactアプリ自体はcreate react appを使って作成しました。
ただ色々と拡張する必要があったので、react-app-rewiredcustomize-craを使いました。
UIフレームワークにはAnt Designを使用しています。

バックエンドは認証処理をしたかったのでおまけ程度に書いています。
express-generatorを使って作りました。
認証にはexpress-jwtを使用しています。

空の画面だけでさみしかったので、echartsでダッシュボードっぽくしてみました。
データは僕のQiitaのダッシュボードから持ってきました。(APIではなく直書きです。)
グラフにはechartsを利用しています。

↓で詳細について説明してみます。

Ant Designの導入

Ant DesignはLESSで作られているので、Webpackで読み込める状態にする必要があります。
webpackのバージョン次第では、localIdentNameが云々とエラーが出てしまうので、そちらの対応もしています。

config-overrides.js
const path = require('path');
const {
    override,
    disableEsLint,
    fixBabelImports,
    addLessLoader,
    addWebpackAlias,
} = require('customize-cra');
const theme = require('./src/theme');

const modifyVars = {};
Object.keys(theme).forEach((key) => {
    modifyVars[`@${key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`] = theme[key];
});

const config = {
    webpack: override(
        disableEsLint(),
        addWebpackAlias({
            '@': path.resolve(__dirname, 'src')
        }),
        fixBabelImports('import', {
            libraryName: 'antd',
            libraryDirectory: 'es',
            style: true,
        }),
        // ★ここから
        addLessLoader({
            javascriptEnabled: true,
            modifyVars
        }),
        ((config) => {
            config.module.rules.forEach((rule) => {
                if (!rule.oneOf) {
                    return;
                }
                rule.oneOf.forEach((rule) => {
                    if (!rule.use) {
                        return;
                    }
                    rule.use.forEach((loader) => {
                        if (loader.options && loader.options.localIdentName) {
                            const { localIdentName } = loader.options;
                            delete loader.options.localIdentName;
                            loader.options.modules = { localIdentName };
                        }
                    });
                });
            });

            return config;
        }),
        // ★ここまで
    ),
    devServer: (configFunction) => {
        return (proxy, allowedHost) => {
            const config = configFunction(proxy, allowedHost);
            config.proxy = {
                '/api': {
                    target: 'http://localhost:3030',
                    pathRewrite: { '^/api': '' }
                }
            };
            return config;
        };
    },
};

module.exports = config;

JWT認証

Reactの認証処理はContextProviderを使って実装しています。
ContextProviderは下の階層にプロパティを引き渡すことができます。

ページ遷移の度に、バックエンドの/user/infoというところにリクエストを送り、検証をします。
成功の場合、useridとトークンを保存します。
失敗の場合、ログインページに戻します。

./src/contexts/Auth.js
import React, { useEffect, useState } from 'react';
import useReactRouter from 'use-react-router';

import { getToken, setToken, removeToken, gotoLogin } from '@/utils/auth';
import request from '@/utils/request';

export const AuthContext = React.createContext({});

export const AuthProvider = ({
    children
}) => {
    const { location: { pathname } } = useReactRouter();
    const [isLoggedIn, setIsLoggedIn] = useState(false);
    const [userid, setUserid] = useState(null);

    const checkAuth = () => {
        return request.post('/user/info', {});
    };

    useEffect(() => {
        setIsLoggedIn(false);

        if (pathname === '/login') {
            return;
        }

        const token = getToken();

        if (token) {
            checkAuth().then(({
                token,
                userid
            }) => {
                setToken(token);
                setIsLoggedIn(true);
                setUserid(userid);
            }).catch(() => {
                removeToken();
                gotoLogin();
            });
        } else {
            gotoLogin();
        }
    }, [pathname]);

    return (
        <AuthContext.Provider
            value={{
                isLoggedIn,
                userid
            }}
        >
            {(isLoggedIn || pathname === '/login') && children}
        </AuthContext.Provider>
    );
};

ログインページからは/user/loginにリクエストを送り、usernameとpasswordを検証します。
成功した場合は、tokenが返ってくるのでCookieに保存します。

./src/pages/Login/LoginForm.js
import React, { useState } from 'react';
import useReactRouter from 'use-react-router';
import {
    Form,
    Icon,
    Input,
    Button,
    Checkbox,
    Alert
} from 'antd';

import { setToken } from '@/utils/auth';
import request from '@/utils/request';

import styles from './index.module.less';

const LoginForm = ({
    form
}) => {
    const { history } = useReactRouter();
    const [error, setError] = useState(false);

    const handleSubmit = (e) => {
        e.preventDefault();
        form.validateFields((err, values) => {
            setError(false);
            if (!err) {
                request.post('/user/login', values).then(({ token }) => {
                    setToken(token);
                    history.push('/');
                }).catch(() => {
                    setError(true);
                });
            }
        });
    };

    const { getFieldDecorator } = form;
    return (
        <Form onSubmit={handleSubmit}>
            {error && (
                <Alert
                    description="Password Incorrect."
                    type="error"
                    showIcon
                    style={{ marginBottom: 16 }}
                />
            )}
            <span>username: admin, password: admin</span>
            <Form.Item>
                {getFieldDecorator('username', {
                    rules: [{ required: true, message: 'Please input your username!' }],
                })(
                    <Input
                        autocomplete="off"
                        prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
                        placeholder="Username"
                    />,
                )}
            </Form.Item>
            <Form.Item>
                {getFieldDecorator('password', {
                    rules: [{ required: true, message: 'Please input your Password!' }],
                })(
                    <Input
                        prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
                        type="password"
                        placeholder="Password"
                    />,
                )}
            </Form.Item>
            <Form.Item>
                {getFieldDecorator('remember', {
                    valuePropName: 'checked',
                    initialValue: true,
                })(<Checkbox>Remember me</Checkbox>)}
                <Button
                    type="primary"
                    htmlType="submit"
                    className={styles.loginFormButton}
                >
                    Log in
                </Button>
            </Form.Item>
        </Form>
    );
};

export default Form.create({ name: 'login' })(LoginForm);

終わりに

Reactは便利ですが、部分的なサンプルが多くまとまったものが少ないので不便に感じている人も多いのではないでしょうか。
Reactを始めるきっかけにしてもらえればうれしいです!

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

contenteditableな要素への入力をイベントで検知する

結論

changeイベントは発火しないけど、inputイベントは発火しています。

以下のような要素があったとします。

<div id="editor" contenteditable="true"></div>

これに対する入力がなされたタイミングで処理したい場合があります。
そこで、changeイベントで取れるかな?と思って以下のように書きたくなると思います。

//ダメな例

let editor = document.getElementById("editor");
editor.addEventListener("change",function(){
   console.log("change");
});

しかし、入力してもchangeイベントは発火しません。
contenteditableは、次のようにinputイベントで受け取りましょう。

//よい例

let editor = document.getElementById("editor");
editor.addEventListener("input",function(){
   console.log("input");
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

動画をパララックスする

写真をパララックスにするプラグインの紹介などは結構あったのですが、動画は意外となかったのでメモ。
動画の埋め込み自体あんまり使うことないんですが、どうしてもやらなきゃいけない&時間かけられない時にさくっと実装できます。

動画をパララックスさせたい

1) 下記リンク先 backgroundVideo にアクセス、distフォルダ内のjsのどちらかを読み込ませる。
https://github.com/linnett/backgroundVideo

2) 任意のクラスをVIDEOタグにつけて、以下のようなJavaScriptの記述をする。

index.html
<video class="bv-video"></video>

<script>
new BackgroundVideo('.bv-video', {
  src: [
    //動画保存先のパス
    'MY/EXAMPLE/PATH/SAMPLE.mp4',
    'MY/EXAMPLE/PATH/SAMPLE.webm'
  ]
});
</script>

3) 高さを上書きしたい場合は、 .bv-video-wrap クラスに !important をつけて高さを上書きする。

style.css
.bv-video-wrap {
   height: 500px !important;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】簡単な税込み・割り勘計算アプリを作る

前置き

Vueの基礎文法の復習として以下の要件の税込み・割り勘計算アプリを作りました。
コンポーネントやcomputed、methods、v-on、v-modelなどの簡単な解説も添えたので、Vue初学者の参考になるかと思います。

要件

任意の数と人数が入力されたときに以下の二つの出力をできるようにする。(数、人数ともに整数とする)
1.税込みの値
2.税込み金額を人数で割った値

本編

さて、実際に作成に入っていきましょう。
一旦、完成物のコードを見て頂き、それを沿って解説を入れていこうと思います。

完成版ソースコード

<!DOCTYPE html>
<html>
    <head>
        <title>My first Vue app</title>
        <script src="https://unpkg.com/vue"></script>
        <style>
            .title{
                margin-top: 10px;
                margin-bottom: 10px;
                font-size: 24px;
            }
            #app{
                position: relative;
            }
            .type{
                margin-top: 3px;
                margin-bottom: 1px;
            }

            .coution_num{
                position: absolute;
                top: 15px;
                left: 165px;
                color: red;
                font-size: 10pt; 
            }
            .coution_people{
                position: absolute;
                top: 67px;
                left: 165px;
                color: red;
                font-size: 10pt; 
            }

           button{
               display: inline-block;

               width: 100px;
               margin-top: 5px;
               margin-left: 0px;
               margin-right: 0px;
           }


           .text_tax{
               position: absolute;
               top:125px;
               font-size: 20px;
           }
           .text_exam{
               position: absolute;
               top:160px;
               font-size: 20px;
           }
        </style>

    </head>

    <body>
        <h1 class="title">「税込み、割り勘」計算サイト</h1>
        <div id="app">
            <Calculation />   
        </div>
        <script>
        var calc = Vue.component('Calculation',
        {
            data:function(){
                return{num:'',tax_num:0,people_num:1,exam:0,flag_tax:false,flag_division:false};
            },
            computed:{
                tax:function(event){
                    tax_num = this.num * 1.10;
                    console.log(tax_num)
                    return Math.floor(tax_num);
                },
                exam_2:function(event){
                    tax_num = this.num * 1.10;
                    exam = tax_num / this.people_num;
                    console.log(exam);
                    return Math.ceil(exam);
                },
            },
            methods:{
                doAction:function(event){
                    this.flag_tax = !this.flag_tax;
                },
                doAction_2:function(event){
                    this.flag_division = !this.flag_division;
                }
            },

            template:'<div>\
            <p class="type">金額を入力してください</p>\
            <div><input type="number" min="1" v-model="num"></div>\
            <p class="coution_num" v-if="num < 0">※0以上の値を入れてください</p>\
            <p class="type">人数を入力してください</p>\
            <div><input type="number" min="1" v-model="people_num"></div>\
            <p class="coution_people" v-if="people_num <= 0">※1以上の値を入れてください</p>\
            <button v-on:click="doAction">税込み</button>\
            <button v-on:click="doAction_2">割り勘</button>\
            <p v-if="flag_tax && num >= 0" class="text_tax">税込み:{{tax}}円です</p>\
            <p v-if="flag_division && num >= 0 &&people_num > 0" class="text_exam">一人当たり:{{exam_2}}円です</p>\
            </div>'
        })


        var app = new Vue({
            el:'#app',
        })
        </script>

    </body>
</html>

解説

コンポーネントの出力

61~63行目:

以下の記述をすることで、「Calculationコンポーネント」を呼び出しています。

 <div id="app">
    <Calculation />   
 </div>

65~106行目:

HTMLとして出力される部分は、templateとして記述しています。
templateの中に書かれているHTMLが62行目に Calculation として呼び出されている形になります。

   template:'<div>\
            <p class="type">金額を入力してください</p>\
            <div><input type="number" min="1" v-model="num"></div>\
            <p class="coution_num" v-if="num < 0">※0以上の値を入れてください</p>\
            <p class="type">人数を入力してください</p>\
            <div><input type="number" min="1" v-model="people_num"></div>\
            <p class="coution_people" v-if="people_num <= 0">※1以上の値を入れてください</p>\
            <button v-on:click="doAction">税込み</button>\
            <button v-on:click="doAction_2">割り勘</button>\
            <p v-if="flag_tax && num >= 0" class="text_tax">税込み:{{tax}}円です</p>\
            <p v-if="flag_division && num >= 0 &&people_num > 0" class="text_exam">一人当たり:{{exam_2}}円です</p>\
            </div>'
        })

v-modelにて入力値が即時に反映されるようにする

94,97行目

上記のtemplateタグ内にてv-modelを使っています。
この構文はinputタグに入力された値をVueのdataプロパティの値やコンポーネントの変数にバインドする機能で、この機能を使用することで入力された値をリアルタイムに表示することができます。

今回はコンポーネント内の変数である「num(入力された金額)」、「people_num(割り勘の人数)」がinputに入力される度に変更が反映されています。

v-onにてボタンクリック時にイベントが起きるようにする

99~100行目

<button v-on:click="doAction">税込み</button>\
<button v-on:click="doAction_2">割り勘</button>\

v-onデレクティブは、イベントの属性に値をバインドする機能であり、この機能を使用することでVueオブジェクト内やコンポーネント内の変数を使用できるようになります。

今回はbuttonタグがクリックされた際に、「doAction」と「doAction_2」が発火するように記述しています。
「doAction」と「doAction_2」の具体的な処理はmethods内にて記述していきます。

なぜonclickが使えないのか??

今回、buttonタグをクリックした時にイベントが起きるようにしたいんですが、カウンター変数の値を増やそうとする時は「onclick」を使用しても上手く作動しません。

これはVueのコンポーネント内のdataプロパティで定義した値はコンポーネント内でしか使えないため、コンポーネントの外部であるonclickによってカウンター変数を使うことは出来ないからです。

このような場合にv-onディレクティブを使用します。
イベント名にはHTMLタグなどで使用するイベント名の「on」を取り除いた物を使用します。
例としては、「onclick→click」。

computedにて計算を行う

70行目~81行目:

computed:{
                tax:function(event){
                    tax_num = this.num * 1.10;
                    console.log(tax_num)
                    return Math.floor(tax_num);
                },
                exam_2:function(event){
                    tax_num = this.num * 1.10;
                    exam = tax_num / this.people_num;
                    console.log(exam);
                    return Math.ceil(exam);
                },
            },

computed(算術プロパティ)を使用しています。
今回は、「tax(入力値に税率をかける処理)」と「exam_2(入力値に税率値をかけた値を入力された人数で割る処理)」の二種類を定義していて、それぞれの値をreturnで返しています。
このようにreturnで返した値をtemplete内で使用しています。
また、「this.変数名」はコンポーネント内の変数で、「num(入力された金額)」、「people_num(割り勘の人数)」を持ってきています。

console.logの部分は計算値の確認の為に、記述しています。

methodsにてボタンが押された時の処理を書く

83行目~90行目:

 methods:{
                doAction:function(event){
                    this.flag_tax = !this.flag_tax;
                },
                doAction_2:function(event){
                    this.flag_division = !this.flag_division;
                }
            },

methodを使用して、税込みボタンと割り勘ボタンが押された時の処理を書いています。
先ほどのv-onにて設定された「doActuon」と「doAction_2」の具体的な処理が書いてある形です。

処理内容は「ボタンを押した時に判定値が反転する処理」としていて、これによってボタンを押した時に、templete内のv-ifでの条件分岐が行われるようにしています。

終わりに

現在、定期的にVue初学者向きのコンテンツを発信しているので、興味ある方はぜひ!!

Twitter:https://twitter.com/teriteriteriri
ブログ:https://terrblog.com/

また、今回の試みは自身の学習のために作った事もある為、指摘やアドバイス等、お待ちしております。

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

【Vue.js】v-forで配列をn個ずつ描画する

やりたいこと

kiji.jpg

このように、配列をn個(今回は3個)並べたら改行したい。
前提:配列はAPI等で受け取るデータで、何個来るかわからない。

v-forで描画するまでやってる記事は見かけなかったのでメモです。

そのままv-for

<template>
  <div>
    <div v-for="(item, index) in array" :key="index">
      <li>{{ item }}</li>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      array: [
        'AMETHYST',
        'BLUE-SAPPHIRE',
        'CITRIN',
        'DIAMOND',
        ...
      ]
    }
  }
}
</script>

配列分割して3個ずつv-for

computedで配列を3個ずつに分割し、それをさらに配列に入れたものを作ります。
配列の中に配列を作るイメージ。

computed: {
  groupedArray() {
    const base = this.array.length
    const split_cnt = 3       // 何個ずつに分割するか
    const grouped_array = []
    for (let i=0; i<Math.ceil(base/split_cnt); i++) {
      let multiple_cnt = i * split_cnt  // 3の倍数
      // (i * 3)番目から(i * 3 + 3)番目まで取得
      let result = this.array.slice(multiple_cnt, multiple_cnt + split_cnt) 
      grouped_array.push(result)
    }
    return grouped_array
    // [
    //   ["AMETHYST", "BLUE-SAPPHIRE", "CITRIN"],
    //   ["DIAMOND", "EMERALD", "FIRE-OPAL"], ...
    // ]
  }
}

外側の配列でv-forし、その中で内側の配列をv-forします(伝われ)。

<div v-for="(items, index) in groupedArray" :key="index">
  <li v-for="(item, index) in items" :key="index">
    {{ item }}
  </li>
</div>

おわり。

さいごに

画像をサムネイル表示するときに使いました。
みなさまのご参考になれば幸いです。

参考記事

配列をn個ずつの配列に分割して、それをまとめた配列を作る

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

【React v16.8 FunctionComponent + TypeScript】コンポーネント作成のド基礎

react超初心者用です。
現場でReact v16.8からのFunctionComponent型(今後FCと呼びます)を使用する機会があったのでメモ程度にド基礎をまとめました。
間違っているところがあればご指摘いただけると幸いです!!
超簡単なプロフィールを表示するコンポーネントでまとめます。

全体

import React from 'react'

interface PersonProps {
  name: string,
  age?: number,
  agree?: Function,
}

const Person: React.FC<PersonProps> = props => {
  const {
    name,
    age,
  } = props

  const agree = () => {
    console.log(`${name}${age}才です`)
  }

  return (
    <React.Fragment>
      <span>name : {name}</span><br />
      <span>age : {age}</span><br />
      <button onClick={agree}>agree</button>
    </React.Fragment>
  );
}

export default Person

順を追って説明します。

必要モジュールのインポート

必要なのはReactのみなのでReactをインポートします。

import React from 'react'

プロパティやメゾットの定義

interface ParsonProps {
  name: string,
  age?: number,
}

interfaceを使ってプロパティやメゾットの型の定義をしていきます。
interface 名前で定義できます。

プロパティorメゾット: 型で使用する型を決められます。
親からpropsで渡ってくる時、ここに記述している型以外の型の場合と、空の場合は、TypeScriptがエラーを知らせてくれます。
age?: numberの『?』は省略可能という意味で、プロパティを使用しない場合にはundifindeが入ります。
空になる可能性のあるプロパティ、メゾットにはつけてあげるといいでしょう。

interfaceはtypeでもほぼ同じことができるそうです。
違いについて知りたい方はTypeScriptのInterfaceとTypeの比較
という記事が参考になったので是非

コンポーネント作成

const Person: React.FC<PersonProps> = props => {
  const {
    name,
    age,
  } = props

  const agree = () => {
    console.log(`${name}${age}才です`)
  }

  return (
    <React.Fragment>
      <span>name : {name}</span><br />
      <span>age : {age}</span><br />
      <button onClick={agree}>agree</button>
    </React.Fragment>
  );
}

分けて見ていきます。

Personというコンポネントを作る宣言

const Person: React.FC<PersonProps> = props => {}

型にReactFC型を用いてあげて、ジェネリクスに上記で作成したinterfaceを指定してあげます。
ジェネリクスを用いることによってinterface内に複数、型があってもまとめて記述できます。
そしてpropsを引数に渡してあげます。
これによって、コンポネント内でprops.xxxすると呼び出せます。

propsの扱いと関数

const {
    name,
    age,
  } = props

  const agree = () => {
    console.log(`${name}${age}才です`)
  }

コンポネント内でprops.xxxすると呼び出せます。

と書きましたが、propsは僕はまず分裂代入してから使います。
propsの中身をそれぞれname,ageに代入しなおしてあげます。
これによってagree関数を見ると、props.の記述が不要になることがわかります。
nameもageもagree()もinterfaceで型定義してあげたから使用できるという点が大切かと思います。

render、エクスポート

return (
    <React.Fragment>
      <span>name : {name}</span><br />
      <span>age : {age}</span><br />
      <button onClick={agree}>agree</button>
    </React.Fragment>
  );
export default Person

レンダリングのところは通常どおりです。

親で使ってみる

import Person from '../Person'

<Person
  name="jon"
  age={12}
/>

親で使用時に注意なのが、name,ageには決められた型以外が入るとエラーになる点です。
これもTypeScriptの機能です。
ageは省略可能にしてあるので、プロパティの記述がなくてもエラーになりません。

スクリーンショット 2020-02-10 10.57.36.png
こんな感じででてきて、ボタンを押すとconsoleに『jonは12才です』とでます

まとめ

最初にFC+TypeScriptで書かれてるのをみたときなんじゃこりゃと思いましたが、一つ一つ勉強していくと世界がかわりました。
これがプログラミングのいいところだなと思って嬉しく思ってます。
よんでいただきありがとうございました。
修正点、改善点ございましたらお待ちしています。

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

JavaScriptで文字列をHTML Entityに変換する

HTML Entityへ変換

通常の文字列ならcharCodeAtで解決できますが、サロゲートペアは文字が崩れてしまうのでcodePointAtを利用します。

function convert(value) {
    let result = '';

    for (const word of value) {
        result += `&#x${word.codePointAt(0).toString(16)};`;
    }

    return result;
}

Use case

console.log(convert('?')); // > &#x1f607;
<p>こんにちは&#x1f607;</p>

結果:

こんにちは?

HTML Entityから元に戻す

復元する場合はString.fromCharCodeの代わりにString.fromCodePointを利用します。

function revert(str) {
    return str.replace(/&#(.*?);/g, (m, p1) => String.fromCodePoint(`0${p1}`));
}

Use case

console.log(revert('こんにちは&#x1f607;&#x1f607;&#x1f607;&#x1f607;')); // > こんにちは????
<p>こんにちは????</p>

結果:

こんにちは????

HTML Entityとは

HTML エンティティとは、アンパサンド (&) で始まりセミコロン (;)で終わるテキスト (文字列) のひと固まりです。エンティティは(通常は HTML コードとして解釈される)予約済み文字や、(ノーブレークスペースのように) 見えない文字を表示するためによく使用されます。標準キーボードでは入力が難しい文字の代わりに使用することもできます。

https://developer.mozilla.org/ja/docs/Glossary/Entity より引用

何がうれしいの?

絵文字などのサロゲートペアに対応していないようなCMSなどでもサロゲートペアが使えるようになります(たぶん)。

※ HTMLを直接記述できるようなものの場合のみ

参考文献

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

JavaScriptでサロゲートペアを含む文字列をHTML Entityに変換する

HTML Entityへ変換

通常の文字列ならcharCodeAtで解決できますが、サロゲートペアは文字が崩れてしまうのでcodePointAtを利用します。

function convert(value) {
    let result = '';

    for (const word of value) {
        result += `&#x${word.codePointAt(0).toString(16)};`;
    }

    return result;
}

Example

console.log(convert('?')); // > &#x1f607;
<p>こんにちは&#x1f607;</p>

結果:

こんにちは?

HTML Entityから元に戻す

復元する場合はString.fromCharCodeの代わりにString.fromCodePointを利用します。

function revert(str) {
    return str.replace(/&#(.*?);/g, (m, p1) => String.fromCodePoint(`0${p1}`));
}

Example

console.log(revert('こんにちは&#x1f607;&#x1f607;&#x1f607;&#x1f607;')); // > こんにちは????

HTML Entityとは

HTML エンティティとは、アンパサンド (&) で始まりセミコロン (;)で終わるテキスト (文字列) のひと固まりです。エンティティは(通常は HTML コードとして解釈される)予約済み文字や、(ノーブレークスペースのように) 見えない文字を表示するためによく使用されます。標準キーボードでは入力が難しい文字の代わりに使用することもできます。

https://developer.mozilla.org/ja/docs/Glossary/Entity より引用

何がうれしいの?

絵文字などのサロゲートペアに対応していないようなCMSなどでもサロゲートペアが使えるようになります(たぶん)。

※ HTMLを直接記述できるようなものの場合のみ

参考文献

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

「ピーターの問題」をReact+Typescriptで解けるようにしてみた

先日、Twitterで「ピーターの問題」というのを見かけました。

空欄に数字を入れて式を成立させるという問題です。
自分もJSで解こうとしたのですが、解くのはとても難しそうだったので、
ピーターの問題を解くためのツール
を代わりにJS(TypeScript)で作ってみました!

bad.png

  • create-react-app したものをベースに実装
  • TypeScriptを使用
  • React Hooks (useReducer) がメイン

な構成です。

以下のページで実際に動作できます。

ピーターからの問題

  • 重複したものは選べないようにする
  • 選んだ数字で計算した場合の答えを出す

ようにしたので、問題を解く手助けになれば幸いです。

補足

計算の仕方

values[0] / (values[1] * 10 + values[2]) +
values[3] / (values[4] * 10 + values[5]) +
values[6] / (values[7] * 10 + values[8])

肝心?の計算部分は、このような処理で求めました。

式の表示の仕方

flexboxを使って横並べで表示しています。

.container {
  // これで横並びにしている
  display: flex;
  flex-direction: row;
}

.box {
  width: 100px;
  height: 100px;
  text-align: center;
}

.ope {
  width: 2em;
  height: 100px;
  line-height: 50px; // +  = を縦中央に表示
  text-align: center;
}

お遊びなので、とりあえずそれっぽく出せればいいの精神で適当に。
分数の線?は hr タグ使って表示して手を抜いてます。

useReducer

同じ数字は1度しか使えないという制約があるため、
- どの数字を選んでいるか
- どの数字が選べるか
の管理が必要になります。

一つのセレクトボックスの状態を管理するだけであれば useState というhooksを使うのがお手軽なんですが、今回は相互にセレクトボックスに影響を与えたりして、やや複雑な状態管理が必要だったので useReducer hooks を使いました。

セレクトボックス(をラップしたコンポーネント)は props として受け取った dispatch 経由でアクションを呼ぶだけで、あくまで変更処理は reducer にまとまっています。
初期化処理をあとから追加しましたが、こういう風に実装しておくと簡単に追加でき、かつ状態の変更処理が一箇所にまとまって良いですね。

typeガード

このツールでは、すべての空欄を埋めたときに答えを計算するようにしています。
選択された数字は (null|number)[] という型で扱っていて、 null が含まれていないときに計算するようにしています。

type Value = null | number;
const isNumbers = (numbers: Value[]): numbers is number[] => {
  return -1 === numbers.indexOf(null);
};

// 中略

// values: Values[] な変数・配列がある。

// values[i] はこの時点では、 nullかnumber

if (isNumbers(values)) {
  // values が number[] とTypescriptに教えたため、算術演算できる
  const answer = values[0] / (values[1] * 10 + values[2]) // 中略 
}

実際の全体のコード

codesandbox に載せてあります。
メインとなるファイルだけ、ここにも貼っておきます。

import React, { useReducer, Dispatch } from 'react';
import './App.css';

type Value = null | number;

type State = {
  values: Value[];
  candidates: number[];
};

const initialState: State = {
  values: [null, null, null, null, null, null, null, null, null],
  candidates: [1, 2, 3, 4, 5, 6, 7, 8, 9]
};

type Action =
  | {
      type: 'select';
      index: number;
      value: Value;
    }

  | {
      type: 'clear';
    };

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'select':
      const newValues = state.values.concat();
      newValues[action.index] = action.value;

      const newCandidates = initialState.candidates.concat().filter(n => {
        return -1 === newValues.indexOf(n);
      });

      return { values: newValues, candidates: newCandidates };
    case 'clear':
      return initialState;
    default:
      return state;
  }
};

const isNumbers = (numbers: Value[]): numbers is number[] => {
  return -1 === numbers.indexOf(null);
};

const App = () => {
  const [{ values, candidates }, dispatch] = useReducer(reducer, initialState);

  const answer = isNumbers(values)
    ? values[0] / (values[1] * 10 + values[2]) +
      values[3] / (values[4] * 10 + values[5]) +
      values[6] / (values[7] * 10 + values[8])
    : null;

  const createChoice = (i: number): React.ReactNode => {
    return (
      <Choice
        index={i}
        value={values[i]}
        candidates={candidates.concat()}
        dispatch={dispatch}
      />
    );
  };

  return (
    <div className="app">
      <h1>ピーターからの問題</h1>
      <div className="container">
        <div className="box">
          {createChoice(0)}
          <hr />
          {createChoice(1)}
          {createChoice(2)}
        </div>
        <div className="ope">+</div>
        <div className="box">
          {createChoice(3)}
          <hr />
          {createChoice(4)}
          {createChoice(5)}
        </div>
        <div className="ope">+</div>
        <div className="box">
          {createChoice(6)}
          <hr />
          {createChoice(7)}
          {createChoice(8)}
        </div>
        <div className="ope">=</div>
        <div className="answer">
          {answer ? Math.round(answer * 1000) / 1000 : '?'}
        </div>
      </div>
      <p>
        上の式の答えが1になるように1から9までの数字を1個ずつ入れてください。
      </p>
      <button
        onClick={() => {
          dispatch({ type: 'clear' });
        }}
      >
        最初の状態に戻す
      </button>
    </div>
  );
};

type Props = {
  index: number;
  value: Value;
  candidates: number[];
  dispatch: Dispatch<Action>;
};
const Choice: React.FC<Props> = ({ index, value, candidates, dispatch }) => {
  const onChange = (e: React.ChangeEvent<HTMLSelectElement>): void => {
    dispatch({
      type: 'select',
      index: index,
      value: '' === e.target.value ? null : parseInt(e.target.value, 10)
    });
  };

  if (null !== value) {
    candidates.push(value);
    candidates.sort();
  }

  return (
    <select value={value || ''} onChange={onChange}>
      <option value="">-</option>
      {candidates.map(n => {
        return (
          <option key={n} value={n}>
            {n}
          </option>
        );
      })}
    </select>
  );
};

export default App;

答え

ちなみに答えはこんな感じ。

answer.png

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

「ピーターからの問題」をReact+Typescriptで解けるようにしてみた

先日、Twitterで「ピーターからの問題」というのを見かけました。

空欄に数字を入れて式を成立させるという問題です。
自分もJSで解こうとしたのですが、解くのはとても難しそうだったので、
ピーターからの問題を解くためのツール
を代わりにJS(TypeScript)で作ってみました!

bad.png

  • create-react-app したものをベースに実装
  • TypeScriptを使用
  • React Hooks (useReducer) がメイン

な構成です。

以下のページで実際に動作できます。

ピーターからの問題

  • 重複したものは選べないようにする
  • 選んだ数字で計算した場合の答えを出す

ようにしたので、問題を解く手助けになれば幸いです。

補足

計算の仕方

values[0] / (values[1] * 10 + values[2]) +
values[3] / (values[4] * 10 + values[5]) +
values[6] / (values[7] * 10 + values[8])

肝心?の計算部分は、このような処理で求めました。

式の表示の仕方

flexboxを使って横並べで表示しています。

.container {
  // これで横並びにしている
  display: flex;
  flex-direction: row;
}

.box {
  width: 100px;
  height: 100px;
  text-align: center;
}

.ope {
  width: 2em;
  height: 100px;
  line-height: 50px; // +  = を縦中央に表示
  text-align: center;
}

お遊びなので、とりあえずそれっぽく出せればいいの精神で適当に。
分数の線?は hr タグ使って表示して手を抜いてます。

useReducer

同じ数字は1度しか使えないという制約があるため、
- どの数字を選んでいるか
- どの数字が選べるか
の管理が必要になります。

一つのセレクトボックスの状態を管理するだけであれば useState というhooksを使うのがお手軽なんですが、今回は相互にセレクトボックスに影響を与えたりして、やや複雑な状態管理が必要だったので useReducer hooks を使いました。

セレクトボックス(をラップしたコンポーネント)は props として受け取った dispatch 経由でアクションを呼ぶだけで、あくまで変更処理は reducer にまとまっています。
初期化処理をあとから追加しましたが、こういう風に実装しておくと簡単に追加でき、かつ状態の変更処理が一箇所にまとまって良いですね。

typeガード

このツールでは、すべての空欄を埋めたときに答えを計算するようにしています。
選択された数字は (null|number)[] という型で扱っていて、 null が含まれていないときに計算するようにしています。

type Value = null | number;
const isNumbers = (numbers: Value[]): numbers is number[] => {
  return -1 === numbers.indexOf(null);
};

// 中略

// values: Values[] な変数・配列がある。

// values[i] はこの時点では、 nullかnumber

if (isNumbers(values)) {
  // values が number[] とTypescriptに教えたため、算術演算できる
  const answer = values[0] / (values[1] * 10 + values[2]) // 中略 
}

実際の全体のコード

codesandbox に載せてあります。
メインとなるファイルだけ、ここにも貼っておきます。

import React, { useReducer, Dispatch } from 'react';
import './App.css';

type Value = null | number;

type State = {
  values: Value[];
  candidates: number[];
};

const initialState: State = {
  values: [null, null, null, null, null, null, null, null, null],
  candidates: [1, 2, 3, 4, 5, 6, 7, 8, 9]
};

type Action =
  | {
      type: 'select';
      index: number;
      value: Value;
    }

  | {
      type: 'clear';
    };

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'select':
      const newValues = state.values.concat();
      newValues[action.index] = action.value;

      const newCandidates = initialState.candidates.concat().filter(n => {
        return -1 === newValues.indexOf(n);
      });

      return { values: newValues, candidates: newCandidates };
    case 'clear':
      return initialState;
    default:
      return state;
  }
};

const isNumbers = (numbers: Value[]): numbers is number[] => {
  return -1 === numbers.indexOf(null);
};

const App = () => {
  const [{ values, candidates }, dispatch] = useReducer(reducer, initialState);

  const answer = isNumbers(values)
    ? values[0] / (values[1] * 10 + values[2]) +
      values[3] / (values[4] * 10 + values[5]) +
      values[6] / (values[7] * 10 + values[8])
    : null;

  const createChoice = (i: number): React.ReactNode => {
    return (
      <Choice
        index={i}
        value={values[i]}
        candidates={candidates.concat()}
        dispatch={dispatch}
      />
    );
  };

  return (
    <div className="app">
      <h1>ピーターからの問題</h1>
      <div className="container">
        <div className="box">
          {createChoice(0)}
          <hr />
          {createChoice(1)}
          {createChoice(2)}
        </div>
        <div className="ope">+</div>
        <div className="box">
          {createChoice(3)}
          <hr />
          {createChoice(4)}
          {createChoice(5)}
        </div>
        <div className="ope">+</div>
        <div className="box">
          {createChoice(6)}
          <hr />
          {createChoice(7)}
          {createChoice(8)}
        </div>
        <div className="ope">=</div>
        <div className="answer">
          {answer ? Math.round(answer * 1000) / 1000 : '?'}
        </div>
      </div>
      <p>
        上の式の答えが1になるように1から9までの数字を1個ずつ入れてください。
      </p>
      <button
        onClick={() => {
          dispatch({ type: 'clear' });
        }}
      >
        最初の状態に戻す
      </button>
    </div>
  );
};

type Props = {
  index: number;
  value: Value;
  candidates: number[];
  dispatch: Dispatch<Action>;
};
const Choice: React.FC<Props> = ({ index, value, candidates, dispatch }) => {
  const onChange = (e: React.ChangeEvent<HTMLSelectElement>): void => {
    dispatch({
      type: 'select',
      index: index,
      value: '' === e.target.value ? null : parseInt(e.target.value, 10)
    });
  };

  if (null !== value) {
    candidates.push(value);
    candidates.sort();
  }

  return (
    <select value={value || ''} onChange={onChange}>
      <option value="">-</option>
      {candidates.map(n => {
        return (
          <option key={n} value={n}>
            {n}
          </option>
        );
      })}
    </select>
  );
};

export default App;

答え

ちなみに答えはこんな感じ。

answer.png

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

JavaScriptで簡潔な、複数のキーでのオブジェクトの並び替え

JavaScriptで複数のキーでオブジェクトを並び替えるプログラムです。

例えば、

const fruitsArr = [
  {name: 'ぶどう',   price: 5000, weight: 1000},
  {name: 'もも',    price: 3000, weight: 2000},
  {name: 'りんご',   price: 5000, weight: 5000},
  {name: 'バナナ',  price: 1500, weight: 1000},
  {name: 'メロン',   price: 5000, weight: 1200},
  {name: 'マンゴー', price: 10000, weight: 900},
  {name: 'みかん',  price: 1500, weight: 5000},
]

このような配列を、priceの昇順、同じpriceであればweightの昇順で並び変えます。
compareByAttr関数を作成し、このように書くとシンプルです。compareByAttrは下で実装します。

fruitsArr.sort((o1, o2) => 
  compareByAttr(o1, o2, ['price', 'weight'])
)

console.log(fruitsArr)
//=> [
//      {name: "バナナ", price: 1500, weight: 1000},

//      {name: "みかん", price: 1500, weight: 5000},

//      {name: "もも",   price: 3000, weight: 2000},

//      {name: "ぶどう",  price: 5000, weight: 1000},

//      {name: "メロン",  price: 5000, weight: 1200},
//      {name: "りんご",  price: 5000, weight: 5000},
//      {name: "マンゴー", price: 10000, weight: 900}
// ]

compareByAttrと関連する関数の作成

上記コードのcompareByAttr関数と関連する関数の定義と実装です。

const zipLongest = (...arrays) => {
  const length = Math.max(...(arrays.map(arr => arr.length)))
  return new Array(length).fill().map((_, i) => arrays.map(arr => arr[i]))
}

const compareArr = (arr1, arr2) => {
  const difference = zipLongest(arr1, arr2).find(([v1, v2]) => v1 !== v2)
  if (!difference) {
    return 0
  }
  if (difference[0] > difference[1]) {
    return 1
  }
  return -1
}

const compareByAttr = (o1, o2, attrs) => {
  const o1Values = attrs.map(attr => o1[attr])
  const o2Values = attrs.map(attr => o2[attr])
  return compareArr(o1Values, o2Values)
}

コードの解説

compareByAttr

compareByAttr関数は、2つのオブジェクト(o1o2)と、1つの配列(attrs)を受け取ります。配列には、比較する対象のキーが、優先度の高い順番に並べられています。

まず、各オブジェクトからattrsで指定されたキーを抜き出して配列にします。
以下のように動作します。

const main = () => {
  const o1 = {name: 'ぶどう', price: 5000, weight: 1000}
  const o2 = {name: 'みかん', price: 1500, weight: 5000}

  compareByAttr(o1, o2, ['price', 'weight'])
}

const compareByAttr = (o1, o2, attrs) => {
  const o1Values = attrs.map(attr => o1[attr])
  //=> [5000, 1000]
  const o2Values = attrs.map(attr => o2[attr])
  //=> [1500, 5000]
}

main()

このようにして得られた結果をcompareArr関数に渡してその結果を返します。

const main = () => {
  const o1 = {name: 'ぶどう', price: 5000, weight: 1000}
  const o2 = {name: 'みかん', price: 1500, weight: 5000}

  compareByAttr(o1, o2, ['price', 'weight'])
}

const compareByAttr = (o1, o2, attrs) => {
  const o1Values = attrs.map(attr => o1[attr])
  //=> [5000, 1000]
  const o2Values = attrs.map(attr => o2[attr])
  //=> [1500, 5000]
  return compareArr(o1Values, o2Values)
  //=> return compareArr([5000, 1000], [1500, 5000])
}

main()

zipLongest

compareArr関数の解説をする前に、zipLongest関数を解説します。この関数は、各配列の同じインデックスの要素をまとめます。各配列の長さが異なる場合には、一番長い配列の長さになります。未定義値はundefinedになります。

const a1 = [1, 2, 3]
const a2 = ['Jan', 'Feb', 'Mar']

zipLongest(a1, a2)
//=> [[1, "Jan"], [2, "Feb"], [3, "Mar"]]

const a3 = [1, 2, 3]
const a4 = ['Jan', 'Feb', 'Mar', 'Apr', 'May']

zipLongest(a3, a4)
//=> [[1, "Jan"], [2, "Feb"], [3, "Mar"], [undefined, "Apr"], [undefined, "May"]]

zipLongest関数では、レスト構文を使用して引数全てをarraysに格納しています。
最終的に作成する配列の長さは、各配列の最大値なので、Math.max関数を使用して長さを求めます。
最後に、各配列の同じインデックスの値をまとめます。

const zipLongest = (...arrays) => {
  const length = Math.max(...(arrays.map(arr => arr.length)))
  return new Array(length).fill().map((_, i) => arrays.map(arr => arr[i]))
}

もっと詳細なコードの解説は参考リンクを参照してください。

compareArr

compareArr関数は、2つの配列(arr1,arr2)を受け取り、配列を最初から値を比べていき、arr1の値の方が大きければ1arr2の値の方が大きければ-1を返します。同じ値であれば次の値を見ます。全て同じ値であれば0を返します。

最初の値をみて、違うならばその値同士で判定、同じならば次の値を見る、という操作は、"最初に違う値が出たところで、大小を判定する"と言い換えることができます。そこで、配列を最初から見ていって、違う値が出たところを取得します。

まず、zipLongest関数で2つの配列の同じインデックスをまとめましょう。

const main = () => {
  const arr1 = [100, 500]
  const arr2 = [100, 300]

  compareArr(arr1, arr2)
}

const compareArr = (arr1, arr2) => {
  zipLongest(arr1, arr2)
  //=> [[100, 100], [500, 300]]
}

main()

次に、最初に違う値になる時のarr1arr2の値を取得します。Array.prototype.find()メソッドを使用します。このメソッドは、渡された関数を満たす配列内の最初の要素の値を返します。
find()メソッドに([v1, v2]) => v1 !== v2を渡し、値が等しくない最初の値を取得します。

const main = () => {
  const arr1 = [100, 500]
  const arr2 = [100, 300]

  compareArr(arr1, arr2)
}

const compareArr = (arr1, arr2) => {
  const difference = zipLongest(arr1, arr2).find(([v1, v2]) => v1 !== v2)
  //=> [[100, 100], [500, 300]].find(([v1, v2]) => v1 !== v2)
  //=> [500, 300]
}

main()

違う値が見つからない場合(全て同じ値)のときは、undefinedになります。

const main = () => {
  const arr1 = [100, 500]
  const arr2 = [100, 500]

  compareArr(arr1, arr2)
}

const compareArr = (arr1, arr2) => {
  const difference = zipLongest(arr1, arr2).find(([v1, v2]) => v1 !== v2)
  //=> undefined
}

main()

differenceundefinedの場合は等しいので0を返します。
それ以外の場合はarr1の値(difference[0])の値が大きければ1、そうでなければ-1を返します。

const main = () => {
  const arr1 = [100, 500]
  const arr2 = [100, 300]

  compareArr(arr1, arr2)
}

const compareArr = (arr1, arr2) => {
  const difference = zipLongest(arr1, arr2).find(([v1, v2]) => v1 !== v2)
  //=> [500, 300]
  if (!difference) {
    return 0
  }
  if (difference[0] > difference[1]) {
    // この例では1がreturnされる
    return 1
  }
  return -1
}

main()

まとめ

もう一度全てのコードをコメント付きで書いておきます。コード量は多いですが、呼び出し側はすっきりします。
また、個々の関数はそれ自体で独立性が高く、他の用途でも有用です。

/**
 * 各配列の同じインデックスの要素をまとめます。
 * 各配列の長さが異なる場合には、一番長い配列の長さになります。
 * 未定義値はundefinedになります。
 * 
 * @param  {...Array} arrays 
 */
const zipLongest = (...arrays) => {
  const length = Math.max(...(arrays.map(arr => arr.length)))
  return new Array(length).fill().map((_, i) => arrays.map(arr => arr[i]))
}

/**
 * 2つの配列を比較します。
 * 配列の要素をインデックスの小さい順に比較し、arr1の要素の方が大きければ1を返し、arr2の要素の方が大きければ-1を返します。
 * 全ての値が等しいときは、0を返します。
 * 
 * @param {Array} arr1 比較する配列1個目
 * @param {Array} arr2 比較する配列2個目
 */
const compareArr = (arr1, arr2) => {
  const difference = zipLongest(arr1, arr2).find(([v1, v2]) => v1 !== v2)
    if (!difference) {
      return 0
    }
    if (difference[0] > difference[1]) {
      return 1
    }
    return -1
}

/**
 * 与えられたキーによってオブジェクトの大小を判定します。
 * 与えられたキーに対応する値が、o1の方が大きい場合には1、o2の方が大きい場合には-1を返します。
 * 等しい場合には0を返します。 
 * 
 * @param {Object} o1 比較対象の1個目のオブジェクト
 * @param {Object} o2 比較対象の2個目のオブジェクト
 * @param {Array} attrs 比較する対象のキー。優先度の高い順番に並べる
 */
const compareByAttr = (o1, o2, attrs) => {
  const o1Values = attrs.map(attr => o1[attr])
  const o2Values = attrs.map(attr => o2[attr])
  return compareArr(o1Values, o2Values)
}

// --- 以下、使用方法と実行結果 ---

const fruitsArr = [
  {name: 'ぶどう',   price: 5000, weight: 1000},
  {name: 'もも',     price: 3000, weight: 2000},
  {name: 'りんご',   price: 5000, weight: 5000},
  {name: 'バナナ',   price: 1500, weight: 1000},
  {name: 'メロン',   price: 5000, weight: 1200},
  {name: 'マンゴー', price: 10000, weight: 900},
  {name: 'みかん',  price: 1500, weight: 5000},
]

// priceの昇順、weightの昇順の優先順で並び替え
fruitsArr.sort((o1, o2) => 
  compareByAttr(o1, o2, ['price', 'weight'])
)

console.log(fruitsArr)
//=>[
//      {name: "バナナ", price: 1500, weight: 1000},

//      {name: "みかん", price: 1500, weight: 5000},

//      {name: "もも",   price: 3000, weight: 2000},

//      {name: "ぶどう",  price: 5000, weight: 1000},

//      {name: "メロン",  price: 5000, weight: 1200},
//      {name: "りんご",  price: 5000, weight: 5000},
//      {name: "マンゴー", price: 10000, weight: 900}
// ]

// priceの降順、weightの降順の優先順で並び替え
fruitsArr.sort((o1, o2) => 
  -1 * compareByAttr(o1, o2, ['price', 'weight'])
)
//=> [
// {name: "マンゴー", price: 10000, weight: 900},
// {name: "りんご", price: 5000, weight: 5000},
// {name: "メロン", price: 5000, weight: 1200},
// {name: "ぶどう", price: 5000, weight: 1000},
// {name: "もも", price: 3000, weight: 2000},
// {name: "みかん", price: 1500, weight: 5000},
// {name: "バナナ", price: 1500, weight: 1000}

まとめ その2

"javascript ソート 複数キー"で検索すると、以下のようなコードが出てきます。

fruitsArr.sort((o1,o2) => {
    if(o1.price > o2.price) return 1;
    if(o1.price < o2.price) return -1;
    if(o1.weight > o2.weight) return 1;
    if(o1.weight < o2.weight) return -1;
    return 0;
});

もちろん、これでも動作します。ただ、priceだけで4回も書かなければいけないことが面倒ですし、変更時に漏れも発生します。
また、この程度であればバグの入り込む余地も少なく、レビューもそれほど大変ではありません。しかし、数が大きくなってきたときに、今回のような実装も検討してはいかがでしょうか。

あとがき

Pythonだと、2つ(やそれ以上)のキーで並び替える時には、以下のようにシンプルに書けます。

list1 = [
    {'name': 'ぶどう',   'price': 5000, 'weight': 1000},
    # 中略
    {'name': 'みかん',  'price': 1500, 'weight': 5000},
]

sorted(list1, key=itemgetter('price', 'weight'))

JavaScriptでも似たように書けないか試行錯誤したところ、上記のコードにたどりつきました。

参考リンク

補足

compareArr関数内で、differenceの片方がundefinedのときの処理が必要かも知れないです。わかり次第追記します。

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

javasqriptの便利メソッド等について

初めに

現在、ES6のJavaScriptを学習しており、個人的なアウトプットとしたいと思います。

for分では考えることが多い?

まずはこちらを見てください。

const images = [
  { height: 10, width: 30 },
  { height: 20, width: 90 },
  { height: 54, width: 32 }
];
const areas = [];
for (i = 0; i < images.length; i++){
  areas.push(images[i].height * images[i].width)
}
console.log(areas) /* [300, 1800, 1728] */

こちらのfor文でやっていることはオブジェクトが入っているimagesという配列の数だけfor文内で繰り返しをしてareaという面積を求める処理です。

僕はfor文の中で考えることが多いなという印象があります。
・ imageがトータル数を数える。
・ i を ループするたびに足していく。
・ images[i].heightのところが長い。

上記が僕がこの処理の時に考えたり、感じていることです。。

map()を使ってみる

const images = [
  { height: 10, width: 30 },
  { height: 20, width: 90 },
  { height: 54, width: 32 }
];
const areas = images.map(function(image){
  return image.height * image.width /* [300, 1800, 1728] */
});

こちらはmap()を使っています。map()というのはコールバック関数を実行して配列の中の要素に対して好きな処理をして、結果を返り値として新しい配列を作成するといったものです。
for文お決まりの
for (i = 0; i < images.length; i++)
といったことがなくなってスッキリした印象があるのと、要素へアクセスするために添字を使っていないところもいいところなのかなと思っています。
ですがもう少しスッキリしたいですね。

アロー関数を使ってみる

const images = [
  { height: 10, width: 30 },
  { height: 20, width: 90 },
  { height: 54, width: 32 }
];
const areas = images.map(image =>image.height * image.width);
console.log(areas) /* [300, 1800, 1728] */

さらにスッキリして処理の内容が直感的に分かりやすくなった気がします。
ではもう少しいじってみます。

分割代入を使ってみる

const images = [
  { height: 10, width: 30 },
  { height: 20, width: 90 },
  { height: 54, width: 32 }
];
const areas = images.map(({height, width}) => height * width);
/* [300, 1800, 1728] */

これは分割代入というES6から登場した機能を使っているのですが少し難しいかもです。。
コールバック関数の引数となる部分に{height, width}というimagesのオブジェクト内の値を入れることができます。

最後に

最初のfor文に比べたら少しスッキリしましたね。今回は単純なプログラムなのですが、もっと複雑なプログラムを作る際には便利メソッド(forEach、some, map, every, find, some, reduce)などなどを使いこなせるかが鍵になっているのかなと思った今日この頃でございます。
何かもっとこうしたらとかアドバイスや誤った考え方があればご指摘ください。
よろしくお願いいたします。

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

Promiseの処理をキャンセルする

Promise で Web Speech API をラップして使っていましたが、キャンセルできるように実装するのに試行錯誤しました。想定していたような動きが実現できたので、メモを残しておきます。

先に成果品を貼っておきます。

See the Pen Web Speech API with Promise by 七誌 (@7shi) on CodePen.

概要

前回の記事では正常終了 onendresolve、異常終了 onerrorreject として扱いました。

function speak(lang, text) {
    return new Promise((resolve, reject) => {
        let u = new SpeechSynthesisUtterance(text);
        u.lang = lang;
        u.onend = resolve;
        u.onerror = reject;
        speechSynthesis.speak(u);
    });
}

読み上げ中に speechSynthesis.cancel() を呼ぶことでキャンセルできます。通常終了と同じ onend イベントが発生するため、イベントではキャンセルされたことが検知できません。

何らかの手段でキャンセルされたことを通知する必要があります。

戻り値

resolve への引数は await を通して戻り値になります。

ブラウザのコンソール
> p = new Promise((resolve, reject) => resolve(123))
> await p
123

これを利用して正常終了かキャンセルかを戻り値で区別するように speak を実装します。例外を無視するため終了と同じ扱いとします。

let stop = () => false;
function speak(lang, text) {
  return new Promise((resolve, reject) => {
    let speakend = cancel => {
      speakend = () => false;
      if (cancel) speechSynthesis.cancel();
      resolve(cancel);
      return cancel;
    };
    stop = () => speakend(true);
    let u = new SpeechSynthesisUtterance(text);
    u.lang = lang;
    u.onend = u.onerror = () => speakend(false);
    speechSynthesis.speak(u);
  });
}

キャンセルするには外部から stop() を呼びます。Promise のコンストラクタで stop を書き換えて speakend 経由で resolve(true) を呼べるようにしておくことで、終了イベント onend よりも先に終了させます。

利用方法

複雑さは Promise の中に閉じ込めたため、利用側のコードは簡単になります。

button.onclick = async function() {
  if (stop()) return;
  button.textContent = "Stop";
  for (let [element, lang, text] of texts) {
    element.classList.add("speaking");
    let cancel = await speak(lang, text);
    element.classList.remove("speaking");
    if (cancel) break;
  }
  button.textContent = "Start";
};

ループによっていくつかのテキストを読み上げます。speaking をマークすることで読み上げ個所を示します。await speak() から戻ってマークを解除して、キャンセルされていればループから抜けます。

reject で例外によってキャンセルを通知することも可能ですが、今回は resolve によって戻り値で通知した方が利用側のコードが簡単になると判断しました。

戻り値は正常終了のときに true にした方が自然かもしれませんが、今回はキャンセルに注目して値を設定しました。

参考

Web Speech API の使い方は以下の記事を参照してください。

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

2016年から2019年までのJavaScriptの全て

以下はAlbertoM( Webサイト / Twitter / GitHub / dev.to )による記事、Everything you need to know from ES2016 to ES2019の日本語訳です。

Everything you need to know from ES2016 to ES2019

JavaScriptは絶え間なく進化し続けている言語であり、この数年で多くの新機能がECMAScriptの仕様に追加されました。

この記事は私の著書Complete Guide to Modern JavaScriptからの抜粋であり、そしてこの本はES2016・ES2017・ES2018・ES2019の新しい機能について解説しています。

記事の最後に、全てを要約したチートシートを用意しています。

Everything new in ES2016

ES2016で追加された機能はわずか二つです。
・Array.prototype.includes()
・指数演算子

Array.prototype.includes()

includes()メソッドは、配列に特定の値が含まれる場合はtrueを返し、含まれていなければfalseを返します。

let array = [1,2,4,5];

array.includes(2); // true
array.includes(3); // false

Combine includes() with fromIndex

検索を開始するインデックスをincludes()に渡すことができます。
デフォルトは0で、負の値を渡すこともできます。
最初の値は検索する要素で、2番目の値は開始インデックスです。

let array = [1,3,5,7,9,11];

array.includes(3,1); // true 1番目つまり最初から検索開始する
array.includes(5,4); // false

array.includes(1,-1); // false 最後尾から検索開始する
array.includes(11,-3); // true

array.includes(5,4);はfalseを返します。
4番目の値から検索を開始するため、2番目にある値5は検索範囲外だからです。

array.includes(1,-1);はfalseを返します。
インデックス-1は配列の最後の要素という意味で、そこから検索を開始するためです。

array.includes(11,-3);はtrueを返します。
インデックス-3は配列の最後から3番目の要素という意味で、そこから検索を始めたため範囲内に11を発見しました。

The exponential operator

ES2016より前は、以下のように書いていました。

Math.pow(2,2); // 4
Math.pow(2,3); // 8

指数演算子の導入により、以下のように書けるようになりました。

2**2; // 4
2**3; // 8

複数の演算を組み合わせる際に特に役立ちます。

2**2**2; // 16
Math.pow(Math.pow(2,2),2); // 16

Math.pow()を使うと長く面倒な書式になります。
指数演算子を使うと、同じことをより早くよりクリーンに記述することができます。

ES2017 string padding, Object.entries(), Object.values() and more

ES2017では、多くのクールな新機能が追加されました。
それらを以下に解説していきます。

String padding (.padStart() and .padEnd())

文字列の先頭もしくは末尾にパディングを付けられます。

"hello".padStart(6); // " hello"
"hello".padEnd(6); // "hello "

パディング値に6を指定したのに、スペースが1しか入らなかったのはなぜでしょう。
padStartとpadEndは規定文字数になるように穴埋めするからです。
上記例の場合、"hello"は5文字あるため、不足した1文字だけがパディングされました。

"hi".padStart(10); // "        hi" 8文字埋められた
"welcome".padStart(10); // "   welcome" 4文字埋められた

Right align with padStart

値を右揃えしたいときにもpadStartが使えます。

const strings = ["short", "medium length", "very long string"];
const longestString = strings.sort(str => str.length).map(str => str.length)[0];
strings.forEach(str => console.log(str.padStart(longestString)));

// very long string
//    medium length
//            short

まず、最も長い文字列の長さを取得します。
次いですべての文字列にpadStartを適用し、文字列長を最も長い文字列に合わせました。

Add a custom value to the padding

スペースだけではなく、任意の文字でパディングすることができます。

"hello".padEnd(13," Alberto"); // "hello Alberto"
"1".padStart(3,0); // "001"
"99".padStart(3,0); // "099"

Object.entries() and Object.values()

とりあえずObjectを作成します。

const family = {
  father: "Jonathan Kent",
  mother: "Martha Kent",
  son: "Clark Kent",
}

かつてのJavaScriptは、プロパティに以下のようにアクセスしていました。

Object.keys(family); // ["father", "mother", "son"]
family.father; // "Jonathan Kent"

Object.keys()は、プロパティのキーのみを返します。

ES2017では、プロパティにアクセスする方法が2種類増えました。

Object.values(family); // ["Jonathan Kent", "Martha Kent", "Clark Kent"]

Object.entries(family);
// ["father", "Jonathan Kent"]
// ["mother", "Martha Kent"]
// ["son", "Clark Kent"]

Object.values()はプロパティ値のみの配列を返し、Object.entries()はキーと値の両方を含む配列の配列を返します。

Object.getOwnPropertyDescriptors()

このメソッドは、オブジェクトの持つ全てのプロパティディスクリプタを返します。
ディスクリプタの属性はvalue・writable・get・set・configurable・enumerableです。

const myObj = {
  name: "Alberto",
  age: 25,
  greet() {
    console.log("hello");
  },
}

Object.getOwnPropertyDescriptors(myObj);
// age:{value: 25, writable: true, enumerable: true, configurable: true}

// greet:{value: ƒ, writable: true, enumerable: true, configurable: true}

// name:{value: "Alberto", writable: true, enumerable: true, configurable: true}

Trailing commas in function parameter lists and calls

これは、本当に小さな変更です。
しかしこれで、そのパラメータが最後であるかどうかをいちいち気にせずに末尾カンマを書けるようになりました。

// 昔
const object = {
  prop1: "prop",
  prop2: "propop"
}

// 現在
const object = {
  prop1: "prop",
  prop2: "propop",
}

2番目のプロパティは最後にカンマが増えていることに注目してください。
入れなくてもエラーにはなりませんが、同僚やチームメイトの生活を楽にするためにも入れておくことをお勧めします。

// カンマ入れない
const object = {
  prop1: "prop",
  prop2: "propop"
}

// 同僚が最終行をコピペしてプロパティを追加した
const object = {
  prop1: "prop",
  prop2: "propop"
  prop3: "propopop"
}

// 突然エラーが出るようになった

Shared memory and Atomics

MDNによると (日本語版)

メモリーが共有されている場合、複数のスレッドがメモリー内の同じデータを読み書きできます。アトミック演算では、予測される値の書き込みと読み込みを保証するため、次の演算が開始される前に現在の演算が完了し、その演算が割り込まれないようにします。

Atomicはコンストラクタではありません。
プロパティとメソッドは全て静的であり、newしたりinvokeしたり関数として呼び出したりすることはできません。

Atomicが持つメソッドは以下のようなものがあります。
・ add / sub
・ and / or / xor
・ load / store

Atomicは、汎用固定長バイナリデータバッファSharedArrayBufferなどで使用されます。
いくつかの例を見てみましょう。

Atomics.add(), Atomics.sub(), Atomics.load() and Atomics.store()

// SharedArrayBuffer作成
const buffer = new SharedArrayBuffer(16);
const uint8 = new Uint8Array(buffer);

// これに各計算する
uint8[0] = 10;

console.log(Atomics.add(uint8, 0, 5)); // 10

console.log(uint8[0]) // 15

console.log(Atomics.load(uint8,0)); // 15

最初のAtomics.add()は加算を行いますが、返り値は計算する前の値です。
その後uint8[0]を参照すると、addが実行されたあとなので値が15になっていることが確認できます。

配列値をAtomicに取得するにはAtomics.load()を使い、第一引数が対象の配列、第二引数がインデックスです。

Atomics.sub()Atomics.add()と同じ挙動で、減算を行います。

// SharedArrayBuffer作成
const buffer = new SharedArrayBuffer(16);
const uint8 = new Uint8Array(buffer);

// これに各計算する
uint8[0] = 10;

console.log(Atomics.sub(uint8, 0, 5)); // 10

console.log(uint8[0]) // 5

console.log(Atomics.store(uint8,0,3)); // 3

console.log(Atomics.load(uint8,0)); // 3

Atomics.sub()を使って10から5を引きました。
計算自体の返り値は、Atomics.add()と同じく計算する前の値であり、すなわち10です。

次にAtomics.store()を使い特定の値、今回は配列の0番目のインデックスに3を登録しました。

Atomics.store()は渡した値をそのまま返します。
Atomics.load()すると、値は書き替えられた後なので5ではなく3になります。

Atomics.and(), Atomics.or() and Atomics.xor()

これらはそれぞれAND、OR、XORのビット演算を行います。
ビット演算の詳細はWikipediaなどで読むことができます。

ES2017 Async and Await

ES2017では、async/awaitと呼ばれる新たなPromiseが導入されました。

Promise review

その前に、まず普通のPromise構文を簡単に復習しましょう。

// GitHubからユーザを取得
fetch('api.github.com/user/AlbertoMontalesi').then( res => {
  // 値をJSONで返す
  return res.json();
}).then(res => {
  // 全てが正常に動作したらここに来る
  console.log(res);
}).catch( err => {
  // エラーがあったらここに来る
  console.log(err);
})

GitHubからユーザを取得してコンソールに出力するだけの簡単な例です。

また別の例を見てみましょう。

function walk(amount) {
  return new Promise((resolve,reject) => {
    if (amount < 500) {
      reject ("the value is too small");
    }
    setTimeout(() => resolve(`you walked for ${amount}ms`),amount);
  });
}

walk(1000).then(res => {
  console.log(res);
  return walk(500);
}).then(res => {
  console.log(res);
  return walk(700);
}).then(res => {
  console.log(res);
  return walk(800);
}).then(res => {
  console.log(res);
  return walk(100);
}).then(res => {
  console.log(res);
  return walk(400);
}).then(res => {
  console.log(res);
  return walk(600);
});

// you walked for 1000ms
// you walked for 500ms
// you walked for 700ms
// you walked for 800ms
// uncaught exception: the value is too small

Async and Await

これをasync/awaitで書き換えるとこうなります。

function walk(amount) {
  return new Promise((resolve,reject) => {
    if (amount < 500) {
      reject ("the value is too small");
    }
    setTimeout(() => resolve(`you walked for ${amount}ms`),amount);
  });
}

// asyncなfunctionを作成
async function go() {
  // awaitがあれば終わるまで待つ
  const res = await walk(500);
  console.log(res);
  const res2 = await walk(900);
  console.log(res2);
  const res3 = await walk(600);
  console.log(res3);
  const res4 = await walk(700);
  console.log(res4);
  const res5 = await walk(400);
  console.log(res5);
  console.log("finished");
}

go();

// you walked for 500ms 
// you walked for 900ms 
// you walked for 600ms 
// you walked for 700ms 
// uncaught exception: the value is too small

非同期関数を作成するには、まずasyncキーワードを記載します。
このキーワードが入った関数はPromiseを返すようになります。
Promiseでない値を返そうとした場合、自動的にPromiseでラップされて返されます。
awaitキーワードは、async関数内でのみ機能します。
awaitを書くと、プログラムはPromiseが結果を返すまでそこで停止します。

async関数の外でawaitキーワードを使うとどうなるでしょうか。

// asyncではない関数
function func() {
  let promise = Promise.resolve(1);
  let result = await promise; 
}
func();
// SyntaxError: await is only valid in async functions and async generators


// トップレベル
let response = Promise.resolve("hi");
let result = await response;
// SyntaxError: await is only valid in async functions and async generators

復習:awaitは、async関数内でのみ使用可能。

Error handling

Promiseではエラーを.catch()でキャッチします。
これについては特に違いはありません。

async function asyncFunc() {

  try {
    let response = await fetch('http:your-url');
  } catch(err) {
    console.log(err);
  }
}

asyncFunc(); // TypeError: failed to fetch

関数内でエラーハンドリングしていない場合は、以下のように書くこともできます。

async function asyncFunc(){
  let response = await fetch('http:your-url');
}
asyncFunc(); // Uncaught (in promise) TypeError: Failed to fetch

asyncFunc().catch(console.log); // TypeError: Failed to fetch

ES2018 Async Iteration and more?

ES2018で導入された機能も見ていきましょう。

Rest / Spread for Objects

ES6 (ES2015) でスプレッド構文が導入されたことを覚えていますか?

const veggie = ["tomato","cucumber","beans"];
const meat = ["pork","beef","chicken"];

const menu = [...veggie, "pasta", ...meat];
console.log(menu); // Array [ "tomato", "cucumber", "beans", "pasta", "pork", "beef", "chicken" ]

スプレッド構文にRestパラメータがオブジェクトに対しても使用可能になりました。

let myObj = {
  a:1,
  b:3,
  c:5,
  d:8,
}

// zは残り全部
let { a, b, ...z } = myObj;
console.log(a);     // 1
console.log(b);     // 3
console.log(z);     // {c: 5, d: 8}

// スプレッド構文でクローン
let clone = { ...myObj };
console.log(clone); // {a: 1, b: 3, c: 5, d: 8}

myObj.e = 15;
console.log(clone) // {a: 1, b: 3, c: 5, d: 8}
console.log(myObj) // {a: 1, b: 3, c: 5, d: 8, e: 15}

スプレッド構文を使うとオブジェクトのクローンが簡単に作成できます。
元のオブジェクトを変更しても、クローンしたオブジェクトは変更されません。

Asynchronous Iteration

非同期イテレータを用いて、データを非同期的に反復することができます。

ドキュメントによると、

非同期イテレータは、next()メソッドが{ value, done }のペアを返すこと以外、イテレータにそっくりです。

従って、for-await-ofループで反復処理してPromiseにすることができます。

const iterables = [1,2,3];

async function test() {
    for await (const value of iterables) {
        console.log(value);
    }
}
test();
// 1
// 2
// 3

ループの実行中、[Symbol.asyncIterator]()を用いてデータソースから非同期イテレータを作成します。
次のループにアクセスするたびに、返ってきたPromiseを暗黙的にawaitします。

Promise.prototype.finally()

Promiseが終了したときに呼び出されます。

const myPromise = new Promise((resolve,reject) => {
  resolve();
})
myPromise
  .then( () => {
    console.log('still working');
  })
  .catch( () => {
    console.log('there was an error');
  })
  .finally(()=> {
    console.log('Done!');
  })

.finally()もPromiseを返すので、さらにthencatchを続けることも可能ですが、そこに渡ってくるPromiseはfinallyではなく元の値です。

const myPromise = new Promise((resolve,reject) => {
  resolve();
})
myPromise
.then( () => {
    console.log('still working');
    return 'still working';
  })
  .finally(()=> {
    console.log('Done!');
    return 'Done!';
  })
  .then( res => {
    console.log(res); // still working
  })

見てのとおり、最後のthenに渡ってくるPromiseは、finallyによるものではなく最初のthenが作ったものです。

RegExp features

正規表現の機能が4種類追加されました。

s(dotAll) flag for regular expressions

sフラグが導入されました。
これにより、'.'が改行を含む任意の1文字に一致するようになります。

/foo.bar/s.test('foo\nbar'); // true

RegExp named capture groups

ドキュメントによると、

番号付きキャプチャグループを使用して、正規表現が一致した文字列の特定の個所を参照することができます。
キャプチャグループには一意の番号が割り当てられており、その番号で参照することができますが、これにより正規表現の把握とリファクタリングが難しくなります。
日付に一致する/(\d{4})-(\d{2})-(\d{2})/を例に取ると、どの番号が月に対応し、どの番号が日に対応しているかは、コードをよく調べてみないと理解できません。
また月と日の順番を入れ替えたいとなったら、参照する番号の方まで書き換えなければなりません。
ここでキャプチャグループに(?<name>...)構文を用いて、識別子nameで参照することができるようになります。
日付の正規表現を/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/uと書けます。
各識別子は一意であり、ECMAScriptの命名規則に従う必要があります。
名前付きキャプチャグループには、返り値のgroupsプロパティからアクセスすることができます。
以下の例のように、名前付きキャプチャグループと同時に、番号付きの参照も作成されます。

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result = re.exec('2015-01-02');
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';

// result[0] === '2015-01-02';
// result[1] === '2015';
// result[2] === '01';
// result[3] === '02';

let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
console.log(`one: ${one}, two: ${two}`); // one: foo, two: bar

RegExp Lookbehind Assertions

ドキュメントによると、

後読みアサーションを使用すると、手前に別のパターンが存在するパターンにマッチすることができます。
たとえば$記号を含まずに金額だけマッチするような使い方ができます。
肯定後読みアサーションは(?<=...)と記述します。
$記号を含まずに金額だけマッチさせたい場合は/(?<=$)\d+(\.\d*)?/とし、これは$10.53にマッチして10.53がキャプチャされます。
しかし€10.53にはマッチしません。
否定後読みアサーションは(?<!...)と記述し、手前に別のパターンが存在しないパターンにマッチします。
/(?<!$)\d+(?:\.\d*)/$10.53にマッチしませんが、€10.53にはマッチします。

RegExp Unicode Property Escapes

ドキュメントによると、

\p{…}および\P{…}形式のUnicodeプロパティエスケープが追加されます。
Unicodeプロパティエスケープは、uフラグの指定された正規表現で使用可能な新しいタイプのエスケープシーケンスです。
これを使うと、以下のような記述が可能になります。

const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π'); // true

Lifting template literals restriction

タグ付きテンプレートリテラルを使うと、エスケープシーケンスの制限を気にする必要がなくなります。
詳細はこちらをご覧ください。

What's new in ES2019?

ECMAScriptの最新バージョンである、ES2019で追加されたものを見ていきましょう。

Array.prototype.flat() / Array.prototype.flatMap()

Array.prototype.flat()は、指定された深さまでの配列を再帰的にフラット化します。
深さ引数のデフォルトは1です。
Infinityを指定すると、無制限にネストを解除します。

const letters = ['a', 'b', ['c', 'd', ['e', 'f']]];

// デフォルトは1
letters.flat(); // ['a', 'b', 'c', 'd', ['e', 'f']]

// 2段階
letters.flat(2); // ['a', 'b', 'c', 'd', 'e', 'f']

// 2段 = 1段 * 2
letters.flat().flat(); // ['a', 'b', 'c', 'd', 'e', 'f']

// ネストがなくなるまで再帰的にフラット化
letters.flat(Infinity) // ['a', 'b', 'c', 'd', 'e', 'f']

Array.prototype.flatMap()は、引数の取り扱いはflat()と同じです。
配列を単純にフラット化するのではなく、関数を渡して任意の処理を行うことができます。

let greeting = ["Greetings from", " ", "Vietnam"];

// 普通のmap
greeting.map(x => x.split(" ")); 
// ["Greetings", "from"]
// ["", ""]
// ["Vietnam"]

// mapしてflat
greeting.flatMap(x => x.split(" ")) // ["Greetings", "from", "", "", "Vietnam"]

普通にmap()を使うと、ネストした配列になります。
flatMap()を使うことでフラットな配列にすることができます。

Object.fromEntries()

Key-valueペアからオブジェクトに変換します。

const keyValueArray = [
  ['key1', 'value1'],
  ['key2', 'value2']
]

const obj = Object.fromEntries(keyValueArray)
// {key1: "value1", key2: "value2"}

Object.fromEntries()は引数として配列、Map、その他の反復可能プロトコルを受け取ります。
反復可能プロトコルの詳細についてはこちらをご覧ください。

String.prototype.trimStart() / .trimEnd()

String.prototype.trimStart()は文字列の先頭にある空白を削除し、String.prototype.trimEnd()は文字列の末尾にある空白を削除します。

let str = "    this string has a lot of whitespace   ";

str.length; // 42

str = str.trimStart(); // "this string has a lot of whitespace   "
str.length; // 38

str = str.trimEnd(); // "this string has a lot of whitespace"
str.length; // 35

trimStart()のエイリアスとしてtrimLeft()が、trimEnd()のエイリアスとしてtrimRight()が存在します。

Optional Catch Binding

ES2019より前は、catch句に必ず例外変数を取る必要がありました。
ES2019では省略することができます。

// Before
try {
   ...
} catch(error) {
   ...
}

// ES2019
try {
   ...
} catch {
   ...
}

エラーを無視したいときに便利です。
この機能のユースケースについてはこの記事を強くお勧めします。

Function​.prototype​.toString()

関数の.toString()は、ソースコードを文字列として返します。

function sum(a, b) {
  return a + b;
}

console.log(sum.toString());
// function sum(a, b) {
//    return a + b;
//  }

コメントも含みます。

function sum(a, b) {
  // perform a sum
  return a + b;
}

console.log(sum.toString());
// function sum(a, b) {
//   // perform a sum
//   return a + b;
// }

Symbol.prototype.description

.descriptionSymbolの値を文字列で返します。

const me = Symbol("Alberto");

me.description; // "Alberto"

me.toString() //  "Symbol(Alberto)"

Download the cheatsheet

このリンクから、これらのチートシートをダウンロードすることができます。

よかったらAmazonLeanpubで私の本を買ったり、Educativeのコースを受けてみてください。

感想

全てと言いつつ全てではありませんが、これは元々著者がThe Complete Guide to Modern JavaScriptという書籍を出していて、この記事はその抜粋だからです。

抜粋といってもわりとけっこうな分量でしたが、元の本は全300ページという更に相当な力作となっています。
最初は変数や関数といった基礎部分から順にステップアップしていく内容で、この記事で紹介されている新機能は200ページ以降に出てくる、いわばおまけ部分です。
全編英語なのでなかなかたいへんですが、読み通せば現代のJavaScript事情に詳しくなれることは間違いありません。
しかもKindleなら2000円弱と大変お買い得、これは持ってて正解ですね。

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