20200727のJavaScriptに関する記事は30件です。

宇宙船のコンピュータの整数演算ユニット作成し、船員を助けよう!

宇宙船のコンピュータの整数演算ユニット作成し、船員を助けよう!!

illustrain09-utyuu5.png

問題

以下の入力の場合、実装して、出力の配列[0]の値を示せ。

入力

[1,1,28,3,1,1,2,3,1,3,4,3,1,5,0,3,2,9,1,19,1,9,19,23,1,23,5,27,2,27,10,31,1,6,31,35,1,6,35,39,2,9,39,43,1,6,43,47,1,47,5,51,1,51,13,55,1,55,13,59,1,59,5,63,2,63,6,67,1,5,67,71,1,71,13,75,1,10,75,79,2,79,6,83,2,9,83,87,1,5,87,91,1,91,5,95,2,9,95,99,1,6,99,103,1,9,103,107,2,9,107,111,1,111,6,115,2,9,115,119,1,119,6,123,1,123,9,127,2,127,13,131,1,131,9,135,1,10,135,139,2,139,10,143,1,143,5,147,2,147,6,151,1,151,5,155,1,2,155,159,1,6,159,0,99,2,0,14,0];

出力

入力配列が同じサイズの配列

入力:[1,0,0,0,99] 。出力: [2,0,0,0,99]    原因 (1 + 1 = 2).
入力:[2,3,0,3,99]。出力:[2,3,0,6,99]       原因(3 * 2 = 6).
入力:[2,4,4,5,99,0]。出力:[2,4,4,5,99,9801]  原因(99 * 99 = 9801).
入力:[1,1,1,4,99,5,6,0,99] 。出力:[30,1,1,4,2,5,6,0,99]
制約

なし。言語は限らない。

ボーナス

出力配列[0]が19690720となる場合の入力配列[1]と[2]の値は何番でしょう。
取りうる範囲は0~99とする。

回答

回答例1(JavaScript)
const intCodeComputer = (intCode=[99]) => {
   for(let i=0; i<intCode.length; i+=4){
       if(intCode[i] === 99) return intCode;
       switch(intCode[i]) {
           case 1:
               intCode[intCode[i+3]] = intCode[intCode[i+1]] + intCode[intCode[i+2]];
               break;
           case 2:
               intCode[intCode[i+3]] = intCode[intCode[i+1]] * intCode[intCode[i+2]];
               break;
           default: break;
       }
   }
};

const answer = intCodeComputer([...PUZZLE_INPUT]);
console.log(answer[0]);
回答例2(C#)
static void Main(string[] args)
{
   Console.WriteLine(string.Join(',', OperationRecursive(args, 0)));
}

private static int[] OperationRecursive(int[] sources, int step)
{
   int calc(Func<int, int, int> f, int x, int y) => f(x, y);

   var targets = sources.Skip(step * 4).Take(4).ToArray();

   if (targets[0] == 99)
       return sources;
   if (targets[0] == 1)
       sources.SetValue(calc((x, y) => x + y, sources[targets[1]], sources[targets[2]]), targets[3]);
   if (targets[0] == 2)
       sources.SetValue(calc((x, y) => x * y, sources[targets[1]], sources[targets[2]]), targets[3]);

   return OperationRecursive(sources, step + 1);
}
回答例3(GO)
func main() {
 numlist := []int{}
 for i := 0; i < len(numlist); i += 4 {
   var a = numlist[i+1]
   var b = numlist[i+2]
   var c = numlist[i+3]

   if numlist[i] == 1 {
     var d = numlist[a] + numlist[b]
     numlist[c] = d
   }

   if numlist[i] == 2 {
     var d = numlist[a] * numlist[b]
     numlist[c] = d
   }

   if numlist[i] == 99 {
     break
   }
 }
 fmt.Println(numlist[0])
}
回答例4(Java)
public class Main {
   public static void main(String[] args) {
       int [] arrayCode = {};
       for(int i=0;i<arrayCode.length; i+=4)
       {
           if(arrayCode[i]==99){
               break;
           }

           if(arrayCode[i]==1)
           {
               arrayCode[arrayCode[i+3]] = arrayCode[arrayCode[i+1]] + arrayCode[arrayCode[i+2]];
           }
           else if(arrayCode[i]==2)
           {
             arrayCode[arrayCode[i+3]] = arrayCode[arrayCode[i+1]] * arrayCode[arrayCode[i+2]];
           }
       }
       System.out.print(arrayCode[0]);
   }
}
回答例5(Perl)
my @array = (1,0,0,0,99);

sub calc {
   my @array = @_;
   my $length = @array;

   for (my $i = 0; $i < $length; $i+=4) {
       ($array[$i] == 99) && (last);

        if ($array[$i] == 1) {
            $array[$array[$i+3]] = $array[$array[$i+1]] + $array[$array[$i+2]]
        } elsif ($array[$i] == 2) {
           $array[$array[$i+3]] = $array[$array[$i+1]] * $array[$array[$i+2]]
        }
   }
   return $array[0];
}

$result = &calc(@array);

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

今までJW Playerを使っていたけどplyr.jsをカスタマイズしてみた(2)

前回まで

前回の動画再生まででplyr.jsで動画を再生するところまではできました。
ここからはプレイヤーの肝であるコントロールバーのカスタマイズをしていきます。
前回の導入部分まではいくらでも記事があるのですが、ここからが本当に情報が少なかったです...
とにかく本家のIssuesに投稿したいところを「まず検索しろ!!同じこと何度も聞くんじゃねぇ!」という作者の言葉を胸に、一心不乱に検索して試行錯誤を繰り返します。

とりあえず色を変える

CSSを読み込ませるだけで全体的な色は変えられます。

フォルダ構成
html/
 ├ css/
 │ ├ myplayer.css
 │ └ plyr.css
 ├ js/
 │ ├ jquery.min.js
 │ ├ myplayer.js
 │ └ plyr.min.js
 ├ index.html
 └ test.mp4
index.html
<html>
  <head>
    <link rel="stylesheet" media="all" href="css/plyr.css">
    <link rel="stylesheet" media="all" href="css/myplayer.css">
    <script type="text/javascript" charset="UTF-8" src="js/jquery.min.js"></script>
    <script type="text/javascript" charset="UTF-8" src="js/plyr.min.js"></script>
    <script type="text/javascript" charset="UTF-8" src="js/myplayer.js"></script>
  </head>
  <body>
    <video id="video_player" controls>
      <source src="test.mp4" type="video/mp4" />
    </video>
  </body>
</html>
css/myplayer.css
.plyr--full-ui input[type=range] {
  color: red;  /* とりあえず赤くしてみる */
}

.plyr__control--overlaid {
  background: rgba(255,0,0, .8);  /* とりあえず赤くしてみる */
}

.plyr--video .plyr__control.plyr__tab-focus,
.plyr--video .plyr__control:hover,
.plyr--video .plyr__control[aria-expanded=true] {
  background: red;  /* とりあえず赤くしてみる */
}

.plyr__control.plyr__tab-focus {
  box-shadow: 0 0 0 5px rgba(255,0,0, .5);  /* とりあえず赤くしてみる */
}

.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]::before {
  background: red;  /* とりあえず赤くしてみる */
}

.plyr--audio .plyr__control.plyr__tab-focus,
.plyr--audio .plyr__control:hover,
.plyr--audio .plyr__control[aria-expanded=true] {
  background: red;  /* とりあえず赤くしてみる */
}

cssの「red」と「255,0,0」を書き換えるだけで

スクリーンショット 2020-07-27 23.02.56.png
ボタンなどが赤くなります。

さらに、アイコンの色を変えたい場合は

css/myplayer.css
.plyr--full-ui input[type=range] {
  color: red;  /* とりあえず赤くしてみる */
}

.plyr__control--overlaid {
  background: rgba(255,0,0, .8);  /* とりあえず赤くしてみる */
}

.plyr--video .plyr__control.plyr__tab-focus,
.plyr--video .plyr__control:hover,
.plyr--video .plyr__control[aria-expanded=true] {
  background: red;  /* とりあえず赤くしてみる */
}

.plyr__control.plyr__tab-focus {
  box-shadow: 0 0 0 5px rgba(255,0,0, .5);  /* とりあえず赤くしてみる */
}

.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]::before {
  background: red;  /* とりあえず赤くしてみる */
}

.plyr--audio .plyr__control.plyr__tab-focus,
.plyr--audio .plyr__control:hover,
.plyr--audio .plyr__control[aria-expanded=true] {
  background: red;  /* とりあえず赤くしてみる */
}

.plyr__control {
  color: yellow;  /* アイコンを黄色くしてみる */
}

アイコンが黄色くなりました。
スクリーンショット 2020-07-27 23.07.25.png

好みのボタンを表示する

plyr.jsではデフォルトのボタン以外に「10秒戻る」、「10秒進む」などのボタンを追加したり、「ピクチャーインピクチャーを隠す」などの細かい設定も可能となっています。

全てのボタン
controls: [
    'play-large', // 再生前に表示される再生ボタン
    'restart', // もう一度最初からのボタン
    'rewind', // seektimeで指定した秒数戻るボタン
    'play', // 再生、一時停止ボタン
    'fast-forward', // seektimeで指定した秒数進むボタン
    'progress', // プログレスバーの表示
    'current-time', // 現在時間、残り時間の表示
    'duration', // 動画全体の再生時間
    'mute', // ミュート、ミュート解除ボタン
    'volume', // 音量コントロール
    'captions', // 字幕表示ボタン
    'settings', // 設定メニュー(再生速度、画質選択)
    'pip', // ピクチャーインピクチャー
    'airplay', // AirPlay(MacのSafariのみ)
    'download', // 指定したソースのダウンロードボタン
    'fullscreen' // フルスクリーンボタン
];

controlは下記のようにセットアップに与えることで設定ができます。

js/myplayer.js
$(function() {
  const control = [ 
    'play-large', // 再生前に表示される再生ボタン
    'restart', // もう一度最初からのボタン
    'rewind', // seektimeで指定した秒数戻るボタン
    'play', // 再生、一時停止ボタン
    'fast-forward', // seektimeで指定した秒数進むボタン
    'progress', // プログレスバーの表示
    'current-time', // 現在時間、残り時間の表示
    'duration', // 動画全体の再生時間
    'mute', // ミュート、ミュート解除ボタン
    'volume', // 音量コントロール
    'captions', // 字幕表示ボタン
    'settings', // 設定メニュー(再生速度、画質選択)
    'pip', // ピクチャーインピクチャー
    'airplay', // AirPlay(MacのSafariのみ)
    'download', // 指定したソースのダウンロードボタン
    'fullscreen' // フルスクリーンボタン
  ];
  player = new Plyr('#video_player', { controls: control });
});

スクリーンショット 2020-07-27 23.30.36.png

controlの中身をいじることでボタンの順番や表示するボタンを選択することもできます。

js/myplayer.js
$(function() {
  const control = [ 
    'play', // 再生、一時停止ボタン
    'rewind', // seektimeで指定した秒数戻るボタン
    'fast-forward', // seektimeで指定した秒数進むボタン
    'progress', // プログレスバーの表示
    'current-time', // 現在時間、残り時間の表示
    'mute', // ミュート、ミュート解除ボタン
    'volume', // 音量コントロール
    'settings', // 設定メニュー(再生速度、画質選択)
    'fullscreen' // フルスクリーンボタン
    ];
  player = new Plyr('#video_player', {controls: control});
});

スクリーンショット 2020-07-27 23.32.41.png
デフォルトの見た目を変えるだけなら、位置を変えたり、一つ一つの要素のCSSを指定することでなんとかなりますが、私の場合、このかっこいいプレイヤーを「オリジナルのボタンを追加したい」というオーダーによりカスタマイズするというさらに過酷な道を進みます。

参考リンク:
cssに関しては下記の回答が本当に役に立ちました。
https://github.com/sampotts/plyr/issues/662#issuecomment-498825513

コントローラーの表示は下記の公式文書がわかりやすいが、これにたどり着くのが大変だった。
https://github.com/sampotts/plyr/blob/master/CONTROLS.md

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

今までJW Playerを使っていたけどplyr.jsをカスタマイズしてみた(1)

プロローグ

今までは有料のJW Playerを使っていました。
有料なだけあり、ドキュメントなどもしっかりしていたが、最近無料の動画プレイヤーライブラリも増えてきたことだし、そろそろ乗り換え時かと思い、plyr.jsを使ってみました。

だが!!

あまりに日本語のドキュメントが少ないので、備忘録的に残すことにしました。

動画再生まで

公式サイト( https://plyr.io/ )からZIPで落とすなりする。
落としてきたら、distフォルダにある「plyr.min.js」と「plyr.css」を読み込める場所におく。

htmlフォルダをトップとするなら、下記のような感じで読み込める。

フォルダ構成
html/
 ├ css/
 │ └ plyr.css
 ├ js/
 │ ├ jquery.min.js
 │ ├ myplayer.js
 │ └ plyr.min.js
 ├ index.html
 └ test.mp4
index.html
<html>
  <head>
    <link rel="stylesheet" media="all" href="css/plyr.css">
    <script type="text/javascript" charset="UTF-8" src="js/jquery.min.js"></script>
    <script type="text/javascript" charset="UTF-8" src="js/plyr.min.js"></script>
    <script type="text/javascript" charset="UTF-8" src="js/myplayer.js"></script>
  </head>
  <body>
    <video id="video_player" controls>
      <source src="test.mp4" type="video/mp4" />
    </video>
  </body>
</html>
js/myplayer.js
$(function() {
  player = new Plyr('#video_player');
});

スクリーンショット 2020-07-27 22.48.14.png
とりあえず再生ができるようになりました。

これだけでも十分にかっこいいプレイヤーなのですが、サイト全体の色味と合わせたいなどあると思います。

次回から本格的なカスタマイズに入ります。

参考URL:
https://www.datastadium.co.jp/engineer/e-reports/5627

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

HTMLとJavaScriptの連携

HTMLとJavaScriptの連携ができるとどうなる?

ボタンを押したときにダイアログを出す。
通信して取得したデータをWebページに表示する。
など動きのあるページが作れるようになる!

htmlだけではできない動きがJavaScriptによってできるようになるのでできることが広がる。

前提

JavaScriptの基礎を理解していること。
htmlからJavaScriptの読み込みができる。 (以下)

index.html
<!DOCTYPE html>
<html lang='en'>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <script type="text/javascript" src='./main.js'></script>
    <title>Document</title>
</head>
<body>
    <p>Hello World</p>
</body>
</html>

本記事ではこれをベースに進めていく。

ボタンを押したらダイアログを表示する

このボタンを押したら
スクリーンショット 2020-07-26 19.21.36.png

こんな感じのダイアログを出す。
スクリーンショット 2020-07-26 19.29.43.png

全体像は以下
スクリーンショット 2020-07-26 19.36.36.jpg

3ステップで作成していく。

htmlでボタンを表示する

まずはボタンを作る

index.html
<body>
    <button>ここを押してね!</button>
</body>

buttonタグで囲むだけ。

これを書いたらブラウザでボタンが表示されていることを確認。
押してもなにもおきない。

JavaScript側に関数を用意

main.js
const alertHelloWorld = () => {
    alert("HelloWorld");
}

alertは文字列をダイアログで表示するための関数。

ボタンを押したときに関数を呼び出す。

ボタンタグにonclickを追加する。

index.html
<body>
    <button onclick="alertHelloWorld()">ここを押してね!</button>
</body>

これでブラウザで確認できる。
関数名だけではなく()をつけることに注意。

JavaScriptから文字をWebページに表示させる

ボタン押す前 ボタン押した後
スクリーンショット 2020-07-26 22.49.29.png スクリーンショット 2020-07-26 22.49.36.png

文字が変わる

html側の用意

最初に表示する文字をhtmlに仕込んでおく。

index.html
<body>
    <div id="canvas">ここに文字をいれる</div>
    <button onclick="drawCanvas()">ここを押してね!</button>
</body>

ポイントは<div id="canvas">
div要素にidをつける。
idをつけることでJavaScript側から要素を取得することが可能になる。
id名はとくに決まりはないが、ここでは何かしらを描くところって意味でcanvasという名前にした。

文字を変更する

JavaScript側でやることは
1. idが"canvas"の要素を取得し、
2. そこに新しい文字列をつっこむ
の2つ。

それは以下のように書ける

main.js
const drawCanvas = () => {
    const canvas = document.getElementById("canvas");
    canvas.innerHTML = "Hello JavaScript!";
}

スクリーンショット 2020-07-26 23.15.11.png
この図の黄色い部分がinnerHTML

ポイント

IDを用いた要素の検索。

const 要素 = document.getElementById("ID名");

要素の中身の変更

要素.innerHTML = "新しい中身";

これでブラウザでボタンを押したときに表示が変わることが確認できる。

最初に表示する文字もJavaScriptから設定する。

なるべくJavaScript側に書きたいので最初に表示する文字もJavaScript側で書きたい。

読み込み時に設定するためにmain.jsをで以下のように書く。

main.js
// 初期化用の関数を用意
const initialize = () => {
    const canvas = document.getElementById("canvas");
    canvas.innerHTML = "ここに文字をいれる";
}

// 読み込み時に呼び出す。
initialize();

html側には文字を入れないでおく

index.html
<body>
    <div id="canvas"></div>
    <button onclick="drawCanvas()">ここを押してね!</button>
</body>

で、これでブラウザで確認すると、表示されない。
スクリーンショット 2020-07-27 22.44.25.png

そしてコンソールには以下のようなエラーが表示される。

uncaught TypeError: Cannot set property 'innerHTML' of null at initialize

initializeの該当部分は以下

main.js
    const canvas = document.getElementById("canvas");
    canvas.innerHTML = "ここに文字をいれる";

「nullにはinnerHTMLっていうプロパティーないよ」
=> canvasはnullである (何にも入ってないって意味)
=> document.getElementById("canvas")が失敗している。

と推測できる。

で、じゃあなぜ失敗するかというと、jsの読み込みのタイミングの問題。
現状以下のような流れになっている。

スクリーンショット 2020-07-26 21.53.39.png

bodyの中身を描画する前にjsの読み込みが走っているのでまだ「canvas」がないってこと。
そこでjsの読み込みタイミングを遅くする。
そのやり方の一つが以下。

index.html
// before
<script type="text/javascript" src='./main.js'></script>
// after
<script type="text/javascript" src='./main.js' defer></script>

main.jsを読み込むときにdeferをつける。
するとちゃんと初期化されるようになる。

JavaScriptから色を変える

idから取得した要素は文字の内容を変えるだけでなく他にもいろいろ変えられる。
そこでまずは色を変えてみる。

スクリーンショット 2020-07-27 23.00.43.png

main.js
const drawCanvas = () => {
    const canvas = document.getElementById("canvas");
    canvas.innerHTML = "Hello JavaScript!";
    canvas.style.color = 'red'; // <- ここ
}

色の設定は決められた文字ではなくカラーコードでもOK

スクリーンショット 2020-07-27 23.02.20.png

main.js
const drawCanvas = () => {
    const canvas = document.getElementById("canvas");
    canvas.innerHTML = "Hello JavaScript!";
    canvas.style.color = '#00FF00';
}

他にもいろいろ変えられるのでcssで変えられるものを試してみるとよい。

スクリーンショット 2020-07-27 23.08.35.png

main.js
const drawCanvas = () => {
    const canvas = document.getElementById("canvas");
    canvas.innerHTML = "Hello JavaScript!";
    canvas.style.color = '#00FF00';
    canvas.style.backgroundColor = 'red'; // 背景色変更
    canvas.style.padding = '10px'; // 領域を広くする
}

応用するとこんなこともできる

ここはやらなくて大丈夫。
非同期とかできるようになるとこんな感じのアニメーションとかも作れるようになる。

animation_js.gif

const sleep = (msec) => new Promise(resolve => setTimeout(resolve, msec));

const drawTextAsync = async () => {
    const canvas = document.getElementById("canvas");
    canvas.style.backgroundColor = 'white'; 
    canvas.style.color = 'black'
    canvas.innerHTML = " ";
    await sleep(100);
    const word = "Hello JavaScript!";
    for (var i = 0; i < word.length; i++) {
        const content = word.substr(0, i+1);
        canvas.innerHTML = content;
        await sleep(100);
    }
    canvas.style.backgroundColor = 'red'; 
    canvas.style.color = 'white'
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JS初心者がKintoneカスタマイズ行うために プラグイン編

こんばんみ。「りょうちん」です。今日初めて在宅ワークを経験しました。家の隣工事でめっちゃうるさかったけど。笑

今日からKintoneのプラグイン作成の課題に入りました!むずい!!

というのも、初めは何が出来るのか分からずJavaScriptカスタマイズと比べて何のメリットがあるの?状態でした。
今日調べてて分かったことは、

プラグインは汎用的な機能を追加したい時、同じ機能を色んなアプリに追加したい時、に使うと良さそうです。

そしてプラグインの製作には、初心者の方はKintoneに用意されている「create-plugin」というツールを使うのがおすすめです!
プラグイン製作のための型を用意してくれますし、何より「自動パッケージング & 自動アップロード」で随時デバックを行いながら開発出来るのが扱いやすいです。
残念ながら本日はプラグインの概略を把握するのに殆どの時間を費やしてしまいました...
明日は実際に開発を進めて学んだことをアウトプットしたいと思います!

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

HTML内のjavascriptからGAS(コード.gs)の関数を呼び出す

動機

スプレッドシート上で作ってる論文DBを論文1本ずつ閲覧できるページを作りたい。まあ、ちょっとしたWebアプリです。

やりたいこと

ページのトップに「次へ」と「戻る」ボタンがあって、それを押すと一個ずつ表示される論文が移り変わっていく。

準備

スプレッドシート

スプレッドシートは、2つのシートから構成。

論文DB

1枚は論文DBそのもので、著者名やタイトルなどの情報が並ぶ。
image.png

閲覧画面

もう1枚はこういう感じ。もともとはこの2枚目のシートを閲覧画面にして、ボタンをgsと結びつけてB1セルの数値を動かして、スプレッドシートのvlookup関数を使って順に論文データを表示させようとしていたのだが、iPadのスプレッドシートアプリだと、ボタンが動いてくれなかったので、Webアプリ化することにした。
image.png

なので、この「閲覧画面」シートのB1セルを、どこまで読んだかの記憶場所にすることにする。
そうすると、Webアプリの画面だけでなく、このスプレッドシートの閲覧画面でも読めるようになる。

コード.gs

つづいてスプレッドシートから「ツール」->「スクリプトエディタ」。
コード.gsに以下のコードを書き込む。

function doGet() { 
  myHTML = HtmlService.createTemplateFromFile("MyReader.html");
  return myHTML.evaluate(); 
}

HTML+Javascript

んで、エディタのファイルメニューからhtmlを作成(MyReader.html)。内容は以下のような感じ。
p要素の中に色々と書き込んでいきたいのでjQueryも読み込んでおいた。

MyReader.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <input type="button" id="Back_btn" value="前へ" >
    <input id="PaperNo" name="PaperNo" type="text" />
    <input type="button" id="Next_btn" value="次へ" >
    <P id="eTitle"/>
    <P id="jTitle"/>
    <P id="VolIssue"/>
    <P id="Year"/>
    <P id="eAbst"/>
    <P id="jAbst"/>
  </body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script type="text/javascript">
</script>
</html>

これでデプロイして、https://script.google.com/a/**************/exec にアクセスすると、MyReader.htmlの内容が表示される。まあ、この時点では見ての通りボタンとテキストボックスが表示されるだけ。

HTMLのJavascriptからコード.gsの関数にアクセス

同じプロジェクト内にあるのであれば、google.script.run.関数でHTML(JavaScript)側からコード.gs内の関数を実行できる。
ただ、戻り値を持っている処理の場合にはちょっとややこしい。結論を言うと、次のように関数実行に成功した場合と失敗した場合の処理(ハンドラ)を噛ませてから関数を実行しなければならない.

google.script.run
  .withSuccessHandler(/***/)
  .withFailureHandler(/***/)
  .myFunction();

/***/の箇所は、別に定義したコールバック関数名でも良いし、function(){}というのでコールバック処理を直接書く形でもよい。また、コード.gsで定義しているmyFunction()の戻り値は、各ハンドラが受けとっており、そのままコールバック関数の引数として渡される。

実装

処理としては、webアプリにアクセスして、myReader.htmlが読み込まれると、まずはスプレッドシートの「閲覧画面」シートのB1セルの値を取りに行く(getPaperNo())。getPaperNo()が成功したときにはハンドラはSuccessGetNo(paperno)を呼び出す。引数のpapernoはgetPaperNo()の戻り値。
値取得に成功したら、その値をhtmlのテキストボックスに入れるとともに、その値をキーにして、論文DB側からデータを読み込む(getPaperInfo(paperno))。getPaperInfo(paperno)が成功したときにはハンドラはSuccessGetInfo(paperinfo)を呼び出す。paperinfoはgetPaperInfo(paperno)の戻り値で、JSONオブジェクトになってる。
「次へ」ボタンや「前へ」ボタンが操作された場合には、これまたもともと「閲覧画面」上のボタンと結びつけた関数として用意していたforward()back()をgoogle.script.runを使って呼び出し、成功したら、番号を取得。さらにそれも成功したら論文データを上記と同じ手順で読み込む。

ということで、作ったのが、それぞれ以下の通り。

コード.gs
function forward() {  //   もともと閲覧画面の「次へ」ボタン用に作った関数
  var ss = SpreadsheetApp.openById("*************"); // スプレッドシートを取得
  var sheet = ss.getSheetByName("閲覧画面");
  myrange = sheet.getRange('B1');
  myvalue = myrange.getValue();
  myrange.setValue(myvalue+1);
  return;
}

function Back() {  //   もともと閲覧画面の「前へ」ボタン用に作った関数
  var ss = SpreadsheetApp.openById("*************"); // スプレッドシートを取得
  var sheet = ss.getSheetByName("閲覧画面");
  myrange = sheet.getRange('B1');
  myvalue = myrange.getValue();
  if(myvalue ==1)   return;
  myrange.setValue(myvalue-1);
  return;
}

function doGet() { 
  myHTML = HtmlService.createTemplateFromFile("MyReader.html");
  return myHTML.evaluate(); 
}

function GetPaperNo(){
  var ss = SpreadsheetApp.openById("*************"); // スプレッドシートを取得
  var sheet = ss.getSheetByName("閲覧画面");
  var myrange = sheet.getRange('B1');
  var myvalue = myrange.getValue();
  return myvalue;
}

function GetPaperInfo( PaperNo ){
  var ss = SpreadsheetApp.openById("*************"); // スプレッドシートを取得
  var sheet = ss.getSheetByName("シート1");
  var Author = sheet.getRange(PaperNo+1,2).getValue();
  var eTitle = sheet.getRange(PaperNo+1,3).getValue();
  var jTitle = sheet.getRange(PaperNo+1,4).getValue();
  var volissue = sheet.getRange(PaperNo+1,5).getValue();
  var year = sheet.getRange(PaperNo+1,6).getValue();
  var page = sheet.getRange(PaperNo+1,7).getValue();
  var ISSN = sheet.getRange(PaperNo+1,8).getValue();
  var DOI = sheet.getRange(PaperNo+1,9).getValue();
  var Link = sheet.getRange(PaperNo+1,10).getValue();
  var eAbst = sheet.getRange(PaperNo+1,11).getValue();
  var jAbst = sheet.getRange(PaperNo+1,12).getValue();

  var json = {
    "Author":Author,
    "eTitle":eTitle,
    "jTitle":jTitle,
    "volissue":volissue,
    "year":year,
    "page":page,
    "ISSN":ISSN,
    "DOI":DOI,
    "Link":Link,
    "eAbst":eAbst,
    "jAbst":jAbst
  }
  return json;
}
myReader.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <input type="button" id="Back_btn" value="前へ" >
    <input id="PaperNo" name="PaperNo" type="text" />
    <input type="button" id="Next_btn" value="次へ" >
    <P id="eTitle"/>
    <P id="jTitle"/>
    <P id="VolIssue"/>
    <P id="Year"/>
    <P id="eAbst"/>
    <P id="jAbst"/>
  </body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script type="text/javascript">
var Back_btn = document.getElementById('Back_btn');
var Next_btn = document.getElementById('Next_btn');

$(document).ready( function(){
// ページ読み込み時に実行したい処理
  google.script.run
      .withSuccessHandler( SuccessGetNo )
      .withFailureHandler( Failure )
      .GetPaperNo(); 
});

function Failure(){
  alert("失敗");
}

function SuccessGetNo(PaperNo){  // PaperNoはこの呼び出し元になっているGetPaperNoの戻り値
  alert(PaperNo);
  $("#PaperNo").val(PaperNo);
  google.script.run
    .withSuccessHandler(SuccessGetInfo)
    .withFailureHandler(Failure)
    .GetPaperInfo(PaperNo)
}

function SuccessGetInfo(PaperInfo){
  $("#eTitle").text("原題:"+PaperInfo.eTitle);
  $("#jTitle").text("邦題:"+PaperInfo.jTitle); 
  $("#VolIssue").text("巻号:"+PaperInfo.volissue); 
  $("#Year").text("発刊年: "+PaperInfo.year);
  $("#eAbst").text("Abstract: "+PaperInfo.eAbst);
  $("#jAbst").text("日本語訳: "+PaperInfo.jAbst);

}

Back_btn.addEventListener('click', function(){ // スプレッドシートの方のカウンタを一つ戻したうえで、GetNo、GetPaperInfoをする。
  google.script.run
    .withSuccessHandler(function(){
      google.script.run
        .withSuccessHandler( SuccessGetNo )
        .withFailureHandler( Failure )
        .GetPaperNo(); 
    })
    .withFailureHandler(Failure)
    .Back()
});

Next_btn.addEventListener('click', function(){
  google.script.run
    .withSuccessHandler(function(){
      google.script.run
        .withSuccessHandler( SuccessGetNo )
        .withFailureHandler( Failure )
        .GetPaperNo(); 
    })
    .withFailureHandler(Failure)
    .forward()
});

</script>
</html>

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

javascript 等価演算子と厳密等価演算子の違い

はじめに

純粋な僕「 ===== はイコールの数が違うから、多いほうが一致率が高いんだろうな」
「じゃあ三つある方を使っていこう」
「あれれ〜?おかしいぞ〜?なんで一致率の高い === を使わないで、 == を使ってるんだろう。もしかして、知らねえんじゃないの?ぷぷぷ」

ぷぷぷは俺でした。

違うことがわかりました。

image.png

厳密等価演算子の場合、比較する値の型変換を行ってくれるようです。

これはいい勉強になりました。

考え

型変換を求められる比較の場合は、等価演算子で良いが、
そうではない場合は、厳密等価演算子を使うほうがベターでしょう。

理由は、
例えば数値型の比較をしたい場合に、
文字列型が入ってきてしまう可能性がある(絶対ないとは言い切れないため)
そんな時に、等価演算子を使っていたため、結果が変わってしまうようでは、
バグの原因になる。
これを防ぐためには、厳密等価演算子を利用するのが、重要かと思う。
それをしないことが最もぷぷぷを回避できるのである。

つまり、適材適所
知らないと、よしなに比較をかけないので、
僕はこれからよしなに比較ができるようになったのである。
やったね。

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

あなたのCSS力の助けになってくれる素晴らしいWebサイト12選

以下はAman Varma( Twitter / GitHub / stackoverflow / Webサイト )による記事、12 Super websites to help you with your CSS problems ☺の日本語訳です。

12 Super websites to help you with your CSS problems ☺

これらは、あなたのCSSをより楽しく簡単に装飾してくれる、そしてもしかしたらあなたが躓いているCSSの問題の多くを解決してくれるかもしれないWebサイトたちです。

1. Pixel art

01.png

あなたもきっとピクセルアートが好きに違いありません。
90%の人はpngでピクセルアートを書いてサイトに追加していると思いますが、でもそのピクセルアートをCSSで書けたらもっといいと思いませんか?
そこでPixel Art to CSSです。
書いたピクセルアートをCSSに変換してくれます。

Pixel Art to CSS

2. Gradient

02.jpg

CSSグラデーションは非常に美しいですが、使いこなすのは難しいものです。
正しい色を見つける必要があり、何行ものコードを書かなければなりません。
また互換性に気を使うと、さらに多くのコードが必要になります。
それらの問題を解決するためにCSS GradientのWebサイトが存在します。
直感的なカラーセレクタや後方互換は大きな助けになってくれるでしょう。
ここにはまた、Gradient BackgroundsColor Shadesといったツールも存在します。

CSS Gradient

3. Compatibility

03.jpg

古いブラウザには対応していないCSSタグや、逆に新しすぎて誰も対応していないようなCSSタグがたくさん存在します。
自分の使っているタグがどれだけのブラウザに対応しているかは、様々なサイトを見て確認しなければなりません。
そんなときに役立つのがCanIUseです。
CanIUseは、どのブラウザがどのタグをサポートしているかを教えてくれます。
またNewsセクションでは、ブラウザが新しく対応したタグの状況について教えてくれます。

CanIUse

4. Cheatsheets

04.png

もしかしたら、私のようにCSSタグをよく忘れてしまうかもしれません。
そんなときのために、手元にCSSチートシートを用意しています。
CSSチートシートで検索すると数千もの結果が出てくるのですが、その中でも最高のものであると私が信じているもののひとつがDevhintsです。
CSS以外にも、HTML、Python、Rubyなど、多くの言語のチートシートが用意されています。

Devhints

5. Color Palettes

05.png

あなたのプロジェクトにぴったりのイメージカラーは見つかりましたか?
ネットでは多くの素晴らしいオンラインカラーパレットを見出すことができますが、私が気に入っているのはシンプルなColor Huntです。
新着順、注目順、ランダム順などで並べ替えることができ、最も頭を使わずにカラーパレットを選択することができます。

Color Hunt

6. Unicode

06.png

世界には何万ものUnicodeが存在するので、全てを覚えきれない人もときにはいることでしょう。
この問題を解決するためにUnicode Tableが存在します。
あなたのプロジェクトにマッチする絵文字やUnicodeを、Unicode Tableから探してみましょう。
また、 ?のような面白Unicodeも見つけられるかもしれません。

Unicode Table

7. CSS Validator

07.png

CSSを正しく使えているか、どのように判断しましょう。
W3C CSS Validatorを使えばいいのです。
W3C CSS Validatorは、WebデザイナーやWeb開発者が作ったCSSをチェックできるように、W3Cが制作したサイトです。

CSS Validator

8. Old Browser support

08.png

あなたのWebサイトを、未対応のユーザエージェントを持つ古い古いブラウザや、新しいブラウザでも見れるようにしたいと思いませんか?
そこでAutoprefixerです。
AutoprefixerはCSSを解析し、Can I Useの対応状況を見て、CSSにベンダープレフィックスを追加してくれます。
AutoprefixerはGoogleにも推奨されていて、実際にTwitterやTaobaoが使用しています。

Autoprefixer CSS online

9. Other awesome tools

09.jpg

Bennett Feelyは、初心者にもプロにも助けになるような、素晴らしいWebサイトを幾つも開発しています。
私が最も気に入っているのはCSS Pie Chartで、conic-gradientを使った円グラフを生成することができます。
またClippyはCSSで画像をクリッピングし、Image Effect wit CSSではCSS blend modeを使った様々な画像効果を紹介し、そしてCSS Gradientsでは様々なCSSグラデーションを参照することができます。

Bennettfeely

10. Tobias Ahlin Thanks to Siddhartha Sarkar

10.png

Tobiasは、Webサイトで幾つもの素晴らしいCSSを公開しているもう一人の開発者です。
彼のWebサイトからProjectセクションを見てみると、彼が手掛けた多くの作品、Moving LettersTypeSourceSpinKitなどを見ることができます。
とても役に立ちそうなCSSです。

Tobias Ahlin Bjerrome

11. Hayk An:: Workbench Thanks to Amruth Pillai

11.png

Hayk AnのWebサイトには、インターネット上で最も有用で興奮させられるサイトのいくつかを見つけることができます。
これらはインスピレーションの金脈であり、あなたのウェブサイトデザインにきっと多くの刺激を与えてくれるでしょう。

Hihayk

12. You can suggest in the comment I will make it my number 12 or we can till 20 ;)

あなたの好きを12番目にしてくれていいのよ。

12.gif

ピクセルアート By lipixelart

コメント欄

Spinkit.cssがかっこいいローディングしてくれる。」「この作者のサイトを追加したよ。」
「アプリを作るときにScaleでカラーパレットを選択したよ。」「この作者のサイトを追加したよ。」
「CSSセレクタがわからないときのSelectors Explainedいいぞ。」
「有益なリンクありがとう」
「いくつかお気に入りに登録した!」
「素晴らしいキュレーションありがとう? 」

感想

もはや今のCSSは、私のような一般人のレベルでは何も作れないところにまでなっています。
一からどうにかするのは諦めて、誰かが作ってくれている、これら優れたツールやパーツを拝借させてもらって、ブラッシュアップを図っていきましょう。
単にコピペするだけでもワンランク上の見た目を作れるようになる、素晴らしいサイトたちです。

まあ私はフロントエンドエンジニアではないので、最近はもうBulma突っ込んで適当にdiv生やして終わりますけどね。

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

emotionを使うメリット

この記事について

社内プロジェクトでstyled-componentsが主に使われており、世間でもstyled-componentsが主流な感じなのですが、
徐々に伸びてきているemotionが気になったので導入して使ってみました。

お…!ってなったのでstyled-componentsに比べてよかったこと、悪かったことを簡単にまとめます。
(*随時更新)

emotion初心者なのでこんなメリット/デメリットある!ってことがあれば教えてください:innocent:

emotionとは

後発のCSS-in-Jsのライブラリです。
基本的にはstyled-componentsでできることはほぼできそうな印象を受けました。

個人的に良い!って思った点は
- TypeScriptとの相性◎ (https://emotion.sh/docs/typescript)
- render内(html)が見やすくなる

// styled-componentっぽく書いたりできます
import styled from '@emotion/styled'

const Pin = styled.div`
  color: red;
`

render(<Pin> pin component </Pin>)

でもこっちの書き方が便利です(TSの恩恵受けられる)

import styled from '@emotion/styled'

// Object Stylesっていいます

const Pin = css({
    color: 'ref',
});

render(<div css={Pin}> pin component </div>)

他にもいろんな書き方できるみたいです
https://emotion.sh/docs/introduction

メリット

TypeScriptと一緒に使ったとき

css構文のサポート受けられる

react書いてるとcss(っぽいの)をかくのめっちゃめんどくさいって思ってました。
これがあればちょっとストレス軽減!!
スクリーンショット 2020-05-01 15.54.25.png

コンパイル前に間違ったら叱ってくれる

タイポとか。地味に嬉しいやつです。
スクリーンショット 2020-05-01 15.53.49.png

かぶってるやつ教えてくれる

考えずに書いてるとこんなこともあったりするので…

スクリーンショット 2020-05-01 16.07.35.png

読みやすさ

styled-componentsを使っていると、React Componentとただのタグにstyleつけてるのが混ざって読みにくく感じていました。
緑ばっかりになっててしんどい:mask:

(
  <Header>
  <SiteTitle> ~~ hotel </SiteTitle>
  <Navigation>
   <NavigationList>
    <NavigationItem><a href="/plan">宿泊プラン</a></NavigationItem>
    <NavigationItem><a href="/price">料金案内</a></NavigationItem>
    <NavigationItem><a href="/reserve">宿泊予約</a></NavigationItem>
    <NavigationItem><a href="/access">交通アクセス</a></NavigationItem>
   </NavigationList>
  </Navigation>
 </Header>
);

emotionを使うといつものhtmlのタグにclassNameっぽくかけるので読みやすいです。
ここにreactComponentが追加されたら、
「あ、これ特別なやつ」ってなるので脳の負担が減るように感じました。

return (
  <header css={Header}>
  <h1 css={SiteTitle}> ~~ hotel </h1>
  <nav css={Navigation}>
   <ul css={NavigationList}>
    <li css={NavigationItem}><a href="/plan">宿泊プラン</a></li>
    <li css={NavigationItem}><a href="/price">料金案内</a></li>
    <li css={NavigationItem}><a href="/reserve">宿泊予約</a></li>
    <li css={NavigationItem}><a href="/access">交通アクセス</a></li>
   </ul>
  </nav>
 </header>

);

メディアクエリが簡単

styled-componentsの時は結構書き方めんどくさかったのですが、
emotionでは下記のようにかけます!

const Pin = css({
    color: 'red',
    '@media(min-width: 420px)': {
    color: 'orange'
    } 
});

デメリット

デメリットもいろいろあるみたいです。

create-react-appとの相性の悪さ

emitして設定を書き換えなきゃいけないのですが、無理矢理感があるので、create-react-appで進めるならstyled-componentsで進めたほうが良い気がします。
(結構めんどくさかったです)

経験した中では上記だけです:innocent:
あと、object stylesはTSの恩恵受けられるので書くのですが、書き方めんどくさい(小声)

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

[Javascript][Jquery]jQuery から JavaScriptへの変換

jQuery から JavaScriptへの変換

Jquery から Native のJavascript (Vanilla JS/ Pure JS)
に変換するときの参考。

Selectors

  • $("body") => document.body
  • $("html") => document.documentElement
  • $(selector) => document.querySelector(selector)
  • $elem.find(selector) => elem.querySelector(selector)
  • $elem.parent() => elem.parentNode
  • $elem.closest('.country') => elem.closest('.country')
  • $input.closest('form') => input.form
  • siblings: $el.prev() and $el.next() => el.previousElementSibling and el.nextElementSibling

Input values: getting and setting

  • $input.val() => input.value
  • $input.val("hello") => input.value = "hello"

Event Listeners

  • $elem.on(eventName, handler) => elem.addEventListener(eventName, handler)
  • $elem.off(eventName) => elem.removeEventListener(eventName, handler) // note you must keep a ref to handler for this
  • Key event listeners: use e.key === "+" instead of e.which === 43

To defer event handling to some ancestor

$ancestorElem.on(eventName, elemSelector, handler)

=>

var handler = function(e) {
    var elem = e.target.closest(elemSelector);
    if (elem) {
      // do stuff
    }
}
ancestorElem.addEventListener(eventName, handler, false)

Event handlers

  • key events: (e.which === 45) => (e.key === "Tab") - docs

Class manipulation

  • $elem.addClass(c) => elem.classList.add(c)
  • $elem.removeClass(c) => elem.classList.remove(c)
  • $elem.toggleClass(c) => elem.classList.toggle(c)
  • $elem.hasClass(c) => elem.classList.contains(c)
  • $elem.attr('class') = 'some classes' => elem.className = 'some classes'

Styling

  • $el.css({ top: "10px" }) => el.style.top = "10px"

Scroll position

  • $el.scrollTop() => el.scrollTop
  • $el.scrollTop(10) => el.scrollTop = 10

Utils

  • $.inArray(item, arr) > -1 => arr.indexOf(item) > -1 // note: could use arr.includes(item) but would require polyfill for IE11
  • $.extend({}, defaults, options) => Object.assign(defaults, options)
  • $.trim(s) => s.trim()
  • $input.val() => input.value

Creating/appending elements

$("<div>", {"class": c}).appendTo(parent);

=>

var elem = document.createElement("div");
elem.className = c;
container.appendChild(elem);

To add copy:

$elem.text(s)

=>

var elemText = document.createTextNode(text);
elem.appendChild(elemText)

To append a HTML string:

  • $elem.append(htmlString) => elem.insertAdjacentHTML('beforeend', htmlString);

Attributes

  • $elem.attr("placeholder") => elem.getAttribute("placeholder")
  • $elem.attr("placeholder", p) => elem.setAttribute("placeholder", p)

Properties

  • $el.props("disabled") => el.disabled
  • $el.props("readonly") => el.readOnly

元記事

https://github.com/jackocnr/intl-tel-input/wiki/Converting-jQuery-to-JavaScript

参考

https://wemo.tech/2101

https://www.webprofessional.jp/dom-manipulation-vanilla-javascript-no-jquery/

https://www.willstyle.co.jp/blog/1025/

https://qiita.com/nightyknite/items/668c112c40931515ed67

https://qiita.com/tyoukan__/items/e4582c6774748b7f96cd

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

Vuejs にてhtmlタグのlangプロパティの値を切り替える方法

本文の言語に合わせて、htmlのlangプロパティの値も変更しなきゃ!と思った時

Vuejsで複数の言語に対応したサイトを開発しているところで、本文の言語が切り替わる際に、合わせてhtmlタグのlangも変更しなきゃとふと思った方もいると思う。だが、htmlタグはapp/public/index.htmlにあって、どうすればと思うだろう。その方々に方法を共有します。

*ちなみにlangプロパティの値を変更しなくても特に影響や問題は無いらしい。
しかし、htmlのlangと本文の言語が違う(例:lang='ja'だが、本文内容:Qiita is very goodのように英語)ことにより、ブラウザで「このページを翻訳しますか?」と聞かれる時があるので、langを変更しておくのは良いとのこと。

環境の整理

  • 開発環境:Vuejs
  • 多言語対応のためのプラグイン:Vue i18n
  • 今回例で見せる多言語は3つ:
     日本語:lang = 'jp',
     英語 :lang = 'en',
     韓国語:lang = 'ko',

方法

  1. App.vueを開いて、watch オプションを使用しよう。
  2. 'i18n.locale'をモニタリングし、切り替わった言語を取得する。
  3. handler内で直接コードを書いても構わないが、今回はsetHTMLlangというメソッドを作る。
  4. setHTMLlang内にて、言語(=value変数)を判定し、
    document.get...で取得したhtmlのlangプロパティの値に、jp, en, koをそれぞれ付与する。

完成コード

App.vue
export default {
  name: 'App',
  watch: {
    '$i18n.locale': {
      handler (value) {
        this.setHTMLlang(value)
      }
    }
  },
  methods: {
    setHTMLlang (value) {
      if (value === 'jp') {
        document.getElementsByTagName('html')[0].lang = 'jp'
      } else if (value === 'en') {
        document.getElementsByTagName('html')[0].lang = 'en'
      } else if (value === 'kr') {
        document.getElementsByTagName('html')[0].lang = 'ko'
      }
    }
  }
}

*今回はVuejs環境下での実例だったが、
watchオプションのような機能(モニタリング)が備わっていて、javascriptが使用できる環境下では少しコードを変更し応用できると思う。

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

Vuetifyでよく使うJavascriptチップス

v-selectのitemsは配列のみ

image.png

しかも、text/valueというkeyの指定付き。
でもAPIから取ってきたデータはオブジェクトの場合。

取ってきたデータ

api_data = {
 "id1":"hoge",
 "id2":"huga",
 "id3":"hogehoge",
}

select_items = [
 {
  text: "hoge",
  value: "id1"
 },
 {
  text: "huga",
  value: "id2"
 },
 {
  text: "hogehoge",
  value: "id3"
 }

にする状況の場合。

select_items = Object.keys(api_data).map( key => {
 return {
  text: api_data[key],
  value: key
 }
});

for in を使うと

let select_itemse = [];
for(var key in api_data){
 select_itemse.push({
  text: api_data[key],
  value: key
 })
}

個人的にはmapを使った方がわかりやすいけど、for inは他の言語を触っている人にはわかりやすいと思う。

キーが異なる場合

下記の様なデータの場合

api_data = [
 {
  "id":"id1",
  "title":"hoge"
 },
 {
  "id":"id2",
  "title":"huga"
 },
 {
  "id":"id3",
  "title":"hogehoge"
 }
}

item-text,item-valueプロパティを使います。

image.png

<v-select 
 items="api_data"
 item-text="title"
 item-value="id"
/>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react v15 -> v16へマイグレーション 

  • react v15.6.2 -> v16.13.1
  • react-dom v15.6.2 ->v16.13.1
  • react-router v3.0.3 -> v5.2.0
  • react-router-dom 5.2.0(追加)
  • styled-components v2.3.2 -> v5.1.1
  • react-responsive v2.0.0 -> v8.1.0
  • react-document-meta -> react-helmet(変更)
  • history v3.0.0 -> v5.0.0
  • react-slick ->v0.27.1

react

componentWillReceivePropsが廃止予定

今後削除される機能なので、ロジックを組み直したり, componentDidUpdateに置き換えたりなどの対応が必要です。
今回は急ぎなので、UNSAFE_componentWillReceiveProps に置き換えます。
componentWillReceivePropsと機能は同じです。後ほどリファクタリングします..!

- componentWillReceiveProps(nextProps) { ...
+ UNSAFE_componentWillReceiveProps(nextProps) {

componentWillMountが廃止予定

componentWillReceivePropsと同様です。こちらもUNSAFE_componentWillMount()にしてあとでリファク(

- componentWillMount() { ...
+ UNSAFE_componentWillMount() {

また、自分のコードでcomponentWillMount等の廃止予定ライフサイクルメソッドを使っていなくても、依存パッケージ内で使用されている場合がある。
その場合はパッケージをアップデートするか、アップデート対応していない場合はパッケージの入れ替えを行なった。

ルーティング

Link

Linkコンポーネントは react-routerからreact-router-domへ移動されたので、
import元を全て書き換えます。

- import { Link } from 'react-router'
+ import { Link } from 'react-router-dom'

browserHistoryの廃止

v4からbroserHistoryが廃止になっているので、置き換えが必要になります。

- <Router history={browserHistory}>
-  ...
- </Router>
+ <BrowserRouter>
+  ...
+  </BrouserRouter>
- const hashLocation = browserHistory.getCurrentLocation().hash.replace('#', '')
- const hashLocation = this.props.location.hash.replace('#', '')

子コンポーネントでlocationやhistoryを使いたい場合(propsで渡って来ない場合)は、
react-routerがhooks対応しているのでそちらを使えばバケツリレーにならずスッキリかけます。

- import React, { Component } from 'react'
- class Breadcrumb extends Component {
- componentDidMount() {
-   this.props.dispatch(breadcrumb(browserHistory.getCurrentLocation().pathname));

+ import React, { useEffect } from 'react'
+ const Breadcrumb = (props) => {
+  const location = useLocation()
+  useEffect(() => {
+   props.dispatch(breadcrumb(location.pathname))
+  }, [])

react-routerのhooksに関しては、下記の記事がわかりやすかったです。
React RouterがHooks対応したので使い方を整理する

store.jsx
import { history } from './user'

export default function configureStore(initialState) {
...

sagaMiddleware.run(rootSaga, { history })

styled-components

extend -> styled()

extendはv4から廃止されていたので、styled(Comp) の表記に統一します。

- const ButtonTertiary = Button.extend`
- ...
- `
+ const ButtonTertiary = styled(Button)`
+ ...
+ `

injectGlobal -> createGlobalStyle

injectGlobalというグローバルのCSSの記述が、v4からcreateGlobalStyleというAPIに取って代わられています。

- injectGlobal`
-@font-face {
-   ...
+ export const GlobalStyle = createGlobalStyle`
+ @font-face {
+  ...

react-router-scroll

SPAでは何らかの処理をしない限り、ページ遷移後もスクロールの位置が先頭に戻らない。
今回アップデートしたプロジェクトでは、 react-router-scrollが使われていた。
しかし、3年前から更新がかかっていない...
自分で実装した方が早いと判断し、パッケージを捨てることにする。

- import { useScroll } from 'react-router-scroll';
+ import ScrollTop from './containers/ScrollTop'

ルーティング部分
 <BrowserRouter>
    <ScrollTop>
      <Switch>
        <Route exact path="/" component={Top} />
      </Switch>
    </ScrollTop>
 </BrowserRouter>
ScrollTop.jsx
import { useEffect } from 'react'

export default function ScrollTop(props) {
  useEffect(() => {
    window.scrollTo(0, 0)
  }, [props.location])
  return props.children
}

実はredux周りなんかも入れたらもっと膨大な作業になりました...
こまめなアップデート、目指していきたいです!

参考資料

Styled Components v4について
[React]react-router v4で画面遷移時に前のページのスクロール位置が残る

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

Nest.js における Jest を用いた typeORM repository のテスト

誰向け ??

Nest.jsでのJestを用いたtypeormのrepositoryテストについて知りたい人。悩んでいる人。

背景

最近少しずつ使用者の増えているNest.jsですが、まだ日本語ドキュメントもありません。
さらにtypeORMというORマッパーを使い、そのテストをJestで書いている人の記事は日本語では数えるほど。(英語だと沢山あります)
そして本当に不思議なことにその方達が書いているテストはserviceばかり。repositoryについての記事なんて全然ありません。そんな中でテストを書いてこいと言われてガチで30回くらい挫折した私の苦悩とついにたどり着いたグリーンなテストについて紹介しようと思います。

この記事が末長くnestのテストで苦しむ人の助けになればと思います。この記事には自分の苦悩も綴っておくので、通るテストだけ知りたいという人は最後までスクロールしてください!

テストが緑でpassした時は本当に涙が出ました。

pose_kandou_man.png

環境

DB: Mysql
言語: typescript

実装

いよいよ実装に入っていきます

  1. repositoryでの処理の確認
  2. テストコード

の流れで説明していきます。今回はNestの仕組み自体(そもそもrepositoryとは、など)やjestの記法についての解説は省きます。

repositoryでの処理

今回は以下のようなレポジトリについてのテストを書きます。

createItemInputという引数を受け取り、Itemというentityを作成して返すcreateItemというメソッドを持ったitemRepositoryです。

item: {
  date: string,
  price: number,
  method: string,
  user: User (他ファイルで定義されたEntity)
}
item.repository.ts
export class ItemRepository extends Repository<Item> {
  async createItem(
    createItemInput: CreateItemInput,
  ) {
    const {
      date,
      price,
      method,
      userId
    } = createItemInput;

    // 1. itemを作成し、各種プロパティに代入していく
    const item = new Item();
    item.date = date;
    item.price = price;
    item.method = method;
    // userをuserRepositoryから取得してitemに代入
    const user = await getRepository(User).findOne({id: userId});
    item.user = user;

    // 2. itemを保存するときのtransaction処理の開始
    const queryRunner = getConnection().createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      const savedItem = await queryRunner.manager.save(
        Item,
        item,
      );
      await queryRunner.commitTransaction();
      return item;
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw new InternalServerErrorException();
    } finally {
      await queryRunner.release();
    }
  }
}

一見なんてことないコードなんですが、テストをするとなるとなかなか一筋縄でいきません。

テストを書いてみる

難所その1 : getRepository()が呼び出せない

いざこのテストを書こうとするとこんな思考回路になると思います

んー、そうだ、テストファイル内でgetRepositoryで新しくItemRepositoryとってきて、そこに適切な引数を渡してちゃんとitemが返ってくるか見ればいいな。よし、書けたぞ。

item.repository.spec.ts
it('should create item', () => {
  const repo = getRepository(Item)
  repo.createItem(hoge)
  ...
})

よし、実行してみよう。

ConnectionNotFoundError: Connection "default" was not found.

おめでとうございます!!本日1人目の死亡者が確認されました。
test環境だとデータベースに繋がっていない環境で処理が行われるので、こんな感じで「データベースとのConnectionがないぞ」って怒られます。ちなみにこれはテストファイルに限らず、item.repository.ts内部でのgetRepository()でもこのエラーがでます。

それならとりあえずテストファイルではnew ItemRepository()で回避するか。でもItemRepository内部で呼び出されるgetRepository()の問題はどう解決すればいいんだ...?

はい、jestのドキュメントに書いてありました。jest.spyOn()を使うと呼び出し先のモジュールをモック?スパイ?することができます。(https://jestjs.io/docs/ja/jest-object#jestspyonobject-methodname)

これを実装するとこんな感じになります。ついでにfindOneメソッドをモックしておきました。

const spyGetRepository = jest
    .spyOn(TypeOrm, 'getRepository')
    .mockImplementation(() => {
      const repo = new UserRepository();
      repo.findOne = jest.fn().mockResolvedValue(new User());
      return repo;
    });

これにてgetRepository()は無事エラーを吐かなくなりました?

難所その2 : getConnection()が呼び出せない

先ほどのgetRepository()と同じ理由でgetConnection()がエラーを吐きます。

またこれね、さっきと同じ方法で解決したろ。よし、これで行けるはず。

const spyGetConnection = jest
        .spyOn(TypeOrm, 'getConnection')
        .mockImplementation(() => new Connection())

これだとTypescriptの構文解析先生に怒られます。Connectionの引数に無数の引数を入れる必要があるからです。これを解決するためにtypescriptのasを用います。

const mockConnection = {} as Connection;

これだとなんとか解析を潜り抜けることができます。しかし、ConnectionにはQueryRunnerクラスを返すcreateQueryRunnerメソッドなどがあるので、これらも追加する必要があります。

const spyGetConnection = jest
        .spyOn(TypeOrm, 'getConnection')
        .mockImplementation(() => {
          const mockConnetion = {} as TypeOrm.Connection;
          mockConnetion.createQueryRunner = jest.fn().mockImplementation(() => {
            // ここでQueryRunnerを作成してreturnする
          });
          return mockConnetion;
        });

よし、あとはConnectionと同じノリでQueryRunnerも作成したら終わりだ!!さっきのrepositoryの処理を見直してQueryRunnerの内部に何をモックすればいいか振り返ってみるか

queryRunner.connect();
queryRunner.startTransaction();
queryRunner.manager.save(args)
...

なるほど、connect()とかはjest.fn()でモックすればいいから、managerとかいうクラスを作ってその下にsaveメソッドを生やせばいいんだな。これでどうだ

const spyGetConnection = jest
        .spyOn(TypeOrm, 'getConnection')
        .mockImplementation(() => {
          const mockConnetion = {} as TypeOrm.Connection;
          mockConnetion.createQueryRunner = jest.fn().mockImplementation(() => {
            const qr = {} as TypeOrm.QueryRunner;
            // ↓ここでエラー!!!!
            qr.manager = {save: jest.fn()} as TypeOrm.Manager;
            // ↑ここでエラー!!!!
            qr.connect = jest.fn();
            qr.release = jest.fn();
            qr.startTransaction = transactionMock.start;
            qr.commitTransaction = transactionMock.commit;
            qr.rollbackTransaction = transactionMock.rollback;
            qr.release = transactionMock.release;
            return qr;
          });
          return mockConnetion;
        });

すると、

managerは読み取り専用プロパティであるためmanagerに代入することはできません

saigai_teiden.png

もうこのエラーが出てきた時に目の前が真っ暗になりました。readonlyプロパティをどうやって設定してmockすればいいんだと。

これも悩みに悩んだ結果以下の方法で解決することができます。

const mockConnetion = {} as TypeOrm.Connection;
mockConnetion.createQueryRunner = jest.fn().mockImplementation(() => {
  const qr = {
    manager: {},
  } as TypeOrm.QueryRunner;
  qr.manager = {}
  Object.assign(qr.manager, {
    save: jest.fn().mockResolvedValue({ id: 1 }),
  });
});

初期化段階でmanagerを入れておいて、Object.assignで置き換えることで解決します。これにて全ての苦難をクリアです。おめでとうございます??

最終的なテストコード

お待たせしました。これが紆余曲折を経てたどり着いたコードです。バッドプラクティスな書き方をしていたらご指摘ください!(また、これは本物のコードの一部を取り出してきたものなのでそのままでは動かないと思われます)

item.repository.spec.ts
describe('create', () => {
  /*
  テスト環境ではdbに繋がっていないのでgetRepositoryなどのdbConnection周りのメソッドがエラーを吐くのでモックする必要がある。
  */
  const spyGetRepository = jest
    .spyOn(TypeOrm, 'getRepository')
    .mockImplementation(entity => {
      return new UserRepository()
    });
  let transactionMock;
  beforeEach(() => {
    transactionMock = {
      start: jest.fn(),
      commit: jest.fn(),
      rollback: jest.fn(),
      release: jest.fn(),
    };
  });
  // 正常系動作のテスト
  describe('normal behavior', () => {
    beforeEach(() => {
      /* 
        上記と同様の理由でConnection周りをspyしていく。正常系の動作を規定。
        対象は
        Connectionを返すgetConnection()関数
        QueryRunnerクラスを返すcreateQueryRunnerメソッド、を持つConnectionクラス
        各種メソッド(connect(), startTransaction(), etc...)を持つQueryRunnerクラス
      */
      const spyGetConnection = jest
        .spyOn(TypeOrm, 'getConnection')
        .mockImplementation(() => {
          const mockConnetion = {} as TypeOrm.Connection;
          mockConnetion.createQueryRunner = jest.fn().mockImplementation(() => {
            // QueryRunnerを作成。managerがreadonlyプロパティなので初期化段階で入れておき、Object.assignで変更する必要がある。
            const qr = {
              manager: {},
            } as TypeOrm.QueryRunner;
            qr.manager = {}
            Object.assign(qr.manager, {
              save: jest.fn().mockResolvedValue({ id: 1 }),
            });
            // QueryRunnerに必要なメソッドをモックしていく
            qr.connect = jest.fn();
            qr.release = jest.fn();
            qr.startTransaction = transactionMock.start;
            qr.commitTransaction = transactionMock.commit;
            qr.rollbackTransaction = transactionMock.rollback;
            qr.release = transactionMock.release;
            return qr;
          });
          return mockConnetion;
        });
    });
    it('should create procurement', async () => {
      // createProcurementを発火
      const returnedItem = await mockItemRepository.createItem(引数);
      // Itemが返ってきているかのテスト
      expect(returnedItem).toEqual(
        expect.objectContaining({
          price: expect.anything(),
          ...その他
        }),
      );
      // 正しく処理が行われていればtransactionのrollbackだけ呼ばれない
      expect(transactionMock.start).toHaveBeenCalled();
      expect(transactionMock.commit).toHaveBeenCalled();
      expect(transactionMock.rollback).not.toHaveBeenCalled();
      expect(transactionMock.release).toHaveBeenCalled();
    });
  });

終わりに

私がこのグリーンなテストをかけるまで2週間くらい悩み続けました。本当に何度も諦めかけましたが、同僚の助けもあり無事解決することができました。皆さんのNestライフが充実するものになることを祈っています。参考になったという方はぜひLGTMやストックお願いいたします!!

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

iGarage 体験会カリキュラム

JavaScriptでおみくじを作ってみよう

今日のゴール

ボタンをクリックするとランダムで運勢が表示されるおみくじを作ってみましょう。

スクリーンショット 2020-03-30 0.51.59.png

     ↓クリック↓

スクリーンショット 2020-03-30 0.51.53.png

使用するファイル

MyOmikujiフォルダ内にある、index.html,styles.css,main.jsを編集していきます。

  • index.htmlでは全体の見た目
  • styles.cssではスタイル
  • main.jsではアニメーション

を主に作ります。

見た目の実装

まずはindex.htmlで全体の見た目を作っていきます。

テキストエディタでindex.htmlを開いて、以下を貼り付けましょう。

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>おみくじ</title>
  <link rel="stylesheet" href="css/styles.css">
</head>
<body>
  <script src="js/main.js"></script>
</body>
</html>

これはHTMLを書く際の決まりのようなものです。

次にボタンとなる要素を作ります。
<body>の下の行に、

index.html
  <div id="btn">?</div>

を入れましょう。
最初ボタンに表示される?を入れておきました。
また、id="btn"と入れておくと、後でJavaScriptから扱いやすくなります。

ここまで出来ているか、index.htmlをブラウザで開いて確認してみましょう。
?が表示されていればOKです。

ボタンのスタイルを作ろう

次に、styles.cssを編集してボタンのスタイルを作っていきましょう。

先程、id="btn"として?を表示した部分をボタンの形にしましょう。
styles.cssを開いて、

styles.css
#btn {
  width: 200px;  /* 幅: 200px */
  height: 200px;  /* 高さ: 200px */
  background: #ef454a;  /* 背景色: #ef454a */
  border-radius: 50%;  /* ボタンを円形に */
  margin: 30px auto;  /* ボタンの位置を上から30px,左右の余白を均等に */
}

一度、ブラウザを更新して確認してみましょう。

スクリーンショット 2020-03-30 1.35.20.png

?の位置がずれてしまっているので、直してみましょう。

margin: 30px auto;の下の行を追加します。

styles.css
#btn {
  width: 200px;  /* 幅: 200px */
  height: 200px;  /* 高さ: 200px */
  background: #ef454a;  /* 背景色: #ef454a */
  border-radius: 50%;  /* ボタンを円形に */
  margin: 30px auto;  /* ボタンの位置を上から30px,左右の余白を均等に */
  /* ここから */
  text-align: center;  /* 左右中央揃え */
  line-height: 200px;  /* 高さ調整 */
  color: #fff;  /* 文字の色: 白 */
  font-weight: bold;  /* 太字 */
  font-size: 42px;  /* 文字の大きさ: 42px */
  /* ここまで */
}

ブラウザを更新して確認してみましょう。

スクリーンショット 2020-03-30 2.36.58.png

ボタンをクリックした操作を作ろう

ボタンをクリックできるようにして、クリックした時の処理を書いていきましょう。

main.jsを開いて、

main.js
'use strict';

を入れましょう。これによって厳密なエラーチェックをするようにします。
その下の行に、

main.js
{
  const btn = document.getElementById('btn');

  btn.addEventListener('click', () => {
    btn.textContent = 'hit!';
  });
}

を入れましょう。

ブラウザを更新して変化を見てみましょう。
ボタンをクリックするとhit!と表示されるようになりました。

スクリーンショット 2020-03-30 2.39.43.png

ここで、ボタンがクリックできることがわかるようにスタイルを追加し、さらに、ボタンの形を整えていきましょう。

styles.cssを開いて、
font-size: 42px;の下の行を追加します。

styles.css
#btn {
  width: 200px;  /* 幅: 200px */
  height: 200px;  /* 高さ: 200px */
  background: #ef454a;  /* 背景色: #ef454a */
  border-radius: 50%;  /* ボタンを円形に */
  margin: 30px auto;  /* ボタンの位置を上から30px,左右の余白を均等に */
  text-align: center;  /* 左右中央揃え */
  line-height: 200px;  /* 高さ調整 */
  color: #fff;  /* 文字の色: 白 */
  font-weight: bold;  /* 太字 */
  font-size: 42px;  /* 文字の大きさ: 42px */
  /* ここから */
  cursor: pointer;  /* カーソルをポインターに */
  box-shadow: 0 10px 0 #d1483e;  /* ボタンの下に影をつける */
  user-select: none;  /* 中のテキストを選択不可に */
  /* ここまで */
}

一番下の行に、

styles.css
#btn:hover {
  opacity: 0.9;  /* ホバーした時に色を薄く */
}

/* クリックするとボタンが押し込まれたように */
#btn:active {
  box-shadow: 0 5px 0 #d1483e;
  margin-top: 35px;
}

を追加します。

ブラウザを更新して確認してみましょう。

乱数を表示してみよう

最終的にはランダムで運勢が表示されるようにします。
そのために、乱数を使ってランダムな数値を作ってみましょう。

JavaScriptでは、Math.random()という命令を使うことで0以上1未満のランダムな数値を生成出来ます。

main.js'hit!'の部分をMath.random()に変えます。

main.js
{
  const btn = document.getElementById('btn');

  btn.addEventListener('click', () => {
    btn.textContent = Math.random();
  });
}

このようになります。

ブラウザを更新して確認してみましょう。
0以上1未満のランダムな値が生成されるかと思います。

スクリーンショット 2020-03-30 3.03.02.png

条件分岐で運勢を表示しよう

条件分岐にはif文を使います。

まずは、先程の

main.js
{
  const btn = document.getElementById('btn');

  btn.addEventListener('click', () => {
    btn.textContent = Math.random();
  });
}

を消して、

main.js
{
  const btn = document.getElementById('btn');

  btn.addEventListener('click', () => {
    const n = Math.random();
  });
}

とします。
これでnをランダムな数値としています。

このnMath.random()によって、0以上1未満の乱数となるので、
例えば、大吉を22%の確率で表示したい場合は、

main.js
    if (n < 0.22) {
      btn.textContent = '大吉'
    }

をこの行:

main.js
    const n = Math.random();

の下の行に入れてあげれば良いでしょう。

また、大吉以外の場合も書いてあげましょう。

main.js
    if (n < 0.22) {
      btn.textContent = '大吉'
    } else if (条件2) {
      btn.textContent = '中吉'
    } else if (条件3) {
      btn.textContent = '小吉'
    } else {
      btn.textContent = ''
    }

といったように書いてあげます。
22%の確率で大吉、8%の確率で中吉、13%の確率で小吉、それ以外の場合はと表示させるとしたら、
この条件2条件3をどうすれば良いか考えてみましょう。

書けたら、コーチに見てもらいましょう。

最後に、他の運勢を追加したり確率を変えたりして、オリジナルのおみくじを完成させましょう。


体験会カリキュラムはこれで以上になります。

Web開発では、このような全体の見た目、スタイル、アニメーションなどを取り入れながら作っていきます。
体験会カリキュラムを通して、自分オリジナルのWebサイトのアイデアがふくらんだのではないでしょうか。

また、見た目だけでなく、サイトそのものの構造も自分で作って行くことになります。
詳しい内容はコーチに聞いてみてください。

iGarageでこれらをさらに深く学び、自分だけのWebサイトを作っていきましょう。

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

Node.js+Express+MySQLでWebAPIを作成してみる①

はじめに

NodeでReactを用いてフロントの作業をしていて、せっかくならバックエンドもNodeつかってJavaScriptで作ってみようということでWebApiサーバーを作ってみました。

実行環境

  • CentOS7
  • Node.js 8.17.0

Expressの動作確認

npm install expressでインストール

以下のサンプルソースを任意のフォルダに保存

index.js
var exp = require("express");
var app = exp();

app.get("/",function(req,res){
    res.send("Hello,World");
})

app.listen(3000,function(){
    console.log("成功")
})

保存したフォルダに移動し、node index.jsで実行した後http://localhost:3000/にアクセス。
image.png
サーバーが起動し、GETリクエストのレスポンスが返ってきました。

MySQLの設定

まずはMySQLにログイン。その後
create database ApiTest;
use ApiTest;
create table api_tbl(id int(5),name varchar(10));
insert into api_tbl values(2,'test');
上記のコマンドで、データベースとテーブルを作成、レコードを追加。

データの取得

作成したテーブルデータをGETリクエストで実際に取得してみます。

index.js
var exp = require("express");
var app = exp();

var mysql = require("mysql");
var connection = mysql.createConnection({
    host: "localhost",
    user: "root",
    password: "****",
    database: "ApiTest"
});



app.get("/", function (req, res) {
    connection.query('select * from api_tbl', function (error, results, fields) {
        if (error) throw error;
        res.send(results);
    });
});

//任意のポート番号
app.listen(****, function () {
    console.log("成功");
})

上記のソースを保存し、node index.jsを実行。Postmanを使用してレスポンスを確認します。
image.png

先ほど登録したデータがちゃんと返ってきたので成功です。

感想

Expressを使うことでとても簡単にWebApiが作成できて感動しました!
今回は初めてということでGETリクエストのみの実装でしたが、今後CRUDすべてのAPIを実装し、フロント側もReactなどで開発することで1つのWebアプリケーションを作ろうと考えております。

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

JSのクラスなど初心者覚書

クラスを参考に初心者がメモ書きしています。

クラス宣言

class Rectangle{
  constructor(height,width){
    this.height = height;
    this.width = width;
  }
}

クラスにアクセスする前に、そのクラスを宣言する必要がある。
関数宣言だと、Hoistingがある。

const p = new Rectangle(); // ReferenceError

クラス式

名前付きでも名前なしでもできます。名前付きクラスの名前は、クラス内のローカルとして扱われます。(ただし (インスタンスのではなく) クラスの name プロパティによって取得可能)

// 名前なし
let Rectangle = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name);
// 出力: "Rectangle"

// 名前つき
let Rectangle = class Rectangle2 {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name);
// 出力: "Rectangle2"
  • クラス本体は Strict モード で実行される

    • Strictじゃない場合サイレントエラー
  • "constructor" という名前のメソッドは、クラスに 1つしか定義できません。

    • 2回以上定義されている場合は、SyntaxError がスローされます。

参考 メソッド定義
- 簡略構文は、ECMAScript 第 5 版で導入された getter や setter 構文に似ている

car obj = {
  foo: fucnction(){
  /* コード */
  },
  bar: fucnction(){
  /* コード */
  }
};

以下のように短縮できる

car obj = {
  foo(){
  /* コード */
  },
  bar(){
  /* コード */
  }
};
  • すべてのメソッド定義がコンストラクターではない(簡略構文のみ!)ため、インスタンス化しようとすると TypeError が発生します。

プロトタイプメソッド

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // ゲッター
  get area() {
    return this.calcArea();
  }
  // メソッド
  calcArea() {
    return this.height * this.width;
  }
}
const square = new Rectangle(10, 10);
console.log(square.area); // 100

ゲッターってなんだろう?よくわからなかった、、

ゲッター

動的に計算した値を返すプロパティにアクセスを許可したほうが望ましい場合とは? (まだわからない)

  • 明示的なメソッドを呼び出すことなく内部変数に状態を反映させたい場合があります。 JavaScript では、ゲッターを使ってこれを行うことが可能

  • ゲッターを削除したい場合は、 delete を使用

ゲッターとセッターの定義

  • ゲッターはある属性の値を取得するメソッドです。セッターは属性に値を設定するメソッドです。全ての定義済みコアオブジェクトと、新しいプロパティの追加をサポートしているユーザ定義オブジェクトに対してゲッターとセッターを定義できます。ゲッターとセッターの定義にはオブジェクトリテラル構文を使用します。

わからないこと:

  • 新しいプロパティの追加をサポートしているというのがどういう状態なのか?
  • オブジェクトリテラル構文?

  • ゲッターもセッターも、

__defineGetter__

および

__defineSetter__

という 2 つの特別なメソッドを用いて、オブジェクト作成後でも、そのオブジェクトに追加することができます。両メソッドの第 1 引数にはそのゲッターやセッターの名前を文字列で指定します。第 2 引数にはゲッターやセッターとして呼び出す関数を指定します。前の例を別の方法で実装したものを以下に示します。

o.__defineGetter__("b", function() { return this.a+1; });
o.__defineSetter__("c", function(x) { this.a = x/2; });

わわ、、わからないけど下記の記事を見たらわかったような。
(今の理解: クラスの中のgetと書いてある一部分だけを取得できる。
setはgetとペアで使う?ためにセットするもの?)という意味だと解釈したが違っていたらコメントいただけるとありがたいです。

(class構文の使い方)[https://www.sejuku.net/blog/49551]
- ゲッターメソッドを作ることで、クラスの特定のプロパティを取得することができます。
例えば、「name」というプロパティをゲッターメソッドで定義すると次のようになります。

class User {

    constructor( name, age ) {
        this.name = name;
        this.age = age;
    }

    //ゲッターメソッド
    get myName() {
        return this.name;
    }
}

使い方

var taro = new User('太郎', 32);
console.log( taro.myName );
セッター
class User {
    constructor( name, age ) {
        this.name = name;
        this.age = age;
    }
    //セッターメソッド
    set myName( value ) {
        this.name = value;
    }
    // ゲッターメソッド
    get myName() {
        return this.name;
    }
}

使い方

var taro = new User('太郎', 32);
taro.myName = '花子';
console.log( taro.myName );

静的メソッド

静的メソッドは、クラスのインスタンス化なしで呼ばれ、インスタンス化されていると呼べない

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static distance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;

    return Math.hypot(dx, dy);
  }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
p1.distance; //未定義
p2.distance; //未定義

console.log(Point.distance(p1, p2)); // 7.0710678118654755
  • this に値が付けられずに静的メソッドまたはプロトタイプメソッドが呼ばれると、this の値はメソッド内で undefined になる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ECMAScript proposal updates @ 2020-07 の簡単な要約

これは何

ECMAScript proposal updates @ 2020-07 | ECMAScript Daily の簡単な要約です。次の JavaScript バージョンに加わる可能性のある新文法、新メソッド、新クラスがまとめられています。

正確・詳細な内容を知りたい方はリンク先をあたってください。そこまでするほどではないけどざっくり知りたい、という方向けです。

New Proposals

Proposal Stage
Import Conditions 2
WeakRefs cleanupSome 2
JSON Modules - Note 2
await operations 1
Array.prototype.unique() 1
ResizableArrayBuffer and GrowableSharedArrayBuffer 1

Updated Proposals

Proposal From To
Promise.any 3 4
WeakRefs 3 4
Logical Assignment Operators 3 4
Numeric separators 3 4
.item() 1 2
Record & Tuple 1 2
JSON.parse source text access 1 2

New Proposals

Import Conditions

JSON modules の import にアサーションが書けるようになります。

import json from "./foo.json" assert { type: "json" };

foo.json が実際は JSON ではなかったときに、意図しないスクリプトを実行してしまうセキュリティ問題を防ぐためのようです。ファイルタイプを表すため慣例的に拡張子が使われているものの、本来 Web は拡張子という概念のない世界だから MIME Type とのミスマッチを明示的にすべき、らしいです。

アプリコードを書くときには煩わしいので、Babel や TypeScript による自動付与、VS Code による補完がどうなるか気になります。

WeakRefs cleanupSome

WeakRefs がわかっていないのでわからない :innocent:

ガベージコレクションの話なので、よほど性能チューニングしたいときにしか使わなそう。

JSON Modules - Note

Import のアサーションと JSON modules の話は分けるべきでは?という議論。結論までちゃんと読んでないです。

await operations

Promise.all() などが書きやすくなります。

// before
await Promise.all(users.map(async x => fetchProfile(x.id)));

// after
await.all users.map(async x => fetchProfile(x.id));

all のほか race, allSettled, any も。

Array.prototype.unique()

配列の重複を除くメソッドが使えるようになります。

const data = [
  { id: 1, uid: 10000 },
  { id: 2, uid: 10000 },
  { id: 3, uid: 10001 },
];

data.unique("uid");
// [
//   { id: 2, uid: 10000 },
//   { id: 3, uid: 10001 }
// ]

data.unique(({ id, uid }) => `${id}-${uid}`);
// [
//   { id: 1, uid: 10000 },
//   { id: 2, uid: 10000 },
//   { id: 3, uid: 10001 }
// ]

現時点でも [...new Set(array)] によって配列の重複は除けますが、プリミティブ型でない値には使えません。その点を補います。

一方で、Record & Tuple によってオブジェクト的な値の同値性チェックが簡単になれば、プリミティブ型でない値にも使えるというメリットは薄まりそうです。メソッド名がわかりやすいので追加されること自体はよいことです。

ResizableArrayBuffer and GrowableSharedArrayBuffer

ArrayBuffer の派生クラスが増えます。

let rab = new ResizableArrayBuffer(1024, 1024 ** 2);
rab.resize(1024 * 2);

既存の ArrayBuffer は、領域を拡大するにはバッファーコピーが必要で非効率的だし、32-bit システムではアドレス空間のフラグメント化が生じるから、サイズ可変なこれらが欲しいとのこと。

あまり ArrayBuffer を使わないのでわかりませんが、動画のような巨大なバイナリ処理が効率化できるということでしょうか。

Updated Proposals

Promise.any

複数の promise のうちの一つが解決するまで待つ promise を作れます。

try {
  const first = await Promise.any(promises);
  // Any of the promises was fulfilled.
} catch (error) {
  // All of the promises were rejected.
}

Promise.race([a, b, c])a, b, c のどれか一つが 解決 or 拒否(失敗) すると解決 or 拒否します。Promise.any([a, b, c])a, c が拒否しても b解決 するまで待って、b が解決したら解決、全部拒否したら拒否になります。

3 社の広告(接続先すべて別)のうちどれか一つを表示できればよい、といったケースで役に立ちそう?

WeakRefs

わからない :innocent:

ガベージコレクション (GC) の話なので、よほど性能チューニングしたいときにしか使わなそう。iOS アプリなどと違って Web はページ単位のライフサイクルで、GC タイミングが多いはず(感覚です)。とはいえ Web の高機能化に伴って、明示的なメモリー管理も必要になるかもしれません。

Logical Assignment Operators

論理演算子も代入書式が使えるようになります。

opts.baz ??= "qux";

// eq
opts.baz ?? (opts.baz = "qux");

i = i + 1i += 1 と書くようなものです。デフォルトオプションを設定するのに役立ちそう。||=, &&=, ??= の 3 種類あります。

TypeScript 4.0 Beta ですでに導入されているようです (https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-beta/)

Numeric separators

数値の桁をアンダースコア _ で区切れるようになります。

101_475_938.38 === 101475938.38;

すでにこう書けます(Chrome 84 や Node 13)が、仕様として定まっていないところがあったということでしょうか。ちゃんと読んでいないです。

.item()

N 番目の要素を取得するとき、負の値を指定できるようになります。

["a", "b", "c"].item(-1) === "c";

Array, String, TypedArray にメソッドが追加されます。[] を使わないのは、それが配列インデックスだけでなくオブジェクトのプロパティを指定するときにも使われるためです。arr[-1] という書き方は現状可能ですが、arr["-1"] と書いたことになってしまいます。それらの区別をつけるため、明示的にメソッドを追加するようです。

Record & Tuple

イミュータブルな構造体として Record と Tuple が使えるようになります。

// Record
const doc = #{
  id: 123,
  title: "foo",
};

doc.title === "foo";

// Tuple
const m = #["x", "y", "z"];

m[1] === "y";

Record はオブジェクトのような構造体、Tuple は配列のような構造体です。TypeScript はすでに as const キーワードによってイミュータブルな構造体を表せますが、JS としても同様のことが可能になります。

一方 TypeScript でも実現できていない点として、同値性の評価があります。イミュータブルなので、中の構成が一緒なら同値であると評価するようです。

// non-Record
assert({ x: 1, y: 2 } !== { x: 1, y: 2 });

// Record
assert(#{ x: 1, y: 2 } === #{ x: 1, y: 2 });

プリミティブ型のようにイミュータブルかつ比較も簡単な値に、オブジェクトと同じ情報量を持たせられるのは強力です。

JSON.parse source text access

JSON.parse のパーサーロジックを拡張できるようになります。

ちゃんと読んでいないです。

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

初心者のためのオブジェクト指向を読む

初心者のためのオブジェクト指向 JavaScript

function Person(name) {
  this.name = name;
  this.greeting = function() {
    console.log('Hi! I\'m ' + this.name + '.');
  };
}

コンストラクタ呼び出し

  • コンストラクタは、ES5 以前は、オブジェクトを作成し、初期化するための関数オブジェクトのこと
  • 定義したPersonコンストラクタを使ってPersonオブジェクトを作るには、 newキーワードを付けてコンストラクタを呼びだすコンストラクタES5以前
  • コンストラクタの中で this で参照していたのは、新しく作成されたオブジェクトそのもの
  • newを付けないで呼び出せば、当然ながらオブジェクトは作成されない
let person1 = new Person('Bob');
person1.name;
person1.greeting();

innerHTML

function Person(name){

      this.name = name;
      this.greeting  = function(){
        'Hi! I\'m' + this.name + '.';
      };
    }

    let person1 = new Person('AIAI');
    person1.name;
    person1.greeting();
    console.log(person1.name);//AIAI

    var textPage = document.getElementById('root').textContent;
    console.log(textPage);//aaa

    document.getElementById('root').textContent = person1.name;

     //intextPage = "アイウエオ";
    console.log(textPage);//aaa

[オブジェクトにプロパティを追加]

var user = {
  this.name = {
          'first': first,
          'last' : last
        };
};

user.country = "Japan";
user["prefecture"] = "Shizuoka";

console.log("user", user);

サンプルのコードを少しだけ改造

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Object-oriented JavaScript class further exercises</title>
  </head>

  <body>

    <div id="personTxt"></div>

  </body>

    <script>
      function Person(first, last, age, gender, interests) {
        this.name = {
          'first': first,
          'last' : last
        };
        this.age = age;
        this.gender = gender;
        this.interests = interests;
        this.bio = function() {
          // First define a string, and make it equal to the part of
          // the bio that we know will always be the same.
          var string = this.name.first + ' ' + this.name.last + ' is ' + this.age + ' years old. ';
          // define a variable that will contain the pronoun part of
          // the second sentence
          var pronoun;

          // check what the value of gender is, and set pronoun
          // to an appropriate value in each case
          if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
            pronoun = 'He likes ';
          } else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
            pronoun = 'She likes ';
          } else {
            pronoun = 'They like ';
          }

          // add the pronoun string on to the end of the main string
          string += pronoun;

          // use another conditional to structure the last part of the
          // second sentence depending on whether the number of interests
          // is 1, 2, or 3
          if(this.interests.length === 1) {
            string += this.interests[0] + '.';
          } else if(this.interests.length === 2) {
            string += this.interests[0] + ' and ' + this.interests[1] + '.';
          } else {
            // if there are more than 2 interests, we loop through them
            // all, adding each one to the main string followed by a comma,
            // except for the last one, which needs an and & a full stop
            for(var i = 0; i < this.interests.length; i++) {
              if(i === this.interests.length - 1) {
                string += 'and ' + this.interests[i] + '.';
              } else {
                string += this.interests[i] + ', ';
              }
            }
          }

          // finally, with the string built, we alert() it
          alert(string);
        };
        this.greeting = function() {
          return 'Hi! I\'m ' + this.name.first + '.';
        };
      };

      let person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing','music', 'skiing', 'kickboxing']);

      person1.bio();

      let person2 = Object.create(person1);
      person2.name;
      //person2.greeting();

      document.getElementById('personTxt').textContent = person2.greeting();

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

JSのオブジェクト指向 初心者覚書

初心者のためのオブジェクト指向 JavaScript

function Person(name) {
  this.name = name;
  this.greeting = function() {
    console.log('Hi! I\'m ' + this.name + '.');
  };
}

コンストラクタ呼び出し

  • コンストラクタは、ES5 以前は、オブジェクトを作成し、初期化するための関数オブジェクトのこと
  • 定義したPersonコンストラクタを使ってPersonオブジェクトを作るには、 newキーワードを付けてコンストラクタを呼びだすコンストラクタES5以前
  • コンストラクタの中で this で参照していたのは、新しく作成されたオブジェクトそのもの
  • newを付けないで呼び出せば、当然ながらオブジェクトは作成されない
let person1 = new Person('Bob');
person1.name;
person1.greeting();

innerHTML

function Person(name){

      this.name = name;
      this.greeting  = function(){
        'Hi! I\'m' + this.name + '.';
      };
    }

    let person1 = new Person('AIAI');
    person1.name;
    person1.greeting();
    console.log(person1.name);//AIAI

    var textPage = document.getElementById('root').textContent;
    console.log(textPage);//aaa

    document.getElementById('root').textContent = person1.name;

     //intextPage = "アイウエオ";
    console.log(textPage);//aaa

[オブジェクトにプロパティを追加]

var user = {
  this.name = {
          'first': first,
          'last' : last
        };
};

user.country = "Japan";
user["prefecture"] = "Shizuoka";

console.log("user", user);

サンプルのコードを少しだけ改造

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Object-oriented JavaScript class further exercises</title>
  </head>

  <body>

    <div id="personTxt"></div>

  </body>

    <script>
      function Person(first, last, age, gender, interests) {
        this.name = {
          'first': first,
          'last' : last
        };
        this.age = age;
        this.gender = gender;
        this.interests = interests;
        this.bio = function() {
          // First define a string, and make it equal to the part of
          // the bio that we know will always be the same.
          var string = this.name.first + ' ' + this.name.last + ' is ' + this.age + ' years old. ';
          // define a variable that will contain the pronoun part of
          // the second sentence
          var pronoun;

          // check what the value of gender is, and set pronoun
          // to an appropriate value in each case
          if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
            pronoun = 'He likes ';
          } else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
            pronoun = 'She likes ';
          } else {
            pronoun = 'They like ';
          }

          // add the pronoun string on to the end of the main string
          string += pronoun;

          // use another conditional to structure the last part of the
          // second sentence depending on whether the number of interests
          // is 1, 2, or 3
          if(this.interests.length === 1) {
            string += this.interests[0] + '.';
          } else if(this.interests.length === 2) {
            string += this.interests[0] + ' and ' + this.interests[1] + '.';
          } else {
            // if there are more than 2 interests, we loop through them
            // all, adding each one to the main string followed by a comma,
            // except for the last one, which needs an and & a full stop
            for(var i = 0; i < this.interests.length; i++) {
              if(i === this.interests.length - 1) {
                string += 'and ' + this.interests[i] + '.';
              } else {
                string += this.interests[i] + ', ';
              }
            }
          }

          // finally, with the string built, we alert() it
          alert(string);
        };
        this.greeting = function() {
          return 'Hi! I\'m ' + this.name.first + '.';
        };
      };

      let person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing','music', 'skiing', 'kickboxing']);

      person1.bio();

      let person2 = Object.create(person1);
      person2.name;
      //person2.greeting();

      document.getElementById('personTxt').textContent = person2.greeting();

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

React公式チュートリアルのクラスコンポーネントを関数コンポーネントに書き替える

なかなか手をつけられなかったReact公式サイトのチュートリアルを、一通りやってみた。
ステートフックuseState()を使って、クラスコンポーネントから関数コンポーネントに書き換えてみたのでメモに残す。

公式サイトより:チュートリアルReact の導入
三目並べ完成形

hookを呼び出す際に気をつけること => フックを呼び出すのはトップレベルのみ

フックをループや条件分岐、あるいはネストされた関数内で呼び出してはいけません。代わりに、あなたの React の関数のトップレベルでのみ呼び出してください。これを守ることで、コンポーネントがレンダーされる際に毎回同じ順番で呼び出されるということが保証されます。これが、複数回 useState や useEffect が呼び出された場合でも React がフックの状態を正しく保持するための仕組みです。

参照: React公式サイト フックのルール

Square(三目並べの正方形のマス目)

Square.jsx
import React from "react";

// function Square(props) {
//   return (
//     <button className="square" onClick={props.onClick}>
//       {props.value}
//     </button>
//   );
// }

// Board(盤面) コンポーネントから { onClick, value } = props を受け取っている
const Square = ({ onClick, value }) => {
  return (
    <button className="square" onClick={onClick}>
      {value}
    </button>
  );
};

export default Square;

Board(盤面)

Board.jsx
import React from "react";
// Square(三目並べの正方形のマス目)コンポーネントをimport
import Square from "./Square";

// class Board extends React.Component {
//   renderSquare(i) {
//     return (
//       <Square
//         value={this.props.squares[i]}
//         onClick={() => this.props.onClick(i)}
//       />
//     );
//   }

// Game コンポーネントから { squares, onClick } = props を受け取っている
const Board = ({ squares, onClick }) => {
  const renderSquare = i => {
    return (
      <Square
        value={squares[i]}
        onClick={() => {
          onClick(i);
        }}
      />
    );
  };

  // render() {
  return (
    <>
      <div className="board-row">
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div className="board-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="board-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
    </>
  );
  // }
};

export default Board;

Game

Game(親)コンポーネントは props を使うことで子に情報を渡すことができた。こうすることで、子コンポーネントとの間で常に同期されるようになる。

Game.jsx
// useStateをimport
import React, { useState } from "react";
// Board(盤面)コンポーネントをimport
import Board from "./Board";
import "./style.css";

// class Game extends React.Component {
//   constructor(props) {
//     super(props);
const Game = () => {
  //  クラスのconstructor内で `this.state` の初期化をやめて `useState` フックを使う
  //  this.state = {
  //    history: [
  //      {
  //        squares: Array(9).fill(null)
  //      }
  //   ],
  // useState()を使って、関数コンポーネントに状態を持たせる
  // const [state変数, set関数] = useState(初期値)
  const [history, setHistory] = useState([
    {
      squares: Array(9).fill(null)
    }
  ]);
  // stepNumber: 0,
  const [stepNumber, setStepNumber] = useState(0);
  // xIsNext: true
  const [xIsNext, setXIsNext] = useState(true);
  //     };
  //   }

  // handleClick(i) {
  const handleClick = i => {
    // const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const historyCurrent = history.slice(0, stepNumber + 1);
    // const current = history[history.length - 1];
    const current = historyCurrent[historyCurrent.length - 1];
    // const squares = current.squares.slice();
    const squares = [...current.squares];

    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    // squares[i] = this.state.xIsNext ? "X" : "O";
    squares[i] = xIsNext ? "X" : "O";
    //this.setState({
    //  history: history.concat([
    //    {
    //      squares: squares
    //    }
    //  ]),

    setHistory([...historyCurrent, { squares }]);
    // stepNumber: history.length,
    setStepNumber(historyCurrent.length);
    // xIsNext: !this.state.xIsNext
    setXIsNext(!xIsNext);
  };
  //   });
  // }

  // jumpTo(step) {
  //   this.setState({
  //     stepNumber: step,
  //     xIsNext: (step % 2) === 0
  //   });
  // }

  const jumpTo = step => {
    setStepNumber(step);
    setXIsNext(step % 2 === 0);
  };
  // render() {
  // const history = this.state.history;
  // const current = history[this.state.stepNumber];
  const current = history[stepNumber];
  const winner = calculateWinner(current.squares);

  const moves = history.map((step, move) => {
    const desc = move ? `Go to move # ${move}` : `Go to game start`;
    return (
      <li key={move}>
        {/* <button onClick={() => this.jumpTo(move)}>{desc}</button> */}
        <button onClick={() => jumpTo(move)}>{desc}</button>
      </li>
    );
  });

  let status;
  if (winner) {
    status = `Winner : ${winner}`;
  } else {
    // status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    status = `Next Player : ${xIsNext ? "X" : "O"}`;
  }
  return (
    <div className="game">
      <div className="game-board">
        {/* <Board squares={current.squares} onClick={i => this.handleClick(i)} /> */}
        <Board squares={current.squares} onClick={i => handleClick(i)} />
      </div>
      <div className="game-info">
        <div>{status}</div>
        <ol>{moves}</ol>
      </div>
    </div>
  );
  // }
};

const calculateWinner = squares => {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
};

export default Game;

参考: React公式サイト

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

【JavaScript】【初心者】コールバック関数について

コールバック関数とは

コールバック関数について備忘録を残します。
コールバック関数とは関数の引数として渡される関数のこと。
直訳の【呼び返す関数】という意味で好きなタイミングで呼び返すことができる関数です。

下のコードでは、hogeがbaseのコールバック関数として渡されています。

sample.js
const hoge = () => {
    console.log('hogeが呼び出されました。');
};

const base = (callback) => {
    console.log('baseが呼び出されました。');
    callback();
};

base(hoge); //base(hoge());ではないことに注意!

実行結果
baseが呼び出されました。
hogeが呼び出されました。

注意

上の例で、base(hoge());としてしまうと。base(undefined);となり、エラーが発生します。
hogeはreturnのない関数なので返り値はundefinedです。よって実行前の関数自体を引数として渡すように注意して下さい。

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

さくっとVue3をプラウザで試したい時に使えるTips

Vue3がRCになっていよいよリリース間近ですね。
環境構築不要でさくっとVue3の動き確認したいなーって時に使えるTipsを紹介します。

script要素を追加し、CDNからVue3のスクリプトを読み込む

適当なサイトを開き、以下コードをプラウザのdevtoolsのconsole上で実行してください。
srcにvue-nextのCDNのアドレスを設定したscript要素を作成し、body直下に追加しています。

document.body.appendChild(
  (() => {
    const s = document.createElement("script");
    s.src = "https://unpkg.com/vue@next";
    return s;
  })()
);

スクリーンショット 2020-07-25 5.34.19.png

これでvue-nextのCDNが読み込めたので、console上でVueが使えます。
あとはやりたい放題使えばOKです。

スクリーンショット 2020-07-25 8.32.41.png

動作確認コードの一例

参考までに、自分で動かしてみたコードを紹介します。

Vueコンポーネントの動作確認

DOMを追加して、そこにVue.createApp()でVueをマウントしコンポーネントを表示しています。templatesetup()内を調整すればほとんどの動作を確認できます。

// マウント用のDOMの追加
document.body.insertBefore((() => {
  const d = document.createElement("div");
  d.id = 'vue-app';
  return d;
})(), document.body.firstChild);

// DOM要素へのVueコンポーネントのマウント
const app = Vue.createApp({
  template: `<my-component />`
})
app.component('my-component', {
  template: `
    <h1>Hello Vue3</h1>
    <input type="text" v-model="message"/>
    <div>{{ upperCase }}</div>
  `,
  setup() {
    const message = Vue.ref('')
    const upperCase = Vue.computed(() => message.value.toUpperCase())
    return {
      message,
      upperCase
    }
  }
})
app.mount('#vue-app')

yahoo! JAPANで実行してみた例
Jul-25-2020 08-27-10.gif

リアクティブの動作確認

Vue3のComposition APIのリアクティブAPIの動作確認です。refでもreactiveでも同様にリアクティブに処理を再実行してますね。

const reactiveObj = Vue.reactive({ count: 1 });
const refObj = Vue.ref(1);
const sumVal = Vue.computed(() => reactiveObj.count + refObj.value);

console.log(sumVal.value); // 2

refObj.value = 3;
console.log(sumVal.value); // 4

reactiveObj.count = 10;
console.log(sumVal.value); // 14

スクリーンショット 2020-07-25 5.43.55.png

リアクティブの消失の動作確認

Vue Composition APIを使う上で注意が必要なリアクティブの消失について動作確認です。リアクティブな値から分割代入でプリミティブな値を取得すると、リアクティブを消失するのが良くわかると思います。

const reactiveObj = Vue.reactive({
  num: 1,
  str: "hoge",
  arr: [1, 2, 3],
  obj: { foo: "foo", bar: "bar" },
});

const { num, str, arr, obj } = reactiveObj;

console.log(Vue.isReactive(num)); // false
console.log(Vue.isReactive(str)); // false
console.log(Vue.isReactive(arr)); // true
console.log(Vue.isReactive(obj)); // true

スクリーンショット 2020-07-25 6.33.28.png

終わりに

とても簡単ですが「さくっとVue3をプラウザで試したいときに使えるTips」でした。
ちょっと動きを確認したいときとかに地味に便利なので使ってみてください〜。

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

さくっとVue3をWebブラウザで試したい時に使えるTips

Vue3がRCになっていよいよリリース間近ですね。
環境構築不要でさくっとVue3の動き確認したいなーって時に使えるTipsを紹介します。

script要素を追加し、CDNからVue3のスクリプトを読み込む

適当なサイトを開き、以下コードをWebブラウザのdevtoolsのconsole上で実行してください。
srcにvue-nextのCDNのアドレスを設定したscript要素を作成し、body直下に追加しています。

document.body.appendChild(
  (() => {
    const s = document.createElement("script");
    s.src = "https://unpkg.com/vue@next";
    return s;
  })()
);

スクリーンショット 2020-07-25 5.34.19.png

これでvue-nextのCDNが読み込めたので、console上でVueが使えます。
あとはやりたい放題使えばOKです。

スクリーンショット 2020-07-25 8.32.41.png

動作確認コードの一例

参考までに、自分で動かしてみたコードを紹介します。

Vueコンポーネントの動作確認

DOMを追加して、そこにVue.createApp()でVueをマウントしコンポーネントを表示しています。templatesetup()内を調整すればほとんどの動作を確認できます。

// マウント用のDOMの追加
document.body.insertBefore((() => {
  const d = document.createElement("div");
  d.id = 'vue-app';
  return d;
})(), document.body.firstChild);

// DOM要素へのVueコンポーネントのマウント
const app = Vue.createApp({
  template: `<my-component />`
})
app.component('my-component', {
  template: `
    <h1>Hello Vue3</h1>
    <input type="text" v-model="message"/>
    <div>{{ upperCase }}</div>
  `,
  setup() {
    const message = Vue.ref('')
    const upperCase = Vue.computed(() => message.value.toUpperCase())
    return {
      message,
      upperCase
    }
  }
})
app.mount('#vue-app')

yahoo! JAPANで実行してみた例
Jul-25-2020 08-27-10.gif

リアクティブの動作確認

Vue3のComposition APIのリアクティブAPIの動作確認です。refでもreactiveでも同様にリアクティブに処理を再実行してますね。

const reactiveObj = Vue.reactive({ count: 1 });
const refObj = Vue.ref(1);
const sumVal = Vue.computed(() => reactiveObj.count + refObj.value);

console.log(sumVal.value); // 2

refObj.value = 3;
console.log(sumVal.value); // 4

reactiveObj.count = 10;
console.log(sumVal.value); // 14

スクリーンショット 2020-07-25 5.43.55.png

リアクティブの消失の動作確認

Vue Composition APIを使う上で注意が必要なリアクティブの消失について動作確認です。リアクティブな値から分割代入でプリミティブな値を取得すると、リアクティブを消失するのが良くわかると思います。

const reactiveObj = Vue.reactive({
  num: 1,
  str: "hoge",
  arr: [1, 2, 3],
  obj: { foo: "foo", bar: "bar" },
});

const { num, str, arr, obj } = reactiveObj;

console.log(Vue.isReactive(num)); // false
console.log(Vue.isReactive(str)); // false
console.log(Vue.isReactive(arr)); // true
console.log(Vue.isReactive(obj)); // true

スクリーンショット 2020-07-25 6.33.28.png

終わりに

とても簡単ですが「さくっとVue3をWebブラウザで試したいときに使えるTips」でした。
ちょっと動きを確認したいときとかに地味に便利なので使ってみてください〜。

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

[Javascript] 初心者が分かったら役に立つ関数宣言式 VS 関数式

関数宣言式 - Function Declarations

一般的にプログラミング言語にて関数宣言と似たような形式です。

function 関数名() {
  ロジック・・・
}
// 例
function funcDeclarations() {
  return 'A function declaration';
}
funcDeclarations(); // 'A function declaration'

関数式 - Function Expressions

柔軟なJavascript言語の特徴を活用した宣言方式です。

var 関数名 = function () {
  ロジック・・・
};
// 例
var funcExpression = function () {
    return 'A function expression';
}
funcExpression(); // 'A function expression'

関数宣言式(Function Declarations)と関数式(Function Expressions)違い

関数宣言式はホイスティングを影響をますが、関数式はホイスティングの影響を受けていないです。

関数宣言式は、コードを実装した位置と関係なく、JavaScriptの特徴であるホイスティングによってブラウザがJavaScriptを解析する際に一番上に引き上げられる。

// 実行前
logMessage();
sumNumbers();

function logMessage() {
  return 'worked';
}

var sumNumbers = function () {
  return 10 + 20;
};

ホイスティングにより、JavaScript解析器はコードを下記のように認識する。

// 実行後
function logMessage() {
  return 'worked';
}

var sumNumbers;

logMessage(); // 'worked'
sumNumbers(); // Uncaught TypeError: sumNumbers is not a function

sumNumbers = function () {
  return 10 + 20;
};

上コードの結果

Uncaught TypeError: sumNumbers is not a function

関数式のsumNumbersにてvarもホイスティングに適用され、位置が上に引き上げられる。

var sumNumbers;

logMessage();
sumNumbers();

しかし、実際sumNumbersに割り当てられる関数ロジックは呼び出した以降に宣言されるので、sumNumbersは関数で認識されずに、変数として認識されます。

ホイスティングが正しくわからなくても、関数と変数をできるだけコードの上端部で宣言すれば、ホイスティングによるスコープのねじれ現象は防止できます。

関数式の長所

「関数式がホイスティングに影響されない」という特徴以外にも、関数宣言式より有効に使われる場合は以下の通りである。

・クロージャとして使用
・コールバックで使用(他の関数の印字に移行できる)

関数式でクロージャを生成する

クロージャは、関数を実行する前に当該関数に変数を渡したいときに使用されます。
理解を深めるため、以下の例題を見てみましょう。

function tabsHandler(index) {
    return function tabClickEvent(event) {
        // 外部関数tabsHandler()のindex印字をここでアクセス出来ます。
        console.log(index); 
    };
}

var tabs = document.querySelectorAll('.tab');
var i;

for (i = 0; i < tabs.length; i += 1) {
    tabs[i].onclick = tabsHandler(i);
}

上記の例は、すべての.tab 要素にクリックイベントを追加する例です。
注目すべき点は、クロージャを用いてtabClickEvent()で外関数tabsHandler()の印字値indexにアクセスした点であります。

function tabsHandler(index) {
    return function tabClickEvent(event) {
        console.log(index);
    };
}

forループに合わせ実行が終わった後、使用者がtabをクリックした時、tabClickEvent()が実行されます。
もしクロージャを使わなかったらすべてのtabのindex値がforループの最後の値であるtabs.lengthの通りであります。

for (i = 0; i < tabs.length; i += 1) {
    tabs[i].onclick = tabsHandler(i);
}

クロージャを使わなかった場合の例は下記をご参照ください。

var tabs = document.querySelectorAll('.tab');
var i;

for (i = 0; i < tabs.length; i += 1) {
    tabs[i].onclick = function (event) {
      console.log(i); 
    };
}

上記のソースはタブが3つであるとしたとき、どのタブをクリックしてもiはforループの最終値である3が示される。

問題点をより把握しやすくforループ中のfunction()を外に持ち出して宣言してみると

var tabs = document.querySelectorAll('.tab');
var i;
var logIndex = function (event) {
  console.log(i); // 3
};

for (i = 0; i < tabs.length; i += 1) {
    tabs[i].onclick = logIndex;
}

logIndexが実行される時点は、すでにforループの実行がすべて終わった時点です。
したがって、どのタブを押してもfor文の最終値である3 が表示されます。

この問題点を解決するためにクロージャを適用すると

function tabsHandler(index) {
    return function tabClickEvent(event) {
        console.log(index);
    };
}

var tabs = document.querySelectorAll('.tab');
var i;

for (i = 0; i < tabs.length; i += 1) {
    tabs[i].onclick = tabsHandler(i);
}

forループが実行されるとき、各i値をtabsHandler() に渡し、クロージャであるtabClickEvent()からtabsHandler()の印字値indexにアクセスできるようになります。

関数式を他の関数の印字で渡す

関数式一般的に臨時変数に格納するために使用します。

// doSthという臨時変数を使用する。
var doSth = function () {
  // ...
};

関数式を臨時変数に格納してなくても、下記の様にコールバック関数として使用出来ます。

$(document).ready(function () {
  console.log('An anonymous function'); // 'An anonymous function'
});

jQueryを使用してよく見られる文法で上記と下記の結果は同じです。

var logMessage = function () {
  console.log('An anonymous function');
};

$(document).ready(logMessage); // 'An anonymous function'

Javascriptで基本的に提供するforEach()を使用する際にも、コールバックを使用出来ます。

var arr = ["a", "b", "c"];
arr.forEach(function (element) {
  console.log(element); // a b c
});

コールバック関数を簡単に説明すると、関数の印字に渡す関数をコールバック関数と言います。

結論

関数式を関数宣言式と比べるとメリットは多いですが、結局はこのような違いを認知した状態で、一貫したコーディングコンベンションでコードを作成することが重要だと思います。
AirBnbのJS Styleガイドでも、関数宣言式よりは関数式を目指しています。
それでも、自分がコーディングしやすい方式で 実現するのがいいんじゃないでしょうか。

備考

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

JS 配列のN個からN個までを取り出す

Lodashのslice関数を作成してみた

const slice = (array = [], start = 0, end = array.length) => {
  const newArray = []
  for (let i = start; i < end; i++) {
    newArray.push(array[i])
  }
  return newArray
}

console.log(slice([1, 2, 3, 4, 5], 1, 4))
// => [ 2, 3, 4 ]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JS 配列からN個返す

LodashのtakeRight関数を作成してみた

const take = (array = [], take = 1) => {
  if (take === 0) {
    return []
  }

  if (array.length < take) {
    return [...array]
  }

  const newArray = []
  for (let i = 0; i < take; i++) {
    newArray.push(array[i])
  }

  return newArray
}

console.log(take([1, 2, 3], 0))
=> []

console.log(take([1, 2, 3], 2))
=> [1, 2]

console.log(take([1, 2, 3], 5))
=> [1, 2, 3]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

three.jsで正四面体を展開する

TL;DR

three.jsを使った立体図形の表示に関しては説明しているサイトが多くありましたが、その図形の展開方法についてはありませんでしたので一例としてまとめました。

HTML、JavaScript、three.jsについてある程度の知識を持っている方を対象としています。

three.jsを使って正四面体を展開するときのポイントは次の通りです。

  • CylinderGeometry(三角柱)を4枚合わせる
  • 各面の中心点(メッシュのposition)の「位置」を計算する
  • 底面に対する各面の回転角を計算する
  • 回転にQuaternionを用いる(回転軸を定義する)
  • 計算には「ピタゴラスの定理」と「三角関数」を用いる
  • 角度はラジアンで計算する

作成したサンプルは次のデモサイトをご確認ください。

デモサイト
※別ウインドウで開きます

開発環境

  • OS : Microsoft Windows 10 Home
  • プロセッサ : Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz、2904 Mhz、2 個のコア、4 個のロジカル プロセッサ
  • メモリ : 16GB
  • プログラミング言語 : JavaScript ※const、letは使用しますがIEを意識してアロー関数は使用していません
  • ブラウザ: Google Chrome

※デモサイトはAWSでサーバレス構成で作成していますため詳細は割愛します

説明の流れ

本記事では「three.jsで正四面体を展開する」という目的に必要な知識を次の順番に説明します。少々長い内容となりますが、ご容赦いただければと思います。

  • 必要な数学知識
    • 3D描画においては表示する座標を計算することが必要になります。小中高レベルの数学知識が役に立ちます。ここではその内容を説明します。
  • ベースとなるHTMLコード
    • JavaScriptの結果を画面に表示させるのに必要なHTMLについて説明します。
  • 正四面体を表示するJavaScriptコード
    • 展開をする前に表示方法を把握しているとコードに対する理解が深まりますので、先に説明します。
  • 正四面体を展開するJavaScriptコード
    • メインのコードです。クロージャを多用していますので読みづらい場合は、先にクロージャについて調べておくと読みやすくなるかもしれません。

1.必要な数学知識

1-1.円周の長さ

小学算数で一般に習う次の公式を用います。

「直径1の円の円周の長さを円周率という」

ですが円の問題を扱うときは直径ではなく、
その半分の「半径」を用いた方が分かりやすくなるため円周の定義としては次が広く知られています。

「半径1の円の円周の長さは円周率の2倍」

円周率は$\pi$(3.141592...)と表します。
JavaScriptではMath.PIで表します。

ラジアン表記の角度ではこの円周率が半円の角度(180°)を表します。
半円の角度はまっ平(水平)を表します。

その半分の$\pi$/2、JavaScriptではMath.PI/2が直角(90°)を表します。

1-2.三角形の内角の和

小学算数で一般に習う次の公式を用います。

「三角形の内角θ1、θ2、θ3の和は180°」

ラジアンで表すと次の通りです。

「三角形の内角θ1、θ2、θ3の和は$\pi$」

JavaScriptで表すと次の通りです。

Math.PI = θ1 + θ2 + θ3;

このあたりの知識を応用することで、直角三角形の直角以外の角度の和は90°ということが分かります。

例えばθ390°ならば、

Math.PI = θ1 + θ2 + Math.PI/2;
Math.PI/2 = θ1 + θ2;

1-3.平方根

中学数学で一般に習う次の定義を用います。

「2乗するとその値になる値のことを平方根という」

$$b = a^{2}$$

が成り立つとき、aをbの平方根といいます。

JavaScriptで表すと次の通りです。

b = Math.pow(a, 2);
に対して
a = Math.sqrt(b);

1-4.ピタゴラスの定理(三平方の定理)

中学数学で一般に習う次の公式を用います。

「辺a、b、cの "直角三角形" において、最も長い辺をcとした場合に次の式が成り立つ」

$$c^{2} = a^{2} + b^{2}$$

JavaScriptで表すと次の通りです。

Math.pow(c, 2) = Math.pow(a, 2) + Math.pow(b, 2);

1-5.三角関数

高校数学で一般に習う次の三角関数の定義を用います。

「辺a、b、cの "直角三角形" において、最も長い辺をc、垂直な辺をa、底辺をb、bとcの間の角度をθとした場合に次の式が成り立つ」

$$sinθ = a \div c$$

$$cosθ = b \div c$$


※矢印に沿って「分母」「分子」の順で考えると分かりやすいです。

JavaScriptで表すと次の通りです。

Math.sin(θ) = a / c;
Math.cos(θ) = b / c;

sin、cosともに「割合」を表すと考えると分かりやすいです。
値の範囲は「-1~1」になります。

主な値は次の通りです。

角度
(度数)
角度
(ラジアン)
sin cos
$0$ $0$ $1$
90° $\pi\times1/2$ $1$ $0$
180° $\pi$ $0$ $-1$
270° $\pi\times3/2$ $-1$ $0$
360° $\pi\times2$ $0$ $1$

この値を利用することで、「一つの辺の長さとそのどちらかの角度」が分かる直角三角形であれば、他の辺の長さを計算することができます。

斜辺に対して高さは「sin」をかけ、
底辺は「cos」をかけると求めることができます。

三角関数は他にもありますが、ここでは「sin」「cos」だけを知っていれば大丈夫です。

1-6.逆三角関数

高校数学で一般に習う逆三角関数の定義を用います。

「$sinθ = a \div c$が成り立つ場合、$θ = arcsin(a \div c)$と定義する」
「$cosθ = b \div c$が成り立つ場合、$θ = arccos(b \div c)$と定義する」

逆関数自体は非常に難しい概念ですが、ポイントは「2辺の角度を表す」ことです。
三角関数で扱ったsin、cosは「2辺の割合」であることに対して逆三角関数は「角度」です。

そのため角度の代わりに扱うことができます。
二辺の長さが決まっていてその角度がわからないときに用いるのがよいです。

JavaScriptで表すと次の通りです。

θ = Math.asin(a/c);
θ = Math.acos(b/c);

./images/01-06.png

逆三角関数は他にもありますが、ここでは「asin」「acos」だけを知っていれば大丈夫です。

1-7.ベクトル

高校数学で一般に習うベクトルの概念を用います。

ベクトルはスカラーと呼ばれる「値」と「方向」を持つもので「矢印」をイメージすると分かりやすくなります。

矢印で表すと矢印の「長さ」がスカラーで矢印の「向き」が方向を表します。
向きは「座標」で表します。

./images/01-07.png

2.ベースとなるHTMLコード

画面を定義するHTMLコードを説明します。

2-1.全体

コード全体を見る場合はここをクリックしてください
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>three.js 正四面体サンプル</title>
  <script type="text/javascript" src="lib/three.min.js"></script>
  <script type="text/javascript" src="lib/OrbitControls.js"></script>
  <script type="text/javascript">
window.onload = function()
{
  'use strict';

  // TODO: ここに3D描画をするコードを書きます
};
  </script>
</head>
<body>
  <div class="rp4Wrap">
    <h2>正四面体</h2>
    <canvas id="rp4"></canvas>
  </div>
  <div class="rp4OpenWrap">
    <h2>正四面体(展開)</h2>
    <div style="margin-bottom:5px;">
      <button id="btnOpen">展開</button>
      <button id="btnReset">リセット</button>
    </div>
    <canvas id="rp4Open"></canvas>
  </div>
</body>
</html>

2-2.個別説明

(1)必要ファイル

<script type="text/javascript" src="lib/three.min.js"></script>
<script type="text/javascript" src="lib/OrbitControls.js"></script>

実行には次のファイルが必要です。

three.min.js

three.jsのライブラリファイル。

公式サイトよりダウンロードできます。

OrbitControls.js

カメラコントロールライブラリ。

メインのライブラリには含まれていませんが、これを利用するとマウスでのカメラ制御が非常に簡単になります。
公式サイトからダウンロードしたファイルの次のパスにあります。

three.js-master/examples/js/controls

(2)実行トリガー

  <script type="text/javascript">
window.onload = function()
{
  'use strict';

  // TODO: ここに3D描画をするコードを書きます
};
  </script>

window.onloadイベントで読み込みが終わった後に3D描画処理を実行します。
念のためuse strictでstrictモードにし変数の宣言忘れなどに気づけるようにします。

(3)body部

<body>
  <div class="rp4Wrap">
    <h2>正四面体</h2>
    <canvas id="rp4"></canvas>
  </div>
  <div class="rp4OpenWrap">
    <h2>正四面体(展開)</h2>
    <div style="margin-bottom:5px;">
      <button id="btnOpen">展開</button>
      <button id="btnReset">リセット</button>
    </div>
    <canvas id="rp4Open"></canvas>
  </div>
</body>

正四面体の表示用と展開用の描画箇所を分けます。
描画はcanvasに行うためそれぞれにid属性を付けておきます。

展開の方は「展開」を実行するボタンと「リセット」をするボタンを用意しておきます。
識別できるようにこちらにもid属性を付けておきます。

3.正四面体を表示するJavaScriptコード

JavaScriptを使って正四面体を表示します。
Viewクラス内に2つのメソッドに分けて定義します。

  • renderメソッド
    • 画面に3D図形を描画する処理をまとめたメソッドです。 汎用的なコードのため使い回せる部分が多いものになります。
  • setSceneRP4メソッド
    • 正四面体を表示する処理(シーンに設定する処理)のメソッドです。 正四面体が関わる処理がまとめられています。

3-1.全体

window.onloadイベントの中に次のコードを入力します。

コード全体を見る場合はここをクリックしてください
  /** defines */
  const CANVAS_WIDTH = 960;
  const CANVAS_HEIGHT = 540;
  const CANVAS_TYPE_RP4 = 1;


  /** classes */
  const View = function() {};
  View.prototype =
  {
    render: function(id, canvasType) {
      const canvas = document.getElementById(id);
      const renderer = new THREE.WebGLRenderer({
        canvas: canvas,
        antialias: true
      });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(CANVAS_WIDTH, CANVAS_HEIGHT);

      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0xffffff);

      const camera = new THREE.PerspectiveCamera(60, CANVAS_WIDTH / CANVAS_HEIGHT);
      camera.position.set(2, 0, 9);

      const controls = new THREE.OrbitControls(camera, canvas);
      controls.enableDamping = true;
      controls.dampingFactor = 0.2;

      switch(canvasType) {
        case CANVAS_TYPE_RP4:
          this.setSceneRP4(scene);
        break;

        default:
          // pass
      }

      /** 描画更新処理 */
      const update = function() {
        controls.update();
        renderer.render(scene, camera);
        requestAnimationFrame(update);
      };

      update();
    },

    setSceneRP4: function(scene) {
      const createMesh = function() {
        const geometry = new THREE.ConeGeometry(3, 3, 3);
        const material = new THREE.MeshBasicMaterial({
          color: 0xccffcc,
          transparent: true,
          opacity: 0.8
        });
        const mesh = new THREE.Mesh(geometry, material);

        return mesh;
      };
      const createLine = function() {
        const geometry = new THREE.ConeBufferGeometry(3, 3, 3);
        const edges = new THREE.EdgesGeometry(geometry);
        const material = new THREE.LineBasicMaterial({ color: 0x000000 });
        const line = new THREE.LineSegments(edges, material);

        return line;
      };

      scene.add(createMesh());
      scene.add(createLine());
    },
  };


  /** objects */
  const view = new View();


  /** run */
  view.render('rp4', CANVAS_TYPE_RP4);

3-2.個別説明

(1)定義部

  const CANVAS_WIDTH = 960;
  const CANVAS_HEIGHT = 540;
  const CANVAS_TYPE_RP4 = 1;

上部に利用する定数を定義します。

定数名 説明
CANVAS_WIDTH キャンバスの幅です
CANVAS_HEIGHT キャンバスの高さです
CANVAS_TYPE_RP4 正四面体表示を表す定数です

(2)renderメソッド

3D図形を指定されたキャンバスに描画します。

引数

引数名 説明
id 描画するcanvasのid
canvasType 定数定義された「CANVAS_TYPE_~」の値

戻り値

なし

各コード説明
      const canvas = document.getElementById(id);

引数で渡されたcanvasのHTMLエレメントを取得します。

      const renderer = new THREE.WebGLRenderer({
        canvas: canvas,
        antialias: true
      });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(CANVAS_WIDTH, CANVAS_HEIGHT);

画面描画をするクラスのインスタンスを生成します。
描画先のキャンバスのHTMLエレメントと、アンチエイリアスを利用する設定を渡します。

アンチエイリアスを有効にすることで滑らかな表示にしています。

画面調整は「setPixelRatio」「setSize」で行うと考えおくのがよいです。

      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0xffffff);

シーンのインスタンスを生成します。
ここでは背景を白(0xffffff)で設定します。

      const camera = new THREE.PerspectiveCamera(60, CANVAS_WIDTH / CANVAS_HEIGHT);
      camera.position.set(2, 0, 9);

カメラのインスタンスを生成します。
ここではパースペクティブカメラ(遠近感のあるカメラ)を用います。
視野角は60°に設定しています。

初期座標は少し右からみて、すこし引くイメージで、
x=2、z=9と指定します。

three.jsでは、各座標とプラスマイナスの関係は次のようになります。

./images/03-02-02.png

ここでは使用していませんが、「THREE.AxesHelper」を利用するとプラスの方向の座標軸が表示されるようになります。
マイナスの方向には表示されません。

      const controls = new THREE.OrbitControls(camera, canvas);
      controls.enableDamping = true;
      controls.dampingFactor = 0.2;

カメラ制御のインスタンスを生成します。
操作が滑らかになるようにプロパティを設定します。

      switch(canvasType) {
        case CANVAS_TYPE_RP4:
          this.setSceneRP4(scene);
        break;

        default:
          // pass
      }

引数で渡されたタイプ情報を元に後述の描画関数「setSceneRP4」を呼び出します。

      const update = function() {
        controls.update();
        renderer.render(scene, camera);
        requestAnimationFrame(update);
      };

      update();

再帰的に呼ばれるアニメーション処理を定義します。
ここではカメラの制御更新と描画オブジェクトの位置や回転が
指定された場合の表示更新をしています。

「requestAnimationFrame」に自身の関数を渡すことで連続実行をしています。

(3)setSceneRP4メソッド

正四面体を引数で渡されたシーンに追加します。

引数

引数名 説明
scene シーンオブジェクト

戻り値

なし

各コード説明
      const createMesh = function() {
        const geometry = new THREE.ConeGeometry(3, 3, 3);
        const material = new THREE.MeshBasicMaterial({
          color: 0xccffcc,
          transparent: true,
          opacity: 0.8
        });
        const mesh = new THREE.Mesh(geometry, material);

        return mesh;
      };

正四面体のメッシュを生成する処理です。

ジオメトリには「ConeGeometry」を用います。
ここでは1辺が3の長さの正三角形の四面体に指定しています。

マテリアルはopacityで半透明に指定しています。

      const createLine = function() {
        const geometry = new THREE.ConeBufferGeometry(3, 3, 3);
        const edges = new THREE.EdgesGeometry(geometry);
        const material = new THREE.LineBasicMaterial({ color: 0x000000 });
        const line = new THREE.LineSegments(edges, material);

        return line;
      };

ラインのメッシュを生成する処理です。

作成する正四面体は各辺に線を引きます。
単に「ConeGeometry」で生成するだけでは線はできないため、
別に「ConeGeometry」の縁に線を引いたメッシュを用意して、「同一座標」に表示させています。

似たような方法で「ConeGeometry」でwireframeのプロパティを設定することもできますが、
この場合は不要な対角線なども表示されてしまうため、「EdgesGeometry」を利用して縁のみを表示するようにしています。

      scene.add(createMesh());
      scene.add(createLine());

正四面体のメッシュと線のみのメッシュをシーンに追加します。

(4)呼び出し処理

  const view = new View();

作成したクラスのインスタンスを生成します。

  view.render('rp4', CANVAS_TYPE_RP4);

表示先のキャンバスのidを指定して正四面体の描画処理を呼び出します。

(5)動作確認

ここまでのコードを書いたらブラウザで開いて次の確認をします。

  • 正四面体が正しく表示されること

4.正四面体を展開するJavaScriptコード

JavaScriptを使って正四面体を展開します。
Viewクラス内にsetSceneRP4Openメソッドを追加して、renderメソッドを更新します。

  • renderメソッド(更新あり)
    • 画面に3D図形を描画する処理をまとめたメソッドです。 汎用的なコードのため使い回せる部分が多いものになります。
  • setSceneRP4Openメソッド
    • 正四面体を展開する処理(シーンに設定する処理)のメソッドです。 内部的に「表示の設定をする」部分と「展開の設定をする」部分に分かれています。

4-1.全体

※「追加 START」~「追加 END」までが追加行になります。

コード全体を見る場合はここをクリックしてください(先ほどのコード全体より長めです)
  /** defines */
  const CANVAS_WIDTH = 960;
  const CANVAS_HEIGHT = 540;
  const CANVAS_TYPE_RP4 = 1;
  // 追加 START-----------------------------------
  const CANVAS_TYPE_RP4_OPEN = 2;
  const ANIM_STATUS_STAY = 0;
  const ANIM_STATUS_WAIT_START = 1;
  const ANIM_STATUS_EXECUTING = 2;
  const ANIM_STATUS_DONE = 3;
  const ANIM_STATUS_WAIT_RESET = 4;
  // 追加 END-------------------------------------


  /** classes */
  const View = function() {
    // 追加 START-----------------------------------
    this.animationStatus = {
      renderCanvas4Open: ANIM_STATUS_STAY
    };
    // 追加 END-------------------------------------
  };
  View.prototype =
  {
    render: function(id, canvasType) {
      const canvas = document.getElementById(id);
      const renderer = new THREE.WebGLRenderer({
        canvas: canvas,
        antialias: true
      });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(CANVAS_WIDTH, CANVAS_HEIGHT);

      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0xffffff);

      const camera = new THREE.PerspectiveCamera(60, CANVAS_WIDTH / CANVAS_HEIGHT);
      camera.position.set(2, 0, 9);

      const controls = new THREE.OrbitControls(camera, canvas);
      controls.enableDamping = true;
      controls.dampingFactor = 0.2;

      // 追加 START-----------------------------------
      let interval;
      // 追加 END-------------------------------------
      switch(canvasType) {
        case CANVAS_TYPE_RP4:
          this.setSceneRP4(scene);
        break;

        // 追加 START-----------------------------------
        case CANVAS_TYPE_RP4_OPEN:
          interval = this.setSceneRP4Open(scene);
        break;
        // 追加 END-------------------------------------

        default:
          // pass
      }

      /** 描画更新処理 */
      const update = function() {
        // 追加 START-----------------------------------
        if (typeof interval !== 'undefined') {
          interval();
        }
        // 追加 END-------------------------------------
        controls.update();
        renderer.render(scene, camera);
        requestAnimationFrame(update);
      };

      update();
    },

    setSceneRP4: function(scene) {
      const createMesh = function() {
        const geometry = new THREE.ConeGeometry(3, 3, 3);
        const material = new THREE.MeshBasicMaterial({
          color: 0xccffcc,
          transparent: true,
          opacity: 0.8
        });
        const mesh = new THREE.Mesh(geometry, material);

        return mesh;
      };
      const createLine = function() {
        const geometry = new THREE.ConeBufferGeometry(3, 3, 3);
        const edges = new THREE.EdgesGeometry(geometry);
        const material = new THREE.LineBasicMaterial({ color: 0x000000 });
        const line = new THREE.LineSegments(edges, material);

        return line;
      };

      scene.add(createMesh());
      scene.add(createLine());
    },

    // 追加 START(上のコンマから)-----------------------------------
    setSceneRP4Open: function(scene) {
      const that = this;
      // 辺の長さ
      const length = 3;
      // 上部3面の傾き角度(ラジアン)
      const angle = Math.acos(1/3);
      // 原点からx軸に平行な辺に下した垂線の長さ
      const oa = length / 2;
      // 原点と移動先中心点を結んだ線分の角度
      const rad = (Math.PI - angle) / 2;
      // 原点から移動先までの距離
      const ab = oa * Math.sin(angle/2) * 2;
      // 原点から移動先までの高さ
      const h = ab * Math.sin(rad);
      // 原点から移動先までの幅
      const w = ab * Math.cos(rad);

      /** 位置を設定する */
      const setPosition = function(index, mesh) {
        mesh.position.y = -1 * length / 2;

        // 回転させるためのクォータニオンを取得
        const quaternion = mesh.quaternion;
        const target = new THREE.Quaternion();

        let axis = null;
        switch (index) {
          case 0:
            // 回転
            axis = new THREE.Vector3(-1, 0, 0).normalize();
            target.setFromAxisAngle(axis, angle);
            quaternion.multiply(target);
            // 移動
            mesh.position.x = 0;
            mesh.position.y += h;
            mesh.position.z += -1 * w;
          break;

          case 1:
            // 回転
            axis = new THREE.Vector3(1, 0, -1 * Math.sqrt(3)).normalize();
            target.setFromAxisAngle(axis, angle);
            quaternion.multiply(target);
            // 移動
            mesh.position.x += w * Math.cos(Math.PI/6);
            mesh.position.y += h;
            mesh.position.z += w * Math.sin(Math.PI/6);
          break;

          case 2:
            // 回転
            axis = new THREE.Vector3(1, 0, Math.sqrt(3)).normalize();
            target.setFromAxisAngle(axis, angle);
            quaternion.multiply(target);
            // 移動
            mesh.position.x += -1 * w * Math.cos(Math.PI/6);
            mesh.position.y += h;
            mesh.position.z += w * Math.sin(Math.PI/6);
          break;

          case 3:
            // 底面のため何もしない
          break;

          default:
            // pass
        }
      };
      /** メッシュを生成する */
      const createMesh = function(index) {
        const geometry = new THREE.CylinderGeometry(length, length, 0.01, 3, 1);
        const material = new THREE.MeshBasicMaterial({
          color: 0xccffcc,
          transparent: true,
          opacity: 0.8
        });
        const mesh = new THREE.Mesh(geometry, material);
        setPosition(index, mesh);

        return mesh;
      };
      /** 線を生成する */
      const createLine = function(index) {
        const geometry = new THREE.CylinderBufferGeometry(length, length, 0.01, 3, 1);
        const edges = new THREE.EdgesGeometry(geometry);
        const material = new THREE.LineBasicMaterial({ color: 0x000000 });
        const line = new THREE.LineSegments(edges, material);
        setPosition(index, line);

        return line;
      };

      for (var i = 0; i < 4; i++) {
        scene.add(createMesh(i));
        scene.add(createLine(i));
      }

      // 100回で展開完了になるように移動・回転する
      const moveAngle = (Math.PI - angle) / 100;
      that.count = 0;
      that.angle = angle;

      // リセット用に位置と回転情報を保存
      let orgChildren = [];
      for (let i = 0; i < scene.children.length; i++) {
        let child = {};
        child.position = {};
        child.rotation = {};
        child.position.x = scene.children[i].position.x;
        child.position.y = scene.children[i].position.y;
        child.position.z = scene.children[i].position.z;
        child.rotation.x = scene.children[i].rotation.x;
        child.rotation.y = scene.children[i].rotation.y;
        child.rotation.z = scene.children[i].rotation.z;
        orgChildren.push(child);
      }

      /** 展開処理 */
      const open = function() {
        const status = that.animationStatus['renderCanvas4Open'];
        if (status === ANIM_STATUS_STAY) {
          return;
        }
        else if (status === ANIM_STATUS_WAIT_START) {
          that.animationStatus['renderCanvas4Open'] = ANIM_STATUS_EXECUTING;
        }
        else if (status === ANIM_STATUS_EXECUTING) {
          // pass
        }
        else if (status === ANIM_STATUS_WAIT_RESET) {
          for (let i = 0; i < scene.children.length; i++) {
            scene.children[i].position.x = orgChildren[i].position.x;
            scene.children[i].position.y = orgChildren[i].position.y;
            scene.children[i].position.z = orgChildren[i].position.z;
            scene.children[i].rotation.x = orgChildren[i].rotation.x;
            scene.children[i].rotation.y = orgChildren[i].rotation.y;
            scene.children[i].rotation.z = orgChildren[i].rotation.z;
          }
          that.animationStatus['renderCanvas4Open'] = ANIM_STATUS_STAY;
          that.count = 0;
          that.angle = angle;
          return;
        }
        // else if (status === ANIM_STATUS_EXECUTING) { }
        // else if (status === ANIM_STATUS_DONE) { }
        else {
          return;
        }

        // angleがMath.PIになると展開完了のため処理を抜ける
        if (that.angle >= Math.PI) {
          that.animationStatus['renderCanvas4Open'] = ANIM_STATUS_DONE;
          return;
        }

        that.count++;
        let nextAngle = that.angle + moveAngle;
        if (nextAngle > Math.PI) {
          nextAngle = Math.PI;
        }
        const w = length/2 * (Math.cos(that.angle) - Math.cos(nextAngle));
        const h = length/2 * (Math.sin(nextAngle) - Math.sin(that.angle));
        that.angle = nextAngle;

        /** 各図形の展開処理(indexはsceneの子要素インデックス) */
        const openMesh = function(index) {

          const quaternion = scene.children[index].quaternion;
          const target = new THREE.Quaternion();

          let axis = new THREE.Vector3(0, 0, 0);
          switch (index) {
            case 0:
            case 1:
              // 回転
              axis = new THREE.Vector3(-1, 0, 0).normalize();
              target.setFromAxisAngle(axis, moveAngle);
              quaternion.multiply(target);
              // 移動
              scene.children[index].position.y += h;
              scene.children[index].position.z -= w;
            break;

            case 2:
            case 3:
              // 回転
              axis = new THREE.Vector3(1, 0, -1 * Math.sqrt(3)).normalize();
              target.setFromAxisAngle(axis, moveAngle);
              quaternion.multiply(target);
              // 移動
              scene.children[index].position.x += w * Math.cos(Math.PI/6);
              scene.children[index].position.y += h;
              scene.children[index].position.z += w * Math.sin(Math.PI/6);
            break;

            case 4:
            case 5:
              // 回転
              axis = new THREE.Vector3(1, 0, Math.sqrt(3)).normalize();
              target.setFromAxisAngle(axis, moveAngle);
              quaternion.multiply(target);
              // 移動
              scene.children[index].position.x += -1 * w * Math.cos(Math.PI/6);
              scene.children[index].position.y += h;
              scene.children[index].position.z += w * Math.sin(Math.PI/6);
            break;

            default:
              // pass
          }
        }

        for (let i = 0; i < 6; i++) {
          openMesh(i);
        }
      };

      return open;
    }
    // 追加 END-------------------------------------
  };


  /** objects */
  const view = new View();


  // 追加 START-----------------------------------
  /** events */
  document.getElementById('btnOpen').addEventListener('click', function() {
    view.animationStatus['renderCanvas4Open'] = ANIM_STATUS_WAIT_START;
  });
  document.getElementById('btnReset').addEventListener('click', function() {
    view.animationStatus['renderCanvas4Open'] = ANIM_STATUS_WAIT_RESET;
  });
  // 追加 END-------------------------------------


  /** run */
  view.render('rp4', CANVAS_TYPE_RP4);
  // 追加 START-----------------------------------
  view.render('rp4Open', CANVAS_TYPE_RP4_OPEN);
  // 追加 END-------------------------------------

4-2.個別説明

(1)定義部(更新)

  const CANVAS_TYPE_RP4_OPEN = 2;
  const ANIM_STATUS_STAY = 0;
  const ANIM_STATUS_WAIT_START = 1;
  const ANIM_STATUS_EXECUTING = 2;
  const ANIM_STATUS_DONE = 3;
  const ANIM_STATUS_WAIT_RESET = 4;

上部に利用する定数を追加定義します。

定数名 説明
CANVAS_TYPE_RP4_OPEN 正四面体展開を表す定数です
ANIM_STATUS_STAY 「待機中」を表すアニメーションのステータスです
ANIM_STATUS_WAIT_START 「展開待ち」を表すアニメーションのステータスです
ANIM_STATUS_EXECUTING 「展開中」を表すアニメーションのステータスです
ANIM_STATUS_DONE 「展開終了」を表すアニメーションのステータスです
ANIM_STATUS_WAIT_RESET 「リセット待ち」を表すアニメーションのステータスです

「ANIM_STATUS_~」はボタン操作に関する状態を表す定数です。

  const View = function() {
    // 追加 START-----------------------------------
    this.animationStatus = {
      renderCanvas4Open: ANIM_STATUS_STAY
    };
    // 追加 END-------------------------------------
  };

クラスのコンストラクタに状態初期化の処理を追加します。
「renderCanvas4Open」というキーが正四面体展開を表します。

(2)renderメソッド(更新)

定義に変更はありません。

各コード説明
      // 追加 START-----------------------------------
      let interval;
      // 追加 END-------------------------------------
      switch(canvasType) {
        case CANVAS_TYPE_RP4:
          this.setSceneRP4(scene);
        break;

        // 追加 START-----------------------------------
        case CANVAS_TYPE_RP4_OPEN:
          interval = this.setSceneRP4Open(scene);
        break;
        // 追加 END-------------------------------------

        default:
          // pass
      }

後述の「setSceneRP4Open」メソッドは連続処理の関数を返します。
その返却値を受け取れるように「interval」変数を定義します。

正四面体展開する「setSceneRP4Open」メソッドの呼び出し処理を追加します。

      const update = function() {
        // 追加 START-----------------------------------
        if (typeof interval !== 'undefined') {
          interval();
        }
        // 追加 END-------------------------------------
        controls.update();
        renderer.render(scene, camera);
        requestAnimationFrame(update);
      };

interval変数が定義されている場合には、その変数を実行する処理を追加します。
intervalは変数でも関数を表すため、関数形式の「interval()」と書きます。

(3)setSceneRP4Openメソッド

正四面体を引数で渡されたシーンに追加します。
また展開処理の関数を返します。

引数

引数名 説明
scene シーンオブジェクト

戻り値

展開処理の関数

各コード説明
      const that = this;

クロージャ内でこのメソッド自身を呼び出したいため、that変数に自分自身を格納しておきます。

      // 辺の長さ
      const length = 3;

正四面体の辺の長さです。

      // 上部3面の傾き角度(ラジアン)
      const angle = Math.acos(1/3);

今回は「setSceneRP4」メソッドとは異なり、正四面体を4つの三角形を結合して作成します。
そのときに必要になる上部3面の底面に対する角度です。

./images/04-02-01.png

正四面体の最上部の頂点から底面に対して垂線を下ろした場合、
底面の中心に対して1/3の部分と交わるため、逆関数を使って角度を計算しています。
※証明は割愛しま。す

./images/04-02-02.png

      // 原点からx軸に平行な辺に下した垂線の長さ
      const oa = length / 2;
      // 原点と移動先中心点を結んだ線分の角度
      const rad = (Math.PI - angle) / 2;
      // 原点から移動先までの距離
      const ab = oa * Math.sin(angle/2) * 2;
      // 原点から移動先までの高さ(2次元におけるyに相当)
      const h = ab * Math.sin(rad);
      // 原点から移動先までの幅(2次元におけるxに相当)
      const w = ab * Math.cos(rad);

ここではsetPositionで利用する、座標計算に必要な値を計算します。
考え方としては、底面の位置に4枚の正三角形を配置し、
1枚ずつ立てて辺と頂点を合わせていくイメージです。

この立てる行為を「回転」、頂点と合わせていく行為を「移動」と考えます。
つまりは「回転」させた後に「移動」をさせるイメージを持つとわかりやすいです。

./images/04-02-03.png

o、a、bはそれぞれ三角形の頂点を表し、頂点同士をつないだ辺をoa、ob、abとします。

わかりやすくするためx軸を抜きで考えます。
そのためにx軸のプラス側からzy座標の平面(2次元)を見ていると考えます。

oを原点(底面の中心点)とします。
aを回転後(立てた後)の下にある辺の中心点とします。
bを底面のうちx軸と平行になっている辺の中心点とします。

./images/04-02-04.png

oaの長さですが、これはthree.jsがどこに表示しているかを把握する必要があるため、
頂点の情報を出力して確認します。

底面の頂点の情報を出力すると次のようになります。

0: n {x: 0, y: 0.004999999888241291, z: 3}
1: n {x: 2.598076105117798, y: 0.004999999888241291, z: -1.5}
2: n {x: -2.598076105117798, y: 0.004999999888241291, z: -1.5}
3: n {x: 0, y: -0.004999999888241291, z: 3}
4: n {x: 2.598076105117798, y: -0.004999999888241291, z: -1.5}
5: n {x: -2.598076105117798, y: -0.004999999888241291, z: -1.5}
6: n {x: 0, y: 0.004999999888241291, z: 0}
7: n {x: 0, y: -0.004999999888241291, z: 0}

次のようなコードで上記の出力がされ頂点を確認することができます。
ジオメトリの「vertices」プロパティを出力します。

// 参考: 頂点情報を出力する
console.log(index, mesh.geometry.vertices);

このz座標でマイナスになっている値が底面のx軸と平行な辺のz座標になります。
この値は三角形の1辺の半分になります。そのためoaは次の式で表せます。

      // 原点からx軸に平行な辺に下した垂線の長さ
      const oa = length / 2;

またbは回転させる前の座標のため、obはoaと等しくなります。

./images/04-02-05.png

今度は移動後の中心点とz軸との角度を求めます。

aからz軸に平行な線と、移動後の中心点とbの延長線が交わるところで菱形ができます。
つまり4辺が同じ長さになります。

aの座標の部分で考えると、移動後の中心点とz軸の角度は、180°から
面の傾き角度を半分にした値ということがわかります。

      // 原点と移動先中心点を結んだ線分の角度
      const rad = (Math.PI - angle) / 2;

./images/04-02-06.png

原点から移動後の中心点までの距離はabの長さと等しくなるためabの長さを求めます。

oabの二等辺三角形に対して、まずはoから垂線を下ろし直角三角形で考えます。
oの部分の角度は面の傾き角度の半分になるためabの半分の長さは

$$oa \times sin(面の傾き/2)$$

になります。abは上記の2倍のため次のようになります。

      // 原点から移動先までの距離
      const ab = oa * Math.sin(angle/2) * 2;

原点から移動後の中心点とその点を結んだ線分とz軸の角度がわかりましたので、
移動後の座標は次の通りになります。

$$y座標: ab \times sin(θ)$$
$$z座標: ab \times cos(θ)$$

JavaScriptで表すと次のようになります。

      // 原点から移動先までの高さ
      const h = ab * Math.sin(rad);
      // 原点から移動先までの幅
      const w = ab * Math.cos(rad);

ここで注意が必要なのは今計算している面については、
移動後の中心点のz座標は原点より奥にありますので、
wについては-1をかけてマイナスにした値が座標になります。

ここでは他の面でもこの計算結果を使うため、マイナスをかけるのは
実際に位置を指定するタイミングにしています。

(4)setPositionクロージャ

各面ごとの位置および回転を設定します。

引数

引数名 説明
index 各面につけられたインデックス(0~3)
mesh 位置や角度を設定する対象の面

戻り値

なし

        mesh.position.y = -1 * length / 2;

先に表示した正四面体と同じ位置に表示させるためy座標における位置を少し下げます。

        // 回転させるためのクォータニオンを取得
        const quaternion = mesh.quaternion;
        const target = new THREE.Quaternion();

回転を設定するための準備です。定型コードと考えて大丈夫です。

          case 0:
            // 回転
            axis = new THREE.Vector3(-1, 0, 0).normalize();
            target.setFromAxisAngle(axis, angle);
            quaternion.multiply(target);
            // 移動
            mesh.position.x = 0;
            mesh.position.y += h;
            mesh.position.z += -1 * w;
          break;

case0はx軸と平行な底辺を持つ面を指します。

            axis = new THREE.Vector3(-1, 0, 0).normalize();
            target.setFromAxisAngle(axis, angle);
            quaternion.multiply(target);

回転のコードですが、Quaternionは回転軸(ベクトル)を中心に回転をするのでその軸を決めます。

x軸のプラスの方向の軸とした場合に回転をさせるとその軸よりz座標が
正の位置にある頂点は反時計回り(yが減算される)に回転されます。

これは「ねじを抜くとき」をイメージするとわかりやすいです。
ねじを抜くときは同じく反時計回りに回します。

今回はこの回転の逆にしたいため、x座標がマイナスのベクトルを指定します。

「setFromAxisAngle」により回転の設定をしますが、
このときに渡すベクトルが正規化されていないと、拡大・縮小されてしまうため、
「normalize」をして回転のみされるようにします。

「multiply」を実行することで回転します。
描画の更新をしていないため、実際はまだ回転はしません。

            mesh.position.x = 0;
            mesh.position.y += h;
            mesh.position.z += -1 * w;

移動後の面の中心点の位置を指定します。

x座標については移動しないので0のままです。
y座標については計算した「原点から移動先までの高さ」を追加して移動させます。
z座標については計算した「原点から移動先までの幅」を追加しますが、
z座標では奥がマイナスになるため、-1をかけた値を追加します。

          case 1:
            // 回転
            axis = new THREE.Vector3(1, 0, -1 * Math.sqrt(3)).normalize();
            target.setFromAxisAngle(axis, angle);
            quaternion.multiply(target);
            // 移動
            mesh.position.x += w * Math.cos(Math.PI/6);
            mesh.position.y += h;
            mesh.position.z += w * Math.sin(Math.PI/6);
          break;

case1はx座標がプラス側の面を指します。

            axis = new THREE.Vector3(1, 0, -1 * Math.sqrt(3)).normalize();
            target.setFromAxisAngle(axis, angle);
            quaternion.multiply(target);

今回の回転軸は底面の辺に沿ってx座標がプラス、z座標がマイナスの方になります。
直角三角形で座標を考え、x座標を1とすると、次の計算式よりz座標は$-\sqrt{3}$になります。

一番長い辺の長さをcとすると、次の通り計算できます。

$$sin30° = 1 \div c$$
$$c = 1 \div sin30°$$
$$c = 2$$

三平方の定理より、残りの一辺b、つまりはz座標も求めることができます。

$$c^{2} = b^{2} + 1^{2}$$
$$4 = b^{2} + 1$$
$$b^{2} = 3$$
$$b = \sqrt{3}$$

今回はz座標は奥を指すため、-1をかけます。

            mesh.position.x += w * Math.cos(Math.PI/6);
            mesh.position.y += h;
            mesh.position.z += w * Math.sin(Math.PI/6);

移動後の面の中心点の位置を指定します。

x座標については計算した「原点から移動先までの幅」を斜線とした直角三角形の辺を求める方法で、x座標を加算します。
y座標については計算した「原点から移動先までの高さ」を追加して移動させます。
z座標については計算した「原点から移動先までの幅」を斜線とした直角三角形の辺を求める方法で、x座標を加算します。

          case 2:
            // 回転
            axis = new THREE.Vector3(1, 0, Math.sqrt(3)).normalize();
            target.setFromAxisAngle(axis, angle);
            quaternion.multiply(target);
            // 移動
            mesh.position.x += -1 * w * Math.cos(Math.PI/6);
            mesh.position.y += h;
            mesh.position.z += w * Math.sin(Math.PI/6);
          break;

case2はx座標がマイナス側の面を指します。

            axis = new THREE.Vector3(1, 0, Math.sqrt(3)).normalize();
            target.setFromAxisAngle(axis, angle);
            quaternion.multiply(target);

今回の回転軸は底面の辺に沿ってx座標がマイナス、z座標がマイナスの方になります。
直角三角形で座標を考えるとcase1同様にz座標は$-\sqrt{3}$になります。

            mesh.position.x += -1 * w * Math.cos(Math.PI/6);
            mesh.position.y += h;
            mesh.position.z += w * Math.sin(Math.PI/6);

移動に関してはcase1に対してx座標がマイナスになります。

      const createMesh = function(index) {
        const geometry = new THREE.CylinderGeometry(length, length, 0.01, 3, 1);
        const material = new THREE.MeshBasicMaterial({
          color: 0xccffcc,
          transparent: true,
          opacity: 0.8
        });
        const mesh = new THREE.Mesh(geometry, material);
        setPosition(index, mesh);

        return mesh;
      };

正四面体のメッシュを生成する処理です。

ジオメトリには「CylinderGeometry」を用います。
この三角柱の高さを非常に小さい値にして平面同様にしています。

平面のジオメトリを使ってしまうと3次元では見えない角度があるので
「CylinderGeometry」を利用しています。

マテリアルのオプションは先の正四面体と同様です。

メッシュ生成後に「setPosition」を呼び出して「回転」と「移動」をしています。

      const createLine = function(index) {
        const geometry = new THREE.CylinderBufferGeometry(length, length, 0.01, 3, 1);
        const edges = new THREE.EdgesGeometry(geometry);
        const material = new THREE.LineBasicMaterial({ color: 0x000000 });
        const line = new THREE.LineSegments(edges, material);
        setPosition(index, line);

        return line;
      };

ラインのメッシュを生成する処理です。

先の正四面体同様に作成する三角形の各辺に線を引きます。

面と同じ座標になるため、ここでも「setPosition」で「回転」と「移動」をします。

      for (var i = 0; i < 4; i++) {
        scene.add(createMesh(i));
        scene.add(createLine(i));
      }

正四面体の各面のメッシュと線のみのメッシュをシーンに追加します。

      // 100回で展開完了になるように移動・回転する
      const moveAngle = (Math.PI - angle) / 100;
      that.count = 0;
      that.angle = angle;

      // リセット用に位置と回転情報を保存
      let orgChildren = [];
      for (let i = 0; i < scene.children.length; i++) {
        let child = {};
        child.position = {};
        child.rotation = {};
        child.position.x = scene.children[i].position.x;
        child.position.y = scene.children[i].position.y;
        child.position.z = scene.children[i].position.z;
        child.rotation.x = scene.children[i].rotation.x;
        child.rotation.y = scene.children[i].rotation.y;
        child.rotation.z = scene.children[i].rotation.z;
        orgChildren.push(child);
      }

展開の前処理です。
今回は100回の展開処理実行完了となるように、展開範囲の角度を100で割ります。

リセット用に展開前の位置と回転の情報を保存します。
「position」「rotation」ともに読み取り専用のプロパティのため、
各値を取り出して格納しています。

        const status = that.animationStatus['renderCanvas4Open'];
        if (status === ANIM_STATUS_STAY) {
          return;
        }
        else if (status === ANIM_STATUS_WAIT_START) {
          that.animationStatus['renderCanvas4Open'] = ANIM_STATUS_EXECUTING;
        }
        else if (status === ANIM_STATUS_EXECUTING) {
          // pass
        }
        else if (status === ANIM_STATUS_WAIT_RESET) {
          for (let i = 0; i < scene.children.length; i++) {
            scene.children[i].position.x = orgChildren[i].position.x;
            scene.children[i].position.y = orgChildren[i].position.y;
            scene.children[i].position.z = orgChildren[i].position.z;
            scene.children[i].rotation.x = orgChildren[i].rotation.x;
            scene.children[i].rotation.y = orgChildren[i].rotation.y;
            scene.children[i].rotation.z = orgChildren[i].rotation.z;
          }
          that.animationStatus['renderCanvas4Open'] = ANIM_STATUS_STAY;
          that.count = 0;
          that.angle = angle;
          return;
        }
        // else if (status === ANIM_STATUS_EXECUTING) { }
        // else if (status === ANIM_STATUS_DONE) { }
        else {
          return;
        }

設定されたステータスに応じて処理を分けています。

        // angleがMath.PIになると展開完了のため処理を抜ける
        if (that.angle >= Math.PI) {
          that.animationStatus['renderCanvas4Open'] = ANIM_STATUS_DONE;
          return;
        }

平面になった場合はステータスを完了にします。

        that.count++;
        let nextAngle = that.angle + moveAngle;
        if (nextAngle > Math.PI) {
          nextAngle = Math.PI;
        }

次の角度として移動角度を加算した値を設定します。

        const w = length/2 * (Math.cos(that.angle) - Math.cos(nextAngle));
        const h = length/2 * (Math.sin(nextAngle) - Math.sin(that.angle));
        that.angle = nextAngle;

移動距離を計算しています。
幅は斜線(length/2)に現在の角度と移動後の角度の差をかけて求めます。

            case 0:
            case 1:
              // 回転
              axis = new THREE.Vector3(-1, 0, 0).normalize();
              target.setFromAxisAngle(axis, moveAngle);
              quaternion.multiply(target);
              // 移動
              scene.children[index].position.y += h;
              scene.children[index].position.z -= w;
            break;

回転は正四面体を表示させた時と同様です。
移動はy座標には移動高さ分を追加します。

x座標は動きませんので何も指定していません。
z座標には移動幅分を引きます。(奥に移動するため)

            case 2:
            case 3:
              // 回転
              axis = new THREE.Vector3(1, 0, -1 * Math.sqrt(3)).normalize();
              target.setFromAxisAngle(axis, moveAngle);
              quaternion.multiply(target);
              // 移動
              scene.children[index].position.x += w * Math.cos(Math.PI/6);
              scene.children[index].position.y += h;
              scene.children[index].position.z += w * Math.sin(Math.PI/6);
            break;

こちらも回転は正四面体を表示させた時と同様です。
移動はy座標には移動高さ分を追加するのは先ほどと同じです。

x座標は移動幅分に正三角形の内角の半分(30°=$\pi/6$)のcosをかけたものを足します。
z座標は移動幅分に正三角形の内角の半分(30°=$\pi/6$)のsinをかけたものを足します。

            case 4:
            case 5:
              // 回転
              axis = new THREE.Vector3(1, 0, Math.sqrt(3)).normalize();
              target.setFromAxisAngle(axis, moveAngle);
              quaternion.multiply(target);
              // 移動
              scene.children[index].position.x += -1 * w * Math.cos(Math.PI/6);
              scene.children[index].position.y += h;
              scene.children[index].position.z += w * Math.sin(Math.PI/6);
            break;

こちらも回転は正四面体を表示させた時と同様です。
移動はy座標には移動高さ分を追加するのは先ほどと同じです。

x座標は移動幅分に正三角形の内角の半分(30°=$\pi/6$)のcosをかけたものに-1をかけたものを足します。(左に展開するため)
z座標は移動幅分に正三角形の内角の半分(30°=$\pi/6$)のsinをかけたものを足します。

        for (let i = 0; i < 6; i++) {
          openMesh(i);
        }

底面以外の面に対して展開の処理を実行します。
面と線があるため、6回実行します。

      return open;

展開処理を返却します。

(5)呼び出し処理(更新)

  // 追加 START-----------------------------------
  /** events */
  document.getElementById('btnOpen').addEventListener('click', function() {
    view.animationStatus['renderCanvas4Open'] = ANIM_STATUS_WAIT_START;
  });
  document.getElementById('btnReset').addEventListener('click', function() {
    view.animationStatus['renderCanvas4Open'] = ANIM_STATUS_WAIT_RESET;
  });
  // 追加 END-------------------------------------

ボタンを押したときのイベントハンドラを追加します。
「animationStatus」プロパティの値を変更することにより、
動作指示を伝えています。

  // 追加 START-----------------------------------
  view.render('rp4Open', CANVAS_TYPE_RP4_OPEN);
  // 追加 END-------------------------------------

表示先のキャンバスのidを指定して正四面体(展開)の描画処理を呼び出します。

(6)動作確認

ここまでのコードを書いたらブラウザで開いて次の確認をします。

  • 展開用の正四面体が正しく表示されること
  • 「展開」ボタンを押すと展開のアニメーションが始まること
  • 「リセット」ボタンを押すと元の状態に戻ること

最後に

ここまでお付き合いいただきましてありがとうございました。

今回は計算部分の考え方をきちんと伝えたかったことと、一つの記事にしては非常に長いソースコードのため読むだけでも大変だったかと思います。

本記事に掲載したコードを書くのに次のサイト様の情報を参考にさせていただきました。

Three.js入門サイト
https://ics.media/tutorial-three/

Three.jsで立方体を転がすサンプルを作ろう
https://qiita.com/MasaoBlue/items/1ff943dde6ab5f53ef02

three.jsは非常に強力な3D描画ツールです。

これを用いて得られる知識はweb上での3D描画だけではなく、3DアプリやVRなどにおいても非常に有用に感じました。
有用なだけでなく実装しているのも楽しいので、これからもより理解を深めてもっと扱えるようになりたいと思います。

以上です。

この記事が3D描画に興味をお持ちの方の助力になれば幸いです。

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

React アプリに Auth0 でシュッと認証を組み込んで Vercel に爆速デプロイする

Auth0 は認証・認可サービスをクラウドで提供している SaaS ベンダーです。

「認証」という機能はどのアプリケーションにも求められる重要な要件ですが、プロダクトの本質的なビジネス価値を持たない場合が多いでしょう。Auth0 を使用することで、この「認証」機能という Undifferentiated Heavy Lifting な作業を排除できます。

本記事では、簡単な React アプリケーションを作成して Auth0 を使用した認証機能を実装します。また、作成したアプリケーションを Vercel(旧 Zeit now)にデプロイする方法を解説します。ユーザのサインアップ後の確認メールなどは SendGrid から送信されます。

以下は、本記事で紹介するアプリケーションの簡単な構成図です。また、本記事で実装されたアプリケーションは Vercel にデプロイしています。こちらからアクセスできます。ソースコードは以下の GitHub リポジトリにホストしています。

https://github.com/daisuke-awaji/auth0-todo-app

auth0-vercel.png

React アプリケーションの雛形を作る

まずは create-react-app を使用して簡単なアプリケーションを実装していきます。

$ create-react-app app --typescript

react-router

$ yarn add react-router react-router-dom
$ yarn add -D @types/react-router @types/react-router-dom

最初の react-router はシームレスなナビゲーションを可能にするメインのライブラリです。ふたつめは react-router-dom で、React ルーターの DOM 結合を提供します。-D オプションをつけることで開発時にのみ使用するライブラリをインストールできます。この例では TypeScript の型定義ライブラリをインストールしています。

react-bootstrap

最低限のデザインを整えるために react-bootstrap を使用します。

$ yarn add react-bootstrap react-bootstrap-icons bootstrap
$ yarn add -D @types/react-bootstrap

auth0-react

Auth0 の SDK である auth0-react をインストールします。後述しますが、従来の auth0-spa-js などに比べると Hooks 化の対応が進んでおり、本当に最小限のコードで認証機能を実装できます。

$ yarn add @auth0/auth0-react

auth0-react を使用して認証機能を実装する

auth0-react の公式ドキュメント でも解説していますが、本記事ではもう少し実際の開発のユースケースを想定してガイドします。

Auth0Provider

まずは index.tsx にて <App/> コンポーネントを <Auth0Provider/> でラップします。このようにしておくことで、 <App/> コンポーネント内で useAuth0 フックを使用できるようになります。 useAuth0 フックを使用することで、以下のような様々な認証に関するステートおよびメソッドを取得することができます。

  • ステートの例: isLoading, isAuthenticated, user
  • メソッドの例: loginWithRedirect, logout
index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { Auth0Provider } from "@auth0/auth0-react";
import "bootstrap/dist/css/bootstrap.min.css";
import { App } from "./App";

ReactDOM.render(
  <Auth0Provider
    domain={process.env.REACT_APP_AUTH0_DOMAIN!}
    clientId={process.env.REACT_APP_AUTH0_CLIENT_ID!}
    redirectUri={window.location.origin}
  >
    <App />
  </Auth0Provider>,
  document.querySelector("#root")
);

domain および、clientId には Auth0 のマネジメントコンソール画面で作成した Application の domain, clientId を入力してください。

image.png

LoginButton

ログインボタンは useAuth0 フックを使用すると簡単に実装できます。loginWithRedirect メソッドを実行すると Auth0 の SSO エンドポイントを使用してログインした後に、自身のアプリケーションにリダイレクトします。

LoginButton.tsx
import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Button } from "react-bootstrap";

function LoginButton() {
  const { isAuthenticated, loginWithRedirect } = useAuth0();

  return !isAuthenticated ? (
    <Button onClick={loginWithRedirect}>Log in</Button>
  ) : null;
}

export default LoginButton;

実際にログインボタンを押した振る舞いは以下のようになります。
login.gif

ログイン処理の際、Auth0 のエンドポイントにアクセスし、自身のアプリケーションにリダイレクトします。事前に Application URIs の各種設定をしておきましょう。以下の例では開発用に localhost:3000、 本番デプロイ用として https://auth0-todo-app.vercel.app/ を設定しています。

image.png

LogoutButton

ログアウト処理には logout メソッドが用意されています。
ログアウト後に表示するパスを returnTo: xxxx で指定します。

LogoutButton.tsx
import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Button } from "react-bootstrap";

function LogoutButton(props: any) {
  const { isAuthenticated, logout } = useAuth0();

  return isAuthenticated ? (
    <Button
      variant="outline-primary"
      onClick={() => {
        logout({ returnTo: window.location.origin });
      }}
      {...props}
    >
      Log out
    </Button>
  ) : null;
}

export default LogoutButton;

UserProfile

ログインユーザの情報は user を使用して取得します。認証プロバイダー(Google, Facebook, etc...)によって取得できる情報は異なります。以下では、ログインユーザのアバター画像を user.picture として取得しています。

UserProfileAvatar.tsx
import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Image } from "react-bootstrap";
export const UserProfileAvatar = (props: any) => {
  const { user } = useAuth0();
  return (
    <Image
      style={{ width: "30px" }}
      src={user.picture}
      roundedCircle
      {...props}
    />
  );
};

この記事のサンプルアプリケーションでは、この情報を組み合わせて、ユーザアイコンをクリックすると各種メニューが表示されるようにしています。

image.png

認証済みのユーザだけ見れるページを制御する

react-router-dom を使用した実装ではおなじみの PrivateRoute を組み込んでいきます。
auth0-react では、ログインしていないユーザが表示しようとすると、ログインページにリダイレクトする withAuthenticationRequired() が用意されています。

以下のように ProtectedRoute のようなルート制御コンポーネントを作成しておくと便利です。

ProtectedRoute.tsx
import { withAuthenticationRequired } from "@auth0/auth0-react";
import React from "react";
import { Route } from "react-router-dom";

export function ProtectedRoute({ component, ...args }: any) {
  return (
    <Route component={withAuthenticationRequired(component, {})} {...args} />
  );
}

ルート制御ではこの ProtectedRoute コンポーネントを使用して、ログインしていないユーザがアクセスできないページを実装できます。以下の例では、/profile はログインしていない状態では表示できません。

App.tsx
import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Route, BrowserRouter as Router, Switch } from "react-router-dom";
import Home from "./pages/Home";
import { ProtectedRoute } from "./components/auth/ProtectedRoute";
import Profile from "./pages/Profile";
import { Layout } from "./components/layout/Layout";
import Support from "./pages/Support";
export function App() {
  const { isLoading } = useAuth0();
  if (isLoading) {
    return <p></p>;
  }

  return (
    <Router>
      <Layout>
        <Switch>
          <Route path="/" exact component={Home} />
          <ProtectedRoute path="/profile" component={Profile} />
          <Route path="/support" exact component={Support} />
        </Switch>
      </Layout>
    </Router>
  );
}

/profile にアクセスしようとするとログインページにリダイレクトされています。

protectedroute.gif

Vercel にデプロイする

Vercel のダッシュボードから Import Project をクリックします。

image.png

今回は GitHub にホスティングしているソースコードを元にデプロイするので、Git リポジトリを選択します。

image.png

GitHub のリポジトリ URL を指定してください。
image.png

最後にビルドコマンドと環境変数を設定します。
Auth0 の domain と clientId は環境変数に指定して React アプリに渡しましょう。

image.png

あとは Deploy ボタンをクリックして完了です。これだけでデプロイができるはずです。

サインアップ後の確認メールを SendGrid から送信する

Auth0 には Welcome メールやパスワードリセット、アカウントの検証などのためにメールを送信する仕組みが用意されています。

image.png

送信するためには事前にメールプロバイダの設定が必要です。今回は無料で提供されている SendGrid を使用してみましょう。

Email Provider の画面から  SendGrid  を選択し、送信元メールアドレス(From)と SendGrid の API キーを入力します。API キーは SendGrid の settings から発行できます。

image.png

ログインに成功すると以下のようなメールが送信されるようになります。

image.png

まとめ

Auth0 + Vercel + SendGrid という構成をチュートリアル的に実装し、その振る舞いを確認してみました。auth0-react ライブラリは非常に使い勝手がよく、簡単に認証機能を組み込むことができます。一連の流れを実装しても 1 ~ 2 時間で実装できました。以前までは苦労して認証の仕組みを整えていましたが随分と楽になりそうです。

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

React アプリ に Auth0 でシュッと認証を組み込んで Vercel に爆速デプロイする

Auth0 は認証・認可サービスをクラウドで提供している SaaS ベンダーです。

「認証」という機能はどのアプリケーションにも求められる重要な要件ですが、プロダクトの本質的なビジネス価値を持たない場合が多いでしょう。Auth0 を使用することで、この「認証」機能という Undifferentiated Heavy Lifting な作業を排除できます。

本記事では、簡単な React アプリケーションを作成して Auth0 を使用した認証機能を実装します。また、作成したアプリケーションを Vercel(旧 Zeit now)にデプロイする方法を解説します。ユーザのサインアップ後の確認メールなどは SendGrid から送信されます。

以下は、本記事で紹介するアプリケーションの簡単な構成図です。また、本記事で実装されたアプリケーションは Vercel にデプロイしています。こちらからアクセスできます。ソースコードは以下の GitHub リポジトリにホストしています。

https://github.com/daisuke-awaji/auth0-todo-app

auth0-vercel.png

React アプリケーションの雛形を作る

まずは create-react-app を使用して簡単なアプリケーションを実装していきます。

$ create-react-app app --typescript

react-router

$ yarn add react-router react-router-dom
$ yarn add -D @types/react-router @types/react-router-dom

最初の react-router はシームレスなナビゲーションを可能にするメインのライブラリです。ふたつめは react-router-dom で、React ルーターの DOM 結合を提供します。-D オプションをつけることで開発時にのみ使用するライブラリをインストールできます。この例では TypeScript の型定義ライブラリをインストールしています。

react-bootstrap

最低限のデザインを整えるために react-bootstrap を使用します。

$ yarn add react-bootstrap react-bootstrap-icons bootstrap
$ yarn add -D @types/react-bootstrap

auth0-react

Auth0 の SDK である auth0-react をインストールします。後述しますが、従来の auth0-spa-js などに比べると Hooks 化の対応が進んでおり、本当に最小限のコードで認証機能を実装できます。

$ yarn add @auth0/auth0-react

auth0-react を使用して認証機能を実装する

auth0-react の公式ドキュメント でも解説していますが、本記事ではもう少し実際の開発のユースケースを想定してガイドします。

Auth0Provider

まずは index.tsx にて <App/> コンポーネントを <Auth0Provider/> でラップします。このようにしておくことで、 <App/> コンポーネント内で useAuth0 フックを使用できるようになります。 useAuth0 フックを使用することで、以下のような様々な認証に関するステートおよびメソッドを取得することができます。

  • ステートの例: isLoading, isAuthenticated, user
  • メソッドの例: loginWithRedirect, logout
index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { Auth0Provider } from "@auth0/auth0-react";
import "bootstrap/dist/css/bootstrap.min.css";
import { App } from "./App";

ReactDOM.render(
  <Auth0Provider
    domain={process.env.REACT_APP_AUTH0_DOMAIN!}
    clientId={process.env.REACT_APP_AUTH0_CLIENT_ID!}
    redirectUri={window.location.origin}
  >
    <App />
  </Auth0Provider>,
  document.querySelector("#root")
);

domain および、clientId には Auth0 のマネジメントコンソール画面で作成した Application の domain, clientId を入力してください。

image.png

LoginButton

ログインボタンは useAuth0 フックを使用すると簡単に実装できます。loginWithRedirect メソッドを実行すると Auth0 の SSO エンドポイントを使用してログインした後に、自身のアプリケーションにリダイレクトします。

LoginButton.tsx
import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Button } from "react-bootstrap";

function LoginButton() {
  const { isAuthenticated, loginWithRedirect } = useAuth0();

  return !isAuthenticated ? (
    <Button onClick={loginWithRedirect}>Log in</Button>
  ) : null;
}

export default LoginButton;

実際にログインボタンを押した振る舞いは以下のようになります。
login.gif

ログイン処理の際、Auth0 のエンドポイントにアクセスし、自身のアプリケーションにリダイレクトします。事前に Application URIs の各種設定をしておきましょう。以下の例では開発用に localhost:3000、 本番デプロイ用として https://auth0-todo-app.vercel.app/ を設定しています。

image.png

LogoutButton

ログアウト処理には logout メソッドが用意されています。
ログアウト後に表示するパスを returnTo: xxxx で指定します。

LogoutButton.tsx
import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Button } from "react-bootstrap";

function LogoutButton(props: any) {
  const { isAuthenticated, logout } = useAuth0();

  return isAuthenticated ? (
    <Button
      variant="outline-primary"
      onClick={() => {
        logout({ returnTo: window.location.origin });
      }}
      {...props}
    >
      Log out
    </Button>
  ) : null;
}

export default LogoutButton;

UserProfile

ログインユーザの情報は user を使用して取得します。認証プロバイダー(Google, Facebook, etc...)によって取得できる情報は異なります。以下では、ログインユーザのアバター画像を user.picture として取得しています。

UserProfileAvatar.tsx
import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Image } from "react-bootstrap";
export const UserProfileAvatar = (props: any) => {
  const { user } = useAuth0();
  return (
    <Image
      style={{ width: "30px" }}
      src={user.picture}
      roundedCircle
      {...props}
    />
  );
};

この記事のサンプルアプリケーションでは、この情報を組み合わせて、ユーザアイコンをクリックすると各種メニューが表示されるようにしています。

image.png

認証済みのユーザだけ見れるページを制御する

react-router-dom を使用した実装ではおなじみの PrivateRoute を組み込んでいきます。
auth0-react では、ログインしていないユーザが表示しようとすると、ログインページにリダイレクトする withAuthenticationRequired() が用意されています。

以下のように ProtectedRoute のようなルート制御コンポーネントを作成しておくと便利です。

ProtectedRoute.tsx
import { withAuthenticationRequired } from "@auth0/auth0-react";
import React from "react";
import { Route } from "react-router-dom";

export function ProtectedRoute({ component, ...args }: any) {
  return (
    <Route component={withAuthenticationRequired(component, {})} {...args} />
  );
}

ルート制御ではこの ProtectedRoute コンポーネントを使用して、ログインしていないユーザがアクセスできないページを実装できます。以下の例では、/profile はログインしていない状態では表示できません。

App.tsx
import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
import { Route, BrowserRouter as Router, Switch } from "react-router-dom";
import Home from "./pages/Home";
import { ProtectedRoute } from "./components/auth/ProtectedRoute";
import Profile from "./pages/Profile";
import { Layout } from "./components/layout/Layout";
import Support from "./pages/Support";
export function App() {
  const { isLoading } = useAuth0();
  if (isLoading) {
    return <p></p>;
  }

  return (
    <Router>
      <Layout>
        <Switch>
          <Route path="/" exact component={Home} />
          <ProtectedRoute path="/profile" component={Profile} />
          <Route path="/support" exact component={Support} />
        </Switch>
      </Layout>
    </Router>
  );
}

/profile にアクセスしようとするとログインページにリダイレクトされています。

protectedroute.gif

Vercel にデプロイする

Vercel のダッシュボードから Import Project をクリックします。

image.png

今回は GitHub にホスティングしているソースコードを元にデプロイするので、Git リポジトリを選択します。

image.png

GitHub のリポジトリ URL を指定してください。
image.png

最後にビルドコマンドと環境変数を設定します。
Auth0 の domain と clientId は環境変数に指定して React アプリに渡しましょう。

image.png

あとは Deploy ボタンをクリックして完了です。これだけでデプロイができるはずです。

サインアップ後の確認メールを SendGrid から送信する

Auth0 には Welcome メールやパスワードリセット、アカウントの検証などのためにメールを送信する仕組みが用意されています。

image.png

送信するためには事前にメールプロバイダの設定が必要です。今回は無料で提供されている SendGrid を使用してみましょう。

Email Provider の画面から  SendGrid  を選択し、送信元メールアドレス(From)と SendGrid の API キーを入力します。API キーは SendGrid の settings から発行できます。

image.png

ログインに成功すると以下のようなメールが送信されるようになります。

image.png

まとめ

Auth0 + Vercel + SendGrid という構成をチュートリアル的に実装し、その振る舞いを確認してみました。auth0-react ライブラリは非常に使い勝手がよく、簡単に認証機能を組み込むことができます。一連の流れを実装しても 1 ~ 2 時間で実装できました。以前までは苦労して認証の仕組みを整えていましたが随分と楽になりそうです。

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