20200708のJavaScriptに関する記事は26件です。

JSモジュール

fun-hooks

import funHooks from 'fun-hooks';

let createHook = funcHooks({
  useProxy: false,
  ready: funHook.ASYNC | funcHooks.QUEUE
});

// Sync (before, after)
function sum(a, b) {
  return a + b;
}

let hookedSum = createHook('sync', sum);

// before
hookedSum.before(function(next, a, b) {
  a = a + 1  
  next(a, b);
});

// after
hookedSum.after(function(next, result) {
  next(result + 1);
});

let result = hookedSum(1, 1);

// hookedSum(1, 1) => hookedSum.before(1, 1) => sum(2, 1) => hookedSum.after(3) => 4

// Async (before, after)
function increment(a, b, callback) {
  callback(a + 1, b + 1);
}
let hookedIncrement = createHook('async', increment);

hookedIncrement.before(function(next, a, b) {
  a = a + 1;
  next(a, b);
});

hookedIncrement.after(function(next, a, b) {
  next(a, b + 1);
});

hookedIncrement(1, 1, function(a, b) {
  console.log(a, b); // 3, 3
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】FizzBuzz問題

はじめに

自分の勉強の際にJavaScriptを使ってよく目にするFizzBuzz問題をといたので記録として残します。いろいろな解き方があると思いますが今回学習した解き方で記述しています。

FizzBuzz問題

・1~100までの連続する整数を出力する。

・3の倍数の時には数の代わりに「Fizz」と出力する。

・5の倍数の時には数の代わりに「Buzz」と出力する。

・3と5両方の倍数の時には数の代わりに「FizzBuzz」と出力する。

・出力の優先度
 FizzBuzz > Fizz, Buzz

js
for (var i = 1; i < 101; i++) {
  if (i % 3 === 0 & i % 5 === 0) {
    console.log('「FizzBuzz」');
  } else if (i % 3 === 0) {
    console.log('「Fizz」');
  } else if (i % 5 === 0) {
    console.log('「Buzz」');
  } else {
    console.log(i);
  }
}
1
2
「Fizz」
4
「Buzz」
・
・
・
13
14
「FizzBuzz」
・
・
・
98
「Fizz」
「Buzz」
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[flutter]はじめてのflutter[FlutterSDK編]

目的

WEBとアプリの比較から自分のやりたい方向性を掴む。

今日やること

・flutterの環境構築

参考)https://flutter.dev

Flutter SDKの取得とパスを通す

公式ドキュメントを参考に進めます。
Get startedからMacOSを選択し、System requirementsで要件を満たしているか確認します。
Get the Flutter SDKの中を進めていきます。

Screen Shot 2020-07-08 at 21.40.15.png

DL後、ターミナルでパスを通します。

vi .zshrc

※bashであれば、

/.bashrc

vimが開きました。
編集モードにするため、iを押して、

export PATH="$PATH:[PATH_TO_FLUTTER_GIT_DIRECTORY]/flutter/bin"

※PATH:[PATH_TO_FLUTTER_GIT_DIRECTORY]は、flutterを格納したディレクトリを選ぶ。
追加後、esc を押下、

:wq 

で保存。
参考) https://flutter.dev/docs/get-started/install/macos#update-your-path

次に、AndroidStudioとXcodeのインストール。

続きは次回!

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

Javascriptで作る「シューティングゲーム開発入門」学習記

Javascriptで「シューティングゲーム」を作る方法を学ぶ

「ゲーム開発」は「ゲームエンジン」を利用して行うのが主流だと思っていたけど、「Javascript」で作られた「シューティングゲーム」を見てからは、「自分も作ってみたい!」と思い始めたので、作る方法を学ぶことにしました。

「シューティングゲーム」作りに必要な機能

「横スクロール型」のゲームを想定すると「必要な機能」は、

  • 「自機・敵・ミサイル」などの座標位置の管理
  • 「自機と敵」「ミサイルと敵」の当たり判定
  • 「ゲームスコア」の管理

などですが、他にも

  • 自機
  • 背景
  • スコア

などの「画像素材」が必要になります。
デザインセンスが「ゼロ」なので、これが一番の問題だったり・・・

「シューティングゲーム」の機能の作り方

既に作り方は公開されていて、

↓こんな感じのシューティングゲームが作れます。
シューティングゲーム

機能がすごくシンプルなので、アレンジしていろいろと機能を追加していきたい。

例えば、

  • ミサイルの弾道を変える
  • 敵の種類を増やす
  • 動きに追従するサブ機を追加する
  • ボス敵を作る

など、どんどんやりたいことが増えてきました。

想像していたよりプログラムがシンプルだったので、いろいろ触りながら学んでいきたいと思います。
→シューティングゲーム開発

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

【キニナル話】jexcel使ってみた.xlsx

まえおき

今回は気になる記事を検証する内容だゾ
たまにこういう感じで記事を書くゾ
今日はブラウザでExcelチックなことができたので
その感想と使いどころを書いてイくぞ


気になった記事

JavaScriptだけでExcel風のスプレッドシートを開発できる「jExcel」を使ってみた!


ていうかさ

ブラウザでExcelチック?
そんなのオンラインOfficeを使えば解決でしょ???
そういわずに聞いてください。


まぁまぁ

自分のWebアプリに
Excelみたいなセル機能が実装できたら
カッコ良いなとか思っている。


加えて

電子化っていいよね。


で!

具体的に何をしたかと言えば
jexcelというものを使えば
ブラウザ上でExcelのような処理ができる
と聞いて実際にやってみたという話


先に結論を言おう

ブラウザ上でCSVの加工ができる。
これはかなりの範囲で応用が効くのではと
思わざるを得なかった。


筆者の職場

紙文化は神
DB連携ではなくCSV連携
CSVをマクロで云々してサーバにアップロードする感じ
正直なところマクロ秋田県


筆者の環境とか案件とか

そんな職場でも一応、Web案件はある。
が後世に残してはいけないPHP5.1.6と
Windows7そしてIE9だ!(ドンッ)

イツノジダイカナ~


ちなみに

最近、手順書の電子化と聞いて
湧きあがりましたが
全てをExcelでやると聞いてまぁそうだよねってなりました。

ちなみにこれを私のいる世界では手順書のWeb化と言うそうです。

そんなわけあるかーい!!!


活用事例(筆者の場合)

話を戻して活用事例
ちょっと、機能が素晴らしすぎて
使い道がたくさん思い浮かぶけど
とりあえず、ブラウザベースでファイルの編集ができたら
ワークブックがどうのと悩む必要ないなと思いました。


脱線

電子化といえば
最近のテクでナウいヤツ!
電子書籍を買ってみたぞ!

と言ってもKi〇dleのサブスクだけどな!!
何百ページあっても持ち運べるのは最高だな!

そんで思うけど
なんでKi〇dleのオススメってイヤらしいやつ多いの?
そうなっているのは私だけ???


まとめ

jExcelを使うと
CSVの編集ができて
CSVのダウンロードができて
数式を入れるとちゃんと反応までしてくれる。

脱Excel!!
脱紙文化!!


おわり

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

アロー関数ってなんぞや

アロー関数

がいまいちわからなかったので
今日はまず初めに表面てきな部分を
一旦浅めに学んでメモっていく。

アロー関数

下の図、nomalの関数式はarrowの関数式に治せます。

let normal = function(a){
    console.log(a);
}
let arrow = (b) => {
    console.log(b);
}
normal('今までの関数式');
arrow('アロー関数式');


//"今までの関数式"
//"アロー関数"

ちなみに、、関数の中身が1文のみの場合は中括弧{}を以下のように省略できます。

let arrowFunc = (y) => console.log(y);

ようは、今までの関数式を見やすく、シンプルにしたものなのではないのか。

しかし、じつは奥が深いようで、
JavaScriptによく出てくる、thisとかリテラルとか、そこを次回、学んでメモって行きます。

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

Kinx ライブラリ - パーサ・コンビネータ(その2)

Kinx ライブラリ - パーサ・コンビネータ(その2)

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。今回もパーサ・コンビネータです。

前回 Kinx ライブラリ - パーサ・コンビネータ(その1) で AST を作るところまで実施しました。

今回はこれを解釈して実行させます。最後には JIT コンパイルして実行するところまでいきます。

その前に

おさらい

前回の最終的なプログラムを再確認しておきましょう。

using Parsek;

function makeAST(first, rest) {
    var expr = first;
    for (var i = 0, l = rest.length(); i < l; ++i) {
        expr = { lhs: expr, op: rest[i][0], rhs: rest[i][1] };
    }
    return expr;
}

var $ = new Parsek();
var number = $.regex(/[1-9][0-9]*|[0-9]/).map(Integer.parseInt);
var addsub = $.oneOf("+-");
var muldiv = $.oneOf("*/%");
var lbr = $.string("(");
var rbr = $.string(")");

var expression;
var factor = $.lazy(&() => $.alt(number, $.seq(lbr, expression, rbr).map(&(value) => value[1])));
var term = $.seqMap(factor, $.seq(muldiv, factor).many(), makeAST);
expression = $.seqMap(term, $.seq(addsub, term).many(), makeAST);

// test
System.println(expression.parseAll("1+2*3+2*(14-2)").value.toJsonString(true));

これです。これについて 1 つだけ問題点を直しておきましょう。問題というほどではないですが。

実はこのパーサー、空白文字があると失敗します。当然ですね、空白を解釈するところが無いので、空白に遭遇すると想定した文字でないため「文法あってないぜ!」と自信満々に答えてくるのです。

空白文字の読み飛ばし処理

普通のプログラミング言語では、トークンの切れ目が任意の数の空白文字になっており、空白は読み飛ばす対象であることが一般的です。そこで空白文字は読み飛ばすようにしましょう。そこで使うのが以下の 2 つです。

  • $.optWhitespace は 0 個以上の任意の空白文字を解釈するパーサーです。具体的には $.regex(/\s*/) と同じです。したがって、空白、改行(CR/LF)、タブにマッチします。尚、$.whitespace というのもあり、こちらは $.regex(/\s+/) で 1 個以上の空白文字にマッチします。
  • parser.skip(anotherParser)parser にマッチした後 anotherParser にマッチするか試し、マッチしたら parser の結果を返すといった動作をします。つまり、anotherParser のマッチ結果をスキップします。

この 2 つを使って、.skip($.optWhitespace) とすることで特定のパーサー・マッチングの後で空白文字を読み飛ばすことが可能になります。

ここでは以下のように lexeme() という関数を用意し、空白文字を読み飛ばす処理を追加しましょう。ignore は無視するものが増えたときにここに追加すればよいのと、最初の出だしでも空白文字のスキップが必要なので定義しておきます。

var ignore = $.optWhitespace;
var lexeme = &(p) => p.skip(ignore);

この lexeme() 関数は、トークンの切れ目にあれば良いので、numberaddsubmuldivlbrrbr の最小単位のパーサーにあれば良いですね。最初のプログラムを書き換えると次のようになります。

using Parsek;

function makeAST(first, rest) {
    var expr = first;
    for (var i = 0, l = rest.length(); i < l; ++i) {
        expr = { lhs: expr, op: rest[i][0], rhs: rest[i][1] };
    }
    return expr;
}

var $ = new Parsek();
var ignore = $.optWhitespace;
var lexeme = &(p) => p.skip(ignore);

var number = lexeme($.regex(/[1-9][0-9]*|[0-9]/)).map(Integer.parseInt);
var addsub = lexeme($.oneOf("+-"));
var muldiv = lexeme($.oneOf("*/%"));
var lbr = lexeme($.string("("));
var rbr = lexeme($.string(")"));

var expression;
var factor = $.lazy(&() => $.alt(number, $.seq(lbr, expression, rbr).map(&(value) => value[1])));
var term = $.seqMap(factor, $.seq(muldiv, factor).many(), makeAST);
expression = $.seqMap(term, $.seq(addsub, term).many(), makeAST);

// test
System.println(expression.parseAll("1+2*3+2*(14-2)").value.toJsonString(true));
System.println(ignore.then(expression).parseAll("  1 + 2 * 3 + 2 * ( 14 - 2 )  ").value.toJsonString(true));
/* Both results are the same.
{
    "lhs": {
        "lhs": 1,
        "op": "+",
        "rhs": {
            "lhs": 2,
            "op": "*",
            "rhs": 3
        }
    },
    "op": "+",
    "rhs": {
        "lhs": 2,
        "op": "*",
        "rhs": {
            "lhs": 14,
            "op": "-",
            "rhs": 2
        }
    }
}
*/

どちらも同じ結果が返ります。

最初の ignore.then() が先頭の空白を無視する部分です。parser.then(anotherParser).skip() とは違って anotherParser のほうを返します。これによって、parser のほうが無視されます。

では、本題に入りましょう。

インタプリタ

まず JIT の前にインタプリタを実装してみましょう。こっちのほうが簡単です。インタプリタは AST をトラバースしながら逐次実行していきます。左辺を評価し、右辺を評価し、その結果で演算すれば OK。Interpreter クラスは以下のようになります。

class Interpreter(opts_) {
    private sequence(r, op, lhs, rhs) {
        return if (!opts_.enableSequence);
        System.println("%d %s %d -> %d" % lhs % op % rhs % r);
    }
    public eval(ast) {
        var lhs = ast.lhs.isObject ? eval(ast.lhs) : ast.lhs;
        var rhs = ast.rhs.isObject ? eval(ast.rhs) : ast.rhs;
        var r = 0;
        switch (ast.op) {
        case '+':
            r = lhs + rhs;
            break;
        case '-':
            r = lhs - rhs;
            break;
        case '*':
            r = lhs * rhs;
            break;
        case '/':
            r = lhs / rhs;
            break;
        case '%':
            r = lhs % rhs;
            break;
        default:
            throw RuntimeException('Invalid operator');
        }
        sequence(r, ast.op, lhs, rhs);
        return r;
    }
}

計算過程も見えるようにしてみました(コンストラクタに { enableSequence: true } を渡すと計算過程を表示します)。こんな感じで実行してみましょう。

System.println(
    "Result = ",
    new Interpreter({ enableSequence: true })
        .eval(ignore.then(expression)
                    .parseAll("  1 + 2 * 3 + 2 * ( 14 - 2 )  ").value)
);
/*
2 * 3 -> 6
1 + 6 -> 7
14 - 2 -> 12
2 * 12 -> 24
7 + 24 -> 31
Result = 31
*/

ちゃんと優先順位に従って計算されてますね! AST が優先順位の通りに構築されているので当然です。別の式でもやってみましょう。

System.println(
    "Result = ",
    new Interpreter({ enableSequence: true })
        .eval(ignore.then(expression)
                    .parseAll("(( 123 ) * 2 * 4 - 3 * ( 12 + 10 )) % 100").value)
);
/*
123 * 2 -> 246
246 * 4 -> 984
12 + 10 -> 22
3 * 22 -> 66
984 - 66 -> 918
918 % 100 -> 18
Result = 18
*/

あってますねー。

JIT コンパイル

お待ちかねの JIT です。ただし、JIT 技術自体は素晴らしいのですが、正直このレベルの計算量(ループも条件分岐もない)だと素直に上記のインタプリタのほうが速いのでご了承ください。インタプリタと同じルートでコンパイルしてるのと、インタプリタ自体が大した事やってないので、JITコンパイル時間がインタプリタ時間(+α)みたいなものになってしまってます。むむ。

あくまで技術ベースということで。もっと速さに差が出る例題にすればよかったな。今度 ここ の記事を参考にして Brainf*ck を題材に書き直すか。

さて、Interpreter では直接計算式を実行してましたが、計算を実行する代わりに同等のマシン語コードを出力するように書き換えれば OK です。JIT ライブラリ を見ながら、マシン語コードに変換する Compiler クラスは以下のようになるでしょう。ちょっと長いですが、そのまま載せます。今回も コンストラクタに { enableListing: true } とすることで中間形式のコンパイルコードを出力できます。

class Compiler(opts_) {
    var regs_, regsLen_;
    enum { MOV, BOP, DIVMOD, RET }
    private initialize() {
        regs_ = [
            // Jit.R0 and Jit.R1 is used as a work register when it is division.
            { reg: Jit.R2, used: false, name: "R2" },
            { reg: Jit.R3, used: false, name: "R3" },
            { reg: Jit.R4, used: false, name: "R4" },
            { reg: Jit.R5, used: false, name: "R5" },
            { reg: Jit.S0, used: false, name: "S0" },
            { reg: Jit.S1, used: false, name: "S1" },
            { reg: Jit.S2, used: false, name: "S2" },
            { reg: Jit.S3, used: false, name: "S3" },
            { reg: Jit.S4, used: false, name: "S4" },
            { reg: Jit.S5, used: false, name: "S5" },
        ];
        regsLen_ = regs_.length();
    }
    private listing(type, op, dst, op1, op2) {
        return if (!opts_.enableListing);
        switch (type) {
        case MOV:
            System.println("%s <- %s" % dst % op1);
            break;
        case BOP:
            System.println("%s <- %s %s %s" % dst % op1 % op % op2);
            break;
        case DIVMOD:
            System.println("R0 <- %s" % op1);
            System.println("R1 <- %s" % op2);
            System.println("%s <- R0 %s R1" % dst % op);
            break;
        case RET:
            System.println("ret %s" % dst);
            break;
        }
    }
    private getReg() {
        for (var i = 0; i < regsLen_; ++i) {
            if (!regs_[i].used) {
                regs_[i].used = true;
                return i;
            }
        }
        throw RuntimeException("Not enough register");
    }
    private releaseReg(i) {
        regs_[i].used = false;
    }
    private compileLeaf(c, leaf) {
        var r = getReg();
        c.mov(regs_[r].reg, Jit.IMM(leaf));
        listing(MOV, null, regs_[r].name, leaf);
        return r;
    }
    private compileNode(c, ast) {
        var rl = ast.lhs.isObject ? compileNode(c, ast.lhs) : compileLeaf(c, ast.lhs);
        var rr = ast.rhs.isObject ? compileNode(c, ast.rhs) : compileLeaf(c, ast.rhs);
        var r = getReg();
        switch (ast.op) {
        case '+':
            c.add(regs_[r].reg, regs_[rl].reg, regs_[rr].reg);
            listing(BOP, ast.op, regs_[r].name, regs_[rl].name, regs_[rr].name);
            break;
        case '-':
            c.sub(regs_[r].reg, regs_[rl].reg, regs_[rr].reg);
            listing(BOP, ast.op, regs_[r].name, regs_[rl].name, regs_[rr].name);
            break;
        case '*':
            c.mul(regs_[r].reg, regs_[rl].reg, regs_[rr].reg);
            listing(BOP, ast.op, regs_[r].name, regs_[rl].name, regs_[rr].name);
            break;
        case '/':
            c.mov(Jit.R0, regs_[rl].reg);
            c.mov(Jit.R1, regs_[rr].reg);
            c.sdiv();
            c.mov(regs_[r].reg, Jit.R0);
            listing(DIVMOD, ast.op, regs_[r].name, regs_[rl].name, regs_[rr].name);
            break;
        case '%':
            c.mov(Jit.R0, regs_[rl].reg);
            c.mov(Jit.R1, regs_[rr].reg);
            c.sdivmod();
            c.mov(regs_[r].reg, Jit.R1);
            listing(DIVMOD, ast.op, regs_[r].name, regs_[rl].name, regs_[rr].name);
            break;
        default:
            throw RuntimeException('Invalid operator');
        }
        releaseReg(rl);
        releaseReg(rr);
        return r;
    }
    public compile(ast) {
        var c = new Jit.Compiler();
        c.enter();
        var r = compileNode(c, ast);
        c.ret(regs_[r].reg);
        listing(RET, null, regs_[r].name);
        return c.generate();
    }
    public run(ast) {
        var code = compile(ast);
        return code.run();
    }
}

レジスタは R2R5S0S5 が利用可能です。R0R1 は除算で使う必要があるのでそのために取っておきます。レジスタの割り当て方は「使うときに予約して、使い終わったら返す」です。

尚、R 系のレジスタは関数呼び出し後に破壊される可能性がありますが、今回は演算するだけで関数呼び出しをしないので S 系と同様の扱いで良いでしょう。使えるレジスタを使い切るとエラーしますが、中間値の多くは短命なので問題ないでしょう。もし保存すべき値が増えたら、メモリへ退避させるコードを出します。また、Kinx の JIT ライブラリにはスタック上にローカル変数領域を持てるので、足りなくなったらそこに割り当てる、というのが一番楽です。

本格的にレジスタ割り付けとかを考えるとかなり奥が深いですが、JIT の場合は 最適化にはあまり時間をかけない というのがセオリーですので、この程度で十分でしょう。なぜならば、最適化の時間がもったいないからです。事前コンパイル(AOT Compilation)のときはコンパイル時間は気にせずに実行時に速ければ速いだけよいので最適化をガンガンしますが、JIT ではコンパイル時間も実行時間のうちなのでそうもいかないのです。

では早速計算させてみます。using Jit; を忘れずに。

using Parsek;
using Jit;

/* 省略 */
System.println(
    "Result = ",
    new Compiler({ enableListing: true })
        .run(ignore.then(expression)
                   .parseAll("  1 + 2 * 3 + 2 * ( 14 - 2 )  ").value)
);
/*
R2 <- 1
R3 <- 2
R4 <- 3
R5 <- R3 * R4
R3 <- R2 + R5
R2 <- 2
R4 <- 14
R5 <- 2
S0 <- R4 - R5
R4 <- R2 * S0
R2 <- R3 + R4
ret R2
Result = 31
*/

あってますねー。もう一つの例もやってみましょう。

System.println(
    "Result = ",
    new Compiler({ enableListing: true })
        .run(ignore.then(expression)
                   .parseAll("(( 123 ) * 2 * 4 - 3 * ( 12 + 10 )) % 100").value)
);
/*
R2 <- 123
R3 <- 2
R4 <- R2 * R3
R2 <- 4
R3 <- R4 * R2
R2 <- 3
R4 <- 12
R5 <- 10
S0 <- R4 + R5
R4 <- R2 * S0
R2 <- R3 - R4
R3 <- 100
R0 <- R2
R1 <- R3
R4 <- R0 % R1
ret R4
Result = 18
*/

こちらもあってます。

実際のマシン語コードもダンプできるのでやってみましょう。

var code = new Compiler()
       .compile(ignore.then(expression)
                      .parseAll("  1 + 2 * 3 + 2 * ( 14 - 2 )  ").value);
code.dump();
System.println(code.run());

Linux での出力です。

       0:   53                                          push rbx
       1:   41 57                                       push r15
       3:   41 56                                       push r14
       5:   48 8b df                                    mov rbx, rdi
       8:   4c 8b fe                                    mov r15, rsi
       b:   4c 8b f2                                    mov r14, rdx
       e:   48 83 ec 10                                 sub rsp, 0x10
      12:   48 c7 c7 01 00 00 00                        mov rdi, 0x1
      19:   48 c7 c1 02 00 00 00                        mov rcx, 0x2
      20:   49 c7 c0 03 00 00 00                        mov r8, 0x3
      27:   49 89 cb                                    mov r11, rcx
      2a:   4d 0f af d8                                 imul r11, r8
      2e:   4a 8d 0c 1f                                 lea rcx, [rdi+r11]
      32:   48 c7 c7 02 00 00 00                        mov rdi, 0x2
      39:   49 c7 c0 0e 00 00 00                        mov r8, 0xe
      40:   49 c7 c3 02 00 00 00                        mov r11, 0x2
      47:   4c 89 c3                                    mov rbx, r8
      4a:   49 2b db                                    sub rbx, r11
      4d:   49 89 f8                                    mov r8, rdi
      50:   4c 0f af c3                                 imul r8, rbx
      54:   4a 8d 3c 01                                 lea rdi, [rcx+r8]
      58:   48 89 f8                                    mov rax, rdi
      5b:   48 83 c4 10                                 add rsp, 0x10
      5f:   41 5e                                       pop r14
      61:   41 5f                                       pop r15
      63:   5b                                          pop rbx
      64:   c3                                          ret
31

ざっくり読み解いてみましょう。

  • 0xe 行目までがプロローグですね。
  • 0x12 行目で rdi レジスタに 0x01 を代入
  • 0x19 行目で rcx レジスタに 0x02 を代入
  • 0x20 行目で r8 レジスタに 0x03 を代入
  • 0x27-0x2a 行目で r11 レジスタに rcx * r8 の結果を代入
  • 0x2e 行目で rcx レジスタに rdi + r11 の結果を代入
  • 0x32 行目で rdi レジスタに 0x02 を代入
  • 0x39 行目で r8 レジスタに 0x0e を代入
  • 0x40-0x4a 行目で rbx レジスタに r8 - r11 の結果を代入
  • 0x4d-0x50 行目で r8 レジスタに rdi * rbx の結果を代入
  • 0x54 行目で rdi レジスタに rcx + r8 の結果を代入
  • 0x58 行目で rax レジスタに rdi を代入
  • 0x5b 行目からはエピローグです。

関数の復帰値は x64 では rax レジスタと決まっているので、これで結果が返ります。

ではもう一つの例で。

var code = new Compiler()
       .compile(ignore.then(expression)
                      .parseAll("(( 123 ) * 2 * 4 - 3 * ( 12 + 10 )) % 100").value);
code.dump();
System.println(code.run());

こちらも Linux での出力です。

       0:   53                                          push rbx
       1:   41 57                                       push r15
       3:   41 56                                       push r14
       5:   48 8b df                                    mov rbx, rdi
       8:   4c 8b fe                                    mov r15, rsi
       b:   4c 8b f2                                    mov r14, rdx
       e:   48 83 ec 10                                 sub rsp, 0x10
      12:   48 c7 c7 7b 00 00 00                        mov rdi, 0x7b
      19:   48 c7 c1 02 00 00 00                        mov rcx, 0x2
      20:   49 89 f8                                    mov r8, rdi
      23:   4c 0f af c1                                 imul r8, rcx
      27:   48 c7 c7 04 00 00 00                        mov rdi, 0x4
      2e:   4c 89 c1                                    mov rcx, r8
      31:   48 0f af cf                                 imul rcx, rdi
      35:   48 c7 c7 03 00 00 00                        mov rdi, 0x3
      3c:   49 c7 c0 0c 00 00 00                        mov r8, 0xc
      43:   49 c7 c3 0a 00 00 00                        mov r11, 0xa
      4a:   4b 8d 1c 18                                 lea rbx, [r8+r11]
      4e:   49 89 f8                                    mov r8, rdi
      51:   4c 0f af c3                                 imul r8, rbx
      55:   48 89 cf                                    mov rdi, rcx
      58:   49 2b f8                                    sub rdi, r8
      5b:   48 c7 c1 64 00 00 00                        mov rcx, 0x64
      62:   48 89 f8                                    mov rax, rdi
      65:   48 89 ce                                    mov rsi, rcx
      68:   48 99                                       cqo
      6a:   48 f7 fe                                    idiv rsi
      6d:   48 89 d6                                    mov rsi, rdx
      70:   49 89 f0                                    mov r8, rsi
      73:   4c 89 c0                                    mov rax, r8
      76:   48 83 c4 10                                 add rsp, 0x10
      7a:   41 5e                                       pop r14
      7c:   41 5f                                       pop r15
      7e:   5b                                          pop rbx
      7f:   c3                                          ret
18

計算結果もあってますね!

おわりに

通常、スクリプト言語系では AST から独自の中間形式コード(バイトコード)に変換して、それを実行します。Kinx もそうです。ただ、やってることは上記と大差ありません。どちらかというと、Compiler と似たようなことをやって、独自の Interpreter で実行する、といった感じですね。ネイティブなマシン語コードに変換できれば Intrpreter は不要です。普通は多倍長演算とかクラス・オブジェクトとかを扱うし、動的型付け言語は型が実行時まで分からないので、マシン語にコンパイルするのが面倒なだけです。特に、x64(Windows/Linux)/ARM とか色々対応しようとするとそれぞれごとにコンパイラを作らなくてはなりません。なのでバイトコード化がベスト・プラクティスな感じではあります。

そうは言っても本気のプロダクトは JIT 頑張ってますよね。Kinx も native 関数などで一部 JIT 化可能です。しかも抽象化アセンブラ(SLJIT)で表現可能な範囲で実現しているので、基本的には色々なプラットフォームへの移植性は高いはず。

いずれにしても、JIT は言語処理系では昔からホットな話題なので、Kinx の JIT ライブラリはちょっとしたオモチャとしては楽しいかなー、と自画自賛してます。しかも今回頑張ってパーサ・コンビネータなんて実装してしまったので、手軽に独自の DSL とか作ったりできるのではないでしょうか。結構楽しいと思いますがいかがでしょう。

Kinx JIT ライブラリのキャッチフレーズは 「気軽に JIT」、ですね。

ということで、では、また次回。

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

Vue Router 直接URLでアクセスするとエラーが起きる

Vue.jsを使っていて、通常の画面遷移のルーティングは問題無いがURLを直打ちすると

Uncaught SyntaxError: Unexpected token <

というエラーになってしまう現象が起きた。

:bomb: index.html内のパスの設定が原因

これを

<script type="text/javascript" src="js/app.js"></script>

このように変えてやるだけ

<script type="text/javascript" src="/js/app.js"></script>

上のパターンだと直接URLを叩いた時とリンクから画面遷移した時で見ている場所が変わってしまう。

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

CSSとJavascriptでFacebook風アニメーションボタンを作ってみた

テーマ

Facebookのデザインはシンプルでイケてますよね。。
(https://www.facebook.com/)

今回は、Facebookの新規投稿ボタンを題材に、
リップルアニメーションという効果をつけてボタンをリッチにするという目的をもって作業を開始しました!

完成形

aa.gif

ソースはcodepenに↓↓↓

See the Pen Ripple Animation Button by TARO (@shun-ichirotanaka) on CodePen.

参考サイト

https://tkzo.jp/blog/how-to-make-ripple-animation/#i-5

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

AWSサーバーレス連絡フォームの作り方?(AWS Lambda、API Gateway、SES)

AWS S3にサイトを安くて簡単にホスティングできますが、S3は静的なサイトしかホスティングできないですので、従来ならバックエンド処理が必要となるフォーム送信の実装などは難しそうですね。しかし、クラウド時代ではそんな心配はありません!ソリューションはサーバーレスアーキテクチャです!AWSはすでに色々なサーバーレス構築のツールを提供しています。

今回はユーザからお問い合わせやフィードバックを送信するたびに、任意な宛先に通知メールを送信するシンプルなサーバーレスメールサービスを一緒に作ります。AWS LambdaとAPI Gatewayを利用し、簡単なAWSサーバーレス構築を紹介します。

構成図はこんな感じです。
diagram.PNG

処理の流れとしては、ユーザーから連絡フォームより入力した情報を収集し、クライアント側のブラウザからAmazon API Gateway RESTfulサービスに投稿します。Amazon API Gatewayは、収集されたユーザー情報をAWS lambda関数に渡します。AWS Lambda関数は、Eメールを自動生成し、Amazon SESを使用してメールサーバーに転送します。

1.フォームを作成

  • index.htmlファイルを作成し、下記のコードを貼り付けます。
index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>連絡フォーム</title>
    <script src="script.js"></script>
    <link rel="stylesheet " href="style.css ">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js "></script>

</head>


<body>
    <h1>連絡フォーム</h1>
    <form id="contact-form" method="post">
        <h4>名前:</h4>
        <input type="text" id="name-input" placeholder="お名前を入力してください。" class="form-control" /><br />
        <h4>件名:</h4>
        <input type="subject " id="subject " " placeholder="お名前を入力してください。 " class="form-control " /><br />
        <h4>メール:</h4>
        <input type="email " id="email-input " placeholder="メールを入力してください。 " class="form-control " /><br />
        <h4>メッセージ:</h4>
        <textarea id="description-input " rows="3 " placeholder="メッセージを入力してください。 " class="form-control "></textarea><br />
        <div class="button-wrapper "><button type="button " onClick="submitToAPI(event) " class="btn btn-lg ">送信</button></div>

    </form>
</body>
</html>

ブラウザでプレビューを確認できます。こんな感じです。
basic-html-form.PNG

  • 見た目はあまり良くないですので、改善しましょう。style.cssを同じフォルダ内で作成し、下記のコードを貼り付けます。
style.css
* {
    box-sizing: border-box;
}

body {
    text-align: center;
    color: rgb(255, 255, 255);
    background-color: #194680;
    padding-top: 20px;
}

#contact-form {
    margin: auto;
    text-align: left;
    padding: 20px;
    max-width: 430px;
    border: white solid 2px;
    border-radius: 10px;
}

button {
    text-align: center;
    margin-top: 20px;
    font-size: 18px;
    border: white solid 2px;
    background-color: white;
    border-radius: 5px;
}

.button-wrapper {
    text-align: center;
}

input,
textarea {
    width: 380px;
}

textarea {
    height: 200px;
    padding: 10px;
}

input {
    height: 35px;
}

ブラウザではこんな感じになります。
styled-form.PNG

2. AWS Lambda関数の定義

  • AWS Lambda画面で新規関数を作成し、hello-worldのnodejsテンプレを選択します。
    lambda2.PNG

  • 基本的な情報の画面では関数を任意に名付けて、実行ロールは最初選択のままで進みます。
    lambda3.PNG

  • 次の画面の関数コードに下記のコードを切り替えます。

index.js
var AWS = require('aws-sdk');
var ses = new AWS.SES();

//宛先のメールアドレスを入力してください。
var RECEIVER = 'example-receiver@gmail.com';

//送信先のメールアドレスを入力してください。
//送信先のメールはAWSに認証登録する必要があります。
var SENDER = 'example-sender@gmail.com';

//クライアントへのレスポンスヘッダーを設定
var response = {
    "isBase64Encoded": false,
    "headers": {
        "Content-Type": 'application/json',
        "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Accept",
        "Access-Control-Allow-Methods": "POST,GET,OPTIONS",
        "Access-Control-Allow-Origin": "*",
    },
    "statusCode": 200,
    "body": "{\"result\": \"テスト成功です!\"}"
};

exports.handler = function(event, context, callback) {
    console.log('受理したイベント:', event);
    sendEmail(event, function(err, data) {
        context.done(err, null);
    });
    callback(null, response);
};

//メール送信処理
function sendEmail(event, done) {
    var params = {
        Destination: {
            ToAddresses: [
                RECEIVER
            ]
        },
        Message: {
            Body: {
                Text: {
                    Data: '● お名前: ' + event.name + '\n● 件名: ' + event.subject + '\n● メール: \n' + event.email + '\n● 内容: \n' + event.desc,
                    Charset: 'UTF-8'
                }
            },
            Subject: {
                Data: event.name + 'からの連絡フォームが来ました!',
                Charset: 'UTF-8'
            }
        },
        Source: SENDER
    };
    ses.sendEmail(params, done);
    return {
        response,
        body: JSON.stringify(params),
        statusCode: 200
    }
}
  • 同じLambda管理画面で、上にスクロールし、アクセス権限のタブに切り替え、ロール名のリンクをクリックします。

lambda-policy.PNG

  • 当関数のIAMポリシーをSES権限を追加します。IAM画面で該当ポリシーをクリックしポリシーを編集 ->JSONのフォーマットに切り替えし、下記のコードを追加します。

lambda-policy3.PNG

{
  "Sid": "FormPolicy",
  "Effect": "Allow",
  "Action": "ses:SendEmail",
  "Resource": "*"
}

こんな感じです。
lambda-policy2.PNG

3. APIゲートウェイでAPI構築

  • AWSメインコンソールよりAPI Gateway管理画面にアクセスし、APIを作成をクリックします。
    api.PNG

  • 次の画面ではREST API構築をクリックします。

api2.PNG

  • 次の画面はREST APIの選択のままで、API名前を任意に付けます。

api2a.PNG

  • 次の画面は上にあるアクションをクリックし、メッソド作成をクリック、POSTを選択します。

api3.PNG

  • 右側の設定画面では、統合タイプLambdaで、Lambda関数は先ほど作成したものを選択します。

api4b.PNG

  • CORSエラーにならないように、CORS機能も有効にします。アクションのドロップダウンをCORS有効化をクリックし、設定はそのままで保存します。

api4c.PNG

  • API Gatewayの最新アップデートでは、ゲートウェイのレスポンスも設定しないと、CORSエラーが発生するため、設定しましょう。右のゲートウェイレスポンス → DEFAULT 4XX編集の順番にナビゲートします

api5a.PNG

  • レスポンスヘッダーに下記の値を追加し、保存します。
  Access-Control-Allow-Origin: '*'

こんな感じです。
api5b.PNG

これでAPIの構築は完了です。

4. SES送信用のメール検証登録

mail.PNG

verifiedの状態になったら、OKです!

  • Lambda画面に戻して、左上にあるテストで動作確認できます。lambda-test.PNG

  • サンプルテストJSONデータ:

{
  "name": "value1",
  "subject": "value2",
  "email": "test@test.com",
  "desc": "value3"
}

5.全コンポーネントを連携する

  • 最後のステップではすべてを繋ぎます!index.htmlと同じいフォルダでscript.jsを作成し、下記のコードを貼り付け、var URLurlの2ヶ所に先ほど設定したAPI GatewayのエンドポイントURLを差し替えます。
script.js
function submitToAPI(e) {
    e.preventDefault();
    //設定したAPI GatewayのエンドポイントURLをここに入れます。
    var URL = "API-GATEWAY-ENDPOINT-URL";

    //フォームの入力値をチェック
    var name = /[A-Za-z]{1}[A-Za-z]/;
    if (!name.test($("#name-input").val())) {
        alert("2文字以上記入してください。");
        return;
    }
    var subject = /[A-Za-z]{1}[A-Za-z]/;
    if (!subject.test($("#subject").val())) {
        alert("2文字以上記入してください。");
        return;
    }
    if ($("#email-input").val() == "") {
        alert("メールを入力してください。");
        return;
    }

    var email = /^([\w-\.]+@([\w-]+\.)+[\w-]{2,6})?$/;
    if (!email.test($("#email-input").val())) {
        alert("メールアドレスは正しくありません。");
        return;
    }

    var name = $("#name-input").val();
    var subject = $("#subject").val();
    var email = $("#email-input").val();
    var desc = $("#description-input").val();
    var data = {
        name: name,
        subject: subject,
        email: email,
        desc: desc
    };

    $.ajax({
        type: "POST",
        //設定したAPI GatewayのエンドポイントURLをここに入れます。
        url: "API-GATEWAY-ENDPOINT-URL",
        dataType: "json",
        crossDomain: "true",
        contentType: "application/json; charset=utf-8",
        data: JSON.stringify(data),


        success: function() {
            // フォームをクリアし、送信成功のメッセージを表示する
            alert("メッセージが送信されました!");
            document.getElementById("contact-form").reset();
            location.reload();
        },
        error: function() {
            // 送信エラーのメッセージを表示する
            alert("メッセージ送信失敗!");
        }
    });
}

6. 結論

フォームより受信するメールはこんな感じです。
demo.PNG

これでAmazon S3で静的Webサイトをホストしても、AWSアーキテクチャを利用し、ユーザよりサイト管理者へフォーム送信機能を実装できました!

初投稿です、、いかがでしょうか、、(´・ω・`)
コメント、フィードバックなど大変モチベーションになりますので、
是非よろしくお願いいたします!
それでは、また今度! (`・∀・´)ノ

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

AWS Lambda、Amazon API Gateway、Amazon SESを使用し、S3静的ウェブサイトの動的な連絡フォームを作りましょう!?

AWS S3にサイトを安く簡単にホスティングできますが、S3は静的なサイトしかホスティングできないですので、従来ならバックエンド処理が必要になるフォーム送信の実装などは難しそうですね。しかし、クラウド時代ではそんな心配はありません!それははサーバーレスアーキテクチャです!AWSはすでに色々なサーバーレス構築のツールを提供しています。

この投稿ではユーザからお問い合わせやフィードバックを送信するたびに、任意な宛先に通知メルを送信するシンプルなサーバーレスメールサービスを一緒に作ります。AWS Lambdaと API Gatewayを利用し、AWSサーバーレスの構築を学びましょう!

構成図はこんな感じです。
diagram.PNG

処理の流れとしては、ユーザーから連絡フォームより入力した情報を収集し、クライアント側のブラウザからAmazon API Gateway RESTfulサービスに投稿します。Amazon API Gatewayは、収集されたユーザー情報をAWS lambda関数に渡します。AWS Lambda関数は、Eメールを自動生成し、Amazon SESを使用してメールサーバーに転送します。

1.フォームを作成

  • index.htmlファイルを作成し、下記のコードを貼り付けます。
index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>連絡フォーム</title>
    <script src="script.js"></script>
    <link rel="stylesheet " href="style.css ">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js "></script>

</head>


<body>
    <h1>連絡フォーム</h1>
    <form id="contact-form" method="post">
        <h4>名前:</h4>
        <input type="text" id="name-input" placeholder="お名前を入力してください。" class="form-control" /><br />
        <h4>件名:</h4>
        <input type="subject " id="subject " " placeholder="お名前を入力してください。 " class="form-control " /><br />
        <h4>メール:</h4>
        <input type="email " id="email-input " placeholder="メールを入力してください。 " class="form-control " /><br />
        <h4>メッセージ:</h4>
        <textarea id="description-input " rows="3 " placeholder="メッセージを入力してください。 " class="form-control "></textarea><br />
        <div class="button-wrapper "><button type="button " onClick="submitToAPI(event) " class="btn btn-lg ">送信</button></div>

    </form>
</body>
</html>

ブラウザでプレビューを確認できます。こんな感じです。
basic-html-form.PNG

  • 見た目はあまり良くないですので、改善しましょう。style.cssを同じフォルダ内で作成し、下記のコードを貼り付けます。
style.css
* {
    box-sizing: border-box;
}

body {
    text-align: center;
    color: rgb(255, 255, 255);
    background-color: #194680;
    padding-top: 20px;
}

#contact-form {
    margin: auto;
    text-align: left;
    padding: 20px;
    max-width: 430px;
    border: white solid 2px;
    border-radius: 10px;
}

button {
    text-align: center;
    margin-top: 20px;
    font-size: 18px;
    border: white solid 2px;
    background-color: white;
    border-radius: 5px;
}

.button-wrapper {
    text-align: center;
}

input,
textarea {
    width: 380px;
}

textarea {
    height: 200px;
    padding: 10px;
}

input {
    height: 35px;
}

ブラウザではこんな感じになります。
styled-form.PNG

2. AWS Lambda関数の定義

  • AWS Lambda画面で新規関数を作成し、hello-worldのnodejsテンプレを選択します。
    lambda2.PNG

  • 基本的な情報の画面では関数を任意に名付けて、実行ロールは最初選択のままで進みます。
    lambda3.PNG

  • 次の画面の関数コードに下記のコードを切り替えます。

index.js
var AWS = require('aws-sdk');
var ses = new AWS.SES();

//宛先のメールアドレスを入力してください。
var RECEIVER = 'example-receiver@gmail.com';

//送信先のメールアドレスを入力してください。
//送信先のメールはAWSに認証登録する必要があります。
var SENDER = 'example-sender@gmail.com';

//クライアントへのレスポンスヘッダーを設定
var response = {
    "isBase64Encoded": false,
    "headers": {
        "Content-Type": 'application/json',
        "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Accept",
        "Access-Control-Allow-Methods": "POST,GET,OPTIONS",
        "Access-Control-Allow-Origin": "*",
    },
    "statusCode": 200,
    "body": "{\"result\": \"テスト成功です!\"}"
};

exports.handler = function(event, context, callback) {
    console.log('受理したイベント:', event);
    sendEmail(event, function(err, data) {
        context.done(err, null);
    });
    callback(null, response);
};

//メール送信処理
function sendEmail(event, done) {
    var params = {
        Destination: {
            ToAddresses: [
                RECEIVER
            ]
        },
        Message: {
            Body: {
                Text: {
                    Data: '● お名前: ' + event.name + '\n● 件名: ' + event.subject + '\n● メール: \n' + event.email + '\n● 内容: \n' + event.desc,
                    Charset: 'UTF-8'
                }
            },
            Subject: {
                Data: event.name + 'からの連絡フォームが来ました!',
                Charset: 'UTF-8'
            }
        },
        Source: SENDER
    };
    ses.sendEmail(params, done);
    return {
        response,
        body: JSON.stringify(params),
        statusCode: 200
    }
}
  • 同じLambda管理画面で、上にスクロールし、アクセス権限のタブに切り替え、ロール名のリンクをクリックします。

lambda-policy.PNG

  • 当関数のIAMポリシーをSES権限を追加します。IAM画面で該当ポリシーをクリックしポリシーを編集 ->JSONのフォーマットに切り替えし、下記のコードを追加します。

lambda-policy3.PNG

{
  "Sid": "FormPolicy",
  "Effect": "Allow",
  "Action": "ses:SendEmail",
  "Resource": "*"
}

こんな感じです。
lambda-policy2.PNG

3. APIゲートウェイでAPI構築

  • AWSメインコンソールよりAPI Gateway管理画面にアクセスし、APIを作成をクリックします。
    api.PNG

  • 次の画面ではREST API構築をクリックします。

api2.PNG

  • 次の画面はREST APIの選択のままで、API名前を任意に付けます。

api2a.PNG

  • 次の画面は上にあるアクションをクリックし、メッソド作成をクリック、POSTを選択します。

api3.PNG

  • 右側の設定画面では、統合タイプLambdaで、Lambda関数は先ほど作成したものを選択します。

api4b.PNG

  • CORSエラーにならないように、CORS機能も有効にします。アクションのドロップダウンをCORS有効化をクリックし、設定はそのままで保存します。

api4c.PNG

  • API Gatewayの最新アップデートでは、ゲートウェイのレスポンスも設定しないと、CORSエラーが発生するため、設定しましょう。右のゲートウェイレスポンス → DEFAULT 4XX編集の順番にナビゲートします

api5a.PNG

  • レスポンスヘッダーに下記の値を追加し、保存します。
  Access-Control-Allow-Origin: '*'

こんな感じです。
api5b.PNG

これでAPIの構築は完了です。

4. SES送信用のメール検証登録

mail.PNG

verifiedの状態になったら、OKです!

5.全コンポーネントを連携する

  • 最後のステップではすべてを繋ぎます!index.htmlと同じいフォルダでscript.jsを作成し、下記のコードを貼り付け、var URLurlの2ヶ所に先ほど設定したAPI GatewayのエンドポイントURLを差し替えます。
script.js
function submitToAPI(e) {
    e.preventDefault();
    //設定したAPI GatewayのエンドポイントURLをここに入れます。
    var URL = "API-GATEWAY-ENDPOINT-URL";

    //フォームの入力値をチェック
    var name = /[A-Za-z]{1}[A-Za-z]/;
    if (!name.test($("#name-input").val())) {
        alert("2文字以上記入してください。");
        return;
    }
    var subject = /[A-Za-z]{1}[A-Za-z]/;
    if (!subject.test($("#subject").val())) {
        alert("2文字以上記入してください。");
        return;
    }
    if ($("#email-input").val() == "") {
        alert("メールを入力してください。");
        return;
    }

    var email = /^([\w-\.]+@([\w-]+\.)+[\w-]{2,6})?$/;
    if (!email.test($("#email-input").val())) {
        alert("メールアドレスは正しくありません。");
        return;
    }

    var name = $("#name-input").val();
    var subject = $("#subject").val();
    var email = $("#email-input").val();
    var desc = $("#description-input").val();
    var data = {
        name: name,
        subject: subject,
        email: email,
        desc: desc
    };

    $.ajax({
        type: "POST",
        //設定したAPI GatewayのエンドポイントURLをここに入れます。
        url: "API-GATEWAY-ENDPOINT-URL",
        dataType: "json",
        crossDomain: "true",
        contentType: "application/json; charset=utf-8",
        data: JSON.stringify(data),


        success: function() {
            // フォームをクリアし、送信成功のメッセージを表示する
            alert("メッセージが送信されました!");
            document.getElementById("contact-form").reset();
            location.reload();
        },
        error: function() {
            // 送信エラーのメッセージを表示する
            alert("メッセージ送信失敗!");
        }
    });
}

6. 結論

これでAmazon S3で静的Webサイトをホストしても、AWSアーキテクチャを利用し、ユーザよりサイト管理者へフォーム送信機能を実装できました!

初投稿です、、いかがでしょうか(´・ω・`)
コメント、フィードバックなど是非よろしくお願いいたします!
それでは、また今度! (`・∀・´)ノ

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

AWS Lambda、API Gateway、SESを使用し、S3静的ウェブサイトの動的な連絡フォームを作りましょう!?

AWS S3にサイトを安くて簡単にホスティングできますが、S3は静的なサイトしかホスティングできないですので、従来ならバックエンド処理が必要となるフォーム送信の実装などは難しそうですね。しかし、クラウド時代ではそんな心配はありません!ソリューションはサーバーレスアーキテクチャです!AWSはすでに色々なサーバーレス構築のツールを提供しています。

今回はユーザからお問い合わせやフィードバックを送信するたびに、任意な宛先に通知メールを送信するシンプルなサーバーレスメールサービスを一緒に作ります。AWS LambdaとAPI Gatewayを利用し、簡単なAWSサーバーレス構築を紹介します。

構成図はこんな感じです。
diagram.PNG

処理の流れとしては、ユーザーから連絡フォームより入力した情報を収集し、クライアント側のブラウザからAmazon API Gateway RESTfulサービスに投稿します。Amazon API Gatewayは、収集されたユーザー情報をAWS lambda関数に渡します。AWS Lambda関数は、Eメールを自動生成し、Amazon SESを使用してメールサーバーに転送します。

1.フォームを作成

  • index.htmlファイルを作成し、下記のコードを貼り付けます。
index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>連絡フォーム</title>
    <script src="script.js"></script>
    <link rel="stylesheet " href="style.css ">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js "></script>

</head>


<body>
    <h1>連絡フォーム</h1>
    <form id="contact-form" method="post">
        <h4>名前:</h4>
        <input type="text" id="name-input" placeholder="お名前を入力してください。" class="form-control" /><br />
        <h4>件名:</h4>
        <input type="subject " id="subject " " placeholder="お名前を入力してください。 " class="form-control " /><br />
        <h4>メール:</h4>
        <input type="email " id="email-input " placeholder="メールを入力してください。 " class="form-control " /><br />
        <h4>メッセージ:</h4>
        <textarea id="description-input " rows="3 " placeholder="メッセージを入力してください。 " class="form-control "></textarea><br />
        <div class="button-wrapper "><button type="button " onClick="submitToAPI(event) " class="btn btn-lg ">送信</button></div>

    </form>
</body>
</html>

ブラウザでプレビューを確認できます。こんな感じです。
basic-html-form.PNG

  • 見た目はあまり良くないですので、改善しましょう。style.cssを同じフォルダ内で作成し、下記のコードを貼り付けます。
style.css
* {
    box-sizing: border-box;
}

body {
    text-align: center;
    color: rgb(255, 255, 255);
    background-color: #194680;
    padding-top: 20px;
}

#contact-form {
    margin: auto;
    text-align: left;
    padding: 20px;
    max-width: 430px;
    border: white solid 2px;
    border-radius: 10px;
}

button {
    text-align: center;
    margin-top: 20px;
    font-size: 18px;
    border: white solid 2px;
    background-color: white;
    border-radius: 5px;
}

.button-wrapper {
    text-align: center;
}

input,
textarea {
    width: 380px;
}

textarea {
    height: 200px;
    padding: 10px;
}

input {
    height: 35px;
}

ブラウザではこんな感じになります。
styled-form.PNG

2. AWS Lambda関数の定義

  • AWS Lambda画面で新規関数を作成し、hello-worldのnodejsテンプレを選択します。
    lambda2.PNG

  • 基本的な情報の画面では関数を任意に名付けて、実行ロールは最初選択のままで進みます。
    lambda3.PNG

  • 次の画面の関数コードに下記のコードを切り替えます。

index.js
var AWS = require('aws-sdk');
var ses = new AWS.SES();

//宛先のメールアドレスを入力してください。
var RECEIVER = 'example-receiver@gmail.com';

//送信先のメールアドレスを入力してください。
//送信先のメールはAWSに認証登録する必要があります。
var SENDER = 'example-sender@gmail.com';

//クライアントへのレスポンスヘッダーを設定
var response = {
    "isBase64Encoded": false,
    "headers": {
        "Content-Type": 'application/json',
        "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Accept",
        "Access-Control-Allow-Methods": "POST,GET,OPTIONS",
        "Access-Control-Allow-Origin": "*",
    },
    "statusCode": 200,
    "body": "{\"result\": \"テスト成功です!\"}"
};

exports.handler = function(event, context, callback) {
    console.log('受理したイベント:', event);
    sendEmail(event, function(err, data) {
        context.done(err, null);
    });
    callback(null, response);
};

//メール送信処理
function sendEmail(event, done) {
    var params = {
        Destination: {
            ToAddresses: [
                RECEIVER
            ]
        },
        Message: {
            Body: {
                Text: {
                    Data: '● お名前: ' + event.name + '\n● 件名: ' + event.subject + '\n● メール: \n' + event.email + '\n● 内容: \n' + event.desc,
                    Charset: 'UTF-8'
                }
            },
            Subject: {
                Data: event.name + 'からの連絡フォームが来ました!',
                Charset: 'UTF-8'
            }
        },
        Source: SENDER
    };
    ses.sendEmail(params, done);
    return {
        response,
        body: JSON.stringify(params),
        statusCode: 200
    }
}
  • 同じLambda管理画面で、上にスクロールし、アクセス権限のタブに切り替え、ロール名のリンクをクリックします。

lambda-policy.PNG

  • 当関数のIAMポリシーをSES権限を追加します。IAM画面で該当ポリシーをクリックしポリシーを編集 ->JSONのフォーマットに切り替えし、下記のコードを追加します。

lambda-policy3.PNG

{
  "Sid": "FormPolicy",
  "Effect": "Allow",
  "Action": "ses:SendEmail",
  "Resource": "*"
}

こんな感じです。
lambda-policy2.PNG

3. APIゲートウェイでAPI構築

  • AWSメインコンソールよりAPI Gateway管理画面にアクセスし、APIを作成をクリックします。
    api.PNG

  • 次の画面ではREST API構築をクリックします。

api2.PNG

  • 次の画面はREST APIの選択のままで、API名前を任意に付けます。

api2a.PNG

  • 次の画面は上にあるアクションをクリックし、メッソド作成をクリック、POSTを選択します。

api3.PNG

  • 右側の設定画面では、統合タイプLambdaで、Lambda関数は先ほど作成したものを選択します。

api4b.PNG

  • CORSエラーにならないように、CORS機能も有効にします。アクションのドロップダウンをCORS有効化をクリックし、設定はそのままで保存します。

api4c.PNG

  • API Gatewayの最新アップデートでは、ゲートウェイのレスポンスも設定しないと、CORSエラーが発生するため、設定しましょう。右のゲートウェイレスポンス → DEFAULT 4XX編集の順番にナビゲートします

api5a.PNG

  • レスポンスヘッダーに下記の値を追加し、保存します。
  Access-Control-Allow-Origin: '*'

こんな感じです。
api5b.PNG

これでAPIの構築は完了です。

4. SES送信用のメール検証登録

mail.PNG

verifiedの状態になったら、OKです!

5.全コンポーネントを連携する

  • 最後のステップではすべてを繋ぎます!index.htmlと同じいフォルダでscript.jsを作成し、下記のコードを貼り付け、var URLurlの2ヶ所に先ほど設定したAPI GatewayのエンドポイントURLを差し替えます。
script.js
function submitToAPI(e) {
    e.preventDefault();
    //設定したAPI GatewayのエンドポイントURLをここに入れます。
    var URL = "API-GATEWAY-ENDPOINT-URL";

    //フォームの入力値をチェック
    var name = /[A-Za-z]{1}[A-Za-z]/;
    if (!name.test($("#name-input").val())) {
        alert("2文字以上記入してください。");
        return;
    }
    var subject = /[A-Za-z]{1}[A-Za-z]/;
    if (!subject.test($("#subject").val())) {
        alert("2文字以上記入してください。");
        return;
    }
    if ($("#email-input").val() == "") {
        alert("メールを入力してください。");
        return;
    }

    var email = /^([\w-\.]+@([\w-]+\.)+[\w-]{2,6})?$/;
    if (!email.test($("#email-input").val())) {
        alert("メールアドレスは正しくありません。");
        return;
    }

    var name = $("#name-input").val();
    var subject = $("#subject").val();
    var email = $("#email-input").val();
    var desc = $("#description-input").val();
    var data = {
        name: name,
        subject: subject,
        email: email,
        desc: desc
    };

    $.ajax({
        type: "POST",
        //設定したAPI GatewayのエンドポイントURLをここに入れます。
        url: "API-GATEWAY-ENDPOINT-URL",
        dataType: "json",
        crossDomain: "true",
        contentType: "application/json; charset=utf-8",
        data: JSON.stringify(data),


        success: function() {
            // フォームをクリアし、送信成功のメッセージを表示する
            alert("メッセージが送信されました!");
            document.getElementById("contact-form").reset();
            location.reload();
        },
        error: function() {
            // 送信エラーのメッセージを表示する
            alert("メッセージ送信失敗!");
        }
    });
}

6. 結論

フォームより受信するメールはこんな感じです。
demo.PNG

これでAmazon S3で静的Webサイトをホストしても、AWSアーキテクチャを利用し、ユーザよりサイト管理者へフォーム送信機能を実装できました!

初投稿です、、いかがでしょうか(´・ω・`)
コメント、フィードバックなど是非よろしくお願いいたします!
それでは、また今度! (`・∀・´)ノ

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

Amazon Chime SDK で16ビデオ通話セッションを超えてみる。

この記事はこちらでも紹介しています。
https://cloud.flect.co.jp/entry/2020/07/08/153456

前回は、Amazon Chime SDKの新機能を使って作ったホワイトボード機能についてご紹介しました。

https://qiita.com/wok/items/83a8700d5c09cc1747d0

今回もAmazon Chime SDKについての話になります。
少しトリッキーな使い方を実験をしてみたのでご紹介したいと思います。

具体的には、単一のSPA(Single Page Application)から複数の会議室に同時に参加できるかを実験しました。
これができれば、次のことが可能になると考えています。

  • (オンラインセミナーなどで、)Amazon Chime SDKの会議室あたりの最大接続数を超える人数に向けた講義が可能になる。
  • (オンラインセミナーなどで、)グループ単位で受講者のプライバシーを保護できる。

実験の結果、次のデモのように、単一のユーザ(登壇者)が複数の会議室に参加することで、会議室あたりのビデオ通話可能な最大ユーザ数(16名)を超えるユーザに対し、同時にビデオ通話ができることが確認できました。
また、登壇者以外の各会議室の参加者(受講者)間の会話や映像は他の会議室には伝わらないため、会議室間でプライバシーも保護することができます。

ezgif-5-2cd8468121a2.gif

このデモでは登壇者が2つの会議室に参加することで、30名のユーザとビデオ通話しています。1

Amazon Chime SDKのビデオ通話セッション数と、大規模なオンラインセミナー

私が所属する会社は、AWS, GCP, Azureを代表とするマルチクラウドを扱えることを強みとしたクラウドインテグレータです。
当然、オンライン会議システムのソリューションを提案する場合も、Amazon Chime SDKと他のオンライン会議システムの強み弱みを比較検討するわけですが、
Amazon Chime SDKは会議室あたりのビデオ通話の最大参加者数(セッション数)が、例えばZoomのビデオウェビナープランなどの100名と比較して、16名と少ないです(2020年7月現在)。2
とはいえ、Amazon Chime SDKには、オンライン会議への接続形態として、下記の2パターンあり音声通話参加者は250名まで接続可能です。

image.png

そして、必要に応じてオンラインのまま、16のビデオ通話セッションを音声通話参加者に割当直してビデオ通話参加者にすることができます(下図)。
つまり、例えば発言中のユーザにビデオ通話セッションを割り当て直して、顔を映すということができます。

対面の講演やセミナーを考えると、質疑の場面以外では受講者が他の受講者の顔を見ることは、あまり多くないと思います。
なので、発表中は登壇者にビデオ通話セッションを割り当てて、質疑の際に質問者(特定の受講者)にビデオ通話セッションを割り当てるという構成にすれば、
Amazon Chime SDKのビデオ通話セッション数でも十分な場合も多いのではないかと考えられます。
image.png

しかし、音声通話参加者が250名を超える場合や、下図のように登壇者側が16名以上の受講者の反応をビデオ映像で見たい場合などには、普通にAmazon Chime SDKを使うだけでは対応できません。
もし、受講者を複数の会議室に分散して参加させ、それらの会議室に登壇者が同時に参加できるようになれば、これを解決できると思われます。
image.png

なお、少し横道にそれますが、16名を超える登壇者がいる場合はAmazon Chime SDKだと対応しきれないかもしれません。
この場合でも適宜ビデオ通話セッションを割り当て直せば十分なことも多いとは思いますが、
討論会でパネリスト(≒登壇者)のリアクションを常時ビデオ映像として配信したいといった要件がある場合は対応が難しいと思われます。

グループで参加したいオンラインセミナー

また、私個人が経験したことではありますが、オンラインの勉強会に参加したときに、発言するとそれが全体に伝わるので躊躇してしまうことがありました。
簡単な理解の確認であれば、横にいる同僚や知り合いにコソコソ話をして確認したいところですが、オンラインの勉強会だとslackなど別のコミュニケーションツールを使って会話をする必要があって面倒です。
最悪の場合、別のツールなのでこちらが話しかけても気づいてもらえない場合があります(涙)。
オンラインセミナーの受講にグループで参加して、受講中はグループ内だけでコミュニケーションが取れる方法があればありがたいと思いました。

また、セミナーの内容によっては、遠方の家族、親戚も一緒にオンラインセミナーで勉強したいが、他人には参加してることが知られたくないようなものがあるかもしれません。
例えば、医療に関わる話であったり、受験や結婚、資産運用などがそれに当たるかもしれません。
こういったものは、シビアな内容のものが多いので家族間で適宜感想を確認しながら受講するということが多いのではないかと思います。

これらについても、下図のように家族やグループ毎に会議室を割り当てて、それらの会議室に登壇者が同時に参加できるようになれば、会議室内の会話や映像は他の会議室には伝わらないので、解決できそうです。
image.png

実装のポイント

ということで、今回はSPAで上記のような構成を作れるかを実験してみました。
今回の実験の内容を含むデモプログラム全体は下記のgitリポジトリにアップしてありますので、ポイントだけ確認していきます。

といっても、特に難しいところはありません。
基本的には単一の会議室に入る処理を繰り返し、それぞれのエンドポイントや参加者を管理しておくようにしておくだけでできます。

たとえば、今回のデモプログラムでは、参加中会議室をそれぞれJoinedMeetingというInterfaceで管理をしています。
このInterfaceには、会議室毎に作成した、MeetingSessionConfigurationMeetingSessionに加え、参加者情報やビデオタイルといった各会議室固有の情報を格納します。

export interface JoinedMeeting{
    meetingSessionConfiguration: MeetingSessionConfiguration,
    meetingSession: MeetingSession,
    roster: { [attendeeId: string]: Attendee },
    videoTileStates: { [id: number]: VideoTileState },
    outputAudioElement: HTMLAudioElement | null,
    <snip>
}

これをSPA全体で複数管理できるようにしてあげます。
今回のデモプログラムではmeetingIdをキーとした連想配列で管理しています。

export interface AppState {
    joinedMeetings : {[id:string]:JoinedMeeting},
    <snip>
}

そして、新しく会議室に参加するたびに、その会議室のMeetingSession.audioVideoからビデオ入力やマイク入力、スピーカーを設定すればよいです。
デバイスの選択し直しの場合も、同様にすべての参加中の会議室のMeetingSession.audioVideoから設定してあげればよいです。
例えば、マイク入力を切り替える場合は次のような感じになります。

    selectInputAudioDevice = (deviceId: string) => {
        const currentSettings = this.state.currentSettings // <- 使用するデバイスなどをグローバルに管理している前提。
        currentSettings.selectedInputAudioDevice = deviceId

        Object.keys(this.state.joinedMeetings).forEach((x:string)=>{
            this.state.joinedMeetings[x].meetingSession.audioVideo.chooseAudioInputDevice(deviceId); // <- ここで設定。
        })
        this.setState({ currentSettings: currentSettings })
    }

その他、考慮が必要なポイント

基本的には上記の通りで良いのですが、複数の会議室に入ると、その分の映像を送受信するデータ量が増加します。
これらについては、Amazon Chime SDKのAPIで送信データ量をコントロールできるようになっているので、そこで調整出来るようにしておくと良いと思います。
特に受講者側は、デフォルトで解像度を下げておくほうが良いかもしれません。


    selectInputVideoResolution = (value: string) =>{
        <snip>
        Object.keys(this.state.joinedMeetings).forEach((x:string)=>{
            this.state.joinedMeetings[x].meetingSession!.audioVideo.chooseVideoInputQuality(
                videoConfig.width, videoConfig.height, videoConfig.frameRate, videoConfig.maxBandwidthKbps // videoConfigで解像度やフレームレート、最大データ送信量が設定してある前提
            );
        })      
        <snip>
    }

FLECT Amazon Chime Meeting

今回説明した機能は、FLECT研究開発室が開発しているビデオ会議を使った新機能のテストベッドに組み込まれています。
下記のリポジトリに公開していますので、ご興味を持たれましたらアクセスしてみてください。

https://github.com/FLECT-DEV-TEAM/FLECT_Amazon_Chime_Meeting

最後に

今回は、Amazon Chime SDKを使ったSPAから複数の会議室に参加できるかを実験してみました。
ネットワークなど計算リソースに関して考慮すべき点は残っていますが、大枠としては可能であることがわかりました。
これにより、複数の会議室から大規模なオンラインセミナーの会場を構成できる見込みを得ることができました。
また、大規模な会場においてグループ間でのプライバシーを保護できる見込みも得ることができました。

弊社FLECTでは、Amazon Chime SDKを用いてこれらの機能を組み込んだシステムの提案、構築をしております。
興味をお持ちいただけたら是非ご相談ください。

次回は、またAmazon Chimeで遊ぶか、以前紹介したマルチバーコードリーダの技術的な内容をご紹介するかをしようと思ってます。
では。

参考・リソース

会議室の参加者のカメラ画像は、次のサイトの動画を用いて擬似的に作成しました。

https://pixabay.com/ja/videos


  1. このデモでは、動画をダミーのカメラデバイスに流し込むことで30名のユーザ(受講者)を模擬している。 

  2. Zoom [https://zoom.us/jp-jp/webinar.html:embed:cite

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

現代フロントエンド開発シリーズ(9) - UI設計は本当に簡単ですか?UIの状態について話す

一般に、UIを設計する場合、モックアップの外観だけがあったため、Webページの設計時に注意すべきところがミスする点がよくあります。UIデザイナーとグラフィックデザイナーを区別する良い方法です。

どうしてそんなこと言いますか?

一般的なグラフィックデザインとは異なり、Webページはスクロール、マウスの移動、コンテンツの長さの変更、端末の使い、幅と高さなど、さまざまな原因でレイアウトが変わることがあります。したがって、UIを設計するのも決して簡単ではありません。

ここでは、さまざまなパターンを1つずつ紹介することはできませんが、いくつかのアイディアを説明します。

状態

UIを設計するときは、状態から始めることが多いです

空っぽい状態

テキスト、タイトル、画像などない状態ではどう表示するか。ユーザーがアカウントを作成したばかりで、情報のないページがたくさんあります。その場合はUIの表示はどうなりますか?

多くのデザイナーはこれらの状況を考慮しないことがよくあり、モックアップは理想的なUI表示で作ってしまった場合があります。開発者も注意しないと、そのままモックアップを実装され、本番環境で違った表示してしまう場合があります。この方法では、実際のアプリケーションに適用できません。

一般的なステータス

通常の状態で、UIはどう表示するのか

まれな状態

まれな状態とは、通常の状況ではまれであるが、存在している状態です。たとえば、タイトルが長すぎる場合はどのように表示しますか?直接改行しますか?または、代わりに「...」を使用しますか?または、2行後に「...」を使用しますか?

もしくは実際の画像に対する画像の比率が正しくない場合はどうなりますか?たとえば、カードデザインの画像比率は16:9ですが、一部のユーザーの画像は1:1、9:16でもあります。直接カットしますか、それとも他の方法を使いますか?

テキストレイアウトの処理

ブラウザでは、 rememを使って、直接書き込むことなく相対フォントサイズでデザインしています。SCSSまたはその他のツールを使って、UIをより体系的に管理できます。

また、テキストを異なるデバイスで比例的にスケーリングする必要があるかどうか、または個別に異なるフォントサイズを使用する必要があるかどうかを検討する必要があります。

空白

空白のシステムを管理する方法は非常に重要です。適切なギャップがあると、UIの一貫性が向上し、管理しやすくなります。たとえば、空白のシステムのセットを定義し、タイトルとタイトルの間のギャップ、カードとカードの間のギャップなどを定義することで、UIの一貫性が向上することができます。

読み込みステータス

データがロードされていない場合は、Placeholderを使用して実行できます。最近、いくつかの偽のUIを作成してそこに配置することがトレンドでしたが`background-positionは実際にレイアウトとペイントを発火します。あまりにも多くは良いことではないので、代わりにスピナーやgifなど使えばよいと思います

エラーが発生した場合はどうすればよいですか?

  • UIは400、404、401、402を処理できますか?ユーザーがフォームを入力している場合、ポップアップなどの表示とかしましたか?データはlocalStorageにもしくは他の方法で保存しましたか?データが直接なくなった場合、ユーザーが落ち込むことが多いので注意が必要です。

  • 入力の検証は正しいですか?(対応するエラーメッセージを設計したり、考えられるエラーをカバーしたりしていますか?たとえば、パスワードの長さ、電子メールの長さ...)

  • エラーメッセージは十分気づかれますか?

    • ユーザーは簡単にエラーを発見できますか?
    • ユーザーが次のステップに進むことができるように、完全な情報とフォローアップができますか?
  • APIが呼び出されたときにUIがすぐに応答できますか?

    • リクエストを呼び出すとき、ロード中、保留状態とかUIに反映していますか?

結論

この記事ではUI設計上いくつ考えなければならないことをまとめました。ご参考になれば幸いです。

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

html周りのエラー

メモ代わり

html周り弄ってた時のメモとして。
javascriptやらphpも込み込み(の予定)

javascriptでjsonが読み込めない(404エラーを吐かれる)

原因
知らん!
最初はjquery使えないせいか?とも思ったがそうでもなかったし。
ただ自前のサーバーで全く同じコードでやった時はエラー吐かれなかったからサーバの設定あたりの問題かと。(断定ではない)
オチ
jsonファイルをテキスト(.txt)に変換してそれを読み込ませたところ、問題なく動作した。

sample.js
function sample()
{
    $.getJSON("latest_data.txt",(data) =>
    {/* 処理あれやこれや*/});
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

typescriptでJavaScriptライブラリを使う方法 ②

本記事は前回の記事の続きになります。

前回の記事はこちらです。

さて、今回はデクラレーションファイルのないサードパッケージ製パッケージをtypescriptで使用する方法について紹介します。

誰かが既に作成していないかを確認する。

今回はlodashを使用します。

下記のコマンドでインストールします。

npm install lodash

そして、tsファイル上でimportします。

library.ts
import _ from 'lodash';

すると、エラーが出るはずです。

node_modulesのディレクトリの中のlodashの中身を見てみましょう。

デクラレーションファイルがありませんね。

こういう時に一番最初に試して欲しいのが、

既存の型定義ファイルがないかを確かめることです。

確かめる方法は、「@types/lodash」でGoogle検索してみましょう。
スクリーンショット 2020-07-08 13.52.49.png
出てきましたね。

実際に開き、githubのリンクがあるのでクリックすると、

DefinitelyTypedというリポジトリがあります。
スクリーンショット 2020-07-08 13.57.56.png

このリポジトリは、typescriptの公式のリポジトリで、開発者以外でもコミットできるようになっています。(プルリクエストはtypescriptの開発チームが毎回確認しているので安全性は高いです)

このDefinitelyTypedというリポジトリの中のtypesディレクトリの中にデクラレーションファイルが入っています。

なので誰かが既に型定義ファイルを作成していてば使用できるということになります。

それでは、使い方について説明していきます。

下記のコマンドでインストールできます。

npm install --save-dev @types/lodash

問題なくインストールできればエラーが消えるはずです。

試しに使ってみましょう。

library.ts
import _ from 'lodash';
_.shuffle([1,2,3,4])

エラーが出ていないと思います。

以上がデクラレーションファイルのないサードパッケージ製パッケージをtypescriptで使用する方法です。

この方法を使えば、ほとんどのサードパッケージ製パッケージを使うことができますので覚えておきましょう。

余談

先ほどのコマンドでインストールしたデクラレーションファイルはどこに保管されるのでしょうか。

node_modulesディレクトリのnodeディレクトリに保管されます。
スクリーンショット 2020-07-08 14.11.12.png

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

PuppeteerでFirefoxを利用するための知見

前置き

Puppeteerでは現在(2020/07/08)、Chrome(Chromium)に加えFirefoxでも動作させることができます。
簡単ですが上手く動作させることができましたので、それまでの知見をまとめます。

検証環境

Windows 10 Pro 1903
puppeteer @4.0.0
Firefox Nightly 80.0a1

準備

  1. Puppeteerのインストール(詳細省略)
  2. Firefox Nightlyのインストール プレリリース版ダウンロードサイト

Firefoxは通常版だとダメで、ブラウザを起動させた後に処理が止まりタイムアウトしてしまいます。
Puppeteerは3.0以上で対応しているようです。1

ソースコード(抜粋)

const browser = await puppeteer.launch(
    {
        product: 'firefox',    // 1. 'chrome' or 'firefox'
        executablePath: 'C:\\Program Files\\Firefox Nightly\\firefox.exe',    // 2. Firefox(Nightly)の実行パス 
        userDataDir: 'C:\\work\\Profile_FirefoxNightly',    // 3. ユーザープロファイルの保存ディレクトリパス
        args: ['-wait-for-browser']    // 4. Firefox(Nightly)の起動を待つ
    }
);
  1. 現在は'chrome'か'firefox'のみ指定できます。chromeはデフォルトなのでFirefoxで動作させたいときのみ必須です。launchメソッドの引数としてではなく、環境変数でも'PUPPETEER_PRODUCT'の変数名でも登録して設定することが可能のようです。
  2. 実行パスは絶対パスでも相対パスでも記述可能らしいです。自分でインストールしたchromeを使用する場合にもこの項目を使用します。
  3. ユーザープロファイルの保存ディレクトリパスを指定しないと、ブラウザの起動はできますが終了時(await browser.close();)にエラーが発生します。指定するディレクトリは空で良いですが、初回起動時に「既定のブラウザに設定するか」などのダイアログが出るので、本利用前に一度起動してもろもろ設定しておいた方が良いです。
  4. -wait-for-browserを指定しないと、ブラウザの起動途中で処理が進んでしまい、エラーになります。-wait-for-browserはFirefox用のオプションのようです。

詳細はPuppeteer公式ドキュメントのpuppeteer.launch([options])の項を参照ください。
Puppeteer API v5.0.0

メモ

一度の処理の中でawait puppeteer.launchを複数回行う場合、product:オプションは一度設定すると後のすべてで有効になるようです。
それに対しexecutablePath:userDataDir:args:オプションは毎回設定することが必要です。
product:オプションは環境変数に設定したのと同じような扱いということなのかも?
一度の処理の中でChromeとFirefoxの両方を使用したい場合は都度product:オプションを指定すれば大丈夫なようです。

その他

Puppeteerが公式でFirefoxをサポートしたことにより、puppeteer-firefoxはdeprecatedになったようです。2

PlaywrightというChromiumとFirefoxに加えWebKitもサポートしている同様のツールがあるそうです。3
参考:Playwrightも知らないで開発してる君たちへ

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

Array.includesと Set.has の速度比較をした。

Array.includesとSet.hasで速度差がどれぐらい出るかを調べた。
良くあるObjectの添字を使ったテクニック等とも比較。

const { performance } = require('perf_hooks');

const test = (len=100) => {

    const targetLen = Math.floor(len /10)

    const  shuffle = (a) => {
        const c = [...a]
        for (let i = c.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [c[i], c[j]] = [c[j], c[i]]
        }
        return c
    }

    const spec = [
        ["nodeのバージョン",process.versions.node],
        ["配列のサイズ" , len ],
        ["検索する項目数" , targetLen ]
    ]


    //配列をlen個作成する [0,1,2,3,4,...,len-1]
    const base = Array.from({length: len}, (v, k) => k)

    //シャッフルする
    const ary = shuffle(base)

    //Set化する
    const set =(() => {
        const start = performance.now()
        const set = new Set(ary)
        const end =  performance.now()
        spec.push([ "Setの生成時間" , end - start])
        return set
    })()

    //Mapの生成
    const map = (() => {
        const pre = base.map(b => [b,b])
        const start = performance.now()
        const map = new Map(pre)
        const end = performance.now()
        spec.push([ "Mapの生成時間" , end - start ])
        return map
    })()

    //数字を添え字にしたオブジェクトを作る {"0":0,"1":1,...,[len-1]:len-1}
    const obj = (() => {
        const start = performance.now()
        const obj = ary.reduce((pre,cur) => {
            pre[`${cur}`] = cur
            return pre
        }, {})

        const end = performance.now()
        spec.push([ "Objectの生成時間" , end - start ])

        return obj
    })() 



console.log(`

${spec.map(r => r.join("|")).join("\n")}

`)


    //存在チェックの試験用の配列
    const target = shuffle(base).slice(0,targetLen)

    const result = [
        ["含むチェックの方式","処理時間時間"],
    ]



    //indexOf を使う
    {
        const start = performance.now();
        for(let t of target ){
            if(ary.indexOf(t) > -1){

            }
        }
        const end = performance.now();

        result.push([ "Array.indexOf" , end - start])
    }


    //Array.inclueds のテスト
    {
        const start = performance.now();
        for(let t of target ){
            if(ary.includes(t)){

            }
        }
        const end = performance.now();

        result.push([ "Array.inclueds" , end - start])
    }

    //Set.has のテスト
    {
        const start = performance.now();
        for(let t of target ){
            if(set.has(t)){

            }
        }
        const end = performance.now();    
        result.push([ "Set.has" , end - start])
    }


    //Map.has のテスト
    {
        const start = performance.now();
        for(let t of target ){
            if(map.has(t)){

            }
        }
        const end = performance.now();    
        result.push([ "Map.has" , end - start])
    }


    //オブジェクトの key を使ったテクニック のテスト

    {
        const start = performance.now();
        for(let t of target ){
            if(obj[t]){

            }
        }
        const end = performance.now();    
        result.push([ "obj[t]" , end - start])
    }


    // in でも試してみる

    {

        const start = performance.now();
        for(let t of target ){
            if( t in obj){

            }
        }
        const end = performance.now();    
        result.push([ "t in obj" , end - start])

    }


console.log(`

${result.map(r => r.join("|")).join("\n")}

`)



}

test(100000)

1回目

項目
nodeのバージョン 12.18.1
配列のサイズ 100000
検索する項目数 10000
Setの生成時間 8.558184027671814
Mapの生成時間 13.922562003135681
Objectの生成時間 31.00487995147705
チェックの方式 処理時間時間
Array.indexOf 944.757071018219
Array.inclueds 944.7714960575104
Set.has 2.41020405292511
Map.has 2.5640859603881836
obj[t] 1.2104129791259766
t in obj 0.7970190048217773

2回目

項目
nodeのバージョン 12.18.1
配列のサイズ 100000
検索する項目数 10000
Setの生成時間 8.59699296951294
Mapの生成時間 11.606724977493286
Objectの生成時間 35.41410505771637
チェックの方式 処理時間時間
Array.indexOf 945.146420955658
Array.inclueds 945.0419319868088
Set.has 2.4140039682388306
Map.has 2.5660550594329834
obj[t] 1.2223680019378662
t in obj 0.8045949935913086

3回目

項目
nodeのバージョン 12.18.1
配列のサイズ 100000
検索する項目数 10000
Setの生成時間 8.761895060539246
Mapの生成時間 11.589484095573425
Objectの生成時間 30.759024024009705
チェックの方式 処理時間時間
Array.indexOf 943.3850150108337
Array.inclueds 947.2091970443726
Set.has 2.461833953857422
Map.has 2.6097559928894043
obj[t] 1.2358649969100952
t in obj 0.8159409761428833

結論 Setを使う。

結果的だけみると in 演算子を使うパターンが一番早いが object に一旦変換するテクニックは
生成コストが高く常に使って効果を得られるようなテクニックではないので使わないほうが良い。

また、存在チェックしたい物が stringやnumber以外の場合は使えない。

Setは生成コストはそれほど高くなく、検索も早いので、状況によっては含むチェックのためだけに 配列から一旦Setを生成する価値はありそう。

Set.has と Map.hasはスピードにあまり差が無い。(MapがSetに依存してるのかな?

indexof と includes は速度に差は無く順当に遅い

あまり不思議な点はなかった。

ちなみに Map の key が オブジェクトでも良いって最近知った。

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

TypeScript4.0でNull合体代入演算子(??=)が導入予定です

はじめに

TypeScript 4.0 Betaが2020/07/07にリリースされました。

この記事はリリースノートの中で個人的に気になった機能、Null合体代入演算子について共有するためのものです。

おことわり

この記事はBeta版に関するものです。正式リリース時に内容が変更になる可能性があります。

Nullish Coalescing Assignment Operator

TypeScript4.0では、Null合体代入演算子(Nullish Coalescing Assignment Operator)が導入される予定です。

Announcing TypeScript 4.0 Beta

この演算子を議論しているIssueはこちらです。

issue #37255 : Nullish Coalescing and Logical Compound Assignments

ECMAScriptのproposalはstage3です。

proposal-logical-assignment

機能

この演算子は、その名の通りTypeScript3.7で導入されたのNull合体演算子??と代入を組み合わせた演算子です。

たとえば、以下の2行はまったく同じ意味になります。

obj.x ??= 42;
obj.x = obj.x ?? 42;
  • obj.xnullundefinedの場合のみ、右辺の値が代入されます。
  • obj.x0""falseなどのFalsyな値の場合は代入されません。

どんなときに使うのか

たとえば

  • 関数の引数にオブジェクトを使う
  • オブジェクトは複数の省略可能なプロパティを持つ
  • 省略した場合、デフォルト値を代入する

という場合にコードが簡潔になります。

function f(option?:
  {
    val1?: string,
    val2?: string
  }
): void {
  option ??= {};
  option.val1 ??= "default1";
  option.val2 ??= "default2";
}

参考記事

PHP: Null合体代入演算子(??=)の使い所を考えてみた

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

typescriptでJavaScriptライブラリを使う方法

今回はtypescriptでjavascriptライブラリを使う方法について紹介します。

※前提として、webpackを使用しているものとします。

今回使用するライブラリはaxiusです。

axiosの詳細についてはこちらの記事が非常に参考になります。

1.ライブラリのインストール

下記のコマンドでライブラリをインストールします。

npm install axios

2.ライブラリを使っていく

今回はlibrary.tsを新規作成し記述していきます。

library.ts
import axios from 'axios';

上記のように記述することでimportできます。

しかし、このままだとエラーが出てしまいます。

なぜエラーが出るかというと、

そもそも、ここで書いたimportはどこから読み込むのでしょうか。
スクリーンショット 2020-07-08 12.29.39.png
node_modulesディレクトリの中のaxiosを読み込みます。

解決方法を解説します。

tsconfig.jsonの中の "moduleResolution": "node",のコメントアウトをなくします。

こちらです。

tsconfig.json
 "moduleResolution": "node",

これで正しく読み込むことができます。

では、moduleResolutionとはなんでしょうか。

簡単に説明すると、moduleResolutionをnodeとしておくと、

moduleをインポートした時にnode_modulesの中を見にいくようになります。

さらに細かく説明すると、

typescriptで使う際には、上の画像の中のindex.d.tsの中を読み込みます。
(ファイル名のdはデクラレーションを意味します)
スクリーンショット 2020-07-08 13.08.24.png

中身を見てみましょう。

index.d.ts
export interface AxiosTransformer {
  (data: any, headers?: any): any;
}

export interface AxiosAdapter {
  (config: AxiosRequestConfig): AxiosPromise<any>;
}

export interface AxiosBasicCredentials {
  username: string;
  password: string;
}

export interface AxiosProxyConfig {
  host: string;
  port: number;
  auth?: {
    username: string;
    password:string;
  };
  protocol?: string;
}

export type Method =
  | 'get' | 'GET'
  | 'delete' | 'DELETE'
  | 'head' | 'HEAD'
  | 'options' | 'OPTIONS'
  | 'post' | 'POST'
  | 'put' | 'PUT'
  | 'patch' | 'PATCH'
  | 'link' | 'LINK'
  | 'unlink' | 'UNLINK'

export type ResponseType = 
  | 'arraybuffer' 
  | 'blob' 
  | 'document' 
  | 'json' 
  | 'text' 
  | 'stream'

export interface AxiosRequestConfig {
  url?: string;
  method?: Method;
  baseURL?: string;
  transformRequest?: AxiosTransformer | AxiosTransformer[];
  transformResponse?: AxiosTransformer | AxiosTransformer[];
  headers?: any;
  params?: any;
  paramsSerializer?: (params: any) => string;
  data?: any;
  timeout?: number;
  timeoutErrorMessage?: string;
  withCredentials?: boolean;
  adapter?: AxiosAdapter;
  auth?: AxiosBasicCredentials;
  responseType?: ResponseType;
  xsrfCookieName?: string;
  xsrfHeaderName?: string;
  onUploadProgress?: (progressEvent: any) => void;
  onDownloadProgress?: (progressEvent: any) => void;
  maxContentLength?: number;
  validateStatus?: (status: number) => boolean;
  maxRedirects?: number;
  socketPath?: string | null;
  httpAgent?: any;
  httpsAgent?: any;
  proxy?: AxiosProxyConfig | false;
  cancelToken?: CancelToken;
}

export interface AxiosResponse<T = any>  {
  data: T;
  status: number;
  statusText: string;
  headers: any;
  config: AxiosRequestConfig;
  request?: any;
}

export interface AxiosError<T = any> extends Error {
  config: AxiosRequestConfig;
  code?: string;
  request?: any;
  response?: AxiosResponse<T>;
  isAxiosError: boolean;
  toJSON: () => object;
}

export interface AxiosPromise<T = any> extends Promise<AxiosResponse<T>> {
}

export interface CancelStatic {
  new (message?: string): Cancel;
}

export interface Cancel {
  message: string;
}

export interface Canceler {
  (message?: string): void;
}

export interface CancelTokenStatic {
  new (executor: (cancel: Canceler) => void): CancelToken;
  source(): CancelTokenSource;
}

export interface CancelToken {
  promise: Promise<Cancel>;
  reason?: Cancel;
  throwIfRequested(): void;
}

export interface CancelTokenSource {
  token: CancelToken;
  cancel: Canceler;
}

export interface AxiosInterceptorManager<V> {
  use(onFulfilled?: (value: V) => V | Promise<V>, onRejected?: (error: any) => any): number;
  eject(id: number): void;
}

export interface AxiosInstance {
  (config: AxiosRequestConfig): AxiosPromise;
  (url: string, config?: AxiosRequestConfig): AxiosPromise;
  defaults: AxiosRequestConfig;
  interceptors: {
    request: AxiosInterceptorManager<AxiosRequestConfig>;
    response: AxiosInterceptorManager<AxiosResponse>;
  };
  getUri(config?: AxiosRequestConfig): string;
  request<T = any, R = AxiosResponse<T>> (config: AxiosRequestConfig): Promise<R>;
  get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
  delete<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
  head<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
  options<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
  post<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
  put<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
  patch<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
}

export interface AxiosStatic extends AxiosInstance {
  create(config?: AxiosRequestConfig): AxiosInstance;
  Cancel: CancelStatic;
  CancelToken: CancelTokenStatic;
  isCancel(value: any): boolean;
  all<T>(values: (T | Promise<T>)[]): Promise<T[]>;
  spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
}

declare const Axios: AxiosStatic;

export default Axios;

この中にtypescriptの型情報などが入っており、このファイルを型定義ファイルを呼びます。

これのおかげでtypescriptでも使えるようなっています。

しかし、全てのサードパッケージ製パッケージにデクラレーションファイルがあるとは限らず、ある方が少ないので注意が必要です。

ない場合にどうすれば良いかを次の記事で解説します。

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

【JavaScript】イベント

はじめに

自分の勉強用メモとして残しています。

イベントとは

JavaScriptにおける「イベント」とは、Webページ上で発生するあらゆるアクションの総称です。

例えば、ボタンのクリック、フォームへの入力、ページの読み込み、マウスポインタを要素の上に合わせるなど、様々な出来事がページ上で発生します。

イベントハンドラとは
ハンドラとは英語のhandlerであり動詞handle(扱う)から由来しています。

JavaScript様々なイベントの発生下タイミングで処理を行うことが出来ます。
例えば、ボタンがクリックされた際に画像を切り替える、ページが読み込まれた際に何かを表示させる、など。
このようなイベントの処理を行うのが「イベントハンドラ」の役目です。

「on」キーワードの隣にイベント名をくっつけたものがイベントハンドラのネーミングパターンとなっています。

イベントハンドラの一覧

イベントハンドラ 説明
onBlur ページやフォーム要素からフォーカスが外 れた時に発生
onFocus ページやフォーム要素にフォーカスが当 たった時に発生
onChange フォーム要素の選択、入力内容が変更され た時に発生
onSelect テキストが選択された時に発生
onSubmit フォームを送信しようとした時に発生
onReset フォームがリセットされた時に発生
onLoad ページや画像の読み込みが完了した時に発 生
onClick 要素やリンクをクリックした時に発生
onDblClick 要素をダブルクリックした時に発生
onKeyUp 押していたキーをあげた時に発生
onKeyDown キーを押した時に発生
onMouseOut マウスが離れた時に発生
onMouseOver マウス乗った時に発生
onMouseUp クリックしたマウスを上げた時に発生
onMouseDown マウスでクリックした時に発生
onMouseMove マウスを動かしている時に発生

参考例

onclick (要素がクリックされた時)を例に挙げてみます。

HTML属性から発火

HTML要素にはイベント属性というものがあり、そこに対応するイベントハンドラとJavaScript内の実行したい関数名を設定することで、要素に設定したイベントによって関数を実行できます。

html
<p id="testid" onclick="test()"> 要素がクリックされた時にイベント発火</p>
js
function test() {
  console.log("クリックされました!");
}

イベントリスナーから発火

同じ要素の同じタイプのイベントに複数の処理の登録が出来ないので、
イベントを複数指定したい場合はこちらの方法を使いましょう。

JS側で取得した要素.addEventListener(イベント名, 関数);

html
<p id="testid"> 要素がクリックされた時にイベント発火</p>
js
var elem1 = document.getElementById("testid");
elem1.addEventListener("click", function(){
  console.log('クリックされました!(イベントリスナー)');
});

該当要素をクリックして、クリックしたタイミングでコンソールログが表示されれば、成功です。

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

簡易HTTPサーバーでブラウザからファイルの入出力を試す

ローカルでスクリプトを動かして処理結果を保存するのと似たような感覚でブラウザからファイルを保存したかったので、Deno でファイルの入出力を仲介するサーバーを作ってみました。

概要

ブラウザをスクリプトの処理系に見立てて、ローカルで動かすことを想定したスクリプトを書きます。

  • test.js

Deno でファイルの入出力を仲介する HTTP サーバーを作って、スクリプトをホスティングします。

std.http

標準ライブラリに HTTP サーバーを作るためのコードが用意されています。

ここにある file_server.ts は単独で実行することも、ライブラリとして使用することも可能です。ファイルの最後を見ると、単独で実行されたときだけ main を呼び出すようになっています。

if (import.meta.main) {
  main();
}

※ Python でよく見るやり方です。

Python
if __name__ == "__main__":
    main()

file_server.ts をライブラリとして使用すれば、レスポンスでファイルを返せます。やり方を示すサンプルが用意されています。

import { serve } from "../server.ts";
import { serveFile } from "../file_server.ts";

const server = serve({ port: 8000 });

console.log('Server running...');

for await (const req of server) {
  serveFile(req, './http/testdata/hello.html').then(response => {
    req.respond(response);
  });
}

これをベースに改造します。

await

for の中のコードを簡単にするため await で書き換えます。

  req.respond(await serveFile(req, './http/testdata/hello.html'));

serveFilereq は必要なのか?と思いましたが、コードを見ると、切断時にファイルを閉じていました。

  req.done.then(() => {
    file.close();
  });

もしここで await を使うとレスポンスが返せずに固まってしまうので、使えません。なるほど、.thenawait の使い分けが少し見えました。

server.ts

どんどん書き換えて、GET ではファイルを返して、POST では受け取ったデータをファイルとして保存するようにしました。

server.ts
import { serve } from "https://deno.land/std/http/server.ts";
import { serveFile } from "https://deno.land/std/http/file_server.ts";

const port = 8080;
const server = serve({ port: port });
console.log(`http://127.0.0.1:${port}`);

const te = new TextEncoder();
for await (const req of server) {
    const hostname = (req.conn.remoteAddr as Deno.NetAddr).hostname;
    console.log(hostname, req.method, req.url);
    if (hostname != "127.0.0.1") {
        req.respond({ status: 403 });
        continue;
    }
    let res: any = {};
    switch (req.method) {
        case "GET":
            let url = req.url;
            if (url == "/end") {
                res.body = te.encode("bye!");
                await req.respond(res);
                server.close();
                continue;
            }
            if (url == "/") url = "/index.html";
            try {
                res = await serveFile(req, url.substring(1));
            } catch (e) {
                console.log(e);
                res.status = 404;
            }
            break;
        case "POST":
            let name = req.url.substring(1);
            try {
                Deno.statSync(name);
                console.log(name, req.contentLength, "denied");
                res.status = 403;
                break;
            } catch {}
            try {
                Deno.writeFile(name, await Deno.readAll(req.body));
                console.log(name, req.contentLength, "created");
                res.body = te.encode("created");
            } catch (e) {
                console.log(e);
                console.log(name, req.contentLength, "failed");
                res.status = 403;
            }
            break;
        default:
            res.status = 403;
            break;
    }
    req.respond(res);
}
  • ローカルでの使用が目的で、外部に公開することは想定しません。
  • ローカルホスト以外からの接続は拒否します。
  • //index.html に読み替えます。
  • /end でサーバーを終了します。
  • ファイルの上書きは拒否します。そのため PUT ではなく POST を使用します。

test.js

ブラウザで動かすスクリプトです。ファイルを2つ保存します。

test.js
function log(str) {
    result.appendChild(document.createTextNode(str));
    result.appendChild(document.createElement("br"));
}

function send(method, url, body) {
    return new Promise((resolve, reject) => {
        let req = new XMLHttpRequest();
        req.open(method, url);
        req.onload = () => {
            log(`response: ${req.status} ${req.response}`);
            resolve();
        };
        if (body) {
            req.send(new Blob([body], { type: "text/plain" }));
        } else {
            req.send();
        }
        log(`${method}: ${url}`);
    });
}

(async () => {
    await send("POST", "1.txt", "hello\n");
    await send("POST", "2.txt", "world\n");
    await send("GET" , "end");
})();

onload でレスポンスを受け取ると send に対する await から抜けます。

index.html

スクリプト単独では動かないため、JavaScript を読み込むための HTML を用意します。div は結果表示用です。

index.html
<!DOCTYPE html>
<html>
    <head>
        <title>File Save Test</title>
    </head>
    <body>
        <div id="result"></div>
        <script src="test.js"></script>
    </body>
</html>

実行結果

server.ts を立ち上げて、ブラウザで http://127.0.0.1:8080 を開くと、test.js が実行されます。test.js から server.ts を終了させます。

$ ls
index.html  server.ts  test.js
$ deno run --allow-net --allow-read --allow-write server.ts
http://127.0.0.1:8080
127.0.0.1 GET /
127.0.0.1 GET /test.js
127.0.0.1 POST /1.txt
1.txt 6 created
127.0.0.1 POST /2.txt
2.txt 6 created
127.0.0.1 GET /end
$ ls
1.txt  2.txt  index.html  server.ts  test.js
ブラウザ
POST: 1.txt
response: 200 created
POST: 2.txt
response: 200 created
GET: end
response: 200 bye!

ファイルができているので、中身を確認します。

$ cat 1.txt
hello
$ cat 2.txt
world

test.js から送った内容が保存されています。

上書きは拒否するように作ったため、再度実行すると次のようになります。

$ deno run --allow-net --allow-read --allow-write server.ts
http://127.0.0.1:8080
127.0.0.1 GET /
127.0.0.1 GET /test.js
127.0.0.1 POST /1.txt
1.txt 6 denied
127.0.0.1 POST /2.txt
2.txt 6 denied
127.0.0.1 GET /end
ブラウザ
POST: 1.txt
response: 403
POST: 2.txt
response: 403
GET: end
response: 200 bye!

CORS

今回は index.html と test.js も自前でホスティングしましたが、これらを別のサイトに置いて結果だけ受け取ることも想定されます。

デフォルトではサイトをまたいだデータのやり取りは禁止されているためエラーになります。

Access to XMLHttpRequest at 'http://127.0.0.1:8080/end' from origin 'https://foobar' has been blocked
by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

サーバー側での受付自体はできているのでクライアント側で無視しても良いのですが、それでは気持ち悪いのでサーバー側でチェックしてヘッダで許可します。

server.ts(許可リストを追加)
const allowOrigins = ["https://foo", "https://bar"];
server.ts(switchの前に追加)
    res.headers = new Headers();
    if (req.headers.get("sec-fetch-site") == "cross-site") {
        const origin = req.headers.get("origin");
        if (origin && allowOrigins.indexOf(origin) >= 0) {
            res.headers.set("Access-Control-Allow-Origin", origin);
            res.headers.set("Access-Control-Allow-Methods", "POST,OPTIONS");
            res.headers.set("Access-Control-Allow-Headers", "Content-Type");
        } else {
            req.respond({ status: 403 });
            continue;
        }
    }

POST の前に OPTIONS のチェックが来るので対応します。要求を受け付けてヘッダだけ返します。

server.ts(switchの中に追加)
        case "OPTIONS":
            break;

感想

Deno の前身である Node.js の頃から言われていたことですが、やはりサーバーとクライアント(ブラウザ)を同じ言語(JavaScript)で書けるのは良いですね。

Node.js は非同期が面倒であまり使わなかったのですが、Deno は Promise ベースで async/await が使えるため、コードも読みやすくなっています。

まだ Deno の情報はあまり多くありませんが、標準ライブラリはそれほど規模が大きくないので、ヒントになりそうなコードを探して読むような進め方をすることになりそうです。

Go との関係

何となく雰囲気が Go に似ていると感じたのですが、実際、標準ライブラリは Go を意識しているようです。

deno_std is a loose port of Go's standard library. When in doubt, simply port Go's source code, documentation, and tests. There are many times when the nature of JavaScript, TypeScript, or Deno itself justifies diverging from Go, but if possible we want to leverage the energy that went into building Go. We generally welcome direct ports of Go's code.

DeepL 翻訳

deno_std は、Go の標準ライブラリの緩やかな移植版です。疑問がある場合は、Goのソースコード、ドキュメント、テストを移植してください。JavaScript、TypeScript、またはDeno自体の性質上、Goからの転用が正当化されることは多々ありますが、可能であれば、Goの構築に費やしたエネルギーを活用したいと考えています。私たちは一般的に、Go のコードを直接移植することを歓迎します。

関連記事

ブラウザで結果を表示するコードは以下の記事に由来します。

サーバーの書き方はプル型パーサーに似ていると思いました。

参考

hostname の取得で req.conn.remoteAddr as Deno.NetAddr とキャストしている部分は以下の issue を参考にしました。

以下の記事は、今回の記事とよく似たコードが出てきて興味深いです。

今回の記事とは別の Servest というライブラリを使用して、凝ったサーバーを作る記事です。

POSTPUT の違いは以下の記事を参考にしました。

XMLHttpRequest の使い方は以下の記事を参考にしました。

CORS については以下の記事を参考にしました。

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

【Vue.js】Jest+scss でユニットテスト時のスタイルのコンパイルを止めた件

Vue CLI 環境で Jest によるユニットテスト環境を構築しようとしたところ、 scss や CSS Modules との兼ね合いでうまくテストを走らせられませんでした。
スタイルに関するユニットテストをしないなら、テスト時はスタイルのコンパイルを止められるのではないかと考え調べてみたところ、実現できることがわかったのでその設定を紹介します。

動作確認した環境

  • macOS Catalina
  • Chrome 83
  • Node.js 12.18.1
  • npm 6.14.5
  • Vue CLI v4.4.4
    • vue 2.6.11
    • sass (dart-sass) 1.26.5
    • sass-loader 8.0.2
    • @vue/test-utils 1.0.3
    • @vue/cli-plugin-unit-jest 4.4.0

テスト時のスタイルのコンパイルを止める方法

vue-jest の設定で experimentalCSSCompile 1false にします:

jest.config.js
module.exports = {
  preset: '@vue/cli-plugin-unit-jest/presets/typescript',
  globals: {
    'vue-jest': {
      experimentalCSSCompile: false
    }
  }
}

ユニットテスト時のスタイルのコンパイルを止めた理由

理由1: スタイルに関するユニットテストは難しい

デザインの変更が頻繁に発生する場合、「この状態のときは○○の色が赤である」といったテストは壊れやすいのではないかと思います。
(デザイナーがわずかに色を調整するとユニットテストが通らなくなる、といったケースなど)

スタイルに関するユニットテストはしないという選択をする場合、テスト時は vue ファイルのスタイルブロックを無視できるのではないかと考えました。

理由2: scss 変数を vue ファイルのスタイルブロックに自動挿入しているケースに対応できる

sass-loaderprependData という設定2で、 vue ファイルの各 <style lang="scss"> ブロック(scoped, module 付きを含む)の先頭部分に scss 変数等を自動挿入することができます。

Vue Loader のドキュメントでも紹介されている手法で、使われている方も多いかと思います。

vue.config.js
module.exports = {
  css: {
    loaderOptions: {
      scss: {
        prependData: `$main-color: #333333;`
      }
    }
  }
}

テスト時のスタイルのコンパイルを止めない場合 jest.config.jsjest.globals['vue-jest'].resources.scssvue.config.jsprependData と近い設定ができるようですが、設定の重複管理は避けたいと思いました。

スタイルのコンパイルを止めることで、この問題を解決できました。

理由3: <style lang="scss" module> があると node-sass のインストールを求められる

テスト時のスタイルのコンパイルを止めない場合、 CSS Modules と scss を併用している vue ファイルのテストをしようとすると node-sass のインストールが必須となります:

sass(dart-sass)をインストールしている場合
 FAIL  tests/unit/example.spec.ts
  ● Test suite failed to run

    [vue-jest] Error: You are trying to use "scss". node-sass is missing.

    To install run:
    npm install --save-dev node-sass

上記のメッセージに従って node-sass をインストールすることでもこのエラーを回避できます。
しかし node-sass は OS 依存のバイナリを含んでいたり sass(dart-sass) と同じく scss をコンパイルする役割を持つパッケージのため、インストールは避けたいと思いました。

スタイルのコンパイルを止めることで、この問題を解決できました。

理由4: CSS Modules を使っていてもテンプレート上の $style.class_name で例外が発生しない

vue ファイルに <style module> ブロックがあると $style というオブジェクトが注入されテンプレート上で利用することになります。

単純に vue ファイルから <style module> ブロックを消すことを考えると、 $style のオブジェクトが注入されなくなってテンプレート上にある $style.class_name のようなプロパティへのアクセスで例外が発生しそうです。

ですが、紹介した設定ではテスト対象の vue ファイルに <style module> がある場合はブロック内の記述は無視しつつ $style = {} を注入してくれるため、上記のような例外は発生しないようになっています。


  1. vue-jestREADME.md のコミットログを追ったところ 2018/04/09 のコミットexperimentalCSSCompile に関する記述が追加されていました。2年以上経過していますが "experimental" は外れていないようです 

  2. sass-loader 7.x.x までは data で、 9.x.x からは additionalData となるようです。 additionalData は prepend だけでなく append もできるそうですが、 8.x.x の prependData と同じように使うことができました 

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

i18nのロケールをJavaScriptにわたす

多言語対応のアプリを作成中に詰まった

jQueryとAjaxを使って、親カテゴリを選択すると子カテゴリの候補がプルダウンで表示されるようにしました。
ところが、子カテゴリだけ、デフォルト表記のベトナム語でしか翻訳されてませんでした。

例:親カテゴリ => 質問
  子カテゴリ => 1,Ngôn ngữ và văn hóa
         2,Tập huấn kỹ thuật
         3,Ứng dụng và thủ tục

原因

Ajaxで子カテゴリを取得する際、コントローラにロケールを渡してないことが原因でした。

category_pulldown.js
$("#parent").on("change", function () {
  // 選択した親カテゴリからIDを取得
  var id = document.getElementById("parent").value;
// 中略
$.ajax({
  type: 'GET',
  data: { parent_id: id }, // IDをparams[:parent_id]に入れて送信
  url: '/categories/pulldown', // categoriesコントローラにて、受け取ったIDで子カテゴリを取得
  dataType: 'json',
}).done(function (children) { // 取得した子カテゴリを表示させる処理...

一部抜粋ですがロケールの取得は以下の方法でやってました。

application_controller.rb
before_action :set_locale

  private

  def set_locale
    I18n.locale = locale
  end

  def locale
    @locale ||= params[:locale] ||= I18n.default_locale
  end

  def default_url_options(options = {})
    options.merge(locale: locale)
  end

現在のロケールをJava Scriptへわたす

まずは、inputタグを使って現在のロケールを保持させます。
以下のようにtype="hidden"と書くと、ユーザーから見ても画面には表示されません。

hoge.html.erb
<input type="hidden" class="current_locale" value="<%= I18n.locale %>">

次にlocale: $('.current_locale').val()を追加すると
親子カテゴリの翻訳言語が統一されました。

category_pulldown.js
$("#parent").on("change", function () {
  // 選択した親カテゴリからIDを取得
  var id = document.getElementById("parent").value;
// 中略
$.ajax({
  type: 'GET',
  data: { parent_id: id, locale: $('.current_locale').val() }, // IDをparams[:parent_id]に入れて送信
  url: '/categories/pulldown', // categoriesコントローラにて、受け取ったIDで子カテゴリを取得
  dataType: 'json',
}).done(function (children) { // 取得した子カテゴリを表示させる処理...

HTMLはRubyからもJavaScriptからも情報の受け渡しができるので便利ですね:smiley:

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

html2canvasを試す

HTML の Element を画像に変換するライブラリ html2canvas を試しました。Chrome では良好ですが、Firefox では文字のサイズが変わってレイアウトが崩れるようです。

See the Pen Test: html2canvas by 七誌 (@7shi) on CodePen.

↑ 左が撮影元の Element、右がそれを画像化したものです。内容はデザインでよく使われる lorem ipsum と呼ばれるダミーテキストで、意味はありません。

シリーズの記事です。同じ目的のライブラリを比較します。

  1. dom-to-imageを試す
  2. html2canvasを試す ← 今回の記事

html2canvas

公式: http://html2canvas.hertzen.com/

驚異的な力技で実装されているようです。

html2canvasは、WebページのDOMやCSSを読み込み、その結果を元に解釈した結果をCanvasエレメント上に描画するライブラリです。 すなわち、実際にスクリーンショットを撮っているわけではなく、挙動としてはレンダリングエンジンに近いです。 (CSSの解釈はなんと1つ1つ実装されているので、作者は本当にすごいと思います)

利用方法

自前でどこかに置かなくても、jsDelivr 経由で参照できます。

<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.0.0-rc.5/dist/html2canvas.min.js"></script>

README のサンプルコードを元に async/await で書き換えます。

(async () => {
  dst.appendChild(await html2canvas(src));
})();

動作結果

この記事の冒頭に貼ったサンプルのスクリーンショットを見ます。

Chrome では上に少し余白ができますが、文字のサイズはそのままで画像化されます。
image.png
Firefox では文字のサイズが変わってしまうため、単語間のスペースが潰れたり文字が重なったりします。
image.png
撮影対象がブラウザに依存するのでなければ、Chrome で利用するのが無難なようです。

※ 手動ですが Firefox で Element を画像化する方法は前回の記事を参照してください。

参考

以下の記事では CDN にあるバージョンは古いとありますが、jsDelivr では npm に登録されている最新版が利用できます。

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

Stack Overflow Developer Survey 2020の要点整理

 この記事の目的

世界のディベロッパーのトレンドを追う上で、非常に重要な資料であるStack Overflow Developer Surveyの最新版である2020年版の要点を整理すること。自分の理解のためだけでなく、毎年出されているにもかかわらず、日本語での情報共有が少ない本調査の要点を日本語で整理することで、多くの日本人の開発者が世界のトレンドを理解することを期待する。

原文
https://insights.stackoverflow.com/survey/2020

 調査概要

・2020年2月に世界中の65,000人以上のディベロッパーにアンケートを実施(注:回答者はアメリカ、インド、ヨーロッパに集中)。
・コロナウイルスが流行る前に調査が実施されたため、仕事や賃金のデータは現時点での情報と異なる可能性があることに注意。

 キーポイント

(1)最も愛された言語はRust, Typescript, Pythonの順番。Rustは五年連続で一位。
(2)回答者の8割がDevOpsが重要だと考えている。
(3)回答者の9割が、プログラミングで壁に当たった時、Stack Overflowを参照している。

 職種

・複数回答ありで、55%がバックエンド、フルスタックと答え、37%がフロントエンドのエンジニアと回答。
・プログラミング経験年数の層の中で一番高い割合が5年から9年で回答者の30%を占めた。5年以下と答えたのは17%。
・一方で、仕事としてプログラミングした経験は5年以下が最多で回答者の40%を占めた。
・回答者の85%は10代でプログラミングを開始した。

 学歴

・46%が学士号を、23%が修士号を取得。また回答者の6割以上が学部時代にコンピューターサイエンスを専攻していた。

 言語その他のランキング

(1)最もポピュラーな技術
Screenshot from 2020-07-07 23-43-55.png

(2)最もポピュラーなフレームワーク
Screenshot from 2020-07-07 23-48-13.png

(3)最もポピュラーなプラットフォーム
Screenshot from 2020-07-07 23-49-21.png

(4)最も必要とされている技術
Screenshot from 2020-07-07 23-51-06.png

(5)最も必要とされているフレームワーク
Screenshot from 2020-07-07 23-52-06.png

(6)最も必要とされているプラットフォーム
Screenshot from 2020-07-07 23-53-30.png

(7)技術ごとの年収ランキング(米ドル換算)
Screenshot from 2020-07-07 23-57-39.png

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