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

都道府県を地方ごとに配列を組み替えて見た

はじめに

業務上でブラウザ状にちょっとした動きをつけるようなことはよくあるのですが最近珍しくjsonを使った非同期通信をやらせてもらえる機会があったので業務の内容とは別でjsonから取得したオブジェクト配列を地方ごとにまとめてみる処理をしてみました。
画面状には地域別で分けた各オブジェクトのタイトルを出力する仕様です。

area.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="/css/style.css">
  <title>都道府県ごとにタイトルを表示する</title>
</head>
<body>
  <header>
  </header>
  <div class="container">
    <section>
      <h3>北海道</h3>
      <ul id="hokkaido">
        <li></li>
      </ul>
    </section>

    <section>
      <h3>東北地方</h3>
      <ul id="tohoku">
        <li></li>
      </ul>
    </section>

    <section>
      <h3>関東地方</h3>
      <ul id="kanto">
        <li></li>
      </ul>

    </section>

    <section>
      <h3>中部地方</h3>
      <ul id="chubu">
        <li></li>
      </ul>

    </section>

    <section>
      <h3>近畿地方</h3>
      <ul id="kinki">
        <li></li>
      </ul>
    </section>

    <section>
      <h3>中地地方</h3>
      <ul id="chugoku">
        <li></li>
      </ul>
    </section>

    <section id="shikoku">
      <h3>四国地方</h3>
      <ul id="chugoku">
        <li></li>
      </ul>
    </section>

    <section>
      <h3>九州地方</h3>
      <ul id="kyushu">
        <li></li>
      </ul>
    </section>

  </div>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
  <script src="/js/area.js"></script>
</body>
</html>
area.js
$(function(){
  const hokkaido_area = [];
  const hokkaido_pref = ["北海道"];
  const tohoku_area = [];
  const tohoku_pref = ["青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県"];
  const kanto_area = [];
  const kanto_pref = ["茨城県","栃木県","群馬県","埼玉県","千葉県","神奈川県","東京都"];
  const chubu_area = [];
  const chubu_pref = ["新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県", "静岡県", "愛知県"];
  const kinki_area = [];
  const kinki_pref= ["三重県","滋賀県","京都府","大阪府","兵庫県","奈良県","和歌山県"];
  const chugoku_area = [];
  const chugoku_pref = ["鳥取県","島根県","岡山県","広島県","山口県"];
  const shikoku_area = [];
  const shikoku_pref = ["徳島県","香川県","愛媛県","高知県"];
  const kyushu_area = [];
  const kyushu_pref = ["福岡県","佐賀県","長崎県","熊本県","大分県","宮崎県","鹿児島県","沖縄県"];

  $.ajax({
    type: "GET",        
    url: "/json/area.json", 
    dataType: "json", 
  })
  .done(function (data) {
    $.each(data, function(index, value){
      if (hokkaido_pref.includes(value.address)){
        hokkaido_area.push(value);
        $("#hokkaido").append(
          `<li>${value.title}</li>`
        );
      } else if (tohoku_pref.includes(value.address)){
        tohoku_area.push(value);
        $("#tohoku").append(
          `<li>${value.title}</li>`
        );
      } else if (kanto_pref.includes(value.address)){
        kanto_area.push(value);
        $("#kanto").append(
          `<li>${value.title}</li>`
        );
      } else if (chubu_pref.includes(value.address)){
        chubu_area.push(value);
        $("#chubu").append(
          `<li>${value.title}</li>`
        );
      } else if (kinki_pref.includes(value.address)){
        kinki_area.push(value);
        $("#kinki").append(
          `<li>${value.title}</li>`
        );
      } else if (chugoku_pref.includes(value.address)){
        chugoku_area.push(value);
        $("#chugoku").append(
          `<li>${value.title}</li>`
        );
      } else if (shikoku_pref.includes(value.address)){
        shikoku_area.push(value);
        $("#shikoku").append(
          `<li>${value.title}</li>`
        );
      } else if (kyushu_pref.includes(value.address)){
        kyushu_area.push(value);
        $("#kyushu").append(
          `<li>${value.title}</li>`
        );
      } 
    });
  }).
  fail(function () {
    window.alert("通信に失敗しました")
  });
}) 

area.json

[
  {
    "title": "建築のオフィス",
    "parent_category": "architecture",
    "category_slug": "production",
    "address": "北海道",
  },
  {
    "title": "東名高速道路横浜町田インターチェンジPCトールゲート(改修)",
    "image": "http://192.168.11.26:10000/works/works_yokohamamachida.jpg",
    "parent_category": "civil",
    "category_slug": "train",
    "address": "北海道",
  },
  {
    "title": "岡崎市民会館(改修)",
    "parent_category": "",
    "category_slug": "architecture",
    "address": "北海道",
  },
  {
    "title": "法音寺三原支院",
    "parent_category": "",
    "category_slug": "civil",
    "address": "広島県",
  }

{~}
{~}
{~}
{~}

];

これで一応出力は出来ました。都道府県をeachさせながらincludes関数で一致するオブジェクトを地方ごとの配列に組み直す処理です。
配列の初期化等の記述だけで記述量が多くなったんですがもっとこうすればみたいなことがありましたら優しく教えてくださいませ。
よろしくお願いいたします。

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

都道府県を地方ごとに配列を組みかえてみた

はじめに

業務上でブラウザ状にちょっとした動きをつけるようなことはよくあるのですが最近珍しくjsonを使った非同期通信をやらせてもらえる機会があったので業務の内容とは別でjsonから取得したオブジェクト配列を地方ごとにまとめてみる処理をしてみました。
画面状には地域別で分けた各オブジェクトのタイトルを出力する仕様です。

area.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="/css/style.css">
  <title>都道府県ごとにタイトルを表示する</title>
</head>
<body>
  <header>
  </header>
  <div class="container">
    <section>
      <h3>北海道</h3>
      <ul id="hokkaido">

      </ul>
    </section>

    <section>
      <h3>東北地方</h3>
      <ul id="tohoku">

      </ul>
    </section>

    <section>
      <h3>関東地方</h3>
      <ul id="kanto">

      </ul>

    </section>

    <section>
      <h3>中部地方</h3>
      <ul id="chubu">

      </ul>

    </section>

    <section>
      <h3>近畿地方</h3>
      <ul id="kinki">

      </ul>
    </section>

    <section>
      <h3>中地地方</h3>
      <ul id="chugoku">

      </ul>
    </section>

    <section id="shikoku">
      <h3>四国地方</h3>
      <ul id="chugoku">

      </ul>
    </section>

    <section>
      <h3>九州地方</h3>
      <ul id="kyushu">

      </ul>
    </section>

  </div>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
  <script src="/js/area.js"></script>
</body>
</html>
area.json

[
  {
    "title": "タイトル1",
    "parent_category": "architecture",
    "category_slug": "production",
    "address": "北海道",
  },
  {
    "title": "タイトル2",
    "parent_category": "civil",
    "category_slug": "train",
    "address": "北海道",
  },
  {
    "title": "タイトル3",
    "parent_category": "civil",
    "category_slug": "architecture",
    "address": "北海道",
  },
  {
    "title": "タイトル4",
    "parent_category": "car",
    "category_slug": "bus",
    "address": "広島県",
  }

{~}
{~}
{~}
{~}

];

area.js
$(function(){
  const hokkaido_area = [];
  const hokkaido_pref = ["北海道"];
  const tohoku_area = [];
  const tohoku_pref = ["青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県"];
  const kanto_area = [];
  const kanto_pref = ["茨城県","栃木県","群馬県","埼玉県","千葉県","神奈川県","東京都"];
  const chubu_area = [];
  const chubu_pref = ["新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県", "静岡県", "愛知県"];
  const kinki_area = [];
  const kinki_pref= ["三重県","滋賀県","京都府","大阪府","兵庫県","奈良県","和歌山県"];
  const chugoku_area = [];
  const chugoku_pref = ["鳥取県","島根県","岡山県","広島県","山口県"];
  const shikoku_area = [];
  const shikoku_pref = ["徳島県","香川県","愛媛県","高知県"];
  const kyushu_area = [];
  const kyushu_pref = ["福岡県","佐賀県","長崎県","熊本県","大分県","宮崎県","鹿児島県","沖縄県"];

  $.ajax({
    type: "GET",        
    url: "/json/area.json", 
    dataType: "json", 
  })
  .done(function (data) {
    $.each(data, function(index, value){
      if (hokkaido_pref.includes(value.address)){
        hokkaido_area.push(value);
        $("#hokkaido").append(
          `<li>${value.title}</li>`
        );
      } else if (tohoku_pref.includes(value.address)){
        tohoku_area.push(value);
        $("#tohoku").append(
          `<li>${value.title}</li>`
        );
      } else if (kanto_pref.includes(value.address)){
        kanto_area.push(value);
        $("#kanto").append(
          `<li>${value.title}</li>`
        );
      } else if (chubu_pref.includes(value.address)){
        chubu_area.push(value);
        $("#chubu").append(
          `<li>${value.title}</li>`
        );
      } else if (kinki_pref.includes(value.address)){
        kinki_area.push(value);
        $("#kinki").append(
          `<li>${value.title}</li>`
        );
      } else if (chugoku_pref.includes(value.address)){
        chugoku_area.push(value);
        $("#chugoku").append(
          `<li>${value.title}</li>`
        );
      } else if (shikoku_pref.includes(value.address)){
        shikoku_area.push(value);
        $("#shikoku").append(
          `<li>${value.title}</li>`
        );
      } else if (kyushu_pref.includes(value.address)){
        kyushu_area.push(value);
        $("#kyushu").append(
          `<li>${value.title}</li>`
        );
      } 
    });
  }).
  fail(function () {
    window.alert("通信に失敗しました")
  });
}) 

これで一応出力は出来ました。都道府県をeachさせながらincludes関数で一致するオブジェクトを地方ごとの配列に組み直す処理です。
配列の初期化等の記述だけで記述量が多くなったんですがもっとこうすればみたいなことがありましたら優しく教えてくださいませ。
よろしくお願いいたします。

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

初心者です。

書籍にあるcss,javascriptのサンプルコードを書いてみたのですが反映されません・・・
どうすればよいのでしょうか??

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

ブラウザレンダリングの仕組み

概要

webサービスを公開するにあったて必ず使われることになるのがブラウザです。ブラウザがユーザーにwebページを表示する仕組みを理解することで、フロントエンド開発に役立てたり、ページ表示までのレスポンスの改善などに役立てていきたいと思い、今回ブラウザのレンダリングの仕組みの基本事項についてまとめました。

レンダリングの流れ

レンダリングの流れ.png

ブラウザがWebページをレンダリングする仕組みは上のような一連の流れになっています。以下でその一つ一つの工程の内容をみていきます。

レンダリングエンジンとJavaScriptエンジン

各工程をみる前に、ブラウザの構成要素をみていきます。
ブラウザにはレンダリングエンジンとJavaScriptエンジンという2つのエンジンが動作しています。

レンダリングエンジン
HTMLやCSSなどを解析し、実際の画面に描画するためのもの。
レンダリングエンジンによってHTMLやCSSの解釈に差があるためデザインがブラウザによって崩れるという問題があります。

JavaScriptエンジン
JavaScriptを実行するためのエンジン。

【主なブラウザと各種エンジン】

ブラウザ レンダリングエンジン JavaScriptエンジン
Google Chrome Blink V8
Safari Webkit Nitro
IE Trident Chakra
Microsoft Edge EdgeHTML Chakra
Mozilla Firefox Gecko SpiderMonkey
Opera Blink V8

リソースのダウンロード

ブラウザにURLが与えられると、ブラウザは画面のレンダリングに必要なリソース(HTMLやCSSファイル、JavaScriptファイルや画像ファイルなど)を読み込み始めます。

リソースは通常サーバ上に保存されていて、TCP/IPプロトコルを用いてブラウザはサーバーにリクエストしてレスポンスとしてリソースを受け取ります。

オブジェクトモデルの構築

ブラウザではレンダリングを行うために取得したリソースから「オブジェクトモデル」と呼ばれるものを構築します。この時、HTMLはDOMに、CSSはCSSOMに変換されます。
以下の図のように、

バイト→文字列→トークン→ノード→オブジェクトモデル(DOM/CSSOM)

という流れでオブジェクトモデルは構築されます。

constructingObjectModel.png
参考:https://developers.google.com/web/fundamentals/performance/critical-rendering-path/constructing-the-object-model

DOMツリーの構築

dom-tree.png

変換

ブラウザはディスクやネットワークからHTMLのバイトを読み取り、utf8などの文字コードに応じてや

のような文字列に変換します

トークン化

文字列をStartTag:htmlやEndTag:headといったトークンに変換します(W3C HTML5 standardによって決められています)。

字句解析

トークンはプロパティとルールを定義するオブジェクトに変換されます。

DOMの構築

オブジェクトはツリー構造にリンクされます。このツリー構造はオリジナルのマークアップの親子関係をそのまま保持します。つまり、HTMLオブジェクトはbodyオブジェクトの親であり、bodyは

オブジェクトの親であるという関係になります。ブラウザでは以降このページを処理する際に必ずこのDOMを使用します。
ブラウザはHTMLを処理する度に上のDOM構築の一連の流れを全て行うため処理するHTMLが多い場合は一連のプロセスに時間がかかりボトルネックになってしまいます。

ページのライフサイクルイベントのDOMContentLoadedはDOMツリーの構築が完了した時点でレンダリングエンジンにより発火されます。そのためDOMContentLoadedイベントの時点では画像やCSSは読み込まれていない可能性があります。同じライサイクルであるloadイベントは画像やCSSなどを含む全てのリソースを読み込んだ時点で発火します。

CSSOMツリーの構築

cssom-tree.png

ブラウザでDOMを構築している際にドキュメントのheadタグで外部のcssスタイルシートを参照しているlinkタグに遭遇すると、ブラウザはページのレンダリングにこのリソースが必要であると想定してこのリソースに対するリクエストを即座にディスパッチし、CSSのパースを行います。HTMLと同様にバイトが文字列、トークン、ノードに変換され最終的にCSSOMツリーを構築します。

DOMツリーとCSSOMツリーはそれぞれ独立しており後の工程で出てくるレンダリングツリーによってリンクされます。

JavaScriptの実行

各種リソースを読み込んだ後は、JavaScriptエンジンによってJavaScriptのコードが解析、実行されます。
まずJavaScriptのコードが字句解析されトークンとなり、次に構文解析され抽象構文木となり、最後にコンパイルされて実行可能なファイルとなり実行されます。

レンダリングツリーの構築

コンテンツを記述したDOMツリーととドキュメントに適用するスタイルルールを記述したCSSOMツリーを結合することでレンダリングツリーを構築する必要があります。レンダリングツリーは各表示要素のレイアウトを計算するために使用され、画面にピクセルをレンダリングするペイント処理の入力となります。

レンダリングツリー構築の手順として、DOMツリーの各要素に対してどのCSSプロパティがマッチするかを計算します。CSSのルールセットにはh1やpのようなCSSセレクタと、widthやcolorのようなCSSプロパティがあり、最初にCSSセレクタによってDOMツリーの要素とCSSルールのマッチングを行います。その後、各DOM要素にどのCSSプロパティがマッチングするかを計算します。

render-tree-construction.png

CSSセレクタのマッチンング

レンダリングエンジンはCSSセレクタを右側から順に評価していきます。

ex)

.name > ul > li > p {
    color: red;
}

上のような場合、レンダリングエンジンはページ内の全ての要素に対して次のように判定します

  1. DOMがpである
  2. pの親要素がliである
  3. liの親要素がulである
  4. ulの親要素のclass名にnameが含まれている

CSSプロパティのマッチング

どのCSSプロパティがDOM要素に適用されるかをレンダリングエンジンが決定するための詳細度という仕組みがあります。詳細度には3つのレベルがあり、各CSSセレクタとの関連は次のようになっています

A. IDセレクタ
B. クラスセレクタ、擬似クラスセレクタ(:first-child)、属性セレクタ([type=input])
C. 要素セレクタ(div)、擬似要素セレクタ(::before)

各レベルの優先度は A > B > Cとなっていて1つでも上位レベルが含まれる場合はそれが優先され、同じレベルでは値が大きい方が優先されます。

ex)

// A=0, B=2,C=0
.wrapper > .container {
 color: red;
}

// A=0, B=1, C=1
div > .container {
 color: white;
}

// A=1, B=0,C=0
#id {
  font-size: 20px;
}

// A=1, B=0,C=0
#id {
  font-size: 30px;
}

// A=1, B=0, C=1
#id::before {
  content: 'user';
}

上の例で2つのcontainerクラスが同じ要素に対して修飾されている場合、詳細度から color:redの方が優先されます。また詳細度が同じ場合は後に定義されたCSSルールセットが適用されますそのため上の例の#idの要素にはfont-size:30pxが適用されます。

レンダリングツリーのレイアウト

レイアウトの工程では端末ビューボード内での各ノードのレイアウト情報を算出しますレイアウト情報には要素の大きさやmargin/padding、x/y/z軸の位置などが含まれます。
レンダリングエンジンはレンダリングツリーのルート要素から順番に各要素が持つCSSプロパティを元にレイアウト情報を決めていきます。

ex)

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

上のようなHTMLの場合、viewportがデバイスのサイズで倍率が1倍ということに決まります。
ネストされた2つのdivがあり、1つ目のdivがviewportの幅の50%になり、2つ目のネストしたdivの幅は親のdivの50%(全体の25%)になります。

layout-viewport.png

レンダリングツリーの描画

webページの描画にはペイントとコンポジットの2つの工程があります。

ペイント

各レイヤごとにテキストや色、画像などをピクセルに書き込みます。
1. 命令リストの作成
2. ピクセルへの書き込み

の2つの工程からなります。

まず前の手順で構築したレンダリングツリーを元にグラフィックエンジンのための命令リストを作成します。命令リストでは「どのピクセルに何色を入れるのか」という命令が入っているため、その命令を元にグラフィックエンジンがピクセルの描画を行います。
次にピクセルの書き込みを行います(ラスタライゼーション)。ピクセルの書き込みはレイヤ単位で行われ、position:abosolute;やopacityといったCSSプロパティなどz軸を考慮する必要のある要素が存在する場合は新しいレイヤが作成されます。

コンポジット

ピクセルを書き出したレイヤを合成してレンダリング結果を出力します。

まとめ

ブラウザレンダリングの仕組みを各工程ごとにみることで、ページライフサイクルのイベントがどの工程で発生するのかをきちんと理解できるようになりました。レンダリングの仕組みの知識を元に、ブラウザの開発ツールを駆使して開発時にデバッグやパフォーマンスのボトルネックの分析、改善に役立てていきたいと思います。

参考資料

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

JS の Date をテスト時にモックしたい(jest-date-mock の紹介)

はじめに

この記事は Opt Technologies Advent Calendar 2019 1日目の記事です。
2日目の記事は @technicakidz さんによる 開発合宿で作ったもの です。

さて、皆さんテストは書いていますか?
今私が所属しているプロダクトでは日付を扱うコードが多く、それらをどうテストするかが悩みの種でした。
これを解決する手立てはないものかと調べたところ、有用っぽいライブラリを見つけたのでご紹介します。

悩ましい関数

先日私が書いたコードに、以下のような関数が含まれていました。

export const isStartOfThisMonth = () => {
  const today = new Date();
  return isSameDay(today, startOfMonth(today));
};

その名の通り、これは「今日が月初であるかどうかを判定する関数」です。
使用している isSameDay() やら startOfMonth() やらは date-fns の関数です。

この関数は、テストを実行するタイミングによって today の中身が変わってしまうので、全くテスタブルではありません。
多くの人は、以下のように書き換えるべきだと言うでしょう。

export const isStartOfThisMonth = (today = new Date()) => {
  return isSameDay(today, startOfMonth(today));
};

これで today を外部から注入できるようになりました。
ついでに変数名も date とかに変えておきましょうか。
外部から渡されるものが「今日」だとは限りませんので。

export const isStartOfThisMonth = (date = new Date()) => {
  return isSameDay(date, startOfMonth(date));
};

当然、この関数を呼び出す関数もあります。
そういった関数もテストをしたいので、それらも同様に書き換えます。

export const isStartOfThisMonth = (date = new Date()) => {
  return isSameDay(date, startOfMonth(date));
};

export const getInitialState = (date = new Date()): Period => {
  const yesterday = startOfYesterday();
  const type = isStartOfThisMonth(date) ? 'lastMonth' : 'thisMonth';
  return { type, from: startOfMonth(yesterday), to: yesterday };
};

getInitialState() は、Redux の初期 state を吐く関数です。
今日が月初であるか否かで戻り値が変わります。

ですが、この関数は「今日が」月初であるか否かで分岐できていません。
いやできていない訳ではないんですが、引数で「今日」以外の値が渡されるとまるで違う挙動をしてしまいます。
引数として存在している以上、いつ誰が想定と違う使い方をするかわかりません。

また、 date という一般的な単語を使っているために分岐の条件がわかりにくくなっています(これは引数の名前を today に戻せば回避できそうですが)。

更に、この調子だと isStartOfThisMonth() を使う関数全部に date = new Date() という引数を追加しなければなりません。

できることなら new Date() を引数で渡したくない。
でも、そうしないとテストができない。
悩みながら歩き続けた果てに、あるライブラリと出会いました。

Ultimate Solution

Jest の issue に Mocking current time for Date #2234 というものがあり、そこにこんなコメントがありました。

image.png

Ultimate Solution !!!
なんと力強い言葉でしょうか。
これはまさに私が求めていたものです、早速使ってみましょう。

jest-date-mock

Mock Date when run unit test cases with jest. Make tests of Date easier.

hustcc/jest-date-mock より引用

jest-date-mock を使うことで、JavaScript の new Date() や Date.now() をモックすることができます。

$ yarn add --dev jest-date-mock

インストールしたら advanceTo() で new Date() をモックできます。
date-fns の関数の戻り値も期待通りモックされます(Moment.js でどうなるかは試してないのでちょっとわからないです)。

import { advanceTo } from 'jest-date-mock';
import { addDays, startOfMonth } from '../common/app-date-fns';
import { isStartOfThisMonth } from '../services/CalendarQuery';

describe('isStartOfThisMonth', () => {
  describe('月初', () => {
    it('true を返す', () => {
      advanceTo(startOfMonth(new Date()));
      expect(isStartOfThisMonth()).toBeTruthy();
    });
  });
  describe('月初以外', () => {
    it('false を返す', () => {
      advanceTo(addDays(startOfMonth(new Date()), 1));
      expect(isStartOfThisMonth()).toBeFalsy();
    });
  });
});

これで、 isStartOfThisMonth() の引数を無用に増やすことなくテストできるようになりました。

ちなみに、jest-date-mock の中では global.Date の書き換えが行われているようです。

jest-date-mock/src/index.js
if (global.window) {
  // dom env
  global.window.Date = dateClass;
} else {
  // node / native env
  global.Date = dateClass;
}

最後に

テストは大事です、でもテストを書くのはつらいです(テストを書きたくてコードを書いている訳ではないので)。
あと JavaScript の Date class もつらいです( new Date() の第二引数とか)。
生きているとつらいことが沢山ありますが、ちょっと探すと誰かが解決策を提示してたりします。
この記事も誰かの助けになれば幸いです。

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

WEB初心のプログラマーがReact+Firebase+Algoliaでクイズアプリを作成するまで

自己紹介

今回がQiita初投稿となります。
普段はメーカーでC++を使った画像処理系のアプリ開発に携わってます。
もともとWEBには一切触れた事がなかったのですが、
世の中でReactやVueといったフレームワークが話題になった時から
徐々にWEB業界に興味を惹かれてきました。
そして本格的に勉強してみようと思い立ち、今回のアプリ制作に至りました。

サービス紹介

Mushiquiという虫食いクイズアプリを作成しました。

きっかけは社内の暗記試験で、解答を含む全文から手っ取り早く虫食い文章を作成するツールが欲しいなと思った経験からでした。
特定のタグで文字を囲うことで、手軽に虫食いクイズが作成できるようになります。
ブラウザ/モバイルから利用が可能で、PWA対応しているため、ホームに追加して利用が可能です。

制作期間

技術の理解を含めて一年半程になります。
WEBの仕組みからの理解で、一進一退の日々でした。
途中段階の出来を見てショックを受け、実際何度か投げだしましたが(汗)、
それでも地道に実装を続け、なんとか当初想定していた機能が一区切りついたため記事にする事に踏み切りました。

代表的な利用技術

フロントエンド

  • react
  • redux
  • redux-first-router
  • algoliasearch
  • draft-js
  • styled-components
  • storybook
  • react-responsive

redux-first-routerはルーティングに使用。
draft-jsはテキストエディタに使用。

バックエンド

  • Firebase
    • Firestore
    • Cloud Function
    • Authentication
    • Hosting
    • Storage
  • Algolia

システム構成

システム構成は以下のようになっています。

今回、バックエンドにはFirebaseを用いており、データベースにはCloud Firestore、認証にはAuthenticationを使っています。
またクイズの全文検索を実現するために、Algoliaを用いており、AlgoliaのインデックスとFirestoreのデータはCloud Functionを使って同期しています。

苦労した点

学ぶ量の多さ

WEBページを作ること自体が初めての自分にとって、
いきなりReactで物を作るというのは情報量との戦いでした。
QiitaやMediumなどを見れば、Reactの使い方はなんとなくわかるのですが、
自分で考えて使えるようになるまでは、
htmlやcssをはじめjsxなどの根底知識が必要で相当の時間を要しました。

そんな中で最後まで心の頼りだったのが、公式ドキュメントでした。
量がそれなりにあって、全部読むには相当時間がかかりますが、
最近では日本語ページも充実しています。
初期のチュートリアルはもちろんなのですが、アプリを作り始めてから暫くして構造が複雑になってきて悩み始めた時にも、render propsやHOCなどのテクニックが記載されているADVANCED GUIDESは個人的にとても助かりました。

コンポーネントの責務分割

サンプルを見よう見まねで作ったアプリの状態から色々な機能が盛り込まれていくと、
似たようなデザインのコンポーネントが散在し、ボタンの修正ひとつで複数のソースを修正しなくてはいけない非効率な状況に陥りました。

そうしたときに、デザインの変更を局所的に留める仕組みを考えておくことは重要で、自分の場合はAtomic Designを用いることでこの手間を大幅に改善することができました。

Atomic Designについて:
http://atomicdesign.bradfrost.com/chapter-2/

Mushiquiでは以下のようにcomponents下のフォルダ構成と責務を決めています。

components
|-atoms
|-moleculeus
|-organisms
|-templates
|-pages

スクリーンショット 2019-11-22 23.21.39.png

全体のレイアウトや色合いを変えたいときはtemplates、
ボタンの振る舞いやデザインを変えたいときはatomsといったように
変更する目的に応じて、変更する場所を決めておけば
仮に第三者から見てもわかりやすいのではないかと思います。

コンポーネントの責務分割(2)

上記の責務の分割をしたうえでも、
WEBとモバイルは同じコンポーネントでもレイアウトが大きく変化する場合があるため、必要に応じて表示部分のみ責務を分割する必要がありました。
(特にtemplatesやorganisms層で発生)
今回、react-responsiveを用いてモバイル用PresenterとWEB用Presenterに振り分けています。

Component
 |-Container.js
 |-WebPresenter.js
 |-MobilePresenter.js
 |-index.js
 |-index.stories.js
 |-style.js

  • Container.js・・・このコンポーネントにおけるStateを管理するClass Component
  • WebPresenter.js・・・WEB向けの表示を行うFunctional Component
  • MobilePresenter.js・・・Mobile向けの表示を行うFunctional Component
  • index.js・・・ContinerとPresenterをrender propを使い接続。ウィンドウサイズに応じた振り分けも実施。
index.js
import React from 'react';
import Responsive from 'react-responsive';
import WebPresenter from './WebPresenter';
import MobilePresenter from './MobilePresenter';
import Container from './Container';

export const Mobile = props => <Responsive {...props} maxWidth={767} />;
export const Default = props => <Responsive {...props} minWidth={768} />;

const FavoriteList = (props) => {
    return <Container 
        {...props}
        render={(containerProps)=><React.Fragment>
            <Default>
                <WebPresenter {...containerProps} />
            </Default>
            <Mobile>
                <MobilePresenter {...containerProps} />
            </Mobile>
        </React.Fragment>}
    />;
};
export default FavoriteList;

Algoliaとの連携を考慮したFirestoreのデータ設計

クイズを全文検索可能にするために、
Firestoreのデータの一部をAlgoliaのインデックスに登録しています。
Firestoreのクイズ更新をCloud Functionでトリガし
更新の反映をAlgoliaのインデックスに対しても行います。
こうしたときに複雑になるのが、Firestoreのデータの持ち方でした。

検索にはクイズ名称やタグ、作成者などのメタ情報をインプットするために
これらの情報は、すべてAlgoliaのIndex内に持つ必要がありました。
一方で検索にはクイズ本体の情報は不要のため、
クイズ本体の変更はトリガしないようなFirestoreのデータ設計が必要となりました。

結果として、検索時に不要なクイズ本体はサブコレクションとして、他のメタ情報と分離することで上記を実現することができました。

スクリーンショット 2019-11-24 0.14.42.png

(図中のrecipesはクイズ本体(quizzes)とメタ情報を集約する概念)

最後に

長くなりましたが、ここまで読んでいただきありがとうございます。
これから始めるという方や、Reactのアプリ制作で悩まれている方の少しでも参考になれば幸いです。
今後はHooksやContextなど今回のアプリでは取り入れられなかった要素を学習しつつ、随時アプリの更新や、今回書ききれなかった内容についても書いていきたいと思います。

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

Javascript で diff (内部仕様についての補足6)

10/14 に投稿したJavascript で diff (通信なし、ローカルで完結)の内部仕様についての補足、その6です。
Javascript で diff (内部仕様についての補足)
Javascript で diff (内部仕様についての補足2)
Javascript で diff (内部仕様についての補足3)
Javascript で diff (内部仕様についての補足4)
Javascript で diff (内部仕様についての補足5)

文字列を単語で区切る

単語単位の差分をとるためには、文を単語に区切る必要があります。
今回は、文を単語に区切る方法を検討することにします。

英数字のようにスペースと句読点で単語単位で区切られているものは、スペース・句読点・記号文字を目印に区切ればよさそうです。
UNICODEブロックを切り出したままの状態では、句読点と普通の文字が混じった状態ですので、句読点をひとつづつ除外していきます。

//ラテン1補助のところに U+00D7「×」、U+00F7「÷」があるので記号として除外
return ((n < 0x00C0) || (n == 0x00D7) || (n == 0x00F7)) ? CB.BREAK : CB.LATIN;  //×÷

//中略

//記号のところにある「々〆〇〻」(〇は漢数字のゼロ) を漢字として除外
if (0x2E80 <= n) { return ((n < 0x3000) || "々〆〇〻".includes(c)) ? CB.KANJI : CB.BREAK; }

//中略

//「・」中黒を記号として除外
if (n == 0x30FB) { return CB.BREAK; }

//etc.

//最後にいろいろな句読点をまとめて除外!
return c.match(/[\u055A-\u055F\u0589\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u166D\u166E\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u1805\u1807-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\uA4FE\uA4FF\uA60D-\uA60F\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB]/) ?
    CB.BREAK : CB.LETTER;

漢字、ひらがな、カタカナ、ハングルは、一続きのUNICODEブロックに収まっていないため、UNICODEの表を見ながらひとつづつ分類しました。というより、一続きのUNICODEブロックに収まっている言語がほとんどありません……
例えば、キリル文字の場合はロシア語で使用される以外にも周辺諸国で使用されている追加文字があるので、それも含めて分類する必要があります。
カタカナの場合は、U+30A0 - U+30FF の他にも、U+3100 - U+31FF の範囲などに追加文字があります。
「々」は「CJK用の記号および分音記号」に分類されていますが、文字列をパースする上では漢字として扱いたいわけです。
また、長音記号「ー」は、

  • ひらがなの後ろにあればひらがな
  • カタカナの後ろにあればカタカナ

とする必要があります。
それぞれの文字を分類しただけでは単語に区切れないため、先頭から順に直前の文字の分類を覚えている状態でスキャンしていきます。
なお、HTMLの差分をとるときのため、"</" は問答無用に一単語扱いとしています。

文字単位で区切る
区切り_文字単位1.png

単語単位で区切る
区切り_単語単位1.png

今のところ、漢字は、一文字づつ区切るようにしていますが、漢字もかたまり単位で認識させたい場合は、LineDiffEngine.SplitWord() 関数の該当部分をコメントアウトしてください。

LineDiffEngine.SplitWord = function(vChar) {
//中略
        if ((c == "/") && (sWord == "") && (vResult.length > 0) && (vResult[vResult.length - 1] == "<")) {
            vResult[vResult.length - 1] = "</";     //for html
        } else {
            switch (cb) {
                case CB.KANJI:  //漢字も一続きで認識したい場合はこの行をコメントアウト
                case CB.BREAK:
                case CB.WHITE:
                    vResult.push(c);
                    break;
                default:
                    sWord += c;
                    break;
            }
        }
//中略
};

漢字は1文字単位で区切る
区切り_文字単位2.png

漢字は一続きの単位で区切る
区切り_単語単位2.png

例文が変だというツッコミは禁止。

実際のコード

実際のコードは下記のようになりました。
LineDiffEngine.DetectCharBlock() 関数で文字の種別を判定、LineDiffEngine.SplitWord() 関数で単語単位に切り出すパースを行っています。
なお、少しでも判定回数を減らすため、

  • 大雑把にUNICODEブロックの切りのいい範囲で文字コードの範囲を分けて分岐
  • さらに詳細に調べる

というようにしています。
ラテン文字とCJK以外の言語で、LineDiffEngine.DetectCharBlock() 関数の終端まできた文字は LineDiffEngine.CHAR_BLOCK.LETTER に分類されます。
今のところ、絵文字は仕様を調べきれていないです……

LineDiffEngine.CHAR_BLOCK = {
      LETTER : 0            //分類しきれなかった文字はすべてここ
    , WHITE : 2             //空白
    , BREAK : 3             //記号、句読点
    , LATIN : 4             //ラテン文字
    , LEFT_BRACKET : 5      //左かっこ(行内差分の境界調整時に使用)
    , HIRAGANA : 11         //ひらがな
    , KATAKANA : 12         //カタカナ
    , INHERITED_KANA : 13   //ー゛゜ 直前の文字に依存
    , KANJI : 15            //漢字
    , HANKANA : 16          //半角カナ
    , HANGUL : 20           //ハングル
    , GREEK : 21            //ギリシャ文字
    , CYRILLIC : 22         //キリル文字
    , HEBREW : 23           //ヘブライ文字
    , ARABIC : 24           //アラビア文字
    , VARIATION : 999       //異体字セレクタ
};
LineDiffEngine.DetectCharBlock = function(c) {
    const n = c.charCodeAt(0), CB = this.CHAR_BLOCK;
    if (n < 0x0250) {
        if ((0x0061 <= n && n < 0x007B) || (0x0041 <= n && n < 0x005B) || (0x0030 <= n && n < 0x003A) || (n == 0x005F)) {
            return CB.LATIN;    //[a-zA-Z0-9_]
        }
        if (n == 0x0009 || n == 0x0020) { return CB.WHITE; }
        //if ((n == 0x00AA) || (n == 0x00B5) || (n == 0x00BA)) { return CB.LATIN; }
        return ((n < 0x00C0) || (n == 0x00D7) || (n == 0x00F7)) ? CB.BREAK : CB.LATIN;  //×÷
    } else if (0x2000 <= n && n < 0x3040) {
        if (0x2E80 <= n) { return ((n < 0x3000) || "々〆〇〻".includes(c)) ? CB.KANJI : CB.BREAK; }
        if (0x2C60 <= n && n < 0x2C80) { return CB.LATIN; } //2C60-2C7F Latin Extended-C
        if (0x2DE0 <= n && n < 0x2E00) { return CB.CYRILLIC; }  //2DE0-2DFF Cyrillic Extended-A
        if ((0x2800 <= n && n < 0x2900) || ((0x2C00 <= n && n < 0x2DE0) && !c.match(/[\u2cf9-\u2cfc\u2cfe\u2cff\u2d70]/))) {
            return CB.LETTER;
        }
        return CB.BREAK;    //General Punctuation etc.
    } else if (0x3040 <= n && n < 0x3200) {
        if (n < 0x3100 || 0x31F0 <= n) {
            if (n == 0x30FB) { return CB.BREAK; }   //・
            if ((n == 0x30FC) || (n == 0x309B) || (n == 0x309C)) { return CB.INHERITED_KANA; }  //ー゛゜
            return ((n >= 0x30A0) ? CB.KATAKANA : CB.HIRAGANA);
        }
        if (0x3130 <= n && n < 0x3190) { return CB.HANGUL; }    //3130-318F Hangle Compatibility Jamo
        return ((0x3190 <= n && n < 0x31A0) || (0x31C0 <= n)) ? CB.KANJI : CB.BREAK;
    } else if ((0x3200 <= n && n < 0xA000) || (0xF900 <= n && n < 0xFB00)) {
        //3400-9FFF CJK Unified Ideographs Extension A, Yijing Hexagram Symbols, CJK Unified Ideographs
        if (0x3400 <= n) { return CB.KANJI; }
        return (((0x3280 <= n) || (0x3220 <= n && n < 0x3260)) ? CB.BREAK : CB.HANGUL);
    } else if (0xFF00 <= n && n < 0xFFF0) {     //Halfwidth and Fullwidth Forms
        if (0xFF61 <= n && n < 0xFFA0) { return CB.HANKANA; }
        if ((0xFF10 <= n && n < 0xFF1A) || (0xFF21 <= n && n < 0xFF3B) || (0xFF41 <= n && n < 0xFF5B)) {
            return CB.KANJI;    //0-9A-Za-z
        }
        return (0xFFA0 < n && n < 0xFFE0) ? CB.HANGUL : CB.BREAK;   //FFA0 hungul space
    } else if (0xD800 <= n && n < 0xDC00) {     //surrogate pair
        const trail = c.charCodeAt(1);
        if (trail < 0xDC00 || 0xDFFF < trail) { return CB.LETTER; } //illegal
        if (0xD840 <= n && n < 0xD880) {
            return CB.KANJI;    //20000-2FFFF CJK Unified Ideographs Extension B-F, CJK Compatibility Ideographs Supplement
        } else if ((n == 0xDB40) && (0xDD00 <= trail && trail < 0xDDF0)) {
            return CB.VARIATION;    //E0100-E01EF Variaion Selectors Supplement
        } else if ((n == 0xD82C) && (trail < 0xDD30)) {
            return (trail == 0xDC00) ? CB.KATAKANA : CB.HIRAGANA
        }
        const cp = (((c - 0xD800) << 10) + (trail - 0xDC00) + 0x10000);
        return ((0x1F000 <= cp && cp < 0x1FA70) ? CB.BREAK : CB.LETTER);    //0x1F000-0x1FA6F Pictographs etc.
    } else if ((0x1100 <= n && n < 0x1200) || (0xA960 <= n && n < 0xD800 && (n < 0xA980 || 0xAC00 <= n))) {
        return CB.HANGUL;
    } else if ((0x0400 <= n && n < 0x0530) || (0xA640 <= n && n < 0xA6A0) || (0x1C80 <= n && n < 0x1C90)) {
        return ((n == 0xA673) || (n == 0xA67E)) ? CB.BREAK : CB.CYRILLIC;
    } else if ((0x0370 <= n && n < 0x0400) || (0x1F00 <= n && n < 0x2000)) {
        return ((n == 0x037E) || (n == 0x0387)) ? CB.BREAK : CB.GREEK;
    } else if ((0x0590 <= n && n < 0x0600) || (0xFB1D <= n && n < 0xFB50)) {
        return ((n == 0x05C0) || (n == 0x05C3) || (n == 0x05C6) || (n == 0x05F3) || (n == 0x05F4)) ? CB.BREAK : CB.HEBREW;
    } else if (0xFE00 <= n && n < 0xFE70) {
        return ((n < 0xFE10) ? CB.VARIATION : CB.BREAK);
    } else if ((0x0600 <= n && n < 0x0780 && (n < 0x0700 || 0x074F < n)) || (0x08A0 <= n && n < 0x0900) || (0xFB50 <= n && n < 0xFF00)) {
        return ((n == 0x060C) || (n == 0x060D) || (n == 0x061B) || (n == 0x061E) || (n == 0x061F) || (0x066A <= n && n < 0x066D) || (n == 0x06D4)) ?
            CB.BREAK : CB.ARABIC;
    } else if ((0x1E00 <= n && n < 0x1F00) || (0xA720 <= n && n < 0xAB70 && (n < 0xA800 || 0xAB30 <= n)) || (0xFB00 <= n && n < 0xFB07)
    || (0x0300 <= n && n < 0x0370)) {
        return CB.LATIN;
    } else if (0x1D00 <= n && n < 0x1D80) { //1D00-1D7F Phonetic Extensions
        if ((0x1D26 <= n && n <= 0x1D2A) || (0x1D5D <= n && n <= 0x1D61) || (0x1D66 <= n && n <= 0x1D6A)) {
            return CB.GREEK;
        }
        return (n == 0x1D2B) ? CB.CYRILLIC : CB.LATIN;  //1D2B Cyrillic Letter Small Capital El
    }
    //return CB.LETTER;
    return c.match(/[\u055A-\u055F\u0589\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u166D\u166E\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u1805\u1807-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\uA4FE\uA4FF\uA60D-\uA60F\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB]/) ?
        CB.BREAK : CB.LETTER;
};
LineDiffEngine.SplitWord = function(vChar) {
    let vResult = [], sWord = "", bDifferent, CB = this.CHAR_BLOCK, cb_prev = CB.LETTER;
    for (let c of vChar) {
        let cb = this.DetectCharBlock(c);
        switch (cb) {
            case CB.HIRAGANA:
            case CB.KATAKANA:
                bDifferent = ((cb_prev != cb) && (cb_prev != CB.INHERITED_KANA));
                break;
            case CB.INHERITED_KANA:
                bDifferent = ((cb_prev != CB.HIRAGANA) && (cb_prev != CB.KATAKANA) && (cb_prev != cb));
                if (!bDifferent) { cb = cb_prev; }
                break;
            case CB.VARIATION:
                bDifferent = false;
                cb = cb_prev;
                break;
            default:
                bDifferent = (cb != cb_prev);
                break;
        }
        if (bDifferent) {
            if (sWord != "") {
                vResult.push(sWord);
                sWord = "";
            }
            cb_prev = cb;
        }
        if ((c == "/") && (sWord == "") && (vResult.length > 0) && (vResult[vResult.length - 1] == "<")) {
            vResult[vResult.length - 1] = "</";     //for html
        } else {
            switch (cb) {
                case CB.KANJI:
                case CB.BREAK:
                case CB.WHITE:
                    vResult.push(c);
                    break;
                default:
                    sWord += c;
                    break;
            }
        }
    }
    if (sWord != "") { vResult.push(sWord); }
    return vResult;
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Js 文字列の操作、エンコードとデコード

JavaScript

文字列の長さを調べる

qiita.js
"abc".length

文字列の置換

qiita.js
//該当文字列が複数の場合、最初の文字列が置換される
"abcdeabcde".replace("abc", "ABC")
//すべての該当文字列を置換したい場合
"abcdeabcde".split("abc").join("ABC")

エンコードとデコード

qiita.js
//エンコード
encodeURI("あいうえお")
encodeURIComponent("あいうえお")
//デコード
decodeURI("%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A")
decodeURIComponent("%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2019/11/24 学習備忘録

JavaScript

文字列の長さを調べる

qiita.js
"abc".length

文字列の置換

qiita.js
//該当文字列が複数の場合、最初の文字列が置換される
"abcdeabcde".replace("abc", "ABC")
//すべての該当文字列を置換したい場合
"abcdeabcde".split("abc").join("ABC")

エンコードとデコード

qiita.js
//エンコード
encodeURI("あいうえお")
encodeURIComponent("あいうえお")
//デコード
decodeURI("%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A")
decodeURIComponent("%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

M5stackをModdableを使ってjavascriptで動かしてみる

はじめに

昨今、とてもはやっているそうなjavascriptを学んでみたかったのです。
何かできないかなぁと調べていたらM5stackがModdable + JavaScriptで動くという記事があったため、試してみようということで始めました。

調べて出てきた記事が以下です。
著者のJavaScript視点からの説明は勉強になりました。ありがとうございます!

JavaScript x IoTの大本命「ModdableSDK」をM5Stackで動かしてみた

本記事では、windowsで構成を作ってみたいと思います。

やりたいこと

  • windowsでのModdableの構成
  • JavaScriptでのプログラミング
  • Moddableで定周期制御やタイマ割込みの周期調べ

開発環境

  • windows 10 Home 64bit
  • Moddable
    • 使用言語はJavaScript

M5stackについて

M5stackはESP32を使用しているLCDディスプレイ付きのマイコンキットです。
ESP32はFree RTOSを基にArudiono(C,C++)を使ってプログラミング可能です。
開発環境では、PlatformIOなどがあります。

m5stackについては以下の記事に中身を調べたことを記載しています。

m5stackで遊ぼう

Moddableについて

ModdableはJavaScriptでマイコンプログラミングするためのSDKだそうです。
公式は以下になります。英語なので中学生以下の英語力しかない自分では読むのが大変です。
Google翻訳を駆使して自分なりの解釈で構成を進めていきます。

Moddable-OpenSource/moddable

対応マイコンは以下だそうです。

  • ESP8266(Xtensa)
  • ESP32(Xtensa)
  • Gecko(Arm)
  • QCA4020(Arm)

GeckoとQCA4020は初めて知りました。
wifi付きマイコンは今すごい増えてるんですね。

ゲッティングスタート

ここから本題です。
頑張って英語を翻訳にぶち込みながら進めていきます。

あなたの環境でセットアップしよう!

今回の自分の環境はwindowsです。セットアップのドキュメントが別にあるのでそれを進めろと言われてます。
ESPシリーズ、Geckoシリーズ、QCA4020はそれぞれでドキュメントはあるようです。
今回はESPシリーズしか使わないので素直に進めていきます。

手順としては先にPC側の設定、次にマイコンとの接続設定となっているようです。マイコンはESP32のセットアップを進めます。
基本的には公式ドキュメントに記載があるフォルダ構成を作ってコマンドを入力していけば問題ないです。
以下の手順は自分がざっくり行った手順を記載しています。詳細は公式ドキュメントを読んでください

PC側の設定の手順

PC側はGitのインストールしているとコマンドですべてできます。

  1. Microsoft Visual Studio 2019 Community EditionのDesktop development for C++インストールする
  2. GitでModdableのダウンロード
  3. xsbugとmcconfigを使い為の環境変数設定の設定
  4. xsbugの起動とmcconfigでサンプルのハローワールドのコンパイルと起動

PC側の設定では、x86 Native Tools Command Prompt for VS 2019を使ってビルドのみではなく環境変数設定とPATHの設定もできます。
なので、手順1.のインストールが終わったらすべてをx86 Native Tools Command Prompt for VS 2019でコマンドを打ち込んで問題なかったです。

常識かもしれませんが・・・環境変数のPATH設定の時にPATHを追加する形で書いてください。
自分はこの辺りが疎すぎてPATHを上書きですべて消してしまって再インストールすることになりました・・・

ESP32の接続設定の手順

  1. USB-UART変換ドライバのインストール(これはUSBさすと自動でインストールされます)
  2. tool chain (all-in-one Windows toolchain and MSYS2) のダウンロード
  3. 'MinGW32を起動してESP-IDFのダウンロード(git clone`でダウンロード)
  4. シェルファイルの作成、中身はコマンドのみを記載(ここだけコマンドラインを使わずにやりました)
  5. m5stackとPCをUSBでつないでCOM PORT番号を確認
  6. 環境変数の設定を行ってコンパイルとダウンロードを実施(最後のコマンドはmcconfig -d -m -p esp32/m5stackにする必要があります)

ここも手順の2.3以外はすべてx86 Native Tools Command Prompt for VS 2019で文章通りのコマンドを打つだけでした。
一か所引っかかったところは6.の環境変数の設定で、\ではなく、/で階層を表せってところでした。
ちゃんと英語で注意書きに書かれていたので見逃していました・・・

やってみた感想

基本的には公式ドキュメントのコマンドを次々打っていく感じでした。
途中、シェルファイルの作成があったり、MinGW32の操作がありましたが、基本的にはフォルダ構成の作成と環境変数の設定だけだったので特に考えないで実行する分には簡単でした。

ですが、今回のハローワールドのサンプルだと特にシリアル通信で返答するわけでもなく一瞬でターミナルが終了するので書き込めてるのかどうか確認しにくかったです。
今回は自分が前もって入れていたテストのファームが入っていたので書き換わっていると確認取れましたが、実際何が起きてるか全くわからなかったです。唯一変わったのはm5stackから音が出ることですが、それで正しいのかわからないです。

この手順をやってみて気づいたのですが、xsbugは基本的にwindowsのみの場合のみデバッグ機能なのか、自分の設定が悪くて使えてないだけなのかわからなかったです。
後は、x86 Native Tools Command Prompt for VS 2019だけで完結しているのでうまくいけばVSCodeでも動かせそうかもしれないですが、自分の技量だとわからないですね・・・。

最後に

今回は環境セットアップのみでしたが、これだけで1日かかりました。
やっぱり環境を作るのが一番時間がかかります・・・
開発環境の設定が自分にとって一番大変な作業なのでどなたかできたら教えてほしいです。

次回はjavascriptでm5stackを動かしてみようと思います。

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

Cypress Command Retry

Retryについて

「Not every command is retried 」とあったので調べてみた

結果

以下の結果から
概ねretry(or wait)されると思っていてよいのではないでしょうか

Command

API Retry(Wait) Link
and - link
as .as() is a utility command. link
blur 〇 link
check 〇 link
children 〇 link
clear 〇 link
clearCookie - link
clearCookies - link
clearLocalStorage - link
click 〇 link
clock cy.clock() is a utility command. link
closest 〇 link
contains 〇 link
dblclick 〇 link
debug .debug() is a utility command. link
document 〇 link
each .each() will only run assertions you've chained once link
end - link
eq 〇 link
exec cy.exec() will only run assertions you've chained once link
filter 〇 link
find 〇 link
first 〇 link
fixture cy.fixture() will only run assertions you've chained once link
focus 〇 link
focused 〇 link
get 〇 link
getCookie cy.getCookie() will only run assertions you've chained once, link
getCookies cy.getCookies() will only run assertions you've chained once link
go 〇 link
hash 〇 link
invoke 〇 link
its 〇 link
last 〇 link
location 〇 link
log - link
next 〇 link
nextAll 〇 link
nextUntil 〇 link
not 〇 link
parent 〇 link
parents 〇 link
parentsUntil 〇 link
pause .pause() is a utility command. link
prev 〇 link
prevAll 〇 link
prevUntil 〇 link
readFile 〇 link
reload 〇 cy.reload() will automatically wait link
request cy.request() will only run assertions you've chained once link
rightclick 〇 .rightclick() will automatically wait link
root 〇 link
route - link
screenshot cy.screenshot() will only run assertions you've chained once link
scrollIntoView 〇 .scrollIntoView() will automatically wait link
scrollTo - link
select 〇 .select() will automatically wait link
server - link
setCookie cy.setCookie() will only run assertions you've chained once link
should 〇 .should() will continue to retry its specified assertions until it times out. link
siblings 〇 link
spread .spread() will only run assertions you've chained once, link
spy - link
stub - link
submit 〇 .submit() will automatically wait for assertions link
task cy.task() will only run assertions you've chained once link
then .then() will only run assertions you've chained once link
tick cy.tick() is a utility command. link
title 〇 link
trigger 〇 .trigger() will automatically wait link
type 〇 .type() will automatically wait link
uncheck 〇 .uncheck() will automatically wait link
url 〇 link
viewport - link
visit 〇 cy.visit() will automatically wait link
wait cy.wait() will only run assertions you've chained once, link
window 〇 link
within .within() will only run assertions you've chained once, link
wrap 〇 link
writeFile cy.writeFile() will only run assertions you've chained once, link
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cypressテストコード(Request関連)

API

integration/sample_spec.js
describe('request test', function() {
  it('', function() {
    cy.server()
    cy.route('GET','**/getUsers').as('getUsers')
    cy.wait('@getUsers')

    cy.get('#results').should('contain', 'user1')
  })
})

Stub

integration/sample_spec.js
describe('request test', function() {
  it('', function() {
    cy.server()
    cy.route('GET', '**/getUsers',
    {data: [
      {id: '1',name: 'user1'},
      {id: '2',name: 'user2'}
    ]}).as('getUsers')

    cy.wait('@getUsers')

    cy.get('#results').should('contain', 'user1')
  })
})

Fixtures

integration/sample_spec.js
describe('request test', function() {
  it('', function() {
    cy.server()
    cy.route('GET', '**/getUsers', 'fixture:getUsers').as('getUsers')

    cy.wait('@getUsers')

    cy.get('#results').should('contain', 'user1')
  })
})
fixtures/getUsers.json
{
  "data": [{
      "id": "1",
      "name": "user1"
    },
    {
      "id": "2",
      "name": "user2"
    }
  ]
}

階層が深くてもいい

integration/sample_spec.js
describe('request test', function() {
  it('', function() {
    cy.server()
    cy.route('GET', '**/getUsers', 'fixture:test/getUsers').as('getUsers')

    cy.wait('@getUsers')

    cy.get('#results').should('contain', 'user1')
  })
})
fixtures/test/getUsers.json
{
  "data": [{
      "id": "1",
      "name": "user1"
    },
    {
      "id": "2",
      "name": "user2"
    }
  ]
}

複数Wait

integration/sample_spec.js
cy.server()
cy.route('activities/*', 'fixture:activities').as('getActivities')
cy.route('messages/*', 'fixture:messages').as('getMessages')

cy.visit('http://localhost:8888/dashboard')

cy.wait(['@getActivities', '@getMessages'])

cy.get('h1').should('contain', 'Dashboard')
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptでurlに改行やスペースを入れて何かを叩く

はまったこと

var profile='[なまえ] hoge\n[あぴーるぽいんと] 文字列連結がとくい。("a"+"b")';
var url="https://hogehoge.com/user&profile="+profile;

このままだと叩けないけど、こんな感じの文字列をurlに乗せて叩きたい

かいけつ

encodeURIComponent使う

var url="https://hogehoge.com/user&profile="+encodeURIComponent(profile);

こことか使っても良さそう

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

Node.js, JavaScript学習まとめ

今回の学習のゴール

  1. Node.jsについて知る
  2. 基本文法を学ぶ
  3. ライブラリを把握する

目次

  1. Node.jsとは
  2. そもそもJavaScriptとは
  3. JavaScriptの基本知識
  4. Node.jsの基本知識
  5. ライブラリの把握
  6. 今後の課題

1. Node.jsとは

スケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動のJavaScript環境
Node.js

  • それぞれの意味
    • スケーラブル : 拡張性が高い
    • 非同期 : 各要求(request)の処理が完了するのを待たずに、それ以降の処理を行う方式
    • イベント駆動 : イベントと呼ばれるアプリや端末上で起きた出来事に対して処理を行うプログラムの実行形式
  • 特徴
    • サーバーサイドで使用できる
    • ノンブロッキングI/Oモデルを採用しており、I/Oの処理を待たずに次の処理を始めることができるので、大量のデータ処理が可能
      • ノンブロッキング : ある処理を行いながら、ほかの処理も同時進行で行えること
      • I/O : Input/Outputの略で、入出力の意

2. そもそもJavaScriptとは

  • ブラウザに実行エンジンが搭載されたプログラミング言語
  • Netscape Navigatorというブラウザ向けに開発され、その後Internet Explorer, Firefox, Chromeなどの主要ブラウザに採用された
  • 特徴

    • ブラウザで動作する
    • 実行エンジンの内部で動的にコンパイルが行われるので、コンパイルしなくとも実行できる
    • 動的型付け言語
  • JavaScript - Wikipedia によると

JavaScriptはプロトタイプベースのオブジェクト指向スクリプト言語であるが、クラスなどのクラスベースに見られる機能も取り込んでいる

  • それぞれの意味
    • オブジェクト指向スクリプト言語
      • オブジェクトを組み立てるように表現して、コンピュータに動作をさせ、script(台本、原稿)のようにプログラムを記述できるプログラミング言語
    • プロトタイプベース
      • 全てのオブジェクトがプロトタイプ(試作品)をベースにして作られているもの
      • プロトタイプと呼ばれるテンプレートをコピーして、新しいオブジェクトが作られるイメージ
    • クラスベース
      • 全てのオブジェクトがクラスをベーシにして作られているもの
      • クラスはオブジェクトを作る設計書のことで、クラスそのものの名前、属性、処理の3つの要素を持つオブジェクトをまとめて定義したもの

js.jpg

3. JavaScriptの基本知識

データ型

  • String : 文字列
  • Number : 数
  • Boolean : 真偽値
  • Null : 値が存在しないまたは無効なオブジェクト
  • Undefined : 値を代入していない変数の値
  • Array : 複数の値を格納可能
  • Object : 基本的に何でも格納可能
// 文字列を整数に変換するメソッド
parseInt('030', 10);    // 第2引数には変換の基数, 30が返される
parseInt('hello', 10);    // NaN(非数  "Not a Number" の略)が返される
// 文字列の操作方法
'hello'.charAt(0); // "h"を返す
'hello, world'.replace('hello', 'goodbye'); // "goodbye, world"を返す
'hello'.toUpperCase(); // "HELLO"を返す

演算子

1 + 1;                          // 数字を加える
'Hello' + 'world';              // 文字列の結合
10 - 1;                         // 減算
2 * 3;                          // 乗算
10 / 2;                         // 除算
var myVariable = 'value';       // 代入
myVariable === 'value';         // 等価 値と型が等しいか真偽値で返す 変数myVariableにvalueが代入されている場合は、trueが返される
!(myVariable === 'value');      // 否定 値と型が等しくないか真偽値で返す
myVariable !== 'value';_        //非等価 値と型が等しくないか真偽値で返す
weather === 'sunny' && temperature < 25    // AND 2つ以上の式を1つに繋げそれぞれの式を個別に評価、全てtrueになった場合その式全体がtrueを返す
weather === 'sunny' || temperature < 25    // OR 2つ以上の式を1つに繋げそれぞれの式を個別に評価し、最初にtrueになったところでその式全体をtrueとして返す
x += 5;    // x = x + 5;の意 複合代入文という

変数

  • varを用いた変数の宣言
var <変数名>;    // 変数の宣言
<変数名> = '値';    // 変数に値を割り当て, 変数の値を変更する
var <変数名> = '値';
<変数名>;    // 変数の値を取得
  • 変数のスコープは関数単位
function f() {
    var num = 123;
    console.log(num);
    {
    var num = 456;
    console.log(num);
    }
    console.log(num);
}
f();

// 実行結果
123
456
456
  • letを用いた変数の宣言
let <変数名> = '値';    // ブロックレベルの変数を宣言
  • 変数のスコープがブロックに限定される
function f() {
    let num = 123;
    console.log(num);
    {
    let num = 456;
    console.log(num);
    }
    console.log(num);
}
f();

// 実行結果
123
456
123

定数

const number = '10';    // 定数の宣言 一度宣言された値は変更不可

配列

// 配列を生成する①
var person = new Array();
person[0] = 'たなか';
person[1] = 'なかむら';
person[2] = 'しぶや';

// 配列を生成する②
var person = ['たなか', 'なかむら', 'さいとう']

// 配列に要素を追加する
person.push(いとう);

条件文 if

var color = 'red'
if (color === 'red') {         // (条件式)がtrueを返した場合、以下の処理が実行される
    alert('好きな色は赤です');    // アラートを使って表示
} else if (color === blue) {
    alert('好きな色は青です');
} else {                       // 2つの(条件式)がfalseを返した場合、elseの後の処理が実行される
    alert('好きな色は黄色です');
}

switchステートメント

var color = 'red'
switch (color) {
  case 'red':
    alert('好きな色は赤です');
    break;    // 値がcaseにマッチした時ループを抜ける

  case 'blue':
    alert('好きな色は青です');
    break;

  // 以下に選択肢を好きなだけ並べることが可能

  default:
    alert('選択肢に好きな色がありません');
}

ループ

  • カウンター : ループの開始地点で、初期化される値
  • 終了条件 : ループが終了する条件
  • イテレーター : 終了条件を満たすまで、カウンターの値をループごとに少量ずつ増加(減少)させる
// forを使ったループ
var sequence = [1, 2, 4, 7, 11, 16, 22];
for (var i = 0; i < sequence.length; i++) {    // カウンター変数を宣言 lengthプロパティを使用して配列の長さを取得し、ループを配列の長さと同じ数になったら、繰り返しを終了
  console.log(sequence[i]);
}
// whileを使ったループ
var sequence = [1, 2, 4, 7, 11, 16, 22];
var i = 0;    // 初期化処理
while (i < sequence.length) {
    console.log(sequence[i]);

    i++;
}
// do...whileを使ったループ
var sequence = [1, 2, 4, 7, 11, 16, 22];
var i = 0;
do {
    console.log(sequence[i]);

    i++;
} while (i < sequence.length)

関数

  • 再利用したい機能をパッケージ化する方法
function sayHello() {
  alert('hello');
}

myFunction();    // functionの呼び出し helloのアラートが表示される
function sum(num1, num2) {    //関数の定義 関数に複数の引数がある場合はカンマで区切る
    var result = num1 + num2;
    return result;
}
sum(1, 2);    # コンソールで実行すると3が返ってくる

イベント

  • ブラウザの中で起きていることを検出し、その応答としてコードを実行する
  • 動作や操作(以下の例ではクリック)に対して特定の処理を与えるための命令のことをイベントハンドラという
  • ブラウザに組み込まれたJavaScript APIの一部として定義されたもの
var page = document.selector('html');    // 関数を定義し変数に代入
page.onclick = function() {    // 無名関数は主にイベント発火のレスポンスとして、一連のコードを走らせるだけのような場合に、イベントハンドラとして使われる
    alert('ページがクリックされた');
};

オブジェクト

  • 関連のあるデータと機能をひとまとめにしたモノ
  • 機能はたいてい変数と関数で構成され、オブジェクトの中ではそれぞれプロパティとメソッドと呼ばれる
var obj = new Object();    // 空のオブジェクトを作成する方法①
var obj = {};    // 空のオブジェクトを作成する方法②オブジェクトリテラル構文
  • オブジェクトリテラル使用してオブジェクトを生成する例
var person = {
  name: ['たかはし', 'なかむら'],
  age: 20,
  gender: 'female',
  greeting: function() {    // オブジェクトのメソッド
    alert('こんにちは、' + this.name[1] + 'と申します。' + this.age + '歳です。');    // thisは現在のオブジェクトを参照しているので、personを指す
  }
};
# コンソールで実行

person    # {name: Array(2), age: 20, gender: "female", greeting: ƒ} と返ってくる
person.name[1]    # "なかむら" と返ってくる
person.greeting()    # こんにちはなかむらと申します。20歳です。 とアラートが返ってくる
person.age = 30;    # 値を上書きすることができる
// サブ名前空間でオブジェクト生成するときの記載方法

name: {
  first: 'たかはし',    // name.firstで"たかはし"が返ってくる
  second: 'なかむら'    // name.secondで"なかむら"が返ってくる
}

継承

  • ”親”クラスからの機能を継承する”子供”のオブジェクトクラス (コンストラクタ) の生成方法について
// コンストラクタ内部にプロパティのみを定義
function Person(first, second, age, gender) {
  this.name = {
    first,
    last
  };
  this.age = age;
  this.gender = gender;
};

// メソッドはすべてコンストラクタのプロトタイプとして定義する
Person.prototype.greeting = function() {
  alert('こんにちは、' + this.name.first + 'と申します。' + this.age + '歳です。');
};

// Personコンストラクタの子であるTeacherコンストラクタを作成
function Teacher(first, second, age, gender, subject) {
  Person.call(this, first, second, age, gender);    // call()関数 その他の場所で定義された関数から呼ぶことができる

  this.subject = subject;    // Teacherだけが持つプロパティを定義
}

prototype弄るのは基本しないらしいby先生

JSON(JavaScript Object Notation)

  • JavaScript オブジェクトの構文に従ったテキストベースのフォーマット
  • ウェブアプリケーションでデータを転送する場合に使われる
  • MIME type(メディアタイプ)がapplication/jsonで、「.json」という拡張子の付いたテキストファイルとしてJSON自身を格納することもできる(以下その例)
{
  "companyName": "Super heroes",
  "homeTown": "Central City",
  "formed": 2005,
  "active": true,
  "members": [
    {
      "name": "Takahashi",
      "age": 28,
      "business description": [
        "labor management",
        "Payroll"
      ]
    },
    {
      "name": "Nakamura",
      "age": 35,
      "business description": [
        "Disclosure",
        "Payment",
        "Sales recording"
      ]
    }
  ]
}
  • このオブジェクトをJavaScriptのプログラムへ読み込む(excellenceという変数に代入したとすると)と、ドットや角括弧を使ってデータへアクセスすることができる ※JSONでは文字列とプロパティ名をシングルクォートではなく、ダブルクォートで括る
excellence.companyName
excellence['members'][1]['business description'][0]    // 2番目のメンバーの1番目の業務内容を参照

Web API

  • Application Programming Interfacesの略
  • 開発者が複雑な機能を簡単に作成できるように、プログラミング言語から提供される構造のこと
  • ブラウザやサイトが動作しているOSの様々な面を操作したり、他のWebサイト、サービスから取得したデータを操作するためのプログラムされた機能である
  • APIのカテゴリ
    • ブラウザAPI : Webブラウザに組込まれているAPIで、ブラウザやコンピュータの環境の情報を取得して複雑な機能を簡単に実装できる(ex. Geolocation API)
      • ブラウザで読み込んだ文書を操作するためのAPI, サーバからデータ取得をするAPI, クライアント側でのデータ保持APIなどがある
    • サードパーティAPI : サードパーティのプラットフォーム(TwitterやFacebook)上に作られた構造で、それらの機能をWebページで利用できるようにする(ex. Twitter API, Google Maps API, YouTube API)

クロージャ

  • 関数とその関数が作られた環境が一体となった特殊なオブジェクトのこと
  • あるコードブロック内で定義された関数などが、そのブロックをスコープとする変数などを参照できる
  • クロージャが用意されていないと、ある関数内で参照できる変数は引数とその関数内で定義されたローカル変数およグローバル変数のみである
  • オブジェクト内部で使用している変数やメソッドを他のプログラムから容易に変更できないようになる(カプセル化)
  • ex. 関数createStopwatchのスコープ内で定義された変数timeと関数の結果が一体となったオブジェクトを変数stopwatchへ代入しているため、変数stopwatchが呼び出される都度、変数timeは0に初期化されることなく、下記のような結果が返ってくる
var createStopwatch = function() {
    var time = 0;
    return function() {
        time += 1;
        console.log(time);
    };
};
var stopwatch = createStopwatch();
stopwatch();    // 1が返ってくる
stopwatch();    // 2が返ってくる
stopwatch();    // 3が返ってくる

4. Node.jsの基本知識

Hello, Nodeを出力

hello.js
console.log('Hello, Node')
  • プログラムを実行
$ node hello.js
  • 実行結果
Hello, Node
  • 'use strict';を宣言するとstrict(厳格)モードで実行できる
    • strictモード : javascriptのコードをより厳しくエラーチェックすることができる仕組み
hello.js
'use strict';    
console.log('Hello, Node')
  • Webサーバとして動作させる場合
hello2.js
var http = require("http");    // HTTPモジュールの読み込み

http.createServer(function(request, response) {    //  HTTPサーバを作成
   response.writeHead(200, {'Content-Type': 'text/plain'});    // レスポンスHTTPヘッダーを設定
   response.end('Hello, Node\n');    // レスポンスボディを送信
}).listen(8000);    // ポート8000でリクエストを行う

//  サーバにアクセスするためのURLを出力 
console.log('Server running at http://127.0.0.1:8000/');
  • プログラムを実行
$ node hello.js    // ブラウザで"http://localhost:8000"にアクセス Hello, Nodeと表示される

非同期処理

  • 処理を実行したら結果を待たずに他の処理を実行できる(複数の処理を平行して実行できる)
    • 同期処理は、上から下へ1行ずつ順番にプログラムが実行されていく(サーバーへアクセスをして値を取得する間プログラムはストップしている)、
  • JavaScriptでは基本的に非同期APIが使用される
// 操作が完了する前に次の処理を実行する 以下は "Second, First"と出力される
setTime(function() {
   console.log('First');
   }, 3000);    // 処理に3秒間かかる
console.log('Second');
  • Node.js ではPromiseという仕組みを使って非同期を実現
    • Promiseとは非同期処理を実現するために用意されたオブジェクト
var promise = new Promise(function(resolve, reject) {    // Promiseオブジェクトを変数に代入 引数にはresolve, reject
  setTimeout(function() {
    resolve('hoge');    // 引数に返したい結果となる値を指定
  }, 3000);
});

promise.then(function(value) {    // then()の中の関数の引数valueにPromiseの結果が格納されいる
  console.log(value);    // 3秒待ってhogeが返されることが約束されている
});

console.log(promise);    // [object Promise]が返される

実行結果

[object Promise]
"hoge"

5. ライブラリの把握

  • axios
    • HTTP通信を簡単に行うことができるJavascriptのライブラリ
  • request
    • 標準のhttpライブラリを使うより簡単で理解しやすい記述でHTTP通信を行うことができるライブラリ
  • Moment.js
    • JavaScriptで日付を扱うためのライブラリ
    • 日時の加算減算や2つの日付の差の計算などができる
  • facebook/jest
    • JavaScriptのテストフレームワーク

6. 今後の課題

  • npm(Node.jsのパッケージを管理するもの)を学習する際に、Node.jsについての理解を深める

参照

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

jsで作ったblobをphpに送信

blob => {
   const xhr = new XMLHttpRequest();
   const formData = new FormData();
   formData.append('audio', blob, 'input.wav');

   xhr.open('POST', '/upload.php', true);
   xhr.send(formData);
}

これだけ

xhr.setRequestHeader('Content-Type', 'multipart/form-data; charset=utf-8; boundary=' + Math.random().toString().substr(2));

これをつけるとbinaryにならず送信できない

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

ESLint v6.7.0

前 v6.6.0 | 次 (2019/12/21 JST)

ESLint 6.7.0 がリリースされました。
小さな機能追加とバグ修正が行われています。

質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。

? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット
? 本家リポジトリ
? 本家サポート チャット

? 本体への機能追加

設定ファイルのignorePatternsプロパティ

? RFC022, #12274

共有設定を含む設定ファイルで、ignorePatternsプロパティが利用できるようになりました。このプロパティは.eslintignoreと同様に ESLint が無視するファイルをコントロールできます。

例(.eslintrc.yml)
ignorePatterns:
- "!.eslintrc.js"
- "/node_modules/"
- "**/node_modules/"

extends:
- eslint:recommended

rules:
  quotes: error
  #...

Suggestions API

? RFC030, #12384

あるエラーが複数の修正候補を持つ場合、ESLint は自動修正を行えず、修正候補を提示する手段もありませんでした。新しい Suggestions API を使うと、エディタの ESLint プラグイン等のインタラクティブな UI を通して修正候補を提示できるようになります。

今のところ、ユーザーはまだこの機能を利用できません。各検証ルールとエディタ プラグインのサポートをお待ちください。

【非推奨】個人設定ファイル

? RFC032, #12426

ESLint が実行されたディレクトリに設定ファイル (.eslintrc.*) が存在しなかった場合、ESLint は OS のホーム ディレクトリにある設定ファイルを読み込みます。この機能を個人設定ファイルと呼びます。

この機能が非推奨になりました。

  • ESLint 7 以降、この機能を利用すると実行時警告が出力されるようになります。
  • ESLint 8 でこの機能は削除されます。

今後もホームディレクトリの設定ファイルを利用したい場合は、CLI オプションで明示的に指定してください。

例
eslint --config ~/.eslintrc.json lib

【非推奨】sourceCode#isSpaceBetweenTokens()

? #12519

sourceCode#isSpaceBetweenTokens()メソッドが名前変更されました。新しい名前はsourceCode#isSpaceBetween()になります。

トークンだけでなく AST ノードを渡しても動作するので、単純に名前が間違っていたという理由です。元の名前のメソッドは非推奨メソッドとして残され、将来のメジャーリリースで削除されます (具体的な削除プランは示されていません)。

? 新しいルール

grouped-accessor-pairs

? #12331

Getter と Setter のペアを離れた場所に定義すると警告するルールです。

例
/* eslint grouped-accessor-pairs: error */

//✘ BAD
const obj1 = {
   get value() { },
   foo() { },
   set value(v) { },
}

//✔ GOOD
const obj2 = {
   get value() { },
   set value(v) { },
   foo() { },
}

Online Playground

no-setter-return

? #12346

Setter に値を返すreturn文を書くと警告するルールです。getter-return ルールの兄弟です。

例
/* eslint no-setter-return: error */

//✘ BAD
const obj1 = {
    set value(v) {
        return this._value
    },
}

//✔ GOOD
const obj2 = {
    set value(v) {
        if (v == null) return
        this._value = v
    },
}

Online Playground

prefer-exponentiation-operator

? #12360

Math.pow()関数の代わりに**演算子を使うように指示するルールです。

例
/* eslint prefer-exponentiation-operator: error */

//✘ BAD
const b1 = Math.pow(2, 8);
const b2 = Math.pow(a, b);
const b3 = Math.pow(a + b, c + d);
const b4 = Math.pow(-1, n);

//✔ GOOD
const a1 = 2 ** 8;
const a2 = a ** b;
const a3 = (a + b) ** (c + d);
const a4 = (-1) ** n;

Online Playground

no-dupe-else-if

? #12504

else-if文の連鎖の中で、重複する条件式のために常にfalseになるif文を警告するルールです。

例
/* eslint no-dupe-else-if: error */

//✘ BAD
if (a || b) {
    //...
} else if (a) {
    //...
}

//✔ GOOD
if (a || b) {
    //...
} else if (c) {
    //...
}

Online Playground

no-constructor-return

? #12529

コンストラクタの中でreturn文で値を返すことを禁止するルールです。

例
/* eslint no-constructor-return: error */

//✘ BAD
class B {
    constructor() {
        return {}
    }
}

//✔ GOOD
class A {
    constructor(arg) {
        if (arg == null) return
        this.value = arg
    }
}

Online Playground

? オプションが追加されたルール

no-underscore-dangle allowAfterThisConstructor

? #11489

this.constructor に続くアンダーバーを許可するオプションが追加されました。

例
/* eslint no-underscore-dangle: [error, { allowAfterThisConstructor: true }] */

//✘ BAD
class B {
    foo(obj) {
        obj.constructor._privateStuff
    }
}

//✔ GOOD
class A {
    foo(obj) {
        this.constructor._privateStuff
    }
}

Online Playground

no-implicit-globals lexicalBindings

? #11996

レキシカル スコープの宣言 class, const, let も警告するルールが追加されました。

例
/* eslint no-implicit-globals: [error, { lexicalBindings: true }] */

//✘ BAD
class B {}
const b1 = 0
let b2 = 0

//✔ GOOD
{
    class A {}
    const a1 = 0
    let a2 = 0
}

Online Playground

no-useless-computed-key enforceForClassMembers

? #12110

クラス構文の不要な Computed Keys も警告するオプションが追加されました。

例
/* eslint no-useless-computed-key: [error, { enforceForClassMembers: true }] */

//✘ BAD
class B {
    ["foo"]() { }
}

//✔ GOOD
class A {
    [foo]() { }
}

Online Playground

no-invalid-this capIsConstructor

? #12308

ES5 スタイルのコンストラクタ (名前が大文字で始まる関数) をコンストラクタとして扱わないオプションが追加されました。React では関数コンポーネントに大文字で始まる関数名を使う習慣があるためです。

例
/* eslint no-invalid-this: [error, { capIsConstructor: false }] */

//✘ BAD
function Foo() {
    this.value = 0
}

Online Playground

✒️ eslint --fix をサポートしたルール

特になし。

⚠️ 非推奨になったルール

特になし。

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

Cypressテストコードまとめ

Cypressリンク

公式Doc
https://docs.cypress.io/guides/overview/why-cypress.html

chromeプラグイン
Cypress Scenario Recorder

Cypress Commands

Visit a page

integration/sample_spec.js
describe('My First Test', function() {
  it('Visits the Kitchen Sink', function() {
    cy.visit('https://example.cypress.io')
  })
})

エレメント取得

integration/sample_spec.js
cy.contains('type')
cy.get('.action-email')
cy.get('form').within(() => {
  cy.get('input').type('Pamela') // Only yield inputs within form
  cy.get('textarea').type('is a developer') // Only yield textareas within form
})

cy.get('nav').children()            // Yield children of nav
cy.get('td').closest('.filled')     // Yield closest el with class '.filled'
cy.contains('ul').end()             // Yield 'null' instead of 'ul' element
cy.get('td').filter('.users')       // Yield all el's with class '.users'
cy.get('.article').find('footer')   // Yield 'footer' within '.article'
cy.get('nav a').first()             // Yield first link in nav
cy.focused()                        // Yields the element currently in focus
cy.get('nav a').last()              // Yield last link in nav
cy.get('nav a:first').next()        // Yield next link in nav
cy.get('.active').nextAll()         // Yield all links next to `.active`
cy.get('div').nextUntil('.warning') // Yield siblings after 'div' until '.warning'
cy.get('input').not('.required')    // Yield all inputs without class '.required'
cy.get('header').parent()           // Yield parent el of `header`
cy.get('aside').parents()           // Yield parents of aside
cy.get('p').parentsUntil('.article') // Yield parents of 'p' until '.article'
cy.get('tr.highlight').prev()       // Yield previous 'tr'
cy.get('.active').prevAll()         // Yield all links previous to `.active`
cy.get('p').prevUntil('.intro')     // Yield siblings before 'p' until '.intro'
cy.get('td').siblings()             // Yield all td's siblings

cy.root()   // Yield root element <html>
cy.get('nav').within(($nav) => {
  cy.root()  // Yield root element <nav>
})

入力

integration/sample_spec.js
cy.get('input').type('Hello, World')     // Type 'Hello, World' into the 'input'

cy.get('select').select('user-1')        // Select the 'user-1' option

cy.get('[type="checkbox"]').check()      // Check checkbox element
cy.get('[type="radio"]').first().check() // Check first radio element

cy.get('[type="checkbox"]').uncheck()    // Unchecks checkbox element

cy.get('[type="text"]').clear()          // Clear text input
cy.get('textarea').type('Hi!').clear()   // Clear textarea
cy.focused().clear()                     // Clear focused input/textarea

エレメント操作

integration/sample_spec.js
cy.get('input').first().focus()   // Focus on the first input

cy.get('[type="email"]').type('me@email.com').blur() // Blur email input
cy.get('[tabindex="1"]').focus().blur()              // Blur el with tabindex

cy.get('button').click()             // Click on button
cy.focused().click()                 // Click on el with focus
cy.contains('Welcome').click()       // Click on first el containing 'Welcome'

cy.get('button').dblclick()          // Double click on button
cy.focused().dblclick()              // Double click on el with focus
cy.contains('Welcome').dblclick()    // Double click on first el containing 'Welcome'

cy.get('.menu').rightclick()         // Right click on .menu
cy.focused().rightclick()            // Right click on el with focus
cy.contains('Today').rightclick()    // Right click on first el containing 'Today'

cy.get('.menu-item').trigger('mouseover')

cy.get('footer').scrollIntoView()       // Scrolls 'footer' into view

cy.scrollTo(0, 500)                     // Scroll the window 500px down
cy.get('.sidebar').scrollTo('bottom')   // Scroll 'sidebar' to its bottom

cy.get('form').submit()   // Submit a form

cy.get('ul>li').each(function () {...}) // Iterate through each 'li'
cy.getCookies().each(function () {...}) // Iterate through each cookie

Assert

integration/sample_spec.js
cy.get(':checkbox').should('be.disabled')
cy.get('form').should('have.class', 'form-horizontal')
cy.get('input').should('not.have.value', 'US')

// and
cy.get('.err').should('be.empty').and('be.hidden') // Assert '.err' is empty & hidden
cy.contains('Login').and('be.visible')             // Assert el is visible
cy.wrap({ foo: 'bar' })
  .should('have.property', 'foo')                  // Assert 'foo' property exists
  .and('eq', 'bar')                                // Assert 'foo' property is 'bar'

// eq
cy.get('tbody>tr').eq(0)    // Yield first 'tr' in 'tbody'
cy.get('ul>li').eq(4)       // Yield fifth 'li' in 'ul'

データ関連

integration/sample_spec.js
cy.setCookie('auth_key', '123key') // Set the 'auth_key' cookie to '123key'

cy.getCookie('auth_key')     // Get cookie with name 'auth_key'
cy.getCookies()              // Get all cookies

cy.clearCookie('authId')     // clear the 'authId' cookie
cy.clearCookies()            // clear all cookies
cy.clearLocalStorage()       // clear all local storage

cy.readFile('menu.json')
cy.writeFile('menu.json')

ブラウザ表示、URL、移動関連

integration/sample_spec.js
// Navigate back or forward to the previous or next URL in the browser’s history.
cy.go('back')

cy.hash()     // Get the url hash

cy.location()       // Get location object
cy.location('host') // Get the host of the location object
cy.location('port') // Get the port of the location object

cy.reload()

cy.url()    // Yields the current URL as a string

cy.viewport(550, 750)    // Set viewport to 550px x 750px
cy.viewport('iphone-6')  // Set viewport to 375px x 667px

その他

integration/sample_spec.js
cy.clock()

cy.exec('npm run build')

cy.fixture('users').as('usersJson')  // load data from users.json
cy.fixture('logo.png').then((logo) => {
  // load data from logo.png
})

// assume App.start calls util.addListeners
cy.spy(util, 'addListeners')
App.start()
expect(util.addListeners).to.be.called

デバッグ

integration/sample_spec.js
cy.debug().getCookie('app') // Pause to debug at beginning of commands
cy.get('nav').debug()       // Debug the `get` command's yield

// Print a message to the Cypress Command Log.
cy.log('created new user')

cy.pause().getCookie('app') // Pause at the beginning of commands
cy.get('nav').pause()       // Pause after the 'get' commands yield
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Drawer Menuの簡単な書き方

https://gimmicklog.com/jquery/203/

drawerメニューの実装

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

ボタンでリップボードからテキストボックスにコピー内容を貼付ける事ができなかった記録

注意 : このページでは「ボタンでリップボードからテキストボックスにコピー内容を貼付ける」方法はわかりません。

  • 動作確認したブラウザ
    • Windows10 Pro 64bit
      • GoogleC hrome 78.0
      • Fire Fox 70.0
      • Microsoft Edge 44.18362
      • Internet Explorer 11.418
    • MacOS Mojave 10.14.6
      • Google Chrome 78.0
      • Fire Fox 70.0
      • Safari 13.0

やりたいこと

ボタンをクリックすると任意のテキストボックスにクリップボードにコピーしてあるテキストを張付けたい。
ボタンでテキストをコピーする方法はこちら

IEの場合だけ張付けができる

(参考)クリップボードを使ったコピーとペースト - Qiita

/** [貼付ける]ボタン押下処理. */
function onClickPaste() {
    let tagetId = 'pasteArea';
    let content = window.clipboardData.getData("Text");
    $(tagetId).val(content);
}

IE以外の場合は貼付けができない

Document.execCommandを試してみたけど張付けできない

MacのFireFox
a.gif

/** [貼付ける]ボタン押下処理. */
function onClickPaste() {
    let tagetId = 'pasteArea';
    $(tagetId).focus();
    let result = document.execCommand('paste');
    alert('張付けの結果:' + result);
}

resultが「false」なので使えないのかもしれない。

返値
Boolean で、コマンドが対応していないか無効であれば false になります。
Document.execCommand() - Web API | MDN

Async Clipboard APIを試してみたけど張付けできない

Unblocking Clipboard Access  |  Web  |  Google Developers

MacのChrome
a.gif

/** [貼付ける]ボタン押下処理. */
function onClickPaste() {
    let tagetId = 'pasteArea';
    navigator.clipboard.readText().then((text) => {
        $(tagetId).val(text);
    });
}

ブラウザで張付け許可を設定してみたが張付けできない

クリップボードからの読み込み
"貼り付け"を使用するには"clipboardRead" permission が必要です。
クリップボードとのやりとり - Mozilla | MDN

具体的にどうすればいいかは、はっきりわからないのでChromeでそれっぽい設定をしてみましたが張付けできませんでした。

  • Google Chrome
    1. [設定] > [詳細設定] > [プライバシーとセキュリティ] > [サイトの設定] > [クリップボード]
    2. 確認ダイアログを表示する場合: [クリップボード...(省略)...サイトに許可しない]をONにすると[クリップボードにコピーされているテキストや画像にサイトがアクセスする際に確認する(推奨)]に変わる
    3. [許可]の[追加]ボタンでサイトを登録する
    4. スクリーンショット 2019-11-24 13.21.05.png

MacのChrome
a.gif

使ったHTML

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="content-type" charset="utf-8">
    <title>貼付けをJavaScriptでやってみる</title>
    <script src="https://code.jquery.com/jquery-1.11.0.min.js" integrity="sha256-spTpc4lvj4dOkKjrGokIrHkJgNA0xMS98Pw9N7ir9oI=" crossorigin="anonymous"></script>
    <script src="sample.js"></script>
</head>
<body>
    <table>
        <tr>
            <td>貼付け場所</td>
            <td><input id="pasteArea" type="text"></td>
        </tr>
        <tr>
            <td colspan="2"><input type="button" id="paste" value="貼付ける" onclick="onClickPaste();"></td>
        </tr>
    </table>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ボタンでクリップボードからテキストボックスにコピー内容を貼付ける事ができなかった記録

注意 : このページでは「ボタンでクリップボードからテキストボックスにコピー内容を貼付ける」方法はわかりません。

  • 動作確認したブラウザ
    • Windows10 Pro 64bit
      • GoogleC hrome 78.0
      • Fire Fox 70.0
      • Microsoft Edge 44.18362
      • Internet Explorer 11.418
    • MacOS Mojave 10.14.6
      • Google Chrome 78.0
      • Fire Fox 70.0
      • Safari 13.0

やりたいこと

ボタンをクリックすると任意のテキストボックスにクリップボードにコピーしてあるテキストを張付けたい。
ボタンでテキストをコピーする方法はこちら

IEの場合だけ張付けができる

(参考)クリップボードを使ったコピーとペースト - Qiita

/** [貼付ける]ボタン押下処理. */
function onClickPaste() {
    let tagetId = 'pasteArea';
    let content = window.clipboardData.getData("Text");
    $(tagetId).val(content);
}

IE以外の場合は貼付けができない

Document.execCommandを試してみたけど張付けできない

MacのFireFox
a.gif

/** [貼付ける]ボタン押下処理. */
function onClickPaste() {
    let tagetId = 'pasteArea';
    $(tagetId).focus();
    let result = document.execCommand('paste');
    alert('張付けの結果:' + result);
}

resultが「false」なので使えないのかもしれない。

返値
Boolean で、コマンドが対応していないか無効であれば false になります。
Document.execCommand() - Web API | MDN

Async Clipboard APIを試してみたけど張付けできない

Unblocking Clipboard Access  |  Web  |  Google Developers

MacのChrome
a.gif

/** [貼付ける]ボタン押下処理. */
function onClickPaste() {
    let tagetId = 'pasteArea';
    navigator.clipboard.readText().then((text) => {
        $(tagetId).val(text);
    });
}

ブラウザで張付け許可を設定してみたが張付けできない

クリップボードからの読み込み
"貼り付け"を使用するには"clipboardRead" permission が必要です。
クリップボードとのやりとり - Mozilla | MDN

具体的にどうすればいいかは、はっきりわからないのでChromeでそれっぽい設定をしてみましたが張付けできませんでした。

  • Google Chrome
    1. [設定] > [詳細設定] > [プライバシーとセキュリティ] > [サイトの設定] > [クリップボード]
    2. 確認ダイアログを表示する場合: [クリップボード...(省略)...サイトに許可しない]をONにすると[クリップボードにコピーされているテキストや画像にサイトがアクセスする際に確認する(推奨)]に変わる
    3. [許可]の[追加]ボタンでサイトを登録する
    4. スクリーンショット 2019-11-24 13.21.05.png

MacのChrome
a.gif

使ったHTML

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="content-type" charset="utf-8">
    <title>貼付けをJavaScriptでやってみる</title>
    <script src="https://code.jquery.com/jquery-1.11.0.min.js" integrity="sha256-spTpc4lvj4dOkKjrGokIrHkJgNA0xMS98Pw9N7ir9oI=" crossorigin="anonymous"></script>
    <script src="sample.js"></script>
</head>
<body>
    <table>
        <tr>
            <td>貼付け場所</td>
            <td><input id="pasteArea" type="text"></td>
        </tr>
        <tr>
            <td colspan="2"><input type="button" id="paste" value="貼付ける" onclick="onClickPaste();"></td>
        </tr>
    </table>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

slackの視認性向上のために「アイコン画像設定」を一括で促す

Slackにおけるアイコンの意義

スクリーンショット 2019-11-24 7.46.37.png

入っているチャンネルの数が増えると、
すべての投稿に目を通すだけでも相当な労力になります。
アイコン画像は、発言の重要性・緊急性を認識する「視認性」を構成する重要な要素となります。

デフォルトアイコンの弊害

  • 発言に対して「重み付け」ができずに投稿を精査する労力が発生する
  • 似たようなアイコンなので、発言者を取り違える
  • 仕様でデフォルトアイコン自体が大きく変更されることがある

アイコン設定への心理的障害

そもそも、なぜアイコン設定しない人がいるのでしょうか?
視認性を下げてやる!という考えでアイコンを未設定にする人はいないと思います。
大半は下記の理由だと思います。

  • ビジネスツールでどんな画像をアイコンとして使っていいか分からない
  • かといって自撮りをupするのは恥ずかしい

特にチームにjoinしたばかりだと、
「アイコン画像なんか設定してふざけてる」と思われるのでは?と考える人もいるでしょう。

アイコン設定を促してあげる

「slackアイコン設定お願いします」と一言メンションを飛ばしてあげると万事解決です。
しかし、メンバーが増えるたびにメンションを送っているのではキリがありません。
「アイコン画像未設定ユーザー一覧」を取得する方法は無いのでしょうか?

slackAPI の Users.list を使う

Users.list でユーザーに関する情報を取得できます。
アイコンを設定しているユーザーは profile 内に is_custom_image というプロパティを持つようになります。

つまり
1. users.list で全ユーザーの情報取得
2. botやゲストユーザーを除外
3. is_custom_image が無いユーザーのIDを抽出
4. 抽出したユーザー達にメンションをつけてメッセージを投稿する。

スクリーンショット 2019-11-24 14.11.12.png

という流れで解決することが出来ます。


※これより下は具体的なコードの話になります。
※使用するコードの全文はgithubに公開しております。
※実行環境としてGoogleAppsScriptを使用します。
GASの詳細な説明は省きますので、ライブラリやスクリプトの設定手順などは別の記事を参照してください。

実装方法

1. 一覧取得

    var token = PropertiesService.getScriptProperties().getProperty('OAuth_token');
    var slackapp = SlackApp.create(token);
    var membersList = slackapp.usersList().members;

先程も紹介した Users.list を使うだけです。

2. botやゲストの除外

    membersList.forEach(function(m) {
        // is_restricted 以外にすることで multi-channel guest を除外
        // is_ultra_restricted 以外にすることで single channel guest を除外
        // 詳細は https://api.slack.com/types/user を参照
        if (!m.deleted && !m.is_bot && m.id !== "USLACKBOT" && !m.profile.is_custom_image 
            && !m.is_restricted && !m.is_ultra_restricted) {
                defaultIconUsers.push(m.id)
            };
        })

コメントでも記載していますが、 is_restricted 及び is_ultra_restricted を参照することでゲストユーザーを除外することが出来ます。
このあたりの判定条件は実情に応じて適宜変更してください。

3. 投稿メッセージの作成

    var message = ""
    if (defaultIconUsers.length > 0) {
        message = "視認性向上のためアイコン画像の設定をお願いします\n"
        defaultIconUsers.forEach(function(d) {
            message += "<@" + d + ">"
    })} else {
        message = "アイコン画像を設定していないユーザーは居ません"
    }
    slackapp.postMessage(channelId, message);

判定結果に応じて条件分岐した後に、メッセージを作成して投稿という流れです。

  • デフォルトアイコンユーザーあり
    スクリーンショット 2019-11-24 14.11.12.png

  • なし
    スクリーンショット 2019-11-24 14.12.12.png

実装手順は以上となります。


おまけ

slack上で外部のお客様(関連会社)とのやりとりが多い会社では、
アイコン画像の設定が非常に重要で、設定するまで注意喚起され続けるというところもあるようです。

数ヶ月に1回このスクリプトを定期実行するだけでも、slackの視認性が向上してくれるのではと考えております。

ご精読ありがとうございました。

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

JavaScriptでストップウォッチを作ってみた

手順

タイマーを停止した時間から再開させる機能の実装。
clickイベントの設定
Date.now()

ボタンを押しても1回だけでは止まらないという不具合を直す
ボタンの状態をbutton要素のdisabled属性で無効化

CSSでストップウォッチ全体の見た目を整える。
bodyのスタイリング
.containerのスタイリング

タイマーのスタイルを整える。
スタイルの設定をするために、buttonタグをdivタグに置き換える。
timerのスタイリング
button要素とdiv要素の違い

divタグを使ったボタンの有効・無効の切り替えを実装。
classList.add()
classList.remove()
classList.contains()

メモ

・textContent プロパティは、 ノードおよびその子孫のテキスト情報を取得・設定するために使います

・clearTimeout() メソッドは、以前の setTimeout() の呼び出して確立されたタイムアウトを解除します。

・padStart()メソッドは、結果の文字列が所定の長さになるように、現在の文字列を別の文字列(必要に応じて繰り返しpadStart()ます。 パディングは、現在の文字列の最初(左)から適用されます。

・elapsedTimeの意味は「経過時間」

・setTimeout() メソッド は、指定された遅延の後に関数またはコードの断片を実行するタイマーを設定する。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ストップウォッチ</title>
    <link rel="stylesheet" href="css/style2.css">
</head>
<body>  
  <div class="container">
   <div id="timer">00:00.000</div>
   <div class="controls">

        <div class="btn" id="start">Start</div>
        <div class="btn" id="stop">Stop</div>
        <div class="btn" id="reset">Reset</div>
    </div>  
  </div>
   <script src="js/main2.js"></script>
</body>
</html>

css/styles.css
body {
  font-family: 'Courier New', monospace;
  font-size: 14px;
  background: #eee
}

.container {
  margin: 20px auto;
  width: 270px;
  background: #fff;
  padding: 15px;
  text-align: center;
}

#timer {
  background: #ddd;
  height: 120px;
  line-height: 120px;
  font-size: 40px;
  margin-bottom: 15px;
}

.btn {
  width: 80px;
  height: 45px;
  line-height: 45px;
  background: #ddd;
  font-weight: bold ;
  cursor: pointer;
  user-select: none;
}

.controls {
  display: flex;
  justify-content: space-between;
}

.inactive {
  opacity: 0.6;
}
js/main.js
'use strict';

{
  const timer = document.getElementById('timer');
  const start = document.getElementById('start');
  const stop = document.getElementById('stop');
  const reset = document.getElementById('reset');

  let startTime;
  let timeoutId;
  let elapsedTime = 0;

  function countUp() {
    const d = new Date(Date.now() - startTime + elapsedTime);
    const m = String(d.getMinutes()).padStart(2, '0');
    const s = String(d.getSeconds()).padStart(2, '0');
    const ms = String(d.getMilliseconds()).padStart(3, '0');
    timer.textContent = `${m}:${s}.${ms}`;

    timeoutId = setTimeout(() => {
      countUp();
    }, 10);
  }

  function setButtonStateInitial() {
    start.classList.remove('inactive');
    stop.classList.add('inactive');
    reset.classList.add('inactive');
  }

  function setButtonStateRunning() {    start.classList.add('inactive');
    stop.classList.remove('inactive');
    reset.classList.add('inactive');
  }

  function setButtonStateStopped() {
    start.classList.remove('inactive');
    stop.classList.add('inactive');
    reset.classList.remove('inactive');
  }

  setButtonStateInitial();


  start.addEventListener('click', () => {
    if (start.classList.contains('inactive') === true) {
      return;
    }
    setButtonStateRunning();
    startTime = Date.now();
    countUp();
  });

  stop.addEventListener('click', () => {
    if(stop.classList.contains('inactive') === true) {
      return;
    }
    setButtonStateStopped();
    clearTimeout(timeoutId);
    elapsedTime += Date.now() - startTime;
  });

  reset.addEventListener('click', () => {
    if(reset.classList.contains('inactive') === true){
      return;
  }
    setButtonStateInitial();
    timer.textContent = '00:00.000';
    elapsedTime = 0;
  });
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

誰でも勝てる三目並べAIで遊ぼう

はじめに

先に,Canvas を用いた三目並べの作成について報告した1。このとき,cpu が選択する一手は,乱数を用いる方法であった。これは簡単な方法であるだけに,試行してみると,非常に弱いことが分かる。三目並べ AI を強くする方法として,ミニマックス法が知られている2 3 4。参考記事2 を参照しながら,Python をJavaScript に書き直すことによって,Canvas を用いた三目並べのプログラムに組み込んだ5。

ゲームの面白さは,勝ったときに,より強く感じられる。そこで,初心者でも勝てるような三目並べ AI の開発を考えた。その結果,誰でも勝てる三目並べAIが完成した6。やってみると判るように,誰でも勝てるだけでなく,負けようとしても勝ってしまう。引き分けに持ち込もうとしても,勝ってしまう。なぜ,そうなるのか,考えてみるのも面白いだろう。プログラム(最弱ミニマックス法)も置いてあるので,見ていただきたい7。

ミニマックス法の実装

JavaScript に書き直したミニマックス法の実装部分を,下記に示す。

function minimax(depth) {
    // ミニマックス法で探索して,着手を返す
    if (state != GAME) { return evaluate(depth);}

    var best_value = 0;
    var value = 0;
    if (my_turn) { value = 10000;}
    else         { value = -10000;}

    for (var i = 0; i < 9; i++) {
        if (board[i] == 0) {
            put_value(i);
            var child_value = minimax(depth+1);
            if (my_turn) {
                if (child_value > value) {
                    value = child_value;
                    best_value = i;
                }
            }
            else {
                if (child_value < value) {
                    value = child_value;
                    best_value = i;
                }
            }
            undo_value(i);
        }
    }
    if (depth == 0) { return best_value;}
    else            { return value;}
}

最弱ミニマックス法の実装

誰でも勝てる三目並べAI(最弱ミニマックス法)の実装部分を,下記に示す。

function minimaxmin(depth) {
    // ミニマックスミニ法で探索して,着手を返す
    if (state != GAME) { return evaluate(depth);}

    var best_value = 0;
    var value = 0;
    if (my_turn) { value = -10000;}
    else         { value = 10000;}

    for (var i = 0; i < 9; i++) {
        if (board[i] == 0) {
            put_value(i);
            var child_value = minimaxmin(depth+1);
            if (my_turn) {
                if (child_value < value) {
                    value = child_value;
                    best_value = i;
                }
            }
            else {
                if (child_value > value) {
                    value = child_value;
                    best_value = i;
                }
            }
            undo_value(i);
        }
    }
    if (depth == 0) { return best_value;}
    else            { return value;}
}

おわりに

Canvas を用いた三目並べのプログラムに,ミニマックス法を組み込んだ。また,この最強ミニマックス法のアルゴリズムを逆にすることによって,誰でも勝てる三目並べAI(最弱ミニマックス法)のプログラムを作成した。ゲームの初心者に,興味を持ってもらうのに役立つと思われる。

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

A-FRAME: 物理演算でボーリングっぽい動きを実現してみる4(ボールの平行移動への抵抗)

A-Frameをつかって物理演算ができるようにしてみます。
今回はボールの平行移動への抵抗を検証します。

例1)linearDamping=0.01
デフォルト値です。
linear1.gifdemo
実際のボーリングだと、もっと滑らかに転がっていく気がします。
抵抗を変化させることで、実現できるのではないかと思います。

例2)linearDamping=1
抵抗を高めてみます。
linear2.gifdemo
ボールが飛んでいかず、静かに落下していきました。
レーンもすり抜けています。
よくわからないので、考えるのはやめて次にいきましょう。

例3)linearDamping=0.99
1はちょっと良くない感じだったので、0.01だけ下げてみましょう。
それでもデフォルトと比べたら大きな抵抗がある状態のはずです。
linear3.gifdemo
いちおうボールは前に飛びましたが、レーンを少し転がって停止しました。
いかにも、抵抗を受けているような結果で、わかりやすいです。

例4)linearDamping=0.0001
デフォルト値の100分の1です。
linear4.gifdemo
気持ち、デフォルトよりはよく転がったように見えます。

例5)linearDamping=0
抵抗を無くしてみました。
linear5.gifdemo
あまり、0.0001のときとの差は感じません。
0.0001で十分小さい値なのでしょう。

まとめ

ボーリングのボールの転がりには0.0001を採用してみます。
レーン側の摩擦係数を変更する事で、より調整ができるかもしれません。

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

Javascript:オブジェクトをマージする関数mergeを考えてみた

連想配列(辞書オブジェクト)n個の共通配列を上書きなしで結合マージするを読んであれこれやってみたのが勉強になったので、あちらのコメントにも書いたが、あんまり書くと先方にも迷惑なのでこちらに書きます。

こうなったらいいな

//こんなオブジェクトがあったとして:
const hira = {
  'dog': 'いぬ',
  'cat': ['ねこ'],
  'human': 'ひと',
}
const kana = {
  'dog': ['イヌ'],
  'cat': ['ネコ', 'ニャンコ'],
}
const kan = {
  'dog': [],
  'cat': '猫',
  'human': '人',
  'car' : '車',
}

//こうなったらいいな...
merge(hira,kana,kan,{})
/*
{
  dog: [ 'いぬ', 'イヌ' ],
  cat: [ 'ねこ', 'ネコ', 'ニャンコ', '猫' ],
  human: [ 'ひと', '人' ],
  car: [ '車' ]
}
*/
  • 同じキーがあったら値を配列にまとめます。
  • 値は配列になってたり、なってなかったりします。

ってことです。(元記事とはちょっと変えてあります。)

こんな感じにしてみました

const merge = ( ...objs ) => objs.reduce( updateMerge, {} )

const updateMerge = (acc, obj) =>  reduceObj( updateObj )( acc )( obj )
const reduceObj = update => init => obj => Object.entries( obj ).reduce( update, init )
const updateObj = (acc, [k, v]) => 
  acc.hasOwnProperty(k) ? { ...acc, [k]:[ acc[k], v ].flat() }
  : { ...acc, [k]: [v].flat() }

こう考えた

おおざっぱに言って、merge はどんな関数でしょう?

  • オブジェクトを複数、引数にし、オブジェクトを返す
  • 引数のオブジェクトを いい感じで空のオブジェクトに足していけばいいのでは?

reduceのことを知っていれば、まさにreduce案件だと気がつきます。
とりあえず、こんな風に書いてみます。

const merge = ( ...objs ) => objs.reduce( updateMerge, {} )

updateMerge というのが出てきましたが、とりあえず置いただけです。中味はまだ決まってませんが「いい感じで足していく」ものです。
さて、updateMergeってどんな関数でしょう?

  • アキュムレータと配列の要素(ここではオブジェクト)を引数にとって、新しいアキュムレータを返す
  • そのオブジェクトの各プロパティを、いい感じでそのアキュムレータに足していけばいいのでは?

これも reduce 案件のようです。
しかしオブジェクトに直接 reduce は使えないので、とりあえずこうしておきます。

const updateMerge = (acc, obj) =>  reduceObj( updateObj )( acc )( obj )

新しい関数がまたふたつ出てきました。これもとりあえず置いただけです。

  • reduceObj はオブジェクトで reduce っぽいことをする関数です。
  • updateObj も「いい感じで足していく」ものです。

まず、reduceObj を考えます。
これは簡単です。Object.entries()を使ってオブジェクトを [(キー), (値)] の配列にして、reduce すればいい。

const reduceObj = update => init => obj => Object.entries( obj ).reduce( update, init )

つぎに、updateObj を考えます。

  • アキュムレータと配列の要素(ここでは [(キー), (値)] )を引数にとって、新しいアキュムレータを返す
  • アキュムレータに既にそのプロパティがあれば、値を値の配列の最後部に加える
  • まだそのプロパティがなければ、新しく作って追加する
  • 値は配列にはいってたりはいってなかったりするので、いい感じにする

こうしてみました。

const updateObj = (acc, [k, v]) => 
  acc.hasOwnProperty(k) ? { ...acc, [k]:[ acc[k], v ].flat() }
  : { ...acc, [k]: [v].flat() }

flat()を使うと配列の中のかっこを一個へらしていい感じでフラットにしてくれます。

これで、「とりあえず置いた」ものはなくなりました。
全部並べて、動くかな? -> 動いた! わーい!

まとめ

  • トップダウンで考えると見通しがいい場合がある
  • 関数内から別の関数を呼ぶときは、どっちが上でもよいらしい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ISBN

すでに似たようなものがたくさんあると思いますすが、
Wikipedeiaの記事を参考にして、JavaScriptでISBNのチェックディジットを計算するページを独自に作ってみましました。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width">
<title>ISBNのチェックディジットを計算</title>
</head>
<body>
  <div>
    <h1>ISBNのチェックディジットを計算</h1>
    <div id="chk_isbn">
      <p>
        <input type="text" class="input" value="978-4-87311-573-3">
      </p>
      <button class="chk">計算</button>
      <span class="output"></span>
    </div>
  </div>
<script>

window.onload=function(){
  "use strict";

  var btn = document.querySelector("#chk_isbn .chk");
  var input = document.querySelector("#chk_isbn .input");
  var output = document.querySelector("#chk_isbn .output");

  btn.onclick = function(){
    var res = [];
    if(checkISBN(input.value, res)){
      //桁数が正しい
      output.innerHTML = res[0]+"  "+res[1];
    }else{
      //桁数が正しくない
      output.innerHTML = res[1];
    }
  }

  input.onchange = function(){
    output.innerHTML = "";
  }

  function checkISBN(text, res){
    var t1 = stringToDigits(text);

    var cd, chk;
    if(t1.length === 9){
      cd = mod_11_10_2(t1);
    }else if(t1.length === 12){
      cd = mod_10_3_1(t1);
    }else if(t1.length === 10){
      chk = t1.pop();
      cd = mod_11_10_2(t1);
    }else if(t1.length === 13){
      chk = t1.pop();
      cd = mod_10_3_1(t1);
    }else{
      res[1] = "9,10,12,13のいずれかの桁数で入力してください。";
      return false;
    }
    if(chk != null && chk === cd){
      res[1] = "◯ 一致しました。";
    }else{
      res[1] = "× 一致していません。";
    }
    res[0] = cd;
    return true;
  }

  function stringToDigits(text){
    var t1 = text.trim();
    var r = [];
    var d = "0123456789";
    for(var i = 0, mi = t1.length; i < mi; i += 1){
      if(t1[i] === "X"){
        r.push(10);
      }else{
        var bj = false;
        for(var j = 0, mj = d.length; j < mj; j += 1){
          if(t1[i] === d[j]){
            bj = true;
            break;
          }
        }
        if(bj){
          r.push(+t1[i]);
        }
      }
    }
    return r;
  }

  function mod_11_10_2(digits){
    var d = 0;
    for(var i = 0; i < digits.length; i += 1){
      d += (+digits[i])*(10-i);
    }
    var r = d%11;
    return 11-r;
  }

  function mod_10_3_1(digits){
    var d = 0;
    for(var i = 0; i < digits.length; i += 1){
      d += (+digits[i])*(i%2?3:1);
    }
    var r = d%10;
    var str = r.toString(10);
    if(str[str.length-1] === "0"){
      r = 0;
      return r;
    }
    return 10-r;
  }
}

</script>
</body>
</html>

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

ISBNの検算

すでに似たようなものがたくさんあると思いますが、
Wikipedeiaの記事を参考にして、JavaScriptでISBNのチェックディジットを計算するページを独自に作ってみましました。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width">
<title>ISBNのチェックディジットを計算</title>
</head>
<body>
  <div>
    <h1>ISBNのチェックディジットを計算</h1>
    <div id="chk_isbn">
      <p>
        <input type="text" class="input" value="978-4-87311-573-3">
      </p>
      <button class="chk">計算</button>
      <span class="output"></span>
    </div>
  </div>
<script>

window.onload=function(){
  "use strict";

  var btn = document.querySelector("#chk_isbn .chk");
  var input = document.querySelector("#chk_isbn .input");
  var output = document.querySelector("#chk_isbn .output");

  btn.onclick = function(){
    var res = [];
    if(checkISBN(input.value, res)){
      //桁数が正しい
      output.innerHTML = res[0]+"  "+res[1];
    }else{
      //桁数が正しくない
      output.innerHTML = res[1];
    }
  }

  input.onchange = function(){
    output.innerHTML = "";
  }

  function checkISBN(text, res){
    var t1 = stringToDigits(text);

    var cd, chk;
    if(t1.length === 9){
      cd = mod_11_10_2(t1);
    }else if(t1.length === 12){
      cd = mod_10_3_1(t1);
    }else if(t1.length === 10){
      chk = t1.pop();
      cd = mod_11_10_2(t1);
    }else if(t1.length === 13){
      chk = t1.pop();
      cd = mod_10_3_1(t1);
    }else{
      res[1] = "9,10,12,13のいずれかの桁数で入力してください。";
      return false;
    }
    if(chk != null && chk === cd){
      res[1] = "◯ 一致しました。";
    }else{
      res[1] = "× 一致していません。";
    }
    res[0] = cd;
    return true;
  }

  function stringToDigits(text){
    var t1 = text.trim();
    var r = [];
    var d = "0123456789";
    for(var i = 0, mi = t1.length; i < mi; i += 1){
      if(t1[i] === "X"){
        r.push(10);
      }else{
        var bj = false;
        for(var j = 0, mj = d.length; j < mj; j += 1){
          if(t1[i] === d[j]){
            bj = true;
            break;
          }
        }
        if(bj){
          r.push(+t1[i]);
        }
      }
    }
    return r;
  }

  function mod_11_10_2(digits){
    var d = 0;
    for(var i = 0; i < digits.length; i += 1){
      d += (+digits[i])*(10-i);
    }
    var r = d%11;
    return 11-r;
  }

  function mod_10_3_1(digits){
    var d = 0;
    for(var i = 0; i < digits.length; i += 1){
      d += (+digits[i])*(i%2?3:1);
    }
    var r = d%10;
    var str = r.toString(10);
    if(str[str.length-1] === "0"){
      r = 0;
      return r;
    }
    return 10-r;
  }
}

</script>
</body>
</html>

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

SharedWorkerを使ってWebSocketやSSEの接続を軽減するサンプル

参考

※この投稿は、↑のページを理解したくて、私が勉強してみたことをまとめた内容になります:bow:

やりたいこと

WebSocketを使うとサーバーとクライアントで双方向にデータを送信できます。
クライアントはサーバーとの接続を開きっぱなしにします。

なので例えばAさんが、WebSocketと通信するページを10タブ開いた場合は、Aさんだけでサーバーと10接続することになります。

computer_server1 (2).png

そこでSharedWorkerからWebSocketと接続することで、Aさんが10タブ開いたとしてもサーバーとの接続は1つだけにすることができます。

computer_server1 (1).png


今回私が最終的に作成したコードはこちらです

https://github.com/okumurakengo/shared-worker-sample

SharedWorkerとは

SharedWorkerはWeb Workerの一種です。
Web Workerとはメイン実行スレッドとは別に、バックグラウンドスレッドでスクリプト操作を実行できます。負荷の高い処理をメインスレッドで実行したくない場合に、Web Workerで処理を行うといった用途で使います。

SharedWorkerは複数ウィンドウ、複数タブからアクセスできる共有のWorkerです。
使い方もWeb Workerとは微妙に違います。

https://github.com/okumurakengo/shared-worker-sample

1. WebSocketサンプル

1-1. 簡単にWebSocketと通信

Nodejsのwsで簡単にWebSocketを使ってみます。
※まだSharedWorkerは出てきません。

yarn add ws
server.js
const http = require("http");
const fs = require("fs");
const WebSocket = require("ws");

// httpサーバー作成
const server = http.createServer();
server.on("request", async (req, res) => {
    try {
        res.write(await fs.promises.readFile(`${__dirname}${req.url === "/" ? "/index.html" : req.url}`));
    } catch(e) {
        console.log(e.message)
        res.writeHead(404)
    }
    res.end();
});
server.listen(8000);
console.log("http server listening ...");

// websockerサーバー作成
const wss = new WebSocket.Server({ port: 8001 });
wss.on("connection", ws => {
    console.log("Socket connected successfully");

    ws.on("message", message => {
        console.log(`Received ${message}`);

        for (client of wss.clients) {
            client.send(`${message} from server!`);
            client.send(`現在のクライアントとの接続数 : ${[...wss.clients].length}`);
        }
    });

    ws.on("close", () => {
        console.log("I lost a client");
    });
});

index.html
<!DOCTYPE html>
<meta charset="UTF-8">
<title>Document</title>
<script src="client.js" defer></script>

<input type="text" id="text" value="hello">
<input type="button" id="button" value="送信">
client.js
const ws = new WebSocket("ws://localhost:8001");

ws.addEventListener("open", e => {
    console.log("Socket Opne");
});

// サーバーからデータを受け取る
ws.addEventListener("message", e => {
    console.log(e.data);
});

// サーバーにデータを送る
button.addEventListener("click", e => {
    ws.send(text.value);
});
$ node server.js # サーバー起動
http server listening ...

この状態で http://localhost:8000 を開いて、送信ボタンを押すと、WebSocketと通信できました。
3つのウィンドウから開いた状態だと、WebSocketと接続している数が3つになることも確認できました。

Screen Shot 2019-11-23 at 22.37.43.png

1-2. SharedWorkerからWebSocketと通信

次に、
メインスレッドのjs -> SharedWorker -> サーバーとデータ送信する動作と、
サーバー -> SharedWorker -> メインスレッドのjsとデータ送信する動作に変更します。

client.jsを変更し、新しくworker.jsを作成します。

client.js
const worker = new SharedWorker("worker.js")
worker.port.start();

button.addEventListener("click", e => {
    console.log(`メインスクリプトからSharedWorkerにデータ送信 「${text.value}」`)
    worker.port.postMessage(text.value)
});

worker.port.addEventListener("message", e => {
    console.log(`メインスクリプトでSharedWorkerからのデータ受信 「${e.data}」`)
});
worker.js
const ws = new WebSocket("ws://localhost:8001");

ws.addEventListener("open", e => {
    console.log("Socket Opne");
});

// サーバーからデータを受け取る
ws.addEventListener("message", e => {
    console.log(`SharedWorkerでサーバーからデータ受信し、メインスクリプトへデータ送信 : 「${e.data}」`)
    // 開いている全ての複数ウィンドウ、複数タブへメッセージ送信
    for (const connection of connections) {
        connection.postMessage(e.data);
    }
});

let connections = [];
self.addEventListener("connect", e => {
    const port = e.ports[0];

    // タブやウィンドウを開くたびに接続を配列に保存します。
    // ※しかし、これだとウィンドウを閉じても配列に残り続けてしまったり、
    //  リロードすると前の接続が残ったまま新しい接続が追加されてしまうといった問題があります。
    //  Broadcast Channel API を使うと簡単に解決できるのでそちらを使った方が良いと思います。
    //  Broadcast Channel API を使ったサンプルは次に説明します。
    connections.push(port);

    port.addEventListener("message", function (e) {
        console.log(`SharedWorkerでメインスクリプトからデータ受信し、サーバーへデータ送信 : 「${e.data}」`)
        ws.send(e.data);
    });

    port.start(); // addEventListenerを使用する場合に必要、onmessageを使う場合は暗黙的に呼び出される
})

SharedWorkerからWebSockerに接続することで、複数のウィンドウを開いている場合でも、WebSocketとの接続は1つだけにすることができました。

Screen Shot 2019-11-23 at 23.32.01.png


※SharedWorker内でconsole.logを使用した場合は、普通に開発者ツールのコンソールには出てきません。

chrome://inspect/#workers を開き、そこから開けるコンソールに表示されます。

Screen Shot 2019-11-23 at 23.35.25.png

1-3. Broadcast Channel API を使ってSharedWorkerからメインスクリプトへデータを送信するようにする

先ほどの例ではタブやウィンドウを開くたびに、connections という配列にe.ports[0]が追加されていくようにしたのですが、
ブラウザを閉じても配列に残ったままになってしまう、
リロードした場合も、前の接続が残ったまま新しい接続が追加されてしまうといった問題があります。

woker.js
// ...

let connections = [];
self.addEventListener("connect", e => {
    const port = e.ports[0];

    connections.push(port);

// ...

SharedWorkerからBroadcast Channel API を使ってメインスクリプトへデータを送信するのが良いようです。

Broadcast Channel API - Web APIs | MDN

BroadcastChannelを使うことで、開いている全てのタブ、ウィンドウ、iframeにデータ送信できます。
※実際にデータを送信したウィンドウ(またはタブなど)では受信しません。自分以外の全てでデータを受信できます。

client.jsとworker.jsをBroadcastChannelを使うように変更します。

client.js
+ const bc = new BroadcastChannel("WebSocketChannel");
  const worker = new SharedWorker("worker.js")
  worker.port.start();

  button.addEventListener("click", e => {
      console.log(`メインスクリプトからSharedWorkerにデータ送信 「${text.value}」`)
      worker.port.postMessage(text.value)
  });


+ bc.addEventListener("message", e => {
- worker.port.addEventListener("message", e => {
      console.log(`メインスクリプトでSharedWorkerからのデータ受信 「${e.data}」`)
  });
worker.js
+ const bc = new BroadcastChannel("WebSocketChannel");
  const ws = new WebSocket("ws://localhost:8001");

  ws.addEventListener("open", e => {
      console.log("Socket Opne");
  });

  // サーバーからデータを受け取る
  ws.addEventListener("message", e => {
      console.log(`SharedWorkerでサーバーからデータ受信し、メインスクリプトへデータ送信 : 「${e.data}」`)
      // 開いている全ての複数ウィンドウ、複数タブへメッセージ送信
-     for (const connection of connections) {
-         connection.postMessage(e.data);
-     }
+     bc.postMessage(e.data);
  });


- let connections = [];
  self.addEventListener("connect", e => {
      const port = e.ports[0];


-     connections.push(port);

      port.addEventListener("message", function (e) {
          console.log(`SharedWorkerでメインスクリプトからデータ受信し、サーバーへデータ送信 : 「${e.data}」`)
          ws.send(e.data);
      });

      port.start(); // addEventListenerを使用する場合に必要、onmessageを使う場合は暗黙的に呼び出される
  })

こちらでも同じように動作することができました。

Screen Shot 2019-11-24 at 0.07.37.png

2. SSE (Server-Sent Event) でもやってみた

server.js
const http = require("http");
const fs = require("fs");

// httpサーバー作成
const server = http.createServer();

server.on("request", async (req, res) => {
  if (req.url === "/events") {

    res.writeHead(200, {
        "Content-Type": "text/event-stream", 
        "Cache-Control": "no-cache",
    });

    setInterval(() => {
        res.write(`data: ${JSON.stringify({ time: new Date().toLocaleTimeString() })}\n\n`)
        res.flushHeaders();
    }, 1000)

  } else {

    try {
        res.write(await fs.promises.readFile(`${__dirname}${req.url === "/" ? "/index.html" : req.url}`));
    } catch(e) {
        console.log(e.message)
        res.writeHead(404)
    }
    res.end();

  }
});
server.listen(8000);
console.log("http server listening ...");
index.html
<!DOCTYPE html>
<meta charset="UTF-8">
<title>Document</title>
<script src="event.js" defer></script>

<ul id="sample"></ul>
event.js
const bc = new BroadcastChannel("WebSocketChannel");
const worker = new SharedWorker("worker.js")
worker.port.start();

bc.addEventListener("message", e => {
    sample.appendChild(document.createElement("li")).textContent = e.data;
});
worker.js
const bc = new BroadcastChannel("WebSocketChannel");
const es = new EventSource("./events");

es.addEventListener("message", e => {
    const { time } = JSON.parse(e.data);
    bc.postMessage(`${time} from SharedWorker!`);
});

Screen Shot 2019-11-24 at 0.55.43.png

SSEでもSharedWorkerから接続して、複数タブを開いても接続は1つだけにすることができました。

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

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

グラブルのコミュニティが抱える課題をウェブアプリで解決して1日で6万PVを叩き出したときの話

みなさんこんにちは。トップゲートの古都ことです。普段は自分のブログ( https://sbfl.net/blog/ )に記事を書いているのですが、今回はQiitaに投稿することにしました。

ちょっといつもの技術記事とは毛色が違うので、普段とは違う楽しみを提供できると思います。

長いから3行でまとめてくれ

  • グラブルコミュニティではスプレッドシートで課題を解決しがちだがモバイルUXが悪い
  • スマホユーザを考慮したウェブアプリを出したところ、大ヒット
  • 世の中の課題を解決できれば、技術的にしょぼくてもヒットを狙える

グラブルについて

グラブルの正式名称は「グランブルーファンタジー」というモバイルゲームです。

IMG_5F2E388CF9F3-1.jpeg

グラブルはHTML5で組まれていて、ブラウザ上で動作します。基本的にスマートフォンを対象としていますが、PC上でもプレイ可能です。

グラブルコミュニティのスプレッドシート文化

多くのスマホ向けゲームでは、装備やキャラを強化するための素材を集める「周回」をしなければいけません。「周回」というのは何度も同じ敵を倒し、素材アイテムを大量に集める行為です。

周回はグラブルにも当然の権利のように存在します。装備を強化するために、同じステージを何十回、何百回とクリアします。

IMG_031D523DBC11-1.jpeg

周回要素の是非についてはここでは触れませんが、周回をするにあたり「いまどれぐらい集まったのか」「あとどれぐらいかかるのか」という情報は欲しくなります。「どれぐらい集まったか」についてはゲーム内で完結し、画面下部に常時表示できるようになりました。しかし「どれぐらいかかるのか」については、グラブルは面倒を見てくれません。

そこでユーザコミュニティ側から何かしらの「計算ツール」が登場します。グラブルコミュニティでは、計算ツールは主にGoogleスプレッドシートで作られ、Twitterなどで共有されます。スプレッドシートを自分のGoogleドライブにコピーして必要事項を記入すれば残り周回回数などを計算できる、という仕組みになっています。

hiro_sheet.png

スプレッドシートによる計算はグラブルコミュニティにおいてはよく使われる手法で、何かしらの周回要素が発生すると大抵の場合誰かがスプレッドシートを作っています。

スプレッドシートの抱える問題

スプレッドシートによる計算ツールは素晴らしいもので、周回における課題を解決しています。しかしその一方でいくつかの問題も抱えます。

グラブルはモバイルゲーム(上級者はPCでやってる人が多いけども……)です。ユーザもモバイル機器に慣れ親しんだ人が多く、普段はPCを触らないという人も存在します。そしてモバイル端末におけるスプレッドシートの体験は劣悪です。狭い画面で何をすればいいのかが分かりにくく、入力も困難となります。

また、世の中はITリテラシーの高い人ばかりではありません。スプレッドシートをうまくコピーできない人や、そもそもGoogleドライブってなんなんだという人までいます。スプレッドシートをうまく使えない、使い方がわからない、という人は多く存在するのです。

スプレッドシートは作成も簡単で便利な反面、ユーザに高い負担をかけることにもなります。特に「パソコンとかよくわからないけど、スマホゲームはよくやる」という人がスプレッドシートを活用しようとすると、多くの問題に直面することになります。

1日6万PVのウェブアプリ

2019年の10月8日に以下のウェブアプリを公開したところ、1日で6万PVほど行きました。次の日にはだいぶ下がりましたけども。ピークは完全に過ぎましたが、今でも1日に600PVほどあります。

グラブル十賢者皮算用ツール https://sbfl.net/app/granbluefantasy/arcarumcalc/
ソースコード https://github.com/subterraneanflower/gbf-arcarumcalc

ga.png

「皮算用」という言葉の使い方が間違っていますが、まあキャッチーな名前にしたかったので……。

ツールの機能としては、現在の所持アイテム数を入力し、周回の大雑把な残り日数を算出するだけの簡単なものです。なぜ残り回数ではなく「日数」かというと、この周回を行うには毎日1枚発行される「パスポート」というアイテムを消費する必要があるからです。

arcarum02.png

アプリの内容としては難しいことは一切やっておらず、大雑把な期待値から「残り日数っぽいもの」を適当に掛け算割り算して計算しているだけです。技術的にはただのチュートリアルレベルです。

本当に解決すべき課題

この周回要素は「アーカルム」というのですが、アーカルムでは専用の強力なキャラが入手できるようになっています。しかしアーカルムでの素材入手数は運に左右されやすく、あとどれぐらいかかるかというのが不透明です。そのため多くの人が「あとどれぐらいでキャラを入手できるのか全くわからない」と不安になってました。

もちろん、それを大雑把に管理するためのスプレッドシートがいくつか作られました。自分のドライブにスプレッドシートをコピーして、必須項目を入力するだけです。しかしそれでも先述の通りスプレッドシートには大きな障壁が存在し、多くの人が使えずにいたというのが実情です。

そして素材を管理するスプレッドシートは数あれど、残り日数を表示してくれるスプレッドシートは皆無でした。みんなが知りたいのは「どれぐらいかかるのか」であって「どのぐらい進んだか」ではない、と私は思っていました。本当に解決すべきなのは「キャラを入手するのにどれだけの日数がかかるのか」を明らかにすることではないのかと感じていました。

そこで自分でサクっと作ってしまうことにしました。アーカルムは運に大きく左右されるので正確な日数は算出できませんが、大雑把に「だいたいこれぐらい」を算出できるのではないかと思い、所持アイテムに基づき適当に計算するだけのウェブアプリ、「グラブル十賢者皮算用ツール」を公開しました。モバイルデバイスで表示することを前提にPCへの最適化は切り捨て、入力項目もできるだけ省きました。正確さを目標とするのではなく、「とりあえずの残り日数」を表示することを目標としました。結果をシェアするためのツイート機能もつけました。

大ヒットでした。

バレット計算機

また別のウェブアプリとして「グラブルバレット計算機」というのも作っています。こっちは1日に600〜4000PVぐらいです。

グラブルバレット計算機 https://sbfl.net/app/granbluefantasy/bulletcalc/
ソースコード https://github.com/subterraneanflower/gbf-bullet-calculator

これはバレットと呼ばれる装備の要求素材の計算機と、おまけで進捗管理機能がついています。バレットの作成には「バレットを作るためのバレットが必要で、そのバレットのために素材が必要」という難解な事情になっていて、こういった計算機の需要がありました。

bulletcalc.png

これも元々はスプレッドシートで行われていたのですが、スプレッドシートにうまく馴染めないという方への受け皿として機能しました。今も多くの方に利用していただいています。

PWAとなっているので端末へのインストールが可能ですが、PWAとしてインストールする方は少数派です。

まとめ

人のコミュニティというのは常に様々な課題を抱えています。一見、今の方法が最適なように見えても、その背後には無数の「脱落者」が存在します。そういった人々を拾い上げることができるような技術を投入できれば、救われる人も増えるでしょう。

そして必ずしも技術に優れている必要はありません。「十賢者皮算用ツール」はソースコードを見れば分かりますが、本当にチュートリアル程度のコードしか書いていません。適切な場に適切な技術を採択すれば、それだけで世界は前へ進むことができます。もちろん、日頃からの技術の研鑽を欠かさない方が選択肢は広がるので、技術力の向上自体をサボることはできませんが……。

私たちは、何が課題で何を採用すれば解決できるのかということを常に考える必要があるでしょう。加えて、高度な技術が要求されたときのために、常に学び続ける姿勢も要求されます。そして自分の技術で何かしらの問題が解決できるとわかったときは、すぐに行動しましょう。

最後まで読んでいただき、ありがとうございました。いつもより少し雰囲気の違う記事になりましたが、楽しんでいただけたら私も嬉しいです。

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