20200208のJavaScriptに関する記事は27件です。

フロントエンド開発時に確認すべき5つの挙動

0. はじめに

 フロントエンド開発をしているとユーザーの予期せぬ行動で、予期せぬ挙動やバグを生み出してしまいます。しかし、フロントエンド開発の経験が浅いと、想定できるユーザーの行動も限られてきます。そこで、本記事では、確認しておくべきユーザーの行動について記載します。1

1. 前のページからの遷移

 前のページからの遷移を考えないことはまずないと思いますが、実装しなければいけないページにたどり着くまでの遷移が1パターンとは限りません。複数のURLから遷移してくることもあるので、どこからきてもエラーとならないように実装する必要があります。

2. 次のページからの戻りの遷移(リンクまたはボタン)

 前のページからの遷移が終われば、ページ遷移後の挙動の確認です。経験が浅い開発者で意外と確認し忘れることが多い挙動だと思います。その中でも、ページ上に意図的に設置されているリンクやボタンをクリックした時の戻りの確認がこの項目です。リンクやボタンの名称は「修正する」だったり、「戻る」だったり、プロダクトオーナーやデザイナーの意向によって異なる場合もあります。

 このとき確認すべき事項としては

1) 入力した値が保持されたままになっているべきか、入力した値は空に戻すのか
2) 戻る挙動によって、サーバーサイドから渡されるべき値が渡ってきておらず、エラーとなっていないか
3) 入力した値が保持されている場合は送信ボタンがdisabledに戻ってないか(保持されない場合は送信ボタンがdisabledになっているか)

などです。リンクまたはボタンでの遷移の場合は、実装方法によって、

  • getで戻る場合
  • postで戻る場合
  • JavaScriptを利用してブラウザのヒストリーバックで戻る場合

などが考えられ、どの実装になっているか、決まっていない場合は、どの実装にするかを決める必要があります。

3. 次のページからの戻りの遷移(ブラウザバック)

 ページ遷移後の戻りの二つ目はブラウザの戻るボタン(最近は<の表記の場合が多い)を利用した場合です。これは、2のヒストリーバックと同じ挙動になりますが、この場合も、1)や2)の確認を行います。

4. 次のページからの戻りの遷移(サーバーサイドバリデーションによるもの)

 ユーザーの意図的な遷移ではなく、サーバーサイドのバリデーションに引っかかって戻ってくる場合も考えられます。ページ遷移でサーバーサイドにリクエストを送る実装になっている場合は、バリデーションエラーで戻ってくる挙動となっている場合があります。フロントエンドのバリデーションだけで弾くことができず、サーバーサイドのバリデーションでしかチェックできない場合などです。
 この時、多くの場合、エラーとなった項目やエラーメッセージがサーバーサイドから送られてきます。この時にフロントではどのような挙動となるかを定義し、実装する必要があります。例えば、エラー対象となるフィールドに色を付けてわかりやすくしたりし、送信ボタンはdisabledにします。さらに、内容が修正された場合には色を戻して送信ボタンのdisabledを解除するといった挙動が考えられます。

5. ブラウザのリロード

 最後に忘れがちなのがブラウザのリロードです。リロードの場合も1) 入力した値が保持されたままになっているべきかなどを考慮する必要があります。

 以上、フロントエンド開発時に確認すべきユーザーの5つの挙動についてご紹介しました。これらは、私がチームで開発している時に新規参画者のコードレビューをしている際に指摘してきたことでもあります。最初のうちは、目的とする挙動を実装するのに精一杯のことが多いですが、慣れてきたらこのような細かい点も考慮に入れるようにしていきたいという、自戒も込めて記述させていただきました。


  1. 今後、確認すべき項目が見つかり次第、順次追加していきます。 

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

AI化、5分でチェック・簡単無料POC! 5min POC(Proof Of Concept) on AI!

https://randomwalkjapan.blogspot.com/2020/02/aipoc-5min-poc-proof-of-concept-on-ai.html

自分の仕事がAI化できるか試してみたい人は多いだろう。Many may wish to check if the task working on can be on AI basis.

出来そうな気がするが、人に頼むとお金がかかるし、自分でやるには大変だし。いろんなシステムも宣伝しているが、使い方を覚えるだけで大変だ。決心してやってみて結局だめだったらお金と時間の無駄だし。It looks possible but will cost if ask someone. But looks hassle if do it your own. Google search shows many advertisements on AI systems but looks hard to learn. Also, even if you decided to do it, if it fails eventually, it will be a waste of time and money.

そんな時、無料で、簡単に、可能性をチェックできるのがこのページだ。先に書いた温度管理のプログラムを読み替えただけだ。プログラムはこちら。In such a case, this page provides you, for free and ease, the feasibility check of concept of your idea. This is just another interpretation of previously shown temperature control program I presented. GitHub is here.

https://github.com/tanakayutaka/Tensorflow.js-tools-/blob/master/index%20-%205%20min%20POC%20tool.html

下のアプリの使い方は簡単。ただ、画面に自分の手持ちデータをそのまま入力し、ボタンを押すだけで完了。AIといっても、言語処理、画像、制御などいろいろあるが、ここでは制御が対象。It is very easy to use below application. Simply, just input your data onto the screen, the click button. There are lots of AI out there such as language processing, image processing and control, here we focus on control.

農業、製造、教育など、あらゆる場面でAIは助けになる何でも屋だ。AI is all rounder covering agriculture, manufacturing, education and many other fields.

深く考えずに、まずは身近のデータを入れて実験してみよう! Just jump in before think too much, then input your local data and experiment.

何らかの状態xのもと、何らかの制御zを行うと、結果yが出る。では、ある状態xの場合、ある結果yを実現するには、どんな制御zをおこなえばいいの?という問題に適用できる。Under some circumstance X, by making some control Z, then got result Y. Now to achieve result Y under some circumstance X, how much control Z should be applied? This is the problem we can test here.

使い方は簡単。データ1-9に何らかの観測値(x軸)、制御値(z軸)、そしてその結果生じた値(y軸)を入れ、空色のボタンを押す。計算後、観測値、目標とする結果を実現する制御値(縦軸z)が表示される。グラフが表示されない場合は、グラフのホームアイコンを押す。The usage is easy. Input data set 1-9, i.e., some observation(x-axis), control value(y-axis) and resulting value(y-axis), then push sky blue button. After calculation, control value(vertical z-axis) is shown as surface to achieve target value at observation value. Click Home Icon on the graph if it is not shown.

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

Adobe PhotoshopでチャンネルごとにTIFFファイルを保存する

はじめに

Photoshopで、psdファイルをチャンネル単位に個別のグレースケールTIFFファイルに保存する、JavaScriptを作りました。

作るに至った経緯を少々。
MKの紙面印刷用データを作る過程で、CMYKからMKに変換したという画像データを支給され、それを取り込んで紙面データを作成して後工程に渡しました。すると、画像データにYが残っていたとのことで紙面データが後工程から戻されてきました。
変換後の画像データに不要色が残っていないかチェックできればこのようなことは起きないわけで、ではチェックツールを作ればよいと。

もちろん、TIFFファイルにドットが載っているかどうかチェックできなければ無意味です。このチェック方法については別途投稿します。

なお、ここではPhotshopでのJavaScriptの動かし方については触れていません。

どう動くのか

  • ひとつのフォルダ内の複数のpsdファイルを同時に処理できます。
  • スクリプトを起動すると、フォルダ選択ダイアログが開きます。このダイアログでpsdファイルを収めたフォルダを指定してやります。
  • psdファイルと同ディレクトリに、「psdファイル名_separated」というフォルダが作成され、その中にチャンネル単位のTIFFファイルが保存されます。

1.Photoshopで見たpsdファイルとチャンネル
psd.png

2.psdファイルを収めたフォルダ。これを処理する
test_folder.png
3.処理後のフォルダ。psdファイルに対応して「psdファイル名_separated」ができている
prpcessed.png
4.「psdファイル名 _
separated」のなか。
result.png

コード

anynameok.jsx
//フォルダチューザを開き、フォルダを取得
var folderObj = Folder.selectDialog("処理したいファイルを収めたフォルダを選択してください");

//フォルダを取得したら、フォルダ内のpsdファイルをリストで取得
if (folderObj) {
  var fileList = folderObj.getFiles('*.psd');
  var fileCount = fileList.length;
    //psdファイルひとつひとつを関数processで処理
  for (var i = 0; i < fileCount; i++) {
        process(fileList[i]);
  };
    alert("処理が終わりました");
} else{
    alert("キャンセルされました");
};

//psdファイルをチャンネル単位で保存する関数
function process(targetFile) {
  //Photoshopでファイルを開く。開かれたファイルはそのままアクティブドキュメントに。
  open(targetFile);
  //アクティブドキュメントのパス(親ディレクトリ)を取得
  var activeDocPath = activeDocument.path;
  //分離後のファイル保存時にサブフォルダを挟むため、そのサブフォルダの命名取得用に拡張子抜きのファイル名を取得しておく
  var fileNameWithoutExt = activeDocument.name.substr(0, activeDocument.name.indexOf('.'));
  //出力先フォルダの作成
  var outFolder = new Folder(activeDocPath + '/' + fileNameWithoutExt + '_separated');
  if (!outFolder.exists) {
    outFolder.create()
  };
  //チャンネル分割のため、画像を統合
  activeDocument.flatten();
  //チャンネル毎にドキュメントを分割。この際、元ファイルは未保存で閉じられ、チャンネル毎のドキュメントのみが未保存で開かれている
  activeDocument.splitChannels();
  //開かれているドキュメントの数すなわち元ファイルのチャンネルの数
  var docCount = documents.length;
  //各ドキュメントを保存し、閉じる
  for (j = 0; j < docCount; j++) {
    var fileObj = new File(outFolder + '/' + activeDocument.name);
    var tiffOpt = new TiffSaveOptions();
    //ここに適宜TIFF保存時のオプションを記述
    activeDocument.saveAs(fileObj, tiffOpt, false, Extension.LOWERCASE);
    activeDocument.close();
  };
};

TIFF保存時の設定については以下が詳しいです。
http://nakatoji.lolipop.jp/index.php?option=com_content&view=article&id=401&catid=30

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

JavaScript・TypeScriptのimport・exportの依存関係を可視化するcode-dependencyの紹介

index.png

はじめに

現在のJavaScriptはexportimportによるモジュールの切り離しと結合が可能であるため。大きなプロジェクトに成長させることができます。

実装が進むにつれてファイル間の依存が複雑になっていき、実装全体の依存関係を把握するのが難しくなっていきます。これはプロジェクトに対して新しいメンバーが増えたときに、コードリーディングの時間を十分に取る必要があります。また、OSSのライブラリに貢献したいときも同様の状況が生まれるでしょう。特に後者は開発メンバーが近くにいるとも限らず、他国の方である可能性も十分に高いため開発に参加するための準備が必要になります。

このような、全体の依存関係の設計を見直したい場合や、新たに開発に参画する場合により短時間に理解を深めるためのツールを作成したので紹介します。

@code-dependencyの紹介

DEMO

百聞は一見にしかずと言われるので、DEMOページをご覧ください。code-dependencyは次のような依存関係の表示を生成することができます。

@code-dependencyを利用する

次に実際に@code-dependencyを利用してみましょう。まずはyarn/npmを利用して@code-dependency/cliインストールします。

# npmを使っている人
npm i -g @code-dependency/cli@latest
# yarnを使っている人
yarn global add @code-dependency/cli@latest

例としてyargs/yargsの依存関係を覗いてみます。

git clone https://github.com/yargs/yargs.git --depth 1
cd yargs

code-dependency --source ./

# http://localhost:3000/project を開きます

ブラウザを開いてみます。ページ左側に走査して取得したファイルツリーが表示されます(.js,.jsx,.ts,.tsxのみ)。

メニューからyargs/index.jsを選択すると以下のような表示になります。

yargs/index.js

URLの例: http://localhost:3000/project/?pathname=yargs%2Findex.js

可視化によってファイルの依存関係が一目瞭然となります。

ユースケース

依存関係の可視化をしただけなので、ここから何を読み取るかは利用者の自由です。例えば、

  • Model/View/Controllerを利用したアーキテクチャが設計通りに参照構造を持っていることを確認する
  • ファイル間の循環参照(例:a.js->b.js->c.js->a.js)をしていないことを確認する
  • SOLIDの原則に従っているか確認する(インターフェース分離の原則がわかりやすい)
  • 新機能を追加するときの依存パターンに既出のものがないか確認する
  • チーム内で依存関係の設計の認識合わせをする

などが挙げられます。メンテナンスされているコードや有名なライブラリ(angular.js, react, vscode, etc...)を観察してみて依存関係の研究に勤しむのもよいかもしれません。(他に考えつく利用用途があればぜひコメントで教えて下さい!)

機能紹介

@code-dependency/cliのREADMEに利用可能な機能は書いています。
tsconfigやwebpackを指定できる他、静的にホスティングできるようにHTMLを出力する機能も搭載しています。

静的にホスティングする

サンプルとして、@code-dependency/cli自体の依存関係を以下にホスティングしています。

この出力は次のようなCLIを叩くことで出力されます。

# current directoryはcode-dependency/packages/cli
code-dependency --source ./src --exclude node_modules --export-static ./docs --public-path https://himenon.github.io/code-dependency/

余談

これは完全に余談ですが、GitHub Pagesにホスティングした状態でLighthouseの結果は以下のとおりです。静的のホスティングされた状態でも快適に使えるように実装しました。

image.png

dotからSVGに変換するエンジンを変更する

デフォルトのまま利用した場合、viz.jsを利用してブラウザ側でdot言語をSVGに変換しています。ただこれには欠点があり、viz.jsはMemory Leakしています。依存関係が数百を超えたあたり(具体的にはdot言語で処理するテキストが100KBのオーダーを超えたあたり)からviz.jsでは処理しきれなくなります。

これを避けるために、NativeのGraphvizを利用することで上限を緩和することができます。
Graphvizの公式サイト(http://www.graphviz.org/)からマシンにインストールして、`dot`コマンドが利用できる状態にしておいてください。

この状態で、--engine dotのフラグを追加して起動すると、viz.jsを利用しない代わりに、NativeのgraphvizがSVGを生成します。

code-dependency --source ./src --engine dot

viz.jsはすでにメンテナンスされていない状態なので、Nativeにインストールされているものを利用することをおすすめします。

既知の不具合について

Nativenのdotエンジンを利用していてもSVGが生成されないことがあります。これはNodeJS側のHeap Mmoeryが足りなくなる場合に以下のようなエラーを起こします。

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

Node v12以降であればHeapの最大値がハードウェアに依存して変化するので、大規模なプロジェクトでなければそこまで心配ないかもしれません。

code-dependencyが提供するAPIについて

code-dependency自体は特にAPIを提供することはしていません。内部でdependency-cruiserが吐き出すdot言語をSVGに変換し、WEBブラウザ上で容易に確認できるようにしたに過ぎません。

依存関係のテストや詳細な情報を取得したい場合はdependency-cruiserを直接利用すると良いでしょう。

最後に

着想自体は2019年4月あたりにあり、一時は自分でASTを解析してd3jsで依存関係の可視化までフルスクラッチでやっていましたが、対応すべき内容が多くなりメンテナスコストが高くなったため、dependency-cruiserを利用する形にしました。もしこのライブラリがなければ、code-dependencyが誕生しなかったので、ぜひdependency-cruiserにスターをつけてあげてください。

また、code-dependencyの機能自体は大体出揃っており、これほしいな、と思う限り今後の更新頻度は減少ることと思います。その前に宣伝効果の高そうなQiitaに一つ記事を書いてシェアしておこうと思った次第です。もし、既存の状態からより効率的な依存関係の把握や情報共有の方法がありましたらIssueやPull Requestを投げてくださると幸いです。

今回のライブラリのより技術的な話は自分のブログの方に掲載していくかもしれません。興味のある方は足を運んでみてください。

関連するライブラリなどの紹介

dependents-view

URL: https://github.com/Himenon/dependents-view
DEMO: https://himenon.github.io/dependents-view/#packages

npmライブラリの逆依存関係をGitHubもしくはGitHub Enterpriseから収集し、まとめ直すアプリケーション(自分でビルドする必要がある)。内容としてはGitHubのNetwork Graphと同様ですが、内容をフィルタリングして利用する機能を持っています。自分のライブラリを変更した場合にどのライブラリまで影響を及ぼすか、を検索することができます。

libcheck

URL: https://github.com/Himenon/node-libcheck

yarn.lock(package-lock.jsonは非対応)からライブラリが複数バージョンインストールされていないかチェックするライブラリ。

例1:利用するライブラリのdependenciesにreactのv15系がインストールされていて、アプリケーション側がdependenciesでreact v16がインストールされているような場合、両方ともnode_modulesに配置されます。これを検出できます。

例2:モノレポのようなpackage.jsonを複数扱う場合にdependenciesの更新漏れをチェックすることができます。

# インストール豊富お
yarn add -D libcheck
# 例えば、reactが複数バージョン入っていないかテストする方法
libcheck --input ./yarn.lock --pattern "react" --test

このページで出てきたリンク一覧

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

初心者によるプログラミング学習ログ 233日目

100日チャレンジの233日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

233日目は

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

javascriptの.submitでif文を使う時のエラー

$('#form').submit(function() {

        // form1が空のとき、エラー文を表示してください
        if ( $('.form1').val() === '') {
            $('.error-message-1').text('入力してーーー');
        } else {
            $('.error-message-1').text('');
        }

        return false;
      });

入力してーーー!と表示されない時のエラー。

if ( $('.form1').val() === '')の、クラスの.form1が、
htmlの

<input class="form1" type="text" placeholder="山田 太郎" name="userName">

input内のクラスに名付けないと反応しない。

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

ドロップダウンメニューの作り方

webサイトなどでよく見るドロップダウンメニューの作り方を自身の備忘のために記録します。
今回は、jQueryを用いて実装します。

まずはHTMLです。jQuery及びJavaScriptもここで読み込んでおきます。

index.html
<body>
 <div id="main">
      <ul class="menu">
        <li>Menu1</li>
        <li>
          Menu2
          <ul class="sub">
            <li><a href="#">Menu2-1</a></li>
            <li><a href="#">Menu2-2</a></li>
          </ul>
        </li>
        <li>
          Menu3
          <ul class="sub">
            <li><a href="#">Menu3-1</a></li>
            <li><a href="#">Menu3-2</a></li>
            <li><a href="#">Menu3-3</a></li>
          </ul>
        </li>
      </ul>
    </div>
    <script
      src="https://code.jquery.com/jquery-3.4.1.min.js"
      integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
      crossorigin="anonymous"
    ></script>
    <script src="app.js"></script>

</body>

次にスタイルを整えます。

style.css
#main {
  margin: 100px auto;
}

ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
  color: #fff;
}

ul.menu li {
  float: left;
  position: relative;
  margin: 0 0 0 1px;
  padding: 5px;
  width: 200px;
  background: #555555;
  display: block;
}

ul.sub {
  display: none;
  position: absolute;
  margin-left: -6px;
  padding: 0;
}

ul.sub li a {
  padding: 5px 10px;
  margin-left: -5px;
  margin-right: -5px;
  margin-bottom: -5px;
  display: block;
  color: #fff;
}

ul.sub li a:hover {
  background: #ff9900;
  text-decoration: none;
}

そしてこれに対して、JavaScriptを実装していきます。

app.js
$(function() {
  $("ul.menu li").hover(
    function() {
      $("ul.sub:not(:animated)", this).slideDown();
    },
    function() {
      $("ul.sub", this).slideUp();
    }
  );
});

これで完成です。

app.js
$(function(){
  //関数など
}};

このコードを書くことで、DOMの処理が終わってから中の関数などが読み込まれるようになります。
基本的に書くようにしましょう。

また、hoverの扱いに関しては、

app.js
$('button').hover(
    function() {

        //マウスカーソルが重なった時の処理

    },
    function() {

        //マウスカーソルが離れた時の処理

    }
);

のように記述します。

以上です。

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

React Nativeでスマートフォンのカレンダーのデータを取得

調査内容

Android/iOS共にNativeコードではスマートフォンのカレンダーデータにアクセスする事ができますが、React Nativeでアクセスする方法について調査しました。今回はAndroidでのみ動作確認を行いましたが、iOSでも動作可能と思われます。
スクリーンショット 2020-02-08 17.40.18.png

設定手順

以下のモジュールを利用します。
https://github.com/wmcmahan/react-native-calendar-events

$ npm install --save react-native-calendar-events
$ react-native link

今回はAndroidで動作確認をしますので、AndroidのNative部分をいくつか修正します。

android/settings.gradleに以下を追加

include ':react-native-calendar-events'
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')

AndroidManifest.xmlに以下を追加

    <uses-permission android:name="android.permission.READ_CALENDAR" />
    <uses-permission android:name="android.permission.WRITE_CALENDAR" />

MainActivity.javaに以下を追加

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        CalendarEventsPackage.onRequestPermissionsResult(requestCode, permissions, grantResults);
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

React Nativeのアプリケーションを記述

App.jsからMainScreen.jsを呼び出す形に変更して、MainScreen.jsに3箇所押せるようにTouchableViewを利用します。

/* App.js */
import React from 'react';
import { StyleSheet, Button, View, } from 'react-native';
import MainScreen from './src/screens/MainScreen'

const App: () => React$Node = () => {
  return (
    <View>
      <MainScreen />
    </View>
  );
};

const styles = StyleSheet.create({
});

export default App;
/* MainScreen.js */
class MainScreen extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <TouchableHighlight style={styles.listItem} onPress={this.authorizeEventStore}>
          <Text style={styles.title}>パーミッションリクエスト</Text>
        </TouchableHighlight>
        <TouchableHighlight style={styles.listItem} onPress={this.findCalendars}>
          <Text style={styles.title}>カレンダー取得</Text>
        </TouchableHighlight>
        <TouchableHighlight style={styles.listItem} onPress={this.fetchEvents}>
          <Text style={styles.title}>イベント取得</Text>
        </TouchableHighlight>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    width: '100%',
    flex: 1,
  },
  listItem: {
    padding: 32,
    borderBottomWidth: 1,
    borderBottomColor: '#ddd',
    backgroundColor: '#fff',
    justifyContent: 'center',
  },
  title: {
    fontSize: 20,
  },
});

こんな感じで表示されます。
スクリーンショット 2020-02-08 17.51.04.png

コードの記述。

それぞれに応じた関数を以下のように記述します。Androidではtarget SDK 23以降はパーミッションの確認が必要なので、RNCalendarEvents.authorizeEventStore()でパーミッションを取得します。

  authorizeEventStore() {
    console.log("confirmAuthorizationStatus");
    RNCalendarEvents.authorizeEventStore()
      .then((status) => {
        console.log(status);
      })
      .catch((error) => {
        console.log(error);
      });
  }

パーミッションリクエストを押すと以下のようなパーミッションの確認がOSから表示されます。
スクリーンショット 2020-02-08 17.53.06.png

後はCalendarの取得とイベントの取得を以下のように記述します。RNCalendarEvents.fetchAllEvents(startDate, endDate, calendars)ではcalendarsでカレンダーIDを指定すれば良いですが、今回は簡略化のためにハードコーディングしています。ボタンを押すと取得された結果がConsole上で出力されます。

  findCalendars() {
    RNCalendarEvents.findCalendars()
      .then((list) => {
        console.log(list);
      })
      .catch((error) => {
        console.log(error);
      });
  }

  fetchEvents() {
    RNCalendarEvents.fetchAllEvents('2019-01-01T17:24:00.000Z', '2019-12-31T17:24:00.000Z,'[2])
      .then((list) => {
        console.log(list);
      })
      .catch((error) => {
        console.log(error);
      })
  }

以上、react-native-calendar-eventsを使う事で簡単にスマートフォンのカレンダーにアクセスする事が可能となりました。

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

webpack + jQuery(webpack.config.js書き方の例)

webpack + jQuery

自分用メモ

webpackはモジュールバンドラー

エントリーポイントに設定したjsファイルを中心に、各モジュールのjsファイルを纏めてbundle

  • モジュール管理できるため機能ごとに開発ができ保守性が上がる
  • 複数のjsファイル(cssや画像も)を一つのファイルにbundleできるためリクエスト数減少とパフォーマンス向上 ※ ファイルサイズの増大によるパフォーマンス低下もありうる
  • 依存関係の解消

CSSをインラインのstyleではなく、CSSファイルとして出力

1. webpack使用の下準備

1.1. webpackでの基本的なパッケージのインストール

$ npm init -y
$ npm i -S jquery
$ npm i -D webpack webpack-cli 
$ npm i -D terser-webpack-plugin optimize-css-assets-webpack-plugin mini-css-extract-plugin
$ npm i -D node-sass css-loader sass-loader style-loader postcss-loader
$ npm i -D babel-loader @babel/core @babel/polyfill @babel/preset-env 
$ npm i -D autoprefixer

# 分割したcssの読み込みをリロードしない(brouser-sinc代用)
$ npm i -D webpack-dev-server

# gulp使うとき
$ npm i -D gulp gulp-eslint gulp-notify gulp-plumber

1.2. package.jsonの編集

package.json
{
  "name": "webpack_test",
  "version": "1.0.0",
  "description": "",
- "main": "index.js",
  "scripts": {
-   "test": "echo \"Error: no test specified\" && exit 1"
    // sassも監視対象
+   "build:dev": "webpack --mode development --watch",
+   "build": "webpack --mode production",
    // dev-serverだけだとjsの監視はされるが、バンドルはされない
    // 保存すると自動リロードがかかるため更新はされるが、監視よりも遅いため二回目リロード時にバンドルが反映される
+   "start": "webpack --mode development --watch & webpack-dev-server",
  },
    "scripts": {
    },
+ "private": true,
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "jquery": "^3.4.1"
  },
  "devDependencies": {
    "@babel/core": "^7.4.5",
    "@babel/polyfill": "^7.4.4",
    "@babel/preset-env": "^7.4.5",
    "autoprefixer": "^9.6.0",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.0.0",
    "mini-css-extract-plugin": "^0.7.0",
    "node-sass": "^4.12.0",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "terser-webpack-plugin": "^1.3.0",
    "webpack": "^4.35.2",
    "webpack-cli": "^3.3.5",
    "webpack-dev-server": "^3.7.2"
  }
}

1.2. webpack.config.jsの作成

webpackをインストールしたらwebpack.config.jsを手動で作成

webpack.config.js
// 開発or本番モードの選択(development、production、noneのいずれか設定必須)
// development: 開発時のファイル出力のモード(最適化より時間短縮,エラー表示などを優先)
// production: 本番時のファイル出力のモード(最適化されて出力される)
const MODE = "development";
// ソースマップの利用有無(productionのときはソースマップを利用しない)
const enabledSourceMap = MODE === "development";

// ファイル出力時の絶対パス指定に使用
const path = require('path');

// プラグイン
// js最適化
const TerserPlugin = require('terser-webpack-plugin');
// css最適化
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
// css抽出
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// jQueryで使用
const webpack = require('webpack');


module.exports = {
  // エントリーポイント(メインのjsファイル)
  entry: './src/js/app.js',
  // ファイルの出力設定
  output: {
    // 出力先(絶対パスでの指定必須)
    path: path.resolve(__dirname, 'dist/js'),
    // 出力ファイル名
    filename: "bundle.js"
  },
  mode: MODE,
  // ソースマップ有効
  devtool: 'source-map',
  // ローダーの設定
  module: {
    rules: [
      {
        // ローダーの対象 // 拡張子 .js の場合
        test: /\.js$/,
        // ローダーの処理対象から外すディレクトリ
        exclude: /node_modules/,
        use: [
          {
            // Babel を利用する
            loader: "babel-loader",
            // Babel のオプションを指定する
            options: {
              presets: [
                // プリセットを指定することで、ES2019 を ES5 に変換
                "@babel/preset-env"
              ]
            }
          }
        ]
      },
      {
        // ローダーの対象 // 拡張子 .scss の場合
        test: /\.scss/,
        // Sassファイルの読み込みとコンパイル
        use: [
          // linkタグに出力する機能
          "style-loader",
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              // css出力先を指定
              publicPath: path.resolve(__dirname, 'dist/css'),
              // developmentのときのみ有効
              hmr: process.env.NODE_ENV === 'development',
            },
          },
          // CSSをバンドルするための機能
          {
            loader: "css-loader",
            options: {
              // CSS内のurl()メソッドの取り込みを禁止する
              // 画像ファイルをbase64でエンコードするとかでは無い限り、必要なし
              url: false,
              // ソースマップの利用有無
              sourceMap: enabledSourceMap,
              // Sass+PostCSSの場合は2を指定
              // 0 => no loaders (default);
              // 1 => postcss-loader;
              // 2 => postcss-loader, sass-loader
              importLoaders: 2
            }
          },
          // PostCSS(Autoprefixer)のための設定
          // ベンダープレフィックスを追加するためのPostCSS用プラグイン
          {
            loader: "postcss-loader",
            options: {
              // PostCSS側でもソースマップを有効にする
              sourceMap: enabledSourceMap,
              plugins: [
                // Autoprefixerを有効化
                require("autoprefixer")({
                  grid: true
                })
              ]
            }
          },
          // Sassをバンドルするための機能
          {
            loader: "sass-loader",
            options: {
              // ソースマップの利用有無
              sourceMap: enabledSourceMap
            }
          }
        ]
      }
    ]
  },
  // mode:puroductionでビルドした場合のファイル圧縮
  optimization: {
    minimizer: production
      ? []
      : [
        // jsファイルの最適化
        new TerserPlugin({
          // すべてのコメント削除
          extractComments: 'all',
          // console.logの出力除去
          terserOptions: {
            compress: { drop_console: true }
          },
        }),
        // 抽出したcssファイルの最適化
        new OptimizeCssAssetsPlugin({})
      ]
  },
  // js, css, html更新時自動的にブラウザをリロード
  devServer: {
    // サーバーの起点ディレクトリ
    // contentBase: "dist",
    // バンドルされるファイルの監視 // パスがサーバー起点と異なる場合に設定
    publicPath: '/dist/js/',
    //コンテンツの変更監視をする
    watchContentBase: true,
    // 実行時(サーバー起動時)ブラウザ自動起動
    open: true,
    // 自動で指定したページを開く
    openPage: "index.html",
    // 同一network内からのアクセス可能に
    host: "0.0.0.0"
  },
  plugins: [
    // 抽出したCSSファイル最適化
    new OptimizeCssAssetsPlugin({
      assetNameRegExp: /\.optimize\.css$/g,
      cssProcessor: require('cssnano'),
      cssProcessorPluginOptions: {
        preset: ['default', { discardComments: { removeAll: true } }],
      },
      canPrint: true
    }),
    // ビルドされたjsファイルからstyleの部分を抽出してcssファイルで出力
    // jsファイルからcssの内容消える
    // v3でのextract-text-pluginのようなもの
    new MiniCssExtractPlugin({
      // 出力先ファイル名 // prefix は output.path
      filename: "../css/[name].css",
      chunkFilename: "[id].css"
    }),
    new webpack.ProvidePlugin({
      $: 'jquery'
    })
  ]
};

2. webpackの処理対象となるjsファイルの書き方

エントリーポイントとなるjsファイルをapp.jsモジュールファイルをmodule1.jsとすると次のように書く

app.js
import "../scss/style.scss";
import jQuery from 'jquery';
import "@babel/polyfill";
import { モジュールから呼び出す関数 } from './modules/module1';
module.js
// 関数を定義
index.html
<!DOCTYPE html>
<html lang="ja" dir="ltr">
<head>
  <meta charset="utf-8">
  <title>webpack_sample</title>
  <link rel="stylesheet" href="./dist/css/main.css">
</head>
<body> 
  // 
  <script src="./dist/js/bundle.js"></script>
</body>

</html>

3. webpack実行

実行コマンド

package.jsonscriptsで設定した名称で実行

$ npm run build:dev  # バンドル(開発用)実行
$ npm run build  # バンドル(本番用)実行
$ npm run start  # バンドルと監視スタート
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webpack + Vue(webpack.config.js書き方の例)

webpack + Vue

自分用メモ

はじめに

webpackはモジュールバンドラーである。エントリーポイントに設定したjsファイルを中心に、各モジュールのjsファイルを纏めてバンドル

  • モジュール管理できるため機能ごとに開発ができ保守性が上がる
  • 複数のjsファイル(cssや画像も)を一つのファイルにbundleできるためリクエスト数減少とパフォーマンス向上 ※ ファイルサイズの増大によるパフォーマンス低下もありうる
  • 依存関係の解消

1. webpack使用の下準備

1.1. webpackでの基本的なパッケージのインストール

$ npm init -y
$ npm i -S vue

# .vueファイルを読み込むために必須
$ npm i -D vue-loader vue-template-compiler

$ npm i -D webpack webpack-cli 
$ npm i -D terser-webpack-plugin optimize-css-assets-webpack-plugin

# .vueファイルではCSSも扱うので必須
$ npm i -D node-sass css-loader
$ npm i -D babel-loader @babel/core @babel/preset-env 

# 分割したcssの読み込みをリロードしない(brouser-sinc代用)
$ npm i -D webpack-dev-server

# gulp使うとき
$ npm i -D gulp gulp-eslint gulp-notify gulp-plumber

1.2. package.jsonの編集

package.json
{
  "name": "webpack_test",
  "version": "1.0.0",
  "description": "",
- "main": "index.js",
  "scripts": {
-   "test": "echo \"Error: no test specified\" && exit 1"
    // sassも監視対象
+   "build:dev": "webpack --mode development --watch",
+   "build": "webpack --mode production",
    // dev-serverだけだとjsの監視はされるが、バンドルはされない
    // 保存すると自動リロードがかかるため更新はされるが、監視よりも遅いため二回目リロード時にバンドルが反映される
+   "start": "webpack --mode development --watch & webpack-dev-server",
  },
    "scripts": {
    },
+ "private": true,
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "jquery": "^3.4.1"
  },
  "devDependencies": {
    "@babel/core": "^7.4.5",
    "@babel/polyfill": "^7.4.4",
    "@babel/preset-env": "^7.4.5",
    "autoprefixer": "^9.6.0",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.0.0",
    "mini-css-extract-plugin": "^0.7.0",
    "node-sass": "^4.12.0",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "terser-webpack-plugin": "^1.3.0",
    "webpack": "^4.35.2",
    "webpack-cli": "^3.3.5",
    "webpack-dev-server": "^3.7.2"
  }
}

1.2. webpack.config.jsの作成

webpackをインストールしたらwebpack.config.jsを手動で作成

webpack.config.js
// 開発or本番モードの選択(development、production、noneのいずれか設定必須)
// development: 開発時のファイル出力のモード(最適化より時間短縮,エラー表示などを優先)
// production: 本番時のファイル出力のモード(最適化されて出力される)
const MODE = "development";
// ソースマップの利用有無(productionのときはソースマップを利用しない)
const enabledSourceMap = MODE === "development";

// ファイル出力時の絶対パス指定に使用
const path = require('path');

// プラグイン
// js最適化
const TerserPlugin = require('terser-webpack-plugin');


module.exports = {
  // エントリーポイント(メインのjsファイル)
  entry: './src/js/app.js',
  // ファイルの出力設定
  output: {
    // 出力先(絶対パスでの指定必須)
    path: path.resolve(__dirname, 'dist/js'),
    // 出力ファイル名
    filename: "bundle.js"
  },
  mode: MODE,
  // ソースマップ有効
  devtool: 'source-map',
  // ローダーの設定
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["vue-style-loader", "css-loader"]
      },
      {
        test: /\.vue$/,
        loader: "vue-loader"
      },
      {
        // ローダーの対象 // 拡張子 .js の場合
        test: /\.js$/,
        // ローダーの処理対象から外すディレクトリ
        exclude: /node_modules/,
        // Babel を利用する
        loader: "babel-loader",
        // Babel のオプションを指定する
        options: {
          presets: [
            // プリセットを指定することで、ES2019 を ES5 に変換
            "@babel/preset-env"
          ]
        }
      }
    ]
  },
  // import 文で .ts ファイルを解決するため
  resolve: {
    // Webpackで利用するときの設定
    alias: {
      vue$: "vue/dist/vue.esm.js"
    },
    extensions: ["*", ".js", ".vue", ".json"]
  },
  plugins: [
    // Vueを読み込めるようにするため
    new VueLoaderPlugin()
  ],
  // mode:puroductionでビルドした場合のファイル圧縮
  optimization: {
    minimizer: production
      ? []
      : [
        // jsファイルの最適化
        new TerserPlugin({
          // すべてのコメント削除
          extractComments: 'all',
          // console.logの出力除去
          terserOptions: {
            compress: { drop_console: true }
          },
        }),
      ]
  },
  // js, css, html更新時自動的にブラウザをリロード
  devServer: {
    // サーバーの起点ディレクトリ
    // contentBase: "dist",
    // バンドルされるファイルの監視 // パスがサーバー起点と異なる場合に設定
    publicPath: '/dist/js/',
    //コンテンツの変更監視をする
    watchContentBase: true,
    // 実行時(サーバー起動時)ブラウザ自動起動
    open: true,
    // 自動で指定したページを開く
    openPage: "index.html",
    // 同一network内からのアクセス可能に
    host: "0.0.0.0"
  }
};

2. webpackの処理対象となるjsファイルの書き方

エントリーポイントとなるjsファイルをapp.jsモジュールファイルをmodule1.jsとすると次のように書く

jsファイル内でVue.component()などのメソッドで作る場合と、.vue拡張子の単一ファイルコンポーネントで作る場合がある
以下はコンポーネントごとに.vue拡張子で作るときである

app.js
import Vue from 'vue';
// コンポーネントファイルがある場合
import App from './components/App.vue';
./components/App.vue
// 関数を定義。
index.html
<!DOCTYPE html>
<html lang="ja" dir="ltr">
<head>
  <meta charset="utf-8">
  <title>webpack_sample</title>
  <link rel="stylesheet" href="./dist//css/main.css">
</head>
<body> 
  // 
  <script src="./dist/js/bundle.js"></script>
</body>

</html>

3. webpack実行

実行コマンド

package.jsonscriptsで設定した名称で実行

$ npm run build:dev  # バンドル(開発用)実行
$ npm run build  # バンドル(本番用)実行
$ npm run start  # バンドルと監視スタート
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsの基礎

Vue.jsの基本的な使い方

  • Reactは単方向データバインディングで一方通行なのに対し、Vueは双方向データバインディングで双方向にデータを流す仕組みになっている。
    • 双方向データバインディングの場合はjsの中身が変わったらすぐに反映される
    • 単方向データバインディングの場合はrenderなどの描画の処理が単方向であるため即時反映とはならない

環境構築 ---未完成

  • vue.esn.jsというtemplate機能のないランタイム限定ビルドがあるので、これも必要に応じてnpmでインストールする

基本的な書き方

  • new Vueでインスタンス生成
  • elでセレクターを書き込み、スコープを指定する
  • dataプロパティはオブジェクトの形で自分が使いたいプロパティを定義する
    • データオブジェクトの中の値を出し入れするための入れ物のようなもの
  • dataで定義したプロパティはhtmlで使うことができる

ファイルのベースづくり

app.js
import Vue from 'vue'

new Vue({
  el: '#app1',
  data: {
    message: 'vueのテンプレートの構文。{{}}で囲って処理がかける'
  }
})

htmlの方では{{}}の中でテンプレートの構文やif文など、またインスタンス化したdataのプロパティなども使える

index.html
<div id="app1">
  {{ message }}
</div>

v-bindを使った属性のバインド

  • 単方向のデータバインディング
  • dataの変更に応じて表示されるが、HTML側の入力でdataが変更されることはない
  • v-bind:とすることでインスタンスで定義したdataのプロパティが属性として出力される
  • v-bind:は省略可能で、:のみで書くことができる
  • class属性を渡すときは連想配列の形で渡す
  • その連想配列の値がtruefalseかでプロパティ名が付くどうかが判別される
  • この例の場合、activetext-dangertrueなのでclass="active text-dangerとして入ってくる

text-dangerのように-を使っている場合''または""で囲む必要がある

app.js
import Vue from 'vue'

new Vue({
  el: '#app2',
  data: {
    message: 'このページをロードしたのは ' + new Date().toLocalString(),
    classObject: {
      active: true,
      'text-danger': true
    }
  }
})
index.html
<div id="app2">
<!-- title属性は補足的な情報を与えるときに使用し、ポインタを重ねると吹き出し(ツールチップ)が表示される -->
  <span v-bind:title="message" :class="classObject">
    この文字にロードした日付が表示される
  </span>
</div>

v-if, v-else-if, v-elseを使った条件分岐で表示非表示

v-ifのみでの条件分岐

app.js
import Vue from 'vue';

new Vue({
  el: '#app3',
  data: {
    isShow: true
  }
})
  • isShowの名称は任意
  • v-ifの中でtruefalseかを判定し、trueであれば、そのタグ自体を表示し、falseであれば、そのタグ自体を非表示にする ※ 非表示の場合DOM自体がなくなる(DOMの残るdisplay:noneとは異なる)
htmlindex.html
<div id="app3">
  <span v-if="isShow">v-ifを使ったDOMの表示非表示</span>
</div>

v-if, v-else-if, v-elseを使った条件分岐

app.js
new Vue({
  el: '#app3',
  data: {
    isShow: 'a'
  }
})
index.html
<div id="app3">
  <span v-if=" isShow === 'a' ">v-ifを使った条件分岐</span>
  <span v-else-if=" isShow === 'b' ">v-ifと同階層に書く</span>
  <span v-else>v-ifと同階層に書く</span>
</div>

v-showを使った要素の表示非表示

  • v-showによる要素は常に描画され、v-ifと異なりDOMを維持する
  • styleだけ非表示になっているだけ(display: none;)
  • v-showtrueなら表示、falseなら非表示(display:none;)
app.js
new Vue({
  el: '#app3',
  data: {
    isShow: true
  }
})
index.html
<div id="app3">
  <span v-show="isShow">v-showで表示</span>
  <span v-show="!isShow">v-showで非表示に</span>
</div>

v-forを使ったループ処理

v-forの値に変数名を割り当てる(今回はtodo)
todo in todosとすることでtodoにjsファイルで定義したtodosの一つ一つの値のデータが入る
dataプロパティの値を配列リテラルの形式で渡すことで、配列一つ一つを回せる

app.js
import Vue from 'vue';

new Vue({
  el: '#app4',
  data: {
    todos: [
      {text: 'v-forで'},
      {text: 'htmlを'},
      {text: 'ループ生成'}
    ]
  }
})
index.html
<div id="app4">
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
</div>

<!-- 出力結果 -->
<ol>
  <li>v-forで</li>
  <li>htmlを</li>
  <li>ループ生成</li>
</ol>

v-onを使ったイベント発火

v-on:のあとにイベント名を指定し、イベント後の発火したいメソッドを指定
v-on:は省略可能で、@のみで書くことができる

  • vueではmethodsプロパティを定義でき、viewの中で使いたい関数を定義する
    • methodsは関数の置き場所として用いられる
    • methodsは関数なので、呼べばそのまま実行される
    • methodsで算出された値はキャッシュされない
  • changeMessageという関数名は任意
  • methodsプロパティ内でthisを使うとdataプロパティにアクセスできる

this.messagemessageにアクセスできる

app.js
import Vue from 'vue';

new Vue({
  el: '#app5',
  data: {
    message: 'Hello Vue.js'
  },
  methods: {
    changeMessage: function() {
      this.message = this.message + '変更しました'
    }
  }
})
index.html
<div id="app5">
  <p>{{ message }}</p>
  <button v-on:click="changeMessage">メッセージを変える</button>
</div>

<!-- 出力結果 -->
メッセージを変える
  <!-- クリック -->
  Hello Vue.js変更しました

v-onでtoggle

v-onでは、必ずしもmethodを使わなければいけないというわけではない
以下はtoggle処理の例である

<div id="app5">
  <button v-on:click="show = !show">
    Toggle
  </button>
</div>

v-model 双方向データバインディング

  • 双方向のデータバインディング
  • dataの変更に応じて表示され、HTMLでフォームの入力を行った際にもdataの値の変更が可能
  • input要素やtextarea要素、select要素に双方向 (two-way) データバインディングを作成
  • v-modeldataのプロパティを指定すると、jsファイルのプロパティと描画が連携される
  • 双方向データバインディングでは入力フォームの値が変わると描画も変わるため、pタグの中身もすぐに反映される
  • 下の例ではinputタグの中にmessageの値がvalueの形で入る
  • ユーザーの入力データが自動で更新される
app.js
import Vue from 'vue';

new Vue({
  el: '#app6',
  data: {
    message: '双方向データバインディング'
  }
})
index.html
<div id="app6">
  <p>{{ message }}</p>
  <input type="text" v-model="message">
</div> 

computed 算出プロパティ

  • computedは関数の処理を書く算出のプロパティである
  • methodsと似ているが、computeddataプロパティの変更に依存して描画に反映される
  • dataやcomputedの値が変化することで自動的に実行
  • computedはjsの方で算出された値が常に結果がキャッシュされており再利用できる。dataの変更(thisの変更)をthisで監視しているため、dataプロパティに変更があった場合のみ描画に反映される
    • thisを持たないcomputeddataの変更を参照できず、ずっと初回のキャッシュを表示し続ける
  • methodsは再描画されるたびに処理が実行される
app.js
import Vue from 'vue';

new Vue({
  el: '#app7',
  data: {
    isShow: true
  }
  computed: {
    showString: function() {
      return (this.isShow) ? Date.now() : 'isShowはfalse'
    },
    showString2() {
      return Date.now()
    }
  },
  methods: {
    showStringMethods() {
      return (this.isShow) ? Date.now() : 'isShowはfalse'
    },
    showStringMethods2() {
      return Date.now()
    }
  }
})

computedの場合は()は不要

index.html
<div id="app7">
  <input type="checkbox" v-model='isShow'>
  <p>isShow: {{ isShow }}</p>
  <p>showString: {{ showString }}</p>
  <p>showString2: {{ showString2 }}</p>
  <p>showStringMethods: {{ showStringMethods() }}</p>
  <p>showStringMethods2: {{ showStringMethods2() }}</p>
</div>

<!-- 出力結果 -->
isShow: true
showString: 123456789
showString2: 123456789
showStringMethods: 123456789
showStringMethods2: 123456789
  <!-- checkbox click -->
  isShow: true
  <!-- 関数内でthisをを持つため、dataの変更を監視して反映 -->
  showString: isShowはfalse 
  <!-- dataが変更されてもthisがないので反映されず、初回のキャッシュ内容保持 -->
  showString12: 123456789
  <!-- methodsはdataに依存せず毎回再描画がされるため更新される -->
  showStringMethods: isShowはfalse 
  showStringMethods2: 234567890

v-htmlでサニタイズを無効化

vue.jsでは自動でサニタイズされ、タグは文字列として読み込まれる
タグとして読み込ませたい場合はサニタイズを無効化するv-htmlを使う

app.js
import Vue from 'vue';

new Vue({
  el: '#app10',
  data: {
    script: '<p style="color:red">タグとして表示</p>'
  }
})
index.html
<div id="app10">
  <p>{{ script }}</p>
  <p v-html="script"></p>
</div>

<!-- 出力結果 -->
<p style="color:red">タグとして表示</p>
タグとして表示  (color:red; 適用)

トランジションとアニメーション

アニメーションにはEnterアニメーションとLeaveアニメーションがある

  • Enter: 非表示から表示に変わるときのアニメーション
    • 変化前の状態のclassはv-enter, 変化後のclassはv-enter-to
    • 変化過程にアニメーションを設定したいときのclassはv-enter-active
  • Leave: 表示から非表示に変わるときのアニメーション
    • 変化前の状態のclassはv-leave, 変化後のclassはv-leave-to
    • 変化過程にアニメーションを設定したいときのclassはv-leave-active

transitionタグでname属性を付けた場合はv-の箇所をその名前に変更できる

app.js
import Vue from 'vue';

new Vue({
  el: '#app11',
  data: {
    show: true
  }
})
index.html
<style>
  .fade-enter-active, .fade-leave-active {
    transition: opacity .5s;
  }
  .fade-enter, .fade-leave-to { /* .fade-leave-active below version 2.1.8 */
    opacity: 0;
  }
</style>

<div id="app11">
  <button v-on:click="show = !show">
    Toggle
  </button>
  <transition name="fade">
    <p v-if="show">hello</p>
  </transition>
</div>

コンポーネントの使い方

コンポーネントとは名前付きの再利用可能なVueインスタンスである(部品のようなもの)

  • Vue.componentでコンポーネントの登録

    • 第一引数にコンポーネント名を指定する、第二引数にコンポーネントに定義したい値(data, templateなど)を設定する
    • コンポーネントでdataを定義するときは関数で定義する ※ 関数ではなくオブジェクトの形にすると複数のコンポーネントで値を共有する形になる。関数にすることで、個々のコンポーネントに値をもたせることができる
  • コンポーネント登録後にnew Vueでインスタンス化する

app.js
// 登録する
Vue.component('sample-component' {
    template: '<div>this is sample</div>'
})

// インスタンス生成
new Vue({
    el: '#app'
})

コンポーネントを挿入したいところに呼び出すコンポーネントのタグを記述し、app.jsを読み込む

sample.html
<div id="app">
    <sample-component></sample-component>
</div>

<script src="app.js"></script>

HTMLが置換され、次のようになる

<div id="app">
    <div>this is sample</div>
</div>

templateについて

temaplateはコンポーネントのHTMLを渡す
htmlファイルでtempalteのタグが挿入される

app.js
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">clicked {{ count }} times</button>'
})
new Vue({el: '#app12'})

コンポーネント名button-counterにコンポーネントが割り当てられ、templateのHTMLが挿入される

index.html
<div id="app12">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

コンポーネントの登録を省略

slotを使用していなければ下記のように省略できる

// 省略なし
<template>
    <div id="app">
        <app-header></app-header>
    </div>
</template>


// 省略あり
<temaplte>
    <div id="app">
        <app-header />
    </div>
</template>

単一ファイルコンポーネントを使うとき

app.js
// 登録する
Vue.component('sample-component', require('./components/SampleComponent.vue').default);

// インスタンス生成
new Vue({
    el: '#app'
})
./components/SampleComponent.vue
// template作成
<template>
    //
</template>

// このtemplateを使ったときの共通の処理を追記する
<script>
export default {
    data: {
        //
    },
    mounted() {
        //
    }
}
</script>

propsで親コンポーネントから受け継ぐ

  • propsを使うことでコンポーネントの親から子へ値を受け渡せる
  • propsは配列の形式で属性を定義し、親コンポーネントからその属性値を受け継いでdataのプロパティのように振る舞う
app.js
Vue.component('blog-post', {
  props: ['title'],
  template: '<h3>{{ title }}</h3>'
})
new Vue({el: '#app13'})
index.html
<div id="app13">
  <!-- 親コンポーネント(コンポーネントの受け渡し先となる生成元のタグのこと) -->
  <!-- このタグを親コンポーネントといい、作成したコンポーネントを子コンポーネントという -->
  <blog-post title="title属性をpropsで渡せる"></blog-post>
  <blog-post title="title属性をpropsで渡せる"></blog-post>
  <blog-post title="title属性をpropsで渡せる"></blog-post>
</div>

オブジェクトでない配列を指定する場合

  • 子が親からデータを受け取るとき、配列で指定するか、オブジェクトの配列として指定するかの2通りある
  • 簡潔に列挙できるが、データの情報が記述できず、itemもvalueも同様に扱われる
parent.vue
<child
    :param0="item",
    :param1="value"
>
child.vue
props: [
    'item',
    'value'
]

オブジェクトの配列として受け取る場合

  • 詳細に記述できるため、違いを明記できる
child.vue
props: {
    item: {
        type: Object
    },
    value: {
        type: Number,
        default: 0
    }
}

emitで子から親へデータを伝達

  • 子から親へのデータの伝搬では$emitと$onを一対として使用するカスタムイベントを活用する
  • 親コンポーネントからこのイベントを監視する場合はv-on:, @でイベント名を紐付けて使用する

子コンポーネント

child.vue
<template>
    <div class="item">
        <span>{{ value }}</span>
        <button @click="updateEvt">追加</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            value: 0
        }
    },
    methods: {
        plus0ne() {
            this.value += 1
            // $emitの第2, 第3...引数で変数を指定するとその変数を親が受け取ることができる
            this.$emit('update', this.value)
        }
    }
}
</script>

親コンポーネント

parent.vue
<template>
    <counter @updated="updateEvt" />
</template>

<script>
import Child from './Child.vue'
export default {
    components: {
        Child
    },
    methods: {
        // 子から変数を受けとることができる
        updateEvt(value) {
            console.log('The value updated' + value)
        }
    }
}
</script>

イベントとメッセージを親コンポーネントに渡す方法

  • v-on:click=$emit('カスタムイベント名')のように$emitで任意のカスタムイベント名を指定することで、そのイベントをイベント名として親子コンポーネントで通知することができる

    • この例ではv-on:カスタムイベント名=func()とするとclickイベントが通知される
  • templateはバッククォートで囲むことで階層構造で文字列を書くことができる(ES6より)

app.js
Vue.component('blog-post', {
  props: ['post'],
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <button v-on:click="$emit('enlarge-text')">
        Enlarge text
      </button>
      <div v-thml="post.content"></div>
    </div>
  `
})
new Vue({
  el: '#app14',
  data: {
    posts: [
      {
        id: 1,
        title: 'sample post1',
        content: '<p>サンプル投稿のコンテント</p>'
      },
      {
        id: 2,
        title: 'sample post2',
        content: '<p>サンプル投稿のコンテント</p>'
      },
      {
        id: 3,
        title: 'sample post3',
        content: '<p>サンプル投稿のコンテント</p>'
      },

    ]
  }
})
index.html
<div id="app14">
  <div id="blog-posts-events-demo">
    <div :style="{ fontSize: postFontSize + 'em' }">
      <blog-post
        v-for="post in posts"
        v-on:enlarge-text="fontSizeScale()"
        v-bind:key="post.id"
        v-bind:post="post"
      ></blog-post>
    </div>
  </div>
</div>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsでGoogle Slidesの新規スライド作成

Google Slides APIをNode.jsから触ってみてます。

の記事の続きです。

Google Slideのcreateを試す

スライドの新規作成が出来そうな雰囲気です。

presentations.createのドキュメントを覗くとスライド作成できそうな雰囲気がありました。

準備

Node.jsでGoogle Slides APIを触ってみるでAPIへのアクセスなどを出来るようにしましょう。

ソースコード

前回記事のapp.jsの中身を書き換えます。

  • SCOPESを変更
app.js
const SCOPES = ['https://www.googleapis.com/auth/presentations'];

元々は'https://www.googleapis.com/auth/presentations.readonly'と書いてあって読み込みのみ許可になってました。

  • listSlides(auth)の中身を以下に書き換え
app.js
省略

function listSlides(auth) {
    const slides = google.slides({version: 'v1', auth});
    slides.presentations.create({
        presentationId: '',
    }, (err, res) => {
        if (err) return console.log('The API returned an error: ' + err);
        console.log(res.data);
    });
}

tokenを再発行

token.jsonは読み込みのみ許可のトークンで作成されていたので、一度token.jsonのファイルを削除します。

再度node app.jsで実行すると、URLが発行されて、ブラウザでアクセスするとこのようにアクセス許可を求められます。

スクリーンショット 2020-02-08 16.41.22.png

許可をして、発行されるコードをターミナル側に貼り付けて実行するとtoken.jsonが再発行されて実行できます。

新規スライドが出来た!

実行してみます。

$ node app.js
{
  presentationId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  pageSize: {
    width: { magnitude: 9144000, unit: 'EMU' },
    height: { magnitude: 5143500, unit: 'EMU' }
  },
  slides: [
    {
      objectId: 'p',
      pageElements: [Array],
      slideProperties: [Object],
      pageProperties: [Object]
    }
  ],
  title: '無題のプレゼンテーション',
  masters: [
    {
      objectId: 'simple-light-2',
      pageType: 'MASTER',
      pageElements: [Array],
      pageProperties: [Object],
      masterProperties: [Object]
    }
  ],

省略

Google Drive上で確認してみると無題のプレゼンテーションが作成されてました。

スクリーンショット 2020-02-08 16.43.15.png

所感

とりあえず、Node.jsからスライドの新規作成ができました。

タイトル指定などをして作ることも出来そうなので引き続き探っていきます。

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

Node.jsでGoogle Slides内のテキストを取得してみる

Node.jsでGoogle Slides APIを触ってみるの続きです。

準備

前回の記事を参照して、スライド情報にNode.jsからアクセス出来るようにしましょう。

適当なスライドを用意する

こちらを用意してみました。

https://docs.google.com/presentation/d/1ziVnaFocZ_YF_cuXyXF5PUKGoE62eX-XlnOEslPkKUc/edit#slide=id.p

https://docs.google.com/presentation/d/<ここがプレゼンテーションID>/edit#slide=id.pになるのでこのスライドのプレゼンテーションIDは1ziVnaFocZ_YF_cuXyXF5PUKGoE62eX-XlnOEslPkKUcになります。

Node.jsでGoogle Slidesのテキストを抽出

前回のコードからプレゼンテーションIDの箇所とfunction listSlides(auth)の中身を書き換えてます。

app.js
const fs = require('fs');
const readline = require('readline');
const {google} = require('googleapis');

//変更箇所: プレゼンテーションID - 試すときは自分のスライドでもやってみましょう
const presentationId = `1ziVnaFocZ_YF_cuXyXF5PUKGoE62eX-XlnOEslPkKUc`;

// If modifying these scopes, delete token.json.
const SCOPES = ['https://www.googleapis.com/auth/presentations.readonly'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = 'token.json';

// Load client secrets from a local file.
fs.readFile('credentials.json', (err, content) => {
  if (err) return console.log('Error loading client secret file:', err);
  // Authorize a client with credentials, then call the Google Slides API.
  authorize(JSON.parse(content), listSlides);
});

/**
 * Create an OAuth2 client with the given credentials, and then execute the
 * given callback function.
 * @param {Object} credentials The authorization client credentials.
 * @param {function} callback The callback to call with the authorized client.
 */
function authorize(credentials, callback) {
  const {client_secret, client_id, redirect_uris} = credentials.installed;
  const oAuth2Client = new google.auth.OAuth2(
      client_id, client_secret, redirect_uris[0]);

  // Check if we have previously stored a token.
  fs.readFile(TOKEN_PATH, (err, token) => {
    if (err) return getNewToken(oAuth2Client, callback);
    oAuth2Client.setCredentials(JSON.parse(token));
    callback(oAuth2Client);
  });
}

/**
 * Get and store new token after prompting for user authorization, and then
 * execute the given callback with the authorized OAuth2 client.
 * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
 * @param {getEventsCallback} callback The callback for the authorized client.
 */
function getNewToken(oAuth2Client, callback) {
  const authUrl = oAuth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: SCOPES,
  });
  console.log('Authorize this app by visiting this url:', authUrl);
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
  rl.question('Enter the code from that page here: ', (code) => {
    rl.close();
    oAuth2Client.getToken(code, (err, token) => {
      if (err) return console.error('Error retrieving access token', err);
      oAuth2Client.setCredentials(token);
      // Store the token to disk for later program executions
      fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
        if (err) return console.error(err);
        console.log('Token stored to', TOKEN_PATH);
      });
      callback(oAuth2Client);
    });
  });
}

/**
 * Prints the number of slides and elements in a sample presentation:
 * https://docs.google.com/presentation/d/1EAYk18WDjIG-zp_0vLm3CsfQh_i8eXc67Jo2O9C6Vuc/edit
 * @param {google.auth.OAuth2} auth The authenticated Google OAuth client.
 * 
 */
function listSlides(auth) {
  const slides = google.slides({version: 'v1', auth});
  slides.presentations.get({
    presentationId: presentationId,
  }, (err, res) => {
    if (err) return console.log('The API returned an error: ' + err);

    /* 変更箇所: スライド中のテキストを抽出するテスト */
    const firstPage = res.data.slides[0]; //1スライド目
    const firstBox = firstPage.pageElements[0]; //最初の要素
    console.log(firstBox.shape.text.textElements[1].textRun.content); //中身のテキスト確認
  });
}

素でアクセすると

res.data.slides[0].pageElements[0].shape.text.textElements[1].textRun.content

みたいな感じで超深い階層になるみたいです。

  • res.data.slides: スライドXXページごとの配列 今回はタイトルスライドから抜き出すので0指定
  • firstPage.pageElements: 対象ページのオブジェクト(テキストボックスなど)の配列 今回はページの最初の部ジェクトなので0指定

実行

$ node app.js 
Node.jsてすと

ちゃんとテキストがとれましたね。

所感

返ってくるオブジェクトが結構複雑な印象です。

詳細はslides周りのSDKを覗くのが良さそうだけどどんなオブジェクトが返ってくるかはまだ見つけられてないのでconsole.logで探していったほうが早いかも

追記: 今回使ってるpresentations.getのAPIリファレンスはここっぽい

https://developers.google.com/slides/reference/rest/v1/presentations/get

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

Nuxtでvue-carouselを使用していたら'ReferenceError: window is not defined'が出たお話

環境

nuxt 2.4.0
vue-carousel 0.18.0

現象

nuxtにvue-caroucelを実装したが、下記のようなエラーになり
リロード時に画面がずっと読み込み状態になってしまった。

ReferenceError: window is not defined

どうやたSSR時に、documentwindowにアクセスすると上記のエラーが発生するみたい

参考サイト

実装方法

~plugin/vue-caroucel.js
import Vue from 'vue'
import VueCarousel from 'vue-carousel'

Vue.use(VueCarousel)
nuxt.config.js
plugins: [
        { src: '~/plugins/vue-carousel', mode: 'client' }
    ],
index.vue
<template>
  <div class="container">
    <carousel :navigation-enabled="true" :per-page="1">
      <slide v-for="(url, key) in images" :key="key">
        <div class="product-img">
          <img class="product-card-img" :src="url" />
        </div>
      </slide>
    </carousel>
  </div>
</template>

<script>
import { Carousel, Slide } from 'vue-carousel'
export default {
  layout: 'client/simple',
  components: {
    Carousel,
    Slide
  },
  data() {
    return {
      hasError: false,
      images: [
        require('@/assets/img/NoImage.png'),
        require('@/assets/img/NoImage.png'),
        require('@/assets/img/NoImage.png'),
        require('@/assets/img/NoImage.png'),
        require('@/assets/img/NoImage.png'),
        require('@/assets/img/NoImage.png')
      ]
    }
  },
  methods: {
    async login() {}
  }
}
</script>

解決方法

各画面でのインポートのを下記のように書き換える

- import { Carousel, Slide } from 'vue-carousel'

+ import Carousel from 'vue-carousel/src/Carousel.vue'
+ import Slide from 'vue-carousel/src/Slide.vue'

まとめ

vueのモジュールをNuxtで使用するには色々とトラブルがあってなかなか大変です。。。
SSRについてしっかり理解して進めないと色々ハマっていきそうだと思いました。

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

Node.jsでGoogle Slides APIを触ってみる

Google SlidesのAPIをNode.jsで触ってみます。

公式チュートリアルになぞりつつ試したメモ
です。

準備

Node.jsのバージョンは13.7.0です。

  • 作業フォルダ作成
$ mkdir slidesapitest
$ cd slidesapitest
  • 利用モジュールをインストール
$ npm init -y
$ npm i googleapis@39
  • app.jsファイルを作成
$ touch app.js

APIをオンにして、 credentials.jsonを作成

公式チュートリアルEnable the Google Slides APIボタンを押して、APIを有効にし、credentials.jsonを作業フォルダのapp.jsと同じ階層に保存します。

ソースコード

app.jsの中身に以下をコピペ。 公式のままです。

app.js
const fs = require('fs');
const readline = require('readline');
const {google} = require('googleapis');

// If modifying these scopes, delete token.json.
const SCOPES = ['https://www.googleapis.com/auth/presentations.readonly'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = 'token.json';

// Load client secrets from a local file.
fs.readFile('credentials.json', (err, content) => {
  if (err) return console.log('Error loading client secret file:', err);
  // Authorize a client with credentials, then call the Google Slides API.
  authorize(JSON.parse(content), listSlides);
});

/**
 * Create an OAuth2 client with the given credentials, and then execute the
 * given callback function.
 * @param {Object} credentials The authorization client credentials.
 * @param {function} callback The callback to call with the authorized client.
 */
function authorize(credentials, callback) {
  const {client_secret, client_id, redirect_uris} = credentials.installed;
  const oAuth2Client = new google.auth.OAuth2(
      client_id, client_secret, redirect_uris[0]);

  // Check if we have previously stored a token.
  fs.readFile(TOKEN_PATH, (err, token) => {
    if (err) return getNewToken(oAuth2Client, callback);
    oAuth2Client.setCredentials(JSON.parse(token));
    callback(oAuth2Client);
  });
}

/**
 * Get and store new token after prompting for user authorization, and then
 * execute the given callback with the authorized OAuth2 client.
 * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
 * @param {getEventsCallback} callback The callback for the authorized client.
 */
function getNewToken(oAuth2Client, callback) {
  const authUrl = oAuth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: SCOPES,
  });
  console.log('Authorize this app by visiting this url:', authUrl);
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
  rl.question('Enter the code from that page here: ', (code) => {
    rl.close();
    oAuth2Client.getToken(code, (err, token) => {
      if (err) return console.error('Error retrieving access token', err);
      oAuth2Client.setCredentials(token);
      // Store the token to disk for later program executions
      fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
        if (err) return console.error(err);
        console.log('Token stored to', TOKEN_PATH);
      });
      callback(oAuth2Client);
    });
  });
}

/**
 * Prints the number of slides and elements in a sample presentation:
 * https://docs.google.com/presentation/d/1EAYk18WDjIG-zp_0vLm3CsfQh_i8eXc67Jo2O9C6Vuc/edit
 * @param {google.auth.OAuth2} auth The authenticated Google OAuth client.
 */
function listSlides(auth) {
  const slides = google.slides({version: 'v1', auth});
  slides.presentations.get({
    presentationId: '1EAYk18WDjIG-zp_0vLm3CsfQh_i8eXc67Jo2O9C6Vuc',
  }, (err, res) => {
    if (err) return console.log('The API returned an error: ' + err);
    const length = res.data.slides.length;
    console.log('The presentation contains %s slides:', length);
    res.data.slides.map((slide, i) => {
      console.log(`- Slide #${i + 1} contains ${slide.pageElements.length} elements.`);
    });
  });
}

アクセストークンの発行

app.jsを実行します。

$ node app.js

実行すると、こんな感じのURLが表示されます。

Authorize this app by visiting this url: https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fpresentations.readonly&response_type=code&client_id=xxxxxxxxxxxx.apps.googleusercontent.com&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob
Enter the code from that page here: <ここにコードをペースト>

クリックするとブラウザ上で以下のようなコードが表示されるのでコピーします。

Enter the code from that page here: <ここにコードをペースト>の箇所にペーストします。

成功するとtoken.jsonというファイルが保存されて、以下のように元になってるスライドの情報が表示されます。

Token stored to token.json
The presentation contains 5 slides:
- Slide #1 contains 4 elements.
- Slide #2 contains 11 elements.
- Slide #3 contains 9 elements.
- Slide #4 contains 5 elements.
- Slide #5 contains 12 elements.

ソースコードの挙動など

ソースコード上で以下のような箇所がありますが、このプレゼンテーションIDでGoogle Slidesのプレゼンを指定します。

app.js
//省略
 slides.presentations.get({
    presentationId: '1EAYk18WDjIG-zp_0vLm3CsfQh_i8eXc67Jo2O9C6Vuc',
  }
//省略

https://docs.google.com/presentation/d/1EAYk18WDjIG-zp_0vLm3CsfQh_i8eXc67Jo2O9C6Vuc/edit#slide=id.ge63a4b4_1_0

先ほど、app.jsを実行した時の表示はスライドの枚数と要素数を表示しています。

The presentation contains 5 slides:
- Slide #1 contains 4 elements.
- Slide #2 contains 11 elements.
- Slide #3 contains 9 elements.
- Slide #4 contains 5 elements.
- Slide #5 contains 12 elements.

所感

取り急ぎ、触ることが出来ました。

思ったより簡単です。

JSONをもっと深ぼっていくとテキスト情報も取れそうですね。

次回やってみます。

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

PlayCanvasでサンディちゃんを歩かせるゲームを作るぞ☆(第4回)

こんなん作ってます。

https://playcanv.as/b/iAPwWXqY/
※本解説よりも開発が進んでいることもございますのでご了承ください。

これはまだゲームではない。

サンディちゃんを操作できるところまで来ましたが、そろそろゲームっぽくしないといけません。
とりあえず、迷路を作ってみましょうか。(テキトー)
壁・スタート・ゴールの配置をJSONファイルで定義、ゲーム開始時にプログラムにファイルを読み込ませて迷路を作らせます。

迷路データ(JSON)

JSONファイルで迷路を定義します。
12×12マスの方眼紙上に正方体を置いていくのをイメージしてください。

JSONファイルには

  • ワールド座標上の壁の設置をはじめる位置
  • マスの数(縦横、今回は12×12)
  • 1マスのサイズ
  • マス上の配置(壁・スタート・ゴール・なにもなし)
  • 行列
{
    "startPosX":1,
    "startPosZ":1,
    "offsetX":-24,//マスを置き始めるワールドX座標
    "offsetZ":-24,//マスを置き始めるワールドZ座標
    "unitX":12,//マスの数(X軸方向)
    "unitZ":12,//マスの数(Z軸方向)
    "sizeX":4,//マスの大きさ(X軸方向)
    "sizeZ":4,//マスの大きさ(Z軸方向)
    "matrix":"999999999999910000000009900999999009909900000009900099999999909000000009909099099999909090099009909090000009909999009909900000900929999999999999"//行列
}

「行列」っていうのはマスの配置そのもので、もとは

999999999999
910000000009
900999999009
909900000009
900099999999
909000000009
909099099999
909090099009
909090000009
909999009909
900000900929
999999999999

という数字の並びを1行に直しただけのものです。
9が壁、1がスタート地点、2がゴール地点、0がなにもなしです。

ASSETSのsrcディレクトリ内にいくつか作っておきます。
s4_05.png

これらの設定をプログラム起動時に読み込ませて迷路を作成させます。

エディタで素材を用意する

プログラムで迷路を作成する際に使う素材をエディタで作っておきます。

templatesディレクトリを作成し、そのなかに「wall」「start」「goal」を作成しておきます。
templates.jpg

ライトグリーンの円柱がスタート地点のオブジェクト、赤のコーンがゴール、グレーの立方体は壁に使います。

templates直下のエンティティはプログラムが使うコピー元としてしか使わないので、templatesのEnabledをオフにして隠してしまいましょ。

生成プログラムを作成

RootでScriptコンポーネントを追加します。
s4_02.png

「<>Root」という表示になると思います。

次にインスペクターで「createMaze」という名前のScriptを作成します。
s4_04.png
s4_03.png

「createMaze.js」をコーディングします。

まずスクリプト属性「mazeData」を作成しましょう。

createMaze.js
CreateMaze.attributes.add('mazeData', {
    type: 'asset',
    assetType: 'json'
});

属性を作成したあとエディタ上でParseをするとmazeDataの項目が出てくるので、そこにASSETS上のJSONファイルをドラッグ&ドロップします。
s4_06.png

ここでセットされたJSONファイルがゲーム起動時にプログラムに読み込まれます。

次に迷路生成部分。

createMaze.js
// initialize code called once per entity
CreateMaze.prototype.initialize = function() {
    // JSONデータ取得
    var mazeData = this.mazeData.resource;

    // templates以下の素材(非表示)のインスタンスを取得しておく
    var templates = this.app.root.findByName('templates');
    var wall = templates.findByName('wall');
    var start = templates.findByName('start');
    var goal = templates.findByName('goal');

    //スタート位置を格納
    var startPos = new pc.Vec3();

    //マスの数だけループして素材を配置していく
    for (var z = 0; z < mazeData.unitZ; z++) {
        for (var x = 0; x < mazeData.unitX; x++) {
            switch(mazeData.matrix.charAt(mazeData.unitZ * z + x)){//if(Math.random() > 0.5){
                case "9":
                    // wallをクローンします
                    var w = wall.clone();
                    w.setPosition(
                        mazeData.offsetX + x * mazeData.sizeX + mazeData.sizeX/2, 
                        0, 
                        mazeData.offsetZ + z * mazeData.sizeZ + mazeData.sizeZ/2);
                    this.app.root.addChild(w);
                    break;
                case "2":
                    // goalをクローンします
                    var g = goal.clone();
                    g.setPosition(
                        mazeData.offsetX + x * mazeData.sizeX + mazeData.sizeX/2, 
                        0, 
                        mazeData.offsetZ + z * mazeData.sizeZ + mazeData.sizeZ/2);
                    this.app.root.addChild(g);
                    break;
                case "1":
                    // startをクローンします
                    var s = start.clone();
                    s.setPosition(
                        mazeData.offsetX + x * mazeData.sizeX + mazeData.sizeX/2, 
                        0, 
                        mazeData.offsetZ + z * mazeData.sizeZ + mazeData.sizeZ/2);

                    // シーンの階層にタイルを追加します。
                    this.app.root.addChild(s);

                    //プレイヤー用にスタート位置を取得しておく
                    startPos = s.getPosition();

                    break;

            }
        }
    }

    //スタート位置にサンディちゃんを置く
    var mainCharacter = this.app.root.findByName('Player');
    startPos.y = startPos.y+1;
    mainCharacter.rigidbody.teleport(startPos);
};

ついでにスタート地点のオブジェクトの位置にサンディちゃんを連れてきておきます。

どや!
s4_play00.png

左上がスタート、右下がゴールとなります。

画面の二分割(プレイヤーの尻を追うカメラと俯瞰カメラ)

このままでは見下ろし型の2Dゲームと変わりません。
3Dやってんだぜっっってなことでカメラを追加してTPS視点も追加してみましょう。

エディタから、カメラエンティティをもう1個追加します。
s4_09.png

ポイントはサンディちゃんの子エンティティとして、後ろに配置することです。
カメラをサンディちゃんの後ろに固定することで、サンディちゃんの尻を常におっかけることになります。(このド変態☆)
Positionは[X:0, Y:2.5, Z:-5]、Rotationが[X:-10, Y:180, Z:0]です。

二つのカメラのViewportをいじります。
デフォのカメラが[X:0.5, Y:0, W:0.5, H:1]、新カメラが[X:0, Y:0, W:0.5, H:1]として水平方向に二つ並べるようにします。

s4_play01.png

少々間抜けな光景ですが、画面の二分割ができました。

お尻は・・・見えませんね・・・
続きはご自分でコーディングしてください(笑)

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

Node.js - Mysql「Error: Cannot enqueue Query after invoking quit.」の対処(Connection pool)

はじめに

本記事はデータベースにおけるコネクションプール(connection pool)について触れていくものです。

実際にハマったシチュエーションをもとに説明していきたいと思います。

  • 事象の詳細
  • 原因
  • 状況を再現してみる
  • 適切な対処方法
  • コネクションプールの実装

事象の詳細

Node.jsにて、Mysqlからデータを取得しようとしたとき、以下のエラーが発生した。

Error: Cannot enqueue Query after invoking quit.

どうやら2回目のGET:/api/todoを呼び出した時に必ず発生するようです。
その時のソースコードは以下です。

const express = require("express");
const mysql = require("mysql");
const app = express();

// データベースへのコネクションを生成
const connection = mysql.createConnection({
  // DB接続に関するオプション設定
});

connection.connect();

app.get("/api/todo", (req, res) => {
  const query = "SELECT * FROM todo;";

  connection.query(query, (err, rows) => {
    if (err) console.log("err: ", err);

    res.json(rows);
  });

  connection.end();
});

// サーバー起動
app.listen(8081, () => console.log("Example app listening on port 8081!"));

原因

コネクションは再利用できない。

変数connectionに対して、end()を呼び出し、丁寧に接続を切っています。
そこで再度connection()を呼び出せばまた接続できるのでは?という発想でした。

状況を再現してみる

実装した処理

同一のコネクションに対して、複数回接続を行う処理を作成してみました。

const mysql = require("mysql");

const connection = mysql.createConnection({
  // DB接続に関するオプション設定
});

for (let index = 0; index < 2; index++) {
  connection.connect();

  const query = connection.query("SELECT * FROM todo;");
  query.on("result", (row, index) => {
    console.log("--- result ---");
    console.log(row);
    console.log(index);
  });

  connection.end();
}

処理結果

nodeコマンドを使って、この処理を実行すると、以下のようなエラーになります。

Error: Cannot enqueue Handshake after invoking quit.

一度使ったんだから、ちゃんと破棄してくれってことですね。

適切な対処方法

何度もアクセス要求ができる接続窓口を作ってあげる。

実際のWebアプリケーションでは、同一のデータベースに対して何度も接続する処理が行われます。
規模にもよりますが、多人数で利用することを考えると、その数は膨大なものになります。

よって、以下の観点からコネクション確立処理は極力減らしたほうが良いです。

  • 本処理自体がオーバーヘッド(overhead)である
  • コネクション確立には時間がかかるため、ユーザーを都度待たせてしまう
  • コネクションの数だけDB側でメモリを確保する必要があるため、高負荷状態になりやすい

それらを実現するのがコネクションプール(connection pool)です。

  • コネクションの状態を保持し、そのコネクションを使いまわすことができる
  • コネクション数に上限を設けることができる

コネクションプールの実装

mysql - Pooling connections を参考に、コネクションプールを作成し、複数回のデータベース接続処理を行ってみます。

使用する関数はcreatePool()です。
実行するクエリが一つの場合、以下のような書き方でOKです。

const mysql = require("mysql");

const pool = mysql.createPool({
  // DB接続に関するオプション設定
});

pool.query("SELECT * FROM todo;", (error, results) => {
  if (error) throw error;
  console.log(results[0]);
});

プールが持つquery関数はpool.getConnection()connection.query()connection.release()を省略してくれます。

複数回のクエリ実行を行いたい場合などは以下です。

pool.getConnection((err, connection) => {
  if (err) throw err;

  connection.query("SELECT something FROM sometable", (error, results) => {
    connection.release();

    if (error) throw error;

    console.log(results[0])
  });
});

おわり

データベースに関する基礎的な知識が不足していたせいで、こんなところでハマってしまいました。
基礎的な部分を固めることができたと思うので、次の課題に取り組んでいきたいと思います。

あと、mysqlライブラリのドキュメントに書いてあるとおりに実装していくと、コールバック地獄に陥りそうですね。
async/awaitなどを使ってもっとスマートに実装していきたいです。

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

特定要素のwidth基準のサイズ指定 by css&js

vwは便利ですけど、画面全体の幅基準なので、responsiveに幅の変化する要素を基準にはできませんね。
以下はそれをjsで実現するものです。

css
:root{
   --base1: 0;
}

.title-box{
   position:relative;
}

.welcome{
   font-size: calc(var(--base1) * 8vw);
   margin-top: calc(var(--base1) * 17vw);
}
.title{
   font-size: calc(var(--base1) * 12vw);
   line-height: calc(var(--base1) * 12vw);
   margin-top: calc(var(--base1) * 3vw);
}
html
<div data-calc-base="base1" class="title-box" >
   <div class="welcome">ようこそ!</div>
   <div class="title" >種子島へ!</div>
   <img src="/img/tanegasima/top.jpg" >
</div>
js
$(window).on('load resize', ()=>{
   $('[data-calc-base]').each(function(){
      document.documentElement.style.setProperty(
         '--' + $(this).data('calc-base'), $(this).width() / $(window).width()
      );
   });
});

inspired by
https://flex-font.com/how_to/

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

【LaravelDB.com】〜機能解説〜 テーブル定義をHTML で”見やすく!”表示(機能アップデート)

LaravelDB.comの新機能[テーブル定義一覧(HTML)]について解説

LaravelDB.comについて知りたい人はこちらの以前の記事からどうぞ!!
https://qiita.com/daisu_yamazaki/items/068595670bdc2b6fe3fc

1. ER図画面から 「 ER図のLoad/Save 」ボタンをクリック

https://laraveldb.com
注意)テーブル定義(データ)が表示されてることを確認してください。

2. 「 MIGRATION/TABLE 」ボタンをクリック

3.ダウンロードしてフォルダを開く

ZIP圧縮ファイルをダウンロードします。
フォルダ内の「 table_design.html 」HTMLファイルをダブルクリックで開きます。

4.HTMLで「テーブル一覧」として表示

これでTeamや他の人にもテーブル情報を共有可能ですね!!

以上

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

ReactComponentの依存度を解析するツールを作りました?

どんなツールか

image.png

React Componentがどのくらい他のコンポーネントに依存しているかをブラウザで確認できるツールです。

[ 注意 ] : 2020/02/05 時点では、まだベータ版みたいなものなので、バグをとても多く含んでいます。また、制作途中なモノなので実装されている機能がショボいです。。。ご了承ください。

使い方

[ 注意 ] : 現状ではtsxファイルのみに対応しています。js・jsx・tsファイルの動作はまだ確認できていません。

既にnpmで公開しているので、npmからダウンロードして使えます。

$> npm install -g react-izon

$> cd ReactComponentを使っているファイルがある所

$> react-izon ./src/index.tsx <- 一番上のコンポーネントのファイルパスを指定

...

listen to http://localhost:9000

コマンドを実行するとサーバーが起動するので、http://localhost:9000にブラウザでアクセスすると解析結果が表示されます。

グローバルにインストールしたく無い人は、npxでも同じ事が出来るので、npxを使ってください。

$> cd ReactComponentを使っているファイルがある所

$> npx react-izon ./src/index.tsx

...

listen to http://localhost:9000

使った主な技術など

lerna ( monorepo )

monorepoとは、一つのリポジトリで複数のパッケージ(モジュール)を管理することです。
今回作ったツールは、このmonorepoを使ってプロジェクト管理できるlernaと言うツールを使いました。
lernaについては、既に他の解説記事がありますので、そちらを参考にして下さい。

作ったツールは、以下のように機能ごとにパッケージとして切り出してそれぞれが互いを参照し合うような形で開発しました。

  • react-izon : cliの処理を担当するパッケージ
  • react-izon-core : 依存関係などを解析して解析結果を出力するパッケージ
  • react-izon-ui : Reactで描画部分を作り、expressを用いてReactで作ったページをブラウザで見れるようにするパッケージ

このように切り出すことで、役割分担ができて開発がし易くなりました。( 慣れが必要だとは思いますが。。。 )
正直な所、今回のツールではmonorepoでなくても良かった感じがしますが、私自身がmonorepoについて色々と学べたので、良しとしておきます。

@babel/parser( AST )

ASTを用いて、コンポーネントの依存を測っているので、ASTを得るために@babel/parserを使いました。基本的に、ASTExplorerAST構造を見て、必要な情報を再帰して取得しています。
また、monorepoで作ったので、ここの処理はreact-izon-coreというパッケージに実装しています。

TypeScript

ツールは、TypeScriptで書きました。TypeScriptにした理由は、VSCodeでのサポートが豊富な事と、monorepoで実装が分断されていても、によってある程度はやり易くなるためです。

oclif

cliツールを作るためのフレームワークです。
TypeScriptに対応していたのと、私がcliを作るのが初めてだったので使ってみました。
フレームワークなので、cliに良くある機能が簡単に実装できたので、使って良かったと思いました。
また、eslintのルールも設定してある物があるので、環境構築にも悩まされないところも良かったです。ドキュメント( 英語 )もあるので、困ったことは特には無かったです。

なぜ作ったのか?

チームでReactを用いて開発していたときに、Reactの経験がまだ無い人がチームの中に何人か居て、その人たちに、Propsの流れコンポーネントの複雑性を説明するのがすごく難しかったことがありました。
そのときに、「フロントエンドが分からない人でも、コンポーネントの複雑さPropsのデータフローを伝え易くするツールを作ろう!」と思い作りました。

今後の予定( 仮 )

他のフレームワーク・ライブラリに対応する

Reactだけでなく、Componentを使う他のフレームワーク・ライブラリに対応したいと思っています。しかし、当分はReactのみになると思いますが。

デザインをもっと良くする

今のデザインはダサいと思っているので、もっと使いやすくて、分かりやすいデザインにして行こうと思っています。

相関図を表示できるようにする

依存関係を図にして、直感的に分かるようにしようと思っています。
この機能をつける事で、チームでの開発に役立ったり、フレームワーク・ライブラリを知らない人にアプリの複雑性などを説明しやすくなると思っています。

Propsなど他の要素からも依存関係を調べる

今のところ、Propsの受け渡しなどの情報をうまく活用できてないので、それを使ってデータフローを可視化したいと思っています。

テストフレームワークで使えるようにする

jestmochaなどで使えるようにしたいと思っていますが、実装するかは謎です。

後書き

ここまで読んで下さり、ありがとうございます。
2020/02/05時点では、まだまだ色々な課題を多く含んでおり、目も当てられないような品物ですが、これから改良していって、皆さんの開発に役立つようなツールにして行けたらいいなと思っております。
いつになるかは分かりませんが。

それでは?

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

Reactの高階コンポーネントのメモ

はじめに

React の高階コンポーネントのメモです。
ご指摘、感想、お待ちしています。。。

目次

  1. 高階コンポーネント???
  2. まずは普通に書いてみる
  3. 高階コンポーネントを作成
  4. 高階コンポーネントをくっ付ける
  5. まとめ

1. 高階コンポーネント???

高階コンポーネントとは。。。
公式の Docs
以下は、引用です。

高階コンポーネント (higher-order component; HOC) はコンポーネントのロジックを再利用するための
React における応用テクニックです。HOC それ自体は React の API の一部ではありません。
HOC は、React のコンポジションの性質から生まれる設計パターンです。

さらにさらに。

具体的には、高階コンポーネントとは、あるコンポーネントを受け取って新規のコンポーネントを返すような関数です。

ほうほう、どうやらコンポーネントのロジックを再利用(=使い回す)ためのテクニックなのか。。
では、実際に書いてみよう。

2. まずは普通に書いてみる

簡単なカウンターのコンポーネントを 2 つ用意します。
カウントの条件は以下

  • ボタンのクリック
  • マウスでホバー
ClickCounter
import React, { Component } from "react";

class ClickCounter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  incrementCount = () => {
    this.setState(prevState => {
      return { count: prevState.count + 1 };
    });
  };

  render() {
    return (
      <div>
        <button onClick={this.incrementCount}>
          clicked {this.state.count} times
        </button>
      </div>
    );
  }
}

export default UpdatedComponent(ClickCounter);
HoverCounter
import React, { Component } from "react";

class HoverCounter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  incrementCount = () => {
    this.setState(prevState => {
      return { count: prevState.count + 1 };
    });
  };

  render() {
    return (
      <div>
        <h3 onMouseOver={this.incrementCount}>
          Hover {this.state.count} times
        </h3>
      </div>
    );
  }
}

export default HoverCounter;

ここで、各コンポーネントで共通の箇所は以下の部分です。

constructor(props) {
        super(props)
        this.state = {
            count: 0
        }
    }

incrementCount = () => {
    this.setState(prevState => {
        return { count: prevState.count + 1 }
    })
}

3. 高階コンポーネントを作成

冒頭の引用にあるように

あるコンポーネントを受け取って新規のコンポーネントを返すような関数です。

高階コンポーネントを作成しましょう。

  • 高階コンポーネント : higherOrderComponent
  • 受け取るコンポーネント : OriginalComponent
  • 新規のコンポーネント : NewComponent

NewComponentで共通箇所として注目した

  • count
  • インクリメントに必要なincrementCount

を定義しています。
それらを、OriginalComponentのpropsとして持たせてあげます。

higherOrderComponent
import React from 'react'

const higherOrderComponent = OriginalComponent => {
    class NewComponent extends React.Component {

        constructor(props) {
            super(props)

            this.state = {
                count: 0
            }
        }

        incrementCount = () => {
            this.setState(prevState => {
                return { count: prevState.count + 1 }
            })
        }

        render() {
            return <OriginalComponent count={this.state.count} incrementCount={this.incrementCount}/>
        }
    }
    return NewComponent
}
export default higherOrderComponent

4. 高階コンポーネントをくっ付ける

高階コンポーネントを利用してみましょう。
higherOrderComponent(ClickCounter)の箇所で利用がされています。
そうすると、countincrementCountは高階コンポーネントがpropsで渡してくれているので
各コンポーネントで定義やロジックの実装が不要になります。

ClickCounter
import React, { Component } from 'react'
import higherOrderComponent from './Counter'

class ClickCounter extends Component {

    render() {
        return (
            <div>
                <button onClick={this.props.incrementCount}>clicked {this.props.count} times</button>
            </div>
        )
    }
}

export default higherOrderComponent(ClickCounter)
HoverCounter
import React, { Component } from 'react'
import higherOrderComponent from './Counter'

class HoverCounter extends Component {

    render() {
        return (
            <div>
                <h3 onMouseOver={this.props.incrementCount}>Hover {this.props.count} times</h3>
            </div>
        )
    }
}

export default higherOrderComponent(HoverCounter)

5. まとめ

高階コンポーネントを利用して、コンポーネントのロジックを共通化できました。
めでたし、めでたし。
おしまい。

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

TypeScript + Tynderから始める宣言的検証生活

皆さんは JSON Schema 使ってますか?
現在では、Web APIのペイロード定義・検証、モックサーバー作成ユーザー入力フォーム検証設定ファイルのスキーマ定義・検証・IDEでのエラー表示など、多くの場面で、また多くの言語でライブラリが整備され利用されています。

JSON Schemaの強み

  • Internet draftのフォーマットで仕様が公開されている
  • 多くの言語での多くの実装(言語によっては複数)が存在する
    • 1回書けば、フロントエンド、複数のバックエンドすべてで利用できる可能性が高い
    • 代替実装が存在すると競争原理が働く

JSON Schemaの嫌いなところ

  • 見辛い
  • 書き辛い

数行の小さなスキーマならばともかく、JSON Schemaって本当に苦痛。汎用のデータフォーマットを人が直接記述するレイヤーのDSLにするのは正直辛い。ヒューマンリーダブルだからといって人が読めるとは限らないのだよ…(古のXML Hellを彷彿する)

JSON Schema 書きたくない?…
そうだ、新しいスキーマ検証ライブラリを作ろう?

準備

本題のスキーマ検証に進む前に、少し型安全性(type safety)についておさらいします。
スキーマ検証によって最終目的であるデータの妥当性検査を行うだけでなく、途中のコードに型安全性を与えることも重要と考えているからです。

型安全性って何?

大変大雑把に言えば、型を間違えたせいでバグらない、ということです。
例えば、加算したつもりが文字列連結になったり、(実は存在しなかった)フィールドの値を文字列結合したらundefinedという文字列が結合されたりするバグが起こせるなら、型安全ではありません。

ちなみに、どんなフィールドを持っているか分からない型のオブジェクト(C#のdynamic, JavaScript等の多くのスクリプト言語でのすべての変数の型)において、(実は存在しなかった)メソッドを呼んだら例外が発生した、という動作は型安全性があると考えます。

実は「型安全性」の定義は基づいている「型システム」(の視点)により変化するものです。
なお、言語が「型安全性」をどれだけ持っているかという「強い型付け」・「弱い型付け」というワードは割と曖昧です。

TypeScriptと静的型安全性

TypeScriptという言語は、JavaScriptの文法を拡張し、JavaScriptに対する型アノテーションを記述できるようにします。TypeScriptコンパイラ(tsc)は、(ES6文法の変換等も行いますし、一部構文のために追加のコード生成を行ったりもしますが)大まかに言えば、型検査(type checking)を行い、元のコードから型アノテーションを削除したコードを出力します。

コンパイラオプションにも依りますが、コンパイル時の型検査によって、幾らかの静的型安全性を提供します。

JavaScriptと動的型安全性

コンパイルされたTypeScriptのコードは、紛れもなくJavaScriptのコードであり、JavaScriptのランタイムにより実行されます。JavaScriptランタイムは、変数・引数・戻り値の型に関心がありません。ランタイムは型検査を行わない(動的型安全性無い)ため、実行時に変数・引数・戻り値が想定した型を持っているか検査するのはプログラマーの責任になります。

  • JavaScriptはそもそも型を明示する文法を持っていないので、例えランタイムが型検査の機能を持っていたとしても、ロード時(静的)・実行時(動的)ともに検査のしようがありません。

静的と動的

ランタイムが型検査を行わないJavaScriptにおいて、プログラマーは一切型を検査しないコードを書くこともできますし、厳密に検査することもできます。検査をしなければコードを小さく、簡略に、また高速にできます。しかし、意図しない型を受け入れれば、プログラムはおそらく意図しない動作をする(=「ある種の(certain)」バグがある)でしょう。ここに 性能および開発リソース と 安全 との間のトレードオフがあります。

  • 型検査をパスすれば「ある種の」バグが無いと言えますが、型検査をパスできなくても「ある種の」バグがない場合(false possible; 偽陽性)が有り得ます。
  • 「ある種の」バグと限定するのは、型検査で発見されるバグとは型の違いによって引き起こされる様々な動作のことであり、型検査ですべてのバグが発見されるわけでは無いためです。
  • そもそも任意のプログラムの「すべてのバグ」を発見することは、チューリングマシンの停止性問題であり、実現できません。

信頼できる呼び出し元から渡されたデータ、信頼できる関数から返されるデータのは(実行せずとも)信頼できる。そう考え、プログラムの実行前(例えばコンパイル時)にそれぞれの期待する型に矛盾がないか検査し、検査が通った場合のみ実行できるようにするのが静的型安全性があるということです。静的型安全性があれば、実行時の検査を行わなくても意図した型を受け入れていることが保証されます。

  • 信頼できるのは「型」だけであって、「値」が信頼できるわけではありません。
  • 数学的に証明できますが、私には書けそうにありませんここに記すには余白が狭すぎるようですね。

実行時に検査して、意図する型と矛盾がある状態のままプログラムを進行させないようにする(例えば例外をスローする)のが動的型安全性です。

型破りな憎いあいつ(any)

それでは、静的型安全性が壊れるのはどんなときでしょうか? TypeScriptの場合で考えると以下のようなケースが考えられます。

  • (外部ライブラリの型定義ファイルが誤っていてコンパイルが通らないので)型をanyで握りつぶした、あるいは、anyを経由して任意の型にキャストした
    • 自分が予想した型と実行時の型が異なるかもしれない
  • 外部ライブラリの型定義ファイルが誤っているがそれに気付かず利用した
  • (外部ライブラリや自分の作成した関数の戻り値の型をを他の関数の引数に利用したいが、計算される型が複雑過ぎて記述できないので)anyまたは正確ではない型を指定した
  • (外部ライブラリや自分の作成した関数の戻り値の型ををFluent APIの次の呼び出しに利用したいが、計算される型がお気持ちに反したので)anyまたはお気持ちに合った型を指定した
    • 例えばconst z = [['', 0]].map(x => x);Array<[string,number]>ではなく (string|number)[][] となってしまう
      • Array.from(map.entries()).map(x => x)で詰む
  • 信頼できないユーザー入力や外部システムからのリクエストをとりあえずanyを経由して任意の型にキャストした
    • いや、unknown型のconstに束縛しましょう
      • const unknownInput: unknown = {...};

他にも怠惰を目的に、あるいは、上手く型推論してくれないため止むを得ず、型システムを欺瞞する際に私達は静的型安全性を破壊します。

今回は、スキーマ検証がテーマなので、静的型安全性を破壊した上で動的型安全性の無いランタイムでプログラムを実行する勇気については多くを語りません(最悪、致命傷となるだけです。安心しましょう。しっかりテストしましょう)。

スキーマを使って入力検証をしよう

入力検証には幾つかの段階があります(要出典)。

  1. 型の検査
  2. 単独の項目の必須・値の長さ・範囲や文字列パターンの検証
  3. 複数項目の相関や整合性の検証
  4. 永続化データやデータモデルとの整合性の検証(存在チェック、重複チェック、権限チェック、意味的なチェック、等)

もし、あなたが動的型安全性のある言語・ランタイムを使用しており、モダンなフレームワークを用いているならば恐らく、ユーザーのフォーム入力やリクエストのペイロードは、あなたの定義した型にマッピングされて渡され、ライブラリまたはランタイム自身によってマッピング先の型とデータの型が比較され、検査をパスする場合のみ正常処理が可能となるでしょう。

動的型安全の世界では、少なくとも「1. 型の検査」について悩む必要がありません。
残念ながらTypeScriptの言語機能・JavaScriptランタイムの機能では、実行時に渡される怪しいデータを型安全の世界に引き戻すことはできません。
型の検査と値の検証を手続的に毎回書くのは非常に手間が掛かるだけでなく、ミスや修正漏れ等によってバグを生む温床となります。

そこで、今回は Tynder というライブラリを使用します。(私がつくりました)

Tynder とは

tynder-logo.png

Tynder は、TypeScriptのサブセット+独自の拡張文法から成るDSLによって

  1. 型の検査
  2. 単独の項目の必須・値の長さ・範囲や文字列パターンの検証
  3. 複数項目の相関や整合性検証の一部 (Union typeによる)

宣言的に行うことができます。

さらに、TynderはDSLからTypeScriptの型定義を生成するので、定義した型をTypeScriptコンパイラによる静的型検査に使用できます。
検証は、Tynder自身の持つバリデーターで行うことができるほか、JSON Schemaを生成することで、他の言語で作成されたサブシステムともスキーマを共有できます。
表現力が高く可読性の高いTypeScriptで一度スキーマを記述すれば、どこでも使えます。

スキーマの例
/// @tynder-external RegExp, Date, Map, Set

/** doc comment */
export type Foo = string | number;

type Boo = @range(-1, 1) number;

/** doc comment */
interface Bar {
    /** doc comment */
    a?: string;                                                   // Optional field
    /** doc comment */
    b: Foo[] | null;                                              // Union type
    c: string[3..5];                                              // Repeated type (with quantity)
    d: (number | string)[..10];                                   // Complex repeated type (with quantity)
    e: Array<number | string, 4..>;                               // Complex repeated type (with quantity)
    f: Array<Array<Foo | string>>;                                // Complex repeated type (nested)
    g: [string, number],                                          // Sequence type
    h: ['zzz', ...<string | 999, 3..5>, number],                  // Sequence type (with quantity)
}

interface Baz {
    i: {x: number, y: number, z: 'zzz'} | number;                 // Union type
    j: {x: number} & ({y: number} & {z: number});                 // Intersection type
    k: ({x: number, y: number, z: 'zzz'} - {z: 'zzz'}) | number;  // Subtraction type
}

/** doc comment */
@msgId('M1111')                                                   // Custom error message id
export interface FooBar extends Bar, Baz {
    /** doc comment */
    @range(-10, 10)
    l: number;                                                    // Ranged value (number)
    @minValue(-10) @maxValue(10)
    m: number;                                                    // Ranged value
    n: @range(-10, 10) number[];                                  // Array of ranged value
    @greaterThan(-10) @lessThan(10)
    o: number;                                                    // Ranged value
    p: integer;                                                   // Integer value
    @range('AAA', 'FFF')
    q: string;                                                    // Ranged value (string)
    @match(/^.+$/)
    r: string;                                                    // Pattern matched value
    s: Foo;                                                       // Refer a defined type
    @msgId('M1234')
    t: number;                                                    // Custom error message id
    @msg({
        required: '"%{name}" of "%{parentType}" is required.',
        typeUnmatched: '"%{name}" of "%{parentType}" should be "%{expectedType}".',
    })
    u: number;                                                    // Custom error message
    @msg('"%{name}" of "%{parentType}" is not valid.')
    v: number;                                                    // Custom error message
}

// line comment
/* block comment */

概ね、見慣れたTypeScriptの構文ですね。
拡張文法のデコレーター(@range()等)によって型が修飾され、値の範囲やパターンが指定されています。
また、配列には量指定子(3..5等)を付加することで、長さを指定しています。

検証

コンパイラを含めるとややフットプリントが大きくなる(約135KB; 2020年2月現在)ので、DSLはプリコンパイルしておくのが望ましいのですが、次のサンプルでは実行時にコンパイルします。

  • コンパイラを除外した時のサイズは約37KBです。
myschema.ts
import { compile } from 'tynder/modules/compiler';

export default const mySchema = compile(`
    type Foo = string;
    interface A {
        @maxLength(4)
        a: Foo;
        z?: boolean;
    }
`);
validate.ts
import { validate,
         getType }           from 'tynder/modules/validator';
import { ValidationContext } from 'tynder/modules/types';
import default as mySchema   from './myschema';

const validated1 = validate({
    a: 'x',
    b: 3,
}, getType(mySchema, 'A')); // {value: {a: 'x', b: 3}}

検証が成功すると入力を含むオブジェクトが返り、失敗するとnullが返ります。

チェリーピックとパッチ

データモデルの一部のみを抜粋して編集させ、検証した後、元のデータモデルにマージしたいこともあります。そのために pick()patch() 関数が用意されています。

import { getType }           from 'tynder/modules/validator';
import { pick,
         patch }             from 'tynder/modules/picker';
import { ValidationContext } from 'tynder/modules/types';
import * as op               from 'tynder/modules/operators';
import default as mySchema   from './myschema';

const original = {
    a: 'x',
    z: false,
};
const needleType = op.picked(getType(mySchema, 'A'), 'a');

try {
    const needle = pick(original, needleType); // {a: 'x'}
    const unknownInput: unknown = { // Edit the needle data
        ...needle,
        a: 'y',
        q: 1234,
    };
    const changed = patch(original, unknownInput, needleType); // {a: 'y', z: false}
} catch (e) {
    console.log(e.message);
    console.log(e.ctx?.errors);
}

検証が成功するとマージされたオブジェクトが返り、失敗すると例外がスローされます。

そして型安全へ

予めDSLをコンパイルして、スキーマファイルとTypeScriptの型定義ファイルを生成しておきます。

tynder compile --indir path/to/schema --outdir path/to/schema-compiled
tynder gen-ts  --indir path/to/schema --outdir path/to/schema-types

型定義とスキーマをimportします。
検証前の入力データはunknown型、検証後のデータはimportした型のconstに束縛します。

import { deserializeFromObject } from 'tynder/modules/lib/serializer';
import { Foo, A }                from './path/to/schema-types/my-schema.d.ts';    // type definitions
import mySchemaJson              from './path/to/schema-compiled/my-schema.json'; // pre-compiled schema

// シリアライズされたスキーマを復元します
const mySchema = deserializeFromObject(mySchemaJson);

const unknownInput: unknown = {a: 'x'};
const validated = validate<A>(unknownInput, getType(mySchema, 'A'));

if (validated) {
    const validatedInput = validated.value; // validatedInputは 型「A」になります
    ...
}

このように記述することで、検証後のデータは型安全となります。
先ほどの pick, patchの例も同様に型安全に留意して書き直してみましょう。

import { pick,
         patch }                 from 'tynder/modules/picker';
import { ValidationContext }     from 'tynder/modules/types';
import * as op                   from 'tynder/modules/operators';
import { deserializeFromObject } from 'tynder/modules/lib/serializer';
import { Foo, A }                from './path/to/schema-types/my-schema.d.ts';    // type definitions
import mySchemaJson              from './path/to/schema-compiled/my-schema.json'; // pre-compiled schema

// シリアライズされたスキーマを復元します
const mySchema = deserializeFromObject(mySchemaJson);

interface Store {
    baz: A;
}
const store: Store = {
    baz: {
        a: 'x',
        z: false,
    }
};

const needleType = op.picked(getType(mySchema, 'A'), 'a');

try {
    const needle = pick(store.baz, needleType); // {a: 'x'}
                                                // `needle` is RecursivePartial<A>
    const unknownInput: unknown = {             // Edit the needle data
        ...needle,
        a: 'y',
        q: 1234,
    };
    store.baz = patch(store.baz, unknownInput, needleType); // {a: 'y', z: false}
} catch (e) {
    console.log(e.message);
    console.log(e.ctx?.errors);
}

さいごに

TypeScript + Tynder で、宣言的な検証を始めてみませんか?

参考文献

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

React Class内での関数定義(bindしよう)

はじめに

個人的なメモレベルです。

メソッド記法での問題点

ReactでsetStateを利用する際に、JSXで、以下のように書くことが多いかなと思う。

クラスのプロパティとして関数を定義
doPlus = () => {
    this.setState({count: this.state.count + 1});
};

<button onClick={this.doPlus}>ボタンを押したらプラスするよ</button>

以下のような書き方の場合、エラーになります。

これクラス内に定義されたメソッドであっても、実行方法によって、thisの参照先は変わってしまうJSの仕組み故。この場合、buttonに関数オブジェクトをそのまま渡しているので、thisの参照先buttonになってしまう。

JSの初心者が最初にぶつかる壁はthisの扱いですよね。。。

ボタンを押下すると、undefindeエラーとなる
doPlus(){
    this.setState({count: this.state.count + 1})
}

<button onClick={this.doPlus}>ボタンを押したらプラスするよ</button>

上記のようにメソッド記法で書きたいんだ!!!て場合は、classの初期化時にthisを必ずbindしておきましょう。

初期化処理
constructor(props) {
    super(props);
    this.state = {
        count: 0
    }
    this.doPlus = this.doPlus.bind(this);
}

これで解決?

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

elecom WRH-300XX3をAPモードで使うときのDHCPサーバの止め方

elecom WRH-300XX3を使ってるのですが、これのDHCPサーバが悪さをしてうまくつながらないときの対処。設定Webページからは設定が見えないので、Chromeの検証から強制的に見えるようにして変更した話(備忘録)。

  • APモードなのでDHCPサーバは不要
  • 仮に使ったとしても、デフォルトゲートウェイは(APモードなので)元ルータに指定したいのにできない

なお、この方法は下手をするとWRH-300X3にアクセスできなくなる可能性があるので、実施する際には自己責任でお願いします。

手順

  1. 設定ページにアクセスして、動作モード選択をAPモードにして再起動。
  2. 「インターネット設定」→「LAN設定」を開く
  3. ページ内で右クリックして、「検証」
  4. elementブラウジングで以下をたどる
    • <html>→<frameset>→<frameset>→<frame>→#document→<html>→<body>→<blockquote>→<form>
  5. ふたつめの→<table>がstyleで非表示にされているので、display:noneを削って表示する。
  6. ページ側にDHCP: フォームが表示され、ClientになっているのでDisableに変更
  7. ついでにIPアドレスを変える場合には、IPアドレスだけでなく、DHCPサーバの配布するIP範囲も変えないと変更できない使用になっているので、そちらも変える
    • もう一つ下の<talble>もdisplay:noneを削って表示すると、DHCPサーバの配布IP範囲が指定できるようになるので、上の方のIPアドレス設定および配布範囲を変更
  8. 「適用」ボタンを押すと自動的に再起動して上記が反映される
- 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Playframework+axiosによるPOSTするときの注意点(CSRFTokenとlist)

PlayFramework(twirl)によるform

PlayFrameworkにはtwirlというテンプレートエンジンが用意されている。

hoge.scala.html
@helper.form(action="/hoge/huga",Symbol("id")->"form1") {
@CSRF.formfield
@helper.inputText(nankanoForm("nanika"),Symbol("id")->"text1",Symbol("placeholder")->"field")
<input type="submit">
}

このようなテンプレートからレンダリングされたhtmlでsubmitすれば入力したデータが
GET/POSTされ用意したnankanoformのcase classなどで受け取ることが出来るが、
クライアント側でajaxリクエストを行うといくつか問題が発生する。

CSRF対策(POST時)

PlayframeworkではCSRFfilterを有効にするとRequestにCSRFTokenが設定されてないものは不正リクエストとして弾く。

上記テンプレートのように@CSRF.formfield
@helper.formのブロック内部に追加することでCSRFfilterの認証を通すことができる。
しかし単純にformの値を取ってきてPOSTをすると、
CSRFTokenが見つからないためにリクエストエラーになってしまう。

PlayFrameworkではFormDataとしてcsrfTokenを含めるという方法を取っている。
上記のテンプレートをレンダリングした結果としてhoge.htmlがクライアント側で表示される。

hoge.html
<form action="hoge/huga" method=POST id="form1">
<input name="csrfToken" value="123...">
<input type="text" name="nanika" id="text1" placeholder="field">
<input type="submit">
</form>

このform内のcsrfTokenの値を取得してPOSTする。

const params = new URLSearchParams()

const csrfToken= document.getElementsByName("csrfToken")[0].value
const text = document.getElementById("text1").value

params.append("csrfToken",csrfToken)
params.append("text1",text)

axios.post("/hoge/huga",params)

一応こういう感じにcsrfTokenに対応させる形で送ると普通にsubmitしたのと同じようにリクエストが通る(もっといい書き方がある気もする)。

上記の方法を取らなくてもapplication.confにsession.sameSite = "lax"と設定してCSRFfilterをfalseにしたり、
routesを以下のようにしてもいいと思う。

+nocsrf
POST /hoge/huga 

他にもplay.filters.csrf.header.bypassHeadersに色々設定して認証を回避したりもできる。
https://www.playframework.com/documentation/2.8.x/ScalaCsrf

listのPOST

formの定義時にlist(number)という風にするとList[Int]みたいな感じで受け取れる。
例えばboxes -> list(number)みたいな感じにするとhtmlではname=boxes[]でinput要素が生成される。
これをPOSTする際には下記のようにする必要がある。

Array.from(document.getElementsByName("boxes[]"))
     .filter(e => e.checked)
     .map(e => e.value)
     .forEach(v => params.append("boxes[]",v))

配列としてappendするのではなく、各要素の値をappendしなければformのcase classへの変換が上手くいかないので注意。

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

非同期APIをPromiseでラップしてasync/awaitで使う

setTimeout と Web Speech API を Promise でラップする例を示します。

Promise とメソッドチェーンだけでは何がやりたいのか分かりにくいかもしれませんが、async/await もセットで考えることで狙いが分かりやすくなるのではないかと思います。

※ Web Speech API の例は単純なので、触れたことがなくても分かるのではないかと思います。

setTimeout

Promise の例としては定番です。

コールバック

非同期 API にコールバックを渡す処理をネストすると、いわゆるコールバック地獄になります。

setTimeout によって1秒後に1、その2秒後に2、その更に3秒後に3を表示する例です。

setTimeout(() => {
    console.log(1);
    setTimeout(() => {
        console.log(2);
        setTimeout(() => {
            console.log(3);
        }, 3000);
    }, 2000);
}, 1000);
実行結果
1
2
3

コールバックが深くなるのに加えて、待機時間と後続の処理との対応が分かりにくいです。

Promise

setTimeout を Promise でラップします。resolve は成功したとき、reject は失敗したときのコールバックです。今回は失敗は考慮しないため reject は無視します。

コールバックは .then で渡します。

function wait(timeout) {
    return new Promise((resolve, reject) => setInterval(resolve, timeout));
}

wait(1000).then(() => {
    console.log(1);
    wait(2000).then(() => {
        console.log(2);
        wait(3000).then(() => {
            console.log(3);
        });
    });
});

これだけだと setTimeoutwait().then に置き換わっただけで、あまりメリットはありません。

コールバックから Promise を返すように書き換えることができます。

wait(1000).then(() => {
    console.log(1);
    return wait(2000);
}).then(() => {
    console.log(2);
    return wait(3000);
}).then(() => {
    console.log(3);
});

コールバックのネストが .then によるメソッドチェーンに変わりました。ネストが深くなることはなくなり、多少読みやすくなりました。

最初のネストした wait().then の書き方と比較すれば、return で Promise を返してコールバックを抜けてから .then でつなぐことで、ネストをフラットにしていることが分かります。

async/await

メソッドチェーンの糖衣構文です。wait の定義はそのままです。

function wait(timeout) {
    return new Promise((resolve, reject) => setInterval(resolve, timeout));
}

(async function () {
    await wait(1000);
    console.log(1);
    await wait(2000);
    console.log(2);
    await wait(3000);
    console.log(3);
})();

同期的な処理のように記述することができました。awaitasync function(非同期関数)の中でしか使えないため、即時実行関数式にしています。

こうして見ると、Promise は非同期 API と async/await の間を取り持っていることが良く分かります。

co

async/await と同じようなことをジェネレーターで実装した co というライブラリがあります。

co は Promise があって async/await がない時代に作られたものです。参考までに簡易的に実装して比較します。

function co(g) {
    let it = g();
    function f(v) {
        let result = it.next(v);
        if (!result.done) return result.value.then(f);
    }
    return Promise.resolve().then(f);
}

co(function* () {
    yield wait(1000);
    console.log(1);
    yield wait(2000);
    console.log(2);
    yield wait(3000);
    console.log(3);
});

async/await が模倣できていて面白いです。

async/await が使える今となっては敢えて co を使う必要はないかもしれませんが、発想は参考になります。また、Promise を検索すると co を使った以前の記事が出て来るため、async/await に読み替えられるということを知っておいても損はないでしょう。

Web Speech API

setInterval では失敗を考慮しませんでしたが、Web Speech API を例に失敗のあるケースを示します。

Web Speech API によってブラウザでテキストの読み上げを行います。読み上げは非同期的に行われますが、終了を待たずに次々指示しても、キューに溜まって順番に処理されます。

function speak(lang, text) {
    let u = new SpeechSynthesisUtterance(text);
    u.lang = lang;
    speechSynthesis.speak(u);
}

speak("en", "Hello, world!");
speak("fr", "Bonjour, monde !");
speak("ja", "こんにちは、世界!");

連続で読み上げるだけであれば終了を待つ必要はありませんが、もし途中で失敗しても無視して次に進んでしまいます。

コールバック

成功した時だけ次に進むようにするため、終了を待ってコールバックで次の読み上げを指示します。

function speak(lang, text, end, error) {
    let u = new SpeechSynthesisUtterance(text);
    u.lang = lang;
    u.onend = end;
    u.onerror = error;
    speechSynthesis.speak(u);
}

speak("en", "Hello, world!", () => {
    speak("fr", "Bonjour, monde !", () => {
        speak("ja", "こんにちは、世界!", () => {
        }, e => console.log(e));
    }, e => console.log(e));
}, e => console.log(e));

失敗した時は例外を表示します。同じ例外処理を何度も書いています。

e => console.log(e)console.log と単純化できますが、後で async/await に書き換える際に e が出て来るため、引数を明示した形で進めます。

Promise

setTimeout では無視した reject を失敗時のコールバックとして使います。失敗時の処理は .catch で与えます。

まずはネストのまま書き換えます。

function speak(lang, text) {
    return new Promise((resolve, reject) => {
        let u = new SpeechSynthesisUtterance(text);
        u.lang = lang;
        u.onend = resolve;
        u.onerror = reject;
        speechSynthesis.speak(u);
    });
}

speak("en", "Hello, world!").then(() => {
    speak("fr", "Bonjour, monde !").then(() => {
        speak("ja", "こんにちは、世界!").then(() => {
        }).catch(e => console.log(e));
    }).catch(e => console.log(e));
}).catch(e => console.log(e));

これをフラットにします。何もしない最後の .then を省略して、失敗時の処理を1つにまとめます。

speak("en", "Hello, world!")
    .then(() => speak("fr", "Bonjour, monde !"))
    .then(() => speak("ja", "こんにちは、世界!"))
    .catch(e => console.log(e));

かなりすっきりしました。

最初の speak だけ書き方が異なりますが、ダミーから始めることで揃える方法があります。

Promise.resolve()
    .then(() => speak("en", "Hello, world!"))
    .then(() => speak("fr", "Bonjour, monde !"))
    .then(() => speak("ja", "こんにちは、世界!"))
    .catch(e => console.log(e));

【参考】Promiseを複数組み合わせる時の基本パターン(直列、並列、分岐)

async/await

メソッドチェーンを非同期の即時実行関数式で書き換えます。speak の定義は Promise と同じため省略します。

(async function () {
    try {
        await speak("en", "Hello, world!");
        await speak("fr", "Bonjour, monde !");
        await speak("ja", "こんにちは、世界!");
    } catch (e) {
        console.log(e);
    }
})();

trycatch によって同期的な処理に近い見た目で記述できました。

co

参考までに co 版を書いておきます。

co(function* () {
    yield speak("en", "Hello, world!");
    yield speak("fr", "Bonjour, monde !");
    yield speak("ja", "こんにちは、世界!");
}).catch(e => console.log(e));

まとめ

Promise で成功と失敗のコールバックに振り分けてラップすることで、async/await によって表記が簡略化できます。

参考

コールバックから async/await への書き換えは、Haskell での bind を明示した書き方から do ブロックへの書き換えに対応します。(いわゆるモナドと関係があります)

Web Speech API の使い方は以下の記事を参照してください。

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

【PWA】YouTubeをみんなでワイワイ見るためのサービスをローンチしました!!

みなさんこんにちは!

今回はYouTubeの動画を複数人で同時視聴できるサービスを作りましたので紹介したいと思います。
ブラウザを開くだけで、友達や恋人と同じ動画を見ることができます!

複数人で同じ動画を見て、あーでもないこーでもないと意見を交わすのはめちゃめちゃ楽しいです。
これはぜひ皆さんにも体験してもらいたいです!

YouTube同時再生サービス DJ7
https://www.dj7.io

初回のアクセスは音がならないように設定してありますのでご安心ください。

アートボード 1.png

できること

同期再生

サービスの要です。複数のデバイス間で再生の状態が同期され、離れている場所でも同じタイミングで同じ動画を視聴できます。
右下をクリックすると動画を画面に大きく表示します。これによって複数人で同じ動画を見ることができます?

pcmbplaying.png

シークバー共有

DJ7ではシークバーの状態もユーザ間で共有されます。これはYouTube Liveでは得られない体験です。
ユーザが自由に操作できて、かつ状態が共有されるサービスは他にないと思います笑
DJ7ではユーザができることに優劣がないので全員が同じ操作をすることができます。

seek.gif

動画の検索

YouTube上の動画を検索して、一番関連度が高いものを追加でします。もちろんURLを直接入れたり、プレイリストの追加にも対応してます!
ちなみにテキストボックスの上のアイコンは同じ部屋にいるユーザです

search.gif

割り込み

あまり馴染みがない機能だと思います。例えば長い動画を流しているときに有効です。
再生中の動画とキューの動画を入れ替えることができます。入れ替わった動画はキューの一番上に追加され、どこまで再生されていたかなどの情報が保存されます。

interrupt.gif

曲の順番の入れ替え

DJ7には再生待ちの動画のリストがあり、その順番をドラッグ・アンド・ドロップで変えることができます。
ちなみにDJ7では再生待ちのリストのことをQueueと呼んでいます。

switch.gif

履歴

再生した動画は履歴に保存されるようになっており、そこからも曲の追加ができます。
履歴の保存はログインが必要ですので、その点だけご了承ください。

history.gif

部屋の作成

自分で部屋を作成して、誰かを呼ぶことも一人で使うこともできます。適当な部屋名を入れてJumpしていただくだけで部屋が作成されます。
こちらの機能もログインが必要です。

DJ7で可能になること

もともと、DJ7はDiscordのMusicBotと同じことがWebでできるようになることを目指して開発していました。
ちなみに、MusicBotとはDiscordの同じチャンネルにいる人同士で音楽を聞くためのサービスです。

しかし、MusicBotには一度に曲を複数追加できない、そもそもインストールがめんどくさいなど、私の中で数多のフラストレーションがありました。
そこでもっとカジュアルに同時視聴を楽しみたいという思いのもと今回のDJ7を開発しました。

最初はMusicBotの代替を目指していたのでYouTubeの動画を読み込んでいても、音楽を聞くことに専念するアプリにしようと考えていました。
しかし、複数人で同じ動画を視聴することが想像以上に楽しい体験だったので結局残すことにしました笑

複数人で同じ動画を見ることができるようになり、以下のようなことが可能になります。

同じ動画を、同じタイミングで見て意見を交わす

これは最初にも書きましたね。これは誰かと通話していることが前提となっていますが、ぜひやってみてほしいです。
生放送を見るのとはまた違った楽しみがあります。

DJ7に集まって作業用BGMを流す

これが元々のMusicBotがメインでできたことになります。MusicBotとは違い通話する必要はないので、よりカジュアルに参加でき、自分の作業にも集中することができます。
また、この用途ではリアルで集合したときにも使えます。どこかに集まったときにBGMを流す役を人を立てるけど、自分が流したい曲があったときに皆さんはどうしてましたか? DJ7ではいちいちリクエストして流してもらうという手間はいりません。

動画を共有する

なにか面白い動画があったときにURLで共有することがあると思います。DJ7ではURLを共有する手間も、URLをクリックして開いてもらう手間も必要ありません。

動画を流してリモートで授業

これはやったことがありませんが、可能だと思います。同じ動画をみて、重要な点は一時停止して解説をするといったことができます。

ほかにもこんな使い方はどうですか?

  • オンラインでカラオケ
  • お笑いのライブを一緒にみて笑う
  • ライブコーディングの同時視聴
  • 製品発表や商品レビューをみんなで思ったことを言いながら視聴
  • 勉強会

私が知らないだけで使い方はもっとたくさんあると思います。ぜひみなさん思い思いの使い方をしてみてください!!

開発スケジュール

今回の開発スケジュールは以下のような感じです。

工程 工数
デモ作成 3日程度
ロジック実装 10日程度
デザイン 10日程度
その他 7日程度

最初の3日で同時視聴ができるデモを簡単に作り、知り合いに見せました。このときポジティブな感想をもらえたのがモチベーションになりました。
そこから仲間内で実際に1ユーザとして使ってもらい意見をもらいながら開発を進めました。
また、1ヶ月で作るということを最初に決めていましたので、機能の実装は最低限必要なものだけに絞り、機能を追加するよりは、すでにある機能のブラッシュアップを優先させること常に意識していました。

次回は今回学んだことを活かして、もっと早くサービスのローンチまで漕ぎ着けたいです。

技術スタック

kousei

今回はフロントエンドにVue、バックエンドにFirebaseを使いました。
とくに同期再生といった根幹の部分はFirestoreだけで実装されています。
Cloud Functionsなども使いませんでしたので、コーディングはフロントエンドだけに集中できました。

Firebase最高。

苦労した点

モバイルについてです。AndroidやiOSでは、OS側の制約により、YouTubeの音をミュートにしなければサイトを訪れたときに自動で再生されません。そのため、サイトを訪れてから最初にYouTubeのプレイヤーを操作していただきミュートを解除していただく必要があります。この点だけご了承していただきたく・・・

また、今回の場合ではあまり難しいロジックはありませんでしたので、実装自体にはあまり苦労しませんでした。

まとめ

今回は公開のためにあまり機能をつけませんでしたが、私自身欲しい機能がまだまだたくさんあるのでバージョンアップを重ねていきたいと思います。
技術についての詳しい話や、サービスの途中経過も適時報告したいと思います。

質問はTwitter @imataka7 または @dj7app までお願いします!

また、こちらのDiscordのサーバでWebサービスを個人開発する人たちで切磋琢磨するコミュニティを運営しています!
Dj7のリリースに至るまでこのコミュニティで沢山のフィードバックを貰えたのはかなり大きかったです。
Webサービスをこれから作りたいという初心者でも大歓迎なので気軽に参加してみて下さい!
https://discord.gg/hNWjDBd

YouTube同期再生プラットフォーム DJ7 https://www.dj7.io

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