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

ブラウザの中でスタンド使いになってみた。

はじめに

今までは真面目に開発していたが、少しふざけたものを作ってみようと思い、今回のChrome拡張を作ってみました。

それと以前拝見した以下の記事や

カタカタカタッターンを可視化した

やAtomのactivate-power-modeに影響され、今回作るChrome拡張を決めました。

リリースしたChrome拡張概要

textareaやinput上でキータイプをするとエフェクトが表示されます。
先ほど紹介したChrome拡張と同様の仕様ですね。

そこにactivate-power-modeのコンボのような連続したキータイプの際に別のエフェクトを発生させます。

jojo_action.gif

キータイプを500ms以内の間隔で連続30回以上タイプした後にエンターキーを押すとセリフと効果音が表示されます。

タイプするたびにlocalstorageにタイプした時間と連続タイプ数を保存します。

Jojo experience - chromeウェブストア

作り方

ソースは以下のリポジトリにあります。
temori1919/jojo_experience

今回はcontent_scriptsスクリプトという形式で作成します。

manifest.json
{
  "manifest_version": 2,
  "name": "Jojo experience",
  "version": "1.0.0",
  "description": "スタンド使いになれるChrome Extensionです",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": [
        "jquery.min.js",
        "soundEffect.js",
        "caretposition.js"
      ]
    }
  ],
  "web_accessible_resources": [
    "images/*"
  ],
  "permissions": [
    "storage"
  ]
}

キャレット位置の取得にcaretposition.jsというjsライブラリを使用させていただきました。

[jQuery対応] textareaのキャレット座標(XY座標)を取得するjavascriptライブラリを作った

soundEffect.js
document.onkeydown = e => {

  const current = document.activeElement

  if (e.key === 'Backspace') {
    return true
  }
  if (current.type === 'textarea' || current.type === 'text' || current.type === 'search') {
    (async () => {
      // タイピング時間の記録
      let type_time = localStorage.type_time
      // タイピング数のカウント
      let type_count = localStorage.type_count

      // タイピングの感覚が500ミリ秒以下ならタイピング数をカウントする
      if (type_time !== undefined && type_count !== undefined && (new Date().getTime() - type_time) < 500) {
        localStorage.type_count = parseInt(type_count) + 1
      } else {
        localStorage.type_count = 1
      }

      localStorage.type_time = new Date().getTime()

      const isEnter = e.key === 'Enter'
      // キャレットの位置の取得
      const caretPosition = Measurement.caretPos(current)

      let prefix = isEnter ? 'ora2' : 'ora'
      let imgUrl = chrome.extension.getURL('images/' + prefix + '.png')

      // オラ
      let image = await appnedCss('images/' + prefix + '.png', isEnter ? rand(80, 100) : rand(10, 20), caretPosition.top + rand(-10, 10), caretPosition.left + rand(-10, 10))
      image = await animateRemove(image, isEnter, caretPosition)

      // タイプカウントの回数が30以上でエンターをタイプしたらセリフと効果音を表示
      if (isEnter && type_count > 30) {
        localStorage.type_count = 1
        // 時
        image = await appnedCss('images/toki.png', rand(80, 100), caretPosition.top, caretPosition.left, 400)
        image = await animateRemove(image, isEnter, caretPosition, 200)

        // エフェクト
        image = await appnedCss('images/effect' + rand(1, 4) + '.png', rand(80, 100), caretPosition.top, caretPosition.left, 600)
        await animateRemove(image, isEnter, caretPosition)
      }
    })()
  }
}

/**
 * 乱数生成
 *
 * @param min
 * @param max
 * @returns {number}
 */
function rand(min, max) {
  return Math.floor(Math.random() * (max - min) + min)
}

/**
 * imgをappend
 *
 * @param imgUrl
 * @param size
 * @param top
 * @param left
 * @param delay
 * @returns {Promise<any>}
 */
function appnedCss(imgUrl, size, top, left, delay) {
  return new Promise(resolve => {
    delay = delay === undefined ? 0: delay
    setTimeout(() => {
      imgUrl = chrome.extension.getURL(imgUrl)
      img = $(`<img width="${size}">`)
      img.attr('src', imgUrl)
      img.css({
        'position': 'absolute',
        'top': top,
        'left': left,
        'zIndex': 100000
      })
      $('body').append(img)
      resolve(img)
    }, delay)
  })
}

/**
 * imgのアニメーションと削除
 *
 * @param img
 * @param isEnter
 * @param positon
 * @param delay
 * @returns {Promise<any>}
 */
function animateRemove(img, isEnter, positon, delay) {
  return new Promise(resolve => {
    delay = delay === undefined ? 0: delay
    setTimeout(() => {
      const size = isEnter ? rand(80, 100) : rand(10, 20)
      img.animate({
        'top': positon.top + rand(-40, 40),
        'left': positon.left + rand(-40, 40),
        'width': size + (isEnter ? rand(30, 50) : rand(10, 20)),
        'opacity': 0
      }, 1000, () => {
        img.remove()
      })
      resolve(null)
    }, delay)
  })
}

本体はES2017で書いています。
コメントは適当なのでご容赦下さいw

キータイプするとタイプした時間とキータイプ回数がカウントUPされます。
前回時間とタイプした時間が500ms以内ならカウントUP、それ以外なら1を保存します。

それとimgを追加してアニメーションで表示しています。
また、エンターが押された後の処理はasync awaitで直列的に処理させるようにしています。

最後に

ちょっとふざけたものを作りたくて、今回の拡張をリリースしました。
正直邪魔にしかならない拡張機能なので、お試しで入れた場合は少し使って削除するのがいいと思いますww

興味がある方は追加してみて下さい。

Jojo experience - chromeウェブストア

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

flutter_web から JS の WebWorkers を呼んでみた

Flutter web でアプリを試作してみた の処理部分を 生 JS の Web worker にくくりだしました。

からです。この文章は、主に後者のメモです。

まとめ

  • Web worker へは、JSON で渡した(思考停止)
  • Web worker から Uint8Array を返すと、ByteBuffer にキャストできる形で返ってくる。
  • ByteBufferasInt8ListInt8List にし、 必要に応じて sublistList<int> を切り出して使用した。

今回の Web worker の入出力

  • 入力: 試行回数と、フィルタの構成(JSON)
  • 出力: フィルタをパスした回数と、そのサンプル群

Web worker 部(処理側)

web/filter.js を置いておくと、webdev serve で参照でき、webdev build 時に build/filter.js にコピーしてくれます。

入力は、Enumindex への変換などが必要で、JSONで渡すことにしました。
処理は、汎用的な話ではないのでざっくり載せておきます。

web/filter.js
/* 前略 */
self.addEventListener('message', function (e) {
  // 指定された試行回数と、フィルタを元に、ランダムな局面を生成しパスする回数を数える
  const param = JSON.parse(e.data);
  const trials = param.trials;
  const filters = param.filters;
  const testFunc = makeTestFunc("yama", filters);
  var samples = [], passedCount = 0;
  const yama = Array.from(initYama);
  for (var i = 0; i < trials; ++i) {
    yama.shuffle();
    if (testFunc(yama)) {
      ++passedCount;
      if (samples.length < 100) {
        samples.push(Array.from(yama));
      }
    }
  }

それで、flutter_web に結果を返すのがこちらの部分。
配列をそのまま返すと、プロセス間のコピーが生じて遅くなるので、Transferable な形で返す。
所有権の移動だけになるらしい。shared なのは未だ無さそう。

最初、パスした回数などもバイナリに埋め込まないと駄目かと思ったのですが、
普通にオブジェクトを渡せば、flutter 側で Map で受け取れるので、
重たいデータだけ変換してやると良いと思います。

web/filter.js
  // Transferable に変換してコピーのロスを減らす
  const result = new Uint8Array(samples.length * 32);
  var offset = 0;
  for (var i = 0; i < samples.length; ++i) {
    const yama = samples[i];
    for (var j = 0; j < 32; ++j) {
      result[offset + j] = yama[j];
    }
    offset += 32;
  }
  self.postMessage({ passedCount: passedCount, samples: result.buffer }, [result.buffer]);
}, false);

この文章を書いていて、最終的に Uint8Array が固定長で良くなったので、Arraypush してるのを直接、
Uint8Array に保存すれば無駄が無くなることに気がついた。

Flutter 部(呼び出し側)

呼び出しは postMessage、結果の受け取りは onMessage.listen で行う。

配列の結果は ByteBuffer にキャストすれば良かった。print(msg.data["samples"]) すると NativeByteBuffer だと言われましたが、問題なくキャストできているようなのでこれ以上深堀りしていない。

ByteBufferasInt8ListInt8List にしておき、
必要に応じて sublistList<int> を切り出して使用した。

lib/main.dart
final w = Worker("filter.js");
w.onMessage.listen((msg) {
  final passedCount = msg.data["passedCount"] as int;
  final samples = msg.data["samples"] as ByteBuffer; /* NativeByteBuffer */
  setState(() {
    _passedCount = passedCount;
    _samples = samples.asInt8List();
    /* (中略) */
w.postMessage(jsonEncode({"trials": _trials, "filters": _filters}));
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactのchildren探訪

ReactのJSXでも子要素を定義できますが、これが結構面白いものでした。

自作エレメントにも子要素

ReactのJSXでは、(DOMを組み立てるものである以上当然ですが)<div>の中に<a>を書いて、そしてさらに文字列を書き込む、ということが可能です。

そして、これはHTML由来のエレメントだけでなく、自作のコンポーネントでも実現可能です。

function SomeWrapper({children}) {
  return(
    <div className="some-class">
      { children }
    </div>
  );
}

このように、子要素はchildrenというPropとして渡ってきます。

childrenの中身が知りたくて

では、このchildrenには何がどのような形式で来るのでしょうか。JSXの変換先であるReact.createElementソースコードに当たってみました。挙動はchildrenの数によって違います。

  • 0個…childrenにはpropとして渡したchildrenが(もしあれば)渡される
  • 1個…childrenには唯一のchildren引数がそのまま渡される1
  • 2個以上…childrenには引数で渡されたものが配列に詰め込まれる

公式な操作方法

現実問題として、今からchildrenの実装を変えてしまうということは互換性問題などを考えれば可能性は薄いのですが、公式にはchildrenのデータ形式は「非公開」ということになっています。そこで、childrenを操作するためのReact.Childrenというユーティリティ関数群があります(リファレンス)。

  • React.Children.mapchildrenの各要素に対して関数を実行して、結果を配列で得る。
  • React.Children.forEachchildrenの各要素に対して関数を実行する。
  • React.Children.countchildrenの個数を返す
  • React.Children.toArraychildrenを本物の配列に変換して返す
  • React.Children.onlychildrenが1つのJSXエレメントであることを保証する

なお、forEachmaptoArrayで処理する際に、キーは子要素間で一意となるようなものがReact側で振られます。また、子要素として<React.Fragment>を渡した場合、1つものとして扱われます。

childrenReact.memo

今のところ、React.createElementをキャッシュするような仕組みはないので、<br />のようなシンプルなJSX要素であっても、2回の実行でオブジェクトとして一致することはありません。さらに、上述のように複数の子要素をもたせた場合、childrenの配列を生成しますので、このインスタンスも一致しません。

そして、PureComponentReact.memochildrenを特別扱いしませんので、(文字列1つだけ指定するような例外的なケースは別として)childrenを指定するようなコンポーネントでは、メモ化は利かないということになってしまいます。

対策としては、

  • 子要素を取るコンポーネントを使う側の場合、「コンポーネント+子要素」全体を1つのコンポーネントとして、それをメモ化する
  • 子要素を取るコンポーネントを作る場合、メモ化を諦める

などが考えられます。そして、children以外でも、表示する文字列をpropで取る場合に、「HTMLタグで装飾したい」とその場で書いたJSXを渡せば同じ事態となります。こちらについては、React.useMemo、クラスのプロパティ、外側で変数に定義するなど、同じJSXインスタンスを使い回す形として対応が可能かと思います。


  1. Context.Consumerなどで子要素代わりにコールバックを書くことがありますが、1個なので関数がそのままchildrenに入る形となっています。 

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

【HTML5】WEBページ上でお絵かき!『Canvas』ハイパーゆとり向けチュートリアルPart3

※記事最下部あたりにHTMLファイルの内容が掲載されております。

前回はマウスのイベントを取得し、それをもとにマウスが投下され動き続けている間中は線を描画するという動作を設定しました。
あのままでは質素なので、機能を増やしてみましょう!

チュートリアル続き

ちょっとだけ前回の復習です!
前回記述したコードの主要部分はこんな感じですね!

    canvas.addEventListener('mousemove', draw_canvas);

    canvas.addEventListener('mousedown', function(e) {
      drawing = true;
      var rect = e.target.getBoundingClientRect();
      before_x = e.clientX - rect.left;
      before_y = e.clientY - rect.top;
    });

    canvas.addEventListener('mouseup', function() {
      drawing = false;
    });

    function draw_canvas(e) {
        if (!drawing){
        return
        };

        var rect = e.target.getBoundingClientRect();
        var x = e.clientX - rect.left;
        var y = e.clientY - rect.top;

        context.lineCap = 'round';
        context.strokeStyle = 'black';
        context.lineWidth = '10';

        context.beginPath();
        context.moveTo(before_x, before_y);
        context.lineTo(x, y);
        context.stroke();
        context.closePath();

        before_x = x;
        before_y = y;
    }

マウスイベントに対する挙動を設定し、更に主要動作としてdraw_canvas()関数を生成しました!この関数の中では、「新しい点を打っては線を引く」このような動作を連続的に行っていましたね!

今回は機能を充実させていこうと思いましたが、コードべた貼り芸の都合上、とってもスクロールが大変な記事になってしまうので、とりあえず消しゴム機能だけ実装してみたいと思います!

まずはページ内に切り替え用ボタンを設置

    <input type="button" value="ペン" id="pencil" class="active" onClick="init.tool(1)">
    <input type="button" value="消しゴム" id="eraser" onClick="init.tool(2)">

・いみ
ぺんとけしごむがほしいとねんじます。

canvasタグの上でも下でもいいので(内部はNG)、ペンボタンと消しゴムボタンとを実装します!
HTMLコードについては基本的に割愛しますが、jsコード部とリンクしてくる部分に関しては説明していこうと思います!
classの部分は『active』という字面からなんとなく察しがつきますが、デフォルトでペンが有効化されるようにしてあります!
『onClick』は、関数名を指定してあげることでクリック→関数を動作というようなスクリプトが実現できます!

onClick="[関数名]"

では今回関数名として指定されている『init.tool()』とはなんでしょうか?実はこれはそのままinit.tool()関数を呼び出しという意味です!
これに関してはjsコード部の解説にて正体が判明します。
正直なところ推奨された記述方法なのかは分かりません!!!!!!!!!

ではjsで消しゴム機能を実装していきましょう!!!

    var pen = document.getElementById('pencil');
    var era = document.getElementById('eraser');

・いみ
ぺんとけしごむをつくってあげます。

これを記述する場所はdraw_canvas関数のすぐ下で問題ありません。(中はNG)
「document.getElementById()」に関しては、Part1あたりでやりましたのでお分かりですね!IDで指定したHTML要素を取得します。

続いての作業で、ちょっとしたテクニックを使います!initオブジェクトのプロパティとして、toolという関数を実装します。(わかりづらい)

init.tool = function(btnNum){
    // code here
    }

・いみ
けしごむとぺんのきりかえをするかんすうをつくります。

jsを触り始めて日が浅いので、こういう書き方が適切かは正直分からないですが、関数の中にある関数を呼び出す方法があまりよくわからなかったので、こんな感じにしました。
initオブジェクトのメソッドとして、toolという関数を実装しています。ちなみに引数としてbtnNumという変数を受け取ります。
この数字によって、消しゴムとペンの切り替えを行います。
ではこの関数の中身を記述していきます。

      if (btnNum == 1){
        context.globalCompositeOperation = 'source-over';
        pen.className = 'active';
        era.className = '';
      }

      else if (btnNum == 2){
        context.globalCompositeOperation = 'destination-out';
        pen.className = '';
        era.className = 'active';

・いみ
1でぺん、2でけしごむになります。

こんな感じで、変数として1が与えられたらペンモード、2が与えられたら消しゴムモードといった感じに実装します。
ではglobal...みたいなやつはなんでしょうか!

context.globalCompositeOperation = '';

↓調べて一番上に出てきたサイトの文言

・source-over (default)
A over B。描画元イメージのうち、描画元イメージが不透明な部分が表示されます。それ以外の部分では描画先イメージが表示されます。
・destination-out
B out A。source-out と同じですが、描画元イメージの代わりに描画先イメージを使います。

次世代 HTML 標準 HTML5 情報サイト

なるほどなぁ・・・・・・・・・・・・・・・・・・・・・
正直全く理解できなかったので、消しゴムのほうの「destination-out」だけでも理解しようともう少し調べた結果、こちらにギリ私の知能でもわかるように書いてありました。

destination-out
描画先 Canvas の内容は、新たな図形と重なり合わない部分だけが残ります。新たな図形も含めて、他の領域は透明になります。

前半部分は相変わらずよく分かりませんが、後半の文に注目です。「新たな図形も含めて、他の領域は透明になります」
・・・このオプションを指定して描画すると、透明な線を描くってことはなんとなく分かります・・・なんとなく・・・。

実は、これでもう消しゴム機能は実装できています。試してみましょう!

表示するHTMLファイルは以下の通り。

<!DOCTYPE html>
<html lang="ja">
<head>
<script type="text/javascript">
function init(){
    var canvas = document.getElementById("mycanvas");
    var context = canvas.getContext('2d');
    var drawing = false;
    var before_x = 0;
    var before_y = 0;
    canvas.addEventListener('mousemove', draw_canvas);

    canvas.addEventListener('mousedown', function(e) {
      drawing = true;
      var rect = e.target.getBoundingClientRect();
      before_x = e.clientX - rect.left;
      before_y = e.clientY - rect.top;
    });

    canvas.addEventListener('mouseup', function() {
      drawing = false;
    });

    function draw_canvas(e) {
        if (!drawing){
        return
        };

        var rect = e.target.getBoundingClientRect();
        var x = e.clientX - rect.left;
        var y = e.clientY - rect.top;

        context.lineCap = 'round';
        context.strokeStyle = 'black';
        context.lineWidth = '10';

        context.beginPath();
        context.moveTo(before_x, before_y);
        context.lineTo(x, y);
        context.stroke();
        context.closePath();

        before_x = x;
        before_y = y;
    }

    var pen = document.getElementById('pencil');
    var era = document.getElementById('eraser');
    // 鉛筆と消しゴムの切り替え
    init.tool = function(btnNum){
    // クリックされボタンが鉛筆だったら
      if (btnNum == 1){
        context.globalCompositeOperation = 'source-over';
        pen.className = 'active';
        era.className = '';
      }
    // クリックされボタンが消しゴムだったら
      else if (btnNum == 2){
        context.globalCompositeOperation = 'destination-out';
        pen.className = '';
        era.className = 'active';
      }
    }
}
</script>   
<meta charset="utf-8">
<title>TUTORIAL</title>
</head>
<body onload="init();">
    <canvas id="mycanvas" width="500" height="300" style="border:#1C1C1C solid 2px"></canvas>
    <input type="button" value="ペン" id="pencil" class="active" onClick="init.tool(1)">
    <input type="button" value="消しゴム" id="eraser" onClick="init.tool(2)">
</body>
</html>

では表示してみます。
(AWS Cloud9のプレビュ画面で、背景色が灰色になってるのは無視してください)

キャプチャ.PNG

ペンボタン、消しゴムボタンが実装され、ボタンを投下することで切り替えを行うことができます。

最後に

今回はお絵描き機能の拡張を行いました!興味のある方はこちらを参考に、全消し機能や線の太さ、色の変更機能を実装してみてください!
それでは次回はタッチ機能を実装してみたいと思います!
なんだか筆圧に近づいてきた気がするぞ(?)!!!

参考サイト様

次世代 HTML 標準 HTML5 情報サイト:http://www.html5.jp/canvas/ref/property/globalCompositeOperation.html
合成とクリッピング:https://developer.mozilla.org/ja/docs/Web/API/Canvas_API/Tutorial/Compositing)
canvasで作るお絵描きツール:https://kigiroku.com/frontend/canvas_draw.html

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

非同期処理の順序制御

非同期処理の順序制御。良く使うパターンなので、切り出してみました。

たとえば、ユーザ入力要素(inputとか)が変更されると、APIにアクセスして、返ってきた値を、反映させるような場合。API処理にかかる時間は不定なので、戻ってくる順番がずれて、表示がおかしくなる場合があります。これを防ぐ処理です。

class AsyncOutOfOrderError {}
class AsyncCommitter {
  requestId = 0;
  commitId = 0;
  async run(callback) {
    this.requestId += 1;
    const currentRequestId = this.requestId;
    const value = await callback();
    if (currentRequestId <= this.commitId) {
      throw new AsyncOutOfOrderError();
    }
    this.commitId = currentRequestId;
    return value;
  }
}

使うときは、

try {
  const value = await this.asyncCommitter.run(async () => {
   // ここに制御対象の処理
  });
  // ここに正常に終了したときの処理
} catch (error) {
  if (!(error instanceof AsyncOutOfOrderError)) {
    throw error;
  }
  // ここに順番がおかしくなったときの処理(あれば)
}

もしかしたら、run(callback, onSuccess, onErrror) とかの方が使いやすいかもしれません。

皆さん、これに相当する処理、どうさされてますか?同等の機能を持つライブラリを知っている人がいたら教えてもらえると幸いです。

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

vue-simple-suggestで簡単にテキスト入力候補をリストで表示

はじめに

正確な名前が分からなかったり、検索項目が多い場合サジェストは非常にUXの質を高めてくれます。

今回はTypescriptとVueで簡単にサジェストの表示を実装したのでその記録を残しておきます。

今回利用するのがこちらです。

vue-simple-suggest

vue-simple-suggestのインストール

まずはvue-simple-suggestをインストールしましょう。

yarn add vue-simple-suggest

これでインストール完了しました。

これだけだとインポートしたときに方エラーが出てしまいます。型の定義ファイルにvue-simple-suggestを宣言しておきましょう

shims-vue.d.ts
declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}

declare module 'vue-simple-suggest';

実装

最終的なコードは以下のようになります。

<template>
  <vue-simple-suggest
    v-model="chosen"
    :list="simpleSuggestionList"
    :filter-by-query="true"
    :styles="autoCompleteStyle"
    display-attribute="firstName"
  >
    <template slot="misc-item-above">
      <h5>ユーザー一覧</h5>
      <tr>
        <th>ファーストネーム</th>
        <th>ラストネーム</th>
      </tr>
    </template>
    <tr slot="suggestion-item" slot-scope="{ suggestion }">
      <td>{{ suggestion.firstName }}</td>
      <
td>{{ suggestion.lastName }}</td>
    </tr>
  </vue-simple-suggest>
</template>

<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import VueSimpleSuggest from "vue-simple-suggest";
import Button from "@/components/Atoms/Button.vue";
import "vue-simple-suggest/dist/styles.css";

@Component({
  name: "FormWIthSearch",
  components: {
    Button,
    VueSimpleSuggest
  }
})
export default class FormWIthSearch extends Vue {
  private chosen: string = "";
  private autoCompleteStyle: {} = {
    vueSimpleSuggest: "position-relative",
    inputWrapper: "",
    defaultInput: "form-control",
    suggestions: "position-absolute list-group",
    suggestItem: "list-group-item"
  };
  public simpleSuggestionList() {
    return [
      { id: 1, firstName: "タロウ", lastName: "サトウ" },
      { id: 2, firstName: "ジロウ", lastName: "イノウエ" },
      { id: 3, firstName: "サブロウ", lastName: "タナカ" }
    ];
  }
}
</script>

順番に説明します。

ボタンは別コンポーネントから読み込んでいますが普通のボタンと思っていただいて構わないです。

CSSにはBootstrapを利用しています。

各プロパティの説明です。

chosenには選択したオプションの値が入ります。今回はfirstNameを指定しています。またこの値から検索で絞り込みます。

続いてautoCompleteStyleです。テキストボックスやサジェストの下に表示されるリストのクラスを付与することができます。

CSSは通常通りお好みのやり方であてていただければ大丈夫です。

simpleSuggestionListでリストの配列を返して検索リストとして割り当てます。

これは一点注意でデフォルトの状態ではidを付けなければエラーがでます。

また私の場合はオブジェクトのfirstNameを検索対象としています。display-atributeを指定しないとデフォルトでは配列に文字列を入れなければなりません。

さらにvue-simple-suggest内でslot="misc-item-above"をタグにつけるとheaderを付けたりできます。

他にもオプションはまだまだたくさんあります。詳しくはgihubのREADMEを読んでみましょう。

まとめ

今回はVueによるサジェストの実装をしました。UXを向上させるには必須ので機能です。

このプラグインを活用して爆速でサジェストを実装しましょう。

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

Gatsbyのブログに目次を追加する

はじめに

ブログを見やすくして、構造化させるのに必須の項目「目次」を設定したのでその方法を記録に残したいと思います。

いちいち自分でマークダウンの目次を書くのが面倒くさいので自動で目次を生成してくれる良さそうなプラグインないかなと思いました。

最初はgatsby-transformer-remarkに標準で搭載されている機能を利用しようとしたのですが、リンクがうまく効かずに404エラーが出てしまっています。

そこで今回利用したのが「gatsby-remark-toc」です。

これを利用することで一瞬でマークダウンに目次を導入することができました。今回はその方法を紹介します。

gatsby-remark-tocの設定

まずはgatsby-remark-tocをインストールしましょう

yarn add gatsby-remark-toc 

インストールしたら次にgatsby-config.jsの設定をします。gatsby-remark-tocを追加してオプションを設定します。

headerには目次上部に出てくる見出しの名称を設定します。私は無難に「目次」と表示させます。

includeに対しては.mdファイルが存在して目次を表示させたいディレクトを指定します。これで指定したファイルのみに目次を表示させられるようになりました。

gatsby-config.js
{
      resolve: 'gatsby-transformer-remark',
      options: {
        plugins: [
          {
            resolve: 'gatsby-remark-toc',
            options: {
              header: '目次',
              include: ['src/pages/blog/*.md'],
           },

たったこれだけで目次の表示は完了です。お疲れ様でした。

これで問題なく目次の構造を作成してHTMLとして上部に出力することができます。ただし、デザインは入っていないので各自で自由にcssを作成して、反映するようにしましょう

まとめ

以上で目次の設定は完了です。非常に簡単でした。

皆さんもgatsby-remark-tocを利用して爆速で目次をマークダウンに作成してみましょう!

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

【Google Cloud / Node.js】Cloud SchedulerをAPI経由で使ってみる

はじめに

Firebase をバックエンドに使ったWebアプリの開発にあたり
「定期実行されるcronサービスを使いたい」
「クライアントアプリからジョブを設定したい(API使いたい)」
と思い、調べたところ Google Cloud PlatformCloud Scheduler が良さげさったので使ってみました。

cronサービスは他にもいくつかありますが、Cloud Schedulerの特徴としては

  • 無料枠がある(1アカウントあたり3タスク)
  • APIが提供されており、HTTP経由でジョブの設定、変更が可能である
  • GCPの他のサービスとの親和性が高い※

などかなと思います。

※ Cloud Functions for Firebaseであれば、Cloud Schedulerの機能をラップしたメソッドを使って定期実行ができるそうです。参考: Firebase Cloud Functionsの定期実行が、それ単体で簡単にできるようになった! - Qiita

本記事は、Node.jsでCloud SchedulerジョブをAPIを使って設定し、 Cloud Functions for Firebase を実行する方法の備忘録です。

事前準備

GCPに登録

Cloud Schedulerは Google Cloud Platform が提供するサービスです。無料使用枠はありますが使用には登録が必要なため、予め登録しておきましょう。
登録完了後、APIを使うためのプロジェクトを作成してください。

APIを有効化

APIを使用するため、 コンソールのAPIライブラリからCloud Schedulerを検索しAPIを有効化しておきます。
今回はCloud FunctionsもAPI経由で実行するため、Cloud Functions APIも有効化しましょう。

Google Cloud API認証用の環境設定

Google Cloud APIの使用にはOAuth認証が必須です。
事前にGCPのサービスアカウントと、秘密鍵を含むJSONファイルを作成し、それを用いて認証を行います。
こちらを参考に、サービスアカウントの作成〜環境変数の設定までを実施します。

Cloud Pub/Sub の設定

Cloud Schedulerでジョブを作成する際、定期実行するジョブの種類(ターゲット)を以下の選択肢から設定する必要があります。

今回はCloud Scheduler → Cloud Pub/Sub → Cloud Functions の順で実行するので、予めPub/Subの設定と実行される関数を準備しておきます。
まず、最終的に実行されるCloud Functionsの関数を functions.pubsub で作成します。トピック名は後ほど設定します。

const functions = require('firebase-functions');

exports.handlePubSubTrigger = functions
  // トリガーとなるPub/Subのトピックを指定する
  .pubsub.topic('[トピック名]')
  // Pub/SubからPUSHがあったときの処理
  .onPublish(async (event, context) => {
    const pubsubMessage = event.data;
    console.log(Buffer.from(pubsubMessage, 'base64').toString());
  });

次に、前段で作成した関数をトリガーするPub/Subのトピックおよびサブスクリプションを作成します。作成は Google Cloud SDK からCLIで作成できるほか、GCPのコンソール画面からも作成可能です。こちらを参照しつつ

  1. トピック作成後、トピック名を実行する関数に設定
  2. サブスクリプションのエンドポイントURLに実行する関数のURLを指定

を行います。

APIを使ってみる

事前準備が完了したら、実際にCloud Scheduler APIを使ってジョブを作成してみます。
なお、Google Cloud API リクエストの実装方法として
1. 単純にHTTPリクエストを発行する
2. クライアントライブラリ( Google APIs Node.js Client )を使う

の2パターンあり、今回は使い勝手のいい後者を採用します。
ジョブ作成するCloud Functionsの関数は以下になります。プロセスとしては、

  1. GCPのOAuth認証
  2. Cloud Schedulerジョブの新規作成(APIのリクエスト)

の2つです。

const functions = require('firebase-functions');
const { google } = require('googleapis');

exports.createSchedulerJob = functions.https.onRequest(
  async (req, res) => {
    // ①GCPのOAuth認証
    const client = await google.auth.getClient({
      scopes: ['https://www.googleapis.com/auth/cloud-platform'],
    });

    // ②ジョブの新規作成
    const projectId = '[PROJECT_ID]'; // プロジェクトID
    const locationId = '[LOCATION_ID]'; // ロケーション名(Cloud Functionsのデフォルトはus-central1)
    const topicName = '[TOPIC_NAME]'; // トリガーするPub/Subのトピック名
    const jobName = '[JOB_NAME]'; //作成するCronジョブ名

    const request = {
      parent: `projects/${projectId}/locations/${locationId}`,
      resource: {
        name: `projects/${projectId}/locations/${locationId}/jobs/${jobName}`,
        schedule: '0 7 * * 1-5', // 定期実行する時刻 今回は月~金のAM7:00に設定
        pubsubTarget: {
          topicName: `projects/${projectId}/topics/${topicName}`,
          data:  Buffer.from('Cron triggered.').toString('base64'),
        },
      },
      auth: client,
    };
    // APIリクエスト
    cloudScheduler.projects.locations.jobs.create(request, (err, response) => {
      if (err) {
        console.error(err);
        return;
      }
      return response.data;
    });
  },
);

① GCPのOAuth認証

まず、クライアントライブラリのauth.getClient()メソッドを使ってOAuth認証を行います。

このとき、scopesの設定が必要になります。
スコープとはアプリケーションに許可するAPI操作の権限の範囲で、これを認証時に明記する必要があります。
各APIに必要なスコープはAPIのドキュメントにAuthorization Scopesとして記載されています。今回はCloud Schedulerに必要なhttps://www.googleapis.com/auth/cloud-platformを指定します。

const client = await google.auth.getClient({
  scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});

補足ですが、auth.getClient()は開発環境において前段で準備した秘密鍵のJSONファイルをよしなに参照してくれるので、コード内で認証ファイルをインポートするなどの必要はありません。クライアントライブラリを使うのにはこうした利点があります。

② Cloud Schedulerジョブの新規作成

OAuth認証後、Cloud Schedulerのジョブを作成します。ジョブの作成には projects.locations.jobs.create を使います。

リクエストとして以下の項目を設定します。

  • parent …ジョブを作成するプロジェクト。
  • resource.name …ジョブの名前。projects/~で始まるパスの形で設定。
  • resource.schedule … ジョブの設定時刻。 unix-cron 文字列形式で設定。
  • resource.pubsubTarget … トリガーするPub/Subトピックの設定。topicNamedataを必須で設定する。dataはBase64エンコードする。
  • auth … 前段で取得した認証情報

なお、resource.pubsubTarget の箇所はトリガーするターゲットに応じてプロパティが変わるので注意してください。

const projectId = '[PROJECT_ID]'; // プロジェクトID
const locationId = '[LOCATION_ID]'; // ロケーション名(Cloud Functionsのデフォルトはus-central1)
const topicName = '[TOPIC_NAME]'; // トリガーするPub/Subのトピック名
const jobName = '[JOB_NAME]'; //作成するCronジョブ名

const request = {
  parent: `projects/${projectId}/locations/${locationId}`,
  resource: {
    name: `projects/${projectId}/locations/${locationId}/jobs/${jobName}`,
    schedule: '0 7 * * 1-5', // 定期実行する時刻 今回は月~金のAM7:00に設定
    pubsubTarget: {
      topicName: `projects/${projectId}/topics/${topicName}`,
      data:  Buffer.from('Cron triggered.').toString('base64'),
    },
  },
  auth: client,
};
// APIリクエスト
cloudScheduler.projects.locations.jobs.create(request, (err, response) => {
  if (err) {
    console.error(err);
    return;
  }
  return response.data;
});
},

無事ジョブが作成されると、レスポンスで作成されたジョブの情報が返却されます。

③ ジョブの変更

一度作成したジョブを変更する際は projects.locations.jobs.patch を使います。
以下の例では、先程作成したジョブの設定時刻を7時から8時に変更します。

const client = await google.auth.getClient({
  scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});
const projectId = '[PROJECT_ID]'; // プロジェクトID
const locationId = '[LOCATION_ID]'; // ロケーション名(Cloud Functionsのデフォルトはus-central1)
const jobName = '[JOB_NAME]'; // 更新するCronジョブ名

const request = {
  name: `projects/${projectId}/locations/${locationId}/jobs/${jobName}`,
  updateMask: 'schedule',
  resource: {
    schedule: '0 8 * * 1-5', // AM8:00に設定変更
  },
  auth: client,
};

cloudScheduler.projects.locations.jobs.patch(request, (err, response) => {
  if (err) {
    console.error(err);
    return;
  }
  return response.data;
});

作成時と異なるのはpubsubTargetが不要となる点と、リクエストにupdateMaskを指定する点です。
updateMaskはジョブのどのフィールドを更新するかを指定するパラメータです。設定しないと更新されないので必ず設定します。

おわりに

API経由でcronジョブをスケジューリングするような記事があまりなかったので、参考になれば幸いです。
内容に誤りや改善点があればコメントいただけるとありがたいです!

参考記事

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

Jestの結果がおかしくなったら--clearCacheしよう。

ある日の出来事...

.vueでレイアウトちょっと変えただけだけどいちおうテストしとこうっと。

やぁ!

yarn run test:unit

結果はどうかな?

pre.png

...なんかめちゃめちゃやんけ!!

Uncovered Lineの表示もソースコードと全然合っていません。

こんな時は慌てず騒がず(騒いだけど...)キャッシュをクリアしましょう。

うりゃ〜!

yarn run test:unit --clearCache
$ vue-cli-service test:unit --clearCache
Cleared /var/folders/2l/gq85xz3j06x5y3rzsmlnq8vr00172j/T/jest_uvl

みたいになります。

再びテストします!やぁ!!

yarn run test:unit

aft.png

ばっちりですね。

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

Javascriptでネストされたオブジェクトの階層構造を可視化してみる

今日必要になったので。
とりあえず以下でできました↓

const nestViewer = (obj, prefix = '') => {
  for (const key in obj) {
    if (typeof obj[key] == 'object') {
      if (Array.isArray(obj[key])) {
        obj[key].forEach((item, index) => {
          if (typeof item != 'object') {
            console.log({ key: `${prefix && prefix + '.'}${key}[${index}]`, value: item })
          } else {
            nestViewer(item, `${prefix && prefix + '.'}${key}[${index}]`)
          }
        })
      } else {
        nestViewer(obj[key], `${prefix && prefix + '.'}${key}`)
      }
    } else {
      console.log({ key: `${prefix && prefix + '.'}${key}`, value: obj[key] })
    }
  }
}

こんな感じの地獄のようなObjectがあったとして


{
  "a": 1,
  "b": [4, 5, 6],
  "c": {
    "a": 7,
    "b": [8, 9, 10],
    "c": {
      "a": 11,
      "b": [12, 13, 14],
      "c": {
        "a": 15,
        "b": [16, 17, 18],
        "c": {
          "a": 19,
          "b": [20, 21, 22],
          "c": {
            "a": 23,
            "b": [24, 25, 26],
            "c": {}
          }
        }
      }
    }
  },
  "d": [
    {
      "a": 7,
      "b": [8, 9, 10],
      "c": {
        "a": 11,
        "b": [12, 13, 14],
        "c": {}
      }
    }
  ]
}

実行結果

{key: "a", value: 1}
{key: "b[0]", value: 4}
{key: "b[1]", value: 5}
{key: "b[2]", value: 6}
{key: "c.a", value: 7}
{key: "c.b[0]", value: 8}
{key: "c.b[1]", value: 9}
{key: "c.b[2]", value: 10}
{key: "c.c.a", value: 11}
{key: "c.c.b[0]", value: 12}
{key: "c.c.b[1]", value: 13}
{key: "c.c.b[2]", value: 14}
{key: "c.c.c.a", value: 15}
{key: "c.c.c.b[0]", value: 16}
{key: "c.c.c.b[1]", value: 17}
{key: "c.c.c.b[2]", value: 18}
{key: "c.c.c.c.a", value: 19}
{key: "c.c.c.c.b[0]", value: 20}
{key: "c.c.c.c.b[1]", value: 21}
{key: "c.c.c.c.b[2]", value: 22}
{key: "c.c.c.c.c.a", value: 23}
{key: "c.c.c.c.c.b[0]", value: 24}
{key: "c.c.c.c.c.b[1]", value: 25}
{key: "c.c.c.c.c.b[2]", value: 26}
{key: "d[0].a", value: 7}
{key: "d[0].b[0]", value: 8}
{key: "d[0].b[1]", value: 9}
{key: "d[0].b[2]", value: 10}
{key: "d[0].c.a", value: 11}
{key: "d[0].c.b[0]", value: 12}
{key: "d[0].c.b[1]", value: 13}
{key: "d[0].c.b[2]", value: 14}

以上

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

ネストされたオブジェクトの階層構造を可視化してみる

今日必要になったので。
とりあえず以下でできました↓

const nestViewer = (obj, prefix = '') => {
  for (const key in obj) {
    if (typeof obj[key] == 'object') {
      if (Array.isArray(obj[key])) {
        obj[key].forEach((item, index) => {
          if (typeof item != 'object') {
            console.log({ key: `${prefix && prefix + '.'}${key}[${index}]`, value: item })
          } else {
            nestViewer(item, `${prefix && prefix + '.'}${key}[${index}]`)
          }
        })
      } else {
        nestViewer(obj[key], `${prefix && prefix + '.'}${key}`)
      }
    } else {
      console.log({ key: `${prefix && prefix + '.'}${key}`, value: obj[key] })
    }
  }
}

こんな感じの地獄のようなObjectがあったとして


{
  "a": 1,
  "b": [4, 5, 6],
  "c": {
    "a": 7,
    "b": [8, 9, 10],
    "c": {
      "a": 11,
      "b": [12, 13, 14],
      "c": {
        "a": 15,
        "b": [16, 17, 18],
        "c": {
          "a": 19,
          "b": [20, 21, 22],
          "c": {
            "a": 23,
            "b": [24, 25, 26],
            "c": {}
          }
        }
      }
    }
  },
  "d": [
    {
      "a": 7,
      "b": [8, 9, 10],
      "c": {
        "a": 11,
        "b": [12, 13, 14],
        "c": {}
      }
    }
  ]
}

実行結果

{key: "a", value: 1}
{key: "b[0]", value: 4}
{key: "b[1]", value: 5}
{key: "b[2]", value: 6}
{key: "c.a", value: 7}
{key: "c.b[0]", value: 8}
{key: "c.b[1]", value: 9}
{key: "c.b[2]", value: 10}
{key: "c.c.a", value: 11}
{key: "c.c.b[0]", value: 12}
{key: "c.c.b[1]", value: 13}
{key: "c.c.b[2]", value: 14}
{key: "c.c.c.a", value: 15}
{key: "c.c.c.b[0]", value: 16}
{key: "c.c.c.b[1]", value: 17}
{key: "c.c.c.b[2]", value: 18}
{key: "c.c.c.c.a", value: 19}
{key: "c.c.c.c.b[0]", value: 20}
{key: "c.c.c.c.b[1]", value: 21}
{key: "c.c.c.c.b[2]", value: 22}
{key: "c.c.c.c.c.a", value: 23}
{key: "c.c.c.c.c.b[0]", value: 24}
{key: "c.c.c.c.c.b[1]", value: 25}
{key: "c.c.c.c.c.b[2]", value: 26}
{key: "d[0].a", value: 7}
{key: "d[0].b[0]", value: 8}
{key: "d[0].b[1]", value: 9}
{key: "d[0].b[2]", value: 10}
{key: "d[0].c.a", value: 11}
{key: "d[0].c.b[0]", value: 12}
{key: "d[0].c.b[1]", value: 13}
{key: "d[0].c.b[2]", value: 14}

以上

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

画像をドラッグ&ドロップでアップロード &

画像をドラッグ&ドロップでアップロード

画像をドラッグ&ドロップで並び替える
方法をご紹介します!

用途

webサイトとかで、施設に画像を結びつけて、かつ、それをソート順で表示したいとかあるときに使えるよ。

ついでに画像も削除できます。
ついでに画像に説明文も登録できます。

画像のドラッグ&ドロップでアップロードと並び替え.png

参考サイト

https://codepen.io/malkafly/pen/gbVYZb
↑ほぼこれを丸パクリです。ありがとうございます!!!

使うプラグイン

  • みんな大好きDropzoneJS
  • jQuey UIのSortable 私はここからyarnで追加しました

コード

html側

<form method="post" action="/api/v1/facility-image/upload/" id="my-awesome-dropzone" class="image-upload-area dropzone">
    <input type="hidden" name="client_id" value="{{ $facility->id }}"></div>
</form>

<ul class="visualizacao sortable dropzone-previews"></ul>
<div class="preview" style="display:none;">
    <li>
        <div>
            <div class="dz-preview dz-file-preview">
                <img class="facility-image" data-dz-thumbnail />
                <input type="text" class="caption">
                <button type="button" class="btn btn-info caption-button" data-image-id="">説明文登録</button>
                <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
                <div class="dz-error-message"><span data-dz-errormessage></span></div>
            </div>
        </div>
    </li>
</div>

javascript側

import Dropzone from 'dropzone'

require('es-jquery-sortable')

let myDropzone = {}

dropzoneSetting()

//画像の並び替え
$(document).ready(function(){
    let group = $('.sortable').sortable({
        group: 'sortable',
        onDrop: function($item, container, _super) {
            var data = group.sortable('serialize').get()
            _super($item, container)

            //DB上の並び順を更新する
            axios.post('/api/v1/facility-image/update-order/', {
                    data: data,
                })
                .catch((error) => {
                    swal('', '並び順の更新に失敗しました', 'error')
                    console.log(error)
                })
        }
    })
})

/**
 * dropzone.jsの処理
 * 
 */
async function dropzoneSetting() {
    Dropzone.autoDiscover = false

    //最大ファイルアップロードサイズ
    let maxFileSize = 3

    //企業ID
    let clientId = $('#client-id').data('clientId')

    myDropzone = new Dropzone('.image-upload-area', {
        url: '/api/v1/facility-image/upload/',
        params: {
            client_id: clientId,
        },
        dictDefaultMessage: 'ここにファイルをドラッグ&ドロップ、または、クリックしてファイルを選択',
        dictRemoveFile: '',
        dictCancelUpload: '',
        dictFileTooBig: 'ファイルサイズは' + maxFileSize + 'MBまで',
        dictInvalidFileType: 'JPEG,PNGのみ可能',
        addRemoveLinks: true,
        maxFilesize: maxFileSize,
        acceptedFiles: 'image/*',
        parallelUploads: 1,
        uploadMultiple: false,
        previewsContainer: '.visualizacao', 
        previewTemplate : $('.preview').html(),
        renameFile: (file) => {
            let fileName = new Date().getTime() + '_' + file.size

            let extension = file.name.split('.').pop()
            return fileName + '.' + extension
        },
    })

    //アップロードに失敗した場合
    myDropzone.on('error', async function(file, errorMessage) {
        //ファイル数が多い場合
        if(errorMessage === '最大8ファイルまでしか添付できません') {
            myDropzone.removeFile(file)
            swal('', errorMessage, 'warning')
        } else if(errorMessage === 'セッションが切れました。もう一度ログインしてください。') {
            await swal('', 'セッションが切れました。もう一度ログインし直してください。', 'warning')
            location.replace(location.href)
        }
    })

    //画像を削除したときに、サーバー上のファイルも削除する
    myDropzone.on("removedfile", function(file) {
        let imageId = file.imageId

        //画像ID
        axios.get('/api/v1/facility-image/delete-by-image-id/' + imageId)
            .catch((error) => {
                console.log(error)
            })
    })

    //既存の画像が表示されたときに情報を追加する
    myDropzone.on("addedfile", function(file) {
        $('.dz-preview').each(function() {
            let imageId = $(this).children('.caption-button').data('imageId')

            //画像IDが設定されていない場合のみ追加する
            if(!imageId) {
                let imageId = file.imageId

                //今回追加した画像の場合は飛ばす
                if(!imageId) {
                    return
                }

                $(this).children('.caption-button').data('imageId', imageId)

                let caption = file.caption
                $(this).children('.caption').val(caption)

                $(this).parents('li').data('imageId', imageId)
            }
        })
    })

    //新しい画像がアップロードされたときに、画像IDを追加する
    myDropzone.on("success", function(file, response) {
        $('.dz-preview').each(function() {
            let existImageId = $(this).children('.caption-button').data('imageId')

            //画像IDが設定されていない場合のみ追加する
            if(!existImageId) {
                let imageId = response
                $(this).children('.caption-button').data('imageId', imageId)
                $(this).parents('li').data('imageId', imageId)
            }
        })
    })

    //すでに保存されている画像を表示する
    $('.exist-image').each(function() {
        let path = $(this).data('path')
        let info = {
            name: '',
            size: '',
            imageId: $(this).data('id'),
            caption: $(this).data('caption')
        }
        myDropzone.emit("addedfile", info)

        if(path) {
            myDropzone.emit("thumbnail", info, path)
        } else {
            let base64 = 'data:' + $(this).data('type') + ';base64,' + $(this).data('binary')
            myDropzone.emit("thumbnail", info, base64)
        }

        myDropzone.emit("complete", info)
    })

    //画像の説明文を登録
    $(document).on('click', '.caption-button', function() {
        let input = $(this).prev()

        axios.post('/api/v1/facility-image/save-caption/', {
                caption: input.val(),
                image_id: $(this).data('imageId'),
            })
            .then(() => {
                swal('', '説明文を登録しました', 'success')
            })
            .catch((error) => {
                swal('', '説明文の登録に失敗しました', 'error')
                console.log(error)
            })
    })
}

まとめ

php側(私はLaravel使用しています)は、自分で実装してくださいね。
cssも自分で書いてね。

全然コードの説明してなくて、すみません。
いや、こういうの毎回やろうと思ったときに、過去のソースひっぱってくるの大変なので、Qiitaに置きました...

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

JavaScriptとElmを比べてみた〜後編・Vue.jsとも比べてみた〜

※前編はこちらやで。

ハスケル子「引き続き、JavaScriptElm・・・」
ハスケル子「そしてVue.jsもちょこっと比べてみましょう」

オブジェクト(のようなもの)

JavaScriptのオブジェクト

const takashi = {
    displayName: "たかし",
    age: 36,
    height: 173,
    weight: 73
};

ワイ「displayNameとかageとかが、プロパティいうやつやな」
ワイ「takashi.ageとかすると、プロパティの値にアクセスできんねん」

Elmのレコード

takashi =
    { displayName = "たかし"
    , age = 36
    , height = 173
    , weight = 73
    }

ワイ「Elmではレコードいうねんな」
ワイ「JSのオブジェクトとけっこう似てるな」
ワイ「displayNameとかageとかのことは」
ワイ「フィールドっていうんやな」
ワイ「JSと同じくtakashi.ageって書けばフィールドの値にアクセスできんねん」

オブジェクトのプロパティを更新

JavaScriptのオブジェクト

takashi.age = 37;
takashi.weight = 83;

ワイ「constで宣言した再代入不可なオブジェクトでも、プロパティは変更できてまうんやな」

Elmのレコード

newTakashi =
    { takashi | age = 37, weight = 83 }

ワイ「Elmでは全ての値が不変やからtakashi.age = 37みたいな上書きはできひんねん
ワイ「せやから、1つ歳をとって10Kg太ったnewTakashiという新しいレコードを作る形になる」
ワイ「元々のtakashiは36歳のまま、別に存在すんねん」

ドキュメントの書き方

JavaScript (JSDoc)

/**
 * @param  {Number} num
 * @param  {String} str
 * @return {String}
 */
function displayNumber (num, str) {
    return num + str;
}

ワイ「関数の使い方が分かりやすいように」
ワイ「引数や戻り値の型をコメントで書いたりすんねんな」
ワイ「まぁ、実装とズレてても動くから、努力目標やけどな・・・」

Elmの場合は型で表現

displayNumber : Int -> String -> String
displayNumber num str =
    String.fromInt num ++ str

ワイ「displayNumber : Int -> String -> Stringいうのは」
ワイ「このdisplayNumberいう関数は」
ワイ「引数としてInt型の値とString型の値を受け取って」
ワイ「戻り値としてString型の値を返す」
ワイ「そんな関数ですよ〜っていう」
ワイ「型注釈いうやつや」

ワイ「しかも、この型注釈で書いた通りに実装せえへんと」
ワイ「ちゃんとコンパイルエラーが出て教えてくれんねん」
ワイ「TYPE MISMATCH(型の不一致)です〜、言うてな」

ワイ「つまり、強制力のある注釈や!」

ビューの書き方

Vue.jsの場合(単一ファイルコンポーネント)

<template>
  <div class="container">
    <button>増やす</button>
    <input type="text">
    <button>減らす</button>
  </div>
</template>

ワイ「ほぼhtmlやな」
ワイ「読みやすいな」

Elmの場合

view model =
    div [ class "container" ]
        [ button [] [ text "増やす" ]
        , input [ type_ "text" ] []
        , button [] [ text "減らす" ]
        ]

ワイ「このdivとかbuttonていうのがタグ名やね・・・?」

ハスケル子「まあそうなんですけど」
ハスケル子「タグ名というより、れっきとしたElmの関数です」
ハスケル子「関数なので───」

joinButton =
    button [] [ text "参加する" ]

ハスケル子「───こんな感じで変数に格納すれば」
ハスケル子「それだけでコンポーネントみたいに使えますし」

commonButton buttonText =
    button [] [ text buttonText ]

ハスケル子「↑こう、引数としてテキストを受け取る関数にすれば」
ハスケル子「propsを受け取って表示するタイプのコンポーネントもサクッと作れます」

ワイ「おお」
ワイ「コンポーネントも関数そのものやから、コードの中で自然に使えるな」

ハスケル子「そうなんです」
ハスケル子「リスト1の分だけ回してli要素を生成したい、なんて場合も簡単です」

イベントリスナ登録(のようなもの)

Vue.jsの場合

<button @click="incrementFunc">増やす</button>
<input type="text">
<button @click="decrementFunc">減らす</button>

ワイ「見たままやな」
ワイ「ボタンをクリックするとincrementFuncdecrementFuncという」
ワイ「関数が実行されんねんな」
ワイ「関数はmethodsの中に書いとけばええんや」

Elmの場合

button [ onClick Increment ] [ text "増やす" ]
, input [ type "text" ] []
, button [ onClick Decrement ] [ text "減らす" ]

ワイ「こう書いておけば、このボタンをクリックした時に・・・?」

ハスケル子「IncrementまたはDecrementというメッセージが生み出されます」

ワイ「メッセージ・・・?」
ワイ「そのメッセージはどこで受け取るん?」

ハスケル子「状態の更新内容を定義するupdateっていう関数で受け取ります」

update msg model =
    case msg of
        Increment ->
            { model | int = model.int + 1 }
        Decrement ->
            { model | int = model.int - 1 }

ハスケル子「Incrementというメッセージが来たら」
ハスケル子「model・・・つまりVuexでいうstorestateみたいなもんですね」
ハスケル子「要はmodelイコール状態です」
ハスケル子「そのmodelの中のint1増加させます」
ハスケル子「メッセージがDecrementだった場合は1減らす感じですね」
ハスケル子「そして、それによって生成された新しいmodelを戻り値として返すって感じです」

ワイ「ほえ〜、新しいmodel、つまり新しい状態を返すと」
ワイ「それがリアクティブにビューに反映されるっていうこと?」

ハスケル子「そうです」

ワイ「そうなんやね〜」
ワイ「Elmって、VueとかReactみたいに仮想DOMを内蔵してたんやね」
ワイ「あとVuexやRedux相当の機能もか」

ハスケル子「そうです」
ハスケル子「っていうかVuexReduxも、Elmの影響を受けてます」
ハスケル子「Elmというか、今みたいなThe Elm Architectureというパターンの影響ですね」
ハスケル子「しかもElmはとってもシンプルなので」
ハスケル子「学習コストが低くて、やめ太郎さんにピッタリです」

ワイ「どういう意味やねん

ハスケル子「オンラインエディタでさっきのカウンタのサンプルコードを色々いじってみると」
ハスケル子「更に分かると思いますよ」

ワイ「やってみるわ」
ワイ「おおきにやで、ハスケル子ちゃん」

ハスケル子「Vue勉強しててもReact勉強してても」
ハスケル子「副作用を起こさないように、とか」
ハスケル子「純粋な・・・つまり参照透過的な関数を書こう、とか」
ハスケル子「色んなドキュメントに書いてあるんですよ」
ハスケル子「じゃあ、そういう風にしか書けないElmやりゃあいいんですよ

ワイ「お、おう・・・」
ワイ「前向きに検討するわ・・・
ワイ「っていうか、今回JSやVueと比較したおかげでElmの文法がかなり分かったから」
ワイ「普通に読み書きできそうやな・・・」
ワイ「やってみるで!

〜おしまい〜


  1. 配列みたいなやつやで。 

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

WKWebViewでwindow.open()をオーバーライドして自画面遷移にした話

経緯

WKWebViewを使用したWebページの表示・遷移をしていた際にアプリが落ちた。

window.open().location.hrefで別ウィンドウを表示しているのが悪いらしい。

一律で自画面遷移にしてしまえwってことでやってみた。

やったこと

以下のスクリプトをWKNavigationDelegatefunc webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)内で実行。

window.open()windowを返すようにすることで、window.open().location.hrefwindow.location.hrefとなるようにしたら自画面遷移になった。

スクリプト

window.open = function (open) {
  return function(){
    return window;
  };
}(window.open);

コード

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  webView.evaluateJavaScript("window.open = function (open) { return function(){return window;};}(window.open);")
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

eslint prettier huskey を使った簡単環境構築

環境を構築するメリット

eslint

  • 一定のコーディングルールを儲けることができる為、コードレビュー負担の削減や可読性の向上を期待できます。
  • プラグインを設定することで即時エラーを確認することができる為、開発スピードを向上できます。

prettier

  • コードフォーマット機能を備えており、コードフォーマットの統一による可読性の向上を期待できます。
  • インデント等を気にせずプログラミングが可能な為、開発スピードを向上できます。

huskey

  • コミット時に追加ファイルをeslintprettierによる自動修正が可能。
  • 自動修正できない場合は、コミット時にエラーがある場合はエラー内容が出力されコミットができない。
  • 定めた条件をクリアしたものだけがコミットできる為、レビュアーの負担を軽減できます。

eslint prettier huskey で作る簡単環境構築

導入する際の、基本的な構築手順を記載します。
導入後に、必要に応じてルールの追加やプラグインの追加も可能です。

1. eslintをinstall

npm install eslint  --save-dev

2. eslintの初期化

./node_modules/eslint/bin/eslint.js init

いくつか質問されるので、好みやプロジェクトに合わせて選択する。
→ eslintの設定ファイル .eslintrc(ファイル形式選択可能) が作成される。
※ style guideについてはいくつか選択肢がありますが、迷ったら最初は厳しすぎない Standard がオススメです。

3. prettierをinstall

npm install prettier eslint-config-prettier eslint-plugin-prettier --save-dev

4. eslintrcファイルにprettierの設定を追加

extends

  "extends": [
    "plugin:prettier/recommended"
  ]

extends(配列)に設定を追記します。

rule

  "rules": {
    "prettier/prettier": [
      "error",
      {
        "singleQuote": true,
        "semi": false
      }
    ]
  }

シングルクォートとセミコロンの設定を追加してます。
ここに追加したいルールを追加することでカスタマイズ可能です。

カスタマイズ例

  "rules": {
    "prettier/prettier": [
      "error",
      {
        "singleQuote": true,
        "semi": false
      }
    ],
    "no-var": "error",
    "prefer-const": "error",
    "object-shorthand": "error",
    "prefer-arrow-callback": "error"
  }

5. huskey lint-staged をinstall

npm install husky lint-staged  --save-dev

6. huskeyの設定追加

package.json に追加

{
  "lint-staged": {
    "*.js": [
      "eslint --fix",
      "git add"
    ]
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  }
}

"scripts": {...},などと同階層に追加します。

おまけ. VSCode (Visual Studio Code) で保存時自動修正

  1. プロジェクトルート配下に、.vscodeディレクトリを作成
  2. .vscode 配下に下記ファイルを配置

setting.json

{
  "eslint.nodePath": "./node_modules/eslint/bin/eslint.js",
  "eslint.autoFixOnSave": true
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

既存のVue.jsのプロジェクトにJestでのテスト環境を構築する

TL;DR

  • VueのプロジェクトにJestによる環境を構築するためにやったことをまとめた
  • Jestについては説明しない

ディレクトリ構成

├── jest.config.js
├── package.json
├── src
│   ├── js
│   │   ├── components
│   │   └── main.js
│   └── sass
├── tests
│   └── unit
└── webpack.config.js

パッケージのインストール

とりあえず基本的なものをインストールします。

$ npm i -D @vue/test-utils jest vue-jest babel-jest

package.jsonにテストを走らせるためのタスクを追加します。

package.json
"scripts": {
  "test:unit": "jest"
}

Jestの設定をする

jest.config.js
module.exports = {
  moduleFileExtensions: ["js", "jsx", "json", "vue"],
  transform: {
    "^.+\\.vue$": "vue-jest",
    "^.+\\.jsx?$": "babel-jest"
  },
  transformIgnorePatterns: ["node_modules/"],
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/js/$1"
  },
  testMatch: [
    "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
  ]
};

試してみる

$ npm run test:unit

> vue.build@1.0.0 test:unit /Users/xxx/project/vue-jest-test
> jest

 PASS  tests/unit/example.spec.js
  HelloWorld.vue
    ✓ renders props.msg when passed (15ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.206s, estimated 5s
Ran all test suites.

スナップショットテストを導入する

$ npm i -D jest-serializer-vue

jest.config.js

jest.config.jsに以下を追加する。

module.exports = {
  ...
  snapshotSerializers: ["jest-serializer-vue"]
  ...
};

追加後のjest.config.js

module.exports = {
  moduleFileExtensions: ["js", "jsx", "json", "vue"],
  transform: {
    "^.+\\.vue$": "vue-jest",
    "^.+\\.jsx?$": "babel-jest"
  },
  transformIgnorePatterns: ["node_modules/"],
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/js/$1"
  },
  snapshotSerializers: ["jest-serializer-vue"],
  testMatch: [
    "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
  ]
};

結果

$ npm run test:unit

> vue.build@1.0.0 test:unit /Users/xxx/project/vue-jest-test
> jest

 PASS  tests/unit/SampleComp.spec.js
  SampleComp.vue
    ✓ renders props.text when passed (16ms)

 › 1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 written, 1 total
Time:        1.429s
Ran all test suites.

実行すると、tests/unit/__snapshots__以下にsnapshotのファイルが格納される。

参考

Snapshot Testing

カバレッジを表示させたい場合

jest.config.jsに以下を追加する。

module.exports = {
  ...
  "collectCoverage": true,
  "collectCoverageFrom": ["src/js/**/*.{js,vue}"]
  ...
};

追加後のjest.config.js

module.exports = {
  moduleFileExtensions: ["js", "jsx", "json", "vue"],
  transform: {
    "^.+\\.vue$": "vue-jest",
    "^.+\\.jsx?$": "babel-jest"
  },
  transformIgnorePatterns: ["node_modules/"],
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/js/$1"
  },
  snapshotSerializers: ["jest-serializer-vue"],
  testMatch: [
    "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
  ],
  testURL: "http://localhost/",
  "collectCoverage": true,
  "collectCoverageFrom": ["src/js/**/*.{js,vue}"]
};

エラーが出る場合

Cannot read property 'bindings' of nullとのエラーが出る場合、

$ npm i -D @babel/preset-env

.babelrcを下記の通り修正する。

{ "presets": ["env"] }

{ "presets": ["@babel/preset-env"] }

Upgrade to Babel 7: Cannot read property 'bindings' of null

参考

Vue Test Utils + Jest でVue.jsの単体テストを行う
Configuring Jest

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

MathBox でトランジションアニメーション(フェードイン/フェードアウト)を作る

 スライドを作ったらトランジションアニメーションも入れたいところです。とりあえず基本のフェードイン/フェードアウトを入れてみます。どんどんメソッドチェーンが長くなるので、一つ一つ区切って理解していくと良いと思います。

フェードイン/フェードアウト

 前回のコードを元に追加していきましょう。
 ふわっと出したり消したい場合は reveal() を使います。 present.slide().reveal().interval() のように適当な位置に差し込みます。スライド開始でフェードインさせたいので、 slide() の後ろに reveal() を入れています。ここで一つ注意が必要で、トランジションの単位でも end() を入れる必要があるということです。ここではスライドが切り替わるタイミングでフェードアウトさせたいので、スライド区切りの end() の手前にもう一つ end() を入れています。

 以下のコードでは一枚目のスライドのみにフェードイン/フェードアウトを適用しています。 end() を削除してみて動作の違いを確認してみて下さい。

 また、同じようにして二枚目もフェードイン/フェードアウトさせるようにしてみて下さい。

present.slide().reveal().interval({
  width: 2,
  expr: function (emit, x, i, time) {
    emit(x, 0);
  },
  items: 1,
  channels: 2,
}).line({
  color: 0x3090FF,
  width: 10,
}).end().end()
  .slide().interval({
    width: 128,
    expr: function (emit, x, i, time) {
      var d = Math.sin((x + time) * 2);
      emit(x, d * .5);
    },
    items: 1,
    channels: 2,
  }).line({
    color: 0xFF3090,
    width: 10,
  });

パラメータの指定

 遷移の期間を少し短くしてみましょう。 reveal({ duration: 0.1 }) のようにパラメータを渡してみましょう。 duration のデフォルト値は 0.3 なので三倍早くなるはずです。他にも色々なパラメータがあるので、ドキュメントを見ながら試してみて下さい。

コード全体

 二枚目のスライドにも reveal() を適用したコード全体は下記の通りです。

<!DOCTYPE html>
<html>
    <head>
    <meta charset="utf-8">
    <title>MathBox - XYZW Test</title>
    <script src="../../build/mathbox-bundle.js"></script>
    <link rel="stylesheet" href="../../build/mathbox.css">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1">
    </head>
    <body>
    <script>
     mathbox = mathBox({
         plugins: ['core', 'controls', 'cursor'],
         controls: {
         klass: THREE.OrbitControls
         },
     });
     three = mathbox.three;

     three.camera.position.set(2.3, 1, 2);
     three.renderer.setClearColor(new THREE.Color(0xFFFFFF), 1.0);

     view = mathbox.cartesian({
         range: [[-6, 6], [-1, 1], [-1, 1]],
         scale: [6, 1, 1],
     });

     present = view.present({
         index: 1
     })

     if (window == top) {
         window.onkeydown = function (e) {
         switch (e.keyCode) {
             case 37:
             case 38:
             present.set('index', present.get('index') - 1);
             break;
             case 39:
             case 40:
             present.set('index', present.get('index') + 1);
             break;
         }
         }
     }

     present.slide().reveal({
         duration: 0.1,
     }).interval({
         width: 2,
         expr: function (emit, x, i, time) {
         emit(x, 0);
         },
         items: 1,
         channels: 2,
     }).line({
         color: 0x3090FF,
         width: 10,
     }).end().end()
        .slide().reveal({
            duration: 0.1,
        }).interval({
            width: 128,
            expr: function (emit, x, i, time) {
            var d = Math.sin((x + time) * 2);
            emit(x, d * .5);
            },
            items: 1,
            channels: 2,
        }).line({
            color: 0xFF3090,
            width: 10,
        });

     present.grid({
         stroke: 'dashed',
     });
    </script>
    </body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ZIP圧縮されたファイルのbase64文字列からファイルを表示する。

※ほぼ自分用メモ。
e-Gov法令APIでは,法令に書式等が含まれている場合,その書式の画像やPDFを一つのZIPに纏めたものをBase64文字列にしたものを,ImageData要素のテキストとして返します。
そこで,このBASE64文字列から元のファイルを復元し,たとえばHTML内に表示したりすることが考えられます。
ImageData要素からのテキストの取り出しはまぁ後でやるとして,とりあえずファイルをちゃんと取り出すところまで。
法技研六法に組み込んだら,もうちょっと丁寧に書き直すかも。

  1. Base64文字列をバイナリに変換
  2. バイナリ文字列をUint8Arrayに変換
  3. Zlib.Unzipを使って,2.のUint8Arrayを解凍
  4. 解凍したものに含まれる各ファイル(Uint8Arrayになっている)をBase64文字列に変換
  5. Base64文字列ファイルを,HTMLに埋め込む。

1. Base64文字列をバイナリに変換

var imageData = "UEsDBBQACAgIAJx4204AAA(中略)AAAA";
var binaryString = atob(imageData);

2 バイナリ文字列をUint8Arrayに変換

var u8a = Uint8Array.from(binaryString.split(""), e => e.charCodeAt(0))

3. Zlib.Unzipを使って解凍

var unzipped = new Zlib.Unzip(u8a);

4. 各ファイルをBase64文字列に変換

var b64s = [];
var filenames = unzipped.getFilenames();
for(var i in filenames ){
    var buffer = unzipped.decompress( filenames[i] );
    var arr = Array.from(buffer, e => String.fromCharCode(e)String.fromCharCode(e)).join(""));
    b64s[i] = arr.join("");
}

5. HTMLに埋め込む(PDFの場合)

for(var i in b64s){
  var obj = document.createElement("object");
  obj.setAttribute("type", "application/pdf" );
  obj.setAttribute("data", "data:application/pdf;base64,"+b64s[i]);
  obj.setAttribute("width", "100%");
  obj.setAttribute("height", "100%");

  document.body.appendChild(obj);
}

参考文献:
-http://var.blog.jp/archives/62330155.html
-https://github.com/imaya/zlib.js
-https://qiita.com/katzueno/items/c490361c3274d0108e7d
-https://stackoverflow.com/questions/43234936/providing-the-object-tag-with-data-directly
-https://www.e-gov.go.jp/elaws/pdf/houreiapi_shiyosyo.pdf

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

【HTML5】WEBページ上でお絵かき!『Canvas』ハイパーゆとり向けチュートリアルPart2

※記事最下部にHTMLファイルの内容が掲載されております。

マウスのクリックや投下状態(これらはマウスイベントと言われています)を検知して、実際に絵を描くことができるページを作ってみましょう!

今回はこちらの参考サイト様を頼りにプログラムを作っていきます!
ではいきましょう!

※こちらの記事は、本チュートリアル記事Part1をお読みになった方か、canvasの前提知識をある程度持っている方向けに書かれております。

チュートリアル続き

まずはPart0、1でやってきた流れをさらっと復習

<body onload="init();">
  <canvas id="chokowa" width="500" height="300"></canvas>
</body>

Canvas領域を定義!

<script>
  function init() {
    // code here.
  }
</script>

init関数を定義!Canvasスクリプトコードを書き込むのはこの中でしたね!

ここからjsコードの解説に移ります!

var canvas = document.getElementById('chokowa');
var context = canvas.getContext('2d');

これもPart1の通りです!IDをトリガーにしてcanvas領域を有効化、そのcanvas領域にcontextという図形の概念的なものを設置します!

復習は以上!ついにお絵描きツールの設定を行っていきます!

var drawing = false;
var before_x = 0;
var before_y = 0;

・いみ
いちぎょうめはまだおえかきしてないっていみです。
にぎょうめとさんぎょうめは、むずかしいおはなしになります。
きゃんばすではふるいてんとあたらしいてんをつないでせんにするので、ここではふるいてんをせっていしています。

1行目に関してはとりあえず抽象的な理解だけしておきましょう!drawing、つまり線を書いている状態かどうかを判定する変数です。デフォルトはもちろんfalse=書いてない状態ですね!

2、3行目ですが、これに関してはそれはそれは深いお話で、、、
まず、マウスを利用したcanvasを利用したお絵描きツールは多くの方々に作られています。
そしてそれらの線描画(=お絵描き)の原理のほとんどは「さっきまでマウスポインタが置かれていた点」「今マウスポインタが置かれている」『線で繋ぐ』というものです。
今回作るお絵描きツールも例にもれず同じ原理を採用しています。
ここまできたら想像できるでしょうか・・・!このbefore_○というのは「さっきまでマウスポインタが置かれていた点」の定義にあたります!

Canvasお絵描きツールの基礎概念を理解した(?)ところで、マウスイベントによる動作を定義していきます。

canvas.addEventListener('mousemove', draw_canvas);

canvas.addEventListener('mousedown', function(e) {
  drawing = true;
  var rect = e.target.getBoundingClientRect();
  before_x = e.clientX - rect.left;
  before_y = e.clientY - rect.top;
});

canvas.addEventListener('mouseup', function() {
  drawing = false;
});

・いみ
まうすをおしたりはなしたりうごかしたりしたときのせっていをしています。

一気に来ましたね!まずは状況の把握から!!!
「canvas.addEventListener(...」という行が三つありますね!そしてそれぞれその後に『mousemove』、『mousedown』、『mouseup』などと書いてあります!
これら三つに関しては察しがつきましたでしょうか!それぞれ、『マウスが動いているとき』、『マウス(の右クリック)が投下されたとき』『マウスの投下が解除されたとき』のイベントのことです!

では、「addEventListener()」とは・・・?
(その前のcanvasはもう分かりますね!canvasオブジェクトに対してaddEventListener()で行われる処理を適用するという意味あいです!)

理解するにあたって、一番下のmouseupイベントの部分が分かりやすいと思うので、そこを基準に解説していきます!

canvas.addEventListener('mouseup', function() {
  drawing = false;
});

・いみ
まうすがはなれたらせんをかくのをやめます。

そもそもaddEventListener()は、次のように定義されています!

[オブジェクト名].addEventListener('[イベント]', [イベントを検知した場合に実行する関数])

[イベント]というのは様々で、上記のマウスが動く、投下される、投下が解除される・・・といったものの他に、タッチ操作可能デバイス上でタッチされた、タッチされたままスライドされた、タッチが離れた・・・などもあり、それぞれmouse○○、touch○○といった名前が定義されています。
(イベント種については、また別の機会にまとめてみたいと思います)
よって先述のコードは
「描画領域(canvasオブジェクト)上でマウスの投下が解除(mouseup)された場合、描画を無効化(drawing = false;)するという動作を行う」
という処理になります!
分かりやすく言うと、「紙からペンが離れた場合、インクを落とすのをやめる」ということになります!当たり前ですね!!!

それでは改めてmousemoveとmousedownに関するコードも見ていきましょう!

canvas.addEventListener('mousemove', draw_canvas);

canvas.addEventListener('mousedown', function(e) {
  drawing = true;
  var rect = e.target.getBoundingClientRect();
  before_x = e.clientX - rect.left;
  before_y = e.clientY - rect.top;
});

・いみ
まうすがうごいたらあるかんすうをじっこうします。それはまだひみつです。
まうすがくりっくされたらいかのどうさをします。

前者はマウスが動いている間中、draw_canvasという関数を利用するということですね!わかりやすい!
(恐らくdraw_canvas関数の中には、連続的に点と点を繋ぎ合わせるような処理が書かれているんだろなぁ・・・的な想像はつきましたでしょうか!?)

後者はマウスが投下された際の処理が記述されていますね。
まずは描画状態を有効化(drawing = true;)、マウスが投下された。つまり紙にペン先が触れている状態なので当然ですね!

では↓は何をしているのでしょうか!

  var rect = e.target.getBoundingClientRect();
  before_x = e.clientX - rect.left;
  before_y = e.clientY - rect.top;

・いみ
まうすのいちにいんくがおちるようにします。

このコードに関しては流用するにしても形式を変えて使うことはないと思うので、簡単に説明いたします!
まずコンピュータの画面上におけるxy座標の対応ですが、領域の左上が(x,y)=(0,0)となっており、右方向に行くにつれてxが増加し、下方向に行くにつれてyが増加していきます!
しかし!canvas上でマウスイベントを取り扱うにあたっては、canvas領域の左上を(x,y)=(0,0)としたいですよね!1行目の「e.target.getBoundingClientRect()」では、イベントが起きた要素(=target、今回はcanvasオブジェクトですね!)の座標を取得します。
それをclientX,Y、すなわちクライアントのマウスポインタの絶対座標から差し引くことで、相対座標を取得し、canvas領域の左上を(x,y)=(0,0)と考えられるようにするわけです!

(英語サイトですが、x,y座標の考え方はこちら
ついでにgetBoundingClientRect()についてはこちらに分かりやすく書いてありました!)

とっても長くなりましたが、次に移ります。

    function draw_canvas(e) {
        //code here
    }

先ほどmousemoveの際にひょっこり登場していたdraw_canvas()関数について解説していきます。「マウスを投下して動かしている間描画を行う」という、言ってしまえば線描画のメイン動作を記述していきます!
(もちろん投下せずに動かしている間の処理も考えてあげなければいけません!)

    function draw_canvas(e) { 
        // 追記!
        if (!drawing){
        return
        };
    }

まずはボタンが押されていない場合の処理から・・・!
これは言わずもがなですね。drawingがtrueでない(描画が無効である)ならばそのまま返します。

それでは本格的な処理!!!

    function draw_canvas(e) {
        if (!drawing){
        return
        };
        // 追記!
        var rect = e.target.getBoundingClientRect();
        var x = e.clientX - rect.left;
        var y = e.clientY - rect.top;
    }
}

mousedownの時の処理と同じですね!rectを使って、マウスポインタの相対座標で考えられるようにしてあげます。

描画する際に引く線について定義してあげましょう

    function draw_canvas(e) {
        if (!drawing){
        return
        };

        var rect = e.target.getBoundingClientRect();
        var x = e.clientX - rect.left;
        var y = e.clientY - rect.top;
        // 追記!
        context.lineCap = 'round';
        context.strokeStyle = 'black';
        context.lineWidth = '10';
    }

下2行はPart1で解説しましたが、lineCapは初登場ですのでちょっとだけ。簡単に言えば線の終端です。roundなので端が丸い線種にしたってことですね!

それではここからが本番というか「点と点を連続的に繋いでいく」動作を記述していきます。

    function draw_canvas(e) {
        if (!drawing){
        return
        };

        var rect = e.target.getBoundingClientRect();
        var x = e.clientX - rect.left;
        var y = e.clientY - rect.top;

        context.lineCap = 'round';
        context.strokeStyle = 'black';
        context.lineWidth = '10';
        // 追記!
        context.beginPath();
        context.moveTo(before_x, before_y);
        context.lineTo(x, y);
        context.stroke();
    }

さて、1行目はPart1で登場しましたが、初期化的な何か!という雑な説明をしていたと思います!まあ実際ほんとに初期化的な何かなのですが、この5行を連続的に見ていくと理解が深まるというか分かりやすいと思いますので、流れを簡単に書きます!

context.beginPath(); で現在のパスを初期化

context.moveTo(before_x, before_y); でパスの開始座標を指定

context.lineTo(x, y); で座標を指定し、開始座標とパスを形成

context.stroke(); でパスを先ほど定義した線種として有効化

『パス』というのは、ある2点をリンクさせる線のことですね!

「マウスが動いたら以前置かれていた座標点と今の座標点を繋ぐ」ところまでできました。
もし次もまた連続してmousemoveイベントが発生するとしたら、その時の「以前置かれていた座標点」は、「今の座標点」ということになりますよね!なので、以下のコードを追記します。

    function draw_canvas(e) {
        if (!drawing){
        return
        };

        var rect = e.target.getBoundingClientRect();
        var x = e.clientX - rect.left;
        var y = e.clientY - rect.top;

        context.lineCap = 'round';
        context.strokeStyle = 'black';
        context.lineWidth = '10';

        context.beginPath();
        context.moveTo(before_x, before_y);
        context.lineTo(x, y);
        context.stroke();
        context.closePath();
        //追記!
        before_x = x;
        before_y = y;
    }

・いみ
まえにまうすがいたばしょといままうすがいるばしょをせんでつなげます。

これでdraw_canvasは完成です!動いたら点を繋ぐ、これを連続的に行うことができますね!

ここまでで、投下される、離される、動く、マウスのイベントによる動作を定義し、線も引かれるようにしました。
つまりもうCanvas領域上でお絵描きができちまうんだ!!!!!!!!!!!!!
さっそくやってみましょう!!!!!!!!!!!!!!!!!

ちなみにここまでのHTMLファイルの全貌はこんな感じ。

chokowa.html
<!DOCTYPE html>
<html lang="ja">
<head>
<script type="text/javascript">
function init(){
    var canvas = document.getElementById("chokowa");
    var context = canvas.getContext('2d');
    var drawing = false;
    var before_x = 0;
    var before_y = 0;
    canvas.addEventListener('mousemove', draw_canvas);
    canvas.addEventListener('mousedown', function(e) {
      drawing = true;
      var rect = e.target.getBoundingClientRect();
      before_x = e.clientX - rect.left;
      before_y = e.clientY - rect.top;
    });
    canvas.addEventListener('mouseup', function() {
      drawing = false;
    });
    function draw_canvas(e) {
        if (!drawing){
        return
        };     
        var rect = e.target.getBoundingClientRect();
        var x = e.clientX - rect.left;
        var y = e.clientY - rect.top;       
        context.lineCap = 'round';
        context.strokeStyle = 'black';
        context.lineWidth = '10';  
        context.beginPath();
        context.moveTo(before_x, before_y);
        context.lineTo(x, y);
        context.stroke();
        context.closePath();
        //追記!
        before_x = x;
        before_y = y;
    }
}
</script>   
<meta charset="utf-8">
<title>TUTORIAL</title>
</head>
<body onload="init();">
    <canvas id="chokowa" width="500" height="300" style="border:#1C1C1C solid 2px"></canvas>
</body>
</html>

ではレッツお絵描き!!!!!!!!!

キャプチャ.PNG

やったぜ。

でもまだ消しゴムとかがないので質素ですね。

最後に

ようやく消せない・色変えられない・線の太さ変えられない・ブラシとか何もないと4拍子揃ったワクワクドキドキお絵描きツールの完成です!
タッチ対応は後回しにして、次回は機能を充実させていきますか!

参考サイト様

canvasで作るお絵描きツール:https://kigiroku.com/frontend/canvas_draw.html
canvasにおけるx,y座標:http://www.andrewsmyk.com/mobiledev/week-04/
getBoundingClientRect()についてのとっても分かりやすい記事:https://cartman0.hatenablog.com/entry/2015/06/29/022301)に分かりやすく書いてありました!

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

typescriptのType predicatesを使って、配列からundefinedを取り除く式を型安全に書く

背景

配列の中にundefinedが紛れていて、それを取り除いた配列を作りたい場合、例えばrubyだと

# ruby
2.5.5 (main)> ["foge", nil, "huga", "foga", nil, "fuge"].compact 
=> ["foge", "huga", "foga", "fuge"]

こんな感じでパッとかけますが、jsだと多少面倒。

// javascript
["foge", undefined, "huga", "foga", undefined, "fuge"].filter(e => e)
 => ["foge", "huga", "foga", "fuge"]

これをtypescriptで書くと悲しいことに、

// typescript
const newArray = ["foge", undefined, "huga", "foga", undefined, "fuge"].filter(e => e);

この newArray の型が、 (string | undefined)[] となってしまう。これをどうにかして string[] にしたいよね、という話。

解決策: Type predicates を使う

公式ドキュメント

例はこんな感じ。

function isHoge(s): s is Hoge {
  return typeof s === 'hogehoge';
}

この例では訳わからないことをしていますが、あくまで例として。

type predicatess is Hoge 部分です。
なにをやっているかを一言で言うと、 この関数がtrueを返す場合、パラメータsの型をHogeとする ということです。

これを使って色々なものが書きやすくなりますが、今回はあくまで上記の filter の例にかぎって紹介します。

完成形

const newArray = ['foge', undefined, 'huga', 'foga', undefined, 'fuge'].filter((e): e is string => Boolean(e));

つまり、 Boolean(e) がtrueを返す場合(この場合だと undefined 以外)、estringとする、と宣言しているわけです。

これで、 newArray の型が、 string[] となります。

PS

typescript強者の方、これよりも良い方法があればご教示ください...

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

[初心者向け]VuexのStoreを細かくモジュール分けしよう

はじめまして、PMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います。

はじめに

Vuexを始めたばかりの自分はVuexに関連する処理をすべてstore.jsなどの一つのファイルに記述してしまっており、ファイルが非常にファットになってしまっおり、可読性も非常に下がってしまっていました。
そこでVuexファイルを細かくモジュール分していきたいとおもいます。

必要な環境

  • Vue
  • Vuex

実装

まずstore/以下に任意のjsファイルを作成してください。今回はuser.jsというファイルを作成しています。

user.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
    //stateデータ
    user: null
}
const mutations = {
    //同じ処理を行うならnewMsgはobjectに
    setUserInfo(state, userInfo) {
        //任意の同期処理
        state.user = userInfo
    }
}
const actions = {
    getUser() {
        //任意の非同期処理
    }
}
export default {
    namespaced: true,
    state,
    actions,
    mutations,
}

このようにstate,mutations,actionsそれぞれを定数に入れてそれをexportしています。

それぞれのモジュールを集約しよう

storeディレクトリと同じ階層に任意のjsファイルを作成してください。今回はstore/index.jsを作成します。

index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './user'
Vue.use(Vuex)
export default new Vuex.Store({
    modules: {
        user: user
    },
})

ここではまずstore/以下の作成したVuexファイルをimport user from './store/user'でインポートします。
そしてモジュールを集約してexportしています。

main.jsで呼び出し

main.js
import Vue from 'vue'
import store from './store/index'
new Vue({
  render: h => h(App),
  store
}).$mount('#app')

mian.js内でstore/indexをインポートし、Vueインスタンス内にインポートしたものを読み込んでください。

Vuexを呼び出す際の注意点

sample.vue
<script>
export default {
  created() {
    this.$store.dispatch("user/getUser");
    this.$store.commit("user/setUserInfo", "hoge");
    this.$store.state.user.user;
  }
};
</script>

このように呼び出し先の指定がindex.jsでの定義から、"user/getUser""user/setUserInfo"state.user.userのようになっていることに注意です。

最後に

このようにVuexのファイルを細かくモジュール分けすることができました。
ファイルを細かくモジュール分けすると可読性が上がり、開発効率が上がると思います。
機会があればぜひ試してみてください。
なにか間違いなどあれば指摘していただけると幸いです。

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

【HTML5】WEBページ上でお絵かき!『Canvas』ハイパーゆとり向けチュートリアルPart1

※記事最下部にHTMLファイルの内容が掲載されております。

Part1では、CreateJSに頼らず生のCanvasスクリプトコードを書いてPert0と同じくシンプルな描画(canvasに丸を置くだけ)を行ってみます!
今回もとってもわかりやすく解説できるように努力します!
このチュートリアルの目的というかゴールですが、タッチデバイスからの入力を受け付け、実際にWEBページ上で自由に描画できるお絵描きツールのようなものを作ることができたらと考えています!
(多分Part3くらいで実現します)

今回はこちらの参考サイト様を基にプログラムを解説していきます!ではよろしくお願いします!

Canvas

HTML5に標準でサポートされている描画用ツールです。Javascriptによって様々な線や図形の定義を行います。

詳しくはPart0の資料に書きましたので、そちらを参照していただければと思います!

ではさっそくチュートリアルに入ります!あなたもCanvasでピカソを目指しましょう!

チュートリアル

<script type="text/javascript">

まずはjsスクリプトを書くよ!って宣言してあげましょう!あとで見た人が分かりやすいようにね必要だよね!!!

ちなみに!HTML5はCanvasを標準サポートしているので、Pert0でしていたようなライブラリのインポートはいらないんです!!!!!

<body onload="init();">
    <canvas id="chokowa" width="500" height="300" style="border:#1C1C1C solid 2px"></canvas>
</body>

Pert0同様、HTMLのbody内にキャンバス領域を生成します!読み込んでいるinit関数やidの指定に関しては後程!

ここから実際にCanvasスクリプトを書いていきます!

<script type="text/javascript">
  function init(){
    // code here.
  }
</script> 

init()関数を定義します!基本的にCanvasスクリプトはすべてこの中に書き込んでいくこととなります!
(init関数は、先ほどのHTMLコードでbody内に読み込んでいます!)

ではjsコードの中身をコピペして作っていきましょう!!

var canvastest = document.getElementById("chokowa");

・いみ
かみをおえかきできるようにします。

一つ一つ見ていきます!(今更ですが筆者は知識レベル0に近いので、寄り道や蛇足が多めです。前提知識豊富な方は流し読み推奨です・・・)
まず、document.getElementById()のdocumentってなんでしょうか!
さくっと調べてでてきたので、こちらを参考に解説していきます!
まず、documentとはHTMLの要素を取得することができるオブジェクトみたいですね!

document.head

例えばこんな風にhead要素に対してdocumentオブジェクトを使うと、headタグの中身が見られるそうです!気になる方はheadになんか書き込んだ上で試してみてください!

では続いてgetElementById()の部分はなんでしょうか!!documentにくっついてるので、documentオブジェクトのプロパティだろうな・・・ってことはなんとなく予想がつきます!
『get Element Id』ですので『要素(Element)のIdを取得(get)』ってところでしょうか!実際にそのままの意味で、()内で指定したID属性を持つHTML要素を取得することができるメソッドなのです。

なので、document.getElementById("chokowa")の意味は「documentというHTMLの要素を取得できるオブジェクトを利用して、"chokowa"というID属性を持つ要素(Element)を取得(get)する!」
というものになります!

var canvas = document.getElementById("chokowa");

結果としてこの一行は、Part0で紹介したStageの部分と同じで、「chokowaという名前(=ID)が指定された紙(=canvas)を有効化する」という内容になります。

長々となりましたが、次いきましょう!

var context = canvas.getContext('2d');

・いみ
いまからかみになんかかきます。
どんなものをかくかはまだひみつです。

さっきと似たような構文ですね・・・同じように見ていきましょう。

canvasというオブジェクトがあって、それに連結する形でgetContextプロパティが用意されており、値として2dが指定されています。描画ツールということで、2dは二次元(の図形)というのは察しが付くところでしょう。

Contextは「文脈」や「状況」を意味します。なので、『canvas.getContext('2d')』の意味は「canvasオブジェクト上に二次元図形を追加」という風なことになります。先述のコード同様、後々使いやすいようにこの内容をオブジェクト(=ここではcontext)として定義しています。

ではPart0を見てくださった方はなんとなーく流れが読めてきていると思いますが、ここから図形の定義に入ります!canvas上に配置したcontextとはどんな形でどんな色なのか!設定していきます!

context.beginPath();

・いみ
わかんないです。

まずはbeginPath()でパスを初期化します!意味が分かりませんね!自分も意味が分かりません!
調べてみた感じ変数の初期化(chokowa = 0;)的なもので、まずはパスをまっさらにする必要があるみたいですね。

パスの概念に関しては、実際にお絵描きツールを作成する際に何となく理解できると思うのでここでの説明はこんなところで。。。

さて、本格的に図形描画を行っていきましょう。

context.arc( 100, 100, 50, 0 * Math.PI / 180, 360 * Math.PI / 180, false ) ;
context.fillStyle = "rgba(0, 191, 255, 1.0)" ;
context.fill() ;
context.strokeStyle = "DeepSkyBlue" ;
context.lineWidth = 8 ;

・いみ
まるくせんをひくいめーじをします。
まるをぬりつぶします。
ぬりつぶしてからせんをひきます。

直感的に理解できる内容が多いと思いますが、一行一行懇切丁寧に説明していきます!

では1行目!arcという文字と指定されている引数達を見る感じ、円の形状を定義していることが伺えますね!
引数は6個あり、それぞれ
1. 配置するx座標
2. 配置するy座標
3. 半径の長さ
4. "弧"描画の開始角度
5. "弧"描画の終了角度
6. "弧"描画の回転方向(true=反時計回りの円、false=時計回りの円)
を示しています。

角度に関してははラジアン指定ですね。分からない方は難しく考えずに↓の感じでそのまま使えばいいと思います。

指定したい角度 * (Math.PI / 180)

2行目はfillStyle、読んで字のごとくどう塗りつぶしたらいいんだい?ということです!参考サイト様でrgbaで指定されていたので、そのまま流用したいと思います!
引数が4つありますので、それぞれ'r', 'g', 'b', 'a'ということが分かりますね!
「rgbは分かるけどaは何?」って方も少なくはないと思いますが、aはalpha値=透明度のことです!
0~1の間で指定して、0に近づくほど薄くなります。

では3行目を見ていきます!

context.arc( 100, 100, 50, 0 * Math.PI / 180, 360 * Math.PI / 180, false ) ;
context.fillStyle = "rgba(0, 191, 255, 1.0)" ;
ここ>context.fill() ;
context.strokeStyle = "DeepSkyBlue" ;
context.lineWidth = 8 ;

fillで先ほどfillStyleで指定した要件を有効化しています。それだけです。
それだけですが、2つはセットで使う必要がありそうですので、忘れないようにしたいですね!

ここまできたら4、5行目はなんとなく察しがつくのではないでしょうか!そうです!描画されるであろう図形の境界を示す部分、つまり「線」の色と太さですね!わかりましたよね!多分自分だったらわかんないです!
4行目では、strokeStyle、つまり線のスタイルを指定しています。でもこの名称だと、太さとか線種とかも一括で指定できそうだけど・・・(調べたらできないっぽい;;)
5行目は線の太さですね。8です。
8pxってことです多分・・・!

それでは図形の設定はこれで終わりです!Part0でも行ったように、図形(=context)を有効化してみましょう!

context.stroke();

・いみ
せんをみえるようにします。

これだけでオッケーです!名前と、これまでの傾向から、意味合い的には「ストローク(線)の有効化」でしょうか。
Part0に書いた気がしますが、図形というのは
「境界となる『線』と『塗りつぶし』」
によって構成されているみたいですね!

では見てみますか!!!

ちなみにここまでのHTMLファイルの全貌

chokowa
<!DOCTYPE html>
<html lang="ja">
<head>
<script type="text/javascript">
function init(){
    var canvas = document.getElementById("chokowa");
    var context = canvas.getContext('2d');
    context.beginPath();
    context.arc( 100, 100, 50, 0 * Math.PI / 180, 360 * Math.PI / 180, false ) ;
    context.fillStyle = "rgba(0, 191, 255, 1.0)" ;
    // 塗りつぶしを実行
    context.fill() ;
    // 線の色
    context.strokeStyle = "DeepSkyBlue" ;
    // 線の太さ
    context.lineWidth = 8 ;
    // 線を描画を実行
    context.stroke() ;
}
</script>   
<meta charset="utf-8">
<title>TUTORIAL</title>
</head>
<body onload="init();">
    <canvas id="chokowa" width="500" height="300" style="border:#1C1C1C solid 2px"></canvas>
</body>
</html>

さあさっそくこれだけ苦労して?描画した図を表示してみましょう!
でん!!!!!!!!!!!!!!!!!!!!

キャプチャ.PNG
はい。
多分Part0で作成したものと同じ描画が成されていると思います。やったね!

最後に

これでチュートリアルPart1は終わりです。逆にわかりにくくなるほどに詳細な説明ができていると思うので、

canvas領域を有効化

(canvas内に)図形を配置します!と宣言

図形のカスタマイズ

図形の有効化

というcanvasスクリプトにおけるメインフローは理解できたかと思います。他の図形やマウス/タッチ入力による描画もこのフレームに収まってくれることを祈りつつ、Part2ではマウスイベントを取得し、実際にWEB上でマウスを使って描画可能なページを作ってみたいと思います。

参考サイト様

Canvas上での円描画:https://lab.syncer.jp/Web/JavaScript/Canvas/4/
HTMLの要素を取得する! JavaScriptのdocumentプロパティの使い方:https://www.sejuku.net/blog/30970

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

JavaScriptでオブジェクトのプロパティの変更をブレークする

はじめに

先日業務中にとあるシステムのプログラムでとある配列の先頭に不正な値が挿入されていることが判明し、その修正対応をしていました。
Developer Toolで配列の要素の変更とかをトリガーにブレークできたりしないかなーって思ったら、そういった機能はなさそうでした。
しかし、配列に変更を加えている箇所を1つ1つ確認するのも面倒だったので、なんとかしてやれないか調査していました。

結論

https://stackoverflow.com/questions/11618278/breakpoint-on-property-change
Stack Overflowに答えがありました。

例:

// 対象オブジェクト
const obj = {
    someProp: 10
};

// 検査対象プロパティを別プロパティへ退避
obj._someProp = obj.someProp;

// アクセッサーを上書き
Object.defineProperty(obj, 'someProp', {
    get: function () {
        return obj._someProp;
    },
    set: function (value) {
        debugger; // ブレーク
        obj._someProp = value;
    }
});

要するに検査対象プロパティの処理を上書きして、値が変更されるところでdebuggerステートメントでブレークポイントを貼るということですね。

配列でもやってみる

JavaScriptでは配列はオブジェクトの一部なので、配列の変更に関しても同じ考えが適用できそうです。
私のケースでは先頭の要素の変更をトリガーにブレークできればよいので、以下のようにしました。

const obj = 対象配列;

// 配列の先頭要素を退避
obj._headEle = obj[0];

// 配列オブジェクトの「0」プロパティのアクセッサを上書き
Object.defineProperty(obj, '0', {
    get: function() {
      return obj._headEle;
    },
    set: function(value) {
      debugger; // ブレーク
      obj._headEle = value;
    }
});

lengthプロパティでもできそうですが、配列オブジェクトのlengthプロパティは上書きできないようになっており、上書きしようとするとエラーが発生してしまいます。

このコードを適当なアプリケーション初期処理内で実行することで、原因箇所を特定し、無事問題を修正することができました。

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

redux-form の validation を関数合成使って見やすくしてみる

私は React + redux-form でフォーム画面を作ることが多いのですが、redux-form の validation がやや冗長になりやすい傾向にあります。

これを関数合成使って、見やすくメンテナンスしやすいコードに出来ないかなと思って試してみました。

※ 基本的な redux-form の知識が必要です。

どのあたりが冗長なの?

例えば、このようなフォームを実装するとします。

SnapCrab_NoName_2019-6-27_11-50-56_No-00.png

redux-form で validate 関数を定義していくと、こんな風に書くと思います。
(共通化などは一旦考えないで書いた場合)

const validate = (values, props) => {
  const errors = {};
  if (!values.option_id) {
    errors.option_id = '必須入力です';
  }
  if (!_.trim(values.name)) {
    errors.name = '必須入力です';
  }
  if (!_.trim(values.email)) {
    errors.email = '必須入力です';
  } else if (!EMAIL_VALIDATION.test(values.email)) {
    errors.email = '正しいメールアドレスを入力して下さい';
  }
  if (
    values.phone_number &&
    !_.inRange(
      values.phone_number.length,
      PHONE_NUMBER_MIN_LENGTH,
      PHONE_NUMBER_MAX_LENGTH
    )
  ) {
    errors.phone_number = '携帯電話は10桁以上20桁以下で入力できます。';
  }
  if (values.birth_day && !BIRTHDAY_PATTERN.test(values.birth_day)) {
    errors.birth_day = '不正な値です';
  }
  if (calcAge(values.birth_day) < ADULT_AGE && !values.aggreement) {
    errors.aggreement = '未成年者は保護者の同意が必要です';
  }
  return errors;
};

綺麗なコードには見えませんが、これぐらいなら許容範囲かもしれません。
しかし、例えば性別や生年月日といった項目が追加されるとどうでしょうか。かなり長くなってしまいます。

また、名前やメールアドレスといった必須入力かどうかを判定するロジックは同じ仕組みなので共通化したいですね。

どんな風に書けると嬉しい?

共通化するにあたって、どういう書き方ができると綺麗かなと考えてみました。
ここは個人的な思想なども入ると思うので一概に正しいとも言えませんが、今回は下記のように書けると良いかなと思います。

const validate = (values, props) => {
  return validates(
    validateRequired('option_id'),
    validateRequired('name'),
    validateRequired('email'),
    validateEmail('email'),
    validatePhoneNumber('phone_number'),
    validateBirthday('birth_day'),
    validateUnderCondition(
      calcAge(values.birth_day) < ADULT_AGE,
      validateRequired('aggreement')
    ),
  )(values, {});
};

どうでしょうか?
それなりに見やすくなったと思います。

validates 関数は高階関数で、values と errors の初期値(ここでは空の Hash) をとり、列挙された関数を順次実行してくれる関数です。

validateRequired など関数は、redux-form の values が hash であることから、指定された key に対して validate を行う関数です。
validateRequired であれば、その key が存在するかどうかといったチェックですね。

validateUnderCondition のみ特別で引数を複数取ります。第二引数に validateRequired といった validation 関数を指定するのですが、第一引数にそれを実行する条件を指定できるようにしてます。
今回では、未成年の場合のみ同意が必要といった validation を表現するために使います。

validates 関数

では、validates 関数を組んでいきます。

export const validates = (...fns) => (...args) => {
  if (fns.length > 1) {
    return fns.reduce((prevFn, nextFn, index) => {
      return nextFn.apply(
        null,
        typeof prevFn === 'function' ? prevFn.apply(null, args) : prevFn
      );
    })[1];
  } else if (fns.length === 1) {
    return fns[0].apply(null, args)[1];
  } else {
    return args[1];
  }
};

高階関数にしつつ、どちらの引数もリストで取るようにしました。
fns は関数のリストで、args は values や errors の初期値といった前提条件となる値のリストです。

reduce を使って関数合成しつつも、apply で動的引数である args を適用しています。
また、fns が一つの場合でも実行できるようにしています。
複数の関数を合成するだけであれば不要ですが、項目が 1 つだけの form もあり得ると思うので条件分岐しています。

残念なところは、6 行目あたりで prevFn が function かどうかの判定が必要になってしまっているところです。
後述しますが、apply を使っているためか、prevFn が関数ではなく prefFn の返り値そのものになってしまってうまく関数合成ができませんでした。

redux-form 以外でも利用できるようにするために apply 使っていたのですが、redux-form の validation に限れば、args を動的引数で取る必要性はないので、apply を辞めるという手もあります。
もし、もっと良い方法ご存知の方は教えて欲しいです。

各 validation 関数

各々の validation 関数は特別なことはしていません。
全て列挙すると長くなるので代表的なものを載せます。

export const validateRequired = key => (values, errors) => {
  if (!_.trim(values[key])) {
    errors[key] = '必須入力です';
  }
  return [values, errors];
};

export const validateUnderCondition = (condition, validateFunc) => (
  values,
  errors
) => {
  if (condition) {
    return validateFunc(values, errors);
  }
  return [values, errors];
};

特に難しいことはしていません。
redux-form の values が hash なので、高階関数の一つ目の引数に key を取って、それを使って値を割り出しています。
validation 関数に関しては、redux-form の構造にどっぷりっといった感じですが、values も errors もただの hash なので redux-form 以外でも使い回すことは出来そうです。

まとめ

この記事ですが、実はあまり redux-form には関係なかったりします。
元は関数合成使って、もう少しうまく書けないかなぁと思ったのが、たまたま redux-form の validate だっただけです。
記事にするにあたって、もっと汎用的に直してから書こうかと思いましたが、むしろイメージしづらいのではと思って、そのまま redux-form の validate を題材にしました。
もちろん、ここにあげているバリデーション以外にも redux-form の FieldArray を使った場合の validation 関数等々、色々と定義はしていますが、validates 関数のおかげで集約しつつ簡単にフォーム側の validate を書けるので重用してます。

また、redux-form 以外でも各 validation 関数はもちろん利用しています。
ただ共通化しただけの関数ですからね。

残念な部分もあるので、もっと改善していきたいと思います。

(書いた後に思いましたが、記事的に validates 関数と validation 関数があって、何が何やらって感じですね。。。表現力磨きたい。

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

JavaScriptとElmを比べてみた〜前編〜

Elmは、JavaScriptにコンパイルできる言語、いわゆるaltJSです。

変数宣言(のようなもの)

JavaScriptの場合

const a = 1;
let b = 1;
var c = 1;

ワイ「const定数いうて再代入できひんやつやな」

Elmの場合

a = 1

ワイ「constletvarも無いねんな」

ハスケル子「はい」
ハスケル子「デフォルトで再代入不可です」
ハスケル子「つまり不変なので、変数ですらなくて」
ハスケル子「ただ値に命名している、値を定義しているって感じですね」
ハスケル子「あとセミコロンも要りません」

ワイ「再代入はできなくても、JSのconstみたいに」
ワイ「オブジェクトのプロパティを一部変更することはできんねやろ?」

ハスケル子「いえ、オブジェクトのプロパティ・・・」
ハスケル子「というかElmではレコードフィールドですね」
ハスケル子「フィールドも上書きできません1
ハスケル子「全ての値が不変です

ワイ「へぇぇ・・・」

関数の定義

JavaScriptの場合

function add (a, b) {
  return a + b;
}

または

const add = function (a, b) {
  return a + b;
}

アロー関数式で書くと↓

const add = (a, b) => a + b;

Elmの場合

add a b =
    a + b

ワイ「Elmではカッコもカンマも無いんやな」
ワイ「さらにreturnも書かへんねやな」

ハスケル子「はい」
ハスケル子「関数の最後に評価された値が自動的に戻り値になります」

ワイ「そもそもfunctionとかいうのも無いんやな」
ワイ「変数とほぼおんなじやん」

ハスケル子「そうですね」
ハスケル子「引数があれば関数って感じです」

関数の実行(適用)

JavaScriptの場合

const result = add(3, 5);

Elmの場合

result = add 3 5

ワイ「関数を適用するにもカッコもカンマも無しなんや」

ハスケル子「はい」
ハスケル子「ただ、add関数の結果を更に別の関数に渡したいときなんかは───」

result = anotherFunc add 3 5

ハスケル子「───と書くと」
ハスケル子「anotherFunc関数に対して、add35という」
ハスケル子「3つの引数を渡してる感じになっちゃうので」

result = anotherFunc (add 3 5)

ハスケル子「↑こうすると、カッコで囲まれた部分が先に実行されます」

ワイ「なるほどな」
ワイ「add 3 5の計算結果、つまり8が」
ワイ「anotherFunc関数の引数として渡される感じか」

ハスケル子「はい」
ハスケル子「または───」

result = anotherFunc <| add 3 5

ハスケル子「───こう書いても同じです」

ワイ「あー、パイプラインいうやつやね」
ワイ「これ読みやすくて好きやわ」

書く順序による影響

JavaScriptの場合

const a = 3;
const b = 5;
const c = a + b;

ワイ「基本、上から順に実行って感じよな」

ハスケル子「はい」
ハスケル子「なので例えば───」

const c = a + b;
const a = 3;
const b = 5;

ハスケル子「───こんな感じで」
ハスケル子「abに値を代入するより上の行で」
ハスケル子「abを使った計算などをしようとすると」
ハスケル子「エラーになっちゃいますよね」

ワイ「なるほどな」
ワイ「でもまあ、それは普通そうやろ」

ハスケル子「それがElmの場合は違うんですよ」

Elmの場合

c = a + b
a = 3
b = 5

ハスケル子「↑これも普通にOKです

ワイ「ええ・・・」
ワイ「ab定義するより上の行で」
ワイ「abを計算に使ってるやん・・・」

ハスケル子「はい」
ハスケル子「関数と同じ感じなんですよ」

ワイ「ああ・・・」
ワイ「JSでも関数はそうやもんな」
ワイ「下の方で宣言した関数、上の方で使えるもんな」

ハスケル子「はい」
ハスケル子「Elmでは全ての値が不変なので、それが可能なんです」

ワイ「全ての値が不変・・・つまり再代入という概念が存在しないということやろ?」
ワイ「それやと順番が関係なくなるの?」
ワイ「なんで・・・?」

haskellko2.jpeg

ハスケル子「JSの場合は───」

let a = 3;
console.log(a);
// 3 と表示。

a = 5;
console.log(a);
// 5 と表示。

ハスケル子「let a = 3;って書いて」
ハスケル子「そのすぐ下の行でaを呼び出したら、aの値は3だけど」
ハスケル子「その後、a = 5;って再代入して」
ハスケル子「その下の行でaを呼び出したら、aの値は5

ワイ「それは分かるわ」
ワイ「当然の時間の流れや」

ハスケル子「でも再代入という概念がないとしたらどうですか?」

ワイ「ああ・・・」
ワイ「再代入できひんなら、aはいつでも3や」
ワイ「a状態が変わることが無いから」
ワイ「a5に変えた後に呼び出したらどうこう・・・」
ワイ「みたいな話がそもそもあり得へんわけやな」

ハスケル子「そうです」
ハスケル子「全ての値が不変で」
ハスケル子「時間が止まってるようなものなので」
ハスケル子「コードの中に前とか後とか無いイメージです」

ワイ「なるほどなー」
ワイ「時が止まってるような」
ワイ「1フレームの中で全てが実行されているような」
ワイ「不思議な感じやな」
ワイ「スタープラチナ・ザ・ワールドみたいやな」

ハスケル子「オラオラオラオラ!って感じです」

ワイ「痛い!痛い!
ワイ「やめてぇや」

ワイ「ええと、つまり」
ワイ「時間が止まっとるようなもんやから」
ワイ「値の定義より上の行で、その値を使っても」
ワイ「問題ないんやな」

ハスケル子「そんな感じです」

ワイ「でも、なんの値も変えられなくて」
ワイ「この言語、何ができるの・・・?」

ハスケル子「普通にシングルページアプリケーションとか」
ハスケル子「動きのあるゲームだって作れますよ」

ワイ「えぇ・・・!?
ワイ「全て不変のはずやのにボール動いてますやん・・・!
ワイ「言うてることちゃいますやん・・・!」

ハスケル子「気になるならElm Guide読んでみてください」

ハスケル子「じゃあ」
ハスケル子「次はオブジェクトとかイベントハンドラについて比べてみましょう」

〜後編に続く〜

後編も書いたで!

JavaScriptとElmを比べてみた〜後編・Vue.jsとも比べてみた〜


  1. 詳しくは後編にて。 

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

Array.prototype.mapとArray.prototype.flatMap

tl;dr

Array.prototype.flatArray.prototype.flatMapES2019から導入される。
サンプルコードを記載する。

Array.prototype.flatの例
Array.prototype.flatMapの例

Array.prototype.flatの例

// このような多階層の配列があるとする
const array = [1,[2,[3]]];


// これをArray#flatすると
const arrayFlatten = array.flat(); // [1,[2,3]]

となる。

// もういちどやれば
arrayFlatten.flat(); // [1,2,3]

となる。

この、flatする回数は引数で指定可能で、デフォルトはの引数は1となっている。

const array2 = [4,[5,[6]]];
array2.flat(1); // [4,[5,6]]
array2.flat(2); // [4,5,6]

再帰的に実行し、全く平坦にする場合はこのようにする

const array3 = [7,[8,[9[10]]]];
array3.flat(Infinity); // [7,8,9,10]

Array.prototype.flatMapの例

ここで、duplicateという関数を定義する。duplicateは、引数を複製し、配列に入れて返す関数とする。

const duplicate = x => [x, x];

// これを配列に適用すると次のようになる
[1,2,3].map(duplicate); // [[1,1],[2,2],[3,3]

// さらにflatする場合は、
[1,2,3].map(duplicate).flat(); // [1,1,2,2,3,3]

//とすればよいが、ここでflatMapの出番となる。
[1,2,3].flatMap(duplicate);  // [1,1,2,2,3,3]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React NativeのFlatListでグリッド状に画像を配置する方法

背景

React Nativeでインスタグラムみたいにグリッド状に画像を配置したい

実装

画像はunsplashから取得しています。
FlatListのデータは、中身のないただのリストにしてますが、実際はAPIから取得してきたデータなどになると思います。

App.js
import React from 'react';
import { Text, View, StyleSheet, Image, FlatList, Dimensions } from 'react-native';
import Constants from 'expo-constants';

const ITEM_WIDTH = Dimensions.get('window').width;

export default class App extends React.Component {
  state = {
    list: [0,1,2,3,4,5,6,7,8,9]
  }
  render() {
    return (
      <View style={styles.container}>
        <FlatList 
        data={this.state.list}
        keyExtractor={(item, index) => index.toString()}
        numColumns={3}

        renderItem={({item}) => (
          <View>
           <Image
            source={{ uri: 'https://source.unsplash.com/random' }}
            style={styles.imageStyle}
            />
          </View>
        )}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingTop: Constants.statusBarHeight,
    backgroundColor: '#ecf0f1',
    padding: 8,
  },
  imageStyle: {
    width: ITEM_WIDTH / 3,
    height: ITEM_WIDTH / 3,
    margin: 1,
    resizeMode: 'cover',
  }
});

Expo Snackにこのコードを貼り付けたら動作します。

解説

ウィンドウの横幅を取得

const ITEM_WIDTH = Dimensions.get('window').width;

Imageにカラム数で割ったwidthとheightを設定。

 imageStyle: {
    width: ITEM_WIDTH / 3,
    height: ITEM_WIDTH / 3,
    margin: 1,
  }

FlatListにカラム数として、numColumnsを設定する。

        <FlatList 
        data={this.state.list}
        keyExtractor={(item, index) => index.toString()}
        numColumns={3}
        ...

終わりに

React Native勉強し始めて一週間ぐらいなんですが、Reactでスマホのアプリ作れるのはすごい。。
ってかAndroid Stdioのシミュレーター重すぎぃ

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

[JavaScript] オブジェクトの配列に対してフィルタリングする

filterメソッドを使えばよい。

let users = [
  {
    name: "Taroimo",
    age: 27
  },
  {
    name: "Hanako",
    age: 18
  },
  {
    name: "Sabuibo",
    age: 36
  }
];

結果は配列で返って来る。

let users1 = users.filter(function(item, index) {
  if (item.age >= 20) return true;
});
console.log(users1);
[ { name: "Taroimo", age: 27 }, { name: "Sabuibo", age: 36, } ]

抽出結果が1件でも配列で返って来る。

let users1 = users.filter(function(item, index) {
  if ((item.name).indexOf("imo") >= 0) return true;
});
console.log(users2);
[ { name: "Taroimo", age: 27 } ]

該当なしの場合も空の配列で返って来る。

let users3 = users.filter(function(item, index) {
  if (item.age >= 65) return true;
});
console.log(users3);
[]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

IE11でjsがコケタときの対応

新しい現場になって初めてのIE対応でパニクったためメモ…。

vue.jsのコードをwebpackで出力したらIEでこけた

結果として、これはvue.jsのせいではなかったです。
社内で途中まで組まれていたコードを引き継いだため、IEで対応してないjavascript関数が使われていたことに気づかなかったのです。しかしながら、なぜデバッグにこんなにもてこづったのか反省があるので書いていきます。

症状

vue.jsのソースコードをwebpackで出力したpacked.jsを読み込ませたところ、vueレンダリング部分、またそれ以外のDOM要素も一部表示できていませんでした。

デベロッパーツールから見てもエラーメッセージがでない

IEにもデベロッパーツールがあります。しかし、このバグが出たときはなんのエラーメッセージも出力されませんでした。vueコンパイルに失敗したとかそういうこともありません。

対応

polyfillを読み込む

先輩エンジニアが教えてくださいました。とりあえずpolyfill読み込めばなんくるないさと。

<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6"></script>

この一行をHTMLに追加。理由はよく分からないですがコレでエラーメッセージが表示されるようになりました!polyfill.io、今後もう少し勉強する必要がありそうです。

includes()がないってエラー

エラーメッセージが表示されるようになりましたが、includes関数がないよって言われるだけで場所は教えてくれない不親切設計。今回はコードの変更箇所ファイルをシラミ潰しに当たりました。

やはりpacked.jsから吐かれているようなので、とりあえずpacked.jsを直接書き換えました。indexOf()の比較に置換します。

from()がないってエラー

やはり同じファイルから吐かれているため、配列のベタがきに置換します。

とりあえず動いた!原因究明へ…

しかしコレはそもそもwebpack出力前のコードに問題があるのでは?ということで、vueのソースコードを確認。するとありました。includes()とfrom()が。
コレらはwebpackで出力される内部処理でbabelによってコンパイルされるとかなんとかってことらしいですが、元のソースコードでincludesとか使うとそのまま出力されてしまうのですね。ソースコードからincludesとfromを消し去った後、再度webpack出力したところ無事動くようになりました。

今後調べたい

BabelにIE非対応の関数を置き換えてくれるとかいうオプションはあるのだろうか…。これも今後調べたいところです。

参考ページ

polyfill.io が便利すぎた

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

JSで非同期関数呼び出しを含むトランザクション処理を書くのは案外難しいらしいという話

ryo_gridです。
 
以下、常識だろ、という話かもしれませんが、個人的には新たな気づきだったので共有したいと思いました。

以下の記事を理解するための前提

  • JSエンジンのおおまかな仕組みを知っている(イベントループうんぬんであるとか)
  • JSはシングルスレッドである、ただし、ランタイム内では例外もある、ということを知っている

ここらへんが良く分からんという方はググって知識を仕入れてから再度読まれるとよいかと思います。

はじめに

まず、以下のような記事をつい昨日書きました、
 
JSでマルチスレッド(ユーザスレッド)してくれるライブラリConcurrent.Threadを試してみる
 
この記事にはConcurrent.Threadによるマルチスレッド化を試してみた話と、async/awaitを使っても(要はコルーチンを使えば)、なんちゃってマルチスレッド書けるよね、という話と、実装例を書きました。
で、前者については特殊な例なのでひとまず置いておこうと思いますが、後者のようなことができるってことは、マルチスレッドにおける排他制御みたいなものが必要になるのでは?とふと思って、以下のような内容を利用しているSNSに投稿してみました。

JSのasync/awaitなのだけど、awaitしている間に別の関数(イベントループに登録されている関数は、何かこれっていう呼び名があるんだっけ?)の処理が走ることによって、awaitを使った関数がトランザクション処理というかクリティカルセクションというか、とにかく、関数の頭から終わりまでの間に扱うオブジェクトをいじられるとまずいとかいう事情がある場合どうなるんだろう?
 
で、まあ、そんな時はawaitなんぞ使うなって話で、awaitかけてた非同期関数が返すPromiseのthenで後続の処理をするとかすればええんか、と思ったのだけど、果たしてそれで問題は回避できるのだろうか。
 
対象の非同期関数は普通に完了を待つと時間がかかってしまうから非同期関数になっているはずで、内部でI/Oとかしている場合、実行権を手放して、やっぱり他の関数が走っちゃうなんてことがあったりするんじゃないかなーとか。
 
I/Oやらで実行権を手放した場合に、実行権を得られるのは同様にI/Oなんかをしている関数だけ、とかそういう実装になってるのかな?
でも、他にI/Oしてる関数が無かった場合、イベントループ(スレッド)が遊んじゃうよなあ。
うーん。わからん。

結論: コルーチンを用いたコードベースで、非同期処理関数呼び出しを含むトランザクション処理を書きたければ、それが担保されるようにコードを書かなければならない

※正式な用語か自信がないですが、イベントループに登録されている処理(関数)を、以下ではイベントタスクと呼称することにします

ありがたいことに、SNSにいる(繋がっている)有識者の方に答えを教えて頂けたのですが、その要点は以下のような感じでした。

  • ロック用の変数用意して自分で排他処理しないとダメ
    • イベントループで実現されているJSの実行機構を想定した時に単純にロックしてうまくいくのだろうか(私)?
    • => unlock時に1つ accept するような Promise を返す lock を実装すればよい
  • もしくは、想定外のコード実行が行われないように、Arrayをタスクキューとして使って、そこに期待した順番でタスクを並べてごにょごにょすれば良い(タスクの実行される順序関係を明確化し、それが守られるようにすればよい)

ということでした。
で、まあ、ひとまず "はじめに" に書いた疑問については終わりです。

というか、そもそもyield とか async/await とかのコルーチンを実現する仕組みが入る前から同じ話だったのでは?

で、上記の回答を受けて、またいろいろ考えていたのですが、掲題のように思ったわけです。
なぜなら、非同期関数(I/Oするものを代表にその他もろもろ)は実行に長時間かかるから非同期実行になっているわけで、その関数が非同期に実行されているからといって、JSのスレッドを占有しているわけがない、つまり、その非同期関数内でI/Oが実行されている間は、スレッドの実行権を解放して、他のイベントタスクが実行されているはず。

というわけで、実際に試してみました。

node.js の場合

  • setIntervalで定期的に処理を呼び出すようまず設定
  • トランザクション処理として動かしたい関数(途中で他のイベントタスクに割り込まれることを想定しない)を定義する
  • トランザクション処理に含む非同期関数はファイルI/O(ファイルの読み込み)をするものとする
  • 定義する関数は、単純にコールバックを登録する版と、Promiseでラップした版の2つ
  • 細かい説明は省きますが、割り込まれていなければコンソールに step1, step2, step3 が続けて出ます。setIntervalで登録してあったイベントタスクに割り込まれると、間にその出力が入ってしまいます
nodejs_warikomi_experiment.js
const fs = require('fs');
const util = require('util');

function transaction1(callback){
  console.log("transaction1: step1")
  console.log("transaction1. step2 (file read)")
  fs.readFile("./50MB.bin", (err, data) => {
    if (err) throw err;
    console.log("transaction1: step3");
  });
  callback()
}

function transaction2(){
  const readFile = util.promisify(fs.readFile);
  console.log("transaction2: step1")
  console.log("transaction2. step2 (file read)");

  return readFile("./50MB.bin")
          .then((data) => {
            console.log("transaction2: step3");
          })
          .catch((err) => {
            throw err;
          });
}

// output message every 100ms
setInterval(()=>{console.log("he he he, I try warikomi!")}, 100)

// call transaction1 func and then call transaction2 func
transaction1((err)=>{transaction2()})

実行結果は以下です。
node.jsのバージョンはv10.13.0です。

PS F:\work\tmp\js_warikomi_testing> node --version
v10.13.0
PS F:\work\tmp\js_warikomi_testing> node .\nodejs_warikomi_experiment.js
transaction1: step1
transaction1. step2 (file read)
transaction2: step1
transaction2. step2 (file read)
he he he, I try warikomi!
he he he, I try warikomi!
he he he, I try warikomi!
he he he, I try warikomi!
he he he, I try warikomi!
transaction2: step3
transaction1: step3
he he he, I try warikomi!
he he he, I try warikomi!
he he he, I try warikomi!
he he he, I try warikomi!
he he he, I try warikomi!
(以降省略)

どちらの実装でも割り込まれました。

Web JS の場合 (Google Chrome)

  • ローカルファイルの読み込みを、XMLHttpRequestによるサーバ上のファイルの読み込みに置き換えました
  • 他は node.js の場合と基本的には同じです
webjs_warikomi_experiment.js
var ajax_util = {
  post: (url) => {
    return new Promise((resolve, reject) => {
      let xhr = new XMLHttpRequest();
      xhr.open('POST', url, true);
      xhr.onload = () => {
        if (xhr.readyState === 4 && xhr.status === 200) {
          resolve(xhr.response);
        } else {
          reject(new Error(xhr.statusText));
        } };
        xhr.onerror = () => {
          reject(new Error(xhr.statusText));
        };
        xhr.send("");
      });
    }
  }

function transaction1(callback){
  console.log("transaction1: step1")
  console.log("transaction1. step2 (file read)")
  var req = new XMLHttpRequest();
  req.onreadystatechange = function() {
    if (req.readyState == 4) { // communication finished
      if (req.status == 200) { // communication succeeded
        console.log("transaction1: step3");
        callback()
      }
    }
  }
  req.open('POST', '10MB.bin', true);
  req.setRequestHeader('content-type',
    'application/x-www-form-urlencoded;charset=UTF-8');
  req.send("");
}

function transaction2(){
  console.log("transaction2: step1")
  console.log("transaction2. step2 (file read)");

  ajax_util.post("10MB.bin") // read 10MB files from server
          .then((data) => {
            console.log("transaction2: step3");
          })
          .catch((err) => {
            throw err;
          });
}

function start_experiment(){
  // output message every 100msec
  setInterval(()=>{console.log("he he he, I try warikomi!")}, 100)
  // call transaction1 func and then call transaction2 func
  transaction1((err)=>{transaction2()})
}
javascript
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>expment that whether transactional execution can be implemented with normal coding style</title>
    <script type="text/javascript" src="webjs_warikomi_experiment.js"></script>
  </head>
  <body>
    <h1>Please see web console with developer tools of chrome browser or something</h1>
    <script type="text/javascript">
      // call function on webjs_warikomi_experiment.js
      start_experiment();
    </script>
  </body>
</html>

実行結果は以下です。
Chromeのバージョンは "73.0.3683.75(Official Build) (32 ビット)" です。

devconsole.png

node.jsの場合と同様に割り込まれました。

なお、自分で試してみたいという方のために以下に上記の例を置いてあります。

http://ryogrid.net/dist/webjs_warikomi_experiment.html

デベロッパーツールのコンソールを見てみて下さい。
なお、setIntervalで100ms間隔でconsole.out("うんたら")としているので、確認が終わったらタブは閉じておいた方が良いと思います。

結論(再)

コルーチンとか使ってなくても非同期処理関数呼び出しを含むトランザクション処理を書きたければ、それが担保されるようにコードを書かなければならない!!!

以上です。

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